diff --git a/bindings/grpc/.dockerignore b/bindings/grpc/.dockerignore new file mode 100644 index 0000000000..906cab75d9 --- /dev/null +++ b/bindings/grpc/.dockerignore @@ -0,0 +1,2 @@ +target +tests diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml index 63b7edbf2d..a7237b21ce 100644 --- a/bindings/grpc/Cargo.toml +++ b/bindings/grpc/Cargo.toml @@ -20,6 +20,7 @@ path = "src/main.rs" anyhow = "1.0" futures = { version = "0.3" } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier" } +identity_ecdsa_verifier = { path = "../../identity_ecdsa_verifier" } identity_iota = { path = "../../identity_iota", features = [ "resolver", "sd-jwt", @@ -33,7 +34,7 @@ identity_stronghold = { path = "../../identity_stronghold", features = [ "send-sync-storage", ] } identity_sui_name_tbd = { path = "../../identity_sui_name_tbd" } -iota-sdk = { version = "1.1.2", features = ["stronghold"] } +iota-sdk = { version = "1.1.5", features = ["stronghold"] } iota-sdk-move = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk" } openssl = { version = "0.10", features = ["vendored"] } prost = "0.13" diff --git a/bindings/grpc/Dockerfile b/bindings/grpc/Dockerfile index b7faca7c63..57d97649a5 100644 --- a/bindings/grpc/Dockerfile +++ b/bindings/grpc/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:bookworm as builder +FROM rust:bookworm AS builder # install protobuf RUN apt-get update && apt-get install -y protobuf-compiler libprotobuf-dev musl-tools @@ -8,7 +8,7 @@ WORKDIR /usr/src/app/bindings/grpc RUN rustup target add x86_64-unknown-linux-musl RUN cargo build --target x86_64-unknown-linux-musl --release --bin identity-grpc -FROM gcr.io/distroless/static-debian11 as runner +FROM gcr.io/distroless/static-debian11 AS runner # get binary COPY --from=builder /usr/src/app/bindings/grpc/target/x86_64-unknown-linux-musl/release/identity-grpc / diff --git a/bindings/grpc/README.md b/bindings/grpc/README.md index f94f0add17..ac67e52ff1 100644 --- a/bindings/grpc/README.md +++ b/bindings/grpc/README.md @@ -21,6 +21,7 @@ Make sure to provide a valid stronghold snapshot at the provided `SNAPSHOT_PATH` | SD-JWT Validation | `sd_jwt/Verification.verify` | [sd_jwt.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/sd_jwt.proto) | | Credential JWT creation | `credentials/Jwt.create` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/credentials.proto) | | Credential JWT validation | `credentials/VcValidation.validate` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/credentials.proto) | +| Presentation JWT validation | `presentation/JwtPresentation.validate` | [presentation.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/presentation.proto) | | DID Document Creation | `document/DocumentService.create` | [document.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/document.proto) | | Domain Linkage - validate domain, let server fetch did-configuration | `domain_linkage/DomainLinkage.validate_domain` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/domain_linkage.proto) | | Domain Linkage - validate domain, pass did-configuration to service | `domain_linkage/DomainLinkage.validate_domain_against_did_configuration` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/domain_linkage.proto) | diff --git a/bindings/grpc/proto/presentation.proto b/bindings/grpc/proto/presentation.proto new file mode 100644 index 0000000000..9ee52f051d --- /dev/null +++ b/bindings/grpc/proto/presentation.proto @@ -0,0 +1,25 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; +package presentation; + +message JwtPresentationRequest { + // Presentation's compact JWT serialization. + string jwt = 1; +} + +message CredentialValidationResult { + oneof result { + string credential = 1; + string error = 2; + } +} + +message JwtPresentationResponse { + repeated CredentialValidationResult credentials = 1; +} + +service CredentialPresentation { + rpc validate(JwtPresentationRequest) returns (JwtPresentationResponse); +} \ No newline at end of file diff --git a/bindings/grpc/proto/utils.proto b/bindings/grpc/proto/utils.proto index 87ea3f7054..25b83df08c 100644 --- a/bindings/grpc/proto/utils.proto +++ b/bindings/grpc/proto/utils.proto @@ -9,6 +9,8 @@ message DataSigningRequest { bytes data = 1; // Signing key's ID. string key_id = 2; + // Key type of the key with id `key_id`. Valid values are: Ed25519, ES256, ES256K. + string key_type = 3; } message DataSigningResponse { @@ -21,3 +23,29 @@ service Signing { rpc sign(DataSigningRequest) returns (DataSigningResponse); } +message DidJwkResolutionRequest { + // did:jwk string + string did = 1; +} + +message DidJwkResolutionResponse { + // JSON DID Document + string doc = 1; +} + +service DidJwk { + rpc resolve(DidJwkResolutionRequest) returns (DidJwkResolutionResponse); +} + +message IotaDidToAliasAddressRequest { + string did = 1; +} + +message IotaDidToAliasAddressResponse { + string alias_address = 1; + string network = 2; +} + +service IotaUtils { + rpc did_iota_to_alias_address(IotaDidToAliasAddressRequest) returns (IotaDidToAliasAddressResponse); +} \ No newline at end of file diff --git a/bindings/grpc/src/lib.rs b/bindings/grpc/src/lib.rs index d26756e597..6c9dc38fad 100644 --- a/bindings/grpc/src/lib.rs +++ b/bindings/grpc/src/lib.rs @@ -5,3 +5,5 @@ pub mod server; pub mod services; +pub mod verifier; + diff --git a/bindings/grpc/src/services/credential/validation.rs b/bindings/grpc/src/services/credential/validation.rs index fd8cfd92bc..401b0ac01f 100644 --- a/bindings/grpc/src/services/credential/validation.rs +++ b/bindings/grpc/src/services/credential/validation.rs @@ -1,7 +1,6 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::FromJson; use identity_iota::core::Object; use identity_iota::core::ToJson; @@ -27,6 +26,8 @@ use tonic::Request; use tonic::Response; use tonic::Status; +use crate::verifier::Verifier; + mod _credentials { tonic::include_proto!("credentials"); } @@ -98,7 +99,7 @@ impl VcValidation for VcValidator { validation_option = validation_option.status_check(StatusCheck::SkipAll); } - let validator = JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); + let validator = JwtCredentialValidator::with_signature_verifier(Verifier::default()); let decoded_credential = validator .validate::<_, Object>(&jwt, &issuer_doc, &validation_option, FailFast::FirstError) .map_err(|mut e| match e.validation_errors.swap_remove(0) { diff --git a/bindings/grpc/src/services/document.rs b/bindings/grpc/src/services/document.rs index db50702c4a..31bcb40b10 100644 --- a/bindings/grpc/src/services/document.rs +++ b/bindings/grpc/src/services/document.rs @@ -78,7 +78,7 @@ impl DocumentService for DocumentSvc { let pub_key = self .storage .key_id_storage() - .get_public_key(&key_id) + .get_public_key_with_type(&key_id, identity_stronghold::StrongholdKeyType::Ed25519) .await .map_err(Error::StrongholdError)?; diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs index 560495c628..c8b40ae097 100644 --- a/bindings/grpc/src/services/domain_linkage.rs +++ b/bindings/grpc/src/services/domain_linkage.rs @@ -18,7 +18,6 @@ use _domain_linkage::ValidateDidResponse; use _domain_linkage::ValidateDomainAgainstDidConfigurationRequest; use _domain_linkage::ValidateDomainRequest; use _domain_linkage::ValidateDomainResponse; -use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::FromJson; use identity_iota::core::Url; use identity_iota::credential::DomainLinkageConfiguration; @@ -38,6 +37,8 @@ use tonic::Response; use tonic::Status; use url::Origin; +use crate::verifier::Verifier; + mod _domain_linkage { tonic::include_proto!("domain_linkage"); } @@ -276,14 +277,12 @@ impl DomainLinkageService { .for_each(|(credential, issuer_did_doc)| { let id = issuer_did_doc.id().to_string(); - if let Err(err) = JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default()) - .validate_linkage( - &issuer_did_doc, - &domain_linkage_configuration, - &domain, - &JwtCredentialValidationOptions::default(), - ) - { + if let Err(err) = JwtDomainLinkageValidator::with_signature_verifier(Verifier::default()).validate_linkage( + &issuer_did_doc, + &domain_linkage_configuration, + domain, + &JwtCredentialValidationOptions::default(), + ) { invalid_dids.push(InvalidDid { service_id: Some(id), credential: Some(credential.as_str().to_string()), diff --git a/bindings/grpc/src/services/mod.rs b/bindings/grpc/src/services/mod.rs index d352b8f858..51f4f3d872 100644 --- a/bindings/grpc/src/services/mod.rs +++ b/bindings/grpc/src/services/mod.rs @@ -5,6 +5,7 @@ pub mod credential; pub mod document; pub mod domain_linkage; pub mod health_check; +pub mod presentation; pub mod sd_jwt; pub mod status_list_2021; pub mod utils; @@ -22,7 +23,8 @@ pub fn routes(client: &IdentityClientReadOnly, stronghold: &StrongholdStorage) - routes.add_service(domain_linkage::service(client)); routes.add_service(document::service(client, stronghold)); routes.add_service(status_list_2021::service()); - routes.add_service(utils::service(stronghold)); + utils::init_services(&mut routes, stronghold); + routes.add_service(presentation::service(client)); routes.routes() } diff --git a/bindings/grpc/src/services/presentation.rs b/bindings/grpc/src/services/presentation.rs new file mode 100644 index 0000000000..50a86a7ac8 --- /dev/null +++ b/bindings/grpc/src/services/presentation.rs @@ -0,0 +1,164 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::verifier::Verifier; +use _presentation::credential_presentation_server::CredentialPresentation as PresentationService; +use _presentation::credential_presentation_server::CredentialPresentationServer; +use _presentation::credential_validation_result::Result as ValidationResult; +use _presentation::CredentialValidationResult; +use _presentation::JwtPresentationRequest; +use _presentation::JwtPresentationResponse; +use identity_iota::core::Object; +use identity_iota::core::ToJson; +use identity_iota::credential::CompoundJwtPresentationValidationError; +use identity_iota::credential::FailFast; +use identity_iota::credential::Jwt; +use identity_iota::credential::JwtCredentialValidationOptions; +use identity_iota::credential::JwtCredentialValidator; +use identity_iota::credential::JwtCredentialValidatorUtils; +use identity_iota::credential::JwtPresentationValidationOptions; +use identity_iota::credential::JwtPresentationValidator; +use identity_iota::credential::JwtPresentationValidatorUtils; +use identity_iota::credential::JwtValidationError; +use identity_iota::did::CoreDID; +use identity_iota::iota::IotaDocument; +use identity_iota::resolver::Error as ResolverError; +use identity_iota::resolver::Resolver; +use identity_sui_name_tbd::client::IdentityClientReadOnly; +use tonic::async_trait; +use tonic::Code; +use tonic::Request; +use tonic::Response; +use tonic::Status; + +mod _presentation { + tonic::include_proto!("presentation"); +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Invalid JWT presentation: {0}")] + InvalidJwtPresentation(#[source] JwtValidationError), + #[error("Resolution error: {0}")] + ResolutionError(#[source] ResolverError), + #[error("Presentation validation error: {0}")] + PresentationValidationError(#[source] CompoundJwtPresentationValidationError), + #[error("Failed to validate jwt credential: {0}")] + CredentialValidationError(#[source] anyhow::Error), +} + +impl From for Status { + fn from(value: Error) -> Self { + let code = match &value { + Error::InvalidJwtPresentation(_) => Code::InvalidArgument, + Error::ResolutionError(_) | Error::PresentationValidationError(_) | Error::CredentialValidationError(_) => { + Code::Internal + } + }; + + Status::new(code, value.to_string()) + } +} + +pub struct PresentationSvc { + resolver: Resolver, +} + +impl PresentationSvc { + pub fn new(client: IdentityClientReadOnly) -> Self { + let mut resolver = Resolver::::new_with_did_key_handler(); + resolver.attach_did_jwk_handler(); + resolver.attach_kinesis_iota_handler(client); + + Self { resolver } + } +} + +#[async_trait] +impl PresentationService for PresentationSvc { + async fn validate(&self, req: Request) -> Result, Status> { + let jwt_presentation = { + let JwtPresentationRequest { jwt } = req.into_inner(); + Jwt::new(jwt) + }; + + let holder_did = JwtPresentationValidatorUtils::extract_holder::(&jwt_presentation) + .map_err(Error::InvalidJwtPresentation)?; + let holder_doc = self + .resolver + .resolve(&holder_did) + .await + .map_err(Error::ResolutionError)?; + + let presentation_validator = JwtPresentationValidator::with_signature_verifier(Verifier::default()); + let mut decoded_presentation = presentation_validator + .validate::( + &jwt_presentation, + &holder_doc, + &JwtPresentationValidationOptions::default(), + ) + .map_err(Error::PresentationValidationError)?; + + let credentials = std::mem::take(&mut decoded_presentation.presentation.verifiable_credential); + let mut decoded_credentials = Vec::with_capacity(credentials.len()); + let credential_validator = JwtCredentialValidator::with_signature_verifier(Verifier::default()); + for credential_jwt in credentials { + let issuer_did = JwtCredentialValidatorUtils::extract_issuer_from_jwt::(&credential_jwt) + .map_err(|e| Error::CredentialValidationError(e.into())); + + if let Err(e) = issuer_did { + let validation_result = CredentialValidationResult { + result: Some(ValidationResult::Error(e.to_string())), + }; + decoded_credentials.push(validation_result); + continue; + } + let issuer_did = issuer_did.unwrap(); + + let issuer_doc = self + .resolver + .resolve(&issuer_did) + .await + .map_err(|e| Error::CredentialValidationError(e.into())); + + if let Err(e) = issuer_doc { + let validation_result = CredentialValidationResult { + result: Some(ValidationResult::Error(e.to_string())), + }; + decoded_credentials.push(validation_result); + continue; + } + let issuer_doc = issuer_doc.unwrap(); + + let validation_result = match credential_validator + .validate::( + &credential_jwt, + &issuer_doc, + &JwtCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .map_err(|e| Error::CredentialValidationError(e.into())) + { + Ok(decoded_credential) => ValidationResult::Credential( + decoded_credential + .credential + .to_json() + .map_err(|e| Status::internal(e.to_string()))?, + ), + Err(e) => ValidationResult::Error(e.to_string()), + }; + + decoded_credentials.push(CredentialValidationResult { + result: Some(validation_result), + }) + } + + Ok(Response::new(JwtPresentationResponse { + credentials: decoded_credentials, + })) + } +} + +pub fn service(client: &IdentityClientReadOnly) -> CredentialPresentationServer { + CredentialPresentationServer::new(PresentationSvc::new(client.clone())) +} diff --git a/bindings/grpc/src/services/sd_jwt.rs b/bindings/grpc/src/services/sd_jwt.rs index 95263f8c87..afccf53bcb 100644 --- a/bindings/grpc/src/services/sd_jwt.rs +++ b/bindings/grpc/src/services/sd_jwt.rs @@ -5,7 +5,6 @@ use _sd_jwt::verification_server::Verification; use _sd_jwt::verification_server::VerificationServer; use _sd_jwt::VerificationRequest; use _sd_jwt::VerificationResponse; -use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::Object; use identity_iota::core::Timestamp; use identity_iota::core::ToJson; @@ -25,6 +24,8 @@ use serde::Deserialize; use serde::Serialize; use thiserror::Error; +use crate::verifier::Verifier; + use self::_sd_jwt::KeyBindingOptions; mod _sd_jwt { @@ -125,7 +126,7 @@ impl Verification for SdJwtService { sd_jwt.jwt = jwt.into(); let decoder = SdObjectDecoder::new_with_sha256(); - let validator = SdJwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default(), decoder); + let validator = SdJwtCredentialValidator::with_signature_verifier(Verifier::default(), decoder); let credential = validator .validate_credential::<_, Object>( &sd_jwt, diff --git a/bindings/grpc/src/services/utils.rs b/bindings/grpc/src/services/utils.rs index 0e7d2fc570..168a6359a6 100644 --- a/bindings/grpc/src/services/utils.rs +++ b/bindings/grpc/src/services/utils.rs @@ -1,17 +1,41 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use _utils::did_jwk_server::DidJwk as DidJwkSvc; +use _utils::did_jwk_server::DidJwkServer; +use _utils::iota_utils_server::IotaUtils as IotaUtilsSvc; +use _utils::iota_utils_server::IotaUtilsServer; use _utils::signing_server::Signing as SigningSvc; use _utils::signing_server::SigningServer; use _utils::DataSigningRequest; use _utils::DataSigningResponse; +use _utils::DidJwkResolutionRequest; +use _utils::DidJwkResolutionResponse; +use _utils::IotaDidToAliasAddressRequest; +use _utils::IotaDidToAliasAddressResponse; +use anyhow::Context; +use identity_iota::core::ToJson; +use identity_iota::did::CoreDID; +use identity_iota::document::DocumentBuilder; +use identity_iota::iota::IotaDID; use identity_iota::storage::JwkStorage; use identity_iota::storage::KeyId; use identity_iota::storage::KeyStorageError; +use identity_iota::storage::KeyType; +use identity_iota::verification::jwk::Jwk; +use identity_iota::verification::jwu::decode_b64_json; +use identity_iota::verification::VerificationMethod; +use identity_stronghold::StrongholdKeyType; use identity_stronghold::StrongholdStorage; +use iota_sdk::types::block::address::AliasAddress; +use iota_sdk::types::block::address::Hrp; +use iota_sdk::types::block::address::ToBech32Ext as _; +use iota_sdk::types::block::output::AliasId; +use tonic::async_trait; use tonic::Request; use tonic::Response; use tonic::Status; +use tonic::service::RoutesBuilder; mod _utils { tonic::include_proto!("utils"); @@ -49,9 +73,17 @@ impl SigningSvc for SigningService { err, )] async fn sign(&self, req: Request) -> Result, Status> { - let DataSigningRequest { data, key_id } = req.into_inner(); + let DataSigningRequest { data, key_id, key_type } = req.into_inner(); let key_id = KeyId::new(key_id); - let public_key_jwk = self.storage.get_public_key(&key_id).await.map_err(Error)?; + let key_type = { + let key_type = KeyType::new(key_type); + StrongholdKeyType::try_from(&key_type).map_err(|e| Status::invalid_argument(e.to_string()))? + }; + let public_key_jwk = self + .storage + .get_public_key_with_type(&key_id, key_type) + .await + .map_err(Error)?; let signature = self .storage .sign(&key_id, &data, &public_key_jwk) @@ -62,6 +94,86 @@ impl SigningSvc for SigningService { } } -pub fn service(stronghold: &StrongholdStorage) -> SigningServer { - SigningServer::new(SigningService::new(stronghold)) +pub fn init_services(routes: &mut RoutesBuilder, stronghold: &StrongholdStorage) { + routes.add_service(SigningServer::new(SigningService::new(stronghold))); + routes.add_service(DidJwkServer::new(DidJwkService {})); + routes.add_service(IotaUtilsServer::new(IotaUtils {})); +} + +#[derive(Debug)] +pub struct DidJwkService {} + +#[tonic::async_trait] +impl DidJwkSvc for DidJwkService { + #[tracing::instrument( + name = "utils/resolve_did_jwk", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn resolve(&self, req: Request) -> Result, Status> { + let DidJwkResolutionRequest { did } = req.into_inner(); + let jwk = parse_did_jwk(&did).map_err(|e| Status::invalid_argument(e.to_string()))?; + let did = CoreDID::parse(did).expect("valid did:jwk"); + let verification_method = + VerificationMethod::new_from_jwk(did.clone(), jwk, Some("0")).map_err(|e| Status::internal(e.to_string()))?; + let verification_method_id = verification_method.id().clone(); + let doc = DocumentBuilder::default() + .id(did) + .verification_method(verification_method) + .assertion_method(verification_method_id.clone()) + .authentication(verification_method_id.clone()) + .capability_invocation(verification_method_id.clone()) + .capability_delegation(verification_method_id.clone()) + .key_agreement(verification_method_id) + .build() + .map_err(|e| Status::internal(e.to_string()))?; + + Ok(Response::new(DidJwkResolutionResponse { + doc: doc.to_json().map_err(|e| Status::internal(e.to_string()))?, + })) + } +} + +fn parse_did_jwk(did: &str) -> anyhow::Result { + let did_parts: [&str; 3] = did + .split(':') + .collect::>() + .try_into() + .map_err(|_| anyhow::anyhow!("invalid did:jwk \"{did}\""))?; + + match did_parts { + ["did", "jwk", data] => decode_b64_json(data).context("failed to deserialize JWK"), + _ => anyhow::bail!("invalid did:jwk string \"{did}\""), + } +} + +#[derive(Debug)] +struct IotaUtils; + +#[async_trait] +impl IotaUtilsSvc for IotaUtils { + #[tracing::instrument( + name = "utils/iota_did_to_alias_address", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn did_iota_to_alias_address( + &self, + req: Request, + ) -> Result, Status> { + let IotaDidToAliasAddressRequest { did } = req.into_inner(); + let iota_did = IotaDID::try_from(did).map_err(|e| Status::invalid_argument(format!("invalid iota did: {e}")))?; + let network = iota_did.network_str().to_string(); + let alias_address = AliasAddress::new(AliasId::from(&iota_did)); + let alias_bech32 = alias_address.to_bech32_unchecked(Hrp::from_str_unchecked(&network)); + + Ok(Response::new(IotaDidToAliasAddressResponse { + alias_address: alias_bech32.to_string(), + network, + })) + } } diff --git a/bindings/grpc/src/verifier.rs b/bindings/grpc/src/verifier.rs new file mode 100644 index 0000000000..7aa1b4a574 --- /dev/null +++ b/bindings/grpc/src/verifier.rs @@ -0,0 +1,30 @@ +use identity_ecdsa_verifier::EcDSAJwsVerifier; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota::verification::{ + jwk::Jwk, + jws::{JwsAlgorithm, JwsVerifier, SignatureVerificationError, SignatureVerificationErrorKind, VerificationInput}, +}; + +#[derive(Debug, Default)] +pub struct Verifier { + eddsa: EdDSAJwsVerifier, + ecdsa: EcDSAJwsVerifier, +} + +impl Verifier { + pub fn new() -> Self { + Self::default() + } +} + +impl JwsVerifier for Verifier { + fn verify(&self, input: VerificationInput, public_key: &Jwk) -> Result<(), SignatureVerificationError> { + match input.alg { + JwsAlgorithm::EdDSA => self.eddsa.verify(input, public_key), + JwsAlgorithm::ES256 | JwsAlgorithm::ES256K => self.ecdsa.verify(input, public_key), + _ => Err(SignatureVerificationError::new( + SignatureVerificationErrorKind::UnsupportedAlg, + )), + } + } +} diff --git a/bindings/grpc/tests/api/domain_linkage.rs b/bindings/grpc/tests/api/domain_linkage.rs index 05819710df..d7b7fd5212 100644 --- a/bindings/grpc/tests/api/domain_linkage.rs +++ b/bindings/grpc/tests/api/domain_linkage.rs @@ -170,7 +170,7 @@ async fn can_validate_did() -> anyhow::Result<()> { credential: jwt.as_str().to_string(), }; - let error = format!("could not get domain linkage config: domain linkage error: error sending request for url ({}.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known", domain2.to_string()); + let error = format!("could not get domain linkage config: domain linkage error: error sending request for url ({domain2}.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known"); let invalid_domain = InvalidDomain { service_id: service_id.clone(), diff --git a/bindings/grpc/tests/api/helpers.rs b/bindings/grpc/tests/api/helpers.rs index 5ff66a6326..1c92321088 100644 --- a/bindings/grpc/tests/api/helpers.rs +++ b/bindings/grpc/tests/api/helpers.rs @@ -290,7 +290,10 @@ impl Entity { if let Some(doc) = new_doc.take() { let Entity { storage, .. } = self; - let public_key = storage.key_id_storage().get_public_key(&key_id).await?; + let public_key = storage + .key_id_storage() + .get_public_key_with_type(&key_id, identity_stronghold::StrongholdKeyType::Ed25519) + .await?; let signer = StorageSigner::new(storage, key_id.clone(), public_key); let identity_client = IdentityClient::new(client.clone(), signer).await?; diff --git a/bindings/grpc/tests/api/utils.rs b/bindings/grpc/tests/api/utils.rs index 9b320bd154..aff9875cbb 100644 --- a/bindings/grpc/tests/api/utils.rs +++ b/bindings/grpc/tests/api/utils.rs @@ -16,7 +16,7 @@ mod _utils { tonic::include_proto!("utils"); } -const SAMPLE_SIGNING_DATA: &'static [u8] = b"I'm just some random data to be signed :)"; +const SAMPLE_SIGNING_DATA: &[u8] = b"I'm just some random data to be signed :)"; #[tokio::test] async fn raw_data_signing_works() -> anyhow::Result<()> { @@ -40,6 +40,7 @@ async fn raw_data_signing_works() -> anyhow::Result<()> { .sign(DataSigningRequest { data: SAMPLE_SIGNING_DATA.to_owned(), key_id: key_id.to_string(), + key_type: "Ed25519".to_string(), }) .await? .into_inner() diff --git a/identity_did/Cargo.toml b/identity_did/Cargo.toml index 473ffc8860..f1a45f837e 100644 --- a/identity_did/Cargo.toml +++ b/identity_did/Cargo.toml @@ -13,7 +13,7 @@ description = "Agnostic implementation of the Decentralized Identifiers (DID) st [dependencies] did_url_parser = { version = "0.2.0", features = ["std", "serde"] } form_urlencoded = { version = "1.2.0", default-features = false, features = ["alloc"] } -identity_core = { version = "=1.4.0", path = "../identity_core", default-features = false } +identity_core = { version = "=1.4.0", path = "../identity_core" } identity_jose = { version = "=1.4.0", path = "../identity_jose" } serde.workspace = true strum.workspace = true diff --git a/identity_did/src/did_jwk.rs b/identity_did/src/did_jwk.rs index 5ebd61021c..ec3352c4d8 100644 --- a/identity_did/src/did_jwk.rs +++ b/identity_did/src/did_jwk.rs @@ -19,10 +19,10 @@ use crate::DID; pub struct DIDJwk(CoreDID); impl DIDJwk { - /// [`DIDJwk`]'s method. + /// [`DIDKey`]'s method. pub const METHOD: &'static str = "jwk"; - /// Tries to parse a [`DIDJwk`] from a string. + /// Tries to parse a [`DIDKey`] from a string. pub fn parse(s: &str) -> Result { s.parse() } diff --git a/identity_did/src/did_key.rs b/identity_did/src/did_key.rs new file mode 100644 index 0000000000..07daf7f6b9 --- /dev/null +++ b/identity_did/src/did_key.rs @@ -0,0 +1,131 @@ +use std::fmt::Debug; +use std::fmt::Display; +use std::str::FromStr; + +use crate::CoreDID; +use crate::DIDUrl; +use crate::Error; +use crate::DID; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +#[repr(transparent)] +#[serde(into = "DIDUrl", try_from = "DIDUrl")] +/// A type representing a `did:key` DID. +pub struct DIDKey(DIDUrl); + +impl DIDKey { + /// [`DIDKey`]'s method. + pub const METHOD: &'static str = "key"; + + /// Tries to parse a [`DIDKey`] from a string. + pub fn parse(s: &str) -> Result { + s.parse() + } + + /// Returns this [`DIDKey`]'s optional fragment. + pub fn fragment(&self) -> Option<&str> { + self.0.fragment() + } + + /// Sets the fragment of this [`DIDKey`]. + pub fn set_fragment(&mut self, fragment: Option<&str>) -> Result<(), Error> { + self.0.set_fragment(fragment) + } +} + +impl AsRef for DIDKey { + fn as_ref(&self) -> &CoreDID { + self.0.did() + } +} + +impl From for CoreDID { + fn from(value: DIDKey) -> Self { + value.0.did().clone() + } +} + +impl<'a> TryFrom<&'a str> for DIDKey { + type Error = Error; + fn try_from(value: &'a str) -> Result { + value.parse() + } +} + +impl TryFrom for DIDKey { + type Error = Error; + fn try_from(value: DIDUrl) -> Result { + if value.did().method() != Self::METHOD { + Err(Error::InvalidMethodName) + } else if value.path().is_some() { + Err(Error::InvalidPath) + } else if value.query().is_some() { + Err(Error::InvalidQuery) + } else if !value.did().method_id().starts_with('z') { + Err(Error::InvalidMethodId) + } else { + Ok(Self(value)) + } + } +} + +impl Display for DIDKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for DIDKey { + type Err = Error; + fn from_str(s: &str) -> Result { + s.parse::().and_then(TryFrom::try_from) + } +} + +impl From for String { + fn from(value: DIDKey) -> Self { + value.to_string() + } +} + +impl TryFrom for DIDKey { + type Error = Error; + fn try_from(value: CoreDID) -> Result { + DIDUrl::new(value, None).try_into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_deserialization() -> Result<(), Error> { + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp".parse::()?; + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#afragment".parse::()?; + + Ok(()) + } + + #[test] + fn test_invalid_serialization() { + assert!( + "did:iota:0xf4d6f08f5a1b80dd578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a" + .parse::() + .is_err() + ); + assert!("did:key:".parse::().is_err()); + assert!("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp/" + .parse::() + .is_err()); + assert!("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp/somepath" + .parse::() + .is_err()); + assert!("did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp?somequery" + .parse::() + .is_err()); + assert!("did:key:6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + .parse::() + .is_err()); + } +} diff --git a/identity_did/src/lib.rs b/identity_did/src/lib.rs index 62c846847e..038feead2f 100644 --- a/identity_did/src/lib.rs +++ b/identity_did/src/lib.rs @@ -19,6 +19,7 @@ #[allow(clippy::module_inception)] mod did; mod did_jwk; +mod did_key; mod did_url; mod error; @@ -28,4 +29,5 @@ pub use ::did_url_parser::DID as BaseDIDUrl; pub use did::CoreDID; pub use did::DID; pub use did_jwk::*; +pub use did_key::*; pub use error::Error; diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 2747f7fae6..de9c280371 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -29,6 +29,7 @@ use crate::utils::DIDUrlQuery; use crate::utils::Queryable; use crate::verifiable::JwsVerificationOptions; use identity_did::CoreDID; +use identity_did::DIDKey; use identity_did::DIDUrl; use identity_verification::MethodRef; use identity_verification::MethodRelationship; @@ -985,7 +986,20 @@ impl CoreDocument { } } +// did:* expansion impl CoreDocument { + /// Creates a [`CoreDocument`] from a did:key DID. + pub fn expand_did_key(did_key: DIDKey) -> Result { + Self::builder(Object::default()) + .id(did_key.clone().into()) + .verification_method(VerificationMethod::try_from(did_key.clone()).map_err(Error::InvalidKeyMaterial)?) + .authentication(MethodRef::Refer(did_key.clone().into())) + .capability_delegation(MethodRef::Refer(did_key.clone().into())) + .capability_invocation(MethodRef::Refer(did_key.clone().into())) + .assertion_method(MethodRef::Refer(did_key.into())) + .build() + } + /// Creates a [`CoreDocument`] from a did:jwk DID. pub fn expand_did_jwk(did_jwk: DIDJwk) -> Result { let verification_method = VerificationMethod::try_from(did_jwk.clone()).map_err(Error::InvalidKeyMaterial)?; @@ -1701,6 +1715,40 @@ mod tests { } } + #[test] + fn test_did_key_expansion() { + let did_key = "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + .parse::() + .unwrap(); + let target_doc = serde_json::from_value(serde_json::json!({ + "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "verificationMethod": [{ + "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "type": "JsonWebKey", + "controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik" + } + }], + "authentication": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + ], + "assertionMethod": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + ], + "capabilityDelegation": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + ], + "capabilityInvocation": [ + "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + ] + })).unwrap(); + + assert_eq!(CoreDocument::expand_did_key(did_key).unwrap(), target_doc); + } + #[test] fn test_did_jwk_expansion() { let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9" diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index 39e560b1c7..84c4dd07f1 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -5,6 +5,7 @@ use core::future::Future; use futures::stream::FuturesUnordered; use futures::TryStreamExt; use identity_did::DIDJwk; +use identity_did::DIDKey; use identity_did::DID; use std::collections::HashSet; @@ -249,6 +250,36 @@ impl Resolver> { } impl + 'static> Resolver> { + /// Creates a new [`Resolver`] with a default handler for `did:key` DIDs. + pub fn new_with_did_key_handler() -> Self { + let mut command_map = HashMap::new(); + let handler = |did_key: DIDKey| async move { CoreDocument::expand_did_key(did_key) }; + + command_map.insert(DIDKey::METHOD.to_string(), SingleThreadedCommand::new(handler)); + Self { + command_map, + _required: PhantomData::, + } + } + + /// Attaches a handler capable of resolving `did:key` DIDs. + pub fn attach_did_key_handler(&mut self) { + let handler = |did_key: DIDKey| async move { CoreDocument::expand_did_key(did_key) }; + self.attach_handler(DIDKey::METHOD.to_string(), handler) + } + + /// Creates a new [`Resolver`] with a default handler for `did:jwk` DIDs. + pub fn new_with_did_jwk_handler() -> Self { + let mut command_map = HashMap::new(); + let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) }; + + command_map.insert(DIDJwk::METHOD.to_string(), SingleThreadedCommand::new(handler)); + Self { + command_map, + _required: PhantomData::, + } + } + /// Attaches a handler capable of resolving `did:jwk` DIDs. pub fn attach_did_jwk_handler(&mut self) { let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) }; @@ -257,6 +288,36 @@ impl + 'static> Resolver } impl + 'static> Resolver> { + /// Creates a new [`Resolver`] with a default handler for `did:jwk` DIDs. + pub fn new_with_did_key_handler() -> Self { + let mut command_map = HashMap::new(); + let handler = |did_key: DIDKey| async move { CoreDocument::expand_did_key(did_key) }; + + command_map.insert(DIDKey::METHOD.to_string(), SendSyncCommand::new(handler)); + Self { + command_map, + _required: PhantomData::, + } + } + + /// Attaches a handler capable of resolving `did:key` DIDs. + pub fn attach_did_key_handler(&mut self) { + let handler = |did_key: DIDKey| async move { CoreDocument::expand_did_key(did_key) }; + self.attach_handler(DIDKey::METHOD.to_string(), handler) + } + + /// Creates a new [`Resolver`] with a default handler for `did:jwk` DIDs. + pub fn new_with_did_jwk_handler() -> Self { + let mut command_map = HashMap::new(); + let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) }; + + command_map.insert(DIDJwk::METHOD.to_string(), SendSyncCommand::new(handler)); + Self { + command_map, + _required: PhantomData::, + } + } + /// Attaches a handler capable of resolving `did:jwk` DIDs. pub fn attach_did_jwk_handler(&mut self) { let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) }; @@ -465,10 +526,19 @@ mod tests { } #[tokio::test] - async fn test_did_jwk_resolution() { - let mut resolver = Resolver::::new(); - resolver.attach_did_jwk_handler(); + async fn test_did_key_resolution() { + let resolver = Resolver::::new_with_did_key_handler(); + let did_key = "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + .parse::() + .unwrap(); + + let doc = resolver.resolve(&did_key).await.unwrap(); + assert_eq!(doc.id(), did_key.as_ref()); + } + #[tokio::test] + async fn test_did_jwk_resolution() { + let resolver = Resolver::::new_with_did_jwk_handler(); let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".parse::().unwrap(); let doc = resolver.resolve(&did_jwk).await.unwrap(); diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index b7c61a998f..dd226c4d10 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -24,6 +24,8 @@ rand = { version = "0.8.5", default-features = false, features = ["std", "std_rn tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"] } zeroize = { version = "1.6.0", default-features = false } zkryptium = { workspace = true, optional = true } +stronghold_ext = { git = "https://github.com/impierce/stronghold_ext", features = ["crypto"] } +anyhow = "1.0.82" [dev-dependencies] anyhow = "1.0.82" diff --git a/identity_stronghold/src/ecdsa.rs b/identity_stronghold/src/ecdsa.rs new file mode 100644 index 0000000000..03ffeaf56c --- /dev/null +++ b/identity_stronghold/src/ecdsa.rs @@ -0,0 +1,35 @@ +use identity_verification::{jwk::{EcCurve, Jwk, JwkParamsEc}, jws::JwsAlgorithm, jwu}; +use stronghold_ext::{Algorithm, Es256, Es256k, VerifyingKey}; +use anyhow::Context; + +pub fn es256_pk_bytes_to_jwk(pk_bytes: &[u8]) -> anyhow::Result { + let pk = ::VerifyingKey::from_slice(&pk_bytes)?; + let mut params = JwkParamsEc::new(); + + let pk_point = pk.to_encoded_point(false); + params.x = pk_point.x().context("missing x coordinate for point-encoded public key").map(jwu::encode_b64)?; + params.y = pk_point.y().context("missing y coordinate for point-encoded public key").map(jwu::encode_b64)?; + params.crv = EcCurve::P256.name().to_string(); + + let mut jwk = Jwk::from_params(params); + jwk.set_alg(JwsAlgorithm::ES256.name()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + + Ok(jwk) +} + +pub fn es256k_pk_bytes_to_jwk(pk_bytes: &[u8]) -> anyhow::Result { + let pk = ::VerifyingKey::from_slice(&pk_bytes)?; + let mut params = JwkParamsEc::new(); + + let pk_point = pk.to_encoded_point(false); + params.x = pk_point.x().context("missing x coordinate for point-encoded public key").map(jwu::encode_b64)?; + params.y = pk_point.y().context("missing y coordinate for point-encoded public key").map(jwu::encode_b64)?; + params.crv = EcCurve::Secp256K1.name().to_string(); + + let mut jwk = Jwk::from_params(params); + jwk.set_alg(JwsAlgorithm::ES256K.name()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + + Ok(jwk) +} \ No newline at end of file diff --git a/identity_stronghold/src/lib.rs b/identity_stronghold/src/lib.rs index ae8f8aef5b..cc82eb60e6 100644 --- a/identity_stronghold/src/lib.rs +++ b/identity_stronghold/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod ed25519; +pub(crate) mod ecdsa; mod storage; pub(crate) mod stronghold_key_type; #[cfg(test)] diff --git a/identity_stronghold/src/storage/mod.rs b/identity_stronghold/src/storage/mod.rs index cb02b9274b..ed7024ae3c 100644 --- a/identity_stronghold/src/storage/mod.rs +++ b/identity_stronghold/src/storage/mod.rs @@ -31,12 +31,15 @@ use iota_stronghold::Location; use iota_stronghold::Stronghold; #[cfg(feature = "bbs-plus")] use jsonprooftoken::jpa::algs::ProofAlgorithm; +use stronghold_ext::{execute_procedure_ext, procs::es256, procs::es256k}; use tokio::sync::MutexGuard; #[cfg(feature = "bbs-plus")] use zeroize::Zeroizing; #[cfg(feature = "bbs-plus")] use zkryptium::bbsplus::keys::BBSplusSecretKey; +use crate::ecdsa::es256_pk_bytes_to_jwk; +use crate::ecdsa::es256k_pk_bytes_to_jwk; use crate::stronghold_key_type::StrongholdKeyType; use crate::utils::get_client; use crate::utils::IDENTITY_VAULT_PATH; @@ -117,11 +120,45 @@ impl StrongholdStorage { .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).with_source(e)) } + async fn get_es256_public_key(&self, key_id: &KeyId) -> KeyStorageResult { + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + key_id.to_string().as_bytes().to_vec(), + ); + let procedure = es256::Es256Procs::PublicKey(es256::PublicKey { private_key: location }); + let pk_bytes: Vec = execute_procedure_ext(&client, procedure) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))? + .into(); + + es256_pk_bytes_to_jwk(&pk_bytes).map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e)) + } + + async fn get_es256k_public_key(&self, key_id: &KeyId) -> KeyStorageResult { + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + key_id.to_string().as_bytes().to_vec(), + ); + let procedure = es256k::Es256kProcs::PublicKey(es256k::PublicKey { private_key: location }); + let pk_bytes: Vec = execute_procedure_ext(&client, procedure) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))? + .into(); + + es256k_pk_bytes_to_jwk(&pk_bytes).map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e)) + } + /// Attepts to retrieve the public key corresponding to the key of id `key_id`, /// returning it as a `key_type` encoded public JWK. pub async fn get_public_key_with_type(&self, key_id: &KeyId, key_type: StrongholdKeyType) -> KeyStorageResult { match key_type { StrongholdKeyType::Ed25519 => self.get_ed25519_public_key(key_id).await, + StrongholdKeyType::Es256 => self.get_es256_public_key(key_id).await, + StrongholdKeyType::Es256k => self.get_es256k_public_key(key_id).await, #[cfg(feature = "bbs-plus")] StrongholdKeyType::Bls12381G2 => self.get_bls12381g2_public_key(key_id).await, #[allow(unreachable_patterns)] diff --git a/identity_stronghold/src/storage/stronghold_jwk_storage.rs b/identity_stronghold/src/storage/stronghold_jwk_storage.rs index efe0f6531b..89d3c9bd60 100644 --- a/identity_stronghold/src/storage/stronghold_jwk_storage.rs +++ b/identity_stronghold/src/storage/stronghold_jwk_storage.rs @@ -3,6 +3,7 @@ //! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). +use anyhow::Context; use async_trait::async_trait; use identity_storage::key_storage::JwkStorage; use identity_storage::JwkGenOutput; @@ -11,8 +12,11 @@ use identity_storage::KeyStorageError; use identity_storage::KeyStorageErrorKind; use identity_storage::KeyStorageResult; use identity_storage::KeyType; +use identity_verification::jwk::EcCurve; use identity_verification::jwk::EdCurve; use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkParams; +use identity_verification::jwk::JwkParamsEc; use identity_verification::jwk::JwkParamsOkp; use identity_verification::jws::JwsAlgorithm; use identity_verification::jwu; @@ -20,14 +24,109 @@ use iota_stronghold::procedures::Ed25519Sign; use iota_stronghold::procedures::GenerateKey; use iota_stronghold::procedures::KeyType as ProceduresKeyType; use iota_stronghold::procedures::StrongholdProcedure; +use iota_stronghold::Client; use iota_stronghold::Location; +use stronghold_ext::Algorithm; +use stronghold_ext::Es256k; use std::str::FromStr; +use stronghold_ext::execute_procedure_ext; +use stronghold_ext::procs::es256::Es256Procs; +use stronghold_ext::procs::es256::GenerateKey as Es256GenKey; +use stronghold_ext::procs::es256::PublicKey as Es256PK; +use stronghold_ext::procs::es256::Sign as Es256Sign; +use stronghold_ext::procs::es256k::Es256kProcs; +use stronghold_ext::procs::es256k::GenerateKey as Es256kGenKey; +use stronghold_ext::procs::es256k::PublicKey as Es256kPK; +use stronghold_ext::procs::es256k::Sign as Es256kSign; +use stronghold_ext::Es256; +use stronghold_ext::VerifyingKey; use crate::ed25519; use crate::stronghold_key_type::StrongholdKeyType; use crate::utils::*; use crate::StrongholdStorage; +fn gen_ed25519(client: &Client, location: Location) -> KeyStorageResult { + let generate_key_procedure = GenerateKey { + ty: ProceduresKeyType::Ed25519, + output: location.clone(), + }; + + client + .execute_procedure(StrongholdProcedure::GenerateKey(generate_key_procedure)) + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("stronghold generate key procedure failed") + .with_source(err) + })?; + + let public_key_procedure = iota_stronghold::procedures::PublicKey { + ty: ProceduresKeyType::Ed25519, + private_key: location, + }; + + let procedure_result = client + .execute_procedure(StrongholdProcedure::PublicKey(public_key_procedure)) + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("stronghold public key procedure failed") + .with_source(err) + })?; + + let public_key: Vec = procedure_result.into(); + let mut params = JwkParamsOkp::new(); + params.x = jwu::encode_b64(public_key); + params.crv = EdCurve::Ed25519.name().to_string(); + + Ok(params.into()) +} + +fn gen_es256(client: &Client, location: Location) -> KeyStorageResult { + execute_procedure_ext( + client, + Es256Procs::GenerateKey(Es256GenKey { + output: location.clone(), + }), + ) + .and_then(|_| execute_procedure_ext(client, Es256Procs::PublicKey(Es256PK { private_key: location }))) + .context("stronghold's procedure execution failed") + .and_then(|output| { + let pk_bytes: Vec = output.into(); + let pk = ::VerifyingKey::from_slice(&pk_bytes)?; + let mut params = JwkParamsEc::new(); + + let pk_point = pk.to_encoded_point(false); + params.x = pk_point.x().context("missing x coordinate for point-encoded public key").map(jwu::encode_b64)?; + params.y = pk_point.y().context("missing y coordinate for point-encoded public key").map(jwu::encode_b64)?; + params.crv = EcCurve::P256.name().to_string(); + Ok(params.into()) + }) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e)) +} + +fn gen_es256k(client: &Client, location: Location) -> KeyStorageResult { + execute_procedure_ext( + client, + Es256kProcs::GenerateKey(Es256kGenKey { + output: location.clone(), + }), + ) + .and_then(|_| execute_procedure_ext(client, Es256kProcs::PublicKey(Es256kPK { private_key: location }))) + .context("stronghold's procedure execution failed") + .and_then(|output| { + let pk_bytes: Vec = output.into(); + let pk = ::VerifyingKey::from_slice(&pk_bytes)?; + let mut params = JwkParamsEc::new(); + + let pk_point = pk.to_encoded_point(false); + params.x = pk_point.x().context("missing x coordinate for point-encoded public key").map(jwu::encode_b64)?; + params.y = pk_point.y().context("missing y coordinate for point-encoded public key").map(jwu::encode_b64)?; + params.crv = EcCurve::Secp256K1.name().to_string(); + Ok(params.into()) + }) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e)) +} + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] impl JwkStorage for StrongholdStorage { @@ -38,54 +137,20 @@ impl JwkStorage for StrongholdStorage { let key_type = StrongholdKeyType::try_from(&key_type)?; check_key_alg_compatibility(key_type, &alg)?; - let keytype: ProceduresKeyType = match key_type { - StrongholdKeyType::Ed25519 => ProceduresKeyType::Ed25519, - StrongholdKeyType::Bls12381G2 => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "`{key_type}` is supported but `JwkStorageBbsPlusExt::generate_bbs` should be called instead." - )), - ) - } - }; - let key_id: KeyId = random_key_id(); let location = Location::generic( IDENTITY_VAULT_PATH.as_bytes().to_vec(), key_id.to_string().as_bytes().to_vec(), ); - let generate_key_procedure = GenerateKey { - ty: keytype.clone(), - output: location.clone(), + let params = match key_type { + StrongholdKeyType::Ed25519 => gen_ed25519(&client, location)?, + StrongholdKeyType::Es256 => gen_es256(&client, location)?, + StrongholdKeyType::Es256k => gen_es256k(&client, location)?, + _ => return Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), }; - - client - .execute_procedure(StrongholdProcedure::GenerateKey(generate_key_procedure)) - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("stronghold generate key procedure failed") - .with_source(err) - })?; - - let public_key_procedure = iota_stronghold::procedures::PublicKey { - ty: keytype, - private_key: location, - }; - - let procedure_result = client - .execute_procedure(StrongholdProcedure::PublicKey(public_key_procedure)) - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("stronghold public key procedure failed") - .with_source(err) - })?; - let public_key: Vec = procedure_result.into(); persist_changes(self.as_secret_manager(), stronghold).await?; - let mut params = JwkParamsOkp::new(); - params.x = jwu::encode_b64(public_key); - params.crv = EdCurve::Ed25519.name().to_string(); let mut jwk: Jwk = Jwk::from_params(params); jwk.set_alg(alg.name()); jwk.set_kid(jwk.thumbprint_sha256_b64()); @@ -147,6 +212,14 @@ impl JwkStorage for StrongholdStorage { JwsAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedSignatureAlgorithm) })?; + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + key_id.to_string().as_bytes().to_vec(), + ); + // Check that `kty` is `Okp` and `crv = Ed25519`. match alg { JwsAlgorithm::EdDSA => { @@ -164,6 +237,29 @@ impl JwkStorage for StrongholdStorage { ); } } + JwsAlgorithm::ES256 => { + return execute_procedure_ext( + &client, + Es256Procs::Sign(Es256Sign { + msg: data.to_vec(), + private_key: location, + }), + ) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e)) + .map(Into::into); + } + + JwsAlgorithm::ES256K => { + return execute_procedure_ext( + &client, + Es256kProcs::Sign(Es256kSign { + msg: data.to_vec(), + private_key: location, + }), + ) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e)) + .map(Into::into); + } other => { return Err( KeyStorageError::new(KeyStorageErrorKind::UnsupportedSignatureAlgorithm) @@ -172,18 +268,11 @@ impl JwkStorage for StrongholdStorage { } }; - let location = Location::generic( - IDENTITY_VAULT_PATH.as_bytes().to_vec(), - key_id.to_string().as_bytes().to_vec(), - ); let procedure: Ed25519Sign = Ed25519Sign { private_key: location, msg: data.to_vec(), }; - let stronghold = self.get_stronghold().await; - let client = get_client(&stronghold)?; - let signature: [u8; 64] = client.execute_procedure(procedure).map_err(|err| { KeyStorageError::new(KeyStorageErrorKind::Unspecified) .with_custom_message("stronghold Ed25519Sign procedure failed") diff --git a/identity_stronghold/src/stronghold_key_type.rs b/identity_stronghold/src/stronghold_key_type.rs index c78deb4d3a..2c49626c59 100644 --- a/identity_stronghold/src/stronghold_key_type.rs +++ b/identity_stronghold/src/stronghold_key_type.rs @@ -7,6 +7,7 @@ use identity_storage::KeyStorageError; use identity_storage::KeyStorageErrorKind; use identity_storage::KeyType; use identity_verification::jwk::BlsCurve; +use identity_verification::jwk::EcCurve; use identity_verification::jwk::EdCurve; use identity_verification::jwk::Jwk; use identity_verification::jwk::JwkType; @@ -23,14 +24,18 @@ pub const BLS12381G2_KEY_TYPE: KeyType = KeyType::from_static_str(BLS12381G2_KEY pub enum StrongholdKeyType { Ed25519, Bls12381G2, + Es256, + Es256k, } impl StrongholdKeyType { /// String representation of the key type. const fn name(&self) -> &'static str { match self { - StrongholdKeyType::Ed25519 => ED25519_KEY_TYPE_STR, - StrongholdKeyType::Bls12381G2 => BLS12381G2_KEY_TYPE_STR, + Self::Ed25519 => ED25519_KEY_TYPE_STR, + Self::Bls12381G2 => BLS12381G2_KEY_TYPE_STR, + Self::Es256 => "ES256", + Self::Es256k => "ES256K", } } } @@ -48,6 +53,8 @@ impl TryFrom<&KeyType> for StrongholdKeyType { match value.as_str() { ED25519_KEY_TYPE_STR => Ok(StrongholdKeyType::Ed25519), BLS12381G2_KEY_TYPE_STR => Ok(StrongholdKeyType::Bls12381G2), + "ES256" => Ok(Self::Es256), + "ES256K" => Ok(Self::Es256k), _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), } } @@ -88,16 +95,25 @@ impl TryFrom<&Jwk> for StrongholdKeyType { .with_custom_message("expected EC parameters for a JWK with `kty` Ec") .with_source(err) })?; - match ec_params.try_bls_curve().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message("only Ed curves are supported for signing") - .with_source(err) - })? { - BlsCurve::BLS12381G2 => Ok(StrongholdKeyType::Bls12381G2), - curve => Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("{curve} not supported")), - ), + if let Ok(bls_curve) = ec_params.try_bls_curve() { + match bls_curve { + BlsCurve::BLS12381G2 => Ok(StrongholdKeyType::Bls12381G2), + curve => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{curve} not supported")), + ), + } + } else if let Ok(ec_curve) = ec_params.try_ec_curve() { + match ec_curve { + EcCurve::P256 => Ok(StrongholdKeyType::Es256), + EcCurve::Secp256K1 => Ok(StrongholdKeyType::Es256k), + curve => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("unsupported EC curve \"{curve}\"")), + ), + } + } else { + Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_custom_message("invalid EC params")) } } other => Err( diff --git a/identity_stronghold/src/utils.rs b/identity_stronghold/src/utils.rs index 0bf83e1f18..dcaf480d7c 100644 --- a/identity_stronghold/src/utils.rs +++ b/identity_stronghold/src/utils.rs @@ -27,6 +27,8 @@ pub fn random_key_id() -> KeyId { pub fn check_key_alg_compatibility(key_type: StrongholdKeyType, alg: &JwsAlgorithm) -> KeyStorageResult<()> { match (key_type, alg) { (StrongholdKeyType::Ed25519, JwsAlgorithm::EdDSA) => Ok(()), + (_, JwsAlgorithm::ES256) => Ok(()), + (_, JwsAlgorithm::ES256K) => Ok(()), (key_type, alg) => Err( KeyStorageError::new(identity_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) .with_custom_message(format!("cannot use key type `{key_type}` with algorithm `{alg}`")), diff --git a/identity_verification/src/jose/mod.rs b/identity_verification/src/jose/mod.rs index 71afcb3fac..60dbdc0dfb 100644 --- a/identity_verification/src/jose/mod.rs +++ b/identity_verification/src/jose/mod.rs @@ -27,3 +27,51 @@ pub mod error { pub use identity_jose::error::*; } + +use error::Error; +use identity_core::convert::BaseEncoding; +use identity_did::DIDKey; +use identity_did::DID as _; +use identity_jose::jwk::EdCurve; +use identity_jose::jwk::JwkParamsOkp; +use identity_jose::jwu::encode_b64; +use jwk::Jwk; + +/// Transcode the public key in `did_key` to `JWK`. +pub fn did_key_to_jwk(did_key: &DIDKey) -> Result { + let decoded = + BaseEncoding::decode_multibase(did_key.method_id()).map_err(|_| Error::KeyError("key is not multibase encoded"))?; + let (key_type, pk_bytes) = decoded.split_at(2); + + // Make sure `did_key` encodes an ED25519 public key. + if key_type != [0xed, 0x01] || pk_bytes.len() != 32 { + return Err(Error::KeyError("invalid ED25519 key")); + } + + let mut params = JwkParamsOkp::new(); + params.crv = EdCurve::Ed25519.name().to_string(); + params.x = encode_b64(pk_bytes); + + Ok(Jwk::from_params(params)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_did_key_to_jwk() { + let target_jwk = serde_json::from_value(serde_json::json!({ + "kty": "OKP", + "crv": "Ed25519", + "x": "O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik" + })) + .unwrap(); + + let did_key = "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + .parse::() + .unwrap(); + let jwk = did_key_to_jwk(&did_key).unwrap(); + assert_eq!(jwk, target_jwk); + } +} diff --git a/identity_verification/src/verification_method/method.rs b/identity_verification/src/verification_method/method.rs index 084956c3a9..35cbab219a 100644 --- a/identity_verification/src/verification_method/method.rs +++ b/identity_verification/src/verification_method/method.rs @@ -6,6 +6,7 @@ use core::fmt::Formatter; use std::borrow::Cow; use identity_did::DIDJwk; +use identity_did::DIDKey; use identity_jose::jwk::Jwk; use serde::de; use serde::Deserialize; @@ -17,6 +18,7 @@ use identity_core::convert::FmtJson; use crate::error::Error; use crate::error::Result; +use crate::jose::did_key_to_jwk; use crate::verification_method::MethodBuilder; use crate::verification_method::MethodData; use crate::verification_method::MethodRef; @@ -248,6 +250,27 @@ impl KeyComparable for VerificationMethod { } } +impl TryFrom for VerificationMethod { + type Error = Error; + fn try_from(value: DIDKey) -> Result { + let mut id: DIDUrl = value.clone().into(); + let _ = id.set_fragment(Some(value.method_id())); + let controller = value.clone().into(); + let method_type = MethodType::JSON_WEB_KEY_2020; + let data = did_key_to_jwk(&value) + .map_err(|_| Error::InvalidKeyDataMultibase) + .map(MethodData::PublicKeyJwk)?; + + Ok(VerificationMethod { + id, + controller, + type_: method_type, + data, + properties: Object::default(), + }) + } +} + impl TryFrom for VerificationMethod { type Error = Error; fn try_from(did: DIDJwk) -> Result {