From d0740ae569619c41d62152e3151d39cace15fe26 Mon Sep 17 00:00:00 2001 From: Kendall Weihe Date: Fri, 23 Aug 2024 17:07:13 -0400 Subject: [PATCH] Add DidDht create, publish and resolve, add unit tests (#308) --- bindings/web5_uniffi/src/lib.rs | 4 +- bindings/web5_uniffi/src/web5.udl | 32 +- .../src/dids/methods/did_dht.rs | 54 ++ .../src/dids/methods/did_dht/mod.rs | 40 - bindings/web5_uniffi_wrapper/src/errors.rs | 7 - .../main/kotlin/web5/sdk/dids/BearerDid.kt | 2 +- .../web5/sdk/dids/methods/dht/DidDht.kt | 120 +-- .../web5/sdk/dids/methods/web/DidWeb.kt | 1 - .../src/main/kotlin/web5/sdk/rust/UniFFI.kt | 570 ++++++-------- .../web5/sdk/dids/methods/dht/DidDhtTests.kt | 414 +++++++++- crates/web5/src/dids/methods/did_dht/mod.rs | 725 +++++++++++++----- crates/web5/src/dids/methods/did_web/mod.rs | 8 +- crates/web5/src/dids/methods/mod.rs | 30 - .../src/dids/resolution/resolution_result.rs | 2 +- crates/web5/src/errors.rs | 2 + crates/web5/src/test_vectors.rs | 2 +- crates/web5_cli/src/dids/create.rs | 35 +- docs/API_DESIGN.md | 79 +- tests/unit_test_cases/did_dht_create.json | 8 + tests/unit_test_cases/did_dht_publish.json | 4 + tests/unit_test_cases/did_dht_resolve.json | 7 + 21 files changed, 1358 insertions(+), 788 deletions(-) create mode 100644 bindings/web5_uniffi_wrapper/src/dids/methods/did_dht.rs delete mode 100644 bindings/web5_uniffi_wrapper/src/dids/methods/did_dht/mod.rs create mode 100644 tests/unit_test_cases/did_dht_create.json create mode 100644 tests/unit_test_cases/did_dht_publish.json create mode 100644 tests/unit_test_cases/did_dht_resolve.json diff --git a/bindings/web5_uniffi/src/lib.rs b/bindings/web5_uniffi/src/lib.rs index 1a9fe786..9b7fcb0a 100644 --- a/bindings/web5_uniffi/src/lib.rs +++ b/bindings/web5_uniffi/src/lib.rs @@ -17,7 +17,7 @@ use web5_uniffi_wrapper::{ data_model::document::Document, did::Did, methods::{ - did_dht::{did_dht_resolve, DidDht}, + did_dht::{did_dht_create, did_dht_publish, did_dht_resolve, DidDhtCreateOptions}, did_jwk::{did_jwk_create, did_jwk_resolve, DidJwkCreateOptions}, did_web::{did_web_create, did_web_resolve, DidWebCreateOptions}, }, @@ -36,7 +36,7 @@ use web5::{ verification_method::VerificationMethod as VerificationMethodData, }, did::Did as DidData, - methods::did_dht::DidDht as DidDhtData, + methods::did_dht::{DidDhtPublishOptions, DidDhtResolveOptions}, portable_did::PortableDid as PortableDidData, resolution::{ document_metadata::DocumentMetadata as DocumentMetadataData, diff --git a/bindings/web5_uniffi/src/web5.udl b/bindings/web5_uniffi/src/web5.udl index 0532138f..2a8d52e9 100644 --- a/bindings/web5_uniffi/src/web5.udl +++ b/bindings/web5_uniffi/src/web5.udl @@ -10,7 +10,10 @@ namespace web5 { ResolutionResult did_web_resolve([ByRef] string uri); [Throws=Web5Error] - ResolutionResult did_dht_resolve([ByRef] string uri); + BearerDid did_dht_create(DidDhtCreateOptions? options); + [Throws=Web5Error] + void did_dht_publish(BearerDid bearer_did, DidDhtPublishOptions? options); + ResolutionResult did_dht_resolve([ByRef] string uri, DidDhtResolveOptions? options); }; [Error] @@ -180,21 +183,22 @@ dictionary DidWebCreateOptions { sequence? verification_method; }; -dictionary DidDhtData { - DidData did; - DocumentData document; +dictionary DidDhtCreateOptions { + boolean? publish; + string? gateway_url; + KeyManager? key_manager; + sequence? service; + sequence? controller; + sequence? also_known_as; + sequence? verification_method; }; -interface DidDht { - [Name=from_identity_key, Throws=Web5Error] - constructor(JwkData identity_key); - [Name=from_uri, Throws=Web5Error] - constructor([ByRef] string uri); - [Throws=Web5Error] - void publish(Signer signer); - [Throws=Web5Error] - void deactivate(Signer signer); - DidDhtData get_data(); +dictionary DidDhtResolveOptions { + string? gateway_url; +}; + +dictionary DidDhtPublishOptions { + string? gateway_url; }; dictionary PortableDidData { diff --git a/bindings/web5_uniffi_wrapper/src/dids/methods/did_dht.rs b/bindings/web5_uniffi_wrapper/src/dids/methods/did_dht.rs new file mode 100644 index 00000000..2169497d --- /dev/null +++ b/bindings/web5_uniffi_wrapper/src/dids/methods/did_dht.rs @@ -0,0 +1,54 @@ +use crate::{ + crypto::key_manager::{KeyManager, ToInnerKeyManager}, + dids::{bearer_did::BearerDid, resolution::resolution_result::ResolutionResult}, + errors::Result, +}; +use std::sync::Arc; +use web5::dids::{ + data_model::{service::Service, verification_method::VerificationMethod}, + methods::did_dht::{ + DidDht as InnerDidDht, DidDhtCreateOptions as InnerDidDhtCreateOptions, + DidDhtPublishOptions, DidDhtResolveOptions, + }, +}; + +pub fn did_dht_resolve(uri: &str, options: Option) -> Arc { + let resolution_result = InnerDidDht::resolve(uri, options); + Arc::new(ResolutionResult(resolution_result)) +} + +#[derive(Default)] +pub struct DidDhtCreateOptions { + pub publish: Option, + pub gateway_url: Option, + pub key_manager: Option>, + pub service: Option>, + pub controller: Option>, + pub also_known_as: Option>, + pub verification_method: Option>, +} + +pub fn did_dht_create(options: Option) -> Result> { + let inner_options = options.map(|o| InnerDidDhtCreateOptions { + publish: o.publish, + gateway_url: o.gateway_url, + key_manager: match o.key_manager { + None => None, + Some(km) => Some(Arc::new(ToInnerKeyManager(km))), + }, + service: o.service, + controller: o.controller, + also_known_as: o.also_known_as, + verification_method: o.verification_method, + }); + + let inner_bearer_did = InnerDidDht::create(inner_options)?; + Ok(Arc::new(BearerDid(inner_bearer_did))) +} + +pub fn did_dht_publish( + bearer_did: Arc, + options: Option, +) -> Result<()> { + Ok(InnerDidDht::publish(bearer_did.0.clone(), options)?) +} diff --git a/bindings/web5_uniffi_wrapper/src/dids/methods/did_dht/mod.rs b/bindings/web5_uniffi_wrapper/src/dids/methods/did_dht/mod.rs deleted file mode 100644 index 83ddb476..00000000 --- a/bindings/web5_uniffi_wrapper/src/dids/methods/did_dht/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::{ - crypto::dsa::{Signer, ToInnerSigner}, - dids::resolution::resolution_result::ResolutionResult, - errors::Result, -}; -use std::sync::Arc; -use web5::{crypto::jwk::Jwk, dids::methods::did_dht::DidDht as InnerDidDht}; - -pub struct DidDht(pub InnerDidDht); - -pub fn did_dht_resolve(uri: &str) -> Result> { - let resolution_result = InnerDidDht::resolve(uri); - Ok(Arc::new(ResolutionResult(resolution_result))) -} - -impl DidDht { - pub fn from_identity_key(public_key: Jwk) -> Result { - let did_dht = InnerDidDht::from_identity_key(public_key)?; - Ok(Self(did_dht)) - } - - pub fn from_uri(uri: &str) -> Result { - let did_dht = InnerDidDht::from_uri(uri)?; - Ok(Self(did_dht)) - } - - pub fn publish(&self, signer: Arc) -> Result<()> { - let inner_signer = Arc::new(ToInnerSigner(signer)); - Ok(self.0.publish(inner_signer)?) - } - - pub fn deactivate(&self, signer: Arc) -> Result<()> { - let inner_signer = Arc::new(ToInnerSigner(signer)); - Ok(self.0.deactivate(inner_signer)?) - } - - pub fn get_data(&self) -> InnerDidDht { - self.0.clone() - } -} diff --git a/bindings/web5_uniffi_wrapper/src/errors.rs b/bindings/web5_uniffi_wrapper/src/errors.rs index 6f63e6d8..3901a686 100644 --- a/bindings/web5_uniffi_wrapper/src/errors.rs +++ b/bindings/web5_uniffi_wrapper/src/errors.rs @@ -5,7 +5,6 @@ use thiserror::Error; use web5::credentials::presentation_definition::PexError; use web5::credentials::CredentialError; use web5::dids::bearer_did::BearerDidError; -use web5::dids::methods::MethodError; use web5::errors::Web5Error as InnerWeb5Error; #[derive(Debug, Error)] @@ -75,12 +74,6 @@ where variant_name.to_string() } -impl From for Web5Error { - fn from(error: MethodError) -> Self { - Web5Error::new(error) - } -} - impl From for Web5Error { fn from(error: CredentialError) -> Self { Web5Error::new(error) 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 47324f5d..50d21b53 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/dids/BearerDid.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/dids/BearerDid.kt @@ -19,7 +19,7 @@ class BearerDid { val document: Document val keyManager: KeyManager - private val rustCoreBearerDid: RustCoreBearerDid + internal val rustCoreBearerDid: RustCoreBearerDid internal constructor(rustCoreBearerDid: RustCoreBearerDid) { this.rustCoreBearerDid = rustCoreBearerDid 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 376ba727..89e45ef1 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 @@ -1,69 +1,72 @@ package web5.sdk.dids.methods.dht -import web5.sdk.crypto.keys.Jwk -import web5.sdk.crypto.signers.Signer -import web5.sdk.dids.Did -import web5.sdk.dids.Document +import web5.sdk.crypto.keys.KeyManager +import web5.sdk.crypto.keys.ToInnerKeyManager +import web5.sdk.dids.BearerDid import web5.sdk.dids.ResolutionResult +import web5.sdk.rust.ServiceData +import web5.sdk.rust.VerificationMethodData import web5.sdk.rust.didDhtResolve as rustCoreDidDhtResolve -import web5.sdk.rust.DidDht as RustCoreDidDht -import web5.sdk.rust.Signer as RustCoreSigner + +data class DidDhtCreateOptions( + val publish: Boolean? = true, + val gatewayUrl: String? = null, + val keyManager: KeyManager? = null, + val service: List? = null, + val controller: List? = null, + val alsoKnownAs: List? = null, + val verificationMethod: List? = null +) + +data class DidDhtPublishOptions( + val gatewayUrl: String? = null +) + +data class DidDhtResolveOptions( + val gatewayUrl: String? = null +) /** * A class representing a DID (Decentralized Identifier) using the DHT method. - * - * @property did The DID associated with this instance. - * @property document The DID document associated with this instance. */ class DidDht { - val did: Did - val document: Document - - private val rustCoreDidDht: RustCoreDidDht - - /** - * Constructs a DidDht instance using an identity key. - * - * @param identityKey The identity key represented as a Jwk. - */ - constructor(identityKey: Jwk) { - rustCoreDidDht = RustCoreDidDht.fromIdentityKey(identityKey.rustCoreJwkData) - - this.did = Did.fromRustCoreDidData(rustCoreDidDht.getData().did) - this.document = rustCoreDidDht.getData().document - } - - /** - * Constructs a DidDht instance using a DID URI. - * - * @param uri The DID URI. - */ - constructor(uri: String) { - rustCoreDidDht = RustCoreDidDht.fromUri(uri) - - this.did = Did.fromRustCoreDidData(rustCoreDidDht.getData().did) - this.document = rustCoreDidDht.getData().document - } - - /** - * Publishes the DID document. - * - * @param signer The signer used to sign the publish operation. - */ - fun publish(signer: Signer) { - rustCoreDidDht.publish(signer as RustCoreSigner) - } + companion object { + /** + * Create a DidDht BearerDid using available options. + * + * @param options The set of options to configure creation. + */ + fun create(options: DidDhtCreateOptions? = null): BearerDid { + val rustCoreOptions = options?.let { opts -> + web5.sdk.rust.DidDhtCreateOptions( + opts.publish, + opts.gatewayUrl, + opts.keyManager?.let { ToInnerKeyManager(it) }, + opts.service, + opts.controller, + opts.alsoKnownAs, + opts.verificationMethod + ) + } + val rustCoreBearerDid = web5.sdk.rust.didDhtCreate(rustCoreOptions) + return BearerDid(rustCoreBearerDid) + } - /** - * Deactivates the DID document. - * - * @param signer The signer used to sign the deactivate operation. - */ - fun deactivate(signer: Signer) { - rustCoreDidDht.deactivate(signer as RustCoreSigner) - } + /** + * Publish a DidDht BearerDid using available options. + * + * @param bearerDid The DidDht BearerDid instance to publish. + * @param options The set of options to configure publish. + */ + fun publish(bearerDid: BearerDid, options: DidDhtPublishOptions? = null) { + web5.sdk.rust.didDhtPublish( + bearerDid.rustCoreBearerDid, + web5.sdk.rust.DidDhtPublishOptions( + options?.gatewayUrl + ) + ) + } - companion object { /** * Resolves a DID URI to a DidResolutionResult. * @@ -71,8 +74,11 @@ class DidDht { * @return DidResolutionResult The result of the DID resolution. */ @JvmStatic - fun resolve(uri: String): ResolutionResult { - return rustCoreDidDhtResolve(uri).getData() + fun resolve(uri: String, options: DidDhtResolveOptions? = null): ResolutionResult { + val rustCoreOptions = web5.sdk.rust.DidDhtResolveOptions( + options?.gatewayUrl + ) + return rustCoreDidDhtResolve(uri, rustCoreOptions).getData() } } } 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 c782cafd..c9f4dc16 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 @@ -2,7 +2,6 @@ package web5.sdk.dids.methods.web import web5.sdk.crypto.keys.KeyManager import web5.sdk.crypto.keys.ToInnerKeyManager -import web5.sdk.crypto.keys.ToOuterKeyManager import web5.sdk.dids.BearerDid import web5.sdk.dids.ResolutionResult import web5.sdk.rust.Dsa 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 755f9012..bba578de 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt @@ -858,14 +858,6 @@ internal open class UniffiVTableCallbackInterfaceVerifier( - - - - - - - - @@ -917,20 +909,6 @@ internal interface UniffiLib : Library { ): Pointer fun uniffi_web5_uniffi_fn_method_did_get_data(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue - fun uniffi_web5_uniffi_fn_clone_diddht(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, - ): Pointer - fun uniffi_web5_uniffi_fn_free_diddht(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, - ): Unit - fun uniffi_web5_uniffi_fn_constructor_diddht_from_identity_key(`identityKey`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, - ): Pointer - fun uniffi_web5_uniffi_fn_constructor_diddht_from_uri(`uri`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, - ): Pointer - fun uniffi_web5_uniffi_fn_method_diddht_deactivate(`ptr`: Pointer,`signer`: Pointer,uniffi_out_err: UniffiRustCallStatus, - ): Unit - fun uniffi_web5_uniffi_fn_method_diddht_get_data(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, - ): RustBuffer.ByValue - fun uniffi_web5_uniffi_fn_method_diddht_publish(`ptr`: Pointer,`signer`: Pointer,uniffi_out_err: UniffiRustCallStatus, - ): Unit fun uniffi_web5_uniffi_fn_clone_document(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, ): Pointer fun uniffi_web5_uniffi_fn_free_document(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, @@ -1039,7 +1017,11 @@ internal interface UniffiLib : Library { ): Unit fun uniffi_web5_uniffi_fn_method_verifier_verify(`ptr`: Pointer,`message`: RustBuffer.ByValue,`signature`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Unit - fun uniffi_web5_uniffi_fn_func_did_dht_resolve(`uri`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + fun uniffi_web5_uniffi_fn_func_did_dht_create(`options`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_web5_uniffi_fn_func_did_dht_publish(`bearerDid`: Pointer,`options`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_web5_uniffi_fn_func_did_dht_resolve(`uri`: RustBuffer.ByValue,`options`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Pointer fun uniffi_web5_uniffi_fn_func_did_jwk_create(`options`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Pointer @@ -1163,6 +1145,10 @@ internal interface UniffiLib : Library { ): Unit fun ffi_web5_uniffi_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Unit + fun uniffi_web5_uniffi_checksum_func_did_dht_create( + ): Short + fun uniffi_web5_uniffi_checksum_func_did_dht_publish( + ): Short fun uniffi_web5_uniffi_checksum_func_did_dht_resolve( ): Short fun uniffi_web5_uniffi_checksum_func_did_jwk_create( @@ -1181,12 +1167,6 @@ internal interface UniffiLib : Library { ): Short fun uniffi_web5_uniffi_checksum_method_did_get_data( ): Short - fun uniffi_web5_uniffi_checksum_method_diddht_deactivate( - ): Short - fun uniffi_web5_uniffi_checksum_method_diddht_get_data( - ): Short - fun uniffi_web5_uniffi_checksum_method_diddht_publish( - ): Short fun uniffi_web5_uniffi_checksum_method_document_get_data( ): Short fun uniffi_web5_uniffi_checksum_method_ed25519signer_sign( @@ -1229,10 +1209,6 @@ internal interface UniffiLib : Library { ): Short fun uniffi_web5_uniffi_checksum_constructor_did_new( ): Short - fun uniffi_web5_uniffi_checksum_constructor_diddht_from_identity_key( - ): Short - fun uniffi_web5_uniffi_checksum_constructor_diddht_from_uri( - ): Short fun uniffi_web5_uniffi_checksum_constructor_document_new( ): Short fun uniffi_web5_uniffi_checksum_constructor_ed25519signer_new( @@ -1268,7 +1244,13 @@ private fun uniffiCheckContractApiVersion(lib: UniffiLib) { @Suppress("UNUSED_PARAMETER") private fun uniffiCheckApiChecksums(lib: UniffiLib) { - if (lib.uniffi_web5_uniffi_checksum_func_did_dht_resolve() != 54117.toShort()) { + if (lib.uniffi_web5_uniffi_checksum_func_did_dht_create() != 3925.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_web5_uniffi_checksum_func_did_dht_publish() != 33632.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_web5_uniffi_checksum_func_did_dht_resolve() != 43251.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } if (lib.uniffi_web5_uniffi_checksum_func_did_jwk_create() != 64914.toShort()) { @@ -1295,15 +1277,6 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_method_did_get_data() != 55630.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } - if (lib.uniffi_web5_uniffi_checksum_method_diddht_deactivate() != 8415.toShort()) { - throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - } - if (lib.uniffi_web5_uniffi_checksum_method_diddht_get_data() != 2858.toShort()) { - throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - } - if (lib.uniffi_web5_uniffi_checksum_method_diddht_publish() != 3488.toShort()) { - throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - } if (lib.uniffi_web5_uniffi_checksum_method_document_get_data() != 16490.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -1367,12 +1340,6 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_constructor_did_new() != 60730.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } - if (lib.uniffi_web5_uniffi_checksum_constructor_diddht_from_identity_key() != 7094.toShort()) { - throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - } - if (lib.uniffi_web5_uniffi_checksum_constructor_diddht_from_uri() != 63936.toShort()) { - throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") - } if (lib.uniffi_web5_uniffi_checksum_constructor_document_new() != 10173.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -2233,289 +2200,6 @@ public object FfiConverterTypeDid: FfiConverter { // -public interface DidDhtInterface { - - fun `deactivate`(`signer`: Signer) - - fun `getData`(): DidDhtData - - fun `publish`(`signer`: Signer) - - companion object -} - -open class DidDht: Disposable, AutoCloseable, DidDhtInterface { - - constructor(pointer: Pointer) { - this.pointer = pointer - this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) - } - - /** - * This constructor can be used to instantiate a fake object. Only used for tests. Any - * attempt to actually use an object constructed this way will fail as there is no - * connected Rust object. - */ - @Suppress("UNUSED_PARAMETER") - constructor(noPointer: NoPointer) { - this.pointer = null - this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) - } - - 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_diddht(ptr, status) - } - } - } - } - - fun uniffiClonePointer(): Pointer { - return uniffiRustCall() { status -> - UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_clone_diddht(pointer!!, status) - } - } - - - @Throws(Web5Exception::class)override fun `deactivate`(`signer`: Signer) - = - callWithPointer { - uniffiRustCallWithError(Web5Exception) { _status -> - UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_method_diddht_deactivate( - it, FfiConverterTypeSigner.lower(`signer`),_status) -} - } - - - - override fun `getData`(): DidDhtData { - return FfiConverterTypeDidDhtData.lift( - callWithPointer { - uniffiRustCall() { _status -> - UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_method_diddht_get_data( - it, _status) -} - } - ) - } - - - - @Throws(Web5Exception::class)override fun `publish`(`signer`: Signer) - = - callWithPointer { - uniffiRustCallWithError(Web5Exception) { _status -> - UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_method_diddht_publish( - it, FfiConverterTypeSigner.lower(`signer`),_status) -} - } - - - - - - - companion object { - - @Throws(Web5Exception::class) fun `fromIdentityKey`(`identityKey`: JwkData): DidDht { - return FfiConverterTypeDidDht.lift( - uniffiRustCallWithError(Web5Exception) { _status -> - UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_constructor_diddht_from_identity_key( - FfiConverterTypeJwkData.lower(`identityKey`),_status) -} - ) - } - - - - @Throws(Web5Exception::class) fun `fromUri`(`uri`: kotlin.String): DidDht { - return FfiConverterTypeDidDht.lift( - uniffiRustCallWithError(Web5Exception) { _status -> - UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_constructor_diddht_from_uri( - FfiConverterString.lower(`uri`),_status) -} - ) - } - - - - } - -} - -public object FfiConverterTypeDidDht: FfiConverter { - - override fun lower(value: DidDht): Pointer { - return value.uniffiClonePointer() - } - - override fun lift(value: Pointer): DidDht { - return DidDht(value) - } - - override fun read(buf: ByteBuffer): DidDht { - // 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: DidDht) = 8UL - - override fun write(value: DidDht, 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 DocumentInterface { fun `getData`(): DocumentData @@ -5632,30 +5316,113 @@ public object FfiConverterTypeDidData: FfiConverterRustBuffer { -data class DidDhtData ( - var `did`: DidData, - var `document`: DocumentData +data class DidDhtCreateOptions ( + var `publish`: kotlin.Boolean?, + var `gatewayUrl`: kotlin.String?, + var `keyManager`: KeyManager?, + var `service`: List?, + var `controller`: List?, + var `alsoKnownAs`: List?, + var `verificationMethod`: List? +) : Disposable { + + @Suppress("UNNECESSARY_SAFE_CALL") // codegen is much simpler if we unconditionally emit safe calls here + override fun destroy() { + + Disposable.destroy( + this.`publish`, + this.`gatewayUrl`, + this.`keyManager`, + this.`service`, + this.`controller`, + this.`alsoKnownAs`, + this.`verificationMethod`) + } + + companion object +} + +public object FfiConverterTypeDidDhtCreateOptions: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): DidDhtCreateOptions { + return DidDhtCreateOptions( + FfiConverterOptionalBoolean.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalTypeKeyManager.read(buf), + FfiConverterOptionalSequenceTypeServiceData.read(buf), + FfiConverterOptionalSequenceString.read(buf), + FfiConverterOptionalSequenceString.read(buf), + FfiConverterOptionalSequenceTypeVerificationMethodData.read(buf), + ) + } + + override fun allocationSize(value: DidDhtCreateOptions) = ( + FfiConverterOptionalBoolean.allocationSize(value.`publish`) + + FfiConverterOptionalString.allocationSize(value.`gatewayUrl`) + + FfiConverterOptionalTypeKeyManager.allocationSize(value.`keyManager`) + + FfiConverterOptionalSequenceTypeServiceData.allocationSize(value.`service`) + + FfiConverterOptionalSequenceString.allocationSize(value.`controller`) + + FfiConverterOptionalSequenceString.allocationSize(value.`alsoKnownAs`) + + FfiConverterOptionalSequenceTypeVerificationMethodData.allocationSize(value.`verificationMethod`) + ) + + override fun write(value: DidDhtCreateOptions, buf: ByteBuffer) { + FfiConverterOptionalBoolean.write(value.`publish`, buf) + FfiConverterOptionalString.write(value.`gatewayUrl`, buf) + FfiConverterOptionalTypeKeyManager.write(value.`keyManager`, buf) + FfiConverterOptionalSequenceTypeServiceData.write(value.`service`, buf) + FfiConverterOptionalSequenceString.write(value.`controller`, buf) + FfiConverterOptionalSequenceString.write(value.`alsoKnownAs`, buf) + FfiConverterOptionalSequenceTypeVerificationMethodData.write(value.`verificationMethod`, buf) + } +} + + + +data class DidDhtPublishOptions ( + var `gatewayUrl`: kotlin.String? ) { companion object } -public object FfiConverterTypeDidDhtData: FfiConverterRustBuffer { - override fun read(buf: ByteBuffer): DidDhtData { - return DidDhtData( - FfiConverterTypeDidData.read(buf), - FfiConverterTypeDocumentData.read(buf), +public object FfiConverterTypeDidDhtPublishOptions: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): DidDhtPublishOptions { + return DidDhtPublishOptions( + FfiConverterOptionalString.read(buf), ) } - override fun allocationSize(value: DidDhtData) = ( - FfiConverterTypeDidData.allocationSize(value.`did`) + - FfiConverterTypeDocumentData.allocationSize(value.`document`) + override fun allocationSize(value: DidDhtPublishOptions) = ( + FfiConverterOptionalString.allocationSize(value.`gatewayUrl`) ) - override fun write(value: DidDhtData, buf: ByteBuffer) { - FfiConverterTypeDidData.write(value.`did`, buf) - FfiConverterTypeDocumentData.write(value.`document`, buf) + override fun write(value: DidDhtPublishOptions, buf: ByteBuffer) { + FfiConverterOptionalString.write(value.`gatewayUrl`, buf) + } +} + + + +data class DidDhtResolveOptions ( + var `gatewayUrl`: kotlin.String? +) { + + companion object +} + +public object FfiConverterTypeDidDhtResolveOptions: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): DidDhtResolveOptions { + return DidDhtResolveOptions( + FfiConverterOptionalString.read(buf), + ) + } + + override fun allocationSize(value: DidDhtResolveOptions) = ( + FfiConverterOptionalString.allocationSize(value.`gatewayUrl`) + ) + + override fun write(value: DidDhtResolveOptions, buf: ByteBuffer) { + FfiConverterOptionalString.write(value.`gatewayUrl`, buf) } } @@ -6412,6 +6179,93 @@ public object FfiConverterOptionalTypeKeyManager: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): DidDhtCreateOptions? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterTypeDidDhtCreateOptions.read(buf) + } + + override fun allocationSize(value: DidDhtCreateOptions?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterTypeDidDhtCreateOptions.allocationSize(value) + } + } + + override fun write(value: DidDhtCreateOptions?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterTypeDidDhtCreateOptions.write(value, buf) + } + } +} + + + + +public object FfiConverterOptionalTypeDidDhtPublishOptions: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): DidDhtPublishOptions? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterTypeDidDhtPublishOptions.read(buf) + } + + override fun allocationSize(value: DidDhtPublishOptions?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterTypeDidDhtPublishOptions.allocationSize(value) + } + } + + override fun write(value: DidDhtPublishOptions?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterTypeDidDhtPublishOptions.write(value, buf) + } + } +} + + + + +public object FfiConverterOptionalTypeDidDhtResolveOptions: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): DidDhtResolveOptions? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterTypeDidDhtResolveOptions.read(buf) + } + + override fun allocationSize(value: DidDhtResolveOptions?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterTypeDidDhtResolveOptions.allocationSize(value) + } + } + + override fun write(value: DidDhtResolveOptions?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterTypeDidDhtResolveOptions.write(value, buf) + } + } +} + + + + public object FfiConverterOptionalTypeDidJwkCreateOptions: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): DidJwkCreateOptions? { if (buf.get().toInt() == 0) { @@ -6862,11 +6716,29 @@ public object FfiConverterMapStringString: FfiConverterRustBuffer + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_func_did_dht_create( + FfiConverterOptionalTypeDidDhtCreateOptions.lower(`options`),_status) +} + ) + } + + + @Throws(Web5Exception::class) fun `didDhtPublish`(`bearerDid`: BearerDid, `options`: DidDhtPublishOptions?) + = + uniffiRustCallWithError(Web5Exception) { _status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_func_did_dht_publish( + FfiConverterTypeBearerDid.lower(`bearerDid`),FfiConverterOptionalTypeDidDhtPublishOptions.lower(`options`),_status) +} + + + fun `didDhtResolve`(`uri`: kotlin.String, `options`: DidDhtResolveOptions?): ResolutionResult { + return FfiConverterTypeResolutionResult.lift( + uniffiRustCall() { _status -> UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_func_did_dht_resolve( - FfiConverterString.lower(`uri`),_status) + FfiConverterString.lower(`uri`),FfiConverterOptionalTypeDidDhtResolveOptions.lower(`options`),_status) } ) } 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 83f6f11f..b372f284 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 @@ -1,18 +1,414 @@ package web5.sdk.dids.methods.dht -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Test +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.fail +import web5.sdk.UnitTestSuite +import web5.sdk.crypto.keys.InMemoryKeyManager import web5.sdk.crypto.keys.Jwk - -import web5.sdk.rust.ed25519GeneratorGenerate as rustCoreEd25519GeneratorGenerate +import web5.sdk.rust.* class DidDhtTests { - @Test - fun `can create did dht`() { - val jwk = rustCoreEd25519GeneratorGenerate() + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Create { + private val testSuite = UnitTestSuite("did_dht_create") + + @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! ${testSuite.tests}") + } + } + + @Test + fun test_can_specify_key_manager() { + testSuite.include() + + val keyManager = InMemoryKeyManager(listOf()) + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = false, + keyManager = keyManager + ) + ) + + val publicJwk = bearerDid.document.verificationMethod.first().publicKeyJwk + assertDoesNotThrow { + keyManager.getSigner(Jwk.fromRustCoreJwkData(publicJwk)) + } + } + + @Test + fun test_can_specify_publish_and_gateway_url() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val gatewayUrl = mockWebServer.url("") + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/octet-stream") + ) + + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = true, + gatewayUrl = gatewayUrl.toString() + ) + ) + + mockWebServer.takeRequest().apply { + assertEquals("/${bearerDid.did.uri.removePrefix("did:dht:")}", path) + assertEquals("PUT", method) + assertEquals("application/octet-stream", headers["Content-Type"]) + } + + mockWebServer.shutdown() + } + + @Test + fun test_should_add_optional_verification_methods() { + testSuite.include() + + val additionalVerificationMethod = VerificationMethodData( + id = "did:web:example.com#key-1", + type = "JsonWebKey", + controller = "did:web:example.com", + publicKeyJwk = JwkData( + kty = "OKP", + crv = "Ed25519", + x = "some pub value", + alg = null, + y = null, + d = null + ) + ) + + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = false, + verificationMethod = listOf(additionalVerificationMethod) + ) + ) + + assertEquals(2, bearerDid.document.verificationMethod.size) + assertEquals(additionalVerificationMethod, bearerDid.document.verificationMethod[1]) + } + + @Test + fun test_should_add_optional_services() { + testSuite.include() + + val service = ServiceData( + id = "did:web:example.com#service-0", + type = "SomeService", + serviceEndpoint = listOf("https://example.com/service") + ) + + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = false, + service = listOf(service) + ) + ) + + assertEquals(service, bearerDid.document.service!!.first()) + } + + @Test + fun test_should_add_optional_also_known_as() { + testSuite.include() + + val alsoKnownAs = listOf("https://alias.example.com") + + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = false, + alsoKnownAs = alsoKnownAs + ) + ) + + assertEquals(alsoKnownAs, bearerDid.document.alsoKnownAs) + } + + @Test + fun test_should_add_optional_controllers() { + testSuite.include() + + val controllers = listOf("did:web:controller.example.com") + + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = false, + controller = controllers + ) + ) + + assertEquals(controllers, bearerDid.document.controller) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Publish { + private val testSuite = UnitTestSuite("did_dht_publish") + + @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! ${testSuite.tests}") + } + } + + @Test + fun test_can_specify_gateway_url() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val gatewayUrl = mockWebServer.url("") + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/octet-stream") + ) + + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = false + ) + ) + + DidDht.publish( + bearerDid, + DidDhtPublishOptions( + gatewayUrl = gatewayUrl.toString() + ) + ) + + val request = mockWebServer.takeRequest() + assertEquals("PUT", request.method) + assertEquals("/${bearerDid.did.uri.removePrefix("did:dht:")}", request.path) + assertEquals("application/octet-stream", request.getHeader("Content-Type")) + + mockWebServer.shutdown() + } + + @Test + fun test_can_handle_network_error() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val gatewayUrl = mockWebServer.url("") + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(500) + .addHeader("Content-Type", "application/octet-stream") + ) + + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = false + ) + ) + + val exception = assertThrows { + DidDht.publish( + bearerDid, + DidDhtPublishOptions( + gatewayUrl = gatewayUrl.toString() + ) + ) + } + + assertEquals("network error failed to PUT DID to mainline", exception.msg) + assertEquals("Network", exception.variant) + + mockWebServer.shutdown() + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Resolve { + private val testSuite = UnitTestSuite("did_dht_resolve") + + @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! ${testSuite.tests}") + } + } + + @Test + fun test_invalid_did() { + testSuite.include() + + val resolutionResult = DidDht.resolve("something invalid") + + assertEquals( + ResolutionMetadataError.INVALID_DID, + resolutionResult.resolutionMetadata.error + ) + } + + @Test + fun test_method_not_supported() { + testSuite.include() + + val resolutionResult = DidDht.resolve("did:web:example") + + assertEquals( + ResolutionMetadataError.METHOD_NOT_SUPPORTED, + resolutionResult.resolutionMetadata.error + ) + } + + @Test + fun test_not_found() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val gatewayUrl = mockWebServer.url("") + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(404) + .addHeader("Content-Type", "application/octet-stream") + ) + + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = false + ) + ) + + val resolutionResult = DidDht.resolve( + bearerDid.did.uri, + DidDhtResolveOptions( + gatewayUrl = gatewayUrl.toString() + ) + ) + + assertEquals( + ResolutionMetadataError.NOT_FOUND, + resolutionResult.resolutionMetadata.error + ) + + mockWebServer.shutdown() + } + + @Test + fun test_internal_error() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val gatewayUrl = mockWebServer.url("") + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(500) + .addHeader("Content-Type", "application/octet-stream") + ) + + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = false + ) + ) + + val resolutionResult = DidDht.resolve( + bearerDid.did.uri, + DidDhtResolveOptions( + gatewayUrl = gatewayUrl.toString() + ) + ) + + assertEquals( + ResolutionMetadataError.INTERNAL_ERROR, + resolutionResult.resolutionMetadata.error + ) + + mockWebServer.shutdown() + } + + @Test + fun test_can_create_then_resolve() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + // Capture the body of the published DID Document + val publishedBody = mutableListOf() + + mockWebServer.dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + return when { + request.method == "PUT" -> { + // Capture the published body + publishedBody.add(request.body.readByteArray()) + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/octet-stream") + } + + request.method == "GET" -> { + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/octet-stream") + .setBody(okio.Buffer().write(publishedBody.first())) + } + + else -> MockResponse().setResponseCode(404) + } + } + } + + val gatewayUrl = mockWebServer.url("") + + val bearerDid = DidDht.create( + DidDhtCreateOptions( + publish = true, + gatewayUrl = gatewayUrl.toString() + ) + ) + + val resolutionResult = DidDht.resolve( + bearerDid.did.uri, + DidDhtResolveOptions( + gatewayUrl = gatewayUrl.toString() + ) + ) - val didDht = DidDht(Jwk.fromRustCoreJwkData(jwk)) + assertNull(resolutionResult.resolutionMetadata.error) + assertNotNull(resolutionResult.document) + assertEquals(bearerDid.document, resolutionResult.document) - assertNotNull(didDht.document.id) + mockWebServer.shutdown() + } } } \ No newline at end of file diff --git a/crates/web5/src/dids/methods/did_dht/mod.rs b/crates/web5/src/dids/methods/did_dht/mod.rs index ed177237..d64731d2 100644 --- a/crates/web5/src/dids/methods/did_dht/mod.rs +++ b/crates/web5/src/dids/methods/did_dht/mod.rs @@ -2,28 +2,28 @@ use bep44::Bep44Message; use reqwest::blocking::Client; use simple_dns::Packet; -use super::{MethodError, Result}; use crate::{ crypto::{ - dsa::{ - ed25519::{self, Ed25519Verifier}, - Signer, - }, + dsa::ed25519::{self, Ed25519Generator, Ed25519Verifier}, jwk::Jwk, + key_managers::{in_memory_key_manager::InMemoryKeyManager, KeyManager}, }, dids::{ - data_model::{document::Document, verification_method::VerificationMethod}, + bearer_did::BearerDid, + data_model::{ + document::Document, service::Service, verification_method::VerificationMethod, + }, did::Did, resolution::{ - resolution_metadata::{ResolutionMetadata, ResolutionMetadataError}, - resolution_result::ResolutionResult, + resolution_metadata::ResolutionMetadataError, resolution_result::ResolutionResult, }, }, + errors::{Result, Web5Error}, }; use std::sync::Arc; mod bep44; -pub mod document_packet; +mod document_packet; const JSON_WEB_KEY: &str = "JsonWebKey"; const DEFAULT_RELAY: &str = "https://diddht.tbddev.org"; @@ -35,98 +35,146 @@ fn create_identifier(identity_key_jwk: &Jwk) -> Result { } #[derive(Clone, Default)] -pub struct DidDht { - pub did: Did, - pub document: Document, +pub struct DidDht; + +#[derive(Default)] +pub struct DidDhtCreateOptions { + pub publish: Option, + pub gateway_url: Option, + pub key_manager: Option>, + pub service: Option>, + pub controller: Option>, + pub also_known_as: Option>, + pub verification_method: Option>, +} + +#[derive(Default)] +pub struct DidDhtPublishOptions { + pub gateway_url: Option, +} + +#[derive(Default)] +pub struct DidDhtResolveOptions { + pub gateway_url: Option, } impl DidDht { - pub fn from_identity_key(identity_key: Jwk) -> Result { - if identity_key.crv != "Ed25519" { - return Err(MethodError::DidCreationFailure( - "Identity key must use Ed25519".to_string(), - )); - } - let did_uri = create_identifier(&identity_key)?; + pub fn create(options: Option) -> Result { + let options = options.unwrap_or_default(); + + let key_manager = options + .key_manager + .unwrap_or_else(|| Arc::new(InMemoryKeyManager::new())); + + let private_jwk = Ed25519Generator::generate(); + let identity_jwk = key_manager.import_private_jwk(private_jwk)?; + + let did_uri = create_identifier(&identity_jwk)?; let identity_key_verification_method = VerificationMethod { id: format!("{}#0", &did_uri), r#type: JSON_WEB_KEY.to_string(), controller: did_uri.clone(), - public_key_jwk: identity_key, + public_key_jwk: identity_jwk, }; - let capability_delegation = vec![identity_key_verification_method.id.clone()]; - let capability_invocation = vec![identity_key_verification_method.id.clone()]; - let authentication = vec![identity_key_verification_method.id.clone()]; - let assertion_method = vec![identity_key_verification_method.id.clone()]; - let verification_methods = vec![identity_key_verification_method]; - - // TODO maybe add additional verification methods and verification purposes - // if let Some(additional_verification_methods) = additional_verification_methods { - // for vm_opts in additional_verification_methods { - // let verification_method = VerificationMethod { - // id: format!("{}#{}", did_uri, &vm_opts.public_key.compute_thumbprint().unwrap()), // TODO: don't unwrap - // r#type: JSON_WEB_KEY.to_string(), - // controller: "foo".to_string(), - // public_key_jwk: vm_opts.public_key, - // }; - - // for purpose in vm_opts.purposes { - // match purpose { - // VerificationPurposes::Authentication => authentication.push(verification_method.id.clone()), - // VerificationPurposes::AssertionMethod => assertion_method.push(verification_method.id.clone()), - // VerificationPurposes::CapabilityInvocation => capability_invocation.push(verification_method.id.clone()), - // VerificationPurposes::CapabilityDelegation => capability_delegation.push(verification_method.id.clone()), - // VerificationPurposes::KeyAgreement => key_agreement.push(verification_method.id.clone()), - // } - // } - - // verification_methods.push(verification_method); - // } - // } - - Ok(Self { - did: Did::parse(&did_uri)?, - document: Document { - id: did_uri.clone(), - verification_method: verification_methods, - capability_delegation: Some(capability_delegation), - capability_invocation: Some(capability_invocation), - authentication: Some(authentication), - assertion_method: Some(assertion_method), - ..Default::default() + let did = Did::parse(&did_uri)?; + let document = Document { + id: did_uri.clone(), + service: options.service, + also_known_as: options.also_known_as, + controller: options.controller, + verification_method: { + let mut methods = vec![identity_key_verification_method.clone()]; + if let Some(mut additional_methods) = options.verification_method { + methods.append(&mut additional_methods); + } + methods }, - }) + capability_delegation: Some(vec![identity_key_verification_method.id.clone()]), + capability_invocation: Some(vec![identity_key_verification_method.id.clone()]), + authentication: Some(vec![identity_key_verification_method.id.clone()]), + assertion_method: Some(vec![identity_key_verification_method.id.clone()]), + ..Default::default() + }; + + let bearer_did = BearerDid { + did, + document, + key_manager, + }; + + if options.publish.unwrap_or(true) { + DidDht::publish( + bearer_did.clone(), + Some(DidDhtPublishOptions { + gateway_url: options.gateway_url, + }), + )?; + } + + Ok(bearer_did) } - pub fn from_uri(uri: &str) -> Result { - let resolution_result = DidDht::resolve(uri); - match resolution_result.document { - None => Err(match resolution_result.resolution_metadata.error { - None => MethodError::ResolutionError(ResolutionMetadataError::InternalError), - Some(e) => MethodError::ResolutionError(e), - }), - Some(document) => { - let identifer = Did::parse(uri)?; - Ok(Self { - did: identifer, - document, - }) - } + pub fn publish(bearer_did: BearerDid, options: Option) -> Result<()> { + let options = options.unwrap_or_default(); + + let packet = bearer_did.document.to_packet().map_err(|e| { + Web5Error::Encoding(format!("failed to convert document to packet {}", e)) + })?; + + let packet_bytes = packet + .build_bytes_vec() + .map_err(|_| Web5Error::Encoding("failed to serialize packet as bytes".to_string()))?; + + let public_jwk = bearer_did.document.verification_method[0] + .public_key_jwk + .clone(); + let signer = bearer_did.key_manager.get_signer(public_jwk)?; + let bep44_message = Bep44Message::new(&packet_bytes, |payload| signer.sign(&payload)) + .map_err(|_| { + Web5Error::Encoding("failed to convert packet bytes to bep44 message".to_string()) + })?; + + let body = bep44_message.encode().map_err(|_| { + Web5Error::Encoding("failed to serialize bep44 message as bytes".to_string()) + })?; + + let url = format!( + "{}/{}", + options + .gateway_url + .unwrap_or_else(|| DEFAULT_RELAY.to_string()) + .trim_end_matches('/'), + bearer_did.did.id.trim_start_matches('/') + ); + + let client = Client::new(); + let response = client + .put(url) + .header("Content-Type", "application/octet-stream") + .body(body) + .send() + .map_err(|_| Web5Error::Network("failed to publish DID to mainline".to_string()))?; + + if response.status() != 200 { + return Err(Web5Error::Network( + "failed to PUT DID to mainline".to_string(), + )); } + + Ok(()) } - pub fn resolve(uri: &str) -> ResolutionResult { - let result: Result = (|| { + pub fn resolve(uri: &str, options: Option) -> ResolutionResult { + let options = options.unwrap_or_default(); + + let result: std::result::Result = (|| { // check did method and decode id let did = Did::parse(uri).map_err(|_| ResolutionMetadataError::InvalidDid)?; if did.method != "dht" { - return Ok(ResolutionResult { - resolution_metadata: ResolutionMetadata { - error: Some(ResolutionMetadataError::MethodNotSupported), - }, - ..Default::default() - }); + return Ok(ResolutionResult::from( + ResolutionMetadataError::MethodNotSupported, + )); } let identity_key = zbase32::decode_full_bytes_str(&did.id) .map_err(|_| ResolutionMetadataError::InvalidPublicKey)?; @@ -136,7 +184,10 @@ impl DidDht { // construct http endpoint from gateway url and last part of did_uri let url = format!( "{}/{}", - DEFAULT_RELAY.trim_end_matches('/'), + options + .gateway_url + .unwrap_or_else(|| DEFAULT_RELAY.to_string()) + .trim_end_matches('/'), did.id.trim_start_matches('/') ); @@ -189,125 +240,423 @@ impl DidDht { match result { Ok(resolution_result) => resolution_result, - Err(err) => ResolutionResult { - resolution_metadata: ResolutionMetadata { - error: Some(match err { - MethodError::ResolutionError(e) => e, - _ => ResolutionMetadataError::InternalError, - }), - }, - ..Default::default() - }, + Err(e) => ResolutionResult::from(e), } } +} - pub fn publish(&self, signer: Arc) -> Result<()> { - let packet = self - .document - .to_packet() - .map_err(|e| MethodError::DidPublishingFailure(e.to_string()))?; - let packet_bytes = packet.build_bytes_vec().map_err(|_| { - MethodError::DidPublishingFailure("Failed to serialize packet as bytes".to_string()) - })?; +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_helpers::UnitTestSuite, test_name}; + use lazy_static::lazy_static; - let bep44_message = Bep44Message::new(&packet_bytes, |payload| signer.sign(&payload)) - .map_err(|_| { - MethodError::DidPublishingFailure( - "Failed to create bep44 message from packet".to_string(), - ) - })?; - let body = bep44_message.encode().map_err(|_| { - MethodError::DidPublishingFailure( - "Failed to serialize bep44 message as bytes".to_string(), - ) - })?; + mod create { + use super::*; - let url = format!( - "{}/{}", - DEFAULT_RELAY.trim_end_matches('/'), - self.did.id.trim_start_matches('/') - ); - let client = Client::new(); - let response = client - .put(url) - .header("Content-Type", "application/octet-stream") - .body(body) - .send() - .map_err(|_| { - MethodError::DidPublishingFailure("Failed to publish DID to mainline".to_string()) - })?; + lazy_static! { + static ref TEST_SUITE: UnitTestSuite = UnitTestSuite::new("did_dht_create"); + } - if response.status() != 200 { - return Err(MethodError::DidPublishingFailure( - "Failed to PUT DID to mainline".to_string(), - )); + #[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() } - Ok(()) - } + #[test] + fn test_can_specify_key_manager() { + TEST_SUITE.include(test_name!()); - pub fn deactivate(&self, _signer: Arc) -> Result<()> { - println!("DidDht.deactivate() called"); - Ok(()) - } -} + let key_manager = Arc::new(InMemoryKeyManager::new()); + let result = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(false), + key_manager: Some(key_manager.clone()), + ..Default::default() + })); -#[cfg(test)] -mod tests { - use crate::crypto::dsa::ed25519::{self, Ed25519Generator, Ed25519Signer}; + assert!(result.is_ok()); - use super::*; + let bearer_did = result.unwrap(); + let public_jwk = bearer_did.document.verification_method[0] + .public_key_jwk + .clone(); + let result = key_manager.get_signer(public_jwk); + assert!(result.is_ok()) + } - #[test] - fn test_from_identity_key() { - let private_jwk = Ed25519Generator::generate(); - let identity_key = ed25519::to_public_jwk(&private_jwk); - let did_dht = - DidDht::from_identity_key(identity_key.clone()).expect("Should create did:dht"); - - assert_eq!(did_dht.did.method, "dht"); - assert_eq!( - did_dht.document.verification_method[0].public_key_jwk, - identity_key - ); - assert_eq!( - did_dht.document.verification_method[0].id, - format!("{}#0", did_dht.did.uri) - ); + #[test] + fn test_can_specify_publish_and_gateway_url() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = mockito::Server::new(); + let gateway_url = mock_server.url(); + + let mock = mock_server + .mock("PUT", mockito::Matcher::Any) + .expect(1) + .with_status(200) + .with_header("content-type", "application/octet-stream") + .create(); + + let result = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(true), + gateway_url: Some(gateway_url.clone()), // Use the mock server's URL + ..Default::default() + })); + + assert!(result.is_ok()); + + mock.assert(); + } + + #[test] + fn test_should_add_optional_verification_methods() { + TEST_SUITE.include(test_name!()); + + let additional_verification_method = VerificationMethod { + id: "did:web:example.com#key-1".to_string(), + r#type: "JsonWebKey".to_string(), + controller: "did:web:example.com".to_string(), + public_key_jwk: Ed25519Generator::generate(), + }; + + let result = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(false), + verification_method: Some(vec![additional_verification_method.clone()]), + ..Default::default() + })); + + assert!(result.is_ok()); + + let did_web = result.unwrap(); + assert_eq!(did_web.document.verification_method.len(), 2); + assert_eq!( + did_web.document.verification_method[1], + additional_verification_method + ); + } + + #[test] + fn test_should_add_optional_services() { + TEST_SUITE.include(test_name!()); + + let service = Service { + id: "did:web:example.com#service-0".to_string(), + r#type: "SomeService".to_string(), + service_endpoint: vec!["https://example.com/service".to_string()], + }; + + let result = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(false), + service: Some(vec![service.clone()]), + ..Default::default() + })); + + assert!(result.is_ok()); + + let did_web = result.unwrap(); + assert_eq!(did_web.document.service.unwrap()[0], service); + } + + #[test] + fn test_should_add_optional_also_known_as() { + TEST_SUITE.include(test_name!()); + + let also_known_as = vec!["https://alias.example.com".to_string()]; + + let result = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(false), + also_known_as: Some(also_known_as.clone()), + ..Default::default() + })); + + assert!(result.is_ok()); + + let did_web = result.unwrap(); + assert_eq!(did_web.document.also_known_as.unwrap(), also_known_as); + } + + #[test] + fn test_should_add_optional_controllers() { + TEST_SUITE.include(test_name!()); + + let controllers = vec!["did:web:controller.example.com".to_string()]; + + let result = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(false), + controller: Some(controllers.clone()), + ..Default::default() + })); + + assert!(result.is_ok()); + + let did_web = result.unwrap(); + assert_eq!(did_web.document.controller.unwrap(), controllers); + } } - #[test] - fn test_publish() { - // Create did:dht - let private_jwk = Ed25519Generator::generate(); - let identity_key = ed25519::to_public_jwk(&private_jwk); - let did_dht = - DidDht::from_identity_key(identity_key.clone()).expect("Should create did:dht"); - - // Publish - let signer = Ed25519Signer::new(private_jwk); - did_dht - .publish(Arc::new(signer)) - .expect("Should publish did"); + mod publish { + use super::*; + + lazy_static! { + static ref TEST_SUITE: UnitTestSuite = UnitTestSuite::new("did_dht_publish"); + } + + #[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_can_specify_gateway_url() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = mockito::Server::new(); + let gateway_url = mock_server.url(); + + let mock = mock_server + .mock("PUT", mockito::Matcher::Any) + .expect(1) + .with_status(200) + .with_header("content-type", "application/octet-stream") + .create(); + + let bearer_did = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(false), + ..Default::default() + })) + .unwrap(); + + let result = DidDht::publish( + bearer_did, + Some(DidDhtPublishOptions { + gateway_url: Some(gateway_url.clone()), // Use the mock server's URL + }), + ); + + assert!(result.is_ok()); + + mock.assert(); + } + + #[test] + fn test_can_handle_network_error() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = mockito::Server::new(); + let gateway_url = mock_server.url(); + + let mock = mock_server + .mock("PUT", mockito::Matcher::Any) + .expect(1) + .with_status(500) + .with_header("content-type", "application/octet-stream") + .create(); + + let bearer_did = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(false), + ..Default::default() + })) + .unwrap(); + + let result = DidDht::publish( + bearer_did, + Some(DidDhtPublishOptions { + gateway_url: Some(gateway_url), + }), + ); + + assert!(result.is_err()); + if let Err(Web5Error::Network(msg)) = result { + assert_eq!(msg, "failed to PUT DID to mainline"); + } else { + panic!("expected Web5Error::Network error"); + } + + mock.assert(); + } } - #[test] - fn test_resolve() { - // Create did:dht - let private_jwk = Ed25519Generator::generate(); - let identity_key = ed25519::to_public_jwk(&private_jwk); - let did_dht = - DidDht::from_identity_key(identity_key.clone()).expect("Should create did:dht"); - - // Publish - let signer = Ed25519Signer::new(private_jwk); - did_dht - .publish(Arc::new(signer)) - .expect("Should publish did"); - - // Resolve from uri - let resolved_did_dht = DidDht::resolve(&did_dht.did.uri); - let resolved_document = resolved_did_dht.document.unwrap(); - assert_eq!(resolved_document, did_dht.document) + mod resolve { + use std::sync::Mutex; + + use super::*; + + lazy_static! { + static ref TEST_SUITE: UnitTestSuite = UnitTestSuite::new("did_dht_resolve"); + } + + #[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_invalid_did() { + TEST_SUITE.include(test_name!()); + + let resolution_result = DidDht::resolve("something invalid", None); + assert_eq!( + resolution_result.resolution_metadata.error, + Some(ResolutionMetadataError::InvalidDid) + ) + } + + #[test] + fn test_method_not_supported() { + TEST_SUITE.include(test_name!()); + + let resolution_result = DidDht::resolve("did:web:example", None); + assert_eq!( + resolution_result.resolution_metadata.error, + Some(ResolutionMetadataError::MethodNotSupported) + ) + } + + #[test] + fn test_not_found() { + TEST_SUITE.include(test_name!()); + + let bearer_did = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(false), + ..Default::default() + })) + .unwrap(); + + let mut mock_server = mockito::Server::new(); + let gateway_url = mock_server.url(); + + let mock = mock_server + .mock("GET", format!("/{}", bearer_did.did.id).as_str()) + .expect(1) + .with_status(404) + .with_header("content-type", "application/octet-stream") + .create(); + + let resolution_result = DidDht::resolve( + &bearer_did.did.uri, + Some(DidDhtResolveOptions { + gateway_url: Some(gateway_url), + }), + ); + assert_eq!( + resolution_result.resolution_metadata.error, + Some(ResolutionMetadataError::NotFound) + ); + + mock.assert(); + } + + #[test] + fn test_internal_error() { + TEST_SUITE.include(test_name!()); + + let bearer_did = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(false), + ..Default::default() + })) + .unwrap(); + + let mut mock_server = mockito::Server::new(); + let gateway_url = mock_server.url(); + + let mock = mock_server + .mock("GET", format!("/{}", bearer_did.did.id).as_str()) + .expect(1) + .with_status(500) + .with_header("content-type", "application/octet-stream") + .create(); + + let resolution_result = DidDht::resolve( + &bearer_did.did.uri, + Some(DidDhtResolveOptions { + gateway_url: Some(gateway_url), + }), + ); + assert_eq!( + resolution_result.resolution_metadata.error, + Some(ResolutionMetadataError::InternalError) + ); + + mock.assert(); + } + + #[test] + fn test_can_create_then_resolve() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = mockito::Server::new(); + let gateway_url = mock_server.url(); + + let published_body = Arc::new(Mutex::new(Vec::new())); + let published_body_clone = Arc::clone(&published_body); + + let mock_publish = mock_server + .mock("PUT", mockito::Matcher::Any) + .expect(1) + .with_status(200) + .with_header("content-type", "application/octet-stream") + .with_body_from_request(move |request| { + let mut body = published_body_clone.lock().unwrap(); + *body = request.body().unwrap().to_vec(); + vec![] // Return an empty response body + }) + .create(); + + let create_result = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(true), + gateway_url: Some(gateway_url), + ..Default::default() + })); + assert!(create_result.is_ok()); + + let bearer_did = create_result.unwrap(); + + let stored_body = published_body.lock().unwrap(); + + let mock_resolve = mock_server + .mock("GET", format!("/{}", bearer_did.did.id).as_str()) + .expect(1) + .with_status(200) + .with_header("content-type", "application/octet-stream") + .with_body(stored_body.clone()) // Use the captured body as the response + .create(); + + let resolution_result = DidDht::resolve( + &bearer_did.did.uri, + Some(DidDhtResolveOptions { + gateway_url: Some(mock_server.url()), + }), + ); + + assert_eq!(resolution_result.resolution_metadata.error, None); + assert!(resolution_result.document.is_some()); + let resolved_document = resolution_result.document.unwrap(); + assert_eq!(resolved_document, bearer_did.document); + + mock_publish.assert(); + mock_resolve.assert(); + } } } diff --git a/crates/web5/src/dids/methods/did_web/mod.rs b/crates/web5/src/dids/methods/did_web/mod.rs index af35a411..72b9c5ac 100644 --- a/crates/web5/src/dids/methods/did_web/mod.rs +++ b/crates/web5/src/dids/methods/did_web/mod.rs @@ -487,11 +487,11 @@ mod tests { .with_body(serde_json::to_string(&bearer_did.document).unwrap()) .create(); - let resolve_result = DidWeb::resolve(&bearer_did.did.uri); + let resolution_result = DidWeb::resolve(&bearer_did.did.uri); - assert_eq!(resolve_result.resolution_metadata.error, None); - assert!(resolve_result.document.is_some()); - let resolved_document = resolve_result.document.unwrap(); + assert_eq!(resolution_result.resolution_metadata.error, None); + assert!(resolution_result.document.is_some()); + let resolved_document = resolution_result.document.unwrap(); assert_eq!(resolved_document, bearer_did.document); } } diff --git a/crates/web5/src/dids/methods/mod.rs b/crates/web5/src/dids/methods/mod.rs index 8200e09d..1620bd77 100644 --- a/crates/web5/src/dids/methods/mod.rs +++ b/crates/web5/src/dids/methods/mod.rs @@ -1,34 +1,4 @@ -use crate::errors::Web5Error; - -use super::resolution::resolution_metadata::ResolutionMetadataError; -use base64::DecodeError; -use serde_json::Error as SerdeJsonError; - pub mod did_dht; pub mod did_web; pub mod did_jwk; - -#[derive(thiserror::Error, Debug)] -pub enum MethodError { - #[error(transparent)] - Web5Error(#[from] Web5Error), - #[error("Failure creating DID: {0}")] - DidCreationFailure(String), - #[error("Failure publishing DID: {0}")] - DidPublishingFailure(String), - #[error("serde json error {0}")] - SerdeJsonError(String), - #[error(transparent)] - DecodeError(#[from] DecodeError), - #[error(transparent)] - ResolutionError(#[from] ResolutionMetadataError), -} - -impl From for MethodError { - fn from(err: SerdeJsonError) -> Self { - MethodError::SerdeJsonError(err.to_string()) - } -} - -type Result = std::result::Result; diff --git a/crates/web5/src/dids/resolution/resolution_result.rs b/crates/web5/src/dids/resolution/resolution_result.rs index e38ed924..bc6d3f41 100644 --- a/crates/web5/src/dids/resolution/resolution_result.rs +++ b/crates/web5/src/dids/resolution/resolution_result.rs @@ -30,7 +30,7 @@ impl ResolutionResult { match did.method.as_str() { "jwk" => DidJwk::resolve(uri), - "dht" => DidDht::resolve(uri), + "dht" => DidDht::resolve(uri, None), "web" => DidWeb::resolve(uri), _ => ResolutionResult { resolution_metadata: ResolutionMetadata { diff --git a/crates/web5/src/errors.rs b/crates/web5/src/errors.rs index 8f2e8c2e..235fc310 100644 --- a/crates/web5/src/errors.rs +++ b/crates/web5/src/errors.rs @@ -20,6 +20,8 @@ pub enum Web5Error { Encoding(String), #[error("mutex error {0}")] Mutex(String), + #[error("network error {0}")] + Network(String), } impl From for Web5Error { diff --git a/crates/web5/src/test_vectors.rs b/crates/web5/src/test_vectors.rs index 6659c66c..ab45e135 100644 --- a/crates/web5/src/test_vectors.rs +++ b/crates/web5/src/test_vectors.rs @@ -146,7 +146,7 @@ mod test_vectors { // }; } - let resolution_result = DidDht::resolve(&vector_input.did_uri); + let resolution_result = DidDht::resolve(&vector_input.did_uri, None); let metadata_error = resolution_result.resolution_metadata.error.as_ref(); let expected_error = vector_output.did_resolution_metadata.error.as_ref(); diff --git a/crates/web5_cli/src/dids/create.rs b/crates/web5_cli/src/dids/create.rs index 6f5f8740..7753b505 100644 --- a/crates/web5_cli/src/dids/create.rs +++ b/crates/web5_cli/src/dids/create.rs @@ -2,12 +2,12 @@ use clap::Subcommand; use std::sync::Arc; use web5::{ crypto::{ - dsa::ed25519::{Ed25519Generator, Ed25519Signer}, + dsa::ed25519::Ed25519Generator, key_managers::{in_memory_key_manager::InMemoryKeyManager, KeyManager}, }, dids::{ methods::{ - did_dht::DidDht, + did_dht::{DidDht, DidDhtCreateOptions}, did_jwk::{DidJwk, DidJwkCreateOptions}, did_web::{DidWeb, DidWebCreateOptions}, }, @@ -64,15 +64,15 @@ impl Commands { let key_manager = InMemoryKeyManager::new(); key_manager.import_private_jwk(private_jwk.clone()).unwrap(); - let did_jwk = DidJwk::create(Some(DidJwkCreateOptions { + let bearer_did = DidJwk::create(Some(DidJwkCreateOptions { key_manager: Some(Arc::new(key_manager)), ..Default::default() })) .unwrap(); let portable_did = PortableDid { - did_uri: did_jwk.did.uri, - document: did_jwk.document, + did_uri: bearer_did.did.uri, + document: bearer_did.document, private_jwks: vec![private_jwk], }; @@ -87,7 +87,7 @@ impl Commands { let key_manager = InMemoryKeyManager::new(); key_manager.import_private_jwk(private_jwk.clone()).unwrap(); - let did_web = DidWeb::create( + let bearer_did = DidWeb::create( domain, Some(DidWebCreateOptions { key_manager: Some(Arc::new(key_manager)), @@ -96,8 +96,8 @@ impl Commands { ) .unwrap(); let portable_did = PortableDid { - did_uri: did_web.did.uri, - document: did_web.document, + did_uri: bearer_did.did.uri, + document: bearer_did.document, private_jwks: vec![private_jwk], }; @@ -109,18 +109,19 @@ impl Commands { json_escape, } => { let private_jwk = Ed25519Generator::generate(); - let signer = Ed25519Signer::new(private_jwk.clone()); - let mut identity_key = private_jwk.clone(); - identity_key.d = None; + let key_manager = InMemoryKeyManager::new(); + key_manager.import_private_jwk(private_jwk.clone()).unwrap(); - let did_dht = DidDht::from_identity_key(identity_key).unwrap(); - if !no_publish { - did_dht.publish(Arc::new(signer)).unwrap(); - } + let bearer_did = DidDht::create(Some(DidDhtCreateOptions { + publish: Some(!no_publish), + key_manager: Some(Arc::new(key_manager)), + ..Default::default() + })) + .unwrap(); let portable_did = PortableDid { - did_uri: did_dht.did.uri, - document: did_dht.document, + did_uri: bearer_did.did.uri, + document: bearer_did.document, private_jwks: vec![private_jwk], }; diff --git a/docs/API_DESIGN.md b/docs/API_DESIGN.md index f0e22591..7b86493d 100644 --- a/docs/API_DESIGN.md +++ b/docs/API_DESIGN.md @@ -57,11 +57,7 @@ - [`DidWeb`](#didweb) - [`DidWebCreateOptions`](#didwebcreateoptions) - [`DidDht`](#diddht) - - [Example: Create \& publish a `did:dht`](#example-create--publish-a-diddht) - - [Example: Create a `did:dht`, add to the Core Properties \& publish](#example-create-a-diddht-add-to-the-core-properties--publish) - - [Example: Instantiate an existing `did:dht`](#example-instantiate-an-existing-diddht) - - [Example: Update a `did:dht`](#example-update-a-diddht) - - [Example: Resolve a `did:dht`](#example-resolve-a-diddht) + - [`DidDhtCreateOptions`](#diddhtcreateoptions) - [`BearerDid`](#bearerdid) - [Example: Instantiate from a `PortableDid`](#example-instantiate-from-a-portabledid) - [`PortableDid`](#portabledid) @@ -647,76 +643,25 @@ CLASS DidWebCreateOptions ```pseudocode! CLASS DidDht - PUBLIC DATA did: Did - PUBLIC DATA document: Document - CONSTRUCTOR(identity_key: Jwk) - CONSTRUCTOR(uri: string) - METHOD publish(signer: Signer) - METHOD deactivate(signer: Signer) + STATIC METHOD create(options: DidDhtCreateOptions?): BearerDid STATIC METHOD resolve(uri: string): ResolutionResult + STATIC METHOD publish(bearer_did: BearerDid) ``` > [!NOTE] > `resolve()` makes use of [`Ed25519Verifier`](#ed25519verifier) internally for DNS packet verification. -#### Example: Create & publish a `did:dht` - -```pseudocode! -key_manager = new InMemoryKeyManager() -identity_key = key_manager.import_private_jwk(Ed25519Generator::generate()) -did_dht = new DidDht(identity_key) -signer = key_manager.get_signer(identity_key) -did_dht.publish(signer) -``` - -#### Example: Create a `did:dht`, add to the Core Properties & publish - -> [!NOTE] -> The call to the `new DidDht()` constructor only adds the minimum requirements to the DID Document. -> If additional [Core Properties](https://www.w3.org/TR/did-core/#core-properties) are required, update the `document` data member prior-to the call to `publish()`. - -```pseudocode! -key_manager = new InMemoryKeyManager() -identity_key = key_manager.import_private_jwk(Ed25519Generator::generate()) -did_dht = new DidDht(identity_key) - -/// Set the alsoKnownAs -did_dht.document.alsoKnownAs = "did:example:efgh" -/// Note: you could also add a verification method, set the controller etc. - -signer = key_manager.get_signer(identity_key) -did_dht.publish(signer) -``` - -#### Example: Instantiate an existing `did:dht` - -```pseudocode! -uri = "did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y" -did_dht = new DidDht(uri) -``` - -#### Example: Update a `did:dht` +#### `DidDhtCreateOptions` ```pseudocode! -uri = "did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y" -did_dht = new DidDht(uri) - -/// Set the alsoKnownAs -did_dht.document.alsoKnownAs = "did:example:efgh" -/// Note: you could also add a verification method, set the controller etc. - -key_manager = new InMemoryKeyManager() -public_jwk = key_manager.import_private_jwk(private_jwk) /// assume private_jwk pre-exists, eg. read from env var -signer = key_manager.get_signer(public_jwk) - -did_dht.publish(signer) -``` - -#### Example: Resolve a `did:dht` - -```pseudocode! -uri = "did:dht:i9xkp8ddcbcg8jwq54ox699wuzxyifsqx4jru45zodqu453ksz6y" -resolution_result = DidDht.resolve(uri) +CLASS DidDhtCreateOptions + PUBLIC DATA key_manager: KeyManager? + PUBLIC DATA service: []Service? + PUBLIC DATA controller: []string? + PUBLIC DATA also_known_as: []string? + PUBLIC DATA verification_method: []VerificationMethod? + PUBLIC DATA publish: bool? = true + PUBLIC DATA gateway_url: string? ``` ## `BearerDid` diff --git a/tests/unit_test_cases/did_dht_create.json b/tests/unit_test_cases/did_dht_create.json new file mode 100644 index 00000000..e65626d4 --- /dev/null +++ b/tests/unit_test_cases/did_dht_create.json @@ -0,0 +1,8 @@ +[ + "test_can_specify_key_manager", + "test_can_specify_publish_and_gateway_url", + "test_should_add_optional_verification_methods", + "test_should_add_optional_services", + "test_should_add_optional_also_known_as", + "test_should_add_optional_controllers" +] diff --git a/tests/unit_test_cases/did_dht_publish.json b/tests/unit_test_cases/did_dht_publish.json new file mode 100644 index 00000000..5a67f9d4 --- /dev/null +++ b/tests/unit_test_cases/did_dht_publish.json @@ -0,0 +1,4 @@ +[ + "test_can_specify_gateway_url", + "test_can_handle_network_error" +] diff --git a/tests/unit_test_cases/did_dht_resolve.json b/tests/unit_test_cases/did_dht_resolve.json new file mode 100644 index 00000000..bdef7ca8 --- /dev/null +++ b/tests/unit_test_cases/did_dht_resolve.json @@ -0,0 +1,7 @@ +[ + "test_invalid_did", + "test_method_not_supported", + "test_not_found", + "test_internal_error", + "test_can_create_then_resolve" +]