From 767cf73b0f46055ff2f80951a1c6abb291292aa1 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 15 May 2024 14:19:32 +0200 Subject: [PATCH 1/9] did:key DID implementation --- identity_did/src/did_key.rs | 122 ++++++++++++++++++++++++++++++++++++ identity_did/src/lib.rs | 1 + 2 files changed, 123 insertions(+) create mode 100644 identity_did/src/did_key.rs diff --git a/identity_did/src/did_key.rs b/identity_did/src/did_key.rs new file mode 100644 index 0000000000..5614828d92 --- /dev/null +++ b/identity_did/src/did_key.rs @@ -0,0 +1,122 @@ +use std::fmt::Debug; +use std::fmt::Display; +use std::str::FromStr; + +use crate::CoreDID; +use crate::DIDUrl; +use crate::Error; +use crate::DID; + +const METHOD: &str = "key"; + +#[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 { + /// 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 TryFrom for DIDKey { + type Error = Error; + fn try_from(value: DIDUrl) -> Result { + if value.did().method() != METHOD { + Err(Error::InvalidMethodName) + } else if value.path().is_some() { + Err(Error::InvalidPath) + } else if value.query().is_some() { + Err(Error::InvalidQuery) + } 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 { + if value.method() != METHOD { + return Err(Error::InvalidMethodName); + } + + Ok(Self(DIDUrl::new(value, None))) + } +} + +#[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()); + } +} diff --git a/identity_did/src/lib.rs b/identity_did/src/lib.rs index 9289419211..7c6f7f63e9 100644 --- a/identity_did/src/lib.rs +++ b/identity_did/src/lib.rs @@ -20,6 +20,7 @@ mod did; mod did_url; mod error; +mod did_key; pub use crate::did_url::DIDUrl; pub use crate::did_url::RelativeDIDUrl; From 8518a8e5be0bb4cea27c5ce3b3df2dc47a26055c Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 15 May 2024 16:36:17 +0200 Subject: [PATCH 2/9] did-key expansion to did doc --- identity_did/src/did_key.rs | 11 +++-- identity_did/src/lib.rs | 1 + .../src/document/core_document.rs | 49 +++++++++++++++++++ identity_verification/src/jose/mod.rs | 48 ++++++++++++++++++ .../src/verification_method/method.rs | 23 +++++++++ 5 files changed, 127 insertions(+), 5 deletions(-) diff --git a/identity_did/src/did_key.rs b/identity_did/src/did_key.rs index 5614828d92..c97629f5e9 100644 --- a/identity_did/src/did_key.rs +++ b/identity_did/src/did_key.rs @@ -53,6 +53,8 @@ impl TryFrom for DIDKey { 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)) } @@ -81,11 +83,7 @@ impl From for String { impl TryFrom for DIDKey { type Error = Error; fn try_from(value: CoreDID) -> Result { - if value.method() != METHOD { - return Err(Error::InvalidMethodName); - } - - Ok(Self(DIDUrl::new(value, None))) + DIDUrl::new(value, None).try_into() } } @@ -118,5 +116,8 @@ mod tests { 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 7c6f7f63e9..1cd2fef85d 100644 --- a/identity_did/src/lib.rs +++ b/identity_did/src/lib.rs @@ -28,3 +28,4 @@ pub use ::did_url_parser::DID as BaseDIDUrl; pub use did::CoreDID; pub use did::DID; pub use error::Error; +pub use did_key::DIDKey; diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 87fddd0fed..e9c6546edf 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -28,6 +28,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; @@ -984,6 +985,20 @@ impl CoreDocument { } } +// did:key expansion +impl CoreDocument { + 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() + } +} + #[cfg(test)] mod tests { use identity_core::convert::FromJson; @@ -1682,4 +1697,38 @@ mod tests { verifier(json); } } + + #[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); + } } diff --git a/identity_verification/src/jose/mod.rs b/identity_verification/src/jose/mod.rs index 71afcb3fac..306bad8aa2 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, DID as _}; +use identity_jose::{ + jwk::{EdCurve, JwkParamsOkp}, + 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 8c48e06893..fc11f4d789 100644 --- a/identity_verification/src/verification_method/method.rs +++ b/identity_verification/src/verification_method/method.rs @@ -5,6 +5,7 @@ use core::fmt::Display; use core::fmt::Formatter; use std::borrow::Cow; +use identity_did::DIDKey; use identity_jose::jwk::Jwk; use serde::de; use serde::Deserialize; @@ -16,6 +17,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; @@ -247,6 +249,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; + 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(), + }) + } +} + // Horrible workaround for a tracked serde issue https://github.com/serde-rs/serde/issues/2200. Serde doesn't "consume" // the input when deserializing flattened enums (MethodData in this case) causing duplication of data (in this case // it ends up in the properties object). This workaround simply removes the duplication. From 954b7746035bad7d714ddbc08596f04ef3036cf2 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 16 May 2024 09:43:25 +0200 Subject: [PATCH 3/9] did:key resolution --- identity_did/src/did_key.rs | 14 +++++-- identity_did/src/lib.rs | 2 +- .../src/document/iota_document.rs | 6 +++ identity_resolver/src/resolution/resolver.rs | 42 +++++++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/identity_did/src/did_key.rs b/identity_did/src/did_key.rs index c97629f5e9..07daf7f6b9 100644 --- a/identity_did/src/did_key.rs +++ b/identity_did/src/did_key.rs @@ -7,8 +7,6 @@ use crate::DIDUrl; use crate::Error; use crate::DID; -const METHOD: &str = "key"; - #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] #[repr(transparent)] #[serde(into = "DIDUrl", try_from = "DIDUrl")] @@ -16,6 +14,9 @@ const METHOD: &str = "key"; 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() @@ -44,10 +45,17 @@ impl From for CoreDID { } } +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() != METHOD { + if value.did().method() != Self::METHOD { Err(Error::InvalidMethodName) } else if value.path().is_some() { Err(Error::InvalidPath) diff --git a/identity_did/src/lib.rs b/identity_did/src/lib.rs index 1cd2fef85d..994fa6622d 100644 --- a/identity_did/src/lib.rs +++ b/identity_did/src/lib.rs @@ -28,4 +28,4 @@ pub use ::did_url_parser::DID as BaseDIDUrl; pub use did::CoreDID; pub use did::DID; pub use error::Error; -pub use did_key::DIDKey; +pub use did_key::*; diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index 7ae60381d7..703dbe897a 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -555,6 +555,12 @@ impl From for CoreDocument { } } +impl From for IotaDocument { + fn from(value: CoreDocument) -> Self { + IotaDocument { document: value, metadata: IotaDocumentMetadata::default() } + } +} + impl TryFrom<(CoreDocument, IotaDocumentMetadata)> for IotaDocument { type Error = Error; /// Converts the tuple into an [`IotaDocument`] if the given [`CoreDocument`] has an identifier satisfying the diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index b8ceffbc7f..33866d34ed 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -4,6 +4,7 @@ use core::future::Future; use futures::stream::FuturesUnordered; use futures::TryStreamExt; +use identity_did::DIDKey; use identity_did::DID; use std::collections::HashSet; @@ -247,6 +248,38 @@ 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).into() } + }; + + command_map.insert(DIDKey::METHOD.to_string(), SingleThreadedCommand::new(handler)); + Self { + command_map, + _required: PhantomData::, + } + } +} + +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).into() } + }; + + command_map.insert(DIDKey::METHOD.to_string(), SendSyncCommand::new(handler)); + Self { + command_map, + _required: PhantomData::, + } + } +} + #[cfg(feature = "iota")] mod iota_handler { use crate::ErrorCause; @@ -414,4 +447,13 @@ mod tests { let doc = resolver.resolve(&did2).await.unwrap(); assert_eq!(doc.id(), &did2); } + + #[tokio::test] + 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()); + } } From 2ccd8df82f4caafeb639a6c41d01ee8eeb3790be Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 16 May 2024 12:06:57 +0200 Subject: [PATCH 4/9] grpc presentation validation service --- bindings/grpc/README.md | 1 + bindings/grpc/proto/presentation.proto | 25 ++++ bindings/grpc/src/services/mod.rs | 2 + bindings/grpc/src/services/presentation.rs | 163 +++++++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 bindings/grpc/proto/presentation.proto create mode 100644 bindings/grpc/src/services/presentation.rs 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/src/services/mod.rs b/bindings/grpc/src/services/mod.rs index 00abe17ce1..8c0a533499 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; @@ -23,6 +24,7 @@ pub fn routes(client: &Client, stronghold: &StrongholdStorage) -> Routes { routes.add_service(document::service(client, stronghold)); routes.add_service(status_list_2021::service()); routes.add_service(utils::service(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..7f8ccf26e2 --- /dev/null +++ b/bindings/grpc/src/services/presentation.rs @@ -0,0 +1,163 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +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_eddsa_verifier::EdDSAJwsVerifier; +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 iota_sdk::client::Client; +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: Client) -> Self { + let mut resolver = Resolver::::new_with_did_key_handler(); + resolver.attach_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(EdDSAJwsVerifier::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(EdDSAJwsVerifier::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: &Client) -> CredentialPresentationServer { + CredentialPresentationServer::new(PresentationSvc::new(client.clone())) +} From f1756c0c66ba835bcfcce6bec966f00b4011faf3 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 25 Jul 2024 09:34:41 +0200 Subject: [PATCH 5/9] did:jwk resolution service --- bindings/grpc/proto/utils.proto | 13 ++++++ bindings/grpc/src/services/mod.rs | 2 +- bindings/grpc/src/services/utils.rs | 66 ++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/bindings/grpc/proto/utils.proto b/bindings/grpc/proto/utils.proto index 87ea3f7054..2caa61244f 100644 --- a/bindings/grpc/proto/utils.proto +++ b/bindings/grpc/proto/utils.proto @@ -21,3 +21,16 @@ 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); +} \ No newline at end of file diff --git a/bindings/grpc/src/services/mod.rs b/bindings/grpc/src/services/mod.rs index 8c0a533499..8a4f197792 100644 --- a/bindings/grpc/src/services/mod.rs +++ b/bindings/grpc/src/services/mod.rs @@ -23,7 +23,7 @@ pub fn routes(client: &Client, stronghold: &StrongholdStorage) -> Routes { 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/utils.rs b/bindings/grpc/src/services/utils.rs index 0e7d2fc570..a8214b332a 100644 --- a/bindings/grpc/src/services/utils.rs +++ b/bindings/grpc/src/services/utils.rs @@ -1,14 +1,26 @@ // 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::signing_server::Signing as SigningSvc; use _utils::signing_server::SigningServer; use _utils::DataSigningRequest; use _utils::DataSigningResponse; +use _utils::DidJwkResolutionRequest; +use _utils::DidJwkResolutionResponse; +use anyhow::Context; +use identity_iota::core::ToJson; +use identity_iota::did::CoreDID; +use identity_iota::document::DocumentBuilder; use identity_iota::storage::JwkStorage; use identity_iota::storage::KeyId; use identity_iota::storage::KeyStorageError; +use identity_iota::verification::jwk::Jwk; +use identity_iota::verification::jwu::decode_b64_json; +use identity_iota::verification::VerificationMethod; use identity_stronghold::StrongholdStorage; +use tonic::transport::server::RoutesBuilder; use tonic::Request; use tonic::Response; use tonic::Status; @@ -62,6 +74,56 @@ 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 {})); +} + +#[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}\""), + } } From 02e646243e88362b825428aef2ef207fcb7a3216 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 25 Jul 2024 11:02:43 +0200 Subject: [PATCH 6/9] did:jwk resolution --- bindings/grpc/src/services/presentation.rs | 1 + identity_did/Cargo.toml | 1 + identity_did/src/did_jwk.rs | 120 ++++++++++++++++++ identity_did/src/lib.rs | 6 +- .../src/document/core_document.rs | 48 ++++++- .../src/document/iota_document.rs | 5 +- identity_resolver/src/resolution/resolver.rs | 72 +++++++++-- identity_verification/src/jose/mod.rs | 12 +- .../src/verification_method/method.rs | 11 +- 9 files changed, 257 insertions(+), 19 deletions(-) create mode 100644 identity_did/src/did_jwk.rs diff --git a/bindings/grpc/src/services/presentation.rs b/bindings/grpc/src/services/presentation.rs index 7f8ccf26e2..5f8a7e86e1 100644 --- a/bindings/grpc/src/services/presentation.rs +++ b/bindings/grpc/src/services/presentation.rs @@ -67,6 +67,7 @@ pub struct PresentationSvc { impl PresentationSvc { pub fn new(client: Client) -> Self { let mut resolver = Resolver::::new_with_did_key_handler(); + resolver.attach_did_jwk_handler(); resolver.attach_iota_handler(client); Self { resolver } diff --git a/identity_did/Cargo.toml b/identity_did/Cargo.toml index 5b4e85069c..18b32330ca 100644 --- a/identity_did/Cargo.toml +++ b/identity_did/Cargo.toml @@ -14,6 +14,7 @@ description = "Agnostic implementation of the Decentralized Identifiers (DID) st 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.3.1", path = "../identity_core" } +identity_jose = { version = "=1.3.1", path = "../identity_jose" } serde.workspace = true strum.workspace = true thiserror.workspace = true diff --git a/identity_did/src/did_jwk.rs b/identity_did/src/did_jwk.rs new file mode 100644 index 0000000000..a93d4ef954 --- /dev/null +++ b/identity_did/src/did_jwk.rs @@ -0,0 +1,120 @@ +use std::fmt::Debug; +use std::fmt::Display; +use std::str::FromStr; + +use identity_jose::jwk::Jwk; +use identity_jose::jwu::decode_b64_json; + +use crate::CoreDID; +use crate::Error; +use crate::DID; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +#[repr(transparent)] +#[serde(into = "CoreDID", try_from = "CoreDID")] +/// A type representing a `did:jwk` DID. +pub struct DIDJwk(CoreDID); + +impl DIDJwk { + /// [`DIDKey`]'s method. + pub const METHOD: &'static str = "jwk"; + + /// Tries to parse a [`DIDKey`] from a string. + pub fn parse(s: &str) -> Result { + s.parse() + } + + /// Returns the JWK encoded inside this did:jwk. + pub fn jwk(&self) -> Jwk { + decode_b64_json(self.method_id()).expect("did:jwk encodes a valid JWK") + } +} + +impl AsRef for DIDJwk { + fn as_ref(&self) -> &CoreDID { + &self.0 + } +} + +impl From for CoreDID { + fn from(value: DIDJwk) -> Self { + value.0 + } +} + +impl<'a> TryFrom<&'a str> for DIDJwk { + type Error = Error; + fn try_from(value: &'a str) -> Result { + value.parse() + } +} + +impl Display for DIDJwk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for DIDJwk { + type Err = Error; + fn from_str(s: &str) -> Result { + s.parse::().and_then(TryFrom::try_from) + } +} + +impl From for String { + fn from(value: DIDJwk) -> Self { + value.to_string() + } +} + +impl TryFrom for DIDJwk { + type Error = Error; + fn try_from(value: CoreDID) -> Result { + let Self::METHOD = value.method() else { + return Err(Error::InvalidMethodName); + }; + decode_b64_json::(value.method_id()) + .map(|_| Self(value)) + .map_err(|_| Error::InvalidMethodId) + } +} + +#[cfg(test)] +mod tests { + use identity_core::convert::FromJson; + + use super::*; + + #[test] + fn test_valid_deserialization() -> Result<(), Error> { + "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".parse::()?; + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9".parse::()?; + + Ok(()) + } + + #[test] + fn test_jwk() { + let did = DIDJwk::parse("did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9").unwrap(); + let target_jwk = Jwk::from_json_value(serde_json::json!({ + "kty":"OKP","crv":"X25519","use":"enc","x":"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08" + })) + .unwrap(); + + assert_eq!(did.jwk(), target_jwk); + } + + #[test] + fn test_invalid_deserialization() { + assert!( + "did:iota:0xf4d6f08f5a1b80dd578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a" + .parse::() + .is_err() + ); + assert!("did:jwk:".parse::().is_err()); + assert!("did:jwk:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + .parse::() + .is_err()); + } +} diff --git a/identity_did/src/lib.rs b/identity_did/src/lib.rs index 994fa6622d..038feead2f 100644 --- a/identity_did/src/lib.rs +++ b/identity_did/src/lib.rs @@ -18,14 +18,16 @@ #[allow(clippy::module_inception)] mod did; +mod did_jwk; +mod did_key; mod did_url; mod error; -mod did_key; pub use crate::did_url::DIDUrl; pub use crate::did_url::RelativeDIDUrl; pub use ::did_url_parser::DID as BaseDIDUrl; pub use did::CoreDID; pub use did::DID; -pub use error::Error; +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 d8686c3ce6..13f370afcb 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -7,6 +7,7 @@ use core::fmt::Formatter; use std::collections::HashMap; use std::convert::Infallible; +use identity_did::DIDJwk; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jws::DecodedJws; use identity_verification::jose::jws::Decoder; @@ -985,8 +986,9 @@ impl CoreDocument { } } -// did:key expansion +// 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()) @@ -997,6 +999,21 @@ impl CoreDocument { .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)?; + let verification_method_id = verification_method.id().clone(); + + DocumentBuilder::default() + .id(did_jwk.into()) + .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()) + .build() + } } #[cfg(test)] @@ -1731,4 +1748,33 @@ mod tests { assert_eq!(CoreDocument::expand_did_key(did_key).unwrap(), target_doc); } + + #[test] + fn test_did_jwk_expansion() { + let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9" + .parse::() + .unwrap(); + let target_doc = serde_json::from_value(serde_json::json!({ + "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", + "verificationMethod": [ + { + "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0", + "type": "JsonWebKey2020", + "controller": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", + "publicKeyJwk": { + "kty":"OKP", + "crv":"X25519", + "use":"enc", + "x":"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08" + } + } + ], + "assertionMethod": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"], + "authentication": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"], + "capabilityInvocation": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"], + "capabilityDelegation": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"] + })).unwrap(); + + assert_eq!(CoreDocument::expand_did_jwk(did_jwk).unwrap(), target_doc); + } } diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index 703dbe897a..bd3404045c 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -557,7 +557,10 @@ impl From for CoreDocument { impl From for IotaDocument { fn from(value: CoreDocument) -> Self { - IotaDocument { document: value, metadata: IotaDocumentMetadata::default() } + IotaDocument { + document: value, + metadata: IotaDocumentMetadata::default(), + } } } diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index 33866d34ed..03956541ab 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -4,6 +4,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; @@ -252,9 +253,7 @@ 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).into() } - }; + let handler = |did_key: DIDKey| async move { CoreDocument::expand_did_key(did_key) }; command_map.insert(DIDKey::METHOD.to_string(), SingleThreadedCommand::new(handler)); Self { @@ -262,15 +261,37 @@ impl + 'static> Resolver _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) }; + self.attach_handler(DIDJwk::METHOD.to_string(), handler) + } } impl + 'static> Resolver> { - /// Creates a new [`Resolver`] with a default handler for `did:key` DIDs. + /// 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).into() } - }; + let handler = |did_key: DIDKey| async move { CoreDocument::expand_did_key(did_key) }; command_map.insert(DIDKey::METHOD.to_string(), SendSyncCommand::new(handler)); Self { @@ -278,6 +299,30 @@ impl + 'static> Resolver> { _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) }; + self.attach_handler(DIDJwk::METHOD.to_string(), handler) + } } #[cfg(feature = "iota")] @@ -451,9 +496,20 @@ mod tests { #[tokio::test] async fn test_did_key_resolution() { let resolver = Resolver::::new_with_did_key_handler(); - let did_key = "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp".parse::().unwrap(); + 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(); + assert_eq!(doc.id(), did_jwk.as_ref()); + } } diff --git a/identity_verification/src/jose/mod.rs b/identity_verification/src/jose/mod.rs index 306bad8aa2..60dbdc0dfb 100644 --- a/identity_verification/src/jose/mod.rs +++ b/identity_verification/src/jose/mod.rs @@ -30,11 +30,11 @@ pub mod error { use error::Error; use identity_core::convert::BaseEncoding; -use identity_did::{DIDKey, DID as _}; -use identity_jose::{ - jwk::{EdCurve, JwkParamsOkp}, - jwu::encode_b64, -}; +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`. @@ -44,7 +44,7 @@ pub fn did_key_to_jwk(did_key: &DIDKey) -> Result { 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 { + if key_type != [0xed, 0x01] || pk_bytes.len() != 32 { return Err(Error::KeyError("invalid ED25519 key")); } diff --git a/identity_verification/src/verification_method/method.rs b/identity_verification/src/verification_method/method.rs index c80d214b0c..35cbab219a 100644 --- a/identity_verification/src/verification_method/method.rs +++ b/identity_verification/src/verification_method/method.rs @@ -5,6 +5,7 @@ use core::fmt::Display; use core::fmt::Formatter; use std::borrow::Cow; +use identity_did::DIDJwk; use identity_did::DIDKey; use identity_jose::jwk::Jwk; use serde::de; @@ -255,7 +256,7 @@ impl TryFrom for VerificationMethod { 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; + let method_type = MethodType::JSON_WEB_KEY_2020; let data = did_key_to_jwk(&value) .map_err(|_| Error::InvalidKeyDataMultibase) .map(MethodData::PublicKeyJwk)?; @@ -270,6 +271,14 @@ impl TryFrom for VerificationMethod { } } +impl TryFrom for VerificationMethod { + type Error = Error; + fn try_from(did: DIDJwk) -> Result { + let jwk = did.jwk(); + Self::new_from_jwk(did, jwk, Some("0")) + } +} + // Horrible workaround for a tracked serde issue https://github.com/serde-rs/serde/issues/2200. Serde doesn't "consume" // the input when deserializing flattened enums (MethodData in this case) causing duplication of data (in this case // it ends up in the properties object). This workaround simply removes the duplication. From c43fd33e56ada2c65229f95c681fcf350e60710a Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 14 Aug 2024 09:20:27 +0200 Subject: [PATCH 7/9] add supoort for ecdsa based VCs --- bindings/grpc/Cargo.toml | 1 + bindings/grpc/proto/utils.proto | 13 ++ bindings/grpc/src/lib.rs | 2 + .../src/services/credential/validation.rs | 4 +- bindings/grpc/src/services/utils.rs | 41 ++++ bindings/grpc/src/verifier.rs | 30 +++ identity_stronghold/Cargo.toml | 2 + .../src/storage/stronghold_jwk_storage.rs | 181 +++++++++++++----- .../src/stronghold_key_type.rs | 40 ++-- identity_stronghold/src/utils.rs | 2 + 10 files changed, 257 insertions(+), 59 deletions(-) create mode 100644 bindings/grpc/src/verifier.rs diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml index 2b542712db..1f0c80caab 100644 --- a/bindings/grpc/Cargo.toml +++ b/bindings/grpc/Cargo.toml @@ -20,6 +20,7 @@ path = "src/main.rs" anyhow = "1.0.75" 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", "domain-linkage", "domain-linkage-fetch", "status-list-2021"] } identity_stronghold = { path = "../../identity_stronghold", features = ["send-sync-storage"] } iota-sdk = { version = "1.1.5", features = ["stronghold"] } diff --git a/bindings/grpc/proto/utils.proto b/bindings/grpc/proto/utils.proto index 2caa61244f..80f06925aa 100644 --- a/bindings/grpc/proto/utils.proto +++ b/bindings/grpc/proto/utils.proto @@ -33,4 +33,17 @@ message DidJwkResolutionResponse { 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 fb218b727b..09828a60f8 100644 --- a/bindings/grpc/src/services/credential/validation.rs +++ b/bindings/grpc/src/services/credential/validation.rs @@ -27,6 +27,8 @@ use tonic::Request; use tonic::Response; use tonic::Status; +use crate::verifier::Verifier; + mod _credentials { tonic::include_proto!("credentials"); } @@ -98,7 +100,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/utils.rs b/bindings/grpc/src/services/utils.rs index a8214b332a..cb48646b91 100644 --- a/bindings/grpc/src/services/utils.rs +++ b/bindings/grpc/src/services/utils.rs @@ -3,16 +3,21 @@ 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; @@ -20,6 +25,11 @@ use identity_iota::verification::jwk::Jwk; use identity_iota::verification::jwu::decode_b64_json; use identity_iota::verification::VerificationMethod; 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::transport::server::RoutesBuilder; use tonic::Request; use tonic::Response; @@ -77,6 +87,7 @@ impl SigningSvc for SigningService { 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)] @@ -127,3 +138,33 @@ fn parse_did_jwk(did: &str) -> anyhow::Result { _ => 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/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index 56ae126bdc..14c55922cd 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/storage/stronghold_jwk_storage.rs b/identity_stronghold/src/storage/stronghold_jwk_storage.rs index b0400c8f65..9315c2f0b1 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 3a9ae72842..6b67f4e242 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}`")), From bbc82f129e0dbad6edea4fe4cda67a8a1dc94b99 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 15 Aug 2024 11:40:31 +0200 Subject: [PATCH 8/9] enable es256, es256k signing --- bindings/grpc/proto/utils.proto | 2 ++ bindings/grpc/src/services/utils.rs | 19 +++++++++---- bindings/grpc/tests/api/utils.rs | 1 + identity_stronghold/src/ecdsa.rs | 35 ++++++++++++++++++++++++ identity_stronghold/src/lib.rs | 1 + identity_stronghold/src/storage/mod.rs | 37 ++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 identity_stronghold/src/ecdsa.rs diff --git a/bindings/grpc/proto/utils.proto b/bindings/grpc/proto/utils.proto index 80f06925aa..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 { diff --git a/bindings/grpc/src/services/utils.rs b/bindings/grpc/src/services/utils.rs index cb48646b91..16a93beda8 100644 --- a/bindings/grpc/src/services/utils.rs +++ b/bindings/grpc/src/services/utils.rs @@ -21,9 +21,11 @@ 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; @@ -71,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) @@ -156,15 +166,14 @@ impl IotaUtilsSvc for IotaUtils { 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 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 + network, })) } } diff --git a/bindings/grpc/tests/api/utils.rs b/bindings/grpc/tests/api/utils.rs index 9b320bd154..0010fc25e9 100644 --- a/bindings/grpc/tests/api/utils.rs +++ b/bindings/grpc/tests/api/utils.rs @@ -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_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)] From 7287ac4a1ebaf641c3c680b84a37ef6091649869 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 15 Aug 2024 14:19:57 +0200 Subject: [PATCH 9/9] use compound verifier --- bindings/grpc/src/services/credential/validation.rs | 1 - bindings/grpc/src/services/domain_linkage.rs | 5 +++-- bindings/grpc/src/services/presentation.rs | 6 +++--- bindings/grpc/src/services/sd_jwt.rs | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bindings/grpc/src/services/credential/validation.rs b/bindings/grpc/src/services/credential/validation.rs index 09828a60f8..1960e54b99 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; diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs index bb8b214982..b85bab2ff0 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,7 +277,7 @@ 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()) + if let Err(err) = JwtDomainLinkageValidator::with_signature_verifier(Verifier::default()) .validate_linkage( &issuer_did_doc, &domain_linkage_configuration, diff --git a/bindings/grpc/src/services/presentation.rs b/bindings/grpc/src/services/presentation.rs index 5f8a7e86e1..3ba3ce6ddc 100644 --- a/bindings/grpc/src/services/presentation.rs +++ b/bindings/grpc/src/services/presentation.rs @@ -7,7 +7,7 @@ use _presentation::credential_validation_result::Result as ValidationResult; use _presentation::CredentialValidationResult; use _presentation::JwtPresentationRequest; use _presentation::JwtPresentationResponse; -use identity_eddsa_verifier::EdDSAJwsVerifier; +use crate::verifier::Verifier; use identity_iota::core::Object; use identity_iota::core::ToJson; use identity_iota::credential::CompoundJwtPresentationValidationError; @@ -90,7 +90,7 @@ impl PresentationService for PresentationSvc { .await .map_err(Error::ResolutionError)?; - let presentation_validator = JwtPresentationValidator::with_signature_verifier(EdDSAJwsVerifier::default()); + let presentation_validator = JwtPresentationValidator::with_signature_verifier(Verifier::default()); let mut decoded_presentation = presentation_validator .validate::( &jwt_presentation, @@ -101,7 +101,7 @@ impl PresentationService for PresentationSvc { 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(EdDSAJwsVerifier::default()); + 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())); diff --git a/bindings/grpc/src/services/sd_jwt.rs b/bindings/grpc/src/services/sd_jwt.rs index af792e51f6..be8ff8b1fe 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,