diff --git a/crates/web5/src/dids/methods/did_web/mod.rs b/crates/web5/src/dids/methods/did_web/mod.rs index abcfc27e..3ec24d1f 100644 --- a/crates/web5/src/dids/methods/did_web/mod.rs +++ b/crates/web5/src/dids/methods/did_web/mod.rs @@ -1,15 +1,19 @@ mod resolver; use super::{MethodError, Result}; -use crate::dids::{ - data_model::document::Document, - did::Did, - resolution::{ - resolution_metadata::{ResolutionMetadata, ResolutionMetadataError}, - resolution_result::ResolutionResult, +use crate::{ + crypto::jwk::Jwk, + dids::{ + data_model::{document::Document, verification_method::VerificationMethod}, + did::Did, + resolution::{ + resolution_metadata::{ResolutionMetadata, ResolutionMetadataError}, + resolution_result::ResolutionResult, + }, }, }; use resolver::Resolver; +use url::Url; #[derive(Clone)] pub struct DidWeb { @@ -18,6 +22,74 @@ pub struct DidWeb { } impl DidWeb { + pub fn new(domain: &str, public_jwk: Jwk) -> Result { + let domain = &domain.to_string(); + let valid_url = if domain.starts_with("http://") || domain.starts_with("https://") { + let url = Url::parse(domain) + .map_err(|e| MethodError::DidCreationFailure(format!("url parse failure {}", e)))?; + + // Ensure "http://" is only allowed for localhost or 127.0.0.1 + if url.scheme() == "http" + && !(url.host_str() == Some("localhost") || url.host_str() == Some("127.0.0.1")) + { + return Err(MethodError::DidCreationFailure( + "only https is allowed except for localhost or 127.0.0.1 with http".to_string(), + )); + } + + // Get the trimmed URL string without the scheme + let trimmed_url = url[url::Position::BeforeHost..].to_string(); + + // Remove the scheme + let normalized = if let Some(trimmed) = trimmed_url.strip_prefix("//") { + trimmed + } else { + &trimmed_url + }; + + normalized.to_string() + } else { + Url::parse(&format!("https://{}", domain)) + .map_err(|e| MethodError::DidCreationFailure(format!("url parse failure {}", e)))?; + domain.clone() + }; + + let mut normalized = valid_url.clone(); + if normalized.ends_with('/') { + normalized = normalized.trim_end_matches('/').to_string() + } + if normalized.ends_with("/did.json") { + normalized = normalized.trim_end_matches("/did.json").to_string() + } + if normalized.ends_with("/.well-known") { + normalized = normalized.trim_end_matches("/.well-known").to_string() + } + + let encoded_domain = normalized.replace(':', "%3A"); + let encoded_domain = encoded_domain.replace('/', ":"); + + let did = format!("did:web:{}", encoded_domain); + + let verification_method = VerificationMethod { + id: format!("{}#key-0", did), + r#type: "JsonWebKey".to_string(), + controller: did.clone(), + public_key_jwk: public_jwk, + }; + + let document = Document { + id: did.clone(), + context: Some(vec!["https://www.w3.org/ns/did/v1".to_string()]), + verification_method: vec![verification_method], + ..Default::default() + }; + + Ok(DidWeb { + did: Did::new(&did)?, + document, + }) + } + pub async fn from_uri(uri: &str) -> Result { let resolution_result = DidWeb::resolve(uri); match resolution_result.document { diff --git a/crates/web5_cli/Cargo.toml b/crates/web5_cli/Cargo.toml index 1aa4906b..36d5f023 100644 --- a/crates/web5_cli/Cargo.toml +++ b/crates/web5_cli/Cargo.toml @@ -11,4 +11,5 @@ chrono = { workspace = true } clap = { version = "4.5.7", features = ["derive"] } serde_json = { workspace = true } web5 = { path = "../web5" } +url = "2.5.2" uuid = { workspace = true } \ No newline at end of file diff --git a/crates/web5_cli/src/dids/create.rs b/crates/web5_cli/src/dids/create.rs index e31201eb..c98cbf19 100644 --- a/crates/web5_cli/src/dids/create.rs +++ b/crates/web5_cli/src/dids/create.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use web5::{ crypto::dsa::ed25519::{Ed25519Generator, Ed25519Signer}, dids::{ - methods::{did_dht::DidDht, did_jwk::DidJwk}, + methods::{did_dht::DidDht, did_jwk::DidJwk, did_web::DidWeb}, portable_did::PortableDid, }, }; @@ -18,6 +18,10 @@ pub enum Commands { }, Web { domain: String, + #[arg(long)] + no_indent: bool, + #[arg(long)] + json_escape: bool, }, Dht { #[arg(long)] @@ -63,8 +67,23 @@ impl Commands { print_portable_did(portable_did, no_indent, json_escape); } - Commands::Web { domain: _ } => { - println!("🚧 not currently supported 🚧"); + Commands::Web { + domain, + no_indent, + json_escape, + } => { + let private_jwk = Ed25519Generator::generate(); + let mut public_jwk = private_jwk.clone(); + public_jwk.d = None; + + let did_web = DidWeb::new(domain, public_jwk).unwrap(); + let portable_did = PortableDid { + did_uri: did_web.did.uri, + document: did_web.document, + private_jwks: vec![private_jwk], + }; + + print_portable_did(portable_did, no_indent, json_escape) } Commands::Dht { no_publish, diff --git a/docs/API_DESIGN.md b/docs/API_DESIGN.md index 40eaf3d9..cee790ca 100644 --- a/docs/API_DESIGN.md +++ b/docs/API_DESIGN.md @@ -2,7 +2,7 @@ **Last Updated** May 30, 2024 -**Version** 2.0.0 +**Version** 2.1.0 **[Custom DSL](./CUSTOM_DSL.md) Version**: 0.1.0 @@ -582,8 +582,13 @@ resolution_result = DidJwk.resolve(uri) ### `DidWeb` +> [!NOTE] +> +> The `CONSTRUCTOR(domain: string, public_jwk: Jwk)` does not publish the DID Document to a host, but merely creates the instance of the `did:web` in the local scope. + ```pseudocode! CLASS DidWeb + CONSTRUCTOR(domain: string, public_jwk: Jwk) CONSTRUCTOR(uri: string) STATIC METHOD resolve(uri: string): ResolutionResult ```