diff --git a/bindings/web5_uniffi/src/lib.rs b/bindings/web5_uniffi/src/lib.rs index 26d98479..d8297f2e 100644 --- a/bindings/web5_uniffi/src/lib.rs +++ b/bindings/web5_uniffi/src/lib.rs @@ -9,6 +9,7 @@ use web5_uniffi_wrapper::{ Signer, Verifier, }, in_memory_key_manager::InMemoryKeyManager, + jwk::Jwk, key_manager::KeyManager, }, dids::{ diff --git a/bindings/web5_uniffi/src/web5.udl b/bindings/web5_uniffi/src/web5.udl index 6337b305..0943619e 100644 --- a/bindings/web5_uniffi/src/web5.udl +++ b/bindings/web5_uniffi/src/web5.udl @@ -22,6 +22,13 @@ dictionary JwkData { string? y; }; +interface Jwk { + constructor(JwkData data); + JwkData get_data(); + [Throws=Web5Error] + string compute_thumbprint(); +}; + [Trait, WithForeign] interface KeyManager { [Throws=Web5Error] diff --git a/bindings/web5_uniffi_wrapper/src/crypto/jwk.rs b/bindings/web5_uniffi_wrapper/src/crypto/jwk.rs new file mode 100644 index 00000000..756b2a1d --- /dev/null +++ b/bindings/web5_uniffi_wrapper/src/crypto/jwk.rs @@ -0,0 +1,19 @@ +use crate::errors::Result; +use web5::crypto::jwk::Jwk as InnerJwk; + +pub struct Jwk(pub InnerJwk); + +impl Jwk { + pub fn new(data: InnerJwk) -> Self { + Self(data) + } + + pub fn get_data(&self) -> InnerJwk { + self.0.clone() + } + + pub fn compute_thumbprint(&self) -> Result { + let thumbprint = self.0.compute_thumbprint()?; + Ok(thumbprint) + } +} diff --git a/bindings/web5_uniffi_wrapper/src/crypto/mod.rs b/bindings/web5_uniffi_wrapper/src/crypto/mod.rs index 70be23b2..88043a9f 100644 --- a/bindings/web5_uniffi_wrapper/src/crypto/mod.rs +++ b/bindings/web5_uniffi_wrapper/src/crypto/mod.rs @@ -1,4 +1,5 @@ pub mod dsa; pub mod in_memory_key_manager; +pub mod jwk; pub mod key_manager; diff --git a/bindings/web5_uniffi_wrapper/src/errors.rs b/bindings/web5_uniffi_wrapper/src/errors.rs index b1581caf..759254d5 100644 --- a/bindings/web5_uniffi_wrapper/src/errors.rs +++ b/bindings/web5_uniffi_wrapper/src/errors.rs @@ -5,7 +5,7 @@ use thiserror::Error; use web5::credentials::presentation_definition::PexError; use web5::credentials::CredentialError; use web5::crypto::dsa::DsaError; -use web5::crypto::{jwk::JwkError, key_managers::KeyManagerError}; +use web5::crypto::key_managers::KeyManagerError; use web5::dids::bearer_did::BearerDidError; use web5::dids::data_model::DataModelError as DidDataModelError; use web5::dids::methods::MethodError; @@ -79,12 +79,6 @@ where variant_name.to_string() } -impl From for Web5Error { - fn from(error: JwkError) -> Self { - Web5Error::new(error) - } -} - impl From for Web5Error { fn from(error: KeyManagerError) -> Self { Web5Error::new(error) @@ -144,13 +138,7 @@ impl From for KeyManagerError { let variant = error.variant(); let msg = error.msg(); - if variant - == variant_name(&KeyManagerError::JwkError(JwkError::ThumbprintFailed( - String::default(), - ))) - { - return KeyManagerError::JwkError(JwkError::ThumbprintFailed(msg.to_string())); - } else if variant == variant_name(&KeyManagerError::KeyGenerationFailed) { + if variant == variant_name(&KeyManagerError::KeyGenerationFailed) { return KeyManagerError::KeyGenerationFailed; } else if variant == variant_name(&KeyManagerError::InternalKeyStoreError(String::default())) diff --git a/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/InMemoryKeyManager.kt b/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/InMemoryKeyManager.kt index f8fd6424..e5b337db 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/InMemoryKeyManager.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/InMemoryKeyManager.kt @@ -1,5 +1,6 @@ package web5.sdk.crypto.keys +import web5.sdk.crypto.signers.ToOuterSigner import web5.sdk.crypto.signers.Signer import web5.sdk.rust.InMemoryKeyManager as RustCoreInMemoryKeyManager @@ -16,7 +17,7 @@ class InMemoryKeyManager : KeyManager { */ constructor(privateJwks: List) { privateJwks.forEach { - this.rustCoreInMemoryKeyManager.importPrivateJwk(it) + this.rustCoreInMemoryKeyManager.importPrivateJwk(it.rustCoreJwkData) } } @@ -27,7 +28,8 @@ class InMemoryKeyManager : KeyManager { * @return Signer The signer for the given public key. */ override fun getSigner(publicJwk: Jwk): Signer { - return this.rustCoreInMemoryKeyManager.getSigner(publicJwk) + val rustCoreSigner = this.rustCoreInMemoryKeyManager.getSigner(publicJwk.rustCoreJwkData) + return ToOuterSigner(rustCoreSigner) } /** @@ -37,6 +39,7 @@ class InMemoryKeyManager : KeyManager { * @return Jwk The public key represented as a JWK. */ fun importPrivateJwk(privateJwk: Jwk): Jwk { - return this.rustCoreInMemoryKeyManager.importPrivateJwk(privateJwk) + val rustCoreJwkData = this.rustCoreInMemoryKeyManager.importPrivateJwk(privateJwk.rustCoreJwkData) + return Jwk.fromRustCoreJwkData(rustCoreJwkData) } } \ No newline at end of file diff --git a/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/Jwk.kt b/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/Jwk.kt index 63325714..bb05d620 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/Jwk.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/Jwk.kt @@ -1,10 +1,44 @@ package web5.sdk.crypto.keys +import web5.sdk.rust.Jwk as RustCoreJwk import web5.sdk.rust.JwkData as RustCoreJwkData /** * Partial representation of a [JSON Web Key as per RFC7517](https://tools.ietf.org/html/rfc7517). * Note that this is a subset of the spec. */ +data class Jwk ( + val alg: String? = null, + val kty: String, + val crv: String, + val x: String, + val y: String? = null, + val d: String? = null +) { + internal val rustCoreJwkData: RustCoreJwkData = RustCoreJwkData( + alg, + kty, + crv, + d, + x, + y + ) -typealias Jwk = RustCoreJwkData \ No newline at end of file + internal companion object { + fun fromRustCoreJwkData(rustCoreJwkData: RustCoreJwkData): Jwk { + return Jwk( + rustCoreJwkData.alg, + rustCoreJwkData.kty, + rustCoreJwkData.crv, + rustCoreJwkData.x, + rustCoreJwkData.y, + rustCoreJwkData.d, + ) + } + } + + fun computeThumbprint(): String { + val rustCoreJwk = RustCoreJwk(rustCoreJwkData) + return rustCoreJwk.computeThumbprint() + } +} diff --git a/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/KeyManager.kt b/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/KeyManager.kt index d8c82a22..2420587b 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/KeyManager.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/crypto/keys/KeyManager.kt @@ -1,6 +1,28 @@ package web5.sdk.crypto.keys +import web5.sdk.crypto.signers.ToOuterSigner +import web5.sdk.crypto.signers.Signer +import web5.sdk.crypto.signers.ToInnerSigner +import web5.sdk.rust.JwkData as RustCoreJwkData import web5.sdk.rust.KeyManager as RustCoreKeyManager +import web5.sdk.rust.Signer as RustCoreSigner -typealias KeyManager = RustCoreKeyManager +interface KeyManager { + fun getSigner(publicJwk: Jwk): Signer +} +internal class ToOuterKeyManager(private val rustCoreKeyManager: RustCoreKeyManager) : KeyManager { + override fun getSigner(publicJwk: Jwk): Signer { + val rustCoreSigner = rustCoreKeyManager.getSigner(publicJwk.rustCoreJwkData) + return ToOuterSigner(rustCoreSigner) + } +} + +internal class ToInnerKeyManager(private val keyManager: KeyManager) : RustCoreKeyManager { + override fun getSigner(publicJwk: RustCoreJwkData): RustCoreSigner { + val jwk = Jwk.fromRustCoreJwkData(publicJwk) + val signer = keyManager.getSigner(jwk) + val innerSigner = ToInnerSigner(signer) + return innerSigner + } +} diff --git a/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Ed25519Signer.kt b/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Ed25519Signer.kt index 48f99639..fbd5a740 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Ed25519Signer.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Ed25519Signer.kt @@ -7,7 +7,7 @@ class Ed25519Signer : Signer { private val rustCoreSigner: RustCoreEd25519Signer constructor(privateKey: Jwk) { - this.rustCoreSigner = RustCoreEd25519Signer(privateKey) + this.rustCoreSigner = RustCoreEd25519Signer(privateKey.rustCoreJwkData) } private constructor(rustCoreSigner: RustCoreEd25519Signer) { diff --git a/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Signer.kt b/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Signer.kt index 413a9129..45f5c3c0 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Signer.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Signer.kt @@ -2,4 +2,18 @@ package web5.sdk.crypto.signers import web5.sdk.rust.Signer as RustCoreSigner -typealias Signer = RustCoreSigner \ No newline at end of file +interface Signer { + fun sign(payload: ByteArray): ByteArray +} + +internal class ToOuterSigner(private val rustCoreSigner: RustCoreSigner) : Signer { + override fun sign(payload: ByteArray): ByteArray { + return rustCoreSigner.sign(payload) + } +} + +internal class ToInnerSigner(private val signer: Signer) : RustCoreSigner { + override fun sign(payload: ByteArray): ByteArray { + return signer.sign(payload) + } +} \ No newline at end of file diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/BearerDid.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/BearerDid.kt index 80ea877b..d844134d 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/BearerDid.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/BearerDid.kt @@ -2,6 +2,9 @@ package web5.sdk.dids import web5.sdk.crypto.signers.Signer import web5.sdk.crypto.keys.KeyManager +import web5.sdk.crypto.keys.ToInnerKeyManager +import web5.sdk.crypto.keys.ToOuterKeyManager +import web5.sdk.crypto.signers.ToOuterSigner import web5.sdk.rust.BearerDid as RustCoreBearerDid /** @@ -25,7 +28,8 @@ class BearerDid { * @param keyManager The key manager to handle keys. */ constructor(uri: String, keyManager: KeyManager) { - this.rustCoreBearerDid = RustCoreBearerDid(uri, keyManager) + val innerKeyManager = ToInnerKeyManager(keyManager) + this.rustCoreBearerDid = RustCoreBearerDid(uri, innerKeyManager) this.did = Did.fromRustCoreDidData(this.rustCoreBearerDid.getData().did) this.document = this.rustCoreBearerDid.getData().document @@ -43,7 +47,7 @@ class BearerDid { val data = this.rustCoreBearerDid.getData() this.did = Did.fromRustCoreDidData(data.did) this.document = data.document - this.keyManager = data.keyManager + this.keyManager = ToOuterKeyManager(data.keyManager) } /** @@ -53,6 +57,6 @@ class BearerDid { */ fun getSigner(): Signer { val keyId = this.document.verificationMethod.first().id - return this.rustCoreBearerDid.getSigner(keyId) + return ToOuterSigner(this.rustCoreBearerDid.getSigner(keyId)) } } diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/Did.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/Did.kt index 15d03577..1835ad21 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/Did.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/Did.kt @@ -20,7 +20,7 @@ data class Did ( fun parse(uri: String): Did { val rustCoreDid = RustCoreDid(uri) val data = rustCoreDid.getData() - return Did.fromRustCoreDidData(data) + return fromRustCoreDidData(data) } internal fun fromRustCoreDidData(data: RustCoreDidData): Did { diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/PortableDid.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/PortableDid.kt index 245d15e9..7494064d 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/PortableDid.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/PortableDid.kt @@ -20,6 +20,6 @@ class PortableDid { this.didUri = rustCorePortableDid.getData().didUri this.document = rustCorePortableDid.getData().document - this.privateKeys = rustCorePortableDid.getData().privateJwks + this.privateKeys = rustCorePortableDid.getData().privateJwks.map {Jwk.fromRustCoreJwkData(it) } } } \ No newline at end of file diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt index af30cae1..376ba727 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/dht/DidDht.kt @@ -27,7 +27,7 @@ class DidDht { * @param identityKey The identity key represented as a Jwk. */ constructor(identityKey: Jwk) { - rustCoreDidDht = RustCoreDidDht.fromIdentityKey(identityKey) + rustCoreDidDht = RustCoreDidDht.fromIdentityKey(identityKey.rustCoreJwkData) this.did = Did.fromRustCoreDidData(rustCoreDidDht.getData().did) this.document = rustCoreDidDht.getData().document diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt index d9292760..ec4c16d0 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/jwk/DidJwk.kt @@ -23,7 +23,7 @@ class DidJwk { * @param publicKey The public key represented as a Jwk. */ constructor(publicKey: Jwk) { - val rustCoreDidJwk = RustCoreDidJwk.fromPublicJwk(publicKey) + val rustCoreDidJwk = RustCoreDidJwk.fromPublicJwk(publicKey.rustCoreJwkData) this.did = Did.fromRustCoreDidData(rustCoreDidJwk.getData().did) this.document = rustCoreDidJwk.getData().document diff --git a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt index 6a86c5d2..c5db49c9 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/methods/web/DidWeb.kt @@ -39,7 +39,7 @@ class DidWeb { */ constructor(domain: String, publicKey: Jwk) { val rustCoreDidWeb = runBlocking { - RustCoreDidWeb.fromPublicJwk(domain, publicKey); + RustCoreDidWeb.fromPublicJwk(domain, publicKey.rustCoreJwkData); } this.did = Did.fromRustCoreDidData(rustCoreDidWeb.getData().did) diff --git a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt index ad87c9e1..a1ad115a 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt @@ -869,6 +869,14 @@ internal open class UniffiVTableCallbackInterfaceVerifier( + + + + + + + + @@ -992,6 +1000,16 @@ internal interface UniffiLib : Library { ): Pointer fun uniffi_web5_uniffi_fn_method_inmemorykeymanager_import_private_jwk(`ptr`: Pointer,`privateKey`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue + fun uniffi_web5_uniffi_fn_clone_jwk(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_web5_uniffi_fn_free_jwk(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_web5_uniffi_fn_constructor_jwk_new(`data`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_web5_uniffi_fn_method_jwk_compute_thumbprint(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_web5_uniffi_fn_method_jwk_get_data(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue fun uniffi_web5_uniffi_fn_clone_keymanager(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, ): Pointer fun uniffi_web5_uniffi_fn_free_keymanager(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, @@ -1208,6 +1226,10 @@ internal interface UniffiLib : Library { ): Short fun uniffi_web5_uniffi_checksum_method_inmemorykeymanager_import_private_jwk( ): Short + fun uniffi_web5_uniffi_checksum_method_jwk_compute_thumbprint( + ): Short + fun uniffi_web5_uniffi_checksum_method_jwk_get_data( + ): Short fun uniffi_web5_uniffi_checksum_method_keymanager_get_signer( ): Short fun uniffi_web5_uniffi_checksum_method_portabledid_get_data( @@ -1250,6 +1272,8 @@ internal interface UniffiLib : Library { ): Short fun uniffi_web5_uniffi_checksum_constructor_inmemorykeymanager_new( ): Short + fun uniffi_web5_uniffi_checksum_constructor_jwk_new( + ): Short fun uniffi_web5_uniffi_checksum_constructor_portabledid_new( ): Short fun uniffi_web5_uniffi_checksum_constructor_presentationdefinition_new( @@ -1332,6 +1356,12 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_method_inmemorykeymanager_import_private_jwk() != 54213.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_web5_uniffi_checksum_method_jwk_compute_thumbprint() != 15254.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_web5_uniffi_checksum_method_jwk_get_data() != 12450.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_web5_uniffi_checksum_method_keymanager_get_signer() != 27148.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -1395,6 +1425,9 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_constructor_inmemorykeymanager_new() != 16598.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_web5_uniffi_checksum_constructor_jwk_new() != 59888.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_web5_uniffi_checksum_constructor_portabledid_new() != 37852.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -4081,6 +4114,260 @@ public object FfiConverterTypeInMemoryKeyManager: FfiConverter + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_constructor_jwk_new( + FfiConverterTypeJwkData.lower(`data`),_status) +} + ) + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_free_jwk(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_clone_jwk(pointer!!, status) + } + } + + + @Throws(Web5Exception::class)override fun `computeThumbprint`(): kotlin.String { + return FfiConverterString.lift( + callWithPointer { + uniffiRustCallWithError(Web5Exception) { _status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_method_jwk_compute_thumbprint( + it, _status) +} + } + ) + } + + + override fun `getData`(): JwkData { + return FfiConverterTypeJwkData.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_method_jwk_get_data( + it, _status) +} + } + ) + } + + + + + + + companion object + +} + +public object FfiConverterTypeJwk: FfiConverter { + + override fun lower(value: Jwk): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): Jwk { + return Jwk(value) + } + + override fun read(buf: ByteBuffer): Jwk { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: Jwk) = 8UL + + override fun write(value: Jwk, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + public interface KeyManager { fun `getSigner`(`publicJwk`: JwkData): Signer diff --git a/bound/kt/src/test/kotlin/web5/sdk/crypto/keys/InMemoryKeyManagerTest.kt b/bound/kt/src/test/kotlin/web5/sdk/crypto/keys/InMemoryKeyManagerTest.kt index 7176aef0..7c13c94c 100644 --- a/bound/kt/src/test/kotlin/web5/sdk/crypto/keys/InMemoryKeyManagerTest.kt +++ b/bound/kt/src/test/kotlin/web5/sdk/crypto/keys/InMemoryKeyManagerTest.kt @@ -14,9 +14,9 @@ class InMemoryKeyManagerTest { fun `test key manager`() { val privateJwk = rustCoreEd25519GeneratorGenerate() - val keyManager = InMemoryKeyManager(listOf(privateJwk)) + val keyManager = InMemoryKeyManager(listOf(Jwk.fromRustCoreJwkData(privateJwk))) - val signer = keyManager.getSigner(privateJwk) + val signer = keyManager.getSigner(Jwk.fromRustCoreJwkData(privateJwk)) val payload = signer.sign("abc".toByteArray()) assertNotNull(payload) @@ -36,10 +36,10 @@ class InMemoryKeyManagerTest { val privateJwk = rustCoreEd25519GeneratorGenerate() val keyManager = InMemoryKeyManager(listOf()) - keyManager.importPrivateJwk(privateJwk) + keyManager.importPrivateJwk(Jwk.fromRustCoreJwkData(privateJwk)) privateJwk.d = null - val signer = keyManager.getSigner(privateJwk) + val signer = keyManager.getSigner(Jwk.fromRustCoreJwkData(privateJwk)) val payload = signer.sign("abc".toByteArray()) assertNotNull(payload) diff --git a/bound/kt/src/test/kotlin/web5/sdk/crypto/keys/JwkTest.kt b/bound/kt/src/test/kotlin/web5/sdk/crypto/keys/JwkTest.kt new file mode 100644 index 00000000..d7a8b624 --- /dev/null +++ b/bound/kt/src/test/kotlin/web5/sdk/crypto/keys/JwkTest.kt @@ -0,0 +1,148 @@ +package web5.sdk.crypto.keys + +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.fail +import web5.sdk.UnitTestSuite +import web5.sdk.rust.Web5Exception + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class JwkTest { + + private val testSuite = UnitTestSuite("jwk_compute_thumbprint") + + @AfterAll + fun verifyAllTestsIncluded() { + if (testSuite.tests.isNotEmpty()) { + println("The following tests were not included or executed:") + testSuite.tests.forEach { println(it) } + fail("Not all tests were executed! ${this.testSuite.tests}") + } + } + + @Test + fun test_ec_valid() { + this.testSuite.include() + val jwk = Jwk( + kty = "EC", + crv = "secp256k1", + x = "x_value", + y = "y_value" + ) + + val thumbprint = jwk.computeThumbprint() + assertEquals("yiiszVT5Lwt6760MW19cHaJ61qJKIfe20sUW8dNxBv4", thumbprint) + } + + @Test + fun test_okp_valid() { + this.testSuite.include() + val jwk = Jwk( + kty = "OKP", + crv = "Ed25519", + x = "x_value" + ) + + val thumbprint = jwk.computeThumbprint() + assertEquals("nDMRVZm4lpedGjuJGO4y3YVJJ0krDF0aSz4KhlncDdI", thumbprint) + } + + @Test + fun test_unsupported_kty() { + this.testSuite.include() + val jwk = Jwk( + kty = "RSA", + crv = "RS256", + x = "x_value", + y = "y_value" + ) + + val exception = assertThrows { + jwk.computeThumbprint() + } + + assertEquals("data member error kty not supported RSA", exception.msg) + } + + @Test + fun test_empty_kty() { + this.testSuite.include() + val jwk = Jwk( + kty = "", + crv = "Ed25519", + x = "x_value" + ) + + val exception = assertThrows { + jwk.computeThumbprint() + } + + assertEquals("data member error kty cannot be empty", exception.msg) + } + + @Test + fun test_empty_x() { + this.testSuite.include() + val jwk = Jwk( + kty = "OKP", + crv = "Ed25519", + x = "" + ) + + val exception = assertThrows { + jwk.computeThumbprint() + } + + assertEquals("data member error x cannot be empty", exception.msg) + } + + @Test + fun test_empty_crv() { + this.testSuite.include() + val jwk = Jwk( + kty = "EC", + crv = "", + x = "x_value", + y = "y_value" + ) + + val exception = assertThrows { + jwk.computeThumbprint() + } + + assertEquals("data member error crv cannot be empty", exception.msg) + } + + @Test + fun test_ec_missing_y() { + this.testSuite.include() + val jwk = Jwk( + kty = "EC", + crv = "P-256", + x = "x_value" + ) + + val exception = assertThrows { + jwk.computeThumbprint() + } + + assertEquals("data member error missing y", exception.msg) + } + + @Test + fun test_ec_empty_y() { + this.testSuite.include() + val jwk = Jwk( + kty = "EC", + crv = "P-256", + x = "x_value", + y = "" + ) + + val exception = assertThrows { + jwk.computeThumbprint() + } + + assertEquals("data member error y cannot be empty", exception.msg) + } +} diff --git a/bound/kt/src/test/kotlin/web5/sdk/crypto/signers/Ed25519SignerTest.kt b/bound/kt/src/test/kotlin/web5/sdk/crypto/signers/Ed25519SignerTest.kt index 07971b1c..54dfb223 100644 --- a/bound/kt/src/test/kotlin/web5/sdk/crypto/signers/Ed25519SignerTest.kt +++ b/bound/kt/src/test/kotlin/web5/sdk/crypto/signers/Ed25519SignerTest.kt @@ -3,6 +3,7 @@ package web5.sdk.crypto.signers import org.junit.jupiter.api.Test import web5.sdk.crypto.keys.InMemoryKeyManager import org.junit.jupiter.api.Assertions.assertNotNull +import web5.sdk.crypto.keys.Jwk import web5.sdk.rust.ed25519GeneratorGenerate as rustCoreEd25519GeneratorGenerate @@ -11,7 +12,7 @@ class Ed25519SignerTest { @Test fun `test signer`() { val rustCorePrivateJwk = rustCoreEd25519GeneratorGenerate() - val ed25519Signer = Ed25519Signer(rustCorePrivateJwk) + val ed25519Signer = Ed25519Signer(Jwk.fromRustCoreJwkData(rustCorePrivateJwk)) val payload = ed25519Signer.sign("abc".toByteArray()) @@ -23,7 +24,7 @@ class Ed25519SignerTest { val privateJwk = rustCoreEd25519GeneratorGenerate() val keyManager = InMemoryKeyManager(listOf()) - val publicJwk = keyManager.importPrivateJwk(privateJwk) + val publicJwk = keyManager.importPrivateJwk(Jwk.fromRustCoreJwkData(privateJwk)) val ed25519Signer = keyManager.getSigner(publicJwk) val payload = ed25519Signer.sign("abc".toByteArray()) diff --git a/bound/kt/src/test/kotlin/web5/sdk/dids/BearerDidTest.kt b/bound/kt/src/test/kotlin/web5/sdk/dids/BearerDidTest.kt index 4e21cd70..423b778f 100644 --- a/bound/kt/src/test/kotlin/web5/sdk/dids/BearerDidTest.kt +++ b/bound/kt/src/test/kotlin/web5/sdk/dids/BearerDidTest.kt @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test import web5.sdk.crypto.keys.InMemoryKeyManager +import web5.sdk.crypto.keys.Jwk import web5.sdk.dids.methods.jwk.DidJwk import web5.sdk.rust.ed25519GeneratorGenerate @@ -14,7 +15,7 @@ class BearerDidTest { val privateJwk = ed25519GeneratorGenerate() val keyManager = InMemoryKeyManager(listOf()) - val publicJwk = keyManager.importPrivateJwk(privateJwk) + val publicJwk = keyManager.importPrivateJwk(Jwk.fromRustCoreJwkData(privateJwk)) val didJwk = DidJwk(publicJwk) @@ -28,7 +29,7 @@ class BearerDidTest { val privateJwk = ed25519GeneratorGenerate() val keyManager = InMemoryKeyManager(listOf()) - val publicJwk = keyManager.importPrivateJwk(privateJwk) + val publicJwk = keyManager.importPrivateJwk(Jwk.fromRustCoreJwkData(privateJwk)) val didJwk = DidJwk(publicJwk) diff --git a/bound/kt/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTests.kt b/bound/kt/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTests.kt index 081e88d4..83f6f11f 100644 --- a/bound/kt/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTests.kt +++ b/bound/kt/src/test/kotlin/web5/sdk/dids/methods/dht/DidDhtTests.kt @@ -2,6 +2,7 @@ package web5.sdk.dids.methods.dht import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test +import web5.sdk.crypto.keys.Jwk import web5.sdk.rust.ed25519GeneratorGenerate as rustCoreEd25519GeneratorGenerate @@ -10,7 +11,7 @@ class DidDhtTests { fun `can create did dht`() { val jwk = rustCoreEd25519GeneratorGenerate() - val didDht = DidDht(jwk) + val didDht = DidDht(Jwk.fromRustCoreJwkData(jwk)) assertNotNull(didDht.document.id) } diff --git a/bound/kt/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTests.kt b/bound/kt/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTests.kt index f1ec04a3..86191aeb 100644 --- a/bound/kt/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTests.kt +++ b/bound/kt/src/test/kotlin/web5/sdk/dids/methods/jwk/DidJwkTests.kt @@ -2,6 +2,7 @@ package web5.sdk.dids.methods.jwk import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test +import web5.sdk.crypto.keys.Jwk import web5.sdk.rust.DidJwk as RustCoreDidJwk @@ -12,7 +13,7 @@ class DidJwkTests { fun `can create did jwk same as rust core`() { val jwk = rustCoreEd25519GeneratorGenerate() - val didJwk = DidJwk(jwk) + val didJwk = DidJwk(Jwk.fromRustCoreJwkData(jwk)) val rustCoreDidJwk = RustCoreDidJwk.fromPublicJwk(jwk); assertEquals(rustCoreDidJwk.getData().did.uri, didJwk.did.uri) diff --git a/bound/kt/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTests.kt b/bound/kt/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTests.kt index e56cdcc3..0167097b 100644 --- a/bound/kt/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTests.kt +++ b/bound/kt/src/test/kotlin/web5/sdk/dids/methods/web/DidWebTests.kt @@ -2,6 +2,7 @@ package web5.sdk.dids.methods.web import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test +import web5.sdk.crypto.keys.Jwk import web5.sdk.rust.ed25519GeneratorGenerate class DidWebTests { @@ -11,7 +12,7 @@ class DidWebTests { val domain = "example.com" val jwk = ed25519GeneratorGenerate(); - val didWeb = DidWeb(domain, jwk) + val didWeb = DidWeb(domain, Jwk.fromRustCoreJwkData(jwk)) assertEquals(didWeb.did.uri, "did:web:example.com") assertEquals(didWeb.document.verificationMethod.get(0).publicKeyJwk, jwk) } diff --git a/crates/web5/src/crypto/jwk.rs b/crates/web5/src/crypto/jwk.rs index 50d1be28..2a4751f1 100644 --- a/crates/web5/src/crypto/jwk.rs +++ b/crates/web5/src/crypto/jwk.rs @@ -1,3 +1,4 @@ +use crate::errors::{Result, Web5Error}; use base64::{engine::general_purpose, Engine}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -16,37 +17,43 @@ pub struct Jwk { } impl Jwk { - pub fn is_private_key(&self) -> bool { - self.d.is_some() - } - - pub fn is_public_key(&self) -> bool { + pub(crate) fn is_public_key(&self) -> bool { self.d.is_none() } } -#[derive(thiserror::Error, Debug, Clone, PartialEq)] -pub enum JwkError { - #[error("thumbprint computation failed {0}")] - ThumbprintFailed(String), -} - -type Result = std::result::Result; - impl Jwk { pub fn compute_thumbprint(&self) -> Result { + if self.kty.is_empty() { + return Err(Web5Error::DataMember("kty cannot be empty".to_string())); + } + + if self.x.is_empty() { + return Err(Web5Error::DataMember("x cannot be empty".to_string())); + } + + if self.crv.is_empty() { + return Err(Web5Error::DataMember("crv cannot be empty".to_string())); + } + let thumbprint_json_string = match self.kty.as_str() { - "EC" => format!( - r#"{{"crv":"{}","kty":"EC","x":"{}","y":"{}"}}"#, - self.crv, - self.x, - self.y + "EC" => { + let y = self + .y .as_ref() - .ok_or(JwkError::ThumbprintFailed("missing y".to_string()))?, - ), - "OKP" => format!(r#"{{"crv":"{}","kty":"OKP","x":"{}"}}"#, self.crv, self.x,), + .ok_or(Web5Error::DataMember("missing y".to_string()))?; + if y.is_empty() { + return Err(Web5Error::DataMember("y cannot be empty".to_string())); + } + + format!( + r#"{{"crv":"{}","kty":"EC","x":"{}","y":"{}"}}"#, + self.crv, self.x, y, + ) + } + "OKP" => format!(r#"{{"crv":"{}","kty":"OKP","x":"{}"}}"#, self.crv, self.x), _ => { - return Err(JwkError::ThumbprintFailed(format!( + return Err(Web5Error::DataMember(format!( "kty not supported {0}", self.kty ))) @@ -61,3 +68,159 @@ impl Jwk { Ok(thumbprint) } } + +#[cfg(test)] +mod tests { + use super::*; + + mod compute_thumbprint { + use super::*; + use crate::{errors::Web5Error, test_helpers::UnitTestSuite, test_name}; + use std::sync::LazyLock; + + static TEST_SUITE: LazyLock = + LazyLock::new(|| UnitTestSuite::new("jwk_compute_thumbprint")); + + #[test] + fn z_assert_all_suite_cases_covered() { + // fn name prefixed with `z_*` b/c rust test harness executes in alphabetical order, + // unless intentionally executed with "shuffle" https://doc.rust-lang.org/rustc/tests/index.html#--shuffle + // this may not work if shuffled or if test list grows to the extent of 100ms being insufficient wait time + + // wait 100ms to be last-in-queue of mutex lock + std::thread::sleep(std::time::Duration::from_millis(100)); + + TEST_SUITE.assert_coverage() + } + + #[test] + fn test_ec_valid() { + TEST_SUITE.include(test_name!()); + + let jwk = Jwk { + kty: "EC".to_string(), + crv: "secp256k1".to_string(), + x: "x_value".to_string(), + y: Some("y_value".to_string()), + ..Default::default() + }; + + let thumbprint = jwk.compute_thumbprint().unwrap(); + assert_eq!(thumbprint, "yiiszVT5Lwt6760MW19cHaJ61qJKIfe20sUW8dNxBv4"); + } + + #[test] + fn test_okp_valid() { + TEST_SUITE.include(test_name!()); + + let jwk = Jwk { + kty: "OKP".to_string(), + crv: "Ed25519".to_string(), + x: "x_value".to_string(), + ..Default::default() + }; + + let thumbprint = jwk.compute_thumbprint().unwrap(); + assert_eq!(thumbprint, "nDMRVZm4lpedGjuJGO4y3YVJJ0krDF0aSz4KhlncDdI"); + } + + #[test] + fn test_unsupported_kty() { + TEST_SUITE.include(test_name!()); + + let jwk = Jwk { + kty: "RSA".to_string(), + crv: "RS256".to_string(), + x: "x_value".to_string(), + y: Some("y_value".to_string()), + ..Default::default() + }; + + let err = jwk.compute_thumbprint().unwrap_err(); + assert!(matches!(err, Web5Error::DataMember(_))); + assert_eq!(err.to_string(), "data member error kty not supported RSA"); + } + + #[test] + fn test_empty_kty() { + TEST_SUITE.include(test_name!()); + + let jwk = Jwk { + kty: "".to_string(), + crv: "Ed25519".to_string(), + x: "x_value".to_string(), + ..Default::default() + }; + + let err = jwk.compute_thumbprint().unwrap_err(); + assert!(matches!(err, Web5Error::DataMember(_))); + assert_eq!(err.to_string(), "data member error kty cannot be empty"); + } + + #[test] + fn test_empty_x() { + TEST_SUITE.include(test_name!()); + + let jwk = Jwk { + kty: "OKP".to_string(), + crv: "Ed25519".to_string(), + x: "".to_string(), + ..Default::default() + }; + + let err = jwk.compute_thumbprint().unwrap_err(); + assert!(matches!(err, Web5Error::DataMember(_))); + assert_eq!(err.to_string(), "data member error x cannot be empty"); + } + + #[test] + fn test_empty_crv() { + TEST_SUITE.include(test_name!()); + + let jwk = Jwk { + kty: "EC".to_string(), + crv: "".to_string(), + x: "x_value".to_string(), + y: Some("y_value".to_string()), + ..Default::default() + }; + + let err = jwk.compute_thumbprint().unwrap_err(); + assert!(matches!(err, Web5Error::DataMember(_))); + assert_eq!(err.to_string(), "data member error crv cannot be empty"); + } + + #[test] + fn test_ec_missing_y() { + TEST_SUITE.include(test_name!()); + + let jwk = Jwk { + kty: "EC".to_string(), + crv: "P-256".to_string(), + x: "x_value".to_string(), + ..Default::default() + }; + + let err = jwk.compute_thumbprint().unwrap_err(); + assert!(matches!(err, Web5Error::DataMember(_))); + assert_eq!(err.to_string(), "data member error missing y"); + } + + #[test] + fn test_ec_empty_y() { + TEST_SUITE.include(test_name!()); + + let jwk = Jwk { + kty: "EC".to_string(), + crv: "P-256".to_string(), + x: "x_value".to_string(), + y: Some("".to_string()), + ..Default::default() + }; + + let err = jwk.compute_thumbprint().unwrap_err(); + assert!(matches!(err, Web5Error::DataMember(_))); + assert_eq!(err.to_string(), "data member error y cannot be empty"); + } + } +} diff --git a/crates/web5/src/crypto/key_managers/mod.rs b/crates/web5/src/crypto/key_managers/mod.rs index 2ebd07cb..2a5fbcdf 100644 --- a/crates/web5/src/crypto/key_managers/mod.rs +++ b/crates/web5/src/crypto/key_managers/mod.rs @@ -1,12 +1,12 @@ +use crate::errors::Web5Error; + pub mod in_memory_key_manager; pub mod key_manager; -use crate::crypto::jwk::JwkError; - #[derive(thiserror::Error, Debug, Clone, PartialEq)] pub enum KeyManagerError { #[error(transparent)] - JwkError(#[from] JwkError), + Web5Error(#[from] Web5Error), #[error("Key generation failed")] KeyGenerationFailed, #[error("{0}")] diff --git a/crates/web5/src/dids/methods/did_dht/document_packet/mod.rs b/crates/web5/src/dids/methods/did_dht/document_packet/mod.rs index b59a737b..8276acba 100644 --- a/crates/web5/src/dids/methods/did_dht/document_packet/mod.rs +++ b/crates/web5/src/dids/methods/did_dht/document_packet/mod.rs @@ -1,6 +1,7 @@ -use crate::crypto::{dsa::DsaError, jwk::JwkError}; +use crate::crypto::dsa::DsaError; use crate::dids::data_model::document::Document; use crate::dids::data_model::{service::Service, verification_method::VerificationMethod}; +use crate::errors::Web5Error; use simple_dns::SimpleDnsError; use std::collections::HashMap; @@ -52,7 +53,7 @@ pub enum DocumentPacketError { #[error(transparent)] Dns(#[from] SimpleDnsError), #[error(transparent)] - JwkError(#[from] JwkError), + Web5Error(#[from] Web5Error), #[error("DNS packet was malformed: {0}")] RootRecord(String), #[error("Could not convert between publicKeyJwk and resource record: {0}")] diff --git a/crates/web5/src/errors.rs b/crates/web5/src/errors.rs index 59d31ec4..9537e3e4 100644 --- a/crates/web5/src/errors.rs +++ b/crates/web5/src/errors.rs @@ -6,6 +6,8 @@ pub enum Web5Error { Json(String), #[error("parameter error {0}")] Parameter(String), + #[error("data member error {0}")] + DataMember(String), } impl From for Web5Error { diff --git a/tests/unit_test_cases/jwk_compute_thumbprint.json b/tests/unit_test_cases/jwk_compute_thumbprint.json new file mode 100644 index 00000000..2e7735a5 --- /dev/null +++ b/tests/unit_test_cases/jwk_compute_thumbprint.json @@ -0,0 +1,10 @@ +[ + "test_ec_valid", + "test_okp_valid", + "test_unsupported_kty", + "test_empty_kty", + "test_empty_x", + "test_empty_crv", + "test_ec_missing_y", + "test_ec_empty_y" +]