From 6003cf0f68fb97b1e834a40fb95756ccf31ecb72 Mon Sep 17 00:00:00 2001 From: Alberto Solavagione Date: Mon, 4 Mar 2024 15:52:55 +0100 Subject: [PATCH 01/33] Support BBS+ and JWP (#1285) --- Cargo.toml | 2 + examples/0_basic/7_revoke_vc.rs | 2 + examples/1_advanced/10_zkp_revocation.rs | 526 ++++++++++++++++++ examples/1_advanced/9_zkp.rs | 257 +++++++++ examples/Cargo.toml | 9 + identity_credential/Cargo.toml | 3 + .../src/credential/credential.rs | 10 + identity_credential/src/credential/jpt.rs | 30 + .../src/credential/jwp_credential_options.rs | 25 + .../src/credential/jwt_serialization.rs | 51 ++ identity_credential/src/credential/mod.rs | 6 + .../credential/revocation_bitmap_status.rs | 2 +- identity_credential/src/error.rs | 8 + .../presentation/jwp_presentation_builder.rs | 95 ++++ .../presentation/jwp_presentation_options.rs | 30 + identity_credential/src/presentation/mod.rs | 4 + identity_credential/src/revocation/mod.rs | 3 + .../revocation/validity_timeframe_2024/mod.rs | 5 + .../revocation_timeframe_status.rs | 287 ++++++++++ .../decoded_jpt_credential.rs | 16 + .../jpt_credential_validation_options.rs | 84 +++ .../jpt_credential_validator.rs | 222 ++++++++ .../jpt_credential_validator_utils.rs | 236 ++++++++ .../jpt_credential_validation/mod.rs | 9 + .../decoded_jpt_presentation.rs | 19 + .../jpt_presentation_validation_options.rs | 37 ++ .../jpt_presentation_validator.rs | 223 ++++++++ .../jpt_presentation_validator_utils.rs | 96 ++++ .../jpt_presentation_validation/mod.rs | 9 + .../jwt_credential_validation/error.rs | 12 + identity_credential/src/validator/mod.rs | 4 + .../verifiable/jwp_verification_options.rs | 33 ++ identity_document/src/verifiable/mod.rs | 2 + identity_jose/Cargo.toml | 1 + identity_jose/src/jwk/jwk_ext.rs | 173 ++++++ identity_jose/src/jwk/key_operation.rs | 8 + identity_jose/src/jwk/key_use.rs | 4 + identity_jose/src/jwk/mod.rs | 1 + identity_storage/Cargo.toml | 3 + .../src/key_storage/jwk_storage.rs | 30 + .../src/key_storage/key_storage_error.rs | 4 + identity_storage/src/key_storage/memstore.rs | 290 ++++++++++ identity_storage/src/storage/error.rs | 10 + .../src/storage/jwk_document_ext.rs | 26 +- .../src/storage/jwp_document_ext.rs | 347 ++++++++++++ identity_storage/src/storage/mod.rs | 7 + .../src/storage/timeframe_revocation_ext.rs | 193 +++++++ 47 files changed, 3446 insertions(+), 8 deletions(-) create mode 100644 examples/1_advanced/10_zkp_revocation.rs create mode 100644 examples/1_advanced/9_zkp.rs create mode 100644 identity_credential/src/credential/jpt.rs create mode 100644 identity_credential/src/credential/jwp_credential_options.rs create mode 100644 identity_credential/src/presentation/jwp_presentation_builder.rs create mode 100644 identity_credential/src/presentation/jwp_presentation_options.rs create mode 100644 identity_credential/src/revocation/validity_timeframe_2024/mod.rs create mode 100644 identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs create mode 100644 identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs create mode 100644 identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs create mode 100644 identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs create mode 100644 identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs create mode 100644 identity_credential/src/validator/jpt_credential_validation/mod.rs create mode 100644 identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs create mode 100644 identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs create mode 100644 identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs create mode 100644 identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs create mode 100644 identity_credential/src/validator/jpt_presentation_validation/mod.rs create mode 100644 identity_document/src/verifiable/jwp_verification_options.rs create mode 100644 identity_jose/src/jwk/jwk_ext.rs create mode 100644 identity_storage/src/storage/jwp_document_ext.rs create mode 100644 identity_storage/src/storage/timeframe_revocation_ext.rs diff --git a/Cargo.toml b/Cargo.toml index d4726e0d4c..9dc0983411 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ serde = { version = "1.0", default-features = false, features = ["alloc", "deriv thiserror = { version = "1.0", default-features = false } strum = { version = "0.25", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0", default-features = false } +json-proof-token = { version = "0.3.3" } +zkryptium = { version = "0.1.9", default-features = false, features = ["bbsplus"] } [workspace.package] authors = ["IOTA Stiftung"] diff --git a/examples/0_basic/7_revoke_vc.rs b/examples/0_basic/7_revoke_vc.rs index 48d947a7ff..864041f3e3 100644 --- a/examples/0_basic/7_revoke_vc.rs +++ b/examples/0_basic/7_revoke_vc.rs @@ -110,6 +110,8 @@ async fn main() -> anyhow::Result<()> { // Publish the updated Alias Output. issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; + println!("DID Document > {issuer_document:#}"); + // Create a credential subject indicating the degree earned by Alice. let subject: Subject = Subject::from_json_value(json!({ "id": alice_document.id().as_str(), diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs new file mode 100644 index 0000000000..ad669e63fd --- /dev/null +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -0,0 +1,526 @@ +use examples::get_address_with_funds; +use examples::random_stronghold_path; +use examples::MemStorage; +use examples::API_ENDPOINT; +use examples::FAUCET_ENDPOINT; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota::core::json; +use identity_iota::core::Duration; +use identity_iota::core::FromJson; +use identity_iota::core::Object; +use identity_iota::core::Timestamp; +use identity_iota::core::Url; +use identity_iota::credential::Credential; +use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::DecodedJwtPresentation; +use identity_iota::credential::FailFast; +use identity_iota::credential::Jpt; +use identity_iota::credential::JptCredentialValidationOptions; +use identity_iota::credential::JptCredentialValidator; +use identity_iota::credential::JptCredentialValidatorUtils; +use identity_iota::credential::JptPresentationValidationOptions; +use identity_iota::credential::JptPresentationValidator; +use identity_iota::credential::JptPresentationValidatorUtils; +use identity_iota::credential::JwpCredentialOptions; +use identity_iota::credential::JwpPresentationOptions; +use identity_iota::credential::Jwt; +use identity_iota::credential::JwtPresentationOptions; +use identity_iota::credential::JwtPresentationValidationOptions; +use identity_iota::credential::JwtPresentationValidator; +use identity_iota::credential::JwtPresentationValidatorUtils; +use identity_iota::credential::JwtValidationError; +use identity_iota::credential::Presentation; +use identity_iota::credential::PresentationBuilder; +use identity_iota::credential::RevocationBitmap; +use identity_iota::credential::RevocationTimeframeStatus; +use identity_iota::credential::SelectiveDisclosurePresentation; +use identity_iota::credential::Status; +use identity_iota::credential::StatusCheck; +use identity_iota::credential::Subject; +use identity_iota::credential::SubjectHolderRelationship; +use identity_iota::did::CoreDID; +use identity_iota::did::DIDUrl; +use identity_iota::did::DID; +use identity_iota::document::verifiable::JwsVerificationOptions; +use identity_iota::document::Service; +use identity_iota::iota::IotaClientExt; +use identity_iota::iota::IotaDocument; +use identity_iota::iota::IotaIdentityClientExt; +use identity_iota::iota::NetworkName; +use identity_iota::resolver::Resolver; +use identity_iota::storage::JwkDocumentExt; +use identity_iota::storage::JwkMemStore; +use identity_iota::storage::JwpDocumentExt; +use identity_iota::storage::JwsSignatureOptions; +use identity_iota::storage::KeyIdMemstore; +use identity_iota::storage::KeyType; +use identity_iota::storage::TimeframeRevocationExtension; +use identity_iota::verification::jws::JwsAlgorithm; +use identity_iota::verification::MethodScope; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; +use iota_sdk::client::Client; +use iota_sdk::client::Password; +use iota_sdk::types::block::address::Address; +use iota_sdk::types::block::output::AliasOutput; +use iota_sdk::types::block::output::AliasOutputBuilder; +use iota_sdk::types::block::output::RentStructure; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use std::thread; +use std::time::Duration as SleepDuration; + +async fn create_did( + client: &Client, + secret_manager: &SecretManager, + storage: &MemStorage, + key_type: KeyType, + alg: Option, + proof_alg: Option, +) -> anyhow::Result<(Address, IotaDocument, String)> { + // Get an address with funds for testing. + let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT).await?; + + // Get the Bech32 human-readable part (HRP) of the network. + let network_name: NetworkName = client.network_name().await?; + + // Create a new DID document with a placeholder DID. + // The DID will be derived from the Alias Id of the Alias Output after publishing. + let mut document: IotaDocument = IotaDocument::new(&network_name); + + // New Verification Method containing a BBS+ key + let fragment = if let Some(alg) = alg { + document + .generate_method(storage, key_type, alg, None, MethodScope::VerificationMethod) + .await? + } else if let Some(proof_alg) = proof_alg { + let fragment = document + .generate_method_jwp(storage, key_type, proof_alg, None, MethodScope::VerificationMethod) + .await?; + + // Create a new empty revocation bitmap. No credential is revoked yet. + let revocation_bitmap: RevocationBitmap = RevocationBitmap::new(); + + // Add the revocation bitmap to the DID document of the issuer as a service. + let service_id: DIDUrl = document.id().to_url().join("#my-revocation-service")?; + let service: Service = revocation_bitmap.to_service(service_id)?; + + assert!(document.insert_service(service).is_ok()); + + fragment + } else { + return Err(anyhow::Error::msg("You have to pass at least one algorithm")); + }; + + // Construct an Alias Output containing the DID document, with the wallet address + // set as both the state controller and governor. + let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; + + // Publish the Alias Output and get the published DID document. + let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; + println!("Published DID document: {document:#}"); + + Ok((address, document, fragment)) +} + +/// Demonstrates how to create an Anonymous Credential with BBS+. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Create a new client to interact with the IOTA ledger. + let client: Client = Client::builder() + .with_primary_node(API_ENDPOINT, None)? + .finish() + .await?; + + let secret_manager_issuer = SecretManager::Stronghold( + StrongholdSecretManager::builder() + .password(Password::from("secure_password_1".to_owned())) + .build(random_stronghold_path())?, + ); + + let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + + let secret_manager_holder = SecretManager::Stronghold( + StrongholdSecretManager::builder() + .password(Password::from("secure_password_2".to_owned())) + .build(random_stronghold_path())?, + ); + + let storage_holder: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + + let (_, mut issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did( + &client, + &secret_manager_issuer, + &storage_issuer, + JwkMemStore::BLS12381SHA256_KEY_TYPE, + None, + Some(ProofAlgorithm::BLS12381_SHA256), + ) + .await?; + + let (_, holder_document, fragment_holder): (Address, IotaDocument, String) = create_did( + &client, + &secret_manager_holder, + &storage_holder, + JwkMemStore::ED25519_KEY_TYPE, + Some(JwsAlgorithm::EdDSA), + None, + ) + .await?; + + // Create a credential subject indicating the degree earned by Alice. + let subject: Subject = Subject::from_json_value(json!({ + "id": holder_document.id().as_str(), + "name": "Alice", + "mainCourses": ["Object-oriented Programming", "Mathematics"], + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", + }))?; + + // ========================================================================================= + // Step 1: Create a new RevocationTimeframeStatus containing the current validityTimeframe + // ======================================================================================= + let duration = Duration::minutes(1); + // The issuer also chooses a unique `RevocationBitmap` index to be able to revoke it later. + let service_url = issuer_document.id().to_url().join("#my-revocation-service")?; + let credential_index: u32 = 5; + + let start_validity_timeframe = Timestamp::now_utc(); + let status: Status = + RevocationTimeframeStatus::new(Some(start_validity_timeframe), duration, service_url, credential_index)?.into(); + + // Build credential using subject above and issuer. + let credential: Credential = CredentialBuilder::default() + .id(Url::parse("https://example.edu/credentials/3732")?) + .issuer(Url::parse(issuer_document.id().as_str())?) + .type_("UniversityDegreeCredential") + .subject(subject) + .status(status) + .build()?; + + let credential_jpt: Jpt = issuer_document + .create_credential_jpt( + &credential, + &storage_issuer, + &fragment_issuer, + &JwpCredentialOptions::default(), + None, + ) + .await?; + + // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure, + // that the issuance date is not in the future and that the expiration date is not in the past: + let decoded_jpt = JptCredentialValidator::validate::<_, Object>( + &credential_jpt, + &issuer_document, + &JptCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); + + assert_eq!(credential, decoded_jpt.credential); + + // Issuer sends the Verifiable Credential to the holder. + println!( + "Sending credential (as JPT) to the holder: {}\n", + credential_jpt.as_str() + ); + + // Holder validate the credential and retrieve the JwpIssued, needed to construct the JwpPresented + + let validation_result = JptCredentialValidator::validate::<_, Object>( + &credential_jpt, + &issuer_document, + &JptCredentialValidationOptions::default(), + FailFast::FirstError, + ); + + let decoded_credential = validation_result.unwrap(); + + // =========================================================================== + // Credential's Status check + // =========================================================================== + + // Timeframe check + let timeframe_result = JptCredentialValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &decoded_credential.credential, + None, + StatusCheck::Strict, + ); + + assert!(timeframe_result.is_ok()); + + let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024( + &decoded_credential.credential, + &issuer_document, + StatusCheck::Strict, + ); + + assert!(revocation_result.is_ok()); + + // Both checks + + let revocation_result = JptCredentialValidatorUtils::check_timeframes_and_revocation_with_validity_timeframe_2024( + &decoded_credential.credential, + &issuer_document, + None, + StatusCheck::Strict, + ); + + assert!(revocation_result.is_ok()); + + let challenge: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + let method_id = decoded_credential + .decoded_jwp + .get_issuer_protected_header() + .kid() + .unwrap(); + + let mut selective_disclosure_presentation = SelectiveDisclosurePresentation::new(&decoded_credential.decoded_jwp); + selective_disclosure_presentation + .conceal_in_subject("mainCourses[1]") + .unwrap(); + selective_disclosure_presentation + .conceal_in_subject("degree.name") + .unwrap(); + + let presentation_jpt: Jpt = issuer_document + .create_presentation_jpt( + &mut selective_disclosure_presentation, + method_id, + &JwpPresentationOptions::default().nonce(challenge), + ) + .await?; + + // Holder sends a Presentation JPT to the Verifier. + println!( + "Sending presentation (as JPT) to the verifier: {}\n", + presentation_jpt.as_str() + ); + + // =========================================================================== + // Step 2a: Verifier receives the Presentation and verifies it. + // =========================================================================== + + let presentation_validation_options = JptPresentationValidationOptions::default().nonce(challenge); + + // Verifier validate the Presented Credential and retrieve the JwpPresented + let decoded_presented_credential = JptPresentationValidator::validate::<_, Object>( + &presentation_jpt, + &issuer_document, + &presentation_validation_options, + FailFast::FirstError, + ) + .unwrap(); + + // Check validityTimeframe + + let timeframe_result = JptPresentationValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &decoded_presented_credential.credential, + None, + StatusCheck::Strict, + ); + + assert!(timeframe_result.is_ok()); + + // Since no errors were thrown by `verify_presentation` we know that the validation was successful. + println!( + "Presented Credential successfully validated: {:#}", + decoded_presented_credential.credential + ); + + // =========================================================================== + // Step 2b: Waiting for the next validityTimeframe, will result in the Credential timeframe interval NOT valid + // =========================================================================== + + thread::sleep(SleepDuration::from_secs(61)); + + let timeframe_result = JptPresentationValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &decoded_presented_credential.credential, + None, + StatusCheck::Strict, + ); + + // We expect validation to no longer succeed because the credential was NOT updated. + if matches!(timeframe_result.unwrap_err(), JwtValidationError::OutsideTimeframe) { + println!("Validity Timeframe interval NOT valid\n"); + } + + // =========================================================================== + // 3: Update credential + // =========================================================================== + + // =========================================================================== + // 3.1: Issuer sends the holder a challenge and requests a signed Verifiable Presentation. + // =========================================================================== + + // A unique random challenge generated by the requester per presentation can mitigate replay attacks. + let challenge: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + // The Holder and Issuer also agree that the signature should have an expiry date + // 10 minutes from now. + let expires: Timestamp = Timestamp::now_utc().checked_add(Duration::minutes(10)).unwrap(); + + // =========================================================================== + // 3.2: Holder creates and signs a verifiable presentation from the issued credential. + // =========================================================================== + + // Create an unsigned Presentation from the previously issued ZK Verifiable Credential. + let presentation: Presentation = + PresentationBuilder::new(holder_document.id().to_url().into(), Default::default()) + .credential(credential_jpt) + .build()?; + + // Create a JWT verifiable presentation using the holder's verification method + // and include the requested challenge and expiry timestamp. + let presentation_jwt: Jwt = holder_document + .create_presentation_jwt( + &presentation, + &storage_holder, + &fragment_holder, + &JwsSignatureOptions::default().nonce(challenge.to_owned()), + &JwtPresentationOptions::default().expiration_date(expires), + ) + .await?; + + // =========================================================================== + // 3.3: Holder sends a verifiable presentation to the verifier. + // =========================================================================== + println!( + "Sending presentation (as JWT) to the Issuer: {}\n", + presentation_jwt.as_str() + ); + + // =========================================================================== + // 3.4: Issuer validate Verifiable Presentation and ZK Verifiable Credential. + // =========================================================================== + + // ================================================ + // 3.4.1: Issuer validate Verifiable Presentation. + // ================================================ + + let presentation_verifier_options: JwsVerificationOptions = + JwsVerificationOptions::default().nonce(challenge.to_owned()); + + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + + // Resolve the holder's document. + let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; + let holder: IotaDocument = resolver.resolve(&holder_did).await?; + + // Validate presentation. Note that this doesn't validate the included credentials. + let presentation_validation_options = + JwtPresentationValidationOptions::default().presentation_verifier_options(presentation_verifier_options); + let presentation: DecodedJwtPresentation = JwtPresentationValidator::with_signature_verifier( + EdDSAJwsVerifier::default(), + ) + .validate(&presentation_jwt, &holder, &presentation_validation_options)?; + + // ======================================================================= + // 3.4.2: Issuer validate ZK Verifiable Credential inside the Presentation. + // ======================================================================== + + let validation_options: JptCredentialValidationOptions = JptCredentialValidationOptions::default() + .subject_holder_relationship(holder_did.to_url().into(), SubjectHolderRelationship::AlwaysSubject); + + let jpt_credentials: &Vec = &presentation.presentation.verifiable_credential; + + // Extract ZK Verifiable Credential in JPT format + let jpt_vc = jpt_credentials.first().unwrap(); + + // Issuer checks the Credential integrity. + let mut verified_credential_result = + JptCredentialValidator::validate::<_, Object>(jpt_vc, &issuer_document, &validation_options, FailFast::FirstError) + .unwrap(); + + // Issuer checks if the Credential has been revoked + let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024( + &verified_credential_result.credential, + &issuer_document, + StatusCheck::Strict, + ); + + assert!(!revocation_result.is_err_and(|e| matches!(e, JwtValidationError::Revoked))); + + // =========================================================================== + // 3.5: Issuer ready for Update. + // =========================================================================== + + // Since no errors were thrown during the Verifiable Presentation validation and the verification of inner Credentials + println!( + "Ready for Update - VP successfully validated: {:#?}", + presentation.presentation + ); + + // Issuer updates the credential + let new_credential_jpt = issuer_document + .update( + &storage_issuer, + &fragment_issuer, + None, + duration, + &mut verified_credential_result.decoded_jwp, + ) + .await?; + + // Issuer sends back the credential updated + + println!( + "Sending updated credential (as JPT) to the holder: {}\n", + new_credential_jpt.as_str() + ); + + // Holder check validity of the updated credential + + let validation_result = JptCredentialValidator::validate::<_, Object>( + &new_credential_jpt, + &issuer_document, + &JptCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); + + let timeframe_result = JptCredentialValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &validation_result.credential, + None, + StatusCheck::Strict, + ); + + assert!(!timeframe_result + .as_ref() + .is_err_and(|e| matches!(e, JwtValidationError::OutsideTimeframe))); + println!("Updated credential is VALID!"); + + // =========================================================================== + // Issuer decides to Revoke Holder's Credential + // =========================================================================== + + println!("Issuer decides to revoke the Credential"); + + // Update the RevocationBitmap service in the issuer's DID Document. + // This revokes the credential's unique index. + + issuer_document.revoke_credentials("my-revocation-service", &[credential_index])?; + + // Publish the changes. + let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?; + let rent_structure: RentStructure = client.get_rent_structure().await?; + let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) + .with_minimum_storage_deposit(rent_structure) + .finish()?; + issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?; + + // Holder checks if his credential has been revoked by the Issuer + let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024( + &decoded_credential.credential, + &issuer_document, + StatusCheck::Strict, + ); + assert!(revocation_result.is_err_and(|e| matches!(e, JwtValidationError::Revoked))); + println!("Credential Revoked!"); + Ok(()) +} diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs new file mode 100644 index 0000000000..bca63edd94 --- /dev/null +++ b/examples/1_advanced/9_zkp.rs @@ -0,0 +1,257 @@ +use examples::get_address_with_funds; +use examples::random_stronghold_path; +use examples::MemStorage; +use examples::API_ENDPOINT; +use examples::FAUCET_ENDPOINT; +use identity_iota::core::json; +use identity_iota::core::FromJson; +use identity_iota::core::Object; +use identity_iota::core::Url; +use identity_iota::credential::Credential; +use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::FailFast; +use identity_iota::credential::Jpt; +use identity_iota::credential::JptCredentialValidationOptions; +use identity_iota::credential::JptCredentialValidator; +use identity_iota::credential::JptCredentialValidatorUtils; +use identity_iota::credential::JptPresentationValidationOptions; +use identity_iota::credential::JptPresentationValidator; +use identity_iota::credential::JptPresentationValidatorUtils; +use identity_iota::credential::JwpCredentialOptions; +use identity_iota::credential::JwpPresentationOptions; +use identity_iota::credential::SelectiveDisclosurePresentation; +use identity_iota::credential::Subject; +use identity_iota::did::CoreDID; +use identity_iota::did::DID; +use identity_iota::iota::IotaClientExt; +use identity_iota::iota::IotaDocument; +use identity_iota::iota::IotaIdentityClientExt; +use identity_iota::iota::NetworkName; +use identity_iota::resolver::Resolver; +use identity_iota::storage::JwkMemStore; +use identity_iota::storage::JwpDocumentExt; +use identity_iota::storage::KeyIdMemstore; +use identity_iota::storage::KeyType; +use identity_iota::verification::MethodScope; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; +use iota_sdk::client::Client; +use iota_sdk::client::Password; +use iota_sdk::types::block::address::Address; +use iota_sdk::types::block::output::AliasOutput; +use jsonprooftoken::jpa::algs::ProofAlgorithm; + +// Creates a DID with a JWP verification method. +async fn create_did( + client: &Client, + secret_manager: &SecretManager, + storage: &MemStorage, + key_type: KeyType, + alg: ProofAlgorithm, +) -> anyhow::Result<(Address, IotaDocument, String)> { + // Get an address with funds for testing. + let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT).await?; + + // Get the Bech32 human-readable part (HRP) of the network. + let network_name: NetworkName = client.network_name().await?; + + // Create a new DID document with a placeholder DID. + // The DID will be derived from the Alias Id of the Alias Output after publishing. + let mut document: IotaDocument = IotaDocument::new(&network_name); + + let fragment = document + .generate_method_jwp(storage, key_type, alg, None, MethodScope::VerificationMethod) + .await?; + + // Construct an Alias Output containing the DID document, with the wallet address + // set as both the state controller and governor. + let alias_output: AliasOutput = client.new_did_output(address, document, None).await?; + + // Publish the Alias Output and get the published DID document. + let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; + println!("Published DID document: {document:#}"); + + Ok((address, document, fragment)) +} + +/// Demonstrates how to create an Anonymous Credential with BBS+. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // =========================================================================== + // Step 1: Create identity for the issuer. + // =========================================================================== + + // Create a new client to interact with the IOTA ledger. + let client: Client = Client::builder() + .with_primary_node(API_ENDPOINT, None)? + .finish() + .await?; + + let secret_manager_issuer = SecretManager::Stronghold( + StrongholdSecretManager::builder() + .password(Password::from("secure_password_1".to_owned())) + .build(random_stronghold_path())?, + ); + + let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + + let (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did( + &client, + &secret_manager_issuer, + &storage_issuer, + JwkMemStore::BLS12381SHA256_KEY_TYPE, + ProofAlgorithm::BLS12381_SHA256, + ) + .await?; + + // =========================================================================== + // Step 2: Issuer creates and signs a Verifiable Credential with BBS algorithm. + // =========================================================================== + + // Create a credential subject indicating the degree earned by Alice. + let subject: Subject = Subject::from_json_value(json!({ + "name": "Alice", + "mainCourses": ["Object-oriented Programming", "Mathematics"], + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", + }))?; + + // Build credential using subject above and issuer. + let credential: Credential = CredentialBuilder::default() + .id(Url::parse("https://example.edu/credentials/3732")?) + .issuer(Url::parse(issuer_document.id().as_str())?) + .type_("UniversityDegreeCredential") + .subject(subject) + .build()?; + + let credential_jpt: Jpt = issuer_document + .create_credential_jpt( + &credential, + &storage_issuer, + &fragment_issuer, + &JwpCredentialOptions::default(), + None, + ) + .await?; + + // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure, + // that the issuance date is not in the future and that the expiration date is not in the past: + let decoded_jpt = JptCredentialValidator::validate::<_, Object>( + &credential_jpt, + &issuer_document, + &JptCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); + + assert_eq!(credential, decoded_jpt.credential); + + // =========================================================================== + // Step 3: Issuer sends the Verifiable Credential to the holder. + // =========================================================================== + println!( + "Sending credential (as JPT) to the holder: {}\n", + credential_jpt.as_str() + ); + + // ============================================================================================ + // Step 4: Holder resolve Issuer's DID, retrieve Issuer's document and validate the Credential + // ============================================================================================ + + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client); + + // Holder resolve Issuer DID + let issuer: CoreDID = JptCredentialValidatorUtils::extract_issuer_from_issued_jpt(&credential_jpt).unwrap(); + let issuer_document: IotaDocument = resolver.resolve(&issuer).await?; + + // Holder validate the credential and retrieve the JwpIssued, needed to construct the JwpPresented + let decoded_credential = JptCredentialValidator::validate::<_, Object>( + &credential_jpt, + &issuer_document, + &JptCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); + + // =========================================================================== + // Step 5: Verifier sends the holder a challenge and requests a Presentation. + // + // Please be aware that when we mention "Presentation," we are not alluding to the Verifiable Presentation standard as defined by W3C (https://www.w3.org/TR/vc-data-model/#presentations). + // Instead, our reference is to a JWP Presentation (https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form), which differs from the W3C standard. + // =========================================================================== + + // A unique random challenge generated by the requester per presentation can mitigate replay attacks. + let challenge: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + // ========================================================================================================= + // Step 6: Holder engages in the Selective Disclosure of credential's attributes. + // ========================================================================================================= + + let method_id = decoded_credential + .decoded_jwp + .get_issuer_protected_header() + .kid() + .unwrap(); + + let mut selective_disclosure_presentation = SelectiveDisclosurePresentation::new(&decoded_credential.decoded_jwp); + selective_disclosure_presentation + .conceal_in_subject("mainCourses[1]") + .unwrap(); + selective_disclosure_presentation + .conceal_in_subject("degree.name") + .unwrap(); + + // ======================================================================================================================================= + // Step 7: Holder needs Issuer's Public Key to compute the Signature Proof of Knowledge and construct the Presentation + // JPT. + // ======================================================================================================================================= + + // Construct a JPT(JWP in the Presentation form) representing the Selectively Disclosed Verifiable Credential + let presentation_jpt: Jpt = issuer_document + .create_presentation_jpt( + &mut selective_disclosure_presentation, + method_id, + &JwpPresentationOptions::default().nonce(challenge), + ) + .await?; + + // =========================================================================== + // Step 8: Holder sends a Presentation JPT to the Verifier. + // =========================================================================== + + println!( + "Sending presentation (as JPT) to the verifier: {}\n", + presentation_jpt.as_str() + ); + + // =========================================================================== + // Step 9: Verifier receives the Presentation and verifies it. + // =========================================================================== + + // Verifier resolve Issuer DID + let issuer: CoreDID = JptPresentationValidatorUtils::extract_issuer_from_presented_jpt(&presentation_jpt).unwrap(); + let issuer_document: IotaDocument = resolver.resolve(&issuer).await?; + + let presentation_validation_options = JptPresentationValidationOptions::default().nonce(challenge); + + // Verifier validate the Presented Credential and retrieve the JwpPresented + let decoded_presented_credential = JptPresentationValidator::validate::<_, Object>( + &presentation_jpt, + &issuer_document, + &presentation_validation_options, + FailFast::FirstError, + ) + .unwrap(); + + // Since no errors were thrown by `verify_presentation` we know that the validation was successful. + println!( + "Presented Credential successfully validated: {:#?}", + decoded_presented_credential.credential + ); + + Ok(()) +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 81dcc688bd..136049bb28 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -11,6 +11,7 @@ identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-feature identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021"] } identity_stronghold = { path = "../identity_stronghold", default-features = false } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } +json-proof-token.workspace = true primitive-types = "0.12.1" rand = "0.8.5" sd-jwt-payload = { version = "0.2.0", default-features = false, features = ["sha"] } @@ -91,3 +92,11 @@ name = "7_sd_jwt" [[example]] path = "1_advanced/8_status_list_2021.rs" name = "8_status_list_2021" + +[[example]] +path = "1_advanced/9_zkp.rs" +name = "9_zkp" + +[[example]] +path = "1_advanced/10_zkp_revocation.rs" +name = "10_zkp_revocation" \ No newline at end of file diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 876d5577d5..a09b971799 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -31,6 +31,9 @@ serde_repr = { version = "0.1", default-features = false, optional = true } strum.workspace = true thiserror.workspace = true url = { version = "2.5", default-features = false } +json-proof-token.workspace = true +zkryptium.workspace = true +async-trait = { version = "0.1.64", default-features = false } [dev-dependencies] anyhow = "1.0.62" diff --git a/identity_credential/src/credential/credential.rs b/identity_credential/src/credential/credential.rs index decbb8b7c2..bd7860c4e3 100644 --- a/identity_credential/src/credential/credential.rs +++ b/identity_credential/src/credential/credential.rs @@ -5,6 +5,7 @@ use core::fmt::Display; use core::fmt::Formatter; use identity_core::convert::ToJson; +use jsonprooftoken::jpt::claims::JptClaims; use once_cell::sync::Lazy; use serde::Deserialize; use serde::Serialize; @@ -174,6 +175,15 @@ impl Credential { .to_json() .map_err(|err| Error::JwtClaimsSetSerializationError(err.into())) } + + ///Serializes the [`Credential`] as a JPT claims set + pub fn serialize_jpt(&self, custom_claims: Option) -> Result + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?; + Ok(jwt_representation.into()) + } } impl Display for Credential diff --git a/identity_credential/src/credential/jpt.rs b/identity_credential/src/credential/jpt.rs new file mode 100644 index 0000000000..0ee1355734 --- /dev/null +++ b/identity_credential/src/credential/jpt.rs @@ -0,0 +1,30 @@ +use serde::Deserialize; +use serde::Serialize; + +/// This JSON Proof Token could represent a JWP both in the Issued and Presented forms. +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +pub struct Jpt(String); + +impl Jpt { + /// Creates a new `Jwt` from the given string. + pub fn new(jpt_string: String) -> Self { + Self(jpt_string) + } + + /// Returns a reference of the JWT string. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl From for Jpt { + fn from(jpt: String) -> Self { + Self::new(jpt) + } +} + +impl From for String { + fn from(jpt: Jpt) -> Self { + jpt.0 + } +} diff --git a/identity_credential/src/credential/jwp_credential_options.rs b/identity_credential/src/credential/jwp_credential_options.rs new file mode 100644 index 0000000000..acecc499b5 --- /dev/null +++ b/identity_credential/src/credential/jwp_credential_options.rs @@ -0,0 +1,25 @@ +/// Options for creating a JSON Web Proof. +#[non_exhaustive] +#[derive(Debug, Default, serde::Serialize, serde::Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +#[serde(default)] +pub struct JwpCredentialOptions { + /// The kid to set in the Issuer Protected Header. + /// + /// If unset, the kid of the JWK with which the JWP is produced is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub kid: Option, +} + +impl JwpCredentialOptions { + /// Creates a new [`JwsSignatureOptions`]. + pub fn new() -> Self { + Self::default() + } + + /// Replace the value of the `kid` field. + pub fn kid(mut self, value: impl Into) -> Self { + self.kid = Some(value.into()); + self + } +} diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index 8a9bc280e8..29c1e0fb86 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; +use jsonprooftoken::jpt::claims::JptClaims; use serde::Deserialize; use serde::Serialize; @@ -360,6 +361,56 @@ where proof: Option>, } +impl<'credential, T> From> for JptClaims +where + T: ToOwned + Serialize, + ::Owned: DeserializeOwned, +{ + fn from(item: CredentialJwtClaims<'credential, T>) -> Self { + let CredentialJwtClaims { + exp, + iss, + issuance_date, + jti, + sub, + vc, + custom, + } = item; + + let mut claims = JptClaims::new(); + + if let Some(exp) = exp { + claims.set_exp(exp); + } + + claims.set_iss(iss.url().to_string()); + + if let Some(iat) = issuance_date.iat { + claims.set_iat(iat); + } + + if let Some(nbf) = issuance_date.nbf { + claims.set_nbf(nbf); + } + + if let Some(jti) = jti { + claims.set_jti(jti.to_string()); + } + + if let Some(sub) = sub { + claims.set_sub(sub.to_string()); + } + + claims.set_claim(Some("vc"), vc, true); + + if let Some(custom) = custom { + claims.set_claim(None, custom, true); + } + + claims + } +} + #[cfg(test)] mod tests { use identity_core::common::Object; diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index efa20a3c87..77c65f74af 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -9,6 +9,8 @@ mod builder; mod credential; mod evidence; mod issuer; +mod jpt; +mod jwp_credential_options; mod jws; mod jwt; mod jwt_serialization; @@ -26,6 +28,8 @@ pub use self::builder::CredentialBuilder; pub use self::credential::Credential; pub use self::evidence::Evidence; pub use self::issuer::Issuer; +pub use self::jpt::Jpt; +pub use self::jwp_credential_options::JwpCredentialOptions; pub use self::jws::Jws; pub use self::jwt::Jwt; pub use self::linked_domain_service::LinkedDomainService; @@ -33,6 +37,8 @@ pub use self::policy::Policy; pub use self::proof::Proof; pub use self::refresh::RefreshService; #[cfg(feature = "revocation-bitmap")] +pub use self::revocation_bitmap_status::try_index_to_u32; +#[cfg(feature = "revocation-bitmap")] pub use self::revocation_bitmap_status::RevocationBitmapStatus; pub use self::schema::Schema; pub use self::status::Status; diff --git a/identity_credential/src/credential/revocation_bitmap_status.rs b/identity_credential/src/credential/revocation_bitmap_status.rs index d4310d154a..b607e1758d 100644 --- a/identity_credential/src/credential/revocation_bitmap_status.rs +++ b/identity_credential/src/credential/revocation_bitmap_status.rs @@ -129,7 +129,7 @@ impl From for Status { } /// Attempts to convert the given index string to a u32. -fn try_index_to_u32(index: &str, name: &str) -> Result { +pub fn try_index_to_u32(index: &str, name: &str) -> Result { u32::from_str(index).map_err(|err| { Error::InvalidStatus(format!( "{name} cannot be converted to an unsigned, 32-bit integer: {err}", diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 356d89d3d2..551521cffd 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -68,4 +68,12 @@ pub enum Error { /// JSON. #[error("could not deserialize JWT claims set")] JwtClaimsSetDeserializationError(#[source] Box), + + /// Caused by a failure to deserialize the JPT claims set representation of a `Credential` JSON. + #[error("could not deserialize JWT claims set")] + JptClaimsSetDeserializationError(#[source] Box), + + /// Cause by an invalid attribute path + #[error("Attribute Not found")] + SelectiveDiscosureError, } diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs new file mode 100644 index 0000000000..76abfc297a --- /dev/null +++ b/identity_credential/src/presentation/jwp_presentation_builder.rs @@ -0,0 +1,95 @@ +use crate::error::Error; +use crate::error::Result; +use jsonprooftoken::jwp::header::PresentationProtectedHeader; +use jsonprooftoken::jwp::issued::JwpIssued; +use jsonprooftoken::jwp::presented::JwpPresentedBuilder; + +/// Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes +/// - @context MUST NOT be blinded +/// - id MUST be blinded +/// - type MUST NOT be blinded +/// - issuer MUST NOT be blinded +/// - issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used) +/// - expirationDate MUST be blinded (if Timeframe Revocation mechanism is used) +/// - credentialSubject (User have to choose which attribute must be blinded) +/// - credentialSchema MUST NOT be blinded +/// - credentialStatus MUST NOT be blinded +/// - refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism) +/// - termsOfUse NO reason to use it in ZK VC (will be in any case blinded) +/// - evidence (User have to choose which attribute must be blinded) +pub struct SelectiveDisclosurePresentation { + jwp_builder: JwpPresentedBuilder, +} + +impl SelectiveDisclosurePresentation { + /// Inizialize a presentation starting from an Issued JWP. + /// The properties `jti`, `nbf`, `issuanceDate`, `expirationDate` and `terminsOfUse` are concealed by default. + pub fn new(issued_jwp: &JwpIssued) -> Self { + let mut jwp_builder = JwpPresentedBuilder::new(issued_jwp); + + jwp_builder.set_undisclosed("jti").ok(); // contains the credential's id, provides linkability + + jwp_builder.set_undisclosed("issuanceDate").ok(); // Depending on the revocation method used it will be necessary or not + jwp_builder.set_undisclosed("nbf").ok(); + + jwp_builder.set_undisclosed("expirationDate").ok(); // Depending on the revocation method used it will be necessary or not + jwp_builder.set_undisclosed("exp").ok(); + + jwp_builder.set_undisclosed("termsOfUse").ok(); // Provides linkability so, there is NO reason to use it in ZK VC + + jwp_builder + .set_undisclosed("vc.credentialStatus.revocationBitmapIndex") + .ok(); + + jwp_builder.set_undisclosed("vc.credentialSubject.id").ok(); + jwp_builder.set_undisclosed("sub").ok(); + + Self { jwp_builder } + } + + /// Selectively disclose "credentialSubject" attributes. + /// # Example + /// ``` + /// { + /// "id": 1234, + /// "name": "Alice", + /// "mainCourses": ["Object-oriented Programming", "Mathematics"], + /// "degree": { + /// "type": "BachelorDegree", + /// "name": "Bachelor of Science and Arts", + /// }, + /// "GPA": "4.0", + /// } + /// ``` + /// If you want to undisclose for example the Mathematics course and the name of the degree: + /// ``` + /// undisclose_subject("mainCourses[1]"); + /// undisclose_subject("degree.name"); + /// ``` + pub fn conceal_in_subject(&mut self, path: &str) -> Result<(), Error> { + let _ = self + .jwp_builder + .set_undisclosed(&("vc.credentialSubject.".to_owned() + path)) + .map_err(|_| Error::SelectiveDiscosureError); + Ok(()) + } + + /// Undisclose "evidence" attributes. + pub fn conceal_in_evidence(&mut self, path: &str) -> Result<(), Error> { + let _ = self + .jwp_builder + .set_undisclosed(&("vc.evidence.".to_owned() + path)) + .map_err(|_| Error::SelectiveDiscosureError); + Ok(()) + } + + /// Set Presenation Protected Header. + pub fn set_presentation_header(&mut self, ph: PresentationProtectedHeader) { + self.jwp_builder.presentation_protected_header(ph); + } + + /// Get the builder. + pub fn builder(&self) -> &JwpPresentedBuilder { + &self.jwp_builder + } +} diff --git a/identity_credential/src/presentation/jwp_presentation_options.rs b/identity_credential/src/presentation/jwp_presentation_options.rs new file mode 100644 index 0000000000..927be649f8 --- /dev/null +++ b/identity_credential/src/presentation/jwp_presentation_options.rs @@ -0,0 +1,30 @@ +use identity_core::common::Url; +use serde::Deserialize; +use serde::Serialize; + +/// Options to be set in the JWT claims of a verifiable presentation. +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct JwpPresentationOptions { + /// Sets the audience for presentation (`aud` property in JWP Presentation Header). + /// Default: `None`. + #[serde(skip_serializing_if = "Option::is_none")] + pub audience: Option, + + /// The nonce to be placed in the Presentation Protected Header. + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, +} + +impl JwpPresentationOptions { + /// Sets the audience for presentation (`aud` property in JWT claims). + pub fn audience(mut self, audience: Url) -> Self { + self.audience = Some(audience); + self + } + + /// Replace the value of the `nonce` field. + pub fn nonce(mut self, value: impl Into) -> Self { + self.nonce = Some(value.into()); + self + } +} diff --git a/identity_credential/src/presentation/mod.rs b/identity_credential/src/presentation/mod.rs index 94f8768e02..8202e03c8e 100644 --- a/identity_credential/src/presentation/mod.rs +++ b/identity_credential/src/presentation/mod.rs @@ -5,14 +5,18 @@ #![allow(clippy::module_inception)] +mod jwp_presentation_builder; +mod jwp_presentation_options; mod jwt_presentation_options; mod jwt_serialization; mod presentation; mod presentation_builder; +pub use self::jwp_presentation_builder::SelectiveDisclosurePresentation; pub use self::jwt_presentation_options::JwtPresentationOptions; pub use self::presentation::Presentation; pub use self::presentation_builder::PresentationBuilder; +pub use jwp_presentation_options::JwpPresentationOptions; #[cfg(feature = "validator")] pub(crate) use self::jwt_serialization::PresentationJwtClaims; diff --git a/identity_credential/src/revocation/mod.rs b/identity_credential/src/revocation/mod.rs index 6732ff4194..f98ad3f89b 100644 --- a/identity_credential/src/revocation/mod.rs +++ b/identity_credential/src/revocation/mod.rs @@ -9,6 +9,9 @@ mod revocation_bitmap_2022; #[cfg(feature = "status-list-2021")] pub mod status_list_2021; +pub mod validity_timeframe_2024; + pub use self::error::RevocationError; pub use self::error::RevocationResult; pub use revocation_bitmap_2022::*; +pub use validity_timeframe_2024::*; diff --git a/identity_credential/src/revocation/validity_timeframe_2024/mod.rs b/identity_credential/src/revocation/validity_timeframe_2024/mod.rs new file mode 100644 index 0000000000..d4f1ab0614 --- /dev/null +++ b/identity_credential/src/revocation/validity_timeframe_2024/mod.rs @@ -0,0 +1,5 @@ +//! Implementation of a new Revocation mechanism for ZK Verifiable Credentials. + +mod revocation_timeframe_status; + +pub use revocation_timeframe_status::*; diff --git a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs new file mode 100644 index 0000000000..e0ceaf4778 --- /dev/null +++ b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs @@ -0,0 +1,287 @@ +use crate::credential::try_index_to_u32; +use crate::credential::Status; +use crate::error::Error; +use crate::error::Result; +use identity_core::common::Duration; +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_core::common::Value; +use identity_did::DIDUrl; +use std::str::FromStr; + +/// Information used to determine the current status of a [`Credential`][crate::credential::Credential] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RevocationTimeframeStatus(Status); + +impl RevocationTimeframeStatus { + /// startValidityTimeframe property name. + pub const START_TIMEFRAME_PROPERTY: &'static str = "startValidityTimeframe"; + /// endValidityTimeframe property name. + pub const END_TIMEFRAME_PROPERTY: &'static str = "endValidityTimeframe"; + const INDEX_PROPERTY: &'static str = "revocationBitmapIndex"; + + /// Type name of the revocation mechanism. + pub const TYPE: &'static str = "RevocationTimeframe2024"; + + /// Creates a new `RevocationTimeframeStatus`. + pub fn new(start_validity: Option, duration: Duration, id: DIDUrl, index: u32) -> Result { + let mut object = Object::new(); + + let start_validity_timeframe = start_validity.unwrap_or(Timestamp::now_utc()); + + let end_validity_timeframe = start_validity_timeframe + .checked_add(duration) + .ok_or(Error::InvalidStatus( + "With that granularity, endValidityTimeFrame will turn out not to be in the valid range for RFC 3339" + .to_owned(), + ))?; + + // id.set_query(Some(&format!("index={index}"))) + // .expect("the string should be non-empty and a valid URL query"); + + object.insert( + Self::START_TIMEFRAME_PROPERTY.to_owned(), + Value::String(start_validity_timeframe.to_rfc3339()), + ); + object.insert( + Self::END_TIMEFRAME_PROPERTY.to_owned(), + Value::String(end_validity_timeframe.to_rfc3339()), + ); + object.insert(Self::INDEX_PROPERTY.to_owned(), Value::String(index.to_string())); + + Ok(Self(Status::new_with_properties( + Url::from(id), + Self::TYPE.to_owned(), + object, + ))) + } + + /// Get startValidityTimeframe value + pub fn start_validity_timeframe(&self) -> Result { + if let Some(Value::String(timeframe)) = self.0.properties.get(Self::START_TIMEFRAME_PROPERTY) { + Timestamp::from_str(timeframe) + .map_err(|_| Error::InvalidStatus(format!("property '{}' is not a string", Self::START_TIMEFRAME_PROPERTY))) + } else { + Err(Error::InvalidStatus(format!( + "property '{}' is not a string", + Self::START_TIMEFRAME_PROPERTY + ))) + } + } + + /// Get endValidityTimeframe value + pub fn end_validity_timeframe(&self) -> Result { + if let Some(Value::String(timeframe)) = self.0.properties.get(Self::END_TIMEFRAME_PROPERTY) { + Timestamp::from_str(timeframe) + .map_err(|_| Error::InvalidStatus(format!("property '{}' is not a string", Self::END_TIMEFRAME_PROPERTY))) + } else { + Err(Error::InvalidStatus(format!( + "property '{}' is not a string", + Self::END_TIMEFRAME_PROPERTY + ))) + } + } + + /// Returns the [`DIDUrl`] of the `RevocationBitmapStatus`, which should resolve + /// to a `RevocationBitmap2022` service in a DID Document. + pub fn id(&self) -> Result { + DIDUrl::parse(self.0.id.as_str()) + .map_err(|err| Error::InvalidStatus(format!("invalid DID Url '{}': {:?}", self.0.id, err))) + } + + /// Returns the index of the credential in the issuer's revocation bitmap if it can be decoded. + pub fn index(&self) -> Result { + if let Some(Value::String(index)) = self.0.properties.get(Self::INDEX_PROPERTY) { + try_index_to_u32(index, Self::INDEX_PROPERTY) + } else { + Err(Error::InvalidStatus(format!( + "expected {} to be an unsigned 32-bit integer expressed as a string", + Self::INDEX_PROPERTY + ))) + } + } +} + +impl TryFrom for RevocationTimeframeStatus { + type Error = Error; + + fn try_from(status: Status) -> Result { + if status.type_ != Self::TYPE { + return Err(Self::Error::InvalidStatus(format!( + "expected type '{}', got '{}'", + Self::TYPE, + status.type_ + ))); + } + + let start_revocation_timeframe: &Value = + if let Some(start_revocation_timeframe) = status.properties.get(Self::START_TIMEFRAME_PROPERTY) { + start_revocation_timeframe + } else { + return Err(Self::Error::InvalidStatus(format!( + "missing required property '{}'", + Self::START_TIMEFRAME_PROPERTY + ))); + }; + + if let Value::String(timeframe) = start_revocation_timeframe { + Timestamp::from_str(timeframe).map_err(|_| { + Self::Error::InvalidStatus(format!( + "property '{}' is not a valid Timestamp", + Self::START_TIMEFRAME_PROPERTY + )) + })? + } else { + return Err(Self::Error::InvalidStatus(format!( + "property '{}' is not a string", + Self::START_TIMEFRAME_PROPERTY + ))); + }; + + let end_revocation_timeframe: &Value = + if let Some(end_revocation_timeframe) = status.properties.get(Self::END_TIMEFRAME_PROPERTY) { + end_revocation_timeframe + } else { + return Err(Self::Error::InvalidStatus(format!( + "missing required property '{}'", + Self::END_TIMEFRAME_PROPERTY + ))); + }; + + if let Value::String(timeframe) = end_revocation_timeframe { + Timestamp::from_str(timeframe).map_err(|_| { + Self::Error::InvalidStatus(format!( + "property '{}' is not a valid Timestamp", + Self::END_TIMEFRAME_PROPERTY + )) + })? + } else { + return Err(Self::Error::InvalidStatus(format!( + "property '{}' is not a string", + Self::END_TIMEFRAME_PROPERTY + ))); + }; + + let revocation_bitmap_index: &Value = + if let Some(revocation_bitmap_index) = status.properties.get(Self::INDEX_PROPERTY) { + revocation_bitmap_index + } else { + return Err(Error::InvalidStatus(format!( + "missing required property '{}'", + Self::INDEX_PROPERTY + ))); + }; + + if let Value::String(index) = revocation_bitmap_index { + try_index_to_u32(index, Self::INDEX_PROPERTY)? + } else { + return Err(Error::InvalidStatus(format!( + "property '{}' is not a string", + Self::INDEX_PROPERTY + ))); + }; + + Ok(Self(status)) + } +} + +impl From for Status { + fn from(status: RevocationTimeframeStatus) -> Self { + status.0 + } +} + +/// Verifier +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VerifierRevocationTimeframeStatus(pub(crate) RevocationTimeframeStatus); + +impl TryFrom for VerifierRevocationTimeframeStatus { + type Error = Error; + + fn try_from(status: Status) -> Result { + if status.type_ != RevocationTimeframeStatus::TYPE { + return Err(Self::Error::InvalidStatus(format!( + "expected type '{}', got '{}'", + RevocationTimeframeStatus::TYPE, + status.type_ + ))); + } + + let start_revocation_timeframe: &Value = if let Some(start_revocation_timeframe) = status + .properties + .get(RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY) + { + start_revocation_timeframe + } else { + return Err(Self::Error::InvalidStatus(format!( + "missing required property '{}'", + RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY + ))); + }; + + if let Value::String(timeframe) = start_revocation_timeframe { + Timestamp::from_str(timeframe).map_err(|_| { + Self::Error::InvalidStatus(format!( + "property '{}' is not a valid Timestamp", + RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY + )) + })? + } else { + return Err(Self::Error::InvalidStatus(format!( + "property '{}' is not a string", + RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY + ))); + }; + + let end_revocation_timeframe: &Value = if let Some(end_revocation_timeframe) = + status.properties.get(RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY) + { + end_revocation_timeframe + } else { + return Err(Self::Error::InvalidStatus(format!( + "missing required property '{}'", + RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY + ))); + }; + + if let Value::String(timeframe) = end_revocation_timeframe { + Timestamp::from_str(timeframe).map_err(|_| { + Self::Error::InvalidStatus(format!( + "property '{}' is not a valid Timestamp", + RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY + )) + })? + } else { + return Err(Self::Error::InvalidStatus(format!( + "property '{}' is not a string", + RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY + ))); + }; + + let revocation_bitmap_index: &Value = + if let Some(revocation_bitmap_index) = status.properties.get(RevocationTimeframeStatus::INDEX_PROPERTY) { + revocation_bitmap_index + } else { + return Err(Error::InvalidStatus(format!( + "missing required property '{}'", + RevocationTimeframeStatus::INDEX_PROPERTY + ))); + }; + + if &Value::Null != revocation_bitmap_index { + return Err(Error::InvalidStatus(format!( + "property '{}' is not a Null", + RevocationTimeframeStatus::INDEX_PROPERTY + ))); + }; + + Ok(Self(RevocationTimeframeStatus(status))) + } +} + +impl From for Status { + fn from(status: VerifierRevocationTimeframeStatus) -> Self { + status.0 .0 + } +} diff --git a/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs b/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs new file mode 100644 index 0000000000..1a970e018a --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs @@ -0,0 +1,16 @@ +use identity_core::common::Object; +use jsonprooftoken::jwp::issued::JwpIssued; + +use crate::credential::Credential; + +/// Decoded [`Credential`] from a cryptographically verified JWP. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct DecodedJptCredential { + /// The decoded credential parsed to the [Verifiable Credentials Data model](https://www.w3.org/TR/vc-data-model/). + pub credential: Credential, + /// The custom claims parsed from the JPT. + pub custom_claims: Option, + /// The decoded and verifier Issued JWP, will be used to construct the Presented JWP + pub decoded_jwp: JwpIssued, +} diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs new file mode 100644 index 0000000000..918afd749a --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs @@ -0,0 +1,84 @@ +use crate::validator::SubjectHolderRelationship; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_document::verifiable::JwpVerificationOptions; +use serde::Deserialize; +use serde::Serialize; + +/// Options to declare validation criteria for [`Credential`](crate::credential::Credential)s. +#[non_exhaustive] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JptCredentialValidationOptions { + /// Declares that the credential is **not** considered valid if it expires before this + /// [`Timestamp`]. + /// Uses the current datetime during validation if not set. + #[serde(default)] + pub earliest_expiry_date: Option, + + /// Declares that the credential is **not** considered valid if it was issued later than this + /// [`Timestamp`]. + /// Uses the current datetime during validation if not set. + #[serde(default)] + pub latest_issuance_date: Option, + + /// Validation behaviour for [`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). + /// + /// Default: [`StatusCheck::Strict`](crate::validator::StatusCheck::Strict). + #[serde(default)] + pub status: crate::validator::StatusCheck, + + /// Declares how credential subjects must relate to the presentation holder during validation. + /// + /// + pub subject_holder_relationship: Option<(Url, SubjectHolderRelationship)>, + + /// Options which affect the verification of the proof on the credential. + #[serde(default)] + pub verification_options: JwpVerificationOptions, +} + +impl JptCredentialValidationOptions { + /// Constructor that sets all options to their defaults. + pub fn new() -> Self { + Self::default() + } + + /// Declare that the credential is **not** considered valid if it expires before this [`Timestamp`]. + /// Uses the current datetime during validation if not set. + pub fn earliest_expiry_date(mut self, timestamp: Timestamp) -> Self { + self.earliest_expiry_date = Some(timestamp); + self + } + + /// Declare that the credential is **not** considered valid if it was issued later than this [`Timestamp`]. + /// Uses the current datetime during validation if not set. + pub fn latest_issuance_date(mut self, timestamp: Timestamp) -> Self { + self.latest_issuance_date = Some(timestamp); + self + } + + /// Sets the validation behaviour for [`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). + pub fn status_check(mut self, status_check: crate::validator::StatusCheck) -> Self { + self.status = status_check; + self + } + + /// Declares how credential subjects must relate to the presentation holder during validation. + /// + /// + pub fn subject_holder_relationship( + mut self, + holder: Url, + subject_holder_relationship: SubjectHolderRelationship, + ) -> Self { + self.subject_holder_relationship = Some((holder, subject_holder_relationship)); + self + } + + /// Set options which affect the verification of the JWP proof. + pub fn verification_options(mut self, options: JwpVerificationOptions) -> Self { + self.verification_options = options; + self + } +} diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs new file mode 100644 index 0000000000..617ea7c5f7 --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs @@ -0,0 +1,222 @@ +use identity_core::convert::FromJson; +use identity_core::convert::ToJson; +use identity_did::CoreDID; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use identity_document::verifiable::JwpVerificationOptions; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwk::key::Jwk as JwkExt; +use jsonprooftoken::jwp::issued::JwpIssuedDecoder; + +use super::DecodedJptCredential; +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; +use crate::credential::Jpt; +use crate::validator::jwt_credential_validation::SignerContext; +use crate::validator::CompoundCredentialValidationError; +use crate::validator::FailFast; +use crate::validator::JptCredentialValidationOptions; +use crate::validator::JwtCredentialValidatorUtils; +use crate::validator::JwtValidationError; + +/// A type for decoding and validating [`Credential`]s in JPT format. +#[non_exhaustive] +pub struct JptCredentialValidator; + +impl JptCredentialValidator { + /// Decodes and validates a [`Credential`] issued as a JPT (JWP Issued Form). A [`DecodedJptCredential`] is returned + /// upon success. + /// + /// The following properties are validated according to `options`: + /// - the issuer's proof on the JWP, + /// - the expiration date, + /// - the issuance date, + /// - the semantic structure. + pub fn validate( + credential_jpt: &Jpt, + issuer: &DOC, + options: &JptCredentialValidationOptions, + fail_fast: FailFast, + ) -> Result, CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + // First verify the JWP proof and decode the result into a credential token, then apply all other validations. + let credential_token = + Self::verify_proof(credential_jpt, issuer, &options.verification_options).map_err(|err| { + CompoundCredentialValidationError { + validation_errors: [err].into(), + } + })?; + + let credential: &Credential = &credential_token.credential; + + Self::validate_credential::(credential, options, fail_fast)?; + + Ok(credential_token) + } + + pub(crate) fn validate_credential( + credential: &Credential, + options: &JptCredentialValidationOptions, + fail_fast: FailFast, + ) -> Result<(), CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + // Run all single concern Credential validations in turn and fail immediately if `fail_fast` is true. + let expiry_date_validation = std::iter::once_with(|| { + JwtCredentialValidatorUtils::check_expires_on_or_after( + credential, + options.earliest_expiry_date.unwrap_or_default(), + ) + }); + + let issuance_date_validation = std::iter::once_with(|| { + JwtCredentialValidatorUtils::check_issued_on_or_before( + credential, + options.latest_issuance_date.unwrap_or_default(), + ) + }); + + let structure_validation = std::iter::once_with(|| JwtCredentialValidatorUtils::check_structure(credential)); + + let subject_holder_validation = std::iter::once_with(|| { + options + .subject_holder_relationship + .as_ref() + .map(|(holder, relationship)| { + JwtCredentialValidatorUtils::check_subject_holder_relationship(credential, holder, *relationship) + }) + .unwrap_or(Ok(())) + }); + + let validation_units_iter = issuance_date_validation + .chain(expiry_date_validation) + .chain(structure_validation) + .chain(subject_holder_validation); + + let validation_units_error_iter = validation_units_iter.filter_map(|result| result.err()); + let validation_errors: Vec = match fail_fast { + FailFast::FirstError => validation_units_error_iter.take(1).collect(), + FailFast::AllErrors => validation_units_error_iter.collect(), + }; + + if validation_errors.is_empty() { + Ok(()) + } else { + Err(CompoundCredentialValidationError { validation_errors }) + } + } + + /// Proof verification function + fn verify_proof( + credential: &Jpt, + issuer: &DOC, + options: &JwpVerificationOptions, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + let decoded = JwpIssuedDecoder::decode(credential.as_str(), SerializationType::COMPACT) + .map_err(JwtValidationError::JwpDecodingError)?; + + // If no method_url is set, parse the `kid` to a DID Url which should be the identifier + // of a verification method in a trusted issuer's DID document. + let method_id: DIDUrl = match &options.method_id { + Some(method_id) => method_id.clone(), + None => { + let kid: &str = decoded + .get_header() + .kid() + .ok_or(JwtValidationError::MethodDataLookupError { + source: None, + message: "could not extract kid from protected header", + signer_ctx: SignerContext::Issuer, + })?; + + // Convert kid to DIDUrl + DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError { + source: Some(err.into()), + message: "could not parse kid as a DID Url", + signer_ctx: SignerContext::Issuer, + })? + } + }; + + // check issuer + let issuer: &CoreDocument = issuer.as_ref(); + + if issuer.id() != method_id.did() { + return Err(JwtValidationError::DocumentMismatch(SignerContext::Issuer)); + } + + // Obtain the public key from the issuer's DID document + let public_key: JwkExt = issuer + .resolve_method(&method_id, options.method_scope) + .and_then(|method| method.data().public_key_jwk()) + .and_then(|k| k.try_into().ok()) //Conversio into jsonprooftoken::Jwk type + .ok_or_else(|| JwtValidationError::MethodDataLookupError { + source: None, + message: "could not extract JWK from a method identified by kid", + signer_ctx: SignerContext::Issuer, + })?; + + let credential_token = Self::verify_decoded_jwp(decoded, &public_key)?; + + // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before + // returning. + let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?; + if &issuer_id != method_id.did() { + return Err(JwtValidationError::IdentifierMismatch { + signer_ctx: SignerContext::Issuer, + }); + }; + Ok(credential_token) + } + + /// Verify the decoded issued JWP proof using the given `public_key`. + fn verify_decoded_jwp( + decoded: JwpIssuedDecoder, + public_key: &JwkExt, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + // Verify Jwp proof + let decoded_jwp = decoded + .verify(public_key) + .map_err(JwtValidationError::JwpProofVerificationError)?; + + let claims = decoded_jwp.get_claims().ok_or("Claims not present").map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + let payloads = decoded_jwp.get_payloads(); + let jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads); + let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + + // Deserialize the raw claims + let credential_claims: CredentialJwtClaims<'_, T> = CredentialJwtClaims::from_json_slice(&jpt_claims_json) + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + let custom_claims = credential_claims.custom.clone(); + + // Construct the credential token containing the credential and the protected header. + let credential: Credential = credential_claims + .try_into_credential() + .map_err(JwtValidationError::CredentialStructure)?; + + Ok(DecodedJptCredential { + credential, + custom_claims, + decoded_jwp, + }) + } +} diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs new file mode 100644 index 0000000000..7a94ca422f --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs @@ -0,0 +1,236 @@ +use crate::credential::Credential; +use crate::revocation::RevocationDocumentExt; +use crate::revocation::RevocationTimeframeStatus; +use std::str::FromStr; + +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::convert::FromJson; +use identity_core::convert::ToJson; +use identity_did::DID; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwp::issued::JwpIssuedDecoder; + +use crate::credential::CredentialJwtClaims; +use crate::credential::Jpt; +use crate::validator::JwtValidationError; +use crate::validator::SignerContext; + +/// Utility functions for verifying JPT credentials. +#[derive(Debug)] +#[non_exhaustive] +pub struct JptCredentialValidatorUtils; + +type ValidationUnitResult = std::result::Result; + +impl JptCredentialValidatorUtils { + /// Utility for extracting the issuer field of a [`Credential`] as a DID. + /// + /// # Errors + /// + /// Fails if the issuer field is not a valid DID. + pub fn extract_issuer(credential: &Credential) -> std::result::Result + where + D: DID, + ::Err: std::error::Error + Send + Sync + 'static, + { + D::from_str(credential.issuer.url().as_str()).map_err(|err| JwtValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } + + /// Utility for extracting the issuer field of a credential in JPT representation as DID. + /// + /// # Errors + /// + /// If the JPT decoding fails or the issuer field is not a valid DID. + pub fn extract_issuer_from_issued_jpt(credential: &Jpt) -> std::result::Result + where + D: DID, + ::Err: std::error::Error + Send + Sync + 'static, + { + let decoded = JwpIssuedDecoder::decode(credential.as_str(), SerializationType::COMPACT) + .map_err(JwtValidationError::JwpDecodingError)?; + let claims = decoded + .get_header() + .claims() + .ok_or("Claims not present") + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + let payloads = decoded.get_payloads(); + let jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads); + let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + + // Deserialize the raw claims + let credential_claims: CredentialJwtClaims<'_, Object> = CredentialJwtClaims::from_json_slice(&jpt_claims_json) + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + D::from_str(credential_claims.iss.url().as_str()).map_err(|err| JwtValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } + + /// Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`. + pub fn check_timeframes_with_validity_timeframe_2024( + credential: &Credential, + validity_timeframe: Option, + status_check: crate::validator::StatusCheck, + ) -> ValidationUnitResult { + if status_check == crate::validator::StatusCheck::SkipAll { + return Ok(()); + } + + match &credential.credential_status { + None => Ok(()), + Some(status) => { + if status.type_ == RevocationTimeframeStatus::TYPE { + let status: RevocationTimeframeStatus = + RevocationTimeframeStatus::try_from(status.clone()).map_err(JwtValidationError::InvalidStatus)?; + + Self::check_validity_timeframe(status, validity_timeframe) + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } + + pub(crate) fn check_validity_timeframe( + status: RevocationTimeframeStatus, + validity_timeframe: Option, + ) -> ValidationUnitResult { + let timeframe = validity_timeframe.unwrap_or(Timestamp::now_utc()); + + let check = status.start_validity_timeframe().is_ok_and(|start| { + status + .end_validity_timeframe() + .is_ok_and(|end| timeframe >= start && timeframe <= end) + }); + + if !check { + Err(JwtValidationError::OutsideTimeframe) + } else { + Ok(()) + } + } + + /// Checks whether the credential status has been revoked + /// + /// Only supports `RevocationTimeframe2024`. + pub fn check_revocation_with_validity_timeframe_2024< + DOC: AsRef + ?Sized, + T, + >( + credential: &Credential, + issuer: &DOC, + status_check: crate::validator::StatusCheck, + ) -> ValidationUnitResult { + if status_check == crate::validator::StatusCheck::SkipAll { + return Ok(()); + } + + match &credential.credential_status { + None => Ok(()), + Some(status) => { + if status.type_ == RevocationTimeframeStatus::TYPE { + let status: RevocationTimeframeStatus = + RevocationTimeframeStatus::try_from(status.clone()).map_err(JwtValidationError::InvalidStatus)?; + + Self::check_revocation_bitmap(issuer, status) + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } + + /// Check the given `status` against the matching [`RevocationBitmap`] service in the issuer's DID Document. + fn check_revocation_bitmap + ?Sized>( + issuer: &DOC, + status: RevocationTimeframeStatus, + ) -> ValidationUnitResult { + let issuer_service_url: identity_did::DIDUrl = status.id().map_err(JwtValidationError::InvalidStatus)?; + + // Check whether index is revoked. + let revocation_bitmap: crate::revocation::RevocationBitmap = issuer + .as_ref() + .resolve_revocation_bitmap(issuer_service_url.into()) + .map_err(|_| JwtValidationError::ServiceLookupError)?; + let index: u32 = status.index().map_err(JwtValidationError::InvalidStatus)?; + if revocation_bitmap.is_revoked(index) { + Err(JwtValidationError::Revoked) + } else { + Ok(()) + } + } + + /// Checks whether the credential status has been revoked or the timeframe interval is INVALID + /// + /// Only supports `RevocationTimeframe2024`. + pub fn check_timeframes_and_revocation_with_validity_timeframe_2024< + DOC: AsRef + ?Sized, + T, + >( + credential: &Credential, + issuer: &DOC, + validity_timeframe: Option, + status_check: crate::validator::StatusCheck, + ) -> ValidationUnitResult { + if status_check == crate::validator::StatusCheck::SkipAll { + return Ok(()); + } + + match &credential.credential_status { + None => Ok(()), + Some(status) => { + if status.type_ == RevocationTimeframeStatus::TYPE { + let status: RevocationTimeframeStatus = + RevocationTimeframeStatus::try_from(status.clone()).map_err(JwtValidationError::InvalidStatus)?; + + let revocation = std::iter::once_with(|| Self::check_revocation_bitmap(issuer, status.clone())); + + let timeframes = std::iter::once_with(|| Self::check_validity_timeframe(status.clone(), validity_timeframe)); + + let checks_iter = revocation.chain(timeframes); + + let checks_error_iter = checks_iter.filter_map(|result| result.err()); + + let mut checks_errors: Vec = checks_error_iter.take(1).collect(); + + match checks_errors.pop() { + Some(err) => Err(err), + None => Ok(()), + } + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } +} diff --git a/identity_credential/src/validator/jpt_credential_validation/mod.rs b/identity_credential/src/validator/jpt_credential_validation/mod.rs new file mode 100644 index 0000000000..bc27a9f43a --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/mod.rs @@ -0,0 +1,9 @@ +mod decoded_jpt_credential; +mod jpt_credential_validation_options; +mod jpt_credential_validator; +mod jpt_credential_validator_utils; + +pub use decoded_jpt_credential::*; +pub use jpt_credential_validation_options::*; +pub use jpt_credential_validator::*; +pub use jpt_credential_validator_utils::*; diff --git a/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs b/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs new file mode 100644 index 0000000000..6693018dd4 --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs @@ -0,0 +1,19 @@ +use identity_core::common::Object; +use identity_core::common::Url; +use jsonprooftoken::jwp::presented::JwpPresented; + +use crate::credential::Credential; + +/// Decoded [`Credential`] from a cryptographically verified JWP. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct DecodedJptPresentation { + /// The decoded credential parsed to the [Verifiable Credentials Data model](https://www.w3.org/TR/vc-data-model/). + pub credential: Credential, + /// The `aud` property parsed from the JWT claims. + pub aud: Option, + /// The custom claims parsed from the JPT. + pub custom_claims: Option, + /// The decoded and verifier Issued JWP, will be used to construct the Presented JWP + pub decoded_jwp: JwpPresented, +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs new file mode 100644 index 0000000000..af5f6f7b0a --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs @@ -0,0 +1,37 @@ +use identity_document::verifiable::JwpVerificationOptions; +use serde::Deserialize; +use serde::Serialize; + +/// Criteria for validating a [`Presentation`](crate::presentation::Presentation). +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct JptPresentationValidationOptions { + /// The nonce to be placed in the Presentation Protected Header. + #[serde(default)] + pub nonce: Option, + + /// Options which affect the verification of the proof on the credential. + #[serde(default)] + pub verification_options: JwpVerificationOptions, +} + +impl JptPresentationValidationOptions { + /// Constructor that sets all options to their defaults. + pub fn new() -> Self { + Self::default() + } + + /// Declare that the presentation is **not** considered valid if it expires before this [`Timestamp`]. + /// Uses the current datetime during validation if not set. + pub fn nonce(mut self, nonce: impl Into) -> Self { + self.nonce = Some(nonce.into()); + self + } + + /// Set options which affect the verification of the JWP proof. + pub fn verification_options(mut self, options: JwpVerificationOptions) -> Self { + self.verification_options = options; + self + } +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs new file mode 100644 index 0000000000..8fb6e925df --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs @@ -0,0 +1,223 @@ +use std::str::FromStr; + +use identity_core::common::Url; +use identity_core::convert::FromJson; +use identity_core::convert::ToJson; +use identity_did::CoreDID; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwk::key::Jwk as JwkExt; +use jsonprooftoken::jwp::presented::JwpPresentedDecoder; + +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; +use crate::credential::Jpt; +use crate::validator::CompoundCredentialValidationError; +use crate::validator::FailFast; +use crate::validator::JwtCredentialValidatorUtils; +use crate::validator::JwtValidationError; +use crate::validator::SignerContext; + +use super::DecodedJptPresentation; +use super::JptPresentationValidationOptions; + +/// A type for decoding and validating Presented [`Credential`]s in JPT format. +#[non_exhaustive] +pub struct JptPresentationValidator; + +impl JptPresentationValidator { + /// Decodes and validates a Presented [`Credential`] issued as a JPT (JWP Presented Form). A + /// [`DecodedJptPresentation`] is returned upon success. + /// + /// The following properties are validated according to `options`: + /// - the holder's proof on the JWP, + /// - the expiration date, + /// - the issuance date, + /// - the semantic structure. + pub fn validate( + presentation_jpt: &Jpt, + issuer: &DOC, + options: &JptPresentationValidationOptions, + fail_fast: FailFast, + ) -> Result, CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + // First verify the JWP proof and decode the result into a presented credential token, then apply all other + // validations. + let presented_credential_token = + Self::verify_proof(presentation_jpt, issuer, options).map_err(|err| CompoundCredentialValidationError { + validation_errors: [err].into(), + })?; + + let credential: &Credential = &presented_credential_token.credential; + + Self::validate_presented_credential::(credential, fail_fast)?; + + Ok(presented_credential_token) + } + + pub(crate) fn validate_presented_credential( + credential: &Credential, + fail_fast: FailFast, + ) -> Result<(), CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + let structure_validation = std::iter::once_with(|| JwtCredentialValidatorUtils::check_structure(credential)); + + let validation_units_iter = structure_validation; + + let validation_units_error_iter = validation_units_iter.filter_map(|result| result.err()); + let validation_errors: Vec = match fail_fast { + FailFast::FirstError => validation_units_error_iter.take(1).collect(), + FailFast::AllErrors => validation_units_error_iter.collect(), + }; + + if validation_errors.is_empty() { + Ok(()) + } else { + Err(CompoundCredentialValidationError { validation_errors }) + } + } + + /// Proof verification function + fn verify_proof( + presentation_jpt: &Jpt, + issuer: &DOC, + options: &JptPresentationValidationOptions, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + let decoded: JwpPresentedDecoder = + JwpPresentedDecoder::decode(presentation_jpt.as_str(), SerializationType::COMPACT) + .map_err(JwtValidationError::JwpDecodingError)?; + + let nonce: Option<&String> = options.nonce.as_ref(); + // Validate the nonce + if decoded.get_presentation_header().nonce() != nonce { + return Err(JwtValidationError::JwsDecodingError( + identity_verification::jose::error::Error::InvalidParam("invalid nonce value"), + )); + } + + // If no method_url is set, parse the `kid` to a DID Url which should be the identifier + // of a verification method in a trusted issuer's DID document. + let method_id: DIDUrl = match &options.verification_options.method_id { + Some(method_id) => method_id.clone(), + None => { + let kid: &str = decoded + .get_issuer_header() + .kid() + .ok_or(JwtValidationError::MethodDataLookupError { + source: None, + message: "could not extract kid from protected header", + signer_ctx: SignerContext::Issuer, + })?; + + // Convert kid to DIDUrl + DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError { + source: Some(err.into()), + message: "could not parse kid as a DID Url", + signer_ctx: SignerContext::Issuer, + })? + } + }; + + // check issuer + let issuer: &CoreDocument = issuer.as_ref(); + + if issuer.id() != method_id.did() { + return Err(JwtValidationError::DocumentMismatch(SignerContext::Issuer)); + } + + // Obtain the public key from the issuer's DID document + let public_key: JwkExt = issuer + .resolve_method(&method_id, options.verification_options.method_scope) + .and_then(|method| method.data().public_key_jwk()) + .and_then(|k| k.try_into().ok()) //Conversio into jsonprooftoken::Jwk type + .ok_or_else(|| JwtValidationError::MethodDataLookupError { + source: None, + message: "could not extract JWK from a method identified by kid", + signer_ctx: SignerContext::Issuer, + })?; + + let credential_token = Self::verify_decoded_jwp(decoded, &public_key)?; + + // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before + // returning. + let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?; + if &issuer_id != method_id.did() { + return Err(JwtValidationError::IdentifierMismatch { + signer_ctx: SignerContext::Issuer, + }); + }; + Ok(credential_token) + } + + /// Verify the decoded presented JWP proof using the given `public_key`. + fn verify_decoded_jwp( + decoded: JwpPresentedDecoder, + public_key: &JwkExt, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + //Verify Jwp proof + let decoded_jwp = decoded + .verify(public_key) + .map_err(JwtValidationError::JwpProofVerificationError)?; + + let claims = decoded_jwp.get_claims().ok_or("Claims not present").map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + let payloads = decoded_jwp.get_payloads(); + let mut jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads); + // if not set the deserializatioon will throw an error since even the iat is not set, so we set this to 0 + jpt_claims.nbf.map_or_else( + || { + jpt_claims.set_nbf(0); + }, + |_| (), + ); + + let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + + // Deserialize the raw claims + let credential_claims: CredentialJwtClaims<'_, T> = CredentialJwtClaims::from_json_slice(&jpt_claims_json) + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + let custom_claims = credential_claims.custom.clone(); + + // Construct the credential token containing the credential and the protected header. + let credential: Credential = credential_claims + .try_into_credential() + .map_err(JwtValidationError::CredentialStructure)?; + + let aud: Option = decoded_jwp.get_presentation_protected_header().aud().and_then(|aud| { + Url::from_str(aud) + .map_err(|_| { + JwtValidationError::JwsDecodingError(identity_verification::jose::error::Error::InvalidParam( + "invalid audience value", + )) + }) + .ok() + }); + + Ok(DecodedJptPresentation { + credential, + aud, + custom_claims, + decoded_jwp, + }) + } +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs new file mode 100644 index 0000000000..dfb5ad280a --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs @@ -0,0 +1,96 @@ +use std::str::FromStr; + +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::convert::FromJson; +use identity_core::convert::ToJson; +use identity_did::DID; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwp::presented::JwpPresentedDecoder; + +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; +use crate::credential::Jpt; +use crate::revocation::RevocationTimeframeStatus; +use crate::revocation::VerifierRevocationTimeframeStatus; +use crate::validator::JptCredentialValidatorUtils; +use crate::validator::JwtValidationError; +use crate::validator::SignerContext; + +/// Utility functions for verifying JPT credentials. +#[derive(Debug)] +#[non_exhaustive] +pub struct JptPresentationValidatorUtils; + +type ValidationUnitResult = std::result::Result; + +impl JptPresentationValidatorUtils { + /// Utility for extracting the issuer field of a credential in JPT representation as DID. + /// + /// # Errors + /// + /// If the JPT decoding fails or the issuer field is not a valid DID. + pub fn extract_issuer_from_presented_jpt(presentation: &Jpt) -> std::result::Result + where + D: DID, + ::Err: std::error::Error + Send + Sync + 'static, + { + let decoded = JwpPresentedDecoder::decode(presentation.as_str(), SerializationType::COMPACT) + .map_err(JwtValidationError::JwpDecodingError)?; + let claims = decoded + .get_issuer_header() + .claims() + .ok_or("Claims not present") + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + let payloads = decoded.get_payloads(); + let jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads); + let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into())) + })?; + + // Deserialize the raw claims + let credential_claims: CredentialJwtClaims<'_, Object> = CredentialJwtClaims::from_json_slice(&jpt_claims_json) + .map_err(|err| { + JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + D::from_str(credential_claims.iss.url().as_str()).map_err(|err| JwtValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } + + /// Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`. + pub fn check_timeframes_with_validity_timeframe_2024( + credential: &Credential, + validity_timeframe: Option, + status_check: crate::validator::StatusCheck, + ) -> ValidationUnitResult { + if status_check == crate::validator::StatusCheck::SkipAll { + return Ok(()); + } + + match &credential.credential_status { + None => Ok(()), + Some(status) => { + if status.type_ == RevocationTimeframeStatus::TYPE { + let status: VerifierRevocationTimeframeStatus = + VerifierRevocationTimeframeStatus::try_from(status.clone()).map_err(JwtValidationError::InvalidStatus)?; + + JptCredentialValidatorUtils::check_validity_timeframe(status.0, validity_timeframe) + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/mod.rs b/identity_credential/src/validator/jpt_presentation_validation/mod.rs new file mode 100644 index 0000000000..a39c129027 --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/mod.rs @@ -0,0 +1,9 @@ +mod decoded_jpt_presentation; +mod jpt_presentation_validation_options; +mod jpt_presentation_validator; +mod jpt_presentation_validator_utils; + +pub use decoded_jpt_presentation::*; +pub use jpt_presentation_validation_options::*; +pub use jpt_presentation_validator::*; +pub use jpt_presentation_validator_utils::*; diff --git a/identity_credential/src/validator/jwt_credential_validation/error.rs b/identity_credential/src/validator/jwt_credential_validation/error.rs index 073ffe303c..dfe2b01f5d 100644 --- a/identity_credential/src/validator/jwt_credential_validation/error.rs +++ b/identity_credential/src/validator/jwt_credential_validation/error.rs @@ -101,6 +101,18 @@ pub enum JwtValidationError { /// Indicates that the credential has been revoked. #[error("credential has been revoked")] Revoked, + + /// Indicates that the credential's timeframe interval is not valid + #[error("timeframe interval not valid")] + OutsideTimeframe, + + /// Indicates that the JWP representation of an issued credential or presentation could not be decoded. + #[error("could not decode jwp")] + JwpDecodingError(#[source] jsonprooftoken::errors::CustomError), + /// Indicates that the verfication of the JWP has failed + #[error("could not verify jwp")] + JwpProofVerificationError(#[source] jsonprooftoken::errors::CustomError), + /// Indicates that the credential has been suspended. #[error("credential has been suspended")] Suspended, diff --git a/identity_credential/src/validator/mod.rs b/identity_credential/src/validator/mod.rs index 37611334c3..11434071e6 100644 --- a/identity_credential/src/validator/mod.rs +++ b/identity_credential/src/validator/mod.rs @@ -3,6 +3,8 @@ //! Verifiable Credential and Presentation validators. +pub use self::jpt_credential_validation::*; +pub use self::jpt_presentation_validation::*; pub use self::jwt_credential_validation::*; pub use self::jwt_presentation_validation::*; pub use self::options::FailFast; @@ -11,6 +13,8 @@ pub use self::options::SubjectHolderRelationship; #[cfg(feature = "sd-jwt")] pub use self::sd_jwt::*; +mod jpt_credential_validation; +mod jpt_presentation_validation; mod jwt_credential_validation; mod jwt_presentation_validation; mod options; diff --git a/identity_document/src/verifiable/jwp_verification_options.rs b/identity_document/src/verifiable/jwp_verification_options.rs new file mode 100644 index 0000000000..0b7ceec064 --- /dev/null +++ b/identity_document/src/verifiable/jwp_verification_options.rs @@ -0,0 +1,33 @@ +use identity_did::DIDUrl; +use identity_verification::MethodScope; + +/// Holds additional options for verifying a JWP +#[non_exhaustive] +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct JwpVerificationOptions { + /// Verify the signing verification method relation matches this. + pub method_scope: Option, + /// The DID URl of the method, whose JWK should be used to verify the JWP. + /// If unset, the `kid` of the JWP is used as the DID Url. + pub method_id: Option, +} + +impl JwpVerificationOptions { + /// Creates a new [`JwpVerificationOptions`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the scope of the verification methods that may be used to verify the given JWP. + pub fn method_scope(mut self, value: MethodScope) -> Self { + self.method_scope = Some(value); + self + } + + /// The DID URl of the method, whose JWK should be used to verify the JWP. + pub fn method_id(mut self, value: DIDUrl) -> Self { + self.method_id = Some(value); + self + } +} diff --git a/identity_document/src/verifiable/mod.rs b/identity_document/src/verifiable/mod.rs index da91055ca1..6f0386d3fb 100644 --- a/identity_document/src/verifiable/mod.rs +++ b/identity_document/src/verifiable/mod.rs @@ -3,6 +3,8 @@ //! Additional functionality for DID assisted digital signatures. +pub use self::jwp_verification_options::JwpVerificationOptions; pub use self::jws_verification_options::JwsVerificationOptions; +mod jwp_verification_options; mod jws_verification_options; diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index aa2a53f13a..6781362356 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -14,6 +14,7 @@ description = "A library for JOSE (JSON Object Signing and Encryption)" [dependencies] identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } iota-crypto = { version = "0.23", default-features = false, features = ["std", "sha"] } +json-proof-token.workspace = true serde.workspace = true serde_json = { version = "1.0", default-features = false, features = ["std"] } subtle = { version = "2.5", default-features = false } diff --git a/identity_jose/src/jwk/jwk_ext.rs b/identity_jose/src/jwk/jwk_ext.rs new file mode 100644 index 0000000000..7c6621a5d0 --- /dev/null +++ b/identity_jose/src/jwk/jwk_ext.rs @@ -0,0 +1,173 @@ +use super::Jwk; +use super::JwkOperation; +use super::JwkParams; +use super::JwkParamsOkp; +use super::JwkType; +use super::JwkUse; +use identity_core::common::Url; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use jsonprooftoken::jwk::alg_parameters::Algorithm; +use jsonprooftoken::jwk::alg_parameters::JwkAlgorithmParameters; +use jsonprooftoken::jwk::alg_parameters::JwkOctetKeyPairParameters; +use jsonprooftoken::jwk::curves::EllipticCurveTypes; +use jsonprooftoken::jwk::key::Jwk as JwkExt; +use jsonprooftoken::jwk::key::KeyOps; +use jsonprooftoken::jwk::key::PKUse; +use jsonprooftoken::jwk::types::KeyType; +use std::str::FromStr; + +impl From for JwkOperation { + fn from(value: KeyOps) -> Self { + match value { + KeyOps::Sign => Self::Sign, + KeyOps::Verify => Self::Verify, + KeyOps::Encrypt => Self::Encrypt, + KeyOps::Decrypt => Self::Decrypt, + KeyOps::WrapKey => Self::WrapKey, + KeyOps::UnwrapKey => Self::UnwrapKey, + KeyOps::DeriveKey => Self::DeriveKey, + KeyOps::DeriveBits => Self::DeriveBits, + KeyOps::ProofGeneration => Self::ProofGeneration, + KeyOps::ProofVerification => Self::ProofVerification, + } + } +} + +impl From for KeyOps { + fn from(value: JwkOperation) -> Self { + match value { + JwkOperation::Sign => Self::Sign, + JwkOperation::Verify => Self::Verify, + JwkOperation::Encrypt => Self::Encrypt, + JwkOperation::Decrypt => Self::Decrypt, + JwkOperation::WrapKey => Self::WrapKey, + JwkOperation::UnwrapKey => Self::UnwrapKey, + JwkOperation::DeriveKey => Self::DeriveKey, + JwkOperation::DeriveBits => Self::DeriveBits, + JwkOperation::ProofGeneration => Self::ProofGeneration, + JwkOperation::ProofVerification => Self::ProofVerification, + } + } +} + +// impl Into for JwkOperation { +// fn into(self) -> KeyOps { +// match self { +// Self::Sign => KeyOps::Sign, +// Self::Verify => KeyOps::Verify, +// Self::Encrypt => KeyOps::Encrypt, +// Self::Decrypt => KeyOps::Decrypt, +// Self::WrapKey => KeyOps::WrapKey, +// Self::UnwrapKey => KeyOps::UnwrapKey, +// Self::DeriveKey => KeyOps::DeriveKey, +// Self::DeriveBits => KeyOps::DeriveBits, +// Self::ProofGeneration => KeyOps::ProofGeneration, +// Self::ProofVerification => KeyOps::ProofVerification, +// } +// } +// } + +impl From for JwkUse { + fn from(value: PKUse) -> Self { + match value { + PKUse::Signature => Self::Signature, + PKUse::Encryption => Self::Encryption, + PKUse::Proof => Self::Proof, + } + } +} + +impl From for PKUse { + fn from(value: JwkUse) -> Self { + match value { + JwkUse::Signature => Self::Signature, + JwkUse::Encryption => Self::Encryption, + JwkUse::Proof => Self::Proof, + } + } +} + +impl From for JwkParamsOkp { + fn from(value: JwkOctetKeyPairParameters) -> Self { + Self { + crv: value.crv.to_string(), + x: value.x, + d: value.d, + } + } +} + +impl TryInto for &JwkParamsOkp { + type Error = crate::error::Error; + + fn try_into(self) -> Result { + Ok(JwkOctetKeyPairParameters { + kty: KeyType::OctetKeyPair, + crv: EllipticCurveTypes::from_str(&self.crv).map_err(|_| Self::Error::KeyError("Invalid crv!"))?, + x: self.x.clone(), + d: self.d.clone(), + }) + } +} + +impl TryFrom for Jwk { + type Error = crate::error::Error; + + fn try_from(value: JwkExt) -> Result { + let x5u = match value.x5u { + Some(v) => Some(Url::from_str(&v).map_err(|_| Self::Error::InvalidClaim("x5u"))?), + None => None, + }; + + let (kty, params) = match value.key_params { + JwkAlgorithmParameters::OctetKeyPair(p) => (JwkType::Okp, JwkParams::Okp(JwkParamsOkp::from(p))), + }; + + Ok(Self { + kty, + use_: value.pk_use.map(JwkUse::from), + key_ops: value + .key_ops + .map(|vec_key_ops| vec_key_ops.into_iter().map(JwkOperation::from).collect()), + alg: value.alg.map(|a| a.to_string()), + kid: value.kid, + x5u, + x5c: value.x5c, + x5t: value.x5t, + x5t_s256: None, + params, + }) + } +} + +impl TryInto for &Jwk { + type Error = crate::error::Error; + + fn try_into(self) -> Result { + let params = match &self.params { + JwkParams::Okp(p) => JwkAlgorithmParameters::OctetKeyPair(p.try_into()?), + _ => return Err(Self::Error::InvalidParam("Parameters not supported!")), + }; + + let alg = match &self.alg { + Some(a) => Some(Algorithm::Proof( + ProofAlgorithm::from_str(a).map_err(|_| Self::Error::KeyError("Invalid alg"))?, + )), + None => None, + }; + + Ok(JwkExt { + kid: self.kid.clone(), + pk_use: self.use_.map(|u| u.into()), + key_ops: self + .key_ops + .as_deref() + .and_then(|vec_key_ops| vec_key_ops.iter().map(|o| Some((*o).into())).collect()), + alg, + x5u: self.x5u.as_ref().map(|v| v.as_str().to_string()), + x5c: self.x5c.clone(), + x5t: self.x5t.clone(), + key_params: params, + }) + } +} diff --git a/identity_jose/src/jwk/key_operation.rs b/identity_jose/src/jwk/key_operation.rs index 8fda0b6a23..ac6b7b0ce8 100644 --- a/identity_jose/src/jwk/key_operation.rs +++ b/identity_jose/src/jwk/key_operation.rs @@ -27,6 +27,10 @@ pub enum JwkOperation { DeriveKey, /// Derive bits not to be used as a key. DeriveBits, + /// Compute proof + ProofGeneration, + /// Verify proof + ProofVerification, } impl JwkOperation { @@ -41,6 +45,8 @@ impl JwkOperation { Self::UnwrapKey => "unwrapKey", Self::DeriveKey => "deriveKey", Self::DeriveBits => "deriveBits", + Self::ProofGeneration => "proofGeneration", + Self::ProofVerification => "proofVerification", } } @@ -55,6 +61,8 @@ impl JwkOperation { Self::UnwrapKey => Self::WrapKey, Self::DeriveKey => Self::DeriveKey, Self::DeriveBits => Self::DeriveBits, + Self::ProofGeneration => Self::ProofVerification, + Self::ProofVerification => Self::ProofGeneration, } } } diff --git a/identity_jose/src/jwk/key_use.rs b/identity_jose/src/jwk/key_use.rs index a686ba79cc..edd427c578 100644 --- a/identity_jose/src/jwk/key_use.rs +++ b/identity_jose/src/jwk/key_use.rs @@ -16,6 +16,9 @@ pub enum JwkUse { /// Encryption. #[serde(rename = "enc")] Encryption, + /// Proof + #[serde(rename = "proof")] + Proof, } impl JwkUse { @@ -24,6 +27,7 @@ impl JwkUse { match self { Self::Signature => "sig", Self::Encryption => "enc", + Self::Proof => "proof", } } } diff --git a/identity_jose/src/jwk/mod.rs b/identity_jose/src/jwk/mod.rs index a714cbf5ac..780c7f9861 100644 --- a/identity_jose/src/jwk/mod.rs +++ b/identity_jose/src/jwk/mod.rs @@ -4,6 +4,7 @@ //! JSON Web Keys ([JWK](https://tools.ietf.org/html/rfc7517)) mod curve; +mod jwk_ext; mod key; mod key_operation; mod key_params; diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index 75086ccab9..c20138235d 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -27,6 +27,9 @@ serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"], optional = true } +json-proof-token.workspace = true +zkryptium.workspace = true + [dev-dependencies] identity_credential = { version = "=1.1.1", path = "../identity_credential", features = ["revocation-bitmap"] } diff --git a/identity_storage/src/key_storage/jwk_storage.rs b/identity_storage/src/key_storage/jwk_storage.rs index 4f1918934c..e0e488be03 100644 --- a/identity_storage/src/key_storage/jwk_storage.rs +++ b/identity_storage/src/key_storage/jwk_storage.rs @@ -4,9 +4,13 @@ use crate::key_storage::KeyId; use crate::key_storage::KeyStorageError; use crate::key_storage::KeyType; +use crate::ProofUpdateCtx; use async_trait::async_trait; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jws::JwsAlgorithm; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwp::header::IssuerProtectedHeader; use super::jwk_gen_output::JwkGenOutput; @@ -62,3 +66,29 @@ pub trait JwkStorage: storage_sub_trait::StorageSendSyncMaybe { /// Returns `true` if the key with the given `key_id` exists in storage, `false` otherwise. async fn exists(&self, key_id: &KeyId) -> KeyStorageResult; } + +/// Extension to the JwkStorage to handle BBS+ keys +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait JwkStorageExt: JwkStorage { + /// Generates a JWK representing a BBS+ signature + async fn generate_bbs_key(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult; + + /// Generate the JPT representing a JWP in the Issuer form + async fn generate_issuer_proof( + &self, + key_id: &KeyId, + header: IssuerProtectedHeader, + claims: JptClaims, + public_key: &Jwk, + ) -> KeyStorageResult; + + /// Update proof functionality for timeframe revocation mechanism + async fn update_proof( + &self, + key_id: &KeyId, + public_key: &Jwk, + proof: &[u8; 112], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult<[u8; 112]>; +} diff --git a/identity_storage/src/key_storage/key_storage_error.rs b/identity_storage/src/key_storage/key_storage_error.rs index 59ff10968e..060d0794a6 100644 --- a/identity_storage/src/key_storage/key_storage_error.rs +++ b/identity_storage/src/key_storage/key_storage_error.rs @@ -23,6 +23,9 @@ pub enum KeyStorageErrorKind { /// Indicates an attempt to parse a signature algorithm that is not recognized by the key storage implementation. UnsupportedSignatureAlgorithm, + /// Indicates an attempt to parse a proof algorithm that is not recognized by the key storage implementation. + UnsupportedProofAlgorithm, + /// Indicates that the key storage implementation is not able to find the requested key. KeyNotFound, @@ -59,6 +62,7 @@ impl KeyStorageErrorKind { Self::UnsupportedKeyType => "key generation failed: the provided multikey schema is not supported", Self::KeyAlgorithmMismatch => "the key type cannot be used with the algorithm", Self::UnsupportedSignatureAlgorithm => "signing algorithm parsing failed", + Self::UnsupportedProofAlgorithm => "proof algorithm parsing failed", Self::KeyNotFound => "key not found in storage", Self::Unavailable => "key storage unavailable", Self::Unauthenticated => "authentication with the key storage failed", diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index f101af4759..4758840da1 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -12,10 +12,25 @@ use identity_verification::jose::jwk::EdCurve; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jwk::JwkType; use identity_verification::jose::jws::JwsAlgorithm; +use identity_verification::jwu; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwk::curves::EllipticCurveTypes; +use jsonprooftoken::jwk::key::Jwk as JwkExt; +use jsonprooftoken::jwk::types::KeyPairSubtype; +use jsonprooftoken::jwp::header::IssuerProtectedHeader; +use jsonprooftoken::jwp::issued::JwpIssuedBuilder; use rand::distributions::DistString; use shared::Shared; use tokio::sync::RwLockReadGuard; use tokio::sync::RwLockWriteGuard; +use zkryptium::bbsplus::keys::BBSplusPublicKey; +use zkryptium::bbsplus::keys::BBSplusSecretKey; +use zkryptium::schemes::algorithms::BBS_BLS12381_SHA256; +use zkryptium::schemes::algorithms::BBS_BLS12381_SHAKE256; +use zkryptium::schemes::generics::Signature; +use zkryptium::utils::message::BBSplusMessage; use super::ed25519::encode_jwk; use super::ed25519::expand_secret_jwk; @@ -26,6 +41,8 @@ use super::KeyStorageErrorKind; use super::KeyStorageResult; use super::KeyType; use crate::key_storage::JwkStorage; +use crate::JwkStorageExt; +use crate::ProofUpdateCtx; /// The map from key ids to JWKs. type JwkKeyStore = HashMap; @@ -189,6 +206,14 @@ impl JwkMemStore { const ED25519_KEY_TYPE_STR: &'static str = "Ed25519"; /// The Ed25519 key type. pub const ED25519_KEY_TYPE: KeyType = KeyType::from_static_str(Self::ED25519_KEY_TYPE_STR); + + const BLS12381SHA256_KEY_TYPE_STR: &'static str = "Bls12381Sha256"; + /// The BLS12381-SHA256 key type + pub const BLS12381SHA256_KEY_TYPE: KeyType = KeyType::from_static_str(Self::BLS12381SHA256_KEY_TYPE_STR); + + const BLS12381SHAKE256_KEY_TYPE_STR: &'static str = "Bls12381Shake256"; + /// The BLS12381-SHAKE256 key type + pub const BLS12381SHAKE256_KEY_TYPE: KeyType = KeyType::from_static_str(Self::BLS12381SHAKE256_KEY_TYPE_STR); } impl MemStoreKeyType { @@ -269,6 +294,271 @@ fn check_key_alg_compatibility(key_type: MemStoreKeyType, alg: JwsAlgorithm) -> } } +/// JwkStorageExt implementation for JwkMemStore +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl JwkStorageExt for JwkMemStore { + async fn generate_bbs_key(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + let keysubtype = + KeyPairSubtype::from_str(key_type.as_str()).map_err(|_| KeyStorageErrorKind::UnsupportedKeyType)?; + + let mut jwk = Jwk::try_from( + JwkExt::generate(keysubtype) + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::RetryableIOFailure).with_source(err))?, + ) + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::RetryableIOFailure).with_source(err))?; + + let kid: KeyId = random_key_id(); + + jwk.set_alg(alg.to_string()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + let public_jwk: Jwk = jwk.to_public().expect("should only panic if kty == oct"); + + let mut jwk_store: RwLockWriteGuard<'_, JwkKeyStore> = self.jwk_store.write().await; + jwk_store.insert(kid.clone(), jwk); + + Ok(JwkGenOutput::new(kid, public_jwk)) + } + + async fn generate_issuer_proof( + &self, + key_id: &KeyId, + header: IssuerProtectedHeader, + claims: JptClaims, + public_key: &Jwk, + ) -> KeyStorageResult { + let jwk_store: RwLockReadGuard<'_, JwkKeyStore> = self.jwk_store.read().await; + + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + match alg { + ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256 => { + let okp_params = public_key.try_okp_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected a Jwk with Okp params in order to sign with {alg}")) + .with_source(err) + })?; + if okp_params.crv != EllipticCurveTypes::Bls12381G2.to_string() { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "expected Jwk with Okp {} crv in order to generate the proof with {alg}", + EllipticCurveTypes::Bls12381G2 + )), + ); + } + } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{other} is not supported")), + ); + } + } + + // Obtain the corresponding private key and sign `data`. + let jwk: &Jwk = jwk_store + .get(key_id) + .ok_or_else(|| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound))?; + + // Deserialize JSON to JwkExt + let jwk_ext: JwkExt = jwk.try_into().map_err(|_| KeyStorageErrorKind::SerializationError)?; + + let jwp = JwpIssuedBuilder::new() + .issuer_protected_header(header) + .jpt_claims(claims) + .build(&jwk_ext) + .map_err(|_| KeyStorageErrorKind::Unspecified)? + .encode(SerializationType::COMPACT) + .map_err(|_| KeyStorageErrorKind::Unspecified)?; + + Ok(jwp) + } + + async fn update_proof( + &self, + key_id: &KeyId, + public_key: &Jwk, + proof: &[u8; 112], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult<[u8; 112]> { + let jwk_store: RwLockReadGuard<'_, JwkKeyStore> = self.jwk_store.read().await; + + let ProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } = ctx; + + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + match alg { + ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256 => { + let okp_params = public_key.try_okp_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected a Jwk with Okp params in order to sign with {alg}")) + .with_source(err) + })?; + if okp_params.crv != EllipticCurveTypes::Bls12381G2.to_string() { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "expected Jwk with Okp {} crv in order to generate the proof with {alg}", + EllipticCurveTypes::Bls12381G2 + )), + ); + } + } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{other} is not supported")), + ); + } + } + + // Obtain the corresponding private key and sign `data`. + let jwk: &Jwk = jwk_store + .get(key_id) + .ok_or_else(|| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound))?; + + let params = jwk.try_okp_params().map_err(|_| KeyStorageErrorKind::Unspecified)?; + + let pk = BBSplusPublicKey::from_bytes(&jwu::decode_b64(¶ms.x).map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `d` param") + .with_source(err) + })?); + + let sk = BBSplusSecretKey::from_bytes( + ¶ms + .d + .as_deref() + .map(jwu::decode_b64) + .ok_or_else(|| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("expected Jwk `d` param to be present") + })? + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `d` param") + .with_source(err) + })?, + ); + + let new_proof = match alg { + ProofAlgorithm::BLS12381_SHA256 => { + let vec_old_start_validity_timeframe = serde_json::to_vec(&old_start_validity_timeframe) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; + let old_start_validity_timeframe = + BBSplusMessage::map_message_to_scalar_as_hash::(&vec_old_start_validity_timeframe, None); + + let vec_new_start_validity_timeframe = serde_json::to_vec(&new_start_validity_timeframe) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; + let new_start_validity_timeframe = + BBSplusMessage::map_message_to_scalar_as_hash::(&vec_new_start_validity_timeframe, None); + + let vec_old_end_validity_timeframe = serde_json::to_vec(&old_end_validity_timeframe) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; + let old_end_validity_timeframe = + BBSplusMessage::map_message_to_scalar_as_hash::(&vec_old_end_validity_timeframe, None); + + let vec_new_end_validity_timeframe = serde_json::to_vec(&new_end_validity_timeframe) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; + let new_end_validity_timeframe = + BBSplusMessage::map_message_to_scalar_as_hash::(&vec_new_end_validity_timeframe, None); + + let proof = Signature::::from_bytes(proof).update_signature( + &sk, + &pk, + number_of_signed_messages, + &old_start_validity_timeframe, + &new_start_validity_timeframe, + index_start_validity_timeframe, + ); + + proof + .update_signature( + &sk, + &pk, + number_of_signed_messages, + &old_end_validity_timeframe, + &new_end_validity_timeframe, + index_end_validity_timeframe, + ) + .to_bytes() + } + ProofAlgorithm::BLS12381_SHAKE256 => { + let vec_old_start_validity_timeframe = serde_json::to_vec(&old_start_validity_timeframe) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; + let old_start_validity_timeframe = BBSplusMessage::map_message_to_scalar_as_hash::( + &vec_old_start_validity_timeframe, + None, + ); + + let vec_new_start_validity_timeframe = serde_json::to_vec(&new_start_validity_timeframe) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; + let new_start_validity_timeframe = BBSplusMessage::map_message_to_scalar_as_hash::( + &vec_new_start_validity_timeframe, + None, + ); + + let vec_old_end_validity_timeframe = serde_json::to_vec(&old_end_validity_timeframe) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; + let old_end_validity_timeframe = + BBSplusMessage::map_message_to_scalar_as_hash::(&vec_old_end_validity_timeframe, None); + + let vec_new_end_validity_timeframe = serde_json::to_vec(&new_end_validity_timeframe) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; + let new_end_validity_timeframe = + BBSplusMessage::map_message_to_scalar_as_hash::(&vec_new_end_validity_timeframe, None); + + let proof = Signature::::from_bytes(proof).update_signature( + &sk, + &pk, + number_of_signed_messages, + &old_start_validity_timeframe, + &new_start_validity_timeframe, + index_start_validity_timeframe, + ); + + proof + .update_signature( + &sk, + &pk, + number_of_signed_messages, + &old_end_validity_timeframe, + &new_end_validity_timeframe, + index_end_validity_timeframe, + ) + .to_bytes() + } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{other} is not supported")), + ); + } + }; + + Ok(new_proof) + } +} + pub(crate) mod shared { use core::fmt::Debug; use core::fmt::Formatter; diff --git a/identity_storage/src/storage/error.rs b/identity_storage/src/storage/error.rs index 7abac68286..a5d8d11185 100644 --- a/identity_storage/src/storage/error.rs +++ b/identity_storage/src/storage/error.rs @@ -27,6 +27,16 @@ pub enum JwkStorageDocumentError { /// Caused by an invalid JWS algorithm. #[error("invalid JWS algorithm")] InvalidJwsAlgorithm, + /// Caused by an invalid JWP algorithm. + #[error("invalid JWP algorithm")] + InvalidJwpAlgorithm, + /// Cannot cunstruct a valid Jwp (issued or presented form) + #[error("Not able to construct a valid Jwp")] + JwpBuildingError, + /// Credential's proof update internal error + #[error("Credential's proof internal error")] + ProofUpdateError(String), + /// Caused by a failure to construct a verification method. #[error("method generation failed: unable to create a valid verification method")] VerificationMethodConstructionError(#[source] identity_verification::Error), diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index 8b412a285a..f9ee100986 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -153,20 +153,20 @@ mod private { // copious amounts of repetition. // NOTE: If such use of macros becomes very common it is probably better to use the duplicate crate: https://docs.rs/duplicate/latest/duplicate/ macro_rules! generate_method_for_document_type { - ($t:ty, $name:ident) => { + ($t:ty, $a:ty, $k:path, $f:path, $name:ident) => { async fn $name( document: &mut $t, storage: &Storage, key_type: KeyType, - alg: JwsAlgorithm, + alg: $a, fragment: Option<&str>, scope: MethodScope, ) -> StorageResult where - K: JwkStorage, + K: $k, I: KeyIdStorage, { - let JwkGenOutput { key_id, jwk } = ::generate(&storage.key_storage(), key_type, alg) + let JwkGenOutput { key_id, jwk } = $f(storage.key_storage(), key_type, alg) .await .map_err(Error::KeyStorageError)?; @@ -304,7 +304,13 @@ macro_rules! purge_method_for_document_type { // CoreDocument // ==================================================================================================================== -generate_method_for_document_type!(CoreDocument, generate_method_core_document); +generate_method_for_document_type!( + CoreDocument, + JwsAlgorithm, + JwkStorage, + JwkStorage::generate, + generate_method_core_document +); purge_method_for_document_type!(CoreDocument, purge_method_core_document); #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] @@ -505,7 +511,7 @@ impl JwkDocumentExt for CoreDocument { /// Attempt to revert key generation. If this succeeds the original `source_error` is returned, /// otherwise [`JwkStorageDocumentError::UndoOperationFailed`] is returned with the `source_error` attached as /// `source`. -async fn try_undo_key_generation(storage: &Storage, key_id: &KeyId, source_error: Error) -> Error +pub(crate) async fn try_undo_key_generation(storage: &Storage, key_id: &KeyId, source_error: Error) -> Error where K: JwkStorage, I: KeyIdStorage, @@ -531,7 +537,13 @@ mod iota_document { use identity_credential::credential::Jwt; use identity_iota_core::IotaDocument; - generate_method_for_document_type!(IotaDocument, generate_method_iota_document); + generate_method_for_document_type!( + IotaDocument, + JwsAlgorithm, + JwkStorage, + JwkStorage::generate, + generate_method_iota_document + ); purge_method_for_document_type!(IotaDocument, purge_method_iota_document); #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] diff --git a/identity_storage/src/storage/jwp_document_ext.rs b/identity_storage/src/storage/jwp_document_ext.rs new file mode 100644 index 0000000000..b99f2f23a2 --- /dev/null +++ b/identity_storage/src/storage/jwp_document_ext.rs @@ -0,0 +1,347 @@ +use super::JwkStorageDocumentError as Error; +use crate::key_id_storage::MethodDigest; +use crate::try_undo_key_generation; +use crate::JwkGenOutput; +use crate::JwkStorageExt; +use crate::KeyIdStorage; +use crate::KeyType; +use crate::Storage; +use crate::StorageResult; +use async_trait::async_trait; +use identity_core::common::Object; +use identity_credential::credential::Credential; +use identity_credential::credential::Jpt; +use identity_credential::credential::JwpCredentialOptions; +use identity_credential::presentation::JwpPresentationOptions; +use identity_credential::presentation::SelectiveDisclosurePresentation; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use identity_verification::MethodData; +use identity_verification::MethodScope; +use identity_verification::VerificationMethod; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwk::key::Jwk; +use jsonprooftoken::jwp::header::IssuerProtectedHeader; +use jsonprooftoken::jwp::header::PresentationProtectedHeader; +use serde::de::DeserializeOwned; +use serde::Serialize; + +///New trait to handle JWP-based operations on DID Documents +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait JwpDocumentExt { + /// Generate new key material in the given `storage` and insert a new verification method with the corresponding + /// public key material into the DID document. This support BBS+ keys. + async fn generate_method_jwp( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage; + + /// Compute a JWP in the Issued form representing the Verifiable Credential + /// See [JSON Web Proof draft section 4.1](https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-issued-form) + async fn create_issued_jwp( + &self, + storage: &Storage, + fragment: &str, + jpt_claims: &JptClaims, + options: &JwpCredentialOptions, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage; + + /// Compute a JWP in the Presented form representing the presented Verifiable Credential after the Selective + /// Disclosure of attributes See [JSON Web Proof draft section 4.2](https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form) + async fn create_presented_jwp( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult; + + /// Produces a JPT where the payload is produced from the given `credential`. + async fn create_credential_jpt( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwpCredentialOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync; + + /// Produces a JPT where the payload contains the Selective Disclosed attributes of a `credential`. + async fn create_presentation_jpt( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult; +} + +// ==================================================================================================================== +// CoreDocument +// ==================================================================================================================== + +generate_method_for_document_type!( + CoreDocument, + ProofAlgorithm, + JwkStorageExt, + JwkStorageExt::generate_bbs_key, + generate_method_core_document +); + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl JwpDocumentExt for CoreDocument { + async fn generate_method_jwp( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage, + { + generate_method_core_document(self, storage, key_type, alg, fragment, scope).await + } + + async fn create_issued_jwp( + &self, + storage: &Storage, + fragment: &str, + jpt_claims: &JptClaims, + options: &JwpCredentialOptions, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage, + { + // Obtain the method corresponding to the given fragment. + let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?; + let MethodData::PublicKeyJwk(ref jwk) = method.data() else { + return Err(Error::NotPublicKeyJwk); + }; + + // Extract JwsAlgorithm. + let alg: ProofAlgorithm = jwk + .alg() + .unwrap_or("") + .parse() + .map_err(|_| Error::InvalidJwpAlgorithm)?; + + let typ = "JPT".to_string(); + + let kid = if let Some(ref kid) = options.kid { + kid.clone() + } else { + method.id().to_string() + }; + + let mut issuer_header = IssuerProtectedHeader::new(alg); + issuer_header.set_typ(Some(typ)); + issuer_header.set_kid(Some(kid)); + + // Get the key identifier corresponding to the given method from the KeyId storage. + let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?; + let key_id = ::get_key_id(storage.key_id_storage(), &method_digest) + .await + .map_err(Error::KeyIdStorageError)?; + + let jwp = ::generate_issuer_proof( + storage.key_storage(), + &key_id, + issuer_header, + jpt_claims.clone(), + jwk, + ) + .await + .map_err(Error::KeyStorageError)?; + + Ok(jwp) + } + + async fn create_presented_jwp( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + // Obtain the method corresponding to the given fragment. + let method: &VerificationMethod = self.resolve_method(method_id, None).ok_or(Error::MethodNotFound)?; + let MethodData::PublicKeyJwk(ref jwk) = method.data() else { + return Err(Error::NotPublicKeyJwk); + }; + + // Extract JwsAlgorithm. + let alg: ProofAlgorithm = jwk + .alg() + .unwrap_or("") + .parse() + .map_err(|_| Error::InvalidJwpAlgorithm)?; + + let public_key: Jwk = jwk.try_into().map_err(|_| Error::NotPublicKeyJwk)?; + + let mut presentation_header = PresentationProtectedHeader::new(alg.into()); + presentation_header.set_nonce(options.nonce.clone()); + presentation_header.set_aud(options.audience.as_ref().map(|u| u.to_string())); + + presentation.set_presentation_header(presentation_header); + + let jwp_builder = presentation.builder(); + + let presented_jwp = jwp_builder.build(&public_key).map_err(|_| Error::JwpBuildingError)?; + + Ok( + presented_jwp + .encode(SerializationType::COMPACT) + .map_err(|e| Error::EncodingError(Box::new(e)))?, + ) + } + + async fn create_credential_jpt( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwpCredentialOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + let jpt_claims = credential + .serialize_jpt(custom_claims) + .map_err(Error::ClaimsSerializationError)?; + + self + .create_issued_jwp(storage, fragment, &jpt_claims, options) + .await + .map(Jpt::new) + } + + async fn create_presentation_jpt( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + self + .create_presented_jwp(presentation, method_id, options) + .await + .map(Jpt::new) + } +} + +// ==================================================================================================================== +// IotaDocument +// ==================================================================================================================== +#[cfg(feature = "iota-document")] +mod iota_document { + use super::*; + use identity_iota_core::IotaDocument; + + generate_method_for_document_type!( + IotaDocument, + ProofAlgorithm, + JwkStorageExt, + JwkStorageExt::generate_bbs_key, + generate_method_iota_document + ); + + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl JwpDocumentExt for IotaDocument { + async fn generate_method_jwp( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage, + { + generate_method_iota_document(self, storage, key_type, alg, fragment, scope).await + } + + async fn create_issued_jwp( + &self, + storage: &Storage, + fragment: &str, + jpt_claims: &JptClaims, + options: &JwpCredentialOptions, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage, + { + self + .core_document() + .create_issued_jwp(storage, fragment, jpt_claims, options) + .await + } + + async fn create_presented_jwp( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + self + .core_document() + .create_presented_jwp(presentation, method_id, options) + .await + } + + async fn create_credential_jpt( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwpCredentialOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + self + .core_document() + .create_credential_jpt(credential, storage, fragment, options, custom_claims) + .await + } + + async fn create_presentation_jpt( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + self + .core_document() + .create_presentation_jpt(presentation, method_id, options) + .await + } + } +} diff --git a/identity_storage/src/storage/mod.rs b/identity_storage/src/storage/mod.rs index efbdc28cbb..fe0c10a532 100644 --- a/identity_storage/src/storage/mod.rs +++ b/identity_storage/src/storage/mod.rs @@ -4,14 +4,21 @@ //! This module provides a type wrapping a key and key id storage. mod error; +#[macro_use] mod jwk_document_ext; +mod jwp_document_ext; mod signature_options; +mod timeframe_revocation_ext; + #[cfg(all(test, feature = "memstore"))] pub(crate) mod tests; pub use error::*; + pub use jwk_document_ext::*; +pub use jwp_document_ext::*; pub use signature_options::*; +pub use timeframe_revocation_ext::*; /// A type wrapping a key and key id storage, typically used with [`JwkStorage`](crate::key_storage::JwkStorage) and /// [`KeyIdStorage`](crate::key_id_storage::KeyIdStorage) that should always be used together when calling methods from diff --git a/identity_storage/src/storage/timeframe_revocation_ext.rs b/identity_storage/src/storage/timeframe_revocation_ext.rs new file mode 100644 index 0000000000..0d3ed740b7 --- /dev/null +++ b/identity_storage/src/storage/timeframe_revocation_ext.rs @@ -0,0 +1,193 @@ +use super::JwkStorageDocumentError as Error; +use crate::JwkStorageExt; +use crate::KeyIdStorage; +use crate::MethodDigest; +use crate::Storage; +use crate::StorageResult; +use async_trait::async_trait; +use identity_core::common::Duration; +use identity_core::common::Timestamp; +use identity_credential::credential::Jpt; +use identity_credential::revocation::RevocationTimeframeStatus; +use identity_document::document::CoreDocument; +use identity_verification::MethodData; +use identity_verification::VerificationMethod; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::payloads::Payloads; +use jsonprooftoken::jwp::issued::JwpIssued; +use serde_json::Value; + +/// Contains information needed to update the signature in the RevocationTimeframe2024 revocation mechanism. +pub struct ProofUpdateCtx { + /// Old `startValidityTimeframe` value + pub old_start_validity_timeframe: String, + /// New `startValidityTimeframe` value to be signed + pub new_start_validity_timeframe: String, + /// Old `endValidityTimeframe` value + pub old_end_validity_timeframe: String, + /// New `endValidityTimeframe` value to be signed + pub new_end_validity_timeframe: String, + /// Index of `startValidityTimeframe` claim inside the array of Claims + pub index_start_validity_timeframe: usize, + /// Index of `endValidityTimeframe` claim inside the array of Claims + pub index_end_validity_timeframe: usize, + /// Number of signed messages, number of payloads in a JWP + pub number_of_signed_messages: usize, +} + +/// CoreDocument and IotaDocument extension to handle Credential' signature update for RevocationTimeframe2024 +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait TimeframeRevocationExtension { + /// Update Credential' signature considering the Timeframe interval + async fn update( + &self, + storage: &Storage, + fragment: &str, + start_validity: Option, + duration: Duration, + credential_jwp: &mut JwpIssued, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage; +} + +// ==================================================================================================================== +// CoreDocument +// ==================================================================================================================== + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl TimeframeRevocationExtension for CoreDocument { + async fn update( + &self, + storage: &Storage, + fragment: &str, + start_validity: Option, + duration: Duration, + credential_jwp: &mut JwpIssued, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage, + { + // Obtain the method corresponding to the given fragment. + let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?; + let MethodData::PublicKeyJwk(ref jwk) = method.data() else { + return Err(Error::NotPublicKeyJwk); + }; + + // Get the key identifier corresponding to the given method from the KeyId storage. + let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?; + let key_id = ::get_key_id(storage.key_id_storage(), &method_digest) + .await + .map_err(Error::KeyIdStorageError)?; + + let new_start_validity_timeframe = start_validity.unwrap_or(Timestamp::now_utc()); + let new_end_validity_timeframe = new_start_validity_timeframe + .checked_add(duration) + .ok_or(Error::ProofUpdateError("Invalid granularity".to_owned()))?; + let new_start_validity_timeframe = new_start_validity_timeframe.to_rfc3339(); + let new_end_validity_timeframe = new_end_validity_timeframe.to_rfc3339(); + + let proof = credential_jwp.get_proof(); + let claims = credential_jwp + .get_claims() + .ok_or(Error::ProofUpdateError("Should not happen".to_owned()))?; + let mut payloads: Payloads = credential_jwp.get_payloads().clone(); + + let index_start_validity_timeframe = claims + .get_claim_index(format!( + "vc.credentialStatus.{}", + RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY + )) + .ok_or(Error::ProofUpdateError( + "'startValidityTimeframe' property NOT found".to_owned(), + ))?; + let index_end_validity_timeframe = claims + .get_claim_index(format!( + "vc.credentialStatus.{}", + RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY + )) + .ok_or(Error::ProofUpdateError( + "'endValidityTimeframe' property NOT found".to_owned(), + ))?; + + let old_start_validity_timeframe = payloads + .replace_payload_at_index( + index_start_validity_timeframe, + Value::String(new_start_validity_timeframe.clone()), + ) + .map(serde_json::from_value::) + .map_err(|_| Error::ProofUpdateError("'startValidityTimeframe' value NOT found".to_owned()))? + .map_err(|_| Error::ProofUpdateError("'startValidityTimeframe' value NOT a JSON String".to_owned()))?; + + let old_end_validity_timeframe = payloads + .replace_payload_at_index( + index_end_validity_timeframe, + Value::String(new_end_validity_timeframe.clone()), + ) + .map(serde_json::from_value::) + .map_err(|_| Error::ProofUpdateError("'endValidityTimeframe' value NOT found".to_owned()))? + .map_err(|_| Error::ProofUpdateError("'endValidityTimeframe' value NOT a JSON String".to_owned()))?; + + let proof: [u8; 112] = proof + .try_into() + .map_err(|_| Error::ProofUpdateError("Invalid bytes length of JWP proof".to_owned()))?; + + let proof_update_ctx = ProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages: payloads.0.len(), + }; + + let new_proof = ::update_proof(storage.key_storage(), &key_id, jwk, &proof, proof_update_ctx) + .await + .map_err(Error::KeyStorageError)?; + + credential_jwp.set_proof(&new_proof); + credential_jwp.set_payloads(payloads); + + let jpt = credential_jwp + .encode(SerializationType::COMPACT) + .map_err(|e| Error::EncodingError(Box::new(e)))?; + + Ok(Jpt::new(jpt)) + } +} + +// ==================================================================================================================== +// IotaDocument +// ==================================================================================================================== +#[cfg(feature = "iota-document")] +mod iota_document { + use super::*; + use identity_iota_core::IotaDocument; + + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl TimeframeRevocationExtension for IotaDocument { + async fn update( + &self, + storage: &Storage, + fragment: &str, + start_validity: Option, + duration: Duration, + credential_jwp: &mut JwpIssued, + ) -> StorageResult + where + K: JwkStorageExt, + I: KeyIdStorage, + { + self + .core_document() + .update(storage, fragment, start_validity, duration, credential_jwp) + .await + } + } +} From bdf6b54d87d7226b759fcfbe3700cd211ab66b80 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 19 Mar 2024 10:07:36 +0100 Subject: [PATCH 02/33] merge main --- README.md | 8 +- bindings/wasm/docs/api-reference.md | 185 +++++++++++++----- bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs | 1 - .../wasm/src/verification/wasm_method_data.rs | 50 +++++ .../wasm/src/verification/wasm_method_type.rs | 5 + .../verification/wasm_verification_method.rs | 19 ++ examples/Cargo.toml | 2 +- identity_credential/Cargo.toml | 4 +- identity_iota/README.md | 10 +- identity_verification/Cargo.toml | 2 +- .../src/verification_method/material.rs | 117 ++++++++++- .../src/verification_method/method.rs | 46 ++++- .../src/verification_method/method_type.rs | 9 +- .../src/verification_method/mod.rs | 1 + 14 files changed, 380 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 658479444f..41ba9d8519 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ## Introduction -IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentralized digital identity, also known as Self-Sovereign Identity (SSI). It implements the W3C [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) specifications. This library can be used to create, resolve and authenticate digital identities and to create verifiable credentials and presentations in order to share information in a verifiable manner and establish trust in the digital world. It does so while supporting secure storage of cryptographic keys, which can be implemented for your preferred key management system. Many of the individual libraries (Rust crates) are agnostic over the concrete DID method, with the exception of some libraries dedicated to implement the [IOTA DID method](https://wiki.iota.org/shimmer/identity.rs/specs/did/iota_did_method_spec/), which is an implementation of decentralized digital identity on the IOTA and Shimmer networks. Written in stable Rust, IOTA Identity has strong guarantees of memory safety and process integrity while maintaining exceptional performance. +IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentralized digital identity, also known as Self-Sovereign Identity (SSI). It implements the W3C [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) specifications. This library can be used to create, resolve and authenticate digital identities and to create verifiable credentials and presentations in order to share information in a verifiable manner and establish trust in the digital world. It does so while supporting secure storage of cryptographic keys, which can be implemented for your preferred key management system. Many of the individual libraries (Rust crates) are agnostic over the concrete DID method, with the exception of some libraries dedicated to implement the [IOTA DID method](https://wiki.iota.org/identity.rs/specs/did/iota_did_method_spec/), which is an implementation of decentralized digital identity on the IOTA and Shimmer networks. Written in stable Rust, IOTA Identity has strong guarantees of memory safety and process integrity while maintaining exceptional performance. ## Bindings @@ -36,8 +36,8 @@ IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentra - API References: - [Rust API Reference](https://docs.rs/identity_iota/latest/identity_iota/): Package documentation (cargo docs). - - [Wasm API Reference](https://wiki.iota.org/shimmer/identity.rs/libraries/wasm/api_reference/): Wasm Package documentation. -- [Identity Documentation Pages](https://wiki.iota.org/shimmer/identity.rs/introduction): Supplementing documentation with context around identity and simple examples on library usage. + - [Wasm API Reference](https://wiki.iota.org/identity.rs/libraries/wasm/api_reference/): Wasm Package documentation. +- [Identity Documentation Pages](https://wiki.iota.org/identity.rs/introduction): Supplementing documentation with context around identity and simple examples on library usage. - [Examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples): Practical code snippets to get you started with the library. ## Prerequisites @@ -238,7 +238,7 @@ For detailed development progress, see the IOTA Identity development [kanban boa We would love to have you help us with the development of IOTA Identity. Each and every contribution is greatly valued! -Please review the [contribution](https://wiki.iota.org/shimmer/identity.rs/contribute) and [workflow](https://wiki.iota.org/shimmer/identity.rs/workflow) sections in the [IOTA Wiki](https://wiki.iota.org/). +Please review the [contribution](https://wiki.iota.org/identity.rs/contribute) and [workflow](https://wiki.iota.org/identity.rs/workflow) sections in the [IOTA Wiki](https://wiki.iota.org/). To contribute directly to the repository, simply fork the project, push your changes to your fork and create a pull request to get them included! diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index e83bbc81b6..2f50e4ed3d 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -11,6 +11,9 @@ if the object is being concurrently modified.

Credential
+
CustomMethodData
+

A custom verification method data format.

+
DIDUrl

A method agnostic DID Url.

@@ -187,8 +190,9 @@ working with storage backed DID documents.

## Members
-
CredentialStatus
-
+
StatusPurpose
+

Purpose of a StatusList2021.

+
SubjectHolderRelationship

Declares how credential subjects must relate to the presentation holder.

See also the Subject-Holder Relationship section of the specification.

@@ -203,9 +207,8 @@ This variant is the default.

Any

The holder is not required to have any kind of relationship to any credential subject.

-
StatusPurpose
-

Purpose of a StatusList2021.

-
+
StateMetadataEncoding
+
FailFast

Declares when validation should return if an error occurs.

@@ -215,6 +218,10 @@ This variant is the default.

FirstError

Return after the first error occurs.

+
MethodRelationship
+
+
CredentialStatus
+
StatusCheck

Controls validation behaviour when checking whether or not a credential has been revoked by its credentialStatus.

@@ -232,18 +239,11 @@ This variant is the default.

SkipAll

Skip all status checks.

-
StateMetadataEncoding
-
-
MethodRelationship
-
## Functions
-
start()
-

Initializes the console error panic hook for better error messages

-
verifyEd25519(alg, signingInput, decodedSignature, publicKey)

Verify a JWS signature secured with the EdDSA algorithm and curve Ed25519.

This function is useful when one is composing a IJwsVerifier that delegates @@ -258,6 +258,9 @@ prior to calling the function.

decodeB64(data)Uint8Array

Decode the given url-safe base64-encoded slice into its raw bytes.

+
start()
+

Initializes the console error panic hook for better error messages

+
@@ -1138,6 +1141,53 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## CustomMethodData +A custom verification method data format. + +**Kind**: global class + +* [CustomMethodData](#CustomMethodData) + * [new CustomMethodData(name, data)](#new_CustomMethodData_new) + * _instance_ + * [.clone()](#CustomMethodData+clone) ⇒ [CustomMethodData](#CustomMethodData) + * [.toJSON()](#CustomMethodData+toJSON) ⇒ any + * _static_ + * [.fromJSON(json)](#CustomMethodData.fromJSON) ⇒ [CustomMethodData](#CustomMethodData) + + + +### new CustomMethodData(name, data) + +| Param | Type | +| --- | --- | +| name | string | +| data | any | + + + +### customMethodData.clone() ⇒ [CustomMethodData](#CustomMethodData) +Deep clones the object. + +**Kind**: instance method of [CustomMethodData](#CustomMethodData) + + +### customMethodData.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [CustomMethodData](#CustomMethodData) + + +### CustomMethodData.fromJSON(json) ⇒ [CustomMethodData](#CustomMethodData) +Deserializes an instance from a JSON object. + +**Kind**: static method of [CustomMethodData](#CustomMethodData) + +| Param | Type | +| --- | --- | +| json | any | + ## DIDUrl @@ -1967,7 +2017,7 @@ if the object is being concurrently modified. * _instance_ * [.id()](#IotaDocument+id) ⇒ [IotaDID](#IotaDID) * [.controller()](#IotaDocument+controller) ⇒ [Array.<IotaDID>](#IotaDID) - * [.setController(controllers)](#IotaDocument+setController) + * [.setController(controller)](#IotaDocument+setController) * [.alsoKnownAs()](#IotaDocument+alsoKnownAs) ⇒ Array.<string> * [.setAlsoKnownAs(urls)](#IotaDocument+setAlsoKnownAs) * [.properties()](#IotaDocument+properties) ⇒ Map.<string, any> @@ -2042,7 +2092,7 @@ during resolution and are omitted when publishing. **Kind**: instance method of [IotaDocument](#IotaDocument) -### iotaDocument.setController(controllers) +### iotaDocument.setController(controller) Sets the controllers of the document. Note: Duplicates will be ignored. @@ -2052,7 +2102,7 @@ Use `null` to remove all controllers. | Param | Type | | --- | --- | -| controllers | [CoreDID](#CoreDID) \| [Array.<CoreDID>](#CoreDID) \| null | +| controller | [Array.<IotaDID>](#IotaDID) \| null | @@ -4343,6 +4393,7 @@ Supported verification method data formats. * [MethodData](#MethodData) * _instance_ + * [.tryCustom()](#MethodData+tryCustom) ⇒ [CustomMethodData](#CustomMethodData) * [.tryDecode()](#MethodData+tryDecode) ⇒ Uint8Array * [.tryPublicKeyJwk()](#MethodData+tryPublicKeyJwk) ⇒ [Jwk](#Jwk) * [.toJSON()](#MethodData+toJSON) ⇒ any @@ -4351,8 +4402,15 @@ Supported verification method data formats. * [.newBase58(data)](#MethodData.newBase58) ⇒ [MethodData](#MethodData) * [.newMultibase(data)](#MethodData.newMultibase) ⇒ [MethodData](#MethodData) * [.newJwk(key)](#MethodData.newJwk) ⇒ [MethodData](#MethodData) + * [.newCustom(name, data)](#MethodData.newCustom) ⇒ [MethodData](#MethodData) * [.fromJSON(json)](#MethodData.fromJSON) ⇒ [MethodData](#MethodData) + + +### methodData.tryCustom() ⇒ [CustomMethodData](#CustomMethodData) +Returns the wrapped custom method data format is `Custom`. + +**Kind**: instance method of [MethodData](#MethodData) ### methodData.tryDecode() ⇒ Uint8Array @@ -4419,6 +4477,18 @@ An error is thrown if the given `key` contains any private components. | --- | --- | | key | [Jwk](#Jwk) | + + +### MethodData.newCustom(name, data) ⇒ [MethodData](#MethodData) +Creates a new custom [MethodData](#MethodData). + +**Kind**: static method of [MethodData](#MethodData) + +| Param | Type | +| --- | --- | +| name | string | +| data | any | + ### MethodData.fromJSON(json) ⇒ [MethodData](#MethodData) @@ -4570,6 +4640,7 @@ Supported verification method types. * [.Ed25519VerificationKey2018()](#MethodType.Ed25519VerificationKey2018) ⇒ [MethodType](#MethodType) * [.X25519KeyAgreementKey2019()](#MethodType.X25519KeyAgreementKey2019) ⇒ [MethodType](#MethodType) * [.JsonWebKey()](#MethodType.JsonWebKey) ⇒ [MethodType](#MethodType) + * [.custom(type_)](#MethodType.custom) ⇒ [MethodType](#MethodType) * [.fromJSON(json)](#MethodType.fromJSON) ⇒ [MethodType](#MethodType) @@ -4605,6 +4676,17 @@ A verification method for use with JWT verification as prescribed by the [Jwk](# in the `publicKeyJwk` entry. **Kind**: static method of [MethodType](#MethodType) + + +### MethodType.custom(type_) ⇒ [MethodType](#MethodType) +A custom method. + +**Kind**: static method of [MethodType](#MethodType) + +| Param | Type | +| --- | --- | +| type_ | string | + ### MethodType.fromJSON(json) ⇒ [MethodType](#MethodType) @@ -5006,11 +5088,9 @@ Representation of an SD-JWT of the format * [.jwt()](#SdJwt+jwt) ⇒ string * [.disclosures()](#SdJwt+disclosures) ⇒ Array.<string> * [.keyBindingJwt()](#SdJwt+keyBindingJwt) ⇒ string \| undefined - * [.toJSON()](#SdJwt+toJSON) ⇒ any * [.clone()](#SdJwt+clone) ⇒ [SdJwt](#SdJwt) * _static_ * [.parse(sd_jwt)](#SdJwt.parse) ⇒ [SdJwt](#SdJwt) - * [.fromJSON(json)](#SdJwt.fromJSON) ⇒ [SdJwt](#SdJwt) @@ -5053,12 +5133,6 @@ The disclosures part. ### sdJwt.keyBindingJwt() ⇒ string \| undefined The optional key binding JWT. -**Kind**: instance method of [SdJwt](#SdJwt) - - -### sdJwt.toJSON() ⇒ any -Serializes this to a JSON object. - **Kind**: instance method of [SdJwt](#SdJwt) @@ -5080,17 +5154,6 @@ Returns `DeserializationError` if parsing fails. | --- | --- | | sd_jwt | string | - - -### SdJwt.fromJSON(json) ⇒ [SdJwt](#SdJwt) -Deserializes an instance from a JSON object. - -**Kind**: static method of [SdJwt](#SdJwt) - -| Param | Type | -| --- | --- | -| json | any | - ## SdJwtCredentialValidator @@ -5967,6 +6030,7 @@ A DID Document Verification Method. **Kind**: global class * [VerificationMethod](#VerificationMethod) + * [new VerificationMethod(id, controller, type_, data)](#new_VerificationMethod_new) * _instance_ * [.id()](#VerificationMethod+id) ⇒ [DIDUrl](#DIDUrl) * [.setId(id)](#VerificationMethod+setId) @@ -5984,6 +6048,19 @@ A DID Document Verification Method. * [.newFromJwk(did, key, [fragment])](#VerificationMethod.newFromJwk) ⇒ [VerificationMethod](#VerificationMethod) * [.fromJSON(json)](#VerificationMethod.fromJSON) ⇒ [VerificationMethod](#VerificationMethod) + + +### new VerificationMethod(id, controller, type_, data) +Create a custom [VerificationMethod](#VerificationMethod). + + +| Param | Type | +| --- | --- | +| id | [DIDUrl](#DIDUrl) | +| controller | [CoreDID](#CoreDID) | +| type_ | [MethodType](#MethodType) | +| data | [MethodData](#MethodData) | + ### verificationMethod.id() ⇒ [DIDUrl](#DIDUrl) @@ -6119,9 +6196,11 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | - + + +## StatusPurpose +Purpose of a [StatusList2021](#StatusList2021). -## CredentialStatus **Kind**: global variable @@ -6150,11 +6229,9 @@ The holder must match the subject only for credentials where the [`nonTransferab The holder is not required to have any kind of relationship to any credential subject. **Kind**: global variable - - -## StatusPurpose -Purpose of a [StatusList2021](#StatusList2021). + +## StateMetadataEncoding **Kind**: global variable @@ -6173,6 +6250,14 @@ Return all errors that occur during validation. ## FirstError Return after the first error occurs. +**Kind**: global variable + + +## MethodRelationship +**Kind**: global variable + + +## CredentialStatus **Kind**: global variable @@ -6205,20 +6290,6 @@ Validate the status if supported, skip any unsupported Skip all status checks. **Kind**: global variable - - -## StateMetadataEncoding -**Kind**: global variable - - -## MethodRelationship -**Kind**: global variable - - -## start() -Initializes the console error panic hook for better error messages - -**Kind**: global function ## verifyEd25519(alg, signingInput, decodedSignature, publicKey) @@ -6263,3 +6334,9 @@ Decode the given url-safe base64-encoded slice into its raw bytes. | --- | --- | | data | Uint8Array | + + +## start() +Initializes the console error panic hook for better error messages + +**Kind**: global function diff --git a/bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs b/bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs index 7b4f201206..c55de229e6 100644 --- a/bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs +++ b/bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs @@ -77,5 +77,4 @@ impl WasmSdJwt { } } -impl_wasm_json!(WasmSdJwt, SdJwt); impl_wasm_clone!(WasmSdJwt, SdJwt); diff --git a/bindings/wasm/src/verification/wasm_method_data.rs b/bindings/wasm/src/verification/wasm_method_data.rs index 5bba4aa5a9..58a9c65820 100644 --- a/bindings/wasm/src/verification/wasm_method_data.rs +++ b/bindings/wasm/src/verification/wasm_method_data.rs @@ -1,6 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_iota::verification::CustomMethodData; use identity_iota::verification::MethodData; use wasm_bindgen::prelude::*; @@ -45,6 +46,27 @@ impl WasmMethodData { Ok(Self(MethodData::PublicKeyJwk(key.0.clone()))) } + /// Creates a new custom {@link MethodData}. + #[wasm_bindgen(js_name = newCustom)] + pub fn new_custom(name: String, data: JsValue) -> Result { + let data = data.into_serde::().wasm_result()?; + Ok(Self(MethodData::Custom(CustomMethodData { name, data }))) + } + + /// Returns the wrapped custom method data format is `Custom`. + #[wasm_bindgen(js_name = tryCustom)] + pub fn try_custom(&self) -> Result { + self + .0 + .custom() + .map(|custom| custom.clone().into()) + .ok_or(WasmError::new( + Cow::Borrowed("MethodDataFormatError"), + Cow::Borrowed("method data format is not Custom"), + )) + .wasm_result() + } + /// Returns a `Uint8Array` containing the decoded bytes of the {@link MethodData}. /// /// This is generally a public key identified by a {@link MethodData} value. @@ -78,3 +100,31 @@ impl From for WasmMethodData { WasmMethodData(data) } } + +/// A custom verification method data format. +#[wasm_bindgen(js_name = CustomMethodData, inspectable)] +pub struct WasmCustomMethodData(pub(crate) CustomMethodData); + +#[wasm_bindgen(js_class = CustomMethodData)] +impl WasmCustomMethodData { + #[wasm_bindgen(constructor)] + pub fn new(name: String, data: JsValue) -> Result { + let data = data.into_serde::().wasm_result()?; + Ok(Self(CustomMethodData { name, data })) + } +} + +impl From for WasmCustomMethodData { + fn from(value: CustomMethodData) -> Self { + Self(value) + } +} + +impl From for CustomMethodData { + fn from(value: WasmCustomMethodData) -> Self { + value.0 + } +} + +impl_wasm_clone!(WasmCustomMethodData, CustomMethodData); +impl_wasm_json!(WasmCustomMethodData, CustomMethodData); diff --git a/bindings/wasm/src/verification/wasm_method_type.rs b/bindings/wasm/src/verification/wasm_method_type.rs index 9fb1fff660..4b7d297a62 100644 --- a/bindings/wasm/src/verification/wasm_method_type.rs +++ b/bindings/wasm/src/verification/wasm_method_type.rs @@ -27,6 +27,11 @@ impl WasmMethodType { WasmMethodType(MethodType::JSON_WEB_KEY) } + /// A custom method. + pub fn custom(type_: String) -> WasmMethodType { + WasmMethodType(MethodType::custom(type_)) + } + /// Returns the {@link MethodType} as a string. #[allow(clippy::inherent_to_string)] #[wasm_bindgen(js_name = toString)] diff --git a/bindings/wasm/src/verification/wasm_verification_method.rs b/bindings/wasm/src/verification/wasm_verification_method.rs index 62b5103c9d..6f01436ffe 100644 --- a/bindings/wasm/src/verification/wasm_verification_method.rs +++ b/bindings/wasm/src/verification/wasm_verification_method.rs @@ -8,6 +8,7 @@ use crate::did::WasmCoreDID; use crate::did::WasmDIDUrl; use crate::error::Result; use crate::error::WasmResult; +use identity_iota::core::Object; use identity_iota::did::CoreDID; use identity_iota::verification::VerificationMethod; use wasm_bindgen::prelude::*; @@ -37,6 +38,24 @@ impl WasmVerificationMethod { .wasm_result() } + /// Create a custom {@link VerificationMethod}. + #[wasm_bindgen(constructor)] + pub fn new( + id: &WasmDIDUrl, + controller: &WasmCoreDID, + type_: &WasmMethodType, + data: &WasmMethodData, + ) -> Result { + VerificationMethod::builder(Object::new()) + .type_(type_.0.clone()) + .data(data.0.clone()) + .controller(controller.0.clone()) + .id(id.0.clone()) + .build() + .map(Self) + .wasm_result() + } + /// Returns a copy of the {@link DIDUrl} of the {@link VerificationMethod}'s `id`. #[wasm_bindgen] pub fn id(&self) -> WasmDIDUrl { diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 136049bb28..0121a2d10c 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -14,7 +14,7 @@ iota-sdk = { version = "1.0", default-features = false, features = ["tls", "clie json-proof-token.workspace = true primitive-types = "0.12.1" rand = "0.8.5" -sd-jwt-payload = { version = "0.2.0", default-features = false, features = ["sha"] } +sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"] } serde_json = { version = "1.0", default-features = false } tokio = { version = "1.29", default-features = false, features = ["rt"] } diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index a09b971799..964c5a26bc 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -22,8 +22,8 @@ indexmap = { version = "2.0", default-features = false, features = ["std", "serd itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } once_cell = { version = "1.18", default-features = false, features = ["std"] } reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true } -roaring = { version = "0.10", default-features = false, features = ["std"], optional = true } -sd-jwt-payload = { version = "0.2.0", default-features = false, features = ["sha"], optional = true } +roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true } +sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"], optional = true } serde.workspace = true serde-aux = { version = "4.3.1", default-features = false, optional = true } serde_json.workspace = true diff --git a/identity_iota/README.md b/identity_iota/README.md index e210d6e10d..69d68defd8 100644 --- a/identity_iota/README.md +++ b/identity_iota/README.md @@ -24,7 +24,7 @@ ## Introduction -IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentralized digital identity, also known as Self-Sovereign Identity (SSI). It implements the W3C [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) specifications. This library can be used to create, resolve and authenticate digital identities and to create verifiable credentials and presentations in order to share information in a verifiable manner and establish trust in the digital world. It does so while supporting secure storage of cryptographic keys, which can be implemented for your preferred key management system. Many of the individual libraries (Rust crates) are agnostic over the concrete DID method, with the exception of some libraries dedicated to implement the [IOTA DID method](https://wiki.iota.org/shimmer/identity.rs/specs/did/iota_did_method_spec/), which is an implementation of decentralized digital identity on the IOTA and Shimmer networks. Written in stable Rust, IOTA Identity has strong guarantees of memory safety and process integrity while maintaining exceptional performance. +IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentralized digital identity, also known as Self-Sovereign Identity (SSI). It implements the W3C [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) specifications. This library can be used to create, resolve and authenticate digital identities and to create verifiable credentials and presentations in order to share information in a verifiable manner and establish trust in the digital world. It does so while supporting secure storage of cryptographic keys, which can be implemented for your preferred key management system. Many of the individual libraries (Rust crates) are agnostic over the concrete DID method, with the exception of some libraries dedicated to implement the [IOTA DID method](https://wiki.iota.org/identity.rs/specs/did/iota_did_method_spec/), which is an implementation of decentralized digital identity on the IOTA and Shimmer networks. Written in stable Rust, IOTA Identity has strong guarantees of memory safety and process integrity while maintaining exceptional performance. ## Bindings @@ -36,8 +36,8 @@ IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentra - API References: - [Rust API Reference](https://docs.rs/identity_iota/latest/identity_iota/): Package documentation (cargo docs). - - [Wasm API Reference](https://wiki.iota.org/shimmer/identity.rs/libraries/wasm/api_reference/): Wasm Package documentation. -- [Identity Documentation Pages](https://wiki.iota.org/shimmer/identity.rs/introduction): Supplementing documentation with context around identity and simple examples on library usage. + - [Wasm API Reference](https://wiki.iota.org/identity.rs/libraries/wasm/api_reference/): Wasm Package documentation. +- [Identity Documentation Pages](https://wiki.iota.org/identity.rs/introduction): Supplementing documentation with context around identity and simple examples on library usage. - [Examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples): Practical code snippets to get you started with the library. ## Prerequisites @@ -74,7 +74,7 @@ version = "1.0.0" edition = "2021" [dependencies] -identity_iota = {version = "1.1.1", features = ["memstore"]} +identity_iota = { version = "1.1.1", features = ["memstore"] } iota-sdk = { version = "1.0.2", default-features = true, features = ["tls", "client", "stronghold"] } tokio = { version = "1", features = ["full"] } anyhow = "1.0.62" @@ -214,7 +214,7 @@ For detailed development progress, see the IOTA Identity development [kanban boa We would love to have you help us with the development of IOTA Identity. Each and every contribution is greatly valued! -Please review the [contribution](https://wiki.iota.org/shimmer/identity.rs/contribute) and [workflow](https://wiki.iota.org/shimmer/identity.rs/workflow) sections in the [IOTA Wiki](https://wiki.iota.org/). +Please review the [contribution](https://wiki.iota.org/identity.rs/contribute) and [workflow](https://wiki.iota.org/identity.rs/workflow) sections in the [IOTA Wiki](https://wiki.iota.org/). To contribute directly to the repository, simply fork the project, push your changes to your fork and create a pull request to get them included! diff --git a/identity_verification/Cargo.toml b/identity_verification/Cargo.toml index 1b6bb11d77..3e4cec6663 100644 --- a/identity_verification/Cargo.toml +++ b/identity_verification/Cargo.toml @@ -14,7 +14,7 @@ identity_did = { version = "=1.1.1", path = "./../identity_did", default-feature identity_jose = { version = "=1.1.1", path = "./../identity_jose", default-features = false } serde.workspace = true strum.workspace = true +serde_json.workspace = true thiserror.workspace = true [dev-dependencies] -serde_json.workspace = true diff --git a/identity_verification/src/verification_method/material.rs b/identity_verification/src/verification_method/material.rs index 4d5f5775aa..8e881253c5 100644 --- a/identity_verification/src/verification_method/material.rs +++ b/identity_verification/src/verification_method/material.rs @@ -5,6 +5,12 @@ use crate::jose::jwk::Jwk; use core::fmt::Debug; use core::fmt::Formatter; use identity_core::convert::BaseEncoding; +use serde::de::Visitor; +use serde::ser::SerializeMap; +use serde::Deserialize; +use serde::Serialize; +use serde::Serializer; +use serde_json::Value; use crate::error::Error; use crate::error::Result; @@ -21,9 +27,9 @@ pub enum MethodData { PublicKeyBase58(String), /// Verification Material in the JSON Web Key format. PublicKeyJwk(Jwk), - /// Verification Material in CAIP-10 format. - /// [CAIP-10](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md) - BlockchainAccountId(String), + /// Arbitrary verification material. + #[serde(untagged)] + Custom(CustomMethodData), } impl MethodData { @@ -39,6 +45,11 @@ impl MethodData { Self::PublicKeyMultibase(BaseEncoding::encode_multibase(&data, None)) } + /// Creates a new `MethodData` variant from custom data. + pub fn new_custom(data: impl Into) -> Self { + Self::Custom(data.into()) + } + /// Returns a `Vec` containing the decoded bytes of the `MethodData`. /// /// This is generally a public key identified by a `MethodType` value. @@ -48,7 +59,7 @@ impl MethodData { /// represented as a vector of bytes. pub fn try_decode(&self) -> Result> { match self { - Self::PublicKeyJwk(_) | Self::BlockchainAccountId(_) => Err(Error::InvalidMethodDataTransformation( + Self::PublicKeyJwk(_) | Self::Custom(_) => Err(Error::InvalidMethodDataTransformation( "method data is not base encoded", )), Self::PublicKeyMultibase(input) => { @@ -71,6 +82,15 @@ impl MethodData { pub fn try_public_key_jwk(&self) -> Result<&Jwk> { self.public_key_jwk().ok_or(Error::NotPublicKeyJwk) } + + /// Returns the custom method data, if any. + pub fn custom(&self) -> Option<&CustomMethodData> { + if let Self::Custom(method_data) = self { + Some(method_data) + } else { + None + } + } } impl Debug for MethodData { @@ -79,7 +99,94 @@ impl Debug for MethodData { Self::PublicKeyJwk(inner) => f.write_fmt(format_args!("PublicKeyJwk({inner:#?})")), Self::PublicKeyMultibase(inner) => f.write_fmt(format_args!("PublicKeyMultibase({inner})")), Self::PublicKeyBase58(inner) => f.write_fmt(format_args!("PublicKeyBase58({inner})")), - Self::BlockchainAccountId(inner) => f.write_fmt(format_args!("BlockchainAccountId({inner})")), + Self::Custom(CustomMethodData { name, data }) => f.write_fmt(format_args!("{name}({data})")), } } } + +#[derive(Clone, Debug, PartialEq, Eq)] +/// Custom verification method. +pub struct CustomMethodData { + /// Verification method's name. + pub name: String, + /// Verification method's data. + pub data: Value, +} + +impl Serialize for CustomMethodData { + fn serialize(&self, serializer: S) -> std::prelude::v1::Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry(&self.name, &self.data)?; + map.end() + } +} + +impl<'de> Deserialize<'de> for CustomMethodData { + fn deserialize(deserializer: D) -> std::prelude::v1::Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_map(CustomMethodDataVisitor) + } +} + +struct CustomMethodDataVisitor; + +impl<'de> Visitor<'de> for CustomMethodDataVisitor { + type Value = CustomMethodData; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("\"\": ") + } + fn visit_map(self, mut map: A) -> std::prelude::v1::Result + where + A: serde::de::MapAccess<'de>, + { + let mut custom_method_data = CustomMethodData { + name: String::default(), + data: Value::Null, + }; + while let Some((name, data)) = map.next_entry::()? { + custom_method_data = CustomMethodData { name, data }; + } + + Ok(custom_method_data) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn serialize_custom_method_data() { + let custom = MethodData::Custom(CustomMethodData { + name: "anArbitraryMethod".to_owned(), + data: json!({"a": 1, "b": 2}), + }); + let target_str = json!({ + "anArbitraryMethod": {"a": 1, "b": 2}, + }) + .to_string(); + assert_eq!(serde_json::to_string(&custom).unwrap(), target_str); + } + #[test] + fn deserialize_custom_method_data() { + let inner_data = json!({ + "firstCustomField": "a random string", + "secondCustomField": 420, + }); + let json_method_data = json!({ + "myCustomVerificationMethod": &inner_data, + }); + let custom = serde_json::from_value::(json_method_data.clone()).unwrap(); + let target_method_data = MethodData::Custom(CustomMethodData { + name: "myCustomVerificationMethod".to_owned(), + data: inner_data, + }); + assert_eq!(custom, target_method_data); + } +} diff --git a/identity_verification/src/verification_method/method.rs b/identity_verification/src/verification_method/method.rs index 360f2efe55..8c48e06893 100644 --- a/identity_verification/src/verification_method/method.rs +++ b/identity_verification/src/verification_method/method.rs @@ -20,6 +20,7 @@ use crate::verification_method::MethodBuilder; use crate::verification_method::MethodData; use crate::verification_method::MethodRef; use crate::verification_method::MethodType; +use crate::CustomMethodData; use identity_did::CoreDID; use identity_did::DIDUrl; use identity_did::DID; @@ -28,8 +29,8 @@ use identity_did::DID; /// /// [Specification](https://www.w3.org/TR/did-core/#verification-method-properties) #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(from = "_VerificationMethod")] pub struct VerificationMethod { - #[serde(deserialize_with = "deserialize_id_with_fragment")] pub(crate) id: DIDUrl, pub(crate) controller: CoreDID, #[serde(rename = "type")] @@ -245,3 +246,46 @@ impl KeyComparable for VerificationMethod { self.id() } } + +// 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. +#[derive(Deserialize)] +struct _VerificationMethod { + #[serde(deserialize_with = "deserialize_id_with_fragment")] + pub(crate) id: DIDUrl, + pub(crate) controller: CoreDID, + #[serde(rename = "type")] + pub(crate) type_: MethodType, + #[serde(flatten)] + pub(crate) data: MethodData, + #[serde(flatten)] + pub(crate) properties: Object, +} + +impl From<_VerificationMethod> for VerificationMethod { + fn from(value: _VerificationMethod) -> Self { + let _VerificationMethod { + id, + controller, + type_, + data, + mut properties, + } = value; + let key = match &data { + MethodData::PublicKeyBase58(_) => "publicKeyBase58", + MethodData::PublicKeyJwk(_) => "publicKeyJwk", + MethodData::PublicKeyMultibase(_) => "publicKeyMultibase", + MethodData::Custom(CustomMethodData { name, .. }) => name.as_str(), + }; + properties.remove(key); + + VerificationMethod { + id, + controller, + type_, + data, + properties, + } + } +} diff --git a/identity_verification/src/verification_method/method_type.rs b/identity_verification/src/verification_method/method_type.rs index aa80ef4580..ae3877948d 100644 --- a/identity_verification/src/verification_method/method_type.rs +++ b/identity_verification/src/verification_method/method_type.rs @@ -12,7 +12,6 @@ use crate::error::Result; const ED25519_VERIFICATION_KEY_2018_STR: &str = "Ed25519VerificationKey2018"; const X25519_KEY_AGREEMENT_KEY_2019_STR: &str = "X25519KeyAgreementKey2019"; const JSON_WEB_KEY_METHOD_TYPE: &str = "JsonWebKey"; -const ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020_STR: &str = "EcdsaSecp256k1RecoverySignature2020"; /// verification method types. #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] @@ -26,9 +25,10 @@ impl MethodType { /// A verification method for use with JWT verification as prescribed by the [`Jwk`](::identity_jose::jwk::Jwk) /// in the [`publicKeyJwk`](crate::MethodData::PublicKeyJwk) entry. pub const JSON_WEB_KEY: Self = Self(Cow::Borrowed(JSON_WEB_KEY_METHOD_TYPE)); - /// The `EcdsaSecp256k1RecoverySignature2020` method type. - pub const ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020: Self = - Self(Cow::Borrowed(ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020_STR)); + /// Construct a custom method type. + pub fn custom(type_: impl AsRef) -> Self { + Self(Cow::Owned(type_.as_ref().to_owned())) + } } impl MethodType { @@ -58,7 +58,6 @@ impl FromStr for MethodType { ED25519_VERIFICATION_KEY_2018_STR => Ok(Self::ED25519_VERIFICATION_KEY_2018), X25519_KEY_AGREEMENT_KEY_2019_STR => Ok(Self::X25519_KEY_AGREEMENT_KEY_2019), JSON_WEB_KEY_METHOD_TYPE => Ok(Self::JSON_WEB_KEY), - ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020_STR => Ok(Self::ECDSA_SECP256K1_RECOVERY_SIGNATURE_2020), _ => Ok(Self(Cow::Owned(string.to_owned()))), } } diff --git a/identity_verification/src/verification_method/mod.rs b/identity_verification/src/verification_method/mod.rs index af6da98529..585b58639c 100644 --- a/identity_verification/src/verification_method/mod.rs +++ b/identity_verification/src/verification_method/mod.rs @@ -15,6 +15,7 @@ mod method_scope; mod method_type; pub use self::builder::MethodBuilder; +pub use self::material::CustomMethodData; pub use self::material::MethodData; pub use self::method::VerificationMethod; pub use self::method_ref::MethodRef; From 37d1bd51f33ce6952d83e3c8043acc2e27ccae69 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 20 Mar 2024 14:17:18 +0100 Subject: [PATCH 03/33] Wasm bindings for Jpt credentials --- bindings/wasm/src/common/types.rs | 6 ++ bindings/wasm/src/credential/jpt.rs | 36 +++++++ .../decoded_jpt_credential.rs | 42 ++++++++ .../jpt_credential_validation_options.rs | 77 +++++++++++++++ .../jpt_credential_validator.rs | 30 ++++++ .../jpt_credential_validator_utils.rs | 95 +++++++++++++++++++ .../jwp_verification_options.rs | 45 +++++++++ .../jpt_credential_validator/mod.rs | 11 +++ bindings/wasm/src/credential/mod.rs | 4 + .../wasm/src/credential/revocation/mod.rs | 1 + .../revocation/validity_timeframe_2024/mod.rs | 3 + .../validity_timeframe_2024/status.rs | 72 ++++++++++++++ examples/Cargo.toml | 2 +- identity_credential/Cargo.toml | 4 +- .../revocation_timeframe_status.rs | 9 +- .../jpt_credential_validator_utils.rs | 2 +- identity_storage/Cargo.toml | 3 +- identity_verification/Cargo.toml | 2 +- 18 files changed, 434 insertions(+), 10 deletions(-) create mode 100644 bindings/wasm/src/credential/jpt.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/mod.rs create mode 100644 bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs create mode 100644 bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs diff --git a/bindings/wasm/src/common/types.rs b/bindings/wasm/src/common/types.rs index 295e0ea447..8264e923ce 100644 --- a/bindings/wasm/src/common/types.rs +++ b/bindings/wasm/src/common/types.rs @@ -75,3 +75,9 @@ impl TryFrom<&Object> for MapStringAny { Ok(map.unchecked_into::()) } } + +impl Default for MapStringAny { + fn default() -> Self { + js_sys::Map::new().unchecked_into() + } +} diff --git a/bindings/wasm/src/credential/jpt.rs b/bindings/wasm/src/credential/jpt.rs new file mode 100644 index 0000000000..090110b018 --- /dev/null +++ b/bindings/wasm/src/credential/jpt.rs @@ -0,0 +1,36 @@ +use identity_iota::credential::Jpt; +use wasm_bindgen::prelude::*; + +/// A JSON Proof Token (JPT). +#[wasm_bindgen(js_name = Jpt)] +pub struct WasmJpt(pub(crate) Jpt); + +#[wasm_bindgen(js_class = Jpt)] +impl WasmJpt { + /// Creates a new {@link Jpt}. + #[wasm_bindgen(constructor)] + pub fn new(jpt_string: String) -> Self { + WasmJpt(Jpt::new(jpt_string)) + } + + // Returns the string representation for this {@link Jpt}. + #[allow(clippy::inherent_to_string)] + #[wasm_bindgen(js_name = "toString")] + pub fn to_string(&self) -> String { + self.0.as_str().to_owned() + } +} + +impl_wasm_clone!(WasmJpt, Jpt); + +impl From for WasmJpt { + fn from(value: Jpt) -> Self { + WasmJpt(value) + } +} + +impl From for Jpt { + fn from(value: WasmJpt) -> Self { + value.0 + } +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs b/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs new file mode 100644 index 0000000000..9cd56b2d23 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs @@ -0,0 +1,42 @@ +use identity_iota::core::Object; +use identity_iota::credential::DecodedJptCredential; +use wasm_bindgen::prelude::*; + +use crate::common::MapStringAny; +use crate::credential::WasmCredential; +use crate::error::Result; + +#[wasm_bindgen(js_name = DecodedJptCredential)] +pub struct WasmDecodedJptCredential(pub(crate) DecodedJptCredential); + +impl_wasm_clone!(WasmDecodedJptCredential, DecodedJptCredential); + +#[wasm_bindgen(js_class = DecodedJptCredential)] +impl WasmDecodedJptCredential { + /// Returns the {@link Credential} embedded into this JPT. + #[wasm_bindgen] + pub fn credential(&self) -> WasmCredential { + WasmCredential(self.0.credential.clone()) + } + + /// Returns the custom claims parsed from the JPT. + #[wasm_bindgen(js_name = "customClaims")] + pub fn custom_claims(&self) -> Result { + match self.0.custom_claims.clone() { + Some(obj) => MapStringAny::try_from(obj), + None => Ok(MapStringAny::default()), + } + } +} + +impl From for WasmDecodedJptCredential { + fn from(value: DecodedJptCredential) -> Self { + WasmDecodedJptCredential(value) + } +} + +impl From for DecodedJptCredential { + fn from(value: WasmDecodedJptCredential) -> Self { + value.0 + } +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs new file mode 100644 index 0000000000..e7881671b2 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs @@ -0,0 +1,77 @@ +use identity_iota::credential::JptCredentialValidationOptions; +use wasm_bindgen::prelude::*; + +use crate::error::Result; +use crate::error::WasmResult; + +/// Options to declare validation criteria for {@link Jpt}. +#[derive(Debug, Default, Clone)] +#[wasm_bindgen(js_name = "JptCredentialValidationOptions", inspectable)] +pub struct WasmJptCredentialValidationOptions(pub(crate) JptCredentialValidationOptions); + +impl_wasm_clone!(WasmJptCredentialValidationOptions, JptCredentialValidationOptions); +impl_wasm_json!(WasmJptCredentialValidationOptions, JptCredentialValidationOptions); + +#[wasm_bindgen(js_class = JptCredentialValidationOptions)] +impl WasmJptCredentialValidationOptions { + /// Creates a new default istance. + #[wasm_bindgen(constructor)] + pub fn new(opts: Option) -> Result { + if let Some(opts) = opts { + opts.into_serde().wasm_result().map(WasmJptCredentialValidationOptions) + } else { + Ok(WasmJptCredentialValidationOptions::default()) + } + } +} + +impl From for WasmJptCredentialValidationOptions { + fn from(value: JptCredentialValidationOptions) -> Self { + WasmJptCredentialValidationOptions(value) + } +} + +impl From for JptCredentialValidationOptions { + fn from(value: WasmJptCredentialValidationOptions) -> Self { + value.0 + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJptCredentialValidationOptions")] + pub type IJptCredentialValidationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_JPT_CREDENTIAL_VALIDATION_OPTIONS: &'static str = r#" +/** Holds options to create a new {@link JwtPresentationValidationOptions}. */ +interface IJwtPresentationValidationOptions { + /** + * Declare that the credential is **not** considered valid if it expires before this {@link Timestamp}. + * Uses the current datetime during validation if not set. + */ + readonly earliestExpiryDate?: Timestamp; + + /** + * Declare that the credential is **not** considered valid if it was issued later than this {@link Timestamp}. + * Uses the current datetime during validation if not set. + */ + readonly latestIssuanceDate?: Timestamp; + + /** + * Validation behaviour for [`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). + */ + readonly status?: StatusCheck; + + /** Declares how credential subjects must relate to the presentation holder during validation. + * + * + */ + readonly subjectHolderRelationship?: [string, SubjectHolderRelationship]; + + /** + * Options which affect the verification of the proof on the credential. + */ + readonly verificationOptions?: JwpVerificationOptions; +}"#; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs new file mode 100644 index 0000000000..5ee75daf8d --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs @@ -0,0 +1,30 @@ +use crate::common::ImportedDocumentLock; +use crate::credential::WasmDecodedJptCredential; +use crate::credential::WasmFailFast; +use crate::credential::WasmJpt; +use crate::credential::WasmJptCredentialValidationOptions; +use crate::did::WasmCoreDocument; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::JptCredentialValidator; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JptCredentialValidator)] +pub struct WasmJptCredentialValidator; + +#[wasm_bindgen(js_class = JptCredentialValidator)] +impl WasmJptCredentialValidator { + #[wasm_bindgen] + pub fn validate( + credential_jpt: &WasmJpt, + issuer: WasmCoreDocument, + options: &WasmJptCredentialValidationOptions, + fail_fast: WasmFailFast, + ) -> Result { + let issuer_doc = ImportedDocumentLock::Core(issuer.0); + let doc = issuer_doc.try_read()?; + JptCredentialValidator::validate(&credential_jpt.0, &doc, &options.0, fail_fast.into()) + .wasm_result() + .map(WasmDecodedJptCredential) + } +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs new file mode 100644 index 0000000000..18e27cf4fd --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs @@ -0,0 +1,95 @@ +use crate::common::ImportedDocumentLock; +use crate::common::WasmTimestamp; +use crate::credential::options::WasmStatusCheck; +use crate::credential::WasmCredential; +use crate::credential::WasmJpt; +use crate::did::WasmCoreDID; +use crate::did::WasmCoreDocument; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::core::Object; +use identity_iota::credential::JptCredentialValidatorUtils; +use identity_iota::did::CoreDID; +use wasm_bindgen::prelude::*; + +/// Utility functions for validating JPT credentials. +#[wasm_bindgen(js_name = JptCredentialValidatorUtils)] +#[derive(Default)] +pub struct WasmJptCredentialValidatorUtils; + +#[wasm_bindgen(js_class = JptCredentialValidatorUtils)] +impl WasmJptCredentialValidatorUtils { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmJptCredentialValidatorUtils { + WasmJptCredentialValidatorUtils + } + + /// Utility for extracting the issuer field of a {@link `Credential`} as a DID. + /// # Errors + /// Fails if the issuer field is not a valid DID. + #[wasm_bindgen(js_name = "extractIssuer")] + pub fn extract_issuer(credential: &WasmCredential) -> Result { + JptCredentialValidatorUtils::extract_issuer::(&credential.0) + .wasm_result() + .map(WasmCoreDID::from) + } + /// Utility for extracting the issuer field of a credential in JPT representation as DID. + /// # Errors + /// If the JPT decoding fails or the issuer field is not a valid DID. + #[wasm_bindgen(js_name = "extractIssuerFromIssuedJpt")] + pub fn extract_issuer_from_issued_jpt(credential: &WasmJpt) -> Result { + JptCredentialValidatorUtils::extract_issuer_from_issued_jpt::(&credential.0) + .wasm_result() + .map(WasmCoreDID::from) + } + + #[wasm_bindgen(js_name = "checkTimeframesWithValidityTimeframe2024")] + pub fn check_timeframes_with_validity_timeframe_2024( + credential: &WasmCredential, + validity_timeframe: Option, + status_check: WasmStatusCheck, + ) -> Result<()> { + JptCredentialValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &credential.0, + validity_timeframe.map(|t| t.0), + status_check.into(), + ) + .wasm_result() + } + + /// Checks whether the credential status has been revoked. + /// + /// Only supports `RevocationTimeframe2024`. + #[wasm_bindgen(js_name = "checkRevocationWithValidityTimeframe2024")] + pub fn check_revocation_with_validity_timeframe_2024( + credential: &WasmCredential, + issuer: WasmCoreDocument, + status_check: WasmStatusCheck, + ) -> Result<()> { + let issuer_doc = ImportedDocumentLock::Core(issuer.0); + let doc = issuer_doc.try_read()?; + JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024(&credential.0, &doc, status_check.into()) + .wasm_result() + } + + /// Checks whether the credential status has been revoked or the timeframe interval is INVALID + /// + /// Only supports `RevocationTimeframe2024`. + #[wasm_bindgen(js_name = "checkTimeframesAndRevocationWithValidityTimeframe2024")] + pub fn check_timeframes_and_revocation_with_validity_timeframe_2024( + credential: &WasmCredential, + issuer: WasmCoreDocument, + validity_timeframe: Option, + status_check: WasmStatusCheck, + ) -> Result<()> { + let issuer_doc = ImportedDocumentLock::Core(issuer.0); + let doc = issuer_doc.try_read()?; + JptCredentialValidatorUtils::check_timeframes_and_revocation_with_validity_timeframe_2024( + &credential.0, + &doc, + validity_timeframe.map(|t| t.0), + status_check.into(), + ) + .wasm_result() + } +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs new file mode 100644 index 0000000000..2f107276fc --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs @@ -0,0 +1,45 @@ +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::document::verifiable::JwpVerificationOptions; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JwpVerificationOptions, inspectable)] +#[derive(Clone, Debug, Default)] +pub struct WasmJwpVerificationOptions(pub(crate) JwpVerificationOptions); + +impl_wasm_clone!(WasmJwpVerificationOptions, JwpVerificationOptions); +impl_wasm_json!(WasmJwpVerificationOptions, JwpVerificationOptions); + +#[wasm_bindgen(js_class = JwpVerificationOptions)] +impl WasmJwpVerificationOptions { + pub fn new(opts: Option) -> Result { + if let Some(opts) = opts { + opts.into_serde().wasm_result().map(WasmJwpVerificationOptions) + } else { + Ok(WasmJwpVerificationOptions::default()) + } + } +} + +// Interface to allow creating {@link JwpVerificationOptions} easily. +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJwpVerificationOptions")] + pub type IJwpVerificationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_JWP_VERIFICATION_OPTIONS: &'static str = r#" +/** Holds options to create a new {@link JwpVerificationOptions}. */ +interface IJwpVerificationOptions { + /** + * Verify the signing verification method relation matches this. + */ + readonly methodScope?: MethodScope; + + /** + * The DID URL of the method, whose JWK should be used to verify the JWP. + * If unset, the `kid` of the JWP is used as the DID URL. + */ + readonly methodId?: DIDUrl; +}"#; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/mod.rs b/bindings/wasm/src/credential/jpt_credential_validator/mod.rs new file mode 100644 index 0000000000..963d7493ca --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/mod.rs @@ -0,0 +1,11 @@ +mod decoded_jpt_credential; +mod jpt_credential_validation_options; +mod jpt_credential_validator; +mod jpt_credential_validator_utils; +mod jwp_verification_options; + +pub use decoded_jpt_credential::*; +pub use jpt_credential_validation_options::*; +pub use jpt_credential_validator::*; +pub use jpt_credential_validator_utils::*; +pub use jwp_verification_options::*; diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs index 832eac1cd4..8f7d07c960 100644 --- a/bindings/wasm/src/credential/mod.rs +++ b/bindings/wasm/src/credential/mod.rs @@ -6,6 +6,8 @@ pub use self::credential::WasmCredential; pub use self::credential_builder::*; pub use self::domain_linkage_configuration::WasmDomainLinkageConfiguration; +pub use self::jpt::*; +pub use self::jpt_credential_validator::*; pub use self::jws::WasmJws; pub use self::jwt::WasmJwt; pub use self::jwt_credential_validation::*; @@ -22,6 +24,8 @@ mod credential_builder; mod domain_linkage_configuration; mod domain_linkage_credential_builder; mod domain_linkage_validator; +mod jpt; +mod jpt_credential_validator; mod jws; mod jwt; mod jwt_credential_validation; diff --git a/bindings/wasm/src/credential/revocation/mod.rs b/bindings/wasm/src/credential/revocation/mod.rs index 7ad04980b4..c0f075df39 100644 --- a/bindings/wasm/src/credential/revocation/mod.rs +++ b/bindings/wasm/src/credential/revocation/mod.rs @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 pub mod status_list_2021; +pub mod validity_timeframe_2024; diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs new file mode 100644 index 0000000000..2da8555a22 --- /dev/null +++ b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs @@ -0,0 +1,3 @@ +mod status; + +pub use status::*; diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs new file mode 100644 index 0000000000..469716f3ec --- /dev/null +++ b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs @@ -0,0 +1,72 @@ +use identity_iota::credential::RevocationTimeframeStatus; +use identity_iota::did::DIDUrl; +use wasm_bindgen::prelude::*; + +use crate::common::WasmDuration; +use crate::common::WasmTimestamp; +use crate::error::Result; +use crate::error::WasmResult; + +/// Information used to determine the current status of a {@link Credential}. +#[wasm_bindgen(js_name = RevocationTimeframeStatus, inspectable)] +pub struct WasmRevocationTimeframeStatus(pub(crate) RevocationTimeframeStatus); + +impl_wasm_clone!(WasmRevocationTimeframeStatus, RevocationTimeframeStatus); +impl_wasm_json!(WasmRevocationTimeframeStatus, RevocationTimeframeStatus); + +#[wasm_bindgen(js_class = RevocationTimeframeStatus)] +impl WasmRevocationTimeframeStatus { + /// Creates a new `RevocationTimeframeStatus`. + #[wasm_bindgen(constructor)] + pub fn new( + id: String, + index: u32, + duration: WasmDuration, + start_validity: Option, + ) -> Result { + RevocationTimeframeStatus::new( + start_validity.map(|t| t.0), + duration.0, + DIDUrl::parse(id).wasm_result()?, + index, + ) + .wasm_result() + .map(WasmRevocationTimeframeStatus) + } + + /// Get startValidityTimeframe value. + #[wasm_bindgen(js_name = "startValidityTimeframe")] + pub fn start_validity_timeframe(&self) -> Result { + self.0.start_validity_timeframe().wasm_result().map(WasmTimestamp) + } + + /// Get endValidityTimeframe value. + #[wasm_bindgen(js_name = "endValidityTimeframe")] + pub fn end_validity_timeframe(&self) -> Result { + self.0.end_validity_timeframe().wasm_result().map(WasmTimestamp) + } + + /// Return the URL fo the `RevocationBitmapStatus`. + #[wasm_bindgen] + pub fn id(&self) -> Result { + self.0.id().wasm_result().map(|url| url.to_string()) + } + + /// Return the index of the credential in the issuer's revocation bitmap if it can be decoded. + #[wasm_bindgen] + pub fn index(&self) -> Result { + self.0.index().wasm_result() + } +} + +impl From for WasmRevocationTimeframeStatus { + fn from(value: RevocationTimeframeStatus) -> Self { + WasmRevocationTimeframeStatus(value) + } +} + +impl From for RevocationTimeframeStatus { + fn from(value: WasmRevocationTimeframeStatus) -> Self { + value.0 + } +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 0121a2d10c..53c15c483a 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -99,4 +99,4 @@ name = "9_zkp" [[example]] path = "1_advanced/10_zkp_revocation.rs" -name = "10_zkp_revocation" \ No newline at end of file +name = "10_zkp_revocation" diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 964c5a26bc..9a58d63683 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -12,6 +12,7 @@ rust-version.workspace = true description = "An implementation of the Verifiable Credentials standard." [dependencies] +async-trait = { version = "0.1.64", default-features = false } flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true } futures = { version = "0.3", default-features = false, optional = true } identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } @@ -20,6 +21,7 @@ identity_document = { version = "=1.1.1", path = "../identity_document", default identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } +json-proof-token.workspace = true once_cell = { version = "1.18", default-features = false, features = ["std"] } reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true } roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true } @@ -31,9 +33,7 @@ serde_repr = { version = "0.1", default-features = false, optional = true } strum.workspace = true thiserror.workspace = true url = { version = "2.5", default-features = false } -json-proof-token.workspace = true zkryptium.workspace = true -async-trait = { version = "0.1.64", default-features = false } [dev-dependencies] anyhow = "1.0.62" diff --git a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs index e0ceaf4778..415568f706 100644 --- a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs +++ b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs @@ -8,10 +8,13 @@ use identity_core::common::Timestamp; use identity_core::common::Url; use identity_core::common::Value; use identity_did::DIDUrl; +use serde::Deserialize; +use serde::Serialize; use std::str::FromStr; /// Information used to determine the current status of a [`Credential`][crate::credential::Credential] -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "Status", into = "Status")] pub struct RevocationTimeframeStatus(Status); impl RevocationTimeframeStatus { @@ -57,7 +60,7 @@ impl RevocationTimeframeStatus { ))) } - /// Get startValidityTimeframe value + /// Get startValidityTimeframe value. pub fn start_validity_timeframe(&self) -> Result { if let Some(Value::String(timeframe)) = self.0.properties.get(Self::START_TIMEFRAME_PROPERTY) { Timestamp::from_str(timeframe) @@ -70,7 +73,7 @@ impl RevocationTimeframeStatus { } } - /// Get endValidityTimeframe value + /// Get endValidityTimeframe value. pub fn end_validity_timeframe(&self) -> Result { if let Some(Value::String(timeframe)) = self.0.properties.get(Self::END_TIMEFRAME_PROPERTY) { Timestamp::from_str(timeframe) diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs index 7a94ca422f..eeba0004a6 100644 --- a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs @@ -128,7 +128,7 @@ impl JptCredentialValidatorUtils { } } - /// Checks whether the credential status has been revoked + /// Checks whether the credential status has been revoked. /// /// Only supports `RevocationTimeframe2024`. pub fn check_revocation_with_validity_timeframe_2024< diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index c20138235d..b5743e2d75 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -21,16 +21,15 @@ identity_document = { version = "=1.1.1", path = "../identity_document", default identity_iota_core = { version = "=1.1.1", path = "../identity_iota_core", default-features = false, optional = true } identity_verification = { version = "=1.1.1", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"], optional = true } +json-proof-token.workspace = true rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default_features = false } serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"], optional = true } -json-proof-token.workspace = true zkryptium.workspace = true - [dev-dependencies] identity_credential = { version = "=1.1.1", path = "../identity_credential", features = ["revocation-bitmap"] } identity_eddsa_verifier = { version = "=1.1.1", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } diff --git a/identity_verification/Cargo.toml b/identity_verification/Cargo.toml index 3e4cec6663..e400aeebeb 100644 --- a/identity_verification/Cargo.toml +++ b/identity_verification/Cargo.toml @@ -13,8 +13,8 @@ identity_core = { version = "=1.1.1", path = "./../identity_core", default-featu identity_did = { version = "=1.1.1", path = "./../identity_did", default-features = false } identity_jose = { version = "=1.1.1", path = "./../identity_jose", default-features = false } serde.workspace = true -strum.workspace = true serde_json.workspace = true +strum.workspace = true thiserror.workspace = true [dev-dependencies] From a02f6fe1c9b50a62c918e96174cbcc5d0bec44af Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 20 Mar 2024 15:36:51 +0100 Subject: [PATCH 04/33] JPT presentation bindings --- .../decoded_jpt_presentation.rs | 48 +++++++++++++++ .../jpt_presentation_validation_options.rs | 59 +++++++++++++++++++ .../jpt_presentation_validator.rs | 38 ++++++++++++ .../jpt_presentation_validator_utils.rs | 38 ++++++++++++ .../jpt_presentiation_validation/mod.rs | 9 +++ bindings/wasm/src/credential/mod.rs | 2 + 6 files changed, 194 insertions(+) create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs new file mode 100644 index 0000000000..7f6c7f3377 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs @@ -0,0 +1,48 @@ +use identity_iota::core::Object; +use identity_iota::credential::DecodedJptPresentation; +use wasm_bindgen::prelude::*; + +use crate::common::MapStringAny; +use crate::credential::WasmCredential; +use crate::error::Result; + +#[wasm_bindgen(js_name = DecodedJptPresentation)] +pub struct WasmDecodedJptPresentation(pub(crate) DecodedJptPresentation); + +impl_wasm_clone!(WasmDecodedJptPresentation, DecodedJptPresentation); + +#[wasm_bindgen(js_class = DecodedJptPresentation)] +impl WasmDecodedJptPresentation { + /// Returns the {@link Credential} embedded into this JPT. + #[wasm_bindgen] + pub fn credential(&self) -> WasmCredential { + WasmCredential(self.0.credential.clone()) + } + + /// Returns the custom claims parsed from the JPT. + #[wasm_bindgen(js_name = "customClaims")] + pub fn custom_claims(&self) -> Result { + match self.0.custom_claims.clone() { + Some(obj) => MapStringAny::try_from(obj), + None => Ok(MapStringAny::default()), + } + } + + /// Returns the `aud` property parsed from the JWT claims. + #[wasm_bindgen] + pub fn aud(&self) -> Option { + self.0.aud.as_ref().map(ToString::to_string) + } +} + +impl From for WasmDecodedJptPresentation { + fn from(value: DecodedJptPresentation) -> Self { + WasmDecodedJptPresentation(value) + } +} + +impl From for DecodedJptPresentation { + fn from(value: WasmDecodedJptPresentation) -> Self { + value.0 + } +} \ No newline at end of file diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs new file mode 100644 index 0000000000..abc290cd32 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs @@ -0,0 +1,59 @@ +use identity_iota::credential::JptPresentationValidationOptions; +use wasm_bindgen::prelude::*; + +use crate::error::Result; +use crate::error::WasmResult; + +/// Options to declare validation criteria for a {@link Jpt} presentation. +#[derive(Debug, Default, Clone)] +#[wasm_bindgen(js_name = "JptPresentationValidationOptions", inspectable)] +pub struct WasmJptPresentationValidationOptions(pub(crate) JptPresentationValidationOptions); + +impl_wasm_clone!(WasmJptPresentationValidationOptions, JptPresentationValidationOptions); +impl_wasm_json!(WasmJptPresentationValidationOptions, JptPresentationValidationOptions); + +#[wasm_bindgen(js_class = JptPresentationValidationOptions)] +impl WasmJptPresentationValidationOptions { + #[wasm_bindgen(constructor)] + pub fn new(opts: Option) -> Result { + if let Some(opts) = opts { + opts.into_serde().wasm_result().map(WasmJptPresentationValidationOptions) + } else { + Ok(WasmJptPresentationValidationOptions::default()) + } + } +} + +impl From for WasmJptPresentationValidationOptions { + fn from(value: JptPresentationValidationOptions) -> Self { + WasmJptPresentationValidationOptions(value) + } +} + +impl From for JptPresentationValidationOptions { + fn from(value: WasmJptPresentationValidationOptions) -> Self { + value.0 + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJptPresentationValidationOptions")] + pub type IJptPresentationValidationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_JPT_PRESENTATION_VALIDATION_OPTIONS: &'static str = r#" +/** Holds options to create a new {@link JwtPresentationValidationOptions}. */ +interface IJwtPresentationValidationOptions { + /** + * The nonce to be placed in the Presentation Protected Header. + */ + readonly nonce?: string; + + /** + * Options which affect the verification of the proof on the credential. + */ + readonly verificationOptions?: JwpVerificationOptions; +}"#; + diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs new file mode 100644 index 0000000000..e059d99f68 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs @@ -0,0 +1,38 @@ +use crate::common::ImportedDocumentLock; +use crate::credential::WasmDecodedJptPresentation; +use crate::credential::WasmFailFast; +use crate::credential::WasmJpt; +use crate::credential::WasmJptPresentationValidationOptions; +use crate::did::WasmCoreDocument; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::JptPresentationValidator; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JptPresentationValidator)] +pub struct WasmJptPresentationValidator; + +#[wasm_bindgen(js_class = JptPresentationValidator)] +impl WasmJptPresentationValidator { + /// Decodes and validates a Presented {@link Credential} issued as a JPT (JWP Presented Form). A + /// {@link DecodedJptPresentation} is returned upon success. + /// + /// The following properties are validated according to `options`: + /// - the holder's proof on the JWP, + /// - the expiration date, + /// - the issuance date, + /// - the semantic structure. + #[wasm_bindgen] + pub fn validate( + presentation_jpt: &WasmJpt, + issuer: WasmCoreDocument, + options: &WasmJptPresentationValidationOptions, + fail_fast: WasmFailFast, + ) -> Result { + let issuer_doc = ImportedDocumentLock::Core(issuer.0); + let doc = issuer_doc.try_read()?; + JptPresentationValidator::validate(&presentation_jpt.0, &doc, &options.0, fail_fast.into()) + .wasm_result() + .map(WasmDecodedJptPresentation) + } +} diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs new file mode 100644 index 0000000000..c27e3429ed --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs @@ -0,0 +1,38 @@ +use crate::common::WasmTimestamp; +use crate::credential::{options::WasmStatusCheck, WasmCredential, WasmJpt}; +use crate::did::WasmCoreDID; +use crate::error::{Result, WasmResult}; +use identity_iota::credential::JptPresentationValidatorUtils; +use wasm_bindgen::prelude::*; + +/// Utility functions for verifying JPT presentations. +#[wasm_bindgen(js_name = JptPresentationValidatorUtils)] +pub struct WasmJptPresentationValidatorUtils; + +#[wasm_bindgen(js_class = JptPresentationValidatorUtils)] +impl WasmJptPresentationValidatorUtils { + /// Utility for extracting the issuer field of a credential in JPT representation as DID. + /// # Errors + /// If the JPT decoding fails or the issuer field is not a valid DID. + #[wasm_bindgen(js_name = "extractIssuerFromPresentedJpt")] + pub fn extract_issuer_from_presented_jpt(presentation: &WasmJpt) -> Result { + JptPresentationValidatorUtils::extract_issuer_from_presented_jpt(&presentation.0) + .wasm_result() + .map(WasmCoreDID) + } + + /// Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`. + #[wasm_bindgen(js_name = "checkTimeframesWithValidityTimeframe2024")] + pub fn check_timeframes_with_validity_timeframe_2024( + credential: &WasmCredential, + validity_timeframe: Option, + status_check: WasmStatusCheck, + ) -> Result<()> { + JptPresentationValidatorUtils::check_timeframes_with_validity_timeframe_2024( + &credential.0, + validity_timeframe.map(|t| t.0), + status_check.into(), + ) + .wasm_result() + } +} diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs new file mode 100644 index 0000000000..274a455416 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs @@ -0,0 +1,9 @@ +mod decoded_jpt_presentation; +mod jpt_presentation_validation_options; +mod jpt_presentation_validator; +mod jpt_presentation_validator_utils; + +pub use decoded_jpt_presentation::*; +pub use jpt_presentation_validation_options::*; +pub use jpt_presentation_validator_utils::*; +pub use jpt_presentation_validator::*; \ No newline at end of file diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs index 8f7d07c960..7ece9f9e8f 100644 --- a/bindings/wasm/src/credential/mod.rs +++ b/bindings/wasm/src/credential/mod.rs @@ -18,6 +18,7 @@ pub use self::presentation::*; pub use self::proof::WasmProof; pub use self::revocation::*; pub use self::types::*; +pub use self::jpt_presentiation_validation::*; mod credential; mod credential_builder; @@ -36,3 +37,4 @@ mod presentation; mod proof; mod revocation; mod types; +mod jpt_presentiation_validation; From 7b45d70868adc435b41586dd8c3d1703bfe7ec9e Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 20 Mar 2024 15:56:24 +0100 Subject: [PATCH 05/33] docs --- bindings/wasm/docs/api-reference.md | 655 ++++++++++++++++-- .../jpt_credential_validation_options.rs | 4 +- .../jpt_presentation_validation_options.rs | 4 +- 3 files changed, 582 insertions(+), 81 deletions(-) diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index 2f50e4ed3d..6fcacf2843 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -17,6 +17,10 @@ if the object is being concurrently modified.

DIDUrl

A method agnostic DID Url.

+
DecodedJptCredential
+
+
DecodedJptPresentation
+
DecodedJws

A cryptographically verified decoded token from a JWS.

Contains the decoded headers and the raw claims.

@@ -67,11 +71,32 @@ if the object is being concurrently modified.

An extension interface that provides helper functions for publication and resolution of DID documents in Alias Outputs.

+
Jpt
+

A JSON Proof Token (JPT).

+
+
JptCredentialValidationOptions
+

Options to declare validation criteria for Jpt.

+
+
JptCredentialValidator
+
+
JptCredentialValidatorUtils
+

Utility functions for validating JPT credentials.

+
+
JptPresentationValidationOptions
+

Options to declare validation criteria for a Jpt presentation.

+
+
JptPresentationValidator
+
+
JptPresentationValidatorUtils
+

Utility functions for verifying JPT presentations.

+
Jwk
JwkGenOutput

The result of a key generation in JwkStorage.

+
JwpVerificationOptions
+
Jws

A wrapper around a JSON Web Signature (JWS).

@@ -144,6 +169,9 @@ verifiable Credentials and Pre
RevocationBitmap

A compressed bitmap for managing credential revocation.

+
RevocationTimeframeStatus
+

Information used to determine the current status of a Credential.

+
SdJwt

Representation of an SD-JWT of the format <Issuer-signed JWT>~<Disclosure 1>~<Disclosure 2>~...~<Disclosure N>~<optional KB-JWT>.

@@ -190,25 +218,6 @@ working with storage backed DID documents.

## Members
-
StatusPurpose
-

Purpose of a StatusList2021.

-
-
SubjectHolderRelationship
-

Declares how credential subjects must relate to the presentation holder.

-

See also the Subject-Holder Relationship section of the specification.

-
-
AlwaysSubject
-

The holder must always match the subject on all credentials, regardless of their nonTransferable property. -This variant is the default.

-
-
SubjectOnNonTransferable
-

The holder must match the subject only for credentials where the nonTransferable property is true.

-
-
Any
-

The holder is not required to have any kind of relationship to any credential subject.

-
-
StateMetadataEncoding
-
FailFast

Declares when validation should return if an error occurs.

@@ -222,6 +231,11 @@ This variant is the default.

CredentialStatus
+
StateMetadataEncoding
+
+
StatusPurpose
+

Purpose of a StatusList2021.

+
StatusCheck

Controls validation behaviour when checking whether or not a credential has been revoked by its credentialStatus.

@@ -239,11 +253,31 @@ This variant is the default.

SkipAll

Skip all status checks.

+
SubjectHolderRelationship
+

Declares how credential subjects must relate to the presentation holder.

+

See also the Subject-Holder Relationship section of the specification.

+
+
AlwaysSubject
+

The holder must always match the subject on all credentials, regardless of their nonTransferable property. +This variant is the default.

+
+
SubjectOnNonTransferable
+

The holder must match the subject only for credentials where the nonTransferable property is true.

+
+
Any
+

The holder is not required to have any kind of relationship to any credential subject.

+
## Functions
+
encodeB64(data)string
+

Encode the given bytes in url-safe base64.

+
+
decodeB64(data)Uint8Array
+

Decode the given url-safe base64-encoded slice into its raw bytes.

+
verifyEd25519(alg, signingInput, decodedSignature, publicKey)

Verify a JWS signature secured with the EdDSA algorithm and curve Ed25519.

This function is useful when one is composing a IJwsVerifier that delegates @@ -252,12 +286,6 @@ This variant is the default.

This function does not check whether alg = EdDSA in the protected header. Callers are expected to assert this prior to calling the function.

-
encodeB64(data)string
-

Encode the given bytes in url-safe base64.

-
-
decodeB64(data)Uint8Array
-

Decode the given url-safe base64-encoded slice into its raw bytes.

-
start()

Initializes the console error panic hook for better error messages

@@ -1335,6 +1363,69 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## DecodedJptCredential +**Kind**: global class + +* [DecodedJptCredential](#DecodedJptCredential) + * [.clone()](#DecodedJptCredential+clone) ⇒ [DecodedJptCredential](#DecodedJptCredential) + * [.credential()](#DecodedJptCredential+credential) ⇒ [Credential](#Credential) + * [.customClaims()](#DecodedJptCredential+customClaims) ⇒ Map.<string, any> + + + +### decodedJptCredential.clone() ⇒ [DecodedJptCredential](#DecodedJptCredential) +Deep clones the object. + +**Kind**: instance method of [DecodedJptCredential](#DecodedJptCredential) + + +### decodedJptCredential.credential() ⇒ [Credential](#Credential) +Returns the [Credential](#Credential) embedded into this JPT. + +**Kind**: instance method of [DecodedJptCredential](#DecodedJptCredential) + + +### decodedJptCredential.customClaims() ⇒ Map.<string, any> +Returns the custom claims parsed from the JPT. + +**Kind**: instance method of [DecodedJptCredential](#DecodedJptCredential) + + +## DecodedJptPresentation +**Kind**: global class + +* [DecodedJptPresentation](#DecodedJptPresentation) + * [.clone()](#DecodedJptPresentation+clone) ⇒ [DecodedJptPresentation](#DecodedJptPresentation) + * [.credential()](#DecodedJptPresentation+credential) ⇒ [Credential](#Credential) + * [.customClaims()](#DecodedJptPresentation+customClaims) ⇒ Map.<string, any> + * [.aud()](#DecodedJptPresentation+aud) ⇒ string \| undefined + + + +### decodedJptPresentation.clone() ⇒ [DecodedJptPresentation](#DecodedJptPresentation) +Deep clones the object. + +**Kind**: instance method of [DecodedJptPresentation](#DecodedJptPresentation) + + +### decodedJptPresentation.credential() ⇒ [Credential](#Credential) +Returns the [Credential](#Credential) embedded into this JPT. + +**Kind**: instance method of [DecodedJptPresentation](#DecodedJptPresentation) + + +### decodedJptPresentation.customClaims() ⇒ Map.<string, any> +Returns the custom claims parsed from the JPT. + +**Kind**: instance method of [DecodedJptPresentation](#DecodedJptPresentation) + + +### decodedJptPresentation.aud() ⇒ string \| undefined +Returns the `aud` property parsed from the JWT claims. + +**Kind**: instance method of [DecodedJptPresentation](#DecodedJptPresentation) ## DecodedJws @@ -2797,6 +2888,292 @@ Fetches the `IAliasOutput` associated with the given DID. | client | IIotaIdentityClient | | did | [IotaDID](#IotaDID) | + + +## Jpt +A JSON Proof Token (JPT). + +**Kind**: global class + +* [Jpt](#Jpt) + * [new Jpt(jpt_string)](#new_Jpt_new) + * [.toString()](#Jpt+toString) ⇒ string + * [.clone()](#Jpt+clone) ⇒ [Jpt](#Jpt) + + + +### new Jpt(jpt_string) +Creates a new [Jpt](#Jpt). + + +| Param | Type | +| --- | --- | +| jpt_string | string | + + + +### jpt.toString() ⇒ string +**Kind**: instance method of [Jpt](#Jpt) + + +### jpt.clone() ⇒ [Jpt](#Jpt) +Deep clones the object. + +**Kind**: instance method of [Jpt](#Jpt) + + +## JptCredentialValidationOptions +Options to declare validation criteria for [Jpt](#Jpt). + +**Kind**: global class + +* [JptCredentialValidationOptions](#JptCredentialValidationOptions) + * [new JptCredentialValidationOptions([opts])](#new_JptCredentialValidationOptions_new) + * _instance_ + * [.clone()](#JptCredentialValidationOptions+clone) ⇒ [JptCredentialValidationOptions](#JptCredentialValidationOptions) + * [.toJSON()](#JptCredentialValidationOptions+toJSON) ⇒ any + * _static_ + * [.fromJSON(json)](#JptCredentialValidationOptions.fromJSON) ⇒ [JptCredentialValidationOptions](#JptCredentialValidationOptions) + + + +### new JptCredentialValidationOptions([opts]) +Creates a new default istance. + + +| Param | Type | +| --- | --- | +| [opts] | IJptCredentialValidationOptions \| undefined | + + + +### jptCredentialValidationOptions.clone() ⇒ [JptCredentialValidationOptions](#JptCredentialValidationOptions) +Deep clones the object. + +**Kind**: instance method of [JptCredentialValidationOptions](#JptCredentialValidationOptions) + + +### jptCredentialValidationOptions.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JptCredentialValidationOptions](#JptCredentialValidationOptions) + + +### JptCredentialValidationOptions.fromJSON(json) ⇒ [JptCredentialValidationOptions](#JptCredentialValidationOptions) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JptCredentialValidationOptions](#JptCredentialValidationOptions) + +| Param | Type | +| --- | --- | +| json | any | + + + +## JptCredentialValidator +**Kind**: global class + + +### JptCredentialValidator.validate(credential_jpt, issuer, options, fail_fast) ⇒ [DecodedJptCredential](#DecodedJptCredential) +**Kind**: static method of [JptCredentialValidator](#JptCredentialValidator) + +| Param | Type | +| --- | --- | +| credential_jpt | [Jpt](#Jpt) | +| issuer | [CoreDocument](#CoreDocument) | +| options | [JptCredentialValidationOptions](#JptCredentialValidationOptions) | +| fail_fast | [FailFast](#FailFast) | + + + +## JptCredentialValidatorUtils +Utility functions for validating JPT credentials. + +**Kind**: global class + +* [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + * [.extractIssuer(credential)](#JptCredentialValidatorUtils.extractIssuer) ⇒ [CoreDID](#CoreDID) + * [.extractIssuerFromIssuedJpt(credential)](#JptCredentialValidatorUtils.extractIssuerFromIssuedJpt) ⇒ [CoreDID](#CoreDID) + * [.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check)](#JptCredentialValidatorUtils.checkTimeframesWithValidityTimeframe2024) + * [.checkRevocationWithValidityTimeframe2024(credential, issuer, status_check)](#JptCredentialValidatorUtils.checkRevocationWithValidityTimeframe2024) + * [.checkTimeframesAndRevocationWithValidityTimeframe2024(credential, issuer, validity_timeframe, status_check)](#JptCredentialValidatorUtils.checkTimeframesAndRevocationWithValidityTimeframe2024) + + + +### JptCredentialValidatorUtils.extractIssuer(credential) ⇒ [CoreDID](#CoreDID) +Utility for extracting the issuer field of a [`Credential`](`Credential`) as a DID. +# Errors +Fails if the issuer field is not a valid DID. + +**Kind**: static method of [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | + + + +### JptCredentialValidatorUtils.extractIssuerFromIssuedJpt(credential) ⇒ [CoreDID](#CoreDID) +Utility for extracting the issuer field of a credential in JPT representation as DID. +# Errors +If the JPT decoding fails or the issuer field is not a valid DID. + +**Kind**: static method of [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Jpt](#Jpt) | + + + +### JptCredentialValidatorUtils.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check) +**Kind**: static method of [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| validity_timeframe | [Timestamp](#Timestamp) \| undefined | +| status_check | [StatusCheck](#StatusCheck) | + + + +### JptCredentialValidatorUtils.checkRevocationWithValidityTimeframe2024(credential, issuer, status_check) +Checks whether the credential status has been revoked. + +Only supports `RevocationTimeframe2024`. + +**Kind**: static method of [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| issuer | [CoreDocument](#CoreDocument) | +| status_check | [StatusCheck](#StatusCheck) | + + + +### JptCredentialValidatorUtils.checkTimeframesAndRevocationWithValidityTimeframe2024(credential, issuer, validity_timeframe, status_check) +Checks whether the credential status has been revoked or the timeframe interval is INVALID + +Only supports `RevocationTimeframe2024`. + +**Kind**: static method of [JptCredentialValidatorUtils](#JptCredentialValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| issuer | [CoreDocument](#CoreDocument) | +| validity_timeframe | [Timestamp](#Timestamp) \| undefined | +| status_check | [StatusCheck](#StatusCheck) | + + + +## JptPresentationValidationOptions +Options to declare validation criteria for a [Jpt](#Jpt) presentation. + +**Kind**: global class + +* [JptPresentationValidationOptions](#JptPresentationValidationOptions) + * [new JptPresentationValidationOptions([opts])](#new_JptPresentationValidationOptions_new) + * _instance_ + * [.clone()](#JptPresentationValidationOptions+clone) ⇒ [JptPresentationValidationOptions](#JptPresentationValidationOptions) + * [.toJSON()](#JptPresentationValidationOptions+toJSON) ⇒ any + * _static_ + * [.fromJSON(json)](#JptPresentationValidationOptions.fromJSON) ⇒ [JptPresentationValidationOptions](#JptPresentationValidationOptions) + + + +### new JptPresentationValidationOptions([opts]) + +| Param | Type | +| --- | --- | +| [opts] | IJptPresentationValidationOptions \| undefined | + + + +### jptPresentationValidationOptions.clone() ⇒ [JptPresentationValidationOptions](#JptPresentationValidationOptions) +Deep clones the object. + +**Kind**: instance method of [JptPresentationValidationOptions](#JptPresentationValidationOptions) + + +### jptPresentationValidationOptions.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JptPresentationValidationOptions](#JptPresentationValidationOptions) + + +### JptPresentationValidationOptions.fromJSON(json) ⇒ [JptPresentationValidationOptions](#JptPresentationValidationOptions) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JptPresentationValidationOptions](#JptPresentationValidationOptions) + +| Param | Type | +| --- | --- | +| json | any | + + + +## JptPresentationValidator +**Kind**: global class + + +### JptPresentationValidator.validate(presentation_jpt, issuer, options, fail_fast) ⇒ [DecodedJptPresentation](#DecodedJptPresentation) +Decodes and validates a Presented [Credential](#Credential) issued as a JPT (JWP Presented Form). A +[DecodedJptPresentation](#DecodedJptPresentation) is returned upon success. + +The following properties are validated according to `options`: +- the holder's proof on the JWP, +- the expiration date, +- the issuance date, +- the semantic structure. + +**Kind**: static method of [JptPresentationValidator](#JptPresentationValidator) + +| Param | Type | +| --- | --- | +| presentation_jpt | [Jpt](#Jpt) | +| issuer | [CoreDocument](#CoreDocument) | +| options | [JptPresentationValidationOptions](#JptPresentationValidationOptions) | +| fail_fast | [FailFast](#FailFast) | + + + +## JptPresentationValidatorUtils +Utility functions for verifying JPT presentations. + +**Kind**: global class + +* [JptPresentationValidatorUtils](#JptPresentationValidatorUtils) + * [.extractIssuerFromPresentedJpt(presentation)](#JptPresentationValidatorUtils.extractIssuerFromPresentedJpt) ⇒ [CoreDID](#CoreDID) + * [.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check)](#JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024) + + + +### JptPresentationValidatorUtils.extractIssuerFromPresentedJpt(presentation) ⇒ [CoreDID](#CoreDID) +Utility for extracting the issuer field of a credential in JPT representation as DID. +# Errors +If the JPT decoding fails or the issuer field is not a valid DID. + +**Kind**: static method of [JptPresentationValidatorUtils](#JptPresentationValidatorUtils) + +| Param | Type | +| --- | --- | +| presentation | [Jpt](#Jpt) | + + + +### JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check) +Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`. + +**Kind**: static method of [JptPresentationValidatorUtils](#JptPresentationValidatorUtils) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| validity_timeframe | [Timestamp](#Timestamp) \| undefined | +| status_check | [StatusCheck](#StatusCheck) | + ## Jwk @@ -3013,6 +3390,51 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## JwpVerificationOptions +**Kind**: global class + +* [JwpVerificationOptions](#JwpVerificationOptions) + * _instance_ + * [.clone()](#JwpVerificationOptions+clone) ⇒ [JwpVerificationOptions](#JwpVerificationOptions) + * [.toJSON()](#JwpVerificationOptions+toJSON) ⇒ any + * _static_ + * [.fromJSON(json)](#JwpVerificationOptions.fromJSON) ⇒ [JwpVerificationOptions](#JwpVerificationOptions) + * [.new([opts])](#JwpVerificationOptions.new) ⇒ [JwpVerificationOptions](#JwpVerificationOptions) + + + +### jwpVerificationOptions.clone() ⇒ [JwpVerificationOptions](#JwpVerificationOptions) +Deep clones the object. + +**Kind**: instance method of [JwpVerificationOptions](#JwpVerificationOptions) + + +### jwpVerificationOptions.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JwpVerificationOptions](#JwpVerificationOptions) + + +### JwpVerificationOptions.fromJSON(json) ⇒ [JwpVerificationOptions](#JwpVerificationOptions) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JwpVerificationOptions](#JwpVerificationOptions) + +| Param | Type | +| --- | --- | +| json | any | + + + +### JwpVerificationOptions.new([opts]) ⇒ [JwpVerificationOptions](#JwpVerificationOptions) +**Kind**: static method of [JwpVerificationOptions](#JwpVerificationOptions) + +| Param | Type | +| --- | --- | +| [opts] | IJwpVerificationOptions \| undefined | + ## Jws @@ -5072,6 +5494,85 @@ if it is a valid Revocation Bitmap Service. | --- | --- | | service | [Service](#Service) | + + +## RevocationTimeframeStatus +Information used to determine the current status of a [Credential](#Credential). + +**Kind**: global class + +* [RevocationTimeframeStatus](#RevocationTimeframeStatus) + * [new RevocationTimeframeStatus(id, index, duration, [start_validity])](#new_RevocationTimeframeStatus_new) + * _instance_ + * [.clone()](#RevocationTimeframeStatus+clone) ⇒ [RevocationTimeframeStatus](#RevocationTimeframeStatus) + * [.toJSON()](#RevocationTimeframeStatus+toJSON) ⇒ any + * [.startValidityTimeframe()](#RevocationTimeframeStatus+startValidityTimeframe) ⇒ [Timestamp](#Timestamp) + * [.endValidityTimeframe()](#RevocationTimeframeStatus+endValidityTimeframe) ⇒ [Timestamp](#Timestamp) + * [.id()](#RevocationTimeframeStatus+id) ⇒ string + * [.index()](#RevocationTimeframeStatus+index) ⇒ number + * _static_ + * [.fromJSON(json)](#RevocationTimeframeStatus.fromJSON) ⇒ [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + + +### new RevocationTimeframeStatus(id, index, duration, [start_validity]) +Creates a new `RevocationTimeframeStatus`. + + +| Param | Type | +| --- | --- | +| id | string | +| index | number | +| duration | [Duration](#Duration) | +| [start_validity] | [Timestamp](#Timestamp) \| undefined | + + + +### revocationTimeframeStatus.clone() ⇒ [RevocationTimeframeStatus](#RevocationTimeframeStatus) +Deep clones the object. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### revocationTimeframeStatus.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### revocationTimeframeStatus.startValidityTimeframe() ⇒ [Timestamp](#Timestamp) +Get startValidityTimeframe value. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### revocationTimeframeStatus.endValidityTimeframe() ⇒ [Timestamp](#Timestamp) +Get endValidityTimeframe value. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### revocationTimeframeStatus.id() ⇒ string +Return the URL fo the `RevocationBitmapStatus`. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### revocationTimeframeStatus.index() ⇒ number +Return the index of the credential in the issuer's revocation bitmap if it can be decoded. + +**Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + + +### RevocationTimeframeStatus.fromJSON(json) ⇒ [RevocationTimeframeStatus](#RevocationTimeframeStatus) +Deserializes an instance from a JSON object. + +**Kind**: static method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) + +| Param | Type | +| --- | --- | +| json | any | + ## SdJwt @@ -6196,43 +6697,6 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | - - -## StatusPurpose -Purpose of a [StatusList2021](#StatusList2021). - -**Kind**: global variable - - -## SubjectHolderRelationship -Declares how credential subjects must relate to the presentation holder. - -See also the [Subject-Holder Relationship](https://www.w3.org/TR/vc-data-model/#subject-holder-relationships) section of the specification. - -**Kind**: global variable - - -## AlwaysSubject -The holder must always match the subject on all credentials, regardless of their [`nonTransferable`](https://www.w3.org/TR/vc-data-model/#nontransferable-property) property. -This variant is the default. - -**Kind**: global variable - - -## SubjectOnNonTransferable -The holder must match the subject only for credentials where the [`nonTransferable`](https://www.w3.org/TR/vc-data-model/#nontransferable-property) property is `true`. - -**Kind**: global variable - - -## Any -The holder is not required to have any kind of relationship to any credential subject. - -**Kind**: global variable - - -## StateMetadataEncoding -**Kind**: global variable ## FailFast @@ -6258,6 +6722,16 @@ Return after the first error occurs. ## CredentialStatus +**Kind**: global variable + + +## StateMetadataEncoding +**Kind**: global variable + + +## StatusPurpose +Purpose of a [StatusList2021](#StatusList2021). + **Kind**: global variable @@ -6290,28 +6764,33 @@ Validate the status if supported, skip any unsupported Skip all status checks. **Kind**: global variable - + -## verifyEd25519(alg, signingInput, decodedSignature, publicKey) -Verify a JWS signature secured with the `EdDSA` algorithm and curve `Ed25519`. +## SubjectHolderRelationship +Declares how credential subjects must relate to the presentation holder. -This function is useful when one is composing a `IJwsVerifier` that delegates -`EdDSA` verification with curve `Ed25519` to this function. +See also the [Subject-Holder Relationship](https://www.w3.org/TR/vc-data-model/#subject-holder-relationships) section of the specification. -# Warning +**Kind**: global variable + -This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this -prior to calling the function. +## AlwaysSubject +The holder must always match the subject on all credentials, regardless of their [`nonTransferable`](https://www.w3.org/TR/vc-data-model/#nontransferable-property) property. +This variant is the default. -**Kind**: global function +**Kind**: global variable + -| Param | Type | -| --- | --- | -| alg | JwsAlgorithm | -| signingInput | Uint8Array | -| decodedSignature | Uint8Array | -| publicKey | [Jwk](#Jwk) | +## SubjectOnNonTransferable +The holder must match the subject only for credentials where the [`nonTransferable`](https://www.w3.org/TR/vc-data-model/#nontransferable-property) property is `true`. + +**Kind**: global variable + +## Any +The holder is not required to have any kind of relationship to any credential subject. + +**Kind**: global variable ## encodeB64(data) ⇒ string @@ -6334,6 +6813,28 @@ Decode the given url-safe base64-encoded slice into its raw bytes. | --- | --- | | data | Uint8Array | + + +## verifyEd25519(alg, signingInput, decodedSignature, publicKey) +Verify a JWS signature secured with the `EdDSA` algorithm and curve `Ed25519`. + +This function is useful when one is composing a `IJwsVerifier` that delegates +`EdDSA` verification with curve `Ed25519` to this function. + +# Warning + +This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this +prior to calling the function. + +**Kind**: global function + +| Param | Type | +| --- | --- | +| alg | JwsAlgorithm | +| signingInput | Uint8Array | +| decodedSignature | Uint8Array | +| publicKey | [Jwk](#Jwk) | + ## start() diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs index e7881671b2..55ff8bd0b2 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs @@ -45,8 +45,8 @@ extern "C" { #[wasm_bindgen(typescript_custom_section)] const I_JPT_CREDENTIAL_VALIDATION_OPTIONS: &'static str = r#" -/** Holds options to create a new {@link JwtPresentationValidationOptions}. */ -interface IJwtPresentationValidationOptions { +/** Holds options to create a new {@link JptCredentialValidationOptions}. */ +interface IJptCredentialValidationOptions { /** * Declare that the credential is **not** considered valid if it expires before this {@link Timestamp}. * Uses the current datetime during validation if not set. diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs index abc290cd32..4be235cb3d 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs @@ -44,8 +44,8 @@ extern "C" { #[wasm_bindgen(typescript_custom_section)] const I_JPT_PRESENTATION_VALIDATION_OPTIONS: &'static str = r#" -/** Holds options to create a new {@link JwtPresentationValidationOptions}. */ -interface IJwtPresentationValidationOptions { +/** Holds options to create a new {@link JptPresentationValidationOptions}. */ +interface IJptPresentationValidationOptions { /** * The nonce to be placed in the Presentation Protected Header. */ From 592bd9c949bef3071dc5233647997244ec6ef70f Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 21 Mar 2024 10:23:28 +0100 Subject: [PATCH 06/33] jsonprooftoken payloads --- bindings/wasm/Cargo.toml | 1 + .../decoded_jpt_presentation.rs | 4 +- .../jpt_presentation_validation_options.rs | 6 +- .../jpt_presentation_validator_utils.rs | 7 +- .../jpt_presentiation_validation/mod.rs | 2 +- bindings/wasm/src/credential/mod.rs | 4 +- bindings/wasm/src/error.rs | 2 + bindings/wasm/src/jpt/encoding.rs | 26 +++ bindings/wasm/src/jpt/jpt_issued.rs | 41 +++++ bindings/wasm/src/jpt/mod.rs | 7 + bindings/wasm/src/jpt/payload.rs | 148 ++++++++++++++++++ bindings/wasm/src/lib.rs | 1 + identity_jose/src/jwk/jwk_ext.rs | 1 + 13 files changed, 241 insertions(+), 9 deletions(-) create mode 100644 bindings/wasm/src/jpt/encoding.rs create mode 100644 bindings/wasm/src/jpt/jpt_issued.rs create mode 100644 bindings/wasm/src/jpt/mod.rs create mode 100644 bindings/wasm/src/jpt/payload.rs diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 27423f1eef..c0d98885b5 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -21,6 +21,7 @@ console_error_panic_hook = { version = "0.1" } futures = { version = "0.3" } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } js-sys = { version = "0.3.61" } +json-proof-token = "0.3.4" proc_typescript = { version = "0.1.0", path = "./proc_typescript" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", default-features = false } diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs index 7f6c7f3377..2fdfe94dcc 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs @@ -32,7 +32,7 @@ impl WasmDecodedJptPresentation { #[wasm_bindgen] pub fn aud(&self) -> Option { self.0.aud.as_ref().map(ToString::to_string) - } + } } impl From for WasmDecodedJptPresentation { @@ -45,4 +45,4 @@ impl From for DecodedJptPresentation { fn from(value: WasmDecodedJptPresentation) -> Self { value.0 } -} \ No newline at end of file +} diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs index 4be235cb3d..478bf7b5b7 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs @@ -17,7 +17,10 @@ impl WasmJptPresentationValidationOptions { #[wasm_bindgen(constructor)] pub fn new(opts: Option) -> Result { if let Some(opts) = opts { - opts.into_serde().wasm_result().map(WasmJptPresentationValidationOptions) + opts + .into_serde() + .wasm_result() + .map(WasmJptPresentationValidationOptions) } else { Ok(WasmJptPresentationValidationOptions::default()) } @@ -56,4 +59,3 @@ interface IJptPresentationValidationOptions { */ readonly verificationOptions?: JwpVerificationOptions; }"#; - diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs index c27e3429ed..3e8e05d97b 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs @@ -1,7 +1,10 @@ use crate::common::WasmTimestamp; -use crate::credential::{options::WasmStatusCheck, WasmCredential, WasmJpt}; +use crate::credential::options::WasmStatusCheck; +use crate::credential::WasmCredential; +use crate::credential::WasmJpt; use crate::did::WasmCoreDID; -use crate::error::{Result, WasmResult}; +use crate::error::Result; +use crate::error::WasmResult; use identity_iota::credential::JptPresentationValidatorUtils; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs index 274a455416..a39c129027 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs @@ -5,5 +5,5 @@ mod jpt_presentation_validator_utils; pub use decoded_jpt_presentation::*; pub use jpt_presentation_validation_options::*; +pub use jpt_presentation_validator::*; pub use jpt_presentation_validator_utils::*; -pub use jpt_presentation_validator::*; \ No newline at end of file diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs index 7ece9f9e8f..033a8cefd6 100644 --- a/bindings/wasm/src/credential/mod.rs +++ b/bindings/wasm/src/credential/mod.rs @@ -8,6 +8,7 @@ pub use self::credential_builder::*; pub use self::domain_linkage_configuration::WasmDomainLinkageConfiguration; pub use self::jpt::*; pub use self::jpt_credential_validator::*; +pub use self::jpt_presentiation_validation::*; pub use self::jws::WasmJws; pub use self::jwt::WasmJwt; pub use self::jwt_credential_validation::*; @@ -18,7 +19,6 @@ pub use self::presentation::*; pub use self::proof::WasmProof; pub use self::revocation::*; pub use self::types::*; -pub use self::jpt_presentiation_validation::*; mod credential; mod credential_builder; @@ -27,6 +27,7 @@ mod domain_linkage_credential_builder; mod domain_linkage_validator; mod jpt; mod jpt_credential_validator; +mod jpt_presentiation_validation; mod jws; mod jwt; mod jwt_credential_validation; @@ -37,4 +38,3 @@ mod presentation; mod proof; mod revocation; mod types; -mod jpt_presentiation_validation; diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index d7e8dfa3d8..035e7838bf 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -126,6 +126,8 @@ macro_rules! impl_wasm_error_from_with_struct_name { } } +impl_wasm_error_from_with_struct_name!(jsonprooftoken::errors::CustomError); + // identity_iota::iota now has some errors where the error message does not include the source error's error message. // This is in compliance with the Rust error handling project group's recommendation: // * An error type with a source error should either return that error via source or include that source's error message diff --git a/bindings/wasm/src/jpt/encoding.rs b/bindings/wasm/src/jpt/encoding.rs new file mode 100644 index 0000000000..bb647762a8 --- /dev/null +++ b/bindings/wasm/src/jpt/encoding.rs @@ -0,0 +1,26 @@ +use jsonprooftoken::encoding::SerializationType; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = SerializationType)] +pub enum WasmSerializationType { + COMPACT = 0, + JSON = 1, +} + +impl From for SerializationType { + fn from(value: WasmSerializationType) -> Self { + match value { + WasmSerializationType::COMPACT => SerializationType::COMPACT, + WasmSerializationType::JSON => SerializationType::JSON, + } + } +} + +impl From for WasmSerializationType { + fn from(value: SerializationType) -> Self { + match value { + SerializationType::COMPACT => WasmSerializationType::COMPACT, + SerializationType::JSON => WasmSerializationType::JSON, + } + } +} diff --git a/bindings/wasm/src/jpt/jpt_issued.rs b/bindings/wasm/src/jpt/jpt_issued.rs new file mode 100644 index 0000000000..80ab7e33bd --- /dev/null +++ b/bindings/wasm/src/jpt/jpt_issued.rs @@ -0,0 +1,41 @@ +use jsonprooftoken::jwp::issued::JwpIssued; +use wasm_bindgen::prelude::*; + +use super::WasmPayloads; +use super::WasmSerializationType; +use crate::error::Result; +use crate::error::WasmResult; + +#[wasm_bindgen(js_name = JwpIssued)] +pub struct WasmJwpIssued(pub(crate) JwpIssued); + +impl_wasm_json!(WasmJwpIssued, JwpIssued); +impl_wasm_clone!(WasmJwpIssued, JwpIssued); + +#[wasm_bindgen(js_class = JwpIssued)] +impl WasmJwpIssued { + #[wasm_bindgen] + pub fn encode(&self, serialization: WasmSerializationType) -> Result { + self.0.encode(serialization.into()).wasm_result() + } + + #[wasm_bindgen(js_name = "setProof")] + pub fn set_proof(&mut self, proof: &[u8]) { + self.0.set_proof(proof) + } + + #[wasm_bindgen(js_name = "getProof")] + pub fn get_proof(&self) -> Vec { + self.0.get_proof().to_owned() + } + + #[wasm_bindgen(js_name = "getPayloads")] + pub fn get_payloads(&self) -> WasmPayloads { + self.0.get_payloads().clone().into() + } + + #[wasm_bindgen(js_name = "setPayloads")] + pub fn set_payloads(&mut self, payloads: WasmPayloads) { + self.0.set_payloads(payloads.into()) + } +} diff --git a/bindings/wasm/src/jpt/mod.rs b/bindings/wasm/src/jpt/mod.rs new file mode 100644 index 0000000000..cbfa5d32a2 --- /dev/null +++ b/bindings/wasm/src/jpt/mod.rs @@ -0,0 +1,7 @@ +mod encoding; +mod jpt_issued; +mod payload; + +pub use encoding::*; +pub use jpt_issued::*; +pub use payload::*; diff --git a/bindings/wasm/src/jpt/payload.rs b/bindings/wasm/src/jpt/payload.rs new file mode 100644 index 0000000000..518a2c43e6 --- /dev/null +++ b/bindings/wasm/src/jpt/payload.rs @@ -0,0 +1,148 @@ +use crate::error::Result; +use crate::error::WasmError; +use crate::error::WasmResult; +use jsonprooftoken::jpt::payloads::PayloadType; +use jsonprooftoken::jpt::payloads::Payloads; +use serde_json::Value; +use std::borrow::Cow; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; + +#[wasm_bindgen(js_name = PayloadType)] +#[derive(Clone, Copy, Debug)] +pub enum WasmPayloadType { + Disclosed = 0, + Undisclosed = 1, + ProofMethods = 2, +} + +impl From for PayloadType { + fn from(value: WasmPayloadType) -> PayloadType { + match value { + WasmPayloadType::Disclosed => PayloadType::Disclosed, + WasmPayloadType::ProofMethods => PayloadType::ProofMethods, + WasmPayloadType::Undisclosed => PayloadType::Undisclosed, + } + } +} + +impl From for WasmPayloadType { + fn from(value: PayloadType) -> WasmPayloadType { + match value { + PayloadType::Disclosed => WasmPayloadType::Disclosed, + PayloadType::ProofMethods => WasmPayloadType::ProofMethods, + PayloadType::Undisclosed => WasmPayloadType::Undisclosed, + } + } +} + +#[wasm_bindgen] +pub struct WasmPayloadEntry(JsValue, pub WasmPayloadType); + +#[wasm_bindgen] +impl WasmPayloadEntry { + #[wasm_bindgen(setter)] + pub fn set_value(&mut self, value: JsValue) { + self.0 = value; + } + #[wasm_bindgen(getter)] + pub fn value(&self) -> JsValue { + self.0.clone() + } +} + +#[wasm_bindgen(js_name = Payloads, inspectable)] +pub struct WasmPayloads(pub(crate) Payloads); + +impl_wasm_json!(WasmPayloads, Payloads); +impl_wasm_clone!(WasmPayloads, Payloads); + +#[wasm_bindgen(js_class = Payloads)] +impl WasmPayloads { + #[wasm_bindgen(constructor)] + pub fn new(entries: Vec) -> Result { + entries + .into_iter() + .map(|WasmPayloadEntry(value, type_)| value.into_serde().wasm_result().map(|value| (value, type_.into()))) + .collect::>>() + .map(Payloads) + .map(WasmPayloads) + } + + #[wasm_bindgen(constructor)] + pub fn new_from_values(values: Vec) -> Result { + let values = values + .into_iter() + .map(|v| v.into_serde().wasm_result()) + .collect::>>()?; + + Ok(Payloads::new_from_values(values).into()) + } + + #[wasm_bindgen(js_name = "getValues")] + pub fn get_values(&self) -> Result> { + self + .0 + .get_values() + .into_iter() + .map(|value| JsValue::from_serde(&value).wasm_result()) + .collect() + } + + #[wasm_bindgen(js_name = "getUndisclosedIndexes")] + pub fn get_undisclosed_indexes(&self) -> Vec { + self.0.get_undisclosed_indexes() + } + + #[wasm_bindgen(js_name = "getDisclosedIndexes")] + pub fn get_disclosed_indexes(&self) -> Vec { + self.0.get_disclosed_indexes() + } + + #[wasm_bindgen(js_name = "getUndisclosedPayloads")] + pub fn get_undisclosed_payloads(&self) -> Result> { + self + .0 + .get_undisclosed_payloads() + .into_iter() + .map(|value| JsValue::from_serde(&value).wasm_result()) + .collect() + } + + #[wasm_bindgen(js_name = "getDisclosedPayloads")] + pub fn get_disclosed_payloads(&self) -> WasmPayloads { + self.0.get_disclosed_payloads().into() + } + + #[wasm_bindgen(js_name = "setUndisclosed")] + pub fn set_undisclosed(&mut self, index: usize) { + self.0.set_undisclosed(index) + } + + #[wasm_bindgen(js_name = "replacePayloadAtIndex")] + pub fn replace_payload_at_index(&mut self, index: usize, value: JsValue) -> Result { + let value = value.into_serde().wasm_result()?; + self + .0 + .replace_payload_at_index(index, value) + .map_err(|_| { + JsValue::from(WasmError::new( + Cow::Borrowed("Index out of bounds"), + Cow::Borrowed("The provided index exceeds the array's bounds"), + )) + }) + .and_then(|v| JsValue::from_serde(&v).wasm_result()) + } +} + +impl From for WasmPayloads { + fn from(value: Payloads) -> Self { + WasmPayloads(value) + } +} + +impl From for Payloads { + fn from(value: WasmPayloads) -> Self { + value.0 + } +} diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 208edca0d0..cf8344925a 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -24,6 +24,7 @@ pub mod did; pub mod error; pub mod iota; pub mod jose; +pub mod jpt; pub mod resolver; pub mod revocation; pub mod sd_jwt; diff --git a/identity_jose/src/jwk/jwk_ext.rs b/identity_jose/src/jwk/jwk_ext.rs index 7c6621a5d0..462b9a44a7 100644 --- a/identity_jose/src/jwk/jwk_ext.rs +++ b/identity_jose/src/jwk/jwk_ext.rs @@ -121,6 +121,7 @@ impl TryFrom for Jwk { let (kty, params) = match value.key_params { JwkAlgorithmParameters::OctetKeyPair(p) => (JwkType::Okp, JwkParams::Okp(JwkParamsOkp::from(p))), + _ => unreachable!(), }; Ok(Self { From c102864cc8c2cdbcf1c799f158b72044c1bdbdc1 Mon Sep 17 00:00:00 2001 From: wulfraem Date: Thu, 21 Mar 2024 10:42:32 +0100 Subject: [PATCH 07/33] Refactor `RevocationTimeframeStatus` to align with other setups (#1340) * refactor `RevocationTimeframeStatus` to other setups * fix smaller typos --- examples/1_advanced/10_zkp_revocation.rs | 9 +- identity_credential/Cargo.toml | 4 +- identity_credential/src/error.rs | 2 +- .../presentation/jwp_presentation_builder.rs | 10 +- .../revocation_timeframe_status.rs | 365 +++++++----------- .../jpt_credential_validator_utils.rs | 31 +- .../jwt_credential_validation/error.rs | 2 +- .../src/document/core_document.rs | 2 +- 8 files changed, 180 insertions(+), 245 deletions(-) diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs index ad669e63fd..6941f51a16 100644 --- a/examples/1_advanced/10_zkp_revocation.rs +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -188,8 +188,13 @@ async fn main() -> anyhow::Result<()> { let credential_index: u32 = 5; let start_validity_timeframe = Timestamp::now_utc(); - let status: Status = - RevocationTimeframeStatus::new(Some(start_validity_timeframe), duration, service_url, credential_index)?.into(); + let status: Status = RevocationTimeframeStatus::new( + Some(start_validity_timeframe), + duration, + service_url.into(), + credential_index, + )? + .into(); // Build credential using subject above and issuer. let credential: Credential = CredentialBuilder::default() diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 9a58d63683..c56a657199 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -27,7 +27,7 @@ reqwest = { version = "0.11", default-features = false, features = ["default-tls roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true } sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"], optional = true } serde.workspace = true -serde-aux = { version = "4.3.1", default-features = false, optional = true } +serde-aux = { version = "4.3.1", default-features = false } serde_json.workspace = true serde_repr = { version = "0.1", default-features = false, optional = true } strum.workspace = true @@ -53,7 +53,7 @@ default = ["revocation-bitmap", "validator", "credential", "presentation", "doma credential = [] presentation = ["credential"] revocation-bitmap = ["dep:flate2", "dep:roaring"] -status-list-2021 = ["revocation-bitmap", "dep:serde-aux"] +status-list-2021 = ["revocation-bitmap"] validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation"] domain-linkage = ["validator"] domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 551521cffd..cf4f9c5066 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -75,5 +75,5 @@ pub enum Error { /// Cause by an invalid attribute path #[error("Attribute Not found")] - SelectiveDiscosureError, + SelectiveDisclosureError, } diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs index 76abfc297a..cb78123578 100644 --- a/identity_credential/src/presentation/jwp_presentation_builder.rs +++ b/identity_credential/src/presentation/jwp_presentation_builder.rs @@ -22,8 +22,8 @@ pub struct SelectiveDisclosurePresentation { } impl SelectiveDisclosurePresentation { - /// Inizialize a presentation starting from an Issued JWP. - /// The properties `jti`, `nbf`, `issuanceDate`, `expirationDate` and `terminsOfUse` are concealed by default. + /// Initialize a presentation starting from an Issued JWP. + /// The properties `jti`, `nbf`, `issuanceDate`, `expirationDate` and `termsOfUse` are concealed by default. pub fn new(issued_jwp: &JwpIssued) -> Self { let mut jwp_builder = JwpPresentedBuilder::new(issued_jwp); @@ -70,7 +70,7 @@ impl SelectiveDisclosurePresentation { let _ = self .jwp_builder .set_undisclosed(&("vc.credentialSubject.".to_owned() + path)) - .map_err(|_| Error::SelectiveDiscosureError); + .map_err(|_| Error::SelectiveDisclosureError); Ok(()) } @@ -79,11 +79,11 @@ impl SelectiveDisclosurePresentation { let _ = self .jwp_builder .set_undisclosed(&("vc.evidence.".to_owned() + path)) - .map_err(|_| Error::SelectiveDiscosureError); + .map_err(|_| Error::SelectiveDisclosureError); Ok(()) } - /// Set Presenation Protected Header. + /// Set Presentation Protected Header. pub fn set_presentation_header(&mut self, ph: PresentationProtectedHeader) { self.jwp_builder.presentation_protected_header(ph); } diff --git a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs index 415568f706..d961746d55 100644 --- a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs +++ b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs @@ -1,4 +1,3 @@ -use crate::credential::try_index_to_u32; use crate::credential::Status; use crate::error::Error; use crate::error::Result; @@ -7,32 +6,63 @@ use identity_core::common::Object; use identity_core::common::Timestamp; use identity_core::common::Url; use identity_core::common::Value; -use identity_did::DIDUrl; +use serde::de::Visitor; use serde::Deserialize; use serde::Serialize; -use std::str::FromStr; + +fn deserialize_status_entry_type<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + struct ExactStrVisitor(&'static str); + impl<'a> Visitor<'a> for ExactStrVisitor { + type Value = &'static str; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "the exact string \"{}\"", self.0) + } + fn visit_str(self, str: &str) -> Result { + if str == self.0 { + Ok(self.0) + } else { + Err(E::custom(format!("not \"{}\"", self.0))) + } + } + } + + deserializer + .deserialize_str(ExactStrVisitor(RevocationTimeframeStatus::TYPE)) + .map(ToOwned::to_owned) +} /// Information used to determine the current status of a [`Credential`][crate::credential::Credential] -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(try_from = "Status", into = "Status")] -pub struct RevocationTimeframeStatus(Status); +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RevocationTimeframeStatus { + id: Url, + #[serde(rename = "type", deserialize_with = "deserialize_status_entry_type")] + type_: String, + start_validity_timeframe: Timestamp, + end_validity_timeframe: Timestamp, + #[serde( + deserialize_with = "serde_aux::prelude::deserialize_option_number_from_string", + skip_serializing_if = "Option::is_none" + )] + revocation_bitmap_index: Option, +} impl RevocationTimeframeStatus { /// startValidityTimeframe property name. pub const START_TIMEFRAME_PROPERTY: &'static str = "startValidityTimeframe"; /// endValidityTimeframe property name. pub const END_TIMEFRAME_PROPERTY: &'static str = "endValidityTimeframe"; - const INDEX_PROPERTY: &'static str = "revocationBitmapIndex"; - /// Type name of the revocation mechanism. pub const TYPE: &'static str = "RevocationTimeframe2024"; + /// index property name for [`Status`] conversion + const INDEX_PROPERTY: &'static str = "revocationBitmapIndex"; /// Creates a new `RevocationTimeframeStatus`. - pub fn new(start_validity: Option, duration: Duration, id: DIDUrl, index: u32) -> Result { - let mut object = Object::new(); - + pub fn new(start_validity: Option, duration: Duration, id: Url, index: u32) -> Result { let start_validity_timeframe = start_validity.unwrap_or(Timestamp::now_utc()); - let end_validity_timeframe = start_validity_timeframe .checked_add(duration) .ok_or(Error::InvalidStatus( @@ -40,158 +70,76 @@ impl RevocationTimeframeStatus { .to_owned(), ))?; - // id.set_query(Some(&format!("index={index}"))) - // .expect("the string should be non-empty and a valid URL query"); - - object.insert( - Self::START_TIMEFRAME_PROPERTY.to_owned(), - Value::String(start_validity_timeframe.to_rfc3339()), - ); - object.insert( - Self::END_TIMEFRAME_PROPERTY.to_owned(), - Value::String(end_validity_timeframe.to_rfc3339()), - ); - object.insert(Self::INDEX_PROPERTY.to_owned(), Value::String(index.to_string())); - - Ok(Self(Status::new_with_properties( - Url::from(id), - Self::TYPE.to_owned(), - object, - ))) + Ok(Self { + id, + type_: Self::TYPE.to_owned(), + start_validity_timeframe, + end_validity_timeframe, + revocation_bitmap_index: Some(index), + }) } /// Get startValidityTimeframe value. - pub fn start_validity_timeframe(&self) -> Result { - if let Some(Value::String(timeframe)) = self.0.properties.get(Self::START_TIMEFRAME_PROPERTY) { - Timestamp::from_str(timeframe) - .map_err(|_| Error::InvalidStatus(format!("property '{}' is not a string", Self::START_TIMEFRAME_PROPERTY))) - } else { - Err(Error::InvalidStatus(format!( - "property '{}' is not a string", - Self::START_TIMEFRAME_PROPERTY - ))) - } + pub fn start_validity_timeframe(&self) -> Timestamp { + self.start_validity_timeframe } /// Get endValidityTimeframe value. - pub fn end_validity_timeframe(&self) -> Result { - if let Some(Value::String(timeframe)) = self.0.properties.get(Self::END_TIMEFRAME_PROPERTY) { - Timestamp::from_str(timeframe) - .map_err(|_| Error::InvalidStatus(format!("property '{}' is not a string", Self::END_TIMEFRAME_PROPERTY))) - } else { - Err(Error::InvalidStatus(format!( - "property '{}' is not a string", - Self::END_TIMEFRAME_PROPERTY - ))) - } + pub fn end_validity_timeframe(&self) -> Timestamp { + self.end_validity_timeframe } - /// Returns the [`DIDUrl`] of the `RevocationBitmapStatus`, which should resolve + /// Returns the [`Url`] of the `RevocationBitmapStatus`, which should resolve /// to a `RevocationBitmap2022` service in a DID Document. - pub fn id(&self) -> Result { - DIDUrl::parse(self.0.id.as_str()) - .map_err(|err| Error::InvalidStatus(format!("invalid DID Url '{}': {:?}", self.0.id, err))) + pub fn id(&self) -> &Url { + &self.id } /// Returns the index of the credential in the issuer's revocation bitmap if it can be decoded. - pub fn index(&self) -> Result { - if let Some(Value::String(index)) = self.0.properties.get(Self::INDEX_PROPERTY) { - try_index_to_u32(index, Self::INDEX_PROPERTY) - } else { - Err(Error::InvalidStatus(format!( - "expected {} to be an unsigned 32-bit integer expressed as a string", - Self::INDEX_PROPERTY - ))) - } + pub fn index(&self) -> Option { + self.revocation_bitmap_index } } -impl TryFrom for RevocationTimeframeStatus { +impl TryFrom<&Status> for RevocationTimeframeStatus { type Error = Error; - - fn try_from(status: Status) -> Result { - if status.type_ != Self::TYPE { - return Err(Self::Error::InvalidStatus(format!( - "expected type '{}', got '{}'", - Self::TYPE, - status.type_ - ))); - } - - let start_revocation_timeframe: &Value = - if let Some(start_revocation_timeframe) = status.properties.get(Self::START_TIMEFRAME_PROPERTY) { - start_revocation_timeframe - } else { - return Err(Self::Error::InvalidStatus(format!( - "missing required property '{}'", - Self::START_TIMEFRAME_PROPERTY - ))); - }; - - if let Value::String(timeframe) = start_revocation_timeframe { - Timestamp::from_str(timeframe).map_err(|_| { - Self::Error::InvalidStatus(format!( - "property '{}' is not a valid Timestamp", - Self::START_TIMEFRAME_PROPERTY - )) - })? - } else { - return Err(Self::Error::InvalidStatus(format!( - "property '{}' is not a string", - Self::START_TIMEFRAME_PROPERTY - ))); - }; - - let end_revocation_timeframe: &Value = - if let Some(end_revocation_timeframe) = status.properties.get(Self::END_TIMEFRAME_PROPERTY) { - end_revocation_timeframe - } else { - return Err(Self::Error::InvalidStatus(format!( - "missing required property '{}'", - Self::END_TIMEFRAME_PROPERTY - ))); - }; - - if let Value::String(timeframe) = end_revocation_timeframe { - Timestamp::from_str(timeframe).map_err(|_| { - Self::Error::InvalidStatus(format!( - "property '{}' is not a valid Timestamp", - Self::END_TIMEFRAME_PROPERTY - )) - })? - } else { - return Err(Self::Error::InvalidStatus(format!( - "property '{}' is not a string", - Self::END_TIMEFRAME_PROPERTY - ))); - }; - - let revocation_bitmap_index: &Value = - if let Some(revocation_bitmap_index) = status.properties.get(Self::INDEX_PROPERTY) { - revocation_bitmap_index - } else { - return Err(Error::InvalidStatus(format!( - "missing required property '{}'", - Self::INDEX_PROPERTY - ))); - }; - - if let Value::String(index) = revocation_bitmap_index { - try_index_to_u32(index, Self::INDEX_PROPERTY)? - } else { - return Err(Error::InvalidStatus(format!( - "property '{}' is not a string", - Self::INDEX_PROPERTY - ))); - }; - - Ok(Self(status)) + fn try_from(status: &Status) -> Result { + // serialize into String to ensure macros work properly + // see [issue](https://github.com/iddm/serde-aux/issues/34#issuecomment-1508207530) in `serde-aux` + let json_status: String = serde_json::to_string(&status) + .map_err(|err| Self::Error::InvalidStatus(format!("failed to read `Status`; {}", &err.to_string())))?; + serde_json::from_str(&json_status).map_err(|err| { + Self::Error::InvalidStatus(format!( + "failed to convert `Status` to `RevocationTimeframeStatus`; {}", + &err.to_string(), + )) + }) } } impl From for Status { - fn from(status: RevocationTimeframeStatus) -> Self { - status.0 + fn from(revocation_timeframe_status: RevocationTimeframeStatus) -> Self { + let mut properties = Object::new(); + properties.insert( + RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY.to_owned(), + Value::String(revocation_timeframe_status.start_validity_timeframe().to_rfc3339()), + ); + properties.insert( + RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY.to_owned(), + Value::String(revocation_timeframe_status.end_validity_timeframe().to_rfc3339()), + ); + if let Some(value) = revocation_timeframe_status.index() { + properties.insert( + RevocationTimeframeStatus::INDEX_PROPERTY.to_owned(), + Value::String(value.to_string()), + ); + } + + Status::new_with_properties( + revocation_timeframe_status.id, + RevocationTimeframeStatus::TYPE.to_owned(), + properties, + ) } } @@ -203,88 +151,67 @@ impl TryFrom for VerifierRevocationTimeframeStatus { type Error = Error; fn try_from(status: Status) -> Result { - if status.type_ != RevocationTimeframeStatus::TYPE { - return Err(Self::Error::InvalidStatus(format!( - "expected type '{}', got '{}'", - RevocationTimeframeStatus::TYPE, - status.type_ - ))); - } + Ok(Self((&status).try_into().map_err(|err: Error| { + Self::Error::InvalidStatus(format!( + "failed to convert `Status` to `VerifierRevocationTimeframeStatus`; {}", + &err.to_string() + )) + })?)) + } +} - let start_revocation_timeframe: &Value = if let Some(start_revocation_timeframe) = status - .properties - .get(RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY) - { - start_revocation_timeframe - } else { - return Err(Self::Error::InvalidStatus(format!( - "missing required property '{}'", - RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY - ))); - }; +impl From for Status { + fn from(status: VerifierRevocationTimeframeStatus) -> Self { + status.0.into() + } +} - if let Value::String(timeframe) = start_revocation_timeframe { - Timestamp::from_str(timeframe).map_err(|_| { - Self::Error::InvalidStatus(format!( - "property '{}' is not a valid Timestamp", - RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY - )) - })? - } else { - return Err(Self::Error::InvalidStatus(format!( - "property '{}' is not a string", - RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY - ))); - }; +#[cfg(test)] +mod tests { + use super::*; + + const EXAMPLE_SERIALIZED: &str = r#"{ + "id": "did:iota:snd:0xae6ccfdb155a69e0ef153fb5fcfd50c08a8fee36babe1f7d71dede8f4e202432#my-revocation-service", + "startValidityTimeframe": "2024-03-19T13:57:50Z", + "endValidityTimeframe": "2024-03-19T13:58:50Z", + "revocationBitmapIndex": "5", + "type": "revocationBitmapIndex" + }"#; + + fn get_example_status() -> anyhow::Result { + let duration = Duration::minutes(1); + let service_url = Url::parse( + "did:iota:snd:0xae6ccfdb155a69e0ef153fb5fcfd50c08a8fee36babe1f7d71dede8f4e202432#my-revocation-service", + )?; + let credential_index: u32 = 5; + let start_validity_timeframe = Timestamp::parse("2024-03-19T13:57:50Z")?; + + Ok(RevocationTimeframeStatus::new( + Some(start_validity_timeframe), + duration, + service_url, + credential_index, + )?) + } - let end_revocation_timeframe: &Value = if let Some(end_revocation_timeframe) = - status.properties.get(RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY) - { - end_revocation_timeframe - } else { - return Err(Self::Error::InvalidStatus(format!( - "missing required property '{}'", - RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY - ))); - }; + #[test] + fn revocation_timeframe_status_serialization_works() -> anyhow::Result<()> { + let status = get_example_status()?; - if let Value::String(timeframe) = end_revocation_timeframe { - Timestamp::from_str(timeframe).map_err(|_| { - Self::Error::InvalidStatus(format!( - "property '{}' is not a valid Timestamp", - RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY - )) - })? - } else { - return Err(Self::Error::InvalidStatus(format!( - "property '{}' is not a string", - RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY - ))); - }; + let serialized = serde_json::to_string(&status).expect("Failed to deserialize"); + dbg!(&serialized); - let revocation_bitmap_index: &Value = - if let Some(revocation_bitmap_index) = status.properties.get(RevocationTimeframeStatus::INDEX_PROPERTY) { - revocation_bitmap_index - } else { - return Err(Error::InvalidStatus(format!( - "missing required property '{}'", - RevocationTimeframeStatus::INDEX_PROPERTY - ))); - }; + Ok(()) + } - if &Value::Null != revocation_bitmap_index { - return Err(Error::InvalidStatus(format!( - "property '{}' is not a Null", - RevocationTimeframeStatus::INDEX_PROPERTY - ))); - }; + #[test] + fn revocation_timeframe_status_deserialization_works() -> anyhow::Result<()> { + let status = get_example_status()?; + let deserialized = + serde_json::from_str::(EXAMPLE_SERIALIZED).expect("Failed to deserialize"); - Ok(Self(RevocationTimeframeStatus(status))) - } -} + assert_eq!(status, deserialized); -impl From for Status { - fn from(status: VerifierRevocationTimeframeStatus) -> Self { - status.0 .0 + Ok(()) } } diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs index eeba0004a6..e1f292f57a 100644 --- a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs @@ -93,7 +93,7 @@ impl JptCredentialValidatorUtils { Some(status) => { if status.type_ == RevocationTimeframeStatus::TYPE { let status: RevocationTimeframeStatus = - RevocationTimeframeStatus::try_from(status.clone()).map_err(JwtValidationError::InvalidStatus)?; + RevocationTimeframeStatus::try_from(status).map_err(JwtValidationError::InvalidStatus)?; Self::check_validity_timeframe(status, validity_timeframe) } else { @@ -115,11 +115,7 @@ impl JptCredentialValidatorUtils { ) -> ValidationUnitResult { let timeframe = validity_timeframe.unwrap_or(Timestamp::now_utc()); - let check = status.start_validity_timeframe().is_ok_and(|start| { - status - .end_validity_timeframe() - .is_ok_and(|end| timeframe >= start && timeframe <= end) - }); + let check = timeframe >= status.start_validity_timeframe() && timeframe <= status.end_validity_timeframe(); if !check { Err(JwtValidationError::OutsideTimeframe) @@ -148,7 +144,7 @@ impl JptCredentialValidatorUtils { Some(status) => { if status.type_ == RevocationTimeframeStatus::TYPE { let status: RevocationTimeframeStatus = - RevocationTimeframeStatus::try_from(status.clone()).map_err(JwtValidationError::InvalidStatus)?; + RevocationTimeframeStatus::try_from(status).map_err(JwtValidationError::InvalidStatus)?; Self::check_revocation_bitmap(issuer, status) } else { @@ -169,19 +165,26 @@ impl JptCredentialValidatorUtils { issuer: &DOC, status: RevocationTimeframeStatus, ) -> ValidationUnitResult { - let issuer_service_url: identity_did::DIDUrl = status.id().map_err(JwtValidationError::InvalidStatus)?; + let issuer_service_url: identity_did::DIDUrl = + identity_did::DIDUrl::parse(status.id().to_string()).map_err(|err| { + JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "could not convert status id to DIDUrl; {}", + err, + ))) + })?; // Check whether index is revoked. let revocation_bitmap: crate::revocation::RevocationBitmap = issuer .as_ref() .resolve_revocation_bitmap(issuer_service_url.into()) .map_err(|_| JwtValidationError::ServiceLookupError)?; - let index: u32 = status.index().map_err(JwtValidationError::InvalidStatus)?; - if revocation_bitmap.is_revoked(index) { - Err(JwtValidationError::Revoked) - } else { - Ok(()) + + if let Some(index) = status.index() { + if revocation_bitmap.is_revoked(index) { + return Err(JwtValidationError::Revoked); + } } + Ok(()) } /// Checks whether the credential status has been revoked or the timeframe interval is INVALID @@ -205,7 +208,7 @@ impl JptCredentialValidatorUtils { Some(status) => { if status.type_ == RevocationTimeframeStatus::TYPE { let status: RevocationTimeframeStatus = - RevocationTimeframeStatus::try_from(status.clone()).map_err(JwtValidationError::InvalidStatus)?; + RevocationTimeframeStatus::try_from(status).map_err(JwtValidationError::InvalidStatus)?; let revocation = std::iter::once_with(|| Self::check_revocation_bitmap(issuer, status.clone())); diff --git a/identity_credential/src/validator/jwt_credential_validation/error.rs b/identity_credential/src/validator/jwt_credential_validation/error.rs index dfe2b01f5d..3cc6a163fa 100644 --- a/identity_credential/src/validator/jwt_credential_validation/error.rs +++ b/identity_credential/src/validator/jwt_credential_validation/error.rs @@ -109,7 +109,7 @@ pub enum JwtValidationError { /// Indicates that the JWP representation of an issued credential or presentation could not be decoded. #[error("could not decode jwp")] JwpDecodingError(#[source] jsonprooftoken::errors::CustomError), - /// Indicates that the verfication of the JWP has failed + /// Indicates that the verification of the JWP has failed #[error("could not verify jwp")] JwpProofVerificationError(#[source] jsonprooftoken::errors::CustomError), diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 87fddd0fed..1b226f9585 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -770,7 +770,7 @@ impl CoreDocument { } /// Returns the first [`Service`] with an `id` property matching the provided `service_query`, if present. - // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains + // NOTE: This method demonstrates unexpected behavior in the edge cases where the document contains // services whose ids are of the form #. pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&Service> where From fd2070cf6e6d825d698db609c53b28b484c77c60 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Fri, 22 Mar 2024 15:54:12 +0100 Subject: [PATCH 08/33] binding coverage for jsonprooftoken --- .../jwp_credential_options.rs | 48 +++++++++++ .../jpt_credential_validator/mod.rs | 2 + bindings/wasm/src/did/wasm_core_document.rs | 3 + .../src/jpt/{jpt_issued.rs => jwp_issued.rs} | 0 .../wasm/src/jpt/jwp_presentation_builder.rs | 80 ++++++++++++++++++ bindings/wasm/src/jpt/mod.rs | 10 ++- .../src/jpt/presentation_protected_header.rs | 83 +++++++++++++++++++ bindings/wasm/src/jpt/proof_algorithm.rs | 49 +++++++++++ 8 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs rename bindings/wasm/src/jpt/{jpt_issued.rs => jwp_issued.rs} (100%) create mode 100644 bindings/wasm/src/jpt/jwp_presentation_builder.rs create mode 100644 bindings/wasm/src/jpt/presentation_protected_header.rs create mode 100644 bindings/wasm/src/jpt/proof_algorithm.rs diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs new file mode 100644 index 0000000000..fd37ad52aa --- /dev/null +++ b/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs @@ -0,0 +1,48 @@ +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::JwpCredentialOptions; +use serde::Deserialize; +use serde::Serialize; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(inspectable)] +#[derive(Serialize, Deserialize, Default)] +pub struct WasmJwpCredentialOptions { + #[wasm_bindgen(getter_with_clone)] + #[serde(skip_serializing_if = "Option::is_none")] + pub kid: Option, +} + +#[wasm_bindgen] +impl WasmJwpCredentialOptions { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmJwpCredentialOptions { + WasmJwpCredentialOptions::default() + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json(value: JsValue) -> Result { + value.into_serde().wasm_result() + } + + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> Result { + JsValue::from_serde(self).wasm_result() + } +} + +impl From for JwpCredentialOptions { + fn from(value: WasmJwpCredentialOptions) -> Self { + let WasmJwpCredentialOptions { kid } = value; + let mut jwp_options = JwpCredentialOptions::default(); + jwp_options.kid = kid; + + jwp_options + } +} + +impl From for WasmJwpCredentialOptions { + fn from(value: JwpCredentialOptions) -> Self { + WasmJwpCredentialOptions { kid: value.kid } + } +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/mod.rs b/bindings/wasm/src/credential/jpt_credential_validator/mod.rs index 963d7493ca..4b6ab97483 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/mod.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/mod.rs @@ -2,10 +2,12 @@ mod decoded_jpt_credential; mod jpt_credential_validation_options; mod jpt_credential_validator; mod jpt_credential_validator_utils; +mod jwp_credential_options; mod jwp_verification_options; pub use decoded_jpt_credential::*; pub use jpt_credential_validation_options::*; pub use jpt_credential_validator::*; pub use jpt_credential_validator_utils::*; +pub use jwp_credential_options::*; pub use jwp_verification_options::*; diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index 0bae16c048..0fe08e6675 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -835,6 +835,9 @@ extern "C" { #[wasm_bindgen(typescript_type = "Promise")] pub type PromiseJwt; + + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseJpt; } #[wasm_bindgen(typescript_custom_section)] diff --git a/bindings/wasm/src/jpt/jpt_issued.rs b/bindings/wasm/src/jpt/jwp_issued.rs similarity index 100% rename from bindings/wasm/src/jpt/jpt_issued.rs rename to bindings/wasm/src/jpt/jwp_issued.rs diff --git a/bindings/wasm/src/jpt/jwp_presentation_builder.rs b/bindings/wasm/src/jpt/jwp_presentation_builder.rs new file mode 100644 index 0000000000..23d15be410 --- /dev/null +++ b/bindings/wasm/src/jpt/jwp_presentation_builder.rs @@ -0,0 +1,80 @@ +use super::WasmJwpIssued; +use super::WasmPresentationProtectedHeader; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::SelectiveDisclosurePresentation; +use wasm_bindgen::prelude::*; + +/// Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes +/// - @context MUST NOT be blinded +/// - id MUST be blinded +/// - type MUST NOT be blinded +/// - issuer MUST NOT be blinded +/// - issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used) +/// - expirationDate MUST be blinded (if Timeframe Revocation mechanism is used) +/// - credentialSubject (User have to choose which attribute must be blinded) +/// - credentialSchema MUST NOT be blinded +/// - credentialStatus MUST NOT be blinded +/// - refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism) +/// - termsOfUse NO reason to use it in ZK VC (will be in any case blinded) +/// - evidence (User have to choose which attribute must be blinded) +#[wasm_bindgen(js_name = SelectiveDisclosurePresentation)] +pub struct WasmSelectiveDisclosurePresentation(pub(crate) SelectiveDisclosurePresentation); + +impl From for SelectiveDisclosurePresentation { + fn from(value: WasmSelectiveDisclosurePresentation) -> Self { + value.0 + } +} + +impl From for WasmSelectiveDisclosurePresentation { + fn from(value: SelectiveDisclosurePresentation) -> Self { + WasmSelectiveDisclosurePresentation(value) + } +} + +#[wasm_bindgen(js_class = SelectiveDisclosurePresentation)] +impl WasmSelectiveDisclosurePresentation { + /// Initialize a presentation starting from an Issued JWP. + /// The properties `jti`, `nbf`, `issuanceDate`, `expirationDate` and `termsOfUse` are concealed by default. + #[wasm_bindgen(constructor)] + pub fn new(issued_jwp: WasmJwpIssued) -> WasmSelectiveDisclosurePresentation { + SelectiveDisclosurePresentation::new(&issued_jwp.0).into() + } + + /// Selectively disclose "credentialSubject" attributes. + /// # Example + /// ``` + /// { + /// "id": 1234, + /// "name": "Alice", + /// "mainCourses": ["Object-oriented Programming", "Mathematics"], + /// "degree": { + /// "type": "BachelorDegree", + /// "name": "Bachelor of Science and Arts", + /// }, + /// "GPA": "4.0", + /// } + /// ``` + /// If you want to undisclose for example the Mathematics course and the name of the degree: + /// ``` + /// undisclose_subject("mainCourses[1]"); + /// undisclose_subject("degree.name"); + /// ``` + #[wasm_bindgen(js_name = concealInSubject)] + pub fn conceal_in_subject(&mut self, path: String) -> Result<()> { + self.0.conceal_in_subject(&path).wasm_result() + } + + /// Undiscloses "evidence" attributes. + #[wasm_bindgen(js_name = concealInEvidence)] + pub fn conceal_in_evidence(&mut self, path: String) -> Result<()> { + self.0.conceal_in_evidence(&path).wasm_result() + } + + /// Sets presentation protected header. + #[wasm_bindgen(js_name = setPresentationHeader)] + pub fn set_presentation_header(&mut self, header: WasmPresentationProtectedHeader) { + self.0.set_presentation_header(header.into()) + } +} diff --git a/bindings/wasm/src/jpt/mod.rs b/bindings/wasm/src/jpt/mod.rs index cbfa5d32a2..ed756e382a 100644 --- a/bindings/wasm/src/jpt/mod.rs +++ b/bindings/wasm/src/jpt/mod.rs @@ -1,7 +1,13 @@ mod encoding; -mod jpt_issued; +mod jwp_issued; +mod jwp_presentation_builder; mod payload; +mod presentation_protected_header; +mod proof_algorithm; pub use encoding::*; -pub use jpt_issued::*; +pub use jwp_issued::*; +pub use jwp_presentation_builder::*; pub use payload::*; +pub use presentation_protected_header::*; +pub use proof_algorithm::*; diff --git a/bindings/wasm/src/jpt/presentation_protected_header.rs b/bindings/wasm/src/jpt/presentation_protected_header.rs new file mode 100644 index 0000000000..a2e907c29b --- /dev/null +++ b/bindings/wasm/src/jpt/presentation_protected_header.rs @@ -0,0 +1,83 @@ +use jsonprooftoken::jpa::algs::PresentationProofAlgorithm; +use jsonprooftoken::jwp::header::PresentationProtectedHeader; +use wasm_bindgen::prelude::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[wasm_bindgen(js_name = PresentationProofAlgorithm)] +#[allow(non_camel_case_types)] +pub enum WasmPresentationProofAlgorithm { + BLS12381_SHA256_PROOF, + BLS12381_SHAKE256_PROOF, + SU_ES256, + MAC_H256, + MAC_H384, + MAC_H512, + MAC_K25519, + MAC_K448, + MAC_H256K, +} + +impl From for PresentationProofAlgorithm { + fn from(value: WasmPresentationProofAlgorithm) -> Self { + match value { + WasmPresentationProofAlgorithm::BLS12381_SHA256_PROOF => PresentationProofAlgorithm::BLS12381_SHA256_PROOF, + WasmPresentationProofAlgorithm::BLS12381_SHAKE256_PROOF => PresentationProofAlgorithm::BLS12381_SHAKE256_PROOF, + WasmPresentationProofAlgorithm::SU_ES256 => PresentationProofAlgorithm::SU_ES256, + WasmPresentationProofAlgorithm::MAC_H256 => PresentationProofAlgorithm::MAC_H256, + WasmPresentationProofAlgorithm::MAC_H384 => PresentationProofAlgorithm::MAC_H384, + WasmPresentationProofAlgorithm::MAC_H512 => PresentationProofAlgorithm::MAC_H512, + WasmPresentationProofAlgorithm::MAC_K25519 => PresentationProofAlgorithm::MAC_K25519, + WasmPresentationProofAlgorithm::MAC_K448 => PresentationProofAlgorithm::MAC_K448, + WasmPresentationProofAlgorithm::MAC_H256K => PresentationProofAlgorithm::MAC_H256K, + } + } +} + +impl From for WasmPresentationProofAlgorithm { + fn from(value: PresentationProofAlgorithm) -> Self { + match value { + PresentationProofAlgorithm::BLS12381_SHA256_PROOF => WasmPresentationProofAlgorithm::BLS12381_SHA256_PROOF, + PresentationProofAlgorithm::BLS12381_SHAKE256_PROOF => WasmPresentationProofAlgorithm::BLS12381_SHAKE256_PROOF, + PresentationProofAlgorithm::SU_ES256 => WasmPresentationProofAlgorithm::SU_ES256, + PresentationProofAlgorithm::MAC_H256 => WasmPresentationProofAlgorithm::MAC_H256, + PresentationProofAlgorithm::MAC_H384 => WasmPresentationProofAlgorithm::MAC_H384, + PresentationProofAlgorithm::MAC_H512 => WasmPresentationProofAlgorithm::MAC_H512, + PresentationProofAlgorithm::MAC_K25519 => WasmPresentationProofAlgorithm::MAC_K25519, + PresentationProofAlgorithm::MAC_K448 => WasmPresentationProofAlgorithm::MAC_K448, + PresentationProofAlgorithm::MAC_H256K => WasmPresentationProofAlgorithm::MAC_H256K, + } + } +} + +#[wasm_bindgen(js_name = PresentationProtectedHeader, inspectable, getter_with_clone)] +pub struct WasmPresentationProtectedHeader { + pub alg: WasmPresentationProofAlgorithm, + /// ID for the key used for the JWP. + pub kid: Option, + /// Who have received the JPT. + pub aud: Option, + /// For replay attacks. + pub nonce: Option, +} + +impl From for PresentationProtectedHeader { + fn from(value: WasmPresentationProtectedHeader) -> Self { + let WasmPresentationProtectedHeader { alg, kid, aud, nonce } = value; + let mut protected_header = PresentationProtectedHeader::new(alg.into()); + protected_header.set_kid(kid); + protected_header.set_aud(aud); + protected_header.set_nonce(nonce); + protected_header + } +} + +impl From for WasmPresentationProtectedHeader { + fn from(value: PresentationProtectedHeader) -> Self { + let alg = value.alg().into(); + let kid = value.kid().cloned(); + let aud = value.aud().cloned(); + let nonce = value.nonce().cloned(); + + WasmPresentationProtectedHeader { alg, kid, aud, nonce } + } +} diff --git a/bindings/wasm/src/jpt/proof_algorithm.rs b/bindings/wasm/src/jpt/proof_algorithm.rs new file mode 100644 index 0000000000..1eec61259d --- /dev/null +++ b/bindings/wasm/src/jpt/proof_algorithm.rs @@ -0,0 +1,49 @@ +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use wasm_bindgen::prelude::*; + +#[allow(non_camel_case_types)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[wasm_bindgen] +pub enum WasmProofAlgorithm { + BLS12381_SHA256, + BLS12381_SHAKE256, + SU_ES256, + MAC_H256, + MAC_H384, + MAC_H512, + MAC_K25519, + MAC_K448, + MAC_H256K, +} + +impl From for WasmProofAlgorithm { + fn from(value: ProofAlgorithm) -> Self { + match value { + ProofAlgorithm::BLS12381_SHA256 => WasmProofAlgorithm::BLS12381_SHA256, + ProofAlgorithm::BLS12381_SHAKE256 => WasmProofAlgorithm::BLS12381_SHAKE256, + ProofAlgorithm::SU_ES256 => WasmProofAlgorithm::SU_ES256, + ProofAlgorithm::MAC_H256 => WasmProofAlgorithm::MAC_H256, + ProofAlgorithm::MAC_H384 => WasmProofAlgorithm::MAC_H384, + ProofAlgorithm::MAC_H512 => WasmProofAlgorithm::MAC_H512, + ProofAlgorithm::MAC_K25519 => WasmProofAlgorithm::MAC_K25519, + ProofAlgorithm::MAC_K448 => WasmProofAlgorithm::MAC_K448, + ProofAlgorithm::MAC_H256K => WasmProofAlgorithm::MAC_H256K, + } + } +} + +impl From for ProofAlgorithm { + fn from(value: WasmProofAlgorithm) -> Self { + match value { + WasmProofAlgorithm::BLS12381_SHA256 => ProofAlgorithm::BLS12381_SHA256, + WasmProofAlgorithm::BLS12381_SHAKE256 => ProofAlgorithm::BLS12381_SHAKE256, + WasmProofAlgorithm::SU_ES256 => ProofAlgorithm::SU_ES256, + WasmProofAlgorithm::MAC_H256 => ProofAlgorithm::MAC_H256, + WasmProofAlgorithm::MAC_H384 => ProofAlgorithm::MAC_H384, + WasmProofAlgorithm::MAC_H512 => ProofAlgorithm::MAC_H512, + WasmProofAlgorithm::MAC_K25519 => ProofAlgorithm::MAC_K25519, + WasmProofAlgorithm::MAC_K448 => ProofAlgorithm::MAC_K448, + WasmProofAlgorithm::MAC_H256K => ProofAlgorithm::MAC_H256K, + } + } +} From 711d4aee32dbf31978584e7893180007340e9d3d Mon Sep 17 00:00:00 2001 From: Alberto Solavagione Date: Mon, 25 Mar 2024 10:33:10 +0100 Subject: [PATCH 09/33] Use latest releases of zkryptium/json-proof-token and add new BLS key representation (#1339) * update zkryptium/json-proof-token deps and new BLS key representation * minor fix --- Cargo.toml | 4 +- identity_jose/src/jwk/curve/bls.rs | 36 ++++ identity_jose/src/jwk/curve/mod.rs | 2 + identity_jose/src/jwk/jwk_ext.rs | 45 ++--- .../src/key_storage/jwk_storage.rs | 5 +- identity_storage/src/key_storage/memstore.rs | 154 +++++++----------- .../src/storage/timeframe_revocation_ext.rs | 19 ++- 7 files changed, 127 insertions(+), 138 deletions(-) create mode 100644 identity_jose/src/jwk/curve/bls.rs diff --git a/Cargo.toml b/Cargo.toml index 9dc0983411..e958044495 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,8 @@ serde = { version = "1.0", default-features = false, features = ["alloc", "deriv thiserror = { version = "1.0", default-features = false } strum = { version = "0.25", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0", default-features = false } -json-proof-token = { version = "0.3.3" } -zkryptium = { version = "0.1.9", default-features = false, features = ["bbsplus"] } +json-proof-token = { version = "0.3.4" } +zkryptium = { version = "0.2.0", default-features = false, features = ["bbsplus"] } [workspace.package] authors = ["IOTA Stiftung"] diff --git a/identity_jose/src/jwk/curve/bls.rs b/identity_jose/src/jwk/curve/bls.rs new file mode 100644 index 0000000000..a93e48b3e3 --- /dev/null +++ b/identity_jose/src/jwk/curve/bls.rs @@ -0,0 +1,36 @@ +use core::fmt::Display; +use core::fmt::Formatter; +use core::fmt::Result; + +/// Supported BLS Curves. +/// +/// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-bls-key-representations-05#name-curve-parameter-registratio) +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum BlsCurve { + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 12 with 381-bit p in the subgroup of G1. + BLS12381G1, + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 12 with 381-bit p in the subgroup of G2. + BLS12381G2, + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 48 with 581-bit p in the subgroup of G1. + BLS48581G1, + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 48 with 581-bit p in the subgroup of G2. + BLS48581G2, +} + +impl BlsCurve { + /// Returns the name of the curve as a string slice. + pub const fn name(self) -> &'static str { + match self { + Self::BLS12381G1 => "BLS12381G1", + Self::BLS12381G2 => "BLS12381G2", + Self::BLS48581G1 => "BLS48581G1", + Self::BLS48581G2 => "BLS48581G2", + } + } +} + +impl Display for BlsCurve { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_str(self.name()) + } +} \ No newline at end of file diff --git a/identity_jose/src/jwk/curve/mod.rs b/identity_jose/src/jwk/curve/mod.rs index 38a1e3bba7..d333ad879c 100644 --- a/identity_jose/src/jwk/curve/mod.rs +++ b/identity_jose/src/jwk/curve/mod.rs @@ -4,7 +4,9 @@ mod ec; mod ecx; mod ed; +mod bls; pub use self::ec::*; pub use self::ecx::*; pub use self::ed::*; +pub use self::bls::*; \ No newline at end of file diff --git a/identity_jose/src/jwk/jwk_ext.rs b/identity_jose/src/jwk/jwk_ext.rs index 462b9a44a7..4b2c3b72fe 100644 --- a/identity_jose/src/jwk/jwk_ext.rs +++ b/identity_jose/src/jwk/jwk_ext.rs @@ -1,14 +1,14 @@ use super::Jwk; use super::JwkOperation; use super::JwkParams; -use super::JwkParamsOkp; +use super::JwkParamsEc; use super::JwkType; use super::JwkUse; use identity_core::common::Url; use jsonprooftoken::jpa::algs::ProofAlgorithm; use jsonprooftoken::jwk::alg_parameters::Algorithm; use jsonprooftoken::jwk::alg_parameters::JwkAlgorithmParameters; -use jsonprooftoken::jwk::alg_parameters::JwkOctetKeyPairParameters; +use jsonprooftoken::jwk::alg_parameters::JwkEllipticCurveKeyParameters; use jsonprooftoken::jwk::curves::EllipticCurveTypes; use jsonprooftoken::jwk::key::Jwk as JwkExt; use jsonprooftoken::jwk::key::KeyOps; @@ -50,23 +50,6 @@ impl From for KeyOps { } } -// impl Into for JwkOperation { -// fn into(self) -> KeyOps { -// match self { -// Self::Sign => KeyOps::Sign, -// Self::Verify => KeyOps::Verify, -// Self::Encrypt => KeyOps::Encrypt, -// Self::Decrypt => KeyOps::Decrypt, -// Self::WrapKey => KeyOps::WrapKey, -// Self::UnwrapKey => KeyOps::UnwrapKey, -// Self::DeriveKey => KeyOps::DeriveKey, -// Self::DeriveBits => KeyOps::DeriveBits, -// Self::ProofGeneration => KeyOps::ProofGeneration, -// Self::ProofVerification => KeyOps::ProofVerification, -// } -// } -// } - impl From for JwkUse { fn from(value: PKUse) -> Self { match value { @@ -87,24 +70,26 @@ impl From for PKUse { } } -impl From for JwkParamsOkp { - fn from(value: JwkOctetKeyPairParameters) -> Self { +impl From for JwkParamsEc { + fn from(value: JwkEllipticCurveKeyParameters) -> Self { Self { crv: value.crv.to_string(), x: value.x, + y: value.y, d: value.d, } } } -impl TryInto for &JwkParamsOkp { +impl TryInto for &JwkParamsEc { type Error = crate::error::Error; - fn try_into(self) -> Result { - Ok(JwkOctetKeyPairParameters { - kty: KeyType::OctetKeyPair, - crv: EllipticCurveTypes::from_str(&self.crv).map_err(|_| Self::Error::KeyError("Invalid crv!"))?, + fn try_into(self) -> Result { + Ok(JwkEllipticCurveKeyParameters { + kty: KeyType::EllipticCurve, + crv: EllipticCurveTypes::from_str(&self.crv).map_err(|_| Self::Error::KeyError("crv not supported!"))?, x: self.x.clone(), + y: self.y.clone(), d: self.d.clone(), }) } @@ -120,8 +105,8 @@ impl TryFrom for Jwk { }; let (kty, params) = match value.key_params { - JwkAlgorithmParameters::OctetKeyPair(p) => (JwkType::Okp, JwkParams::Okp(JwkParamsOkp::from(p))), - _ => unreachable!(), + JwkAlgorithmParameters::EllipticCurve(p) => (JwkType::Ec, JwkParams::Ec(JwkParamsEc::from(p))), + _ => unreachable!() }; Ok(Self { @@ -146,7 +131,7 @@ impl TryInto for &Jwk { fn try_into(self) -> Result { let params = match &self.params { - JwkParams::Okp(p) => JwkAlgorithmParameters::OctetKeyPair(p.try_into()?), + JwkParams::Ec(p) => JwkAlgorithmParameters::EllipticCurve(p.try_into()?), _ => return Err(Self::Error::InvalidParam("Parameters not supported!")), }; @@ -171,4 +156,4 @@ impl TryInto for &Jwk { key_params: params, }) } -} +} \ No newline at end of file diff --git a/identity_storage/src/key_storage/jwk_storage.rs b/identity_storage/src/key_storage/jwk_storage.rs index e0e488be03..d74f51b077 100644 --- a/identity_storage/src/key_storage/jwk_storage.rs +++ b/identity_storage/src/key_storage/jwk_storage.rs @@ -11,6 +11,7 @@ use identity_verification::jose::jws::JwsAlgorithm; use jsonprooftoken::jpa::algs::ProofAlgorithm; use jsonprooftoken::jpt::claims::JptClaims; use jsonprooftoken::jwp::header::IssuerProtectedHeader; +use zkryptium::bbsplus::signature::BBSplusSignature; use super::jwk_gen_output::JwkGenOutput; @@ -88,7 +89,7 @@ pub trait JwkStorageExt: JwkStorage { &self, key_id: &KeyId, public_key: &Jwk, - proof: &[u8; 112], + proof: &[u8; BBSplusSignature::BYTES], ctx: ProofUpdateCtx, - ) -> KeyStorageResult<[u8; 112]>; + ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]>; } diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index 4758840da1..b0a8061666 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -12,11 +12,11 @@ use identity_verification::jose::jwk::EdCurve; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jwk::JwkType; use identity_verification::jose::jws::JwsAlgorithm; +use identity_verification::jwk::BlsCurve; use identity_verification::jwu; use jsonprooftoken::encoding::SerializationType; use jsonprooftoken::jpa::algs::ProofAlgorithm; use jsonprooftoken::jpt::claims::JptClaims; -use jsonprooftoken::jwk::curves::EllipticCurveTypes; use jsonprooftoken::jwk::key::Jwk as JwkExt; use jsonprooftoken::jwk::types::KeyPairSubtype; use jsonprooftoken::jwp::header::IssuerProtectedHeader; @@ -25,12 +25,11 @@ use rand::distributions::DistString; use shared::Shared; use tokio::sync::RwLockReadGuard; use tokio::sync::RwLockWriteGuard; -use zkryptium::bbsplus::keys::BBSplusPublicKey; use zkryptium::bbsplus::keys::BBSplusSecretKey; -use zkryptium::schemes::algorithms::BBS_BLS12381_SHA256; -use zkryptium::schemes::algorithms::BBS_BLS12381_SHAKE256; +use zkryptium::bbsplus::signature::BBSplusSignature; +use zkryptium::schemes::algorithms::BbsBls12381Sha256; +use zkryptium::schemes::algorithms::BbsBls12381Shake256; use zkryptium::schemes::generics::Signature; -use zkryptium::utils::message::BBSplusMessage; use super::ed25519::encode_jwk; use super::ed25519::expand_secret_jwk; @@ -339,16 +338,16 @@ impl JwkStorageExt for JwkMemStore { match alg { ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256 => { - let okp_params = public_key.try_okp_params().map_err(|err| { + let ec_params = public_key.try_ec_params().map_err(|err| { KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected a Jwk with Okp params in order to sign with {alg}")) + .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) .with_source(err) })?; - if okp_params.crv != EllipticCurveTypes::Bls12381G2.to_string() { + if ec_params.crv != BlsCurve::BLS12381G2.to_string() { return Err( KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "expected Jwk with Okp {} crv in order to generate the proof with {alg}", - EllipticCurveTypes::Bls12381G2 + "expected Jwk with EC {} crv in order to generate the proof with {alg}", + BlsCurve::BLS12381G2 )), ); } @@ -384,9 +383,9 @@ impl JwkStorageExt for JwkMemStore { &self, key_id: &KeyId, public_key: &Jwk, - proof: &[u8; 112], + proof: &[u8; BBSplusSignature::BYTES], ctx: ProofUpdateCtx, - ) -> KeyStorageResult<[u8; 112]> { + ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { let jwk_store: RwLockReadGuard<'_, JwkKeyStore> = self.jwk_store.read().await; let ProofUpdateCtx { @@ -409,16 +408,16 @@ impl JwkStorageExt for JwkMemStore { match alg { ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256 => { - let okp_params = public_key.try_okp_params().map_err(|err| { + let ec_params = public_key.try_ec_params().map_err(|err| { KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected a Jwk with Okp params in order to sign with {alg}")) + .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) .with_source(err) })?; - if okp_params.crv != EllipticCurveTypes::Bls12381G2.to_string() { + if ec_params.crv != BlsCurve::BLS12381G2.to_string() { return Err( KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "expected Jwk with Okp {} crv in order to generate the proof with {alg}", - EllipticCurveTypes::Bls12381G2 + "expected Jwk with EC {} crv in order to generate the proof with {alg}", + BlsCurve::BLS12381G2 )), ); } @@ -436,13 +435,7 @@ impl JwkStorageExt for JwkMemStore { .get(key_id) .ok_or_else(|| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound))?; - let params = jwk.try_okp_params().map_err(|_| KeyStorageErrorKind::Unspecified)?; - - let pk = BBSplusPublicKey::from_bytes(&jwu::decode_b64(¶ms.x).map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("unable to decode `d` param") - .with_source(err) - })?); + let params = jwk.try_ec_params().map_err(|_| KeyStorageErrorKind::Unspecified)?; let sk = BBSplusSecretKey::from_bytes( ¶ms @@ -458,94 +451,65 @@ impl JwkStorageExt for JwkMemStore { .with_custom_message("unable to decode `d` param") .with_source(err) })?, - ); + ).map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("key not valid") + })?; let new_proof = match alg { ProofAlgorithm::BLS12381_SHA256 => { - let vec_old_start_validity_timeframe = serde_json::to_vec(&old_start_validity_timeframe) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; - let old_start_validity_timeframe = - BBSplusMessage::map_message_to_scalar_as_hash::(&vec_old_start_validity_timeframe, None); - - let vec_new_start_validity_timeframe = serde_json::to_vec(&new_start_validity_timeframe) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; - let new_start_validity_timeframe = - BBSplusMessage::map_message_to_scalar_as_hash::(&vec_new_start_validity_timeframe, None); - - let vec_old_end_validity_timeframe = serde_json::to_vec(&old_end_validity_timeframe) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; - let old_end_validity_timeframe = - BBSplusMessage::map_message_to_scalar_as_hash::(&vec_old_end_validity_timeframe, None); - - let vec_new_end_validity_timeframe = serde_json::to_vec(&new_end_validity_timeframe) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; - let new_end_validity_timeframe = - BBSplusMessage::map_message_to_scalar_as_hash::(&vec_new_end_validity_timeframe, None); - - let proof = Signature::::from_bytes(proof).update_signature( + let proof = Signature::::from_bytes(proof) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid"))? + .update_signature( &sk, - &pk, - number_of_signed_messages, &old_start_validity_timeframe, &new_start_validity_timeframe, index_start_validity_timeframe, - ); + number_of_signed_messages, + ).map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature update failed") + })?; proof - .update_signature( - &sk, - &pk, - number_of_signed_messages, - &old_end_validity_timeframe, - &new_end_validity_timeframe, - index_end_validity_timeframe, - ) - .to_bytes() + .update_signature( + &sk, + &old_end_validity_timeframe, + &new_end_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + ).map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature update failed") + })? + .to_bytes() } ProofAlgorithm::BLS12381_SHAKE256 => { - let vec_old_start_validity_timeframe = serde_json::to_vec(&old_start_validity_timeframe) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; - let old_start_validity_timeframe = BBSplusMessage::map_message_to_scalar_as_hash::( - &vec_old_start_validity_timeframe, - None, - ); - - let vec_new_start_validity_timeframe = serde_json::to_vec(&new_start_validity_timeframe) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; - let new_start_validity_timeframe = BBSplusMessage::map_message_to_scalar_as_hash::( - &vec_new_start_validity_timeframe, - None, - ); - - let vec_old_end_validity_timeframe = serde_json::to_vec(&old_end_validity_timeframe) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; - let old_end_validity_timeframe = - BBSplusMessage::map_message_to_scalar_as_hash::(&vec_old_end_validity_timeframe, None); - - let vec_new_end_validity_timeframe = serde_json::to_vec(&new_end_validity_timeframe) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified))?; - let new_end_validity_timeframe = - BBSplusMessage::map_message_to_scalar_as_hash::(&vec_new_end_validity_timeframe, None); - - let proof = Signature::::from_bytes(proof).update_signature( + let proof = Signature::::from_bytes(proof) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid"))? + .update_signature( &sk, - &pk, - number_of_signed_messages, &old_start_validity_timeframe, &new_start_validity_timeframe, index_start_validity_timeframe, - ); + number_of_signed_messages, + ).map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature update failed") + })?; proof - .update_signature( - &sk, - &pk, - number_of_signed_messages, - &old_end_validity_timeframe, - &new_end_validity_timeframe, - index_end_validity_timeframe, - ) - .to_bytes() + .update_signature( + &sk, + &old_end_validity_timeframe, + &new_end_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + ).map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature update failed") + })? + .to_bytes() } other => { return Err( diff --git a/identity_storage/src/storage/timeframe_revocation_ext.rs b/identity_storage/src/storage/timeframe_revocation_ext.rs index 0d3ed740b7..d03b7d5d8d 100644 --- a/identity_storage/src/storage/timeframe_revocation_ext.rs +++ b/identity_storage/src/storage/timeframe_revocation_ext.rs @@ -16,17 +16,18 @@ use jsonprooftoken::encoding::SerializationType; use jsonprooftoken::jpt::payloads::Payloads; use jsonprooftoken::jwp::issued::JwpIssued; use serde_json::Value; +use zkryptium::bbsplus::signature::BBSplusSignature; /// Contains information needed to update the signature in the RevocationTimeframe2024 revocation mechanism. pub struct ProofUpdateCtx { /// Old `startValidityTimeframe` value - pub old_start_validity_timeframe: String, + pub old_start_validity_timeframe: Vec, /// New `startValidityTimeframe` value to be signed - pub new_start_validity_timeframe: String, + pub new_start_validity_timeframe: Vec, /// Old `endValidityTimeframe` value - pub old_end_validity_timeframe: String, + pub old_end_validity_timeframe: Vec, /// New `endValidityTimeframe` value to be signed - pub new_end_validity_timeframe: String, + pub new_end_validity_timeframe: Vec, /// Index of `startValidityTimeframe` claim inside the array of Claims pub index_start_validity_timeframe: usize, /// Index of `endValidityTimeframe` claim inside the array of Claims @@ -132,15 +133,15 @@ impl TimeframeRevocationExtension for CoreDocument { .map_err(|_| Error::ProofUpdateError("'endValidityTimeframe' value NOT found".to_owned()))? .map_err(|_| Error::ProofUpdateError("'endValidityTimeframe' value NOT a JSON String".to_owned()))?; - let proof: [u8; 112] = proof + let proof: [u8; BBSplusSignature::BYTES] = proof .try_into() .map_err(|_| Error::ProofUpdateError("Invalid bytes length of JWP proof".to_owned()))?; let proof_update_ctx = ProofUpdateCtx { - old_start_validity_timeframe, - new_start_validity_timeframe, - old_end_validity_timeframe, - new_end_validity_timeframe, + old_start_validity_timeframe: serde_json::to_vec(&old_start_validity_timeframe).unwrap(), + new_start_validity_timeframe: serde_json::to_vec(&new_start_validity_timeframe).unwrap(), + old_end_validity_timeframe: serde_json::to_vec(&old_end_validity_timeframe).unwrap(), + new_end_validity_timeframe: serde_json::to_vec(&new_end_validity_timeframe).unwrap(), index_start_validity_timeframe, index_end_validity_timeframe, number_of_signed_messages: payloads.0.len(), From be35e7b7843c24567e26f1506e7b592a75ebd8a6 Mon Sep 17 00:00:00 2001 From: Alberto Solavagione Date: Wed, 17 Apr 2024 12:00:19 +0200 Subject: [PATCH 10/33] Use zkryptium for cryptographic operations inside Memstore (#1351) * update zkryptium/json-proof-token deps and new BLS key representation * minor fix * use zkryptium for crypto operations and JPT for serialization * fix format --- Cargo.toml | 4 +- examples/1_advanced/10_zkp_revocation.rs | 2 +- examples/1_advanced/9_zkp.rs | 2 +- .../presentation/jwp_presentation_builder.rs | 2 +- identity_jose/src/jwk/curve/bls.rs | 14 +- identity_jose/src/jwk/curve/mod.rs | 4 +- identity_jose/src/jwk/jwk_ext.rs | 4 +- identity_jose/src/jwk/key_params.rs | 13 + identity_storage/src/key_storage/bls.rs | 87 ++++++ .../src/key_storage/jwk_storage.rs | 19 +- identity_storage/src/key_storage/memstore.rs | 248 +++++++++++------- identity_storage/src/key_storage/mod.rs | 2 + .../src/storage/jwp_document_ext.rs | 36 ++- .../src/storage/timeframe_revocation_ext.rs | 7 +- 14 files changed, 316 insertions(+), 128 deletions(-) create mode 100644 identity_storage/src/key_storage/bls.rs diff --git a/Cargo.toml b/Cargo.toml index e958044495..6a6fb06ba6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,8 @@ serde = { version = "1.0", default-features = false, features = ["alloc", "deriv thiserror = { version = "1.0", default-features = false } strum = { version = "0.25", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0", default-features = false } -json-proof-token = { version = "0.3.4" } -zkryptium = { version = "0.2.0", default-features = false, features = ["bbsplus"] } +json-proof-token = { version = "0.3.5" } +zkryptium = { version = "0.2.1", default-features = false, features = ["bbsplus"] } [workspace.package] authors = ["IOTA Stiftung"] diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs index 6941f51a16..db30d71d71 100644 --- a/examples/1_advanced/10_zkp_revocation.rs +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -151,7 +151,7 @@ async fn main() -> anyhow::Result<()> { &client, &secret_manager_issuer, &storage_issuer, - JwkMemStore::BLS12381SHA256_KEY_TYPE, + JwkMemStore::BLS12381G2_KEY_TYPE, None, Some(ProofAlgorithm::BLS12381_SHA256), ) diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs index bca63edd94..744395c3cc 100644 --- a/examples/1_advanced/9_zkp.rs +++ b/examples/1_advanced/9_zkp.rs @@ -99,7 +99,7 @@ async fn main() -> anyhow::Result<()> { &client, &secret_manager_issuer, &storage_issuer, - JwkMemStore::BLS12381SHA256_KEY_TYPE, + JwkMemStore::BLS12381G2_KEY_TYPE, ProofAlgorithm::BLS12381_SHA256, ) .await?; diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs index cb78123578..c1ad0c45a4 100644 --- a/identity_credential/src/presentation/jwp_presentation_builder.rs +++ b/identity_credential/src/presentation/jwp_presentation_builder.rs @@ -85,7 +85,7 @@ impl SelectiveDisclosurePresentation { /// Set Presentation Protected Header. pub fn set_presentation_header(&mut self, ph: PresentationProtectedHeader) { - self.jwp_builder.presentation_protected_header(ph); + self.jwp_builder.set_presentation_protected_header(ph); } /// Get the builder. diff --git a/identity_jose/src/jwk/curve/bls.rs b/identity_jose/src/jwk/curve/bls.rs index a93e48b3e3..aa241bee21 100644 --- a/identity_jose/src/jwk/curve/bls.rs +++ b/identity_jose/src/jwk/curve/bls.rs @@ -7,13 +7,17 @@ use core::fmt::Result; /// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-bls-key-representations-05#name-curve-parameter-registratio) #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub enum BlsCurve { - /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 12 with 381-bit p in the subgroup of G1. + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 12 with 381-bit p in the + /// subgroup of G1. BLS12381G1, - /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 12 with 381-bit p in the subgroup of G2. + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 12 with 381-bit p in the + /// subgroup of G2. BLS12381G2, - /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 48 with 581-bit p in the subgroup of G1. + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 48 with 581-bit p in the + /// subgroup of G1. BLS48581G1, - /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 48 with 581-bit p in the subgroup of G2. + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 48 with 581-bit p in the + /// subgroup of G2. BLS48581G2, } @@ -33,4 +37,4 @@ impl Display for BlsCurve { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.write_str(self.name()) } -} \ No newline at end of file +} diff --git a/identity_jose/src/jwk/curve/mod.rs b/identity_jose/src/jwk/curve/mod.rs index d333ad879c..8e1627219f 100644 --- a/identity_jose/src/jwk/curve/mod.rs +++ b/identity_jose/src/jwk/curve/mod.rs @@ -1,12 +1,12 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod bls; mod ec; mod ecx; mod ed; -mod bls; +pub use self::bls::*; pub use self::ec::*; pub use self::ecx::*; pub use self::ed::*; -pub use self::bls::*; \ No newline at end of file diff --git a/identity_jose/src/jwk/jwk_ext.rs b/identity_jose/src/jwk/jwk_ext.rs index 4b2c3b72fe..97f1d8a6f1 100644 --- a/identity_jose/src/jwk/jwk_ext.rs +++ b/identity_jose/src/jwk/jwk_ext.rs @@ -106,7 +106,7 @@ impl TryFrom for Jwk { let (kty, params) = match value.key_params { JwkAlgorithmParameters::EllipticCurve(p) => (JwkType::Ec, JwkParams::Ec(JwkParamsEc::from(p))), - _ => unreachable!() + _ => unreachable!(), }; Ok(Self { @@ -156,4 +156,4 @@ impl TryInto for &Jwk { key_params: params, }) } -} \ No newline at end of file +} diff --git a/identity_jose/src/jwk/key_params.rs b/identity_jose/src/jwk/key_params.rs index f60d6d5e66..ba3ca23059 100644 --- a/identity_jose/src/jwk/key_params.rs +++ b/identity_jose/src/jwk/key_params.rs @@ -10,6 +10,8 @@ use crate::jwk::EcxCurve; use crate::jwk::EdCurve; use crate::jwk::JwkType; +use super::BlsCurve; + /// Algorithm-specific parameters for JSON Web Keys. /// /// [More Info](https://tools.ietf.org/html/rfc7518#section-6) @@ -149,6 +151,17 @@ impl JwkParamsEc { _ => Err(Error::KeyError("Ec Curve")), } } + + /// Returns the [`BlsCurve`] if it is of a supported type. + pub fn try_bls_curve(&self) -> Result { + match &*self.crv { + "BLS12381G1" => Ok(BlsCurve::BLS12381G1), + "BLS12381G2" => Ok(BlsCurve::BLS12381G2), + "BLS48581G1" => Ok(BlsCurve::BLS48581G1), + "BLS48581G2" => Ok(BlsCurve::BLS48581G2), + _ => Err(Error::KeyError("BLS Curve")), + } + } } impl From for JwkParams { diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs new file mode 100644 index 0000000000..11d27f475d --- /dev/null +++ b/identity_storage/src/key_storage/bls.rs @@ -0,0 +1,87 @@ +use identity_verification::jose::jwk::Jwk; +use identity_verification::jose::jwu; +use identity_verification::jwk::BlsCurve; +use identity_verification::jwk::JwkParamsEc; +use zkryptium::bbsplus::keys::BBSplusPublicKey; +use zkryptium::bbsplus::keys::BBSplusSecretKey; + +use crate::key_storage::KeyStorageError; +use crate::key_storage::KeyStorageErrorKind; +use crate::key_storage::KeyStorageResult; + +pub(crate) fn expand_bls_jwk(jwk: &Jwk) -> KeyStorageResult<(BBSplusSecretKey, BBSplusPublicKey)> { + let params: &JwkParamsEc = jwk.try_ec_params().unwrap(); + + if params + .try_bls_curve() + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(err))? + != BlsCurve::BLS12381G2 + { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("expected an {} key", BlsCurve::BLS12381G2.name())), + ); + } + + let sk: BBSplusSecretKey = params + .d + .as_deref() + .map(jwu::decode_b64) + .ok_or_else(|| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("expected Jwk `d` param to be present") + })? + .map(|v| BBSplusSecretKey::from_bytes(&v)) + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `d` param") + .with_source(err) + })? + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!("invalid BBS+ secret key")) + })?; + + let x: [u8; BBSplusPublicKey::COORDINATE_LEN] = jwu::decode_b64(¶ms.x) + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `x` param") + .with_source(err) + })? + .try_into() + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected key of length {}", BBSplusPublicKey::COORDINATE_LEN)) + })?; + + let y: [u8; BBSplusPublicKey::COORDINATE_LEN] = jwu::decode_b64(¶ms.y) + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `y` param") + .with_source(err) + })? + .try_into() + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected key of length {}", BBSplusPublicKey::COORDINATE_LEN)) + })?; + + let pk = BBSplusPublicKey::from_coordinates(&x, &y).map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!("invalid BBS+ public key")) + })?; + + Ok((sk, pk)) +} + +#[cfg(any(test, feature = "memstore"))] +pub(crate) fn encode_bls_jwk(private_key: &BBSplusSecretKey, public_key: &BBSplusPublicKey) -> Jwk { + let (x, y) = public_key.to_coordinates(); + let x = jwu::encode_b64(x); + let y = jwu::encode_b64(y); + + let d = jwu::encode_b64(private_key.to_bytes()); + let mut params = JwkParamsEc::new(); + params.x = x; + params.y = y; + params.d = Some(d); + params.crv = BlsCurve::BLS12381G2.name().to_owned(); + Jwk::from_params(params) +} diff --git a/identity_storage/src/key_storage/jwk_storage.rs b/identity_storage/src/key_storage/jwk_storage.rs index d74f51b077..1e0fa4055a 100644 --- a/identity_storage/src/key_storage/jwk_storage.rs +++ b/identity_storage/src/key_storage/jwk_storage.rs @@ -9,8 +9,6 @@ use async_trait::async_trait; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jws::JwsAlgorithm; use jsonprooftoken::jpa::algs::ProofAlgorithm; -use jsonprooftoken::jpt::claims::JptClaims; -use jsonprooftoken::jwp::header::IssuerProtectedHeader; use zkryptium::bbsplus::signature::BBSplusSignature; use super::jwk_gen_output::JwkGenOutput; @@ -73,23 +71,24 @@ pub trait JwkStorage: storage_sub_trait::StorageSendSyncMaybe { #[cfg_attr(feature = "send-sync-storage", async_trait)] pub trait JwkStorageExt: JwkStorage { /// Generates a JWK representing a BBS+ signature - async fn generate_bbs_key(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult; + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult; - /// Generate the JPT representing a JWP in the Issuer form - async fn generate_issuer_proof( + /// Sign the provided `data` and `header` using the private key identified by `key_id` according to the requirements + /// of the corresponding `public_key` (see [`Jwk::alg`](Jwk::alg()) etc.). + async fn sign_bbs( &self, key_id: &KeyId, - header: IssuerProtectedHeader, - claims: JptClaims, + data: &[Vec], + header: &[u8], public_key: &Jwk, - ) -> KeyStorageResult; + ) -> KeyStorageResult>; /// Update proof functionality for timeframe revocation mechanism - async fn update_proof( + async fn update_signature( &self, key_id: &KeyId, public_key: &Jwk, - proof: &[u8; BBSplusSignature::BYTES], + signature: &[u8; BBSplusSignature::BYTES], ctx: ProofUpdateCtx, ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]>; } diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index b0a8061666..d264144a9a 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -14,23 +14,20 @@ use identity_verification::jose::jwk::JwkType; use identity_verification::jose::jws::JwsAlgorithm; use identity_verification::jwk::BlsCurve; use identity_verification::jwu; -use jsonprooftoken::encoding::SerializationType; use jsonprooftoken::jpa::algs::ProofAlgorithm; -use jsonprooftoken::jpt::claims::JptClaims; -use jsonprooftoken::jwk::key::Jwk as JwkExt; -use jsonprooftoken::jwk::types::KeyPairSubtype; -use jsonprooftoken::jwp::header::IssuerProtectedHeader; -use jsonprooftoken::jwp::issued::JwpIssuedBuilder; use rand::distributions::DistString; use shared::Shared; use tokio::sync::RwLockReadGuard; use tokio::sync::RwLockWriteGuard; use zkryptium::bbsplus::keys::BBSplusSecretKey; use zkryptium::bbsplus::signature::BBSplusSignature; +use zkryptium::keys::pair::KeyPair; use zkryptium::schemes::algorithms::BbsBls12381Sha256; use zkryptium::schemes::algorithms::BbsBls12381Shake256; use zkryptium::schemes::generics::Signature; +use super::bls::encode_bls_jwk; +use super::bls::expand_bls_jwk; use super::ed25519::encode_jwk; use super::ed25519::expand_secret_jwk; use super::jwk_gen_output::JwkGenOutput; @@ -82,6 +79,12 @@ impl JwkStorage for JwkMemStore { let public_key = private_key.public_key(); (private_key, public_key) } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{other} is not supported")), + ); + } }; let kid: KeyId = random_key_id(); @@ -199,6 +202,7 @@ impl JwkStorage for JwkMemStore { #[derive(Debug, Copy, Clone)] enum MemStoreKeyType { Ed25519, + BLS12381G2, } impl JwkMemStore { @@ -206,19 +210,16 @@ impl JwkMemStore { /// The Ed25519 key type. pub const ED25519_KEY_TYPE: KeyType = KeyType::from_static_str(Self::ED25519_KEY_TYPE_STR); - const BLS12381SHA256_KEY_TYPE_STR: &'static str = "Bls12381Sha256"; - /// The BLS12381-SHA256 key type - pub const BLS12381SHA256_KEY_TYPE: KeyType = KeyType::from_static_str(Self::BLS12381SHA256_KEY_TYPE_STR); - - const BLS12381SHAKE256_KEY_TYPE_STR: &'static str = "Bls12381Shake256"; - /// The BLS12381-SHAKE256 key type - pub const BLS12381SHAKE256_KEY_TYPE: KeyType = KeyType::from_static_str(Self::BLS12381SHAKE256_KEY_TYPE_STR); + const BLS12381G2_KEY_TYPE_STR: &'static str = "BLS12381G2"; + /// The BLS12381G2 key type + pub const BLS12381G2_KEY_TYPE: KeyType = KeyType::from_static_str(Self::BLS12381G2_KEY_TYPE_STR); } impl MemStoreKeyType { const fn name(&self) -> &'static str { match self { MemStoreKeyType::Ed25519 => "Ed25519", + MemStoreKeyType::BLS12381G2 => "BLS12381G2", } } } @@ -235,6 +236,7 @@ impl TryFrom<&KeyType> for MemStoreKeyType { fn try_from(value: &KeyType) -> Result { match value.as_str() { JwkMemStore::ED25519_KEY_TYPE_STR => Ok(MemStoreKeyType::Ed25519), + JwkMemStore::BLS12381G2_KEY_TYPE_STR => Ok(MemStoreKeyType::BLS12381G2), _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), } } @@ -263,6 +265,24 @@ impl TryFrom<&Jwk> for MemStoreKeyType { ), } } + JwkType::Ec => { + let ec_params = jwk.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .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(MemStoreKeyType::BLS12381G2), + curve => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{curve} not supported")), + ), + } + } other => Err( KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) .with_custom_message(format!("Jwk `kty` {other} not supported")), @@ -293,22 +313,61 @@ fn check_key_alg_compatibility(key_type: MemStoreKeyType, alg: JwsAlgorithm) -> } } +/// Check that the key type can be used with the algorithm. +fn check_key_proof_alg_compatibility(key_type: MemStoreKeyType, alg: ProofAlgorithm) -> KeyStorageResult<()> { + match (key_type, alg) { + (MemStoreKeyType::BLS12381G2, ProofAlgorithm::BLS12381_SHA256) => Ok(()), + (MemStoreKeyType::BLS12381G2, ProofAlgorithm::BLS12381_SHAKE256) => Ok(()), + (key_type, alg) => Err( + KeyStorageError::new(crate::key_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) + .with_custom_message(format!("`cannot use key type `{key_type}` with algorithm `{alg}`")), + ), + } +} + /// JwkStorageExt implementation for JwkMemStore #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] impl JwkStorageExt for JwkMemStore { - async fn generate_bbs_key(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { - let keysubtype = - KeyPairSubtype::from_str(key_type.as_str()).map_err(|_| KeyStorageErrorKind::UnsupportedKeyType)?; + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + let key_type: MemStoreKeyType = MemStoreKeyType::try_from(&key_type)?; - let mut jwk = Jwk::try_from( - JwkExt::generate(keysubtype) - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::RetryableIOFailure).with_source(err))?, - ) - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::RetryableIOFailure).with_source(err))?; + check_key_proof_alg_compatibility(key_type, alg)?; + + let (private_key, public_key) = match key_type { + MemStoreKeyType::BLS12381G2 => match alg { + ProofAlgorithm::BLS12381_SHA256 => { + let keypair = KeyPair::::random() + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; + let sk = keypair.private_key().clone(); + let pk = keypair.public_key().clone(); + (sk, pk) + } + ProofAlgorithm::BLS12381_SHAKE256 => { + let keypair = KeyPair::::random() + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; + let sk = keypair.private_key().clone(); + let pk = keypair.public_key().clone(); + (sk, pk) + } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{other} is not supported")), + ); + } + }, + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{other} is not supported")), + ); + } + }; let kid: KeyId = random_key_id(); + let mut jwk: Jwk = encode_bls_jwk(&private_key, &public_key); jwk.set_alg(alg.to_string()); jwk.set_kid(jwk.thumbprint_sha256_b64()); let public_jwk: Jwk = jwk.to_public().expect("should only panic if kty == oct"); @@ -319,13 +378,13 @@ impl JwkStorageExt for JwkMemStore { Ok(JwkGenOutput::new(kid, public_jwk)) } - async fn generate_issuer_proof( + async fn sign_bbs( &self, key_id: &KeyId, - header: IssuerProtectedHeader, - claims: JptClaims, + data: &[Vec], + header: &[u8], public_key: &Jwk, - ) -> KeyStorageResult { + ) -> KeyStorageResult> { let jwk_store: RwLockReadGuard<'_, JwkKeyStore> = self.jwk_store.read().await; // Extract the required alg from the given public key @@ -365,25 +424,34 @@ impl JwkStorageExt for JwkMemStore { .get(key_id) .ok_or_else(|| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound))?; - // Deserialize JSON to JwkExt - let jwk_ext: JwkExt = jwk.try_into().map_err(|_| KeyStorageErrorKind::SerializationError)?; + let (sk, pk) = expand_bls_jwk(jwk)?; - let jwp = JwpIssuedBuilder::new() - .issuer_protected_header(header) - .jpt_claims(claims) - .build(&jwk_ext) - .map_err(|_| KeyStorageErrorKind::Unspecified)? - .encode(SerializationType::COMPACT) - .map_err(|_| KeyStorageErrorKind::Unspecified)?; + let signature = match alg { + ProofAlgorithm::BLS12381_SHA256 => { + Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) + } + ProofAlgorithm::BLS12381_SHAKE256 => { + Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) + } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{other} is not supported")), + ); + } + } + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!("signature failed")) + })?; - Ok(jwp) + Ok(signature.to_vec()) } - async fn update_proof( + async fn update_signature( &self, key_id: &KeyId, public_key: &Jwk, - proof: &[u8; BBSplusSignature::BYTES], + signature: &[u8; BBSplusSignature::BYTES], ctx: ProofUpdateCtx, ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { let jwk_store: RwLockReadGuard<'_, JwkKeyStore> = self.jwk_store.read().await; @@ -451,65 +519,67 @@ impl JwkStorageExt for JwkMemStore { .with_custom_message("unable to decode `d` param") .with_source(err) })?, - ).map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("key not valid") - })?; + ) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("key not valid"))?; let new_proof = match alg { ProofAlgorithm::BLS12381_SHA256 => { - let proof = Signature::::from_bytes(proof) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid"))? - .update_signature( - &sk, - &old_start_validity_timeframe, - &new_start_validity_timeframe, - index_start_validity_timeframe, - number_of_signed_messages, - ).map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("Signature update failed") - })?; - - proof - .update_signature( - &sk, - &old_end_validity_timeframe, - &new_end_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - ).map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("Signature update failed") - })? - .to_bytes() + let signature = Signature::::from_bytes(signature) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid") + })? + .update_signature( + &sk, + &old_start_validity_timeframe, + &new_start_validity_timeframe, + index_start_validity_timeframe, + number_of_signed_messages, + ) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + })?; + + signature + .update_signature( + &sk, + &old_end_validity_timeframe, + &new_end_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + ) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + })? + .to_bytes() } ProofAlgorithm::BLS12381_SHAKE256 => { - let proof = Signature::::from_bytes(proof) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid"))? - .update_signature( - &sk, - &old_start_validity_timeframe, - &new_start_validity_timeframe, - index_start_validity_timeframe, - number_of_signed_messages, - ).map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("Signature update failed") - })?; + let proof = Signature::::from_bytes(signature) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid") + })? + .update_signature( + &sk, + &old_start_validity_timeframe, + &new_start_validity_timeframe, + index_start_validity_timeframe, + number_of_signed_messages, + ) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + })?; proof - .update_signature( - &sk, - &old_end_validity_timeframe, - &new_end_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - ).map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("Signature update failed") - })? - .to_bytes() + .update_signature( + &sk, + &old_end_validity_timeframe, + &new_end_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + ) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + })? + .to_bytes() } other => { return Err( diff --git a/identity_storage/src/key_storage/mod.rs b/identity_storage/src/key_storage/mod.rs index 70006b0537..5daf049bbc 100644 --- a/identity_storage/src/key_storage/mod.rs +++ b/identity_storage/src/key_storage/mod.rs @@ -6,6 +6,8 @@ //! This module provides the [`JwkStorage`] trait that //! abstracts over storages that store JSON Web Keys. +#[cfg(feature = "memstore")] +mod bls; #[cfg(feature = "memstore")] mod ed25519; mod jwk_gen_output; diff --git a/identity_storage/src/storage/jwp_document_ext.rs b/identity_storage/src/storage/jwp_document_ext.rs index b99f2f23a2..5da1608af5 100644 --- a/identity_storage/src/storage/jwp_document_ext.rs +++ b/identity_storage/src/storage/jwp_document_ext.rs @@ -9,6 +9,7 @@ use crate::Storage; use crate::StorageResult; use async_trait::async_trait; use identity_core::common::Object; +use identity_core::convert::ToJson; use identity_credential::credential::Credential; use identity_credential::credential::Jpt; use identity_credential::credential::JwpCredentialOptions; @@ -25,6 +26,7 @@ use jsonprooftoken::jpt::claims::JptClaims; use jsonprooftoken::jwk::key::Jwk; use jsonprooftoken::jwp::header::IssuerProtectedHeader; use jsonprooftoken::jwp::header::PresentationProtectedHeader; +use jsonprooftoken::jwp::issued::JwpIssuedBuilder; use serde::de::DeserializeOwned; use serde::Serialize; @@ -99,7 +101,7 @@ generate_method_for_document_type!( CoreDocument, ProofAlgorithm, JwkStorageExt, - JwkStorageExt::generate_bbs_key, + JwkStorageExt::generate_bbs, generate_method_core_document ); @@ -163,17 +165,27 @@ impl JwpDocumentExt for CoreDocument { .await .map_err(Error::KeyIdStorageError)?; - let jwp = ::generate_issuer_proof( - storage.key_storage(), - &key_id, - issuer_header, - jpt_claims.clone(), - jwk, - ) - .await - .map_err(Error::KeyStorageError)?; + let jwp_builder = JwpIssuedBuilder::new(issuer_header, jpt_claims.clone()); + + let header = jwp_builder.get_issuer_protected_header().map_or_else( + || Err(Error::JwpBuildingError), + |h| h.to_json_vec().map_err(|_| Error::JwpBuildingError), + )?; + + let data = jwp_builder.get_payloads().map_or_else( + || Err(Error::JwpBuildingError), + |p| p.to_bytes().map_err(|_| Error::JwpBuildingError), + )?; + + let signature = ::sign_bbs(storage.key_storage(), &key_id, &data, &header, jwk) + .await + .map_err(Error::KeyStorageError)?; - Ok(jwp) + jwp_builder + .build_with_proof(signature) + .map_err(|_| Error::JwpBuildingError)? + .encode(SerializationType::COMPACT) + .map_err(|err| Error::EncodingError(Box::new(err))) } async fn create_presented_jwp( @@ -262,7 +274,7 @@ mod iota_document { IotaDocument, ProofAlgorithm, JwkStorageExt, - JwkStorageExt::generate_bbs_key, + JwkStorageExt::generate_bbs, generate_method_iota_document ); diff --git a/identity_storage/src/storage/timeframe_revocation_ext.rs b/identity_storage/src/storage/timeframe_revocation_ext.rs index d03b7d5d8d..766eff7acc 100644 --- a/identity_storage/src/storage/timeframe_revocation_ext.rs +++ b/identity_storage/src/storage/timeframe_revocation_ext.rs @@ -147,9 +147,10 @@ impl TimeframeRevocationExtension for CoreDocument { number_of_signed_messages: payloads.0.len(), }; - let new_proof = ::update_proof(storage.key_storage(), &key_id, jwk, &proof, proof_update_ctx) - .await - .map_err(Error::KeyStorageError)?; + let new_proof = + ::update_signature(storage.key_storage(), &key_id, jwk, &proof, proof_update_ctx) + .await + .map_err(Error::KeyStorageError)?; credential_jwp.set_proof(&new_proof); credential_jwp.set_payloads(payloads); From e835a7151aa51cf2c2fc78d3280473d78e16bc02 Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Wed, 24 Apr 2024 12:29:50 +0200 Subject: [PATCH 11/33] Feat/jpt bbs+ sd stronghold impl (#1354) * Implement JwkStorageExt for StrongholdStorage * reorganize code * persist changes to stronghold when creating bbs+ keypair, clippy, fmt * feature gate * zkp wasm example * zkp_revocation wasm example * wasm bindings * fix docs --- Cargo.toml | 3 + bindings/wasm/Cargo.toml | 3 +- bindings/wasm/docs/api-reference.md | 1044 +++++++++++++++-- .../wasm/examples/src/1_advanced/8_zkp.ts | 226 ++++ .../src/1_advanced/9_zkp_revocation.ts | 281 +++++ bindings/wasm/examples/src/main.ts | 6 + bindings/wasm/examples/src/tests/8_zkp.ts | 8 + .../examples/src/tests/9_zkp_revocation.ts | 8 + bindings/wasm/lib/jwk_storage.ts | 23 +- bindings/wasm/src/credential/jpt.rs | 6 + .../decoded_jpt_credential.rs | 7 + .../jpt_credential_validator.rs | 10 +- .../jpt_credential_validator_utils.rs | 24 +- .../jwp_credential_options.rs | 6 +- .../jpt_presentation_validator.rs | 10 +- .../jwp_presentation_options.rs | 34 + .../jpt_presentiation_validation/mod.rs | 2 + .../validity_timeframe_2024/status.rs | 22 +- bindings/wasm/src/iota/iota_document.rs | 143 ++- .../wasm/src/jpt/issuer_protected_header.rs | 49 + bindings/wasm/src/jpt/jpt_claims.rs | 28 + bindings/wasm/src/jpt/jwp_issued.rs | 6 + bindings/wasm/src/jpt/mod.rs | 4 + bindings/wasm/src/jpt/payload.rs | 6 +- bindings/wasm/src/jpt/proof_algorithm.rs | 2 +- .../storage/jpt_timeframe_revocation_ext.rs | 66 ++ bindings/wasm/src/storage/jwk_storage.rs | 3 + .../src/storage/jwk_storage_bbs_plus_ext.rs | 357 ++++++ bindings/wasm/src/storage/mod.rs | 3 + examples/1_advanced/9_zkp.rs | 6 +- examples/Cargo.toml | 2 +- identity_credential/Cargo.toml | 7 +- .../src/credential/credential.rs | 2 + .../src/credential/jwt_serialization.rs | 2 + identity_credential/src/presentation/mod.rs | 4 + identity_credential/src/revocation/mod.rs | 2 + .../jwt_credential_validation/error.rs | 12 +- identity_credential/src/validator/mod.rs | 4 + identity_iota/Cargo.toml | 3 + identity_storage/Cargo.toml | 8 +- identity_storage/src/key_storage/bls.rs | 4 +- .../src/key_storage/jwk_storage.rs | 30 - .../key_storage/jwk_storage_bbs_plus_ext.rs | 38 + identity_storage/src/key_storage/memstore.rs | 534 +++++---- identity_storage/src/key_storage/mod.rs | 6 +- identity_storage/src/storage/mod.rs | 4 + identity_stronghold/Cargo.toml | 7 +- identity_stronghold/src/lib.rs | 4 + .../src/stronghold_jwk_storage.rs | 150 +-- .../src/stronghold_jwk_storage_ext.rs | 330 ++++++ identity_stronghold/src/stronghold_key_id.rs | 2 +- .../src/stronghold_key_type.rs | 98 ++ identity_stronghold/src/utils.rs | 84 ++ 53 files changed, 3128 insertions(+), 605 deletions(-) create mode 100644 bindings/wasm/examples/src/1_advanced/8_zkp.ts create mode 100644 bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts create mode 100644 bindings/wasm/examples/src/tests/8_zkp.ts create mode 100644 bindings/wasm/examples/src/tests/9_zkp_revocation.ts create mode 100644 bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs create mode 100644 bindings/wasm/src/jpt/issuer_protected_header.rs create mode 100644 bindings/wasm/src/jpt/jpt_claims.rs create mode 100644 bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs create mode 100644 bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs create mode 100644 identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs create mode 100644 identity_stronghold/src/stronghold_jwk_storage_ext.rs create mode 100644 identity_stronghold/src/stronghold_key_type.rs create mode 100644 identity_stronghold/src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 6a6fb06ba6..d3f8600a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,6 @@ homepage = "https://www.iota.org" license = "Apache-2.0" repository = "https://github.com/iotaledger/identity.rs" rust-version = "1.65" + +[patch.crates-io] +iota_stronghold = {git = "https://github.com/tensor-programming/stronghold.rs.git", branch = "feat/expose_runner"} \ No newline at end of file diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index c0d98885b5..4d3178de83 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -30,11 +30,12 @@ serde_repr = { version = "0.1", default-features = false } tokio = { version = "1.29", default-features = false, features = ["sync"] } wasm-bindgen = { version = "0.2.85", features = ["serde-serialize"] } wasm-bindgen-futures = { version = "0.4", default-features = false } +zkryptium = "0.2.1" [dependencies.identity_iota] path = "../../identity_iota" default-features = false -features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021"] +features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021", "jpt-bbs-plus"] [dev-dependencies] rand = "0.8.5" diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index 6fcacf2843..309c070aa7 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -71,6 +71,8 @@ if the object is being concurrently modified.

An extension interface that provides helper functions for publication and resolution of DID documents in Alias Outputs.

+
IssuerProtectedHeader
+
Jpt

A JSON Proof Token (JPT).

@@ -95,6 +97,13 @@ and resolution of DID documents in Alias Outputs.

JwkGenOutput

The result of a key generation in JwkStorage.

+
JwpCredentialOptions
+
+
JwpIssued
+
+
JwpPresentationOptions
+

Options to be set in the JWT claims of a verifiable presentation.

+
JwpVerificationOptions
Jws
@@ -148,8 +157,14 @@ use the methods pack and unpack instead.

MethodType

Supported verification method types.

+
PayloadEntry
+
+
Payloads
+
Presentation
+
PresentationProtectedHeader
+
Proof

Represents a cryptographic proof that can be used to validate verifiable credentials and presentations.

@@ -159,6 +174,8 @@ can be utilized to implement standards or user-defined proofs. The presence of t

Note that this proof is not related to JWT and can be used in combination or as an alternative to it.

+
ProofUpdateCtx
+
Resolver

Convenience type for resolving DID documents from different DID methods.

Also provides methods for resolving DID Documents associated with @@ -187,6 +204,23 @@ verifiable Credentials and Pre with their corresponding disclosure digests.

Note: digests are created using the sha-256 algorithm.

+
SelectiveDisclosurePresentation
+

Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes

+
    +
  • @context MUST NOT be blinded
  • +
  • id MUST be blinded
  • +
  • type MUST NOT be blinded
  • +
  • issuer MUST NOT be blinded
  • +
  • issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used)
  • +
  • expirationDate MUST be blinded (if Timeframe Revocation mechanism is used)
  • +
  • credentialSubject (User have to choose which attribute must be blinded)
  • +
  • credentialSchema MUST NOT be blinded
  • +
  • credentialStatus MUST NOT be blinded
  • +
  • refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism)
  • +
  • termsOfUse NO reason to use it in ZK VC (will be in any case blinded)
  • +
  • evidence (User have to choose which attribute must be blinded)
  • +
+
Service

A DID Document Service used to enable trusted interactions associated with a DID subject.

@@ -218,24 +252,10 @@ working with storage backed DID documents.

## Members
-
FailFast
-

Declares when validation should return if an error occurs.

-
-
AllErrors
-

Return all errors that occur during validation.

-
-
FirstError
-

Return after the first error occurs.

-
-
MethodRelationship
-
-
CredentialStatus
+
PresentationProofAlgorithm
-
StateMetadataEncoding
+
ProofAlgorithm
-
StatusPurpose
-

Purpose of a StatusList2021.

-
StatusCheck

Controls validation behaviour when checking whether or not a credential has been revoked by its credentialStatus.

@@ -253,6 +273,10 @@ working with storage backed DID documents.

SkipAll

Skip all status checks.

+
SerializationType
+
+
MethodRelationship
+
SubjectHolderRelationship

Declares how credential subjects must relate to the presentation holder.

See also the Subject-Holder Relationship section of the specification.

@@ -267,17 +291,29 @@ This variant is the default.

Any

The holder is not required to have any kind of relationship to any credential subject.

+
CredentialStatus
+
+
StatusPurpose
+

Purpose of a StatusList2021.

+
+
StateMetadataEncoding
+
+
FailFast
+

Declares when validation should return if an error occurs.

+
+
AllErrors
+

Return all errors that occur during validation.

+
+
FirstError
+

Return after the first error occurs.

+
+
PayloadType
+
## Functions
-
encodeB64(data)string
-

Encode the given bytes in url-safe base64.

-
-
decodeB64(data)Uint8Array
-

Decode the given url-safe base64-encoded slice into its raw bytes.

-
verifyEd25519(alg, signingInput, decodedSignature, publicKey)

Verify a JWS signature secured with the EdDSA algorithm and curve Ed25519.

This function is useful when one is composing a IJwsVerifier that delegates @@ -289,6 +325,12 @@ prior to calling the function.

start()

Initializes the console error panic hook for better error messages

+
encodeB64(data)string
+

Encode the given bytes in url-safe base64.

+
+
decodeB64(data)Uint8Array
+

Decode the given url-safe base64-encoded slice into its raw bytes.

+
@@ -1372,6 +1414,7 @@ Deserializes an instance from a JSON object. * [.clone()](#DecodedJptCredential+clone) ⇒ [DecodedJptCredential](#DecodedJptCredential) * [.credential()](#DecodedJptCredential+credential) ⇒ [Credential](#Credential) * [.customClaims()](#DecodedJptCredential+customClaims) ⇒ Map.<string, any> + * [.decodedJwp()](#DecodedJptCredential+decodedJwp) ⇒ [JwpIssued](#JwpIssued) @@ -1390,6 +1433,10 @@ Returns the [Credential](#Credential) embedded into this JPT. ### decodedJptCredential.customClaims() ⇒ Map.<string, any> Returns the custom claims parsed from the JPT. +**Kind**: instance method of [DecodedJptCredential](#DecodedJptCredential) + + +### decodedJptCredential.decodedJwp() ⇒ [JwpIssued](#JwpIssued) **Kind**: instance method of [DecodedJptCredential](#DecodedJptCredential) @@ -2149,6 +2196,11 @@ if the object is being concurrently modified. * [.createJws(storage, fragment, payload, options)](#IotaDocument+createJws) ⇒ [Promise.<Jws>](#Jws) * [.createCredentialJwt(storage, fragment, credential, options, [custom_claims])](#IotaDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) * [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#IotaDocument+createPresentationJwt) ⇒ [Promise.<Jwt>](#Jwt) + * [.generateMethodJwp(storage, alg, fragment, scope)](#IotaDocument+generateMethodJwp) ⇒ Promise.<string> + * [.createIssuedJwp(storage, fragment, jpt_claims, options)](#IotaDocument+createIssuedJwp) ⇒ Promise.<string> + * [.createPresentedJwp(presentation, method_id, options)](#IotaDocument+createPresentedJwp) ⇒ Promise.<string> + * [.createCredentialJpt(credential, storage, fragment, options, [custom_claims])](#IotaDocument+createCredentialJpt) ⇒ [Promise.<Jpt>](#Jpt) + * [.createPresentationJpt(presentation, method_id, options)](#IotaDocument+createPresentationJpt) ⇒ [Promise.<Jpt>](#Jpt) * _static_ * [.newWithId(id)](#IotaDocument.newWithId) ⇒ [IotaDocument](#IotaDocument) * [.unpackFromOutput(did, aliasOutput, allowEmpty)](#IotaDocument.unpackFromOutput) ⇒ [IotaDocument](#IotaDocument) @@ -2654,6 +2706,65 @@ private key backed by the `storage` in accordance with the passed `options`. | signature_options | [JwsSignatureOptions](#JwsSignatureOptions) | | presentation_options | [JwtPresentationOptions](#JwtPresentationOptions) | + + +### iotaDocument.generateMethodJwp(storage, alg, fragment, scope) ⇒ Promise.<string> +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| storage | [Storage](#Storage) | +| alg | [ProofAlgorithm](#ProofAlgorithm) | +| fragment | string \| undefined | +| scope | [MethodScope](#MethodScope) | + + + +### iotaDocument.createIssuedJwp(storage, fragment, jpt_claims, options) ⇒ Promise.<string> +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| storage | [Storage](#Storage) | +| fragment | string | +| jpt_claims | JptClaims | +| options | [JwpCredentialOptions](#JwpCredentialOptions) | + + + +### iotaDocument.createPresentedJwp(presentation, method_id, options) ⇒ Promise.<string> +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| presentation | [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) | +| method_id | string | +| options | [JwpPresentationOptions](#JwpPresentationOptions) | + + + +### iotaDocument.createCredentialJpt(credential, storage, fragment, options, [custom_claims]) ⇒ [Promise.<Jpt>](#Jpt) +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| storage | [Storage](#Storage) | +| fragment | string | +| options | [JwpCredentialOptions](#JwpCredentialOptions) | +| [custom_claims] | Map.<string, any> \| undefined | + + + +### iotaDocument.createPresentationJpt(presentation, method_id, options) ⇒ [Promise.<Jpt>](#Jpt) +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| presentation | [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) | +| method_id | string | +| options | [JwpPresentationOptions](#JwpPresentationOptions) | + ### IotaDocument.newWithId(id) ⇒ [IotaDocument](#IotaDocument) @@ -2888,6 +2999,94 @@ Fetches the `IAliasOutput` associated with the given DID. | client | IIotaIdentityClient | | did | [IotaDID](#IotaDID) | + + +## IssuerProtectedHeader +**Kind**: global class + +* [IssuerProtectedHeader](#IssuerProtectedHeader) + * [.typ](#IssuerProtectedHeader+typ) ⇒ string \| undefined + * [.typ](#IssuerProtectedHeader+typ) + * [.alg](#IssuerProtectedHeader+alg) ⇒ [ProofAlgorithm](#ProofAlgorithm) + * [.alg](#IssuerProtectedHeader+alg) + * [.kid](#IssuerProtectedHeader+kid) ⇒ string \| undefined + * [.kid](#IssuerProtectedHeader+kid) + * [.cid](#IssuerProtectedHeader+cid) ⇒ string \| undefined + * [.cid](#IssuerProtectedHeader+cid) + * [.claims()](#IssuerProtectedHeader+claims) ⇒ Array.<string> + + + +### issuerProtectedHeader.typ ⇒ string \| undefined +JWP type (JPT). + +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + + +### issuerProtectedHeader.typ +JWP type (JPT). + +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### issuerProtectedHeader.alg ⇒ [ProofAlgorithm](#ProofAlgorithm) +Algorithm used for the JWP. + +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + + +### issuerProtectedHeader.alg +Algorithm used for the JWP. + +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + +| Param | Type | +| --- | --- | +| arg0 | [ProofAlgorithm](#ProofAlgorithm) | + + + +### issuerProtectedHeader.kid ⇒ string \| undefined +ID for the key used for the JWP. + +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + + +### issuerProtectedHeader.kid +ID for the key used for the JWP. + +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### issuerProtectedHeader.cid ⇒ string \| undefined +Not handled for now. Will be used in the future to resolve external claims + +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + + +### issuerProtectedHeader.cid +Not handled for now. Will be used in the future to resolve external claims + +**Kind**: instance property of [IssuerProtectedHeader](#IssuerProtectedHeader) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### issuerProtectedHeader.claims() ⇒ Array.<string> +**Kind**: instance method of [IssuerProtectedHeader](#IssuerProtectedHeader) ## Jpt @@ -2980,7 +3179,7 @@ Deserializes an instance from a JSON object. | Param | Type | | --- | --- | | credential_jpt | [Jpt](#Jpt) | -| issuer | [CoreDocument](#CoreDocument) | +| issuer | [CoreDocument](#CoreDocument) \| IToCoreDocument | | options | [JptCredentialValidationOptions](#JptCredentialValidationOptions) | | fail_fast | [FailFast](#FailFast) | @@ -3047,7 +3246,7 @@ Only supports `RevocationTimeframe2024`. | Param | Type | | --- | --- | | credential | [Credential](#Credential) | -| issuer | [CoreDocument](#CoreDocument) | +| issuer | [CoreDocument](#CoreDocument) \| IToCoreDocument | | status_check | [StatusCheck](#StatusCheck) | @@ -3062,7 +3261,7 @@ Only supports `RevocationTimeframe2024`. | Param | Type | | --- | --- | | credential | [Credential](#Credential) | -| issuer | [CoreDocument](#CoreDocument) | +| issuer | [CoreDocument](#CoreDocument) \| IToCoreDocument | | validity_timeframe | [Timestamp](#Timestamp) \| undefined | | status_check | [StatusCheck](#StatusCheck) | @@ -3133,7 +3332,7 @@ The following properties are validated according to `options`: | Param | Type | | --- | --- | | presentation_jpt | [Jpt](#Jpt) | -| issuer | [CoreDocument](#CoreDocument) | +| issuer | [CoreDocument](#CoreDocument) \| IToCoreDocument | | options | [JptPresentationValidationOptions](#JptPresentationValidationOptions) | | fail_fast | [FailFast](#FailFast) | @@ -3390,6 +3589,172 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## JwpCredentialOptions +**Kind**: global class + +* [JwpCredentialOptions](#JwpCredentialOptions) + * _instance_ + * [.kid](#JwpCredentialOptions+kid) ⇒ string \| undefined + * [.kid](#JwpCredentialOptions+kid) + * [.toJSON()](#JwpCredentialOptions+toJSON) ⇒ any + * _static_ + * [.fromJSON(value)](#JwpCredentialOptions.fromJSON) ⇒ [JwpCredentialOptions](#JwpCredentialOptions) + + + +### jwpCredentialOptions.kid ⇒ string \| undefined +**Kind**: instance property of [JwpCredentialOptions](#JwpCredentialOptions) + + +### jwpCredentialOptions.kid +**Kind**: instance property of [JwpCredentialOptions](#JwpCredentialOptions) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### jwpCredentialOptions.toJSON() ⇒ any +**Kind**: instance method of [JwpCredentialOptions](#JwpCredentialOptions) + + +### JwpCredentialOptions.fromJSON(value) ⇒ [JwpCredentialOptions](#JwpCredentialOptions) +**Kind**: static method of [JwpCredentialOptions](#JwpCredentialOptions) + +| Param | Type | +| --- | --- | +| value | any | + + + +## JwpIssued +**Kind**: global class + +* [JwpIssued](#JwpIssued) + * _instance_ + * [.toJSON()](#JwpIssued+toJSON) ⇒ any + * [.clone()](#JwpIssued+clone) ⇒ [JwpIssued](#JwpIssued) + * [.encode(serialization)](#JwpIssued+encode) ⇒ string + * [.setProof(proof)](#JwpIssued+setProof) + * [.getProof()](#JwpIssued+getProof) ⇒ Uint8Array + * [.getPayloads()](#JwpIssued+getPayloads) ⇒ [Payloads](#Payloads) + * [.setPayloads(payloads)](#JwpIssued+setPayloads) + * [.getIssuerProtectedHeader()](#JwpIssued+getIssuerProtectedHeader) ⇒ [IssuerProtectedHeader](#IssuerProtectedHeader) + * _static_ + * [.fromJSON(json)](#JwpIssued.fromJSON) ⇒ [JwpIssued](#JwpIssued) + + + +### jwpIssued.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JwpIssued](#JwpIssued) + + +### jwpIssued.clone() ⇒ [JwpIssued](#JwpIssued) +Deep clones the object. + +**Kind**: instance method of [JwpIssued](#JwpIssued) + + +### jwpIssued.encode(serialization) ⇒ string +**Kind**: instance method of [JwpIssued](#JwpIssued) + +| Param | Type | +| --- | --- | +| serialization | [SerializationType](#SerializationType) | + + + +### jwpIssued.setProof(proof) +**Kind**: instance method of [JwpIssued](#JwpIssued) + +| Param | Type | +| --- | --- | +| proof | Uint8Array | + + + +### jwpIssued.getProof() ⇒ Uint8Array +**Kind**: instance method of [JwpIssued](#JwpIssued) + + +### jwpIssued.getPayloads() ⇒ [Payloads](#Payloads) +**Kind**: instance method of [JwpIssued](#JwpIssued) + + +### jwpIssued.setPayloads(payloads) +**Kind**: instance method of [JwpIssued](#JwpIssued) + +| Param | Type | +| --- | --- | +| payloads | [Payloads](#Payloads) | + + + +### jwpIssued.getIssuerProtectedHeader() ⇒ [IssuerProtectedHeader](#IssuerProtectedHeader) +**Kind**: instance method of [JwpIssued](#JwpIssued) + + +### JwpIssued.fromJSON(json) ⇒ [JwpIssued](#JwpIssued) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JwpIssued](#JwpIssued) + +| Param | Type | +| --- | --- | +| json | any | + + + +## JwpPresentationOptions +Options to be set in the JWT claims of a verifiable presentation. + +**Kind**: global class + +* [JwpPresentationOptions](#JwpPresentationOptions) + * [.audience](#JwpPresentationOptions+audience) ⇒ string \| undefined + * [.audience](#JwpPresentationOptions+audience) + * [.nonce](#JwpPresentationOptions+nonce) ⇒ string \| undefined + * [.nonce](#JwpPresentationOptions+nonce) + + + +### jwpPresentationOptions.audience ⇒ string \| undefined +Sets the audience for presentation (`aud` property in JWP Presentation Header). + +**Kind**: instance property of [JwpPresentationOptions](#JwpPresentationOptions) + + +### jwpPresentationOptions.audience +Sets the audience for presentation (`aud` property in JWP Presentation Header). + +**Kind**: instance property of [JwpPresentationOptions](#JwpPresentationOptions) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### jwpPresentationOptions.nonce ⇒ string \| undefined +The nonce to be placed in the Presentation Protected Header. + +**Kind**: instance property of [JwpPresentationOptions](#JwpPresentationOptions) + + +### jwpPresentationOptions.nonce +The nonce to be placed in the Presentation Protected Header. + +**Kind**: instance property of [JwpPresentationOptions](#JwpPresentationOptions) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + ## JwpVerificationOptions @@ -5120,56 +5485,193 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | - + -## Presentation +## PayloadEntry **Kind**: global class -* [Presentation](#Presentation) - * [new Presentation(values)](#new_Presentation_new) - * _instance_ - * [.context()](#Presentation+context) ⇒ Array.<(string\|Record.<string, any>)> - * [.id()](#Presentation+id) ⇒ string \| undefined - * [.type()](#Presentation+type) ⇒ Array.<string> - * [.verifiableCredential()](#Presentation+verifiableCredential) ⇒ [Array.<UnknownCredential>](#UnknownCredential) - * [.holder()](#Presentation+holder) ⇒ string - * [.refreshService()](#Presentation+refreshService) ⇒ Array.<RefreshService> - * [.termsOfUse()](#Presentation+termsOfUse) ⇒ Array.<Policy> - * [.proof()](#Presentation+proof) ⇒ [Proof](#Proof) \| undefined - * [.setProof([proof])](#Presentation+setProof) - * [.properties()](#Presentation+properties) ⇒ Map.<string, any> - * [.toJSON()](#Presentation+toJSON) ⇒ any - * [.clone()](#Presentation+clone) ⇒ [Presentation](#Presentation) - * _static_ - * [.BaseContext()](#Presentation.BaseContext) ⇒ string - * [.BaseType()](#Presentation.BaseType) ⇒ string - * [.fromJSON(json)](#Presentation.fromJSON) ⇒ [Presentation](#Presentation) +* [PayloadEntry](#PayloadEntry) + * [.1](#PayloadEntry+1) ⇒ [PayloadType](#PayloadType) + * [.1](#PayloadEntry+1) + * [.value](#PayloadEntry+value) + * [.value](#PayloadEntry+value) ⇒ any - + -### new Presentation(values) -Constructs a new presentation. +### payloadEntry.1 ⇒ [PayloadType](#PayloadType) +**Kind**: instance property of [PayloadEntry](#PayloadEntry) + +### payloadEntry.1 +**Kind**: instance property of [PayloadEntry](#PayloadEntry) | Param | Type | | --- | --- | -| values | IPresentation | +| arg0 | [PayloadType](#PayloadType) | - + -### presentation.context() ⇒ Array.<(string\|Record.<string, any>)> -Returns a copy of the JSON-LD context(s) applicable to the presentation. +### payloadEntry.value +**Kind**: instance property of [PayloadEntry](#PayloadEntry) -**Kind**: instance method of [Presentation](#Presentation) - +| Param | Type | +| --- | --- | +| value | any | -### presentation.id() ⇒ string \| undefined -Returns a copy of the unique `URI` identifying the presentation. + -**Kind**: instance method of [Presentation](#Presentation) - +### payloadEntry.value ⇒ any +**Kind**: instance property of [PayloadEntry](#PayloadEntry) + -### presentation.type() ⇒ Array.<string> +## Payloads +**Kind**: global class + +* [Payloads](#Payloads) + * [new Payloads(entries)](#new_Payloads_new) + * _instance_ + * [.toJSON()](#Payloads+toJSON) ⇒ any + * [.clone()](#Payloads+clone) ⇒ [Payloads](#Payloads) + * [.getValues()](#Payloads+getValues) ⇒ Array.<any> + * [.getUndisclosedIndexes()](#Payloads+getUndisclosedIndexes) ⇒ Uint32Array + * [.getDisclosedIndexes()](#Payloads+getDisclosedIndexes) ⇒ Uint32Array + * [.getUndisclosedPayloads()](#Payloads+getUndisclosedPayloads) ⇒ Array.<any> + * [.getDisclosedPayloads()](#Payloads+getDisclosedPayloads) ⇒ [Payloads](#Payloads) + * [.setUndisclosed(index)](#Payloads+setUndisclosed) + * [.replacePayloadAtIndex(index, value)](#Payloads+replacePayloadAtIndex) ⇒ any + * _static_ + * [.fromJSON(json)](#Payloads.fromJSON) ⇒ [Payloads](#Payloads) + * [.newFromValues(values)](#Payloads.newFromValues) ⇒ [Payloads](#Payloads) + + + +### new Payloads(entries) + +| Param | Type | +| --- | --- | +| entries | [Array.<PayloadEntry>](#PayloadEntry) | + + + +### payloads.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.clone() ⇒ [Payloads](#Payloads) +Deep clones the object. + +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.getValues() ⇒ Array.<any> +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.getUndisclosedIndexes() ⇒ Uint32Array +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.getDisclosedIndexes() ⇒ Uint32Array +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.getUndisclosedPayloads() ⇒ Array.<any> +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.getDisclosedPayloads() ⇒ [Payloads](#Payloads) +**Kind**: instance method of [Payloads](#Payloads) + + +### payloads.setUndisclosed(index) +**Kind**: instance method of [Payloads](#Payloads) + +| Param | Type | +| --- | --- | +| index | number | + + + +### payloads.replacePayloadAtIndex(index, value) ⇒ any +**Kind**: instance method of [Payloads](#Payloads) + +| Param | Type | +| --- | --- | +| index | number | +| value | any | + + + +### Payloads.fromJSON(json) ⇒ [Payloads](#Payloads) +Deserializes an instance from a JSON object. + +**Kind**: static method of [Payloads](#Payloads) + +| Param | Type | +| --- | --- | +| json | any | + + + +### Payloads.newFromValues(values) ⇒ [Payloads](#Payloads) +**Kind**: static method of [Payloads](#Payloads) + +| Param | Type | +| --- | --- | +| values | Array.<any> | + + + +## Presentation +**Kind**: global class + +* [Presentation](#Presentation) + * [new Presentation(values)](#new_Presentation_new) + * _instance_ + * [.context()](#Presentation+context) ⇒ Array.<(string\|Record.<string, any>)> + * [.id()](#Presentation+id) ⇒ string \| undefined + * [.type()](#Presentation+type) ⇒ Array.<string> + * [.verifiableCredential()](#Presentation+verifiableCredential) ⇒ [Array.<UnknownCredential>](#UnknownCredential) + * [.holder()](#Presentation+holder) ⇒ string + * [.refreshService()](#Presentation+refreshService) ⇒ Array.<RefreshService> + * [.termsOfUse()](#Presentation+termsOfUse) ⇒ Array.<Policy> + * [.proof()](#Presentation+proof) ⇒ [Proof](#Proof) \| undefined + * [.setProof([proof])](#Presentation+setProof) + * [.properties()](#Presentation+properties) ⇒ Map.<string, any> + * [.toJSON()](#Presentation+toJSON) ⇒ any + * [.clone()](#Presentation+clone) ⇒ [Presentation](#Presentation) + * _static_ + * [.BaseContext()](#Presentation.BaseContext) ⇒ string + * [.BaseType()](#Presentation.BaseType) ⇒ string + * [.fromJSON(json)](#Presentation.fromJSON) ⇒ [Presentation](#Presentation) + + + +### new Presentation(values) +Constructs a new presentation. + + +| Param | Type | +| --- | --- | +| values | IPresentation | + + + +### presentation.context() ⇒ Array.<(string\|Record.<string, any>)> +Returns a copy of the JSON-LD context(s) applicable to the presentation. + +**Kind**: instance method of [Presentation](#Presentation) + + +### presentation.id() ⇒ string \| undefined +Returns a copy of the unique `URI` identifying the presentation. + +**Kind**: instance method of [Presentation](#Presentation) + + +### presentation.type() ⇒ Array.<string> Returns a copy of the URIs defining the type of the presentation. **Kind**: instance method of [Presentation](#Presentation) @@ -5257,6 +5759,85 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## PresentationProtectedHeader +**Kind**: global class + +* [PresentationProtectedHeader](#PresentationProtectedHeader) + * [.alg](#PresentationProtectedHeader+alg) ⇒ [PresentationProofAlgorithm](#PresentationProofAlgorithm) + * [.alg](#PresentationProtectedHeader+alg) + * [.kid](#PresentationProtectedHeader+kid) ⇒ string \| undefined + * [.kid](#PresentationProtectedHeader+kid) + * [.aud](#PresentationProtectedHeader+aud) ⇒ string \| undefined + * [.aud](#PresentationProtectedHeader+aud) + * [.nonce](#PresentationProtectedHeader+nonce) ⇒ string \| undefined + * [.nonce](#PresentationProtectedHeader+nonce) + + + +### presentationProtectedHeader.alg ⇒ [PresentationProofAlgorithm](#PresentationProofAlgorithm) +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + + +### presentationProtectedHeader.alg +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + +| Param | Type | +| --- | --- | +| arg0 | [PresentationProofAlgorithm](#PresentationProofAlgorithm) | + + + +### presentationProtectedHeader.kid ⇒ string \| undefined +ID for the key used for the JWP. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + + +### presentationProtectedHeader.kid +ID for the key used for the JWP. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### presentationProtectedHeader.aud ⇒ string \| undefined +Who have received the JPT. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + + +### presentationProtectedHeader.aud +Who have received the JPT. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + + + +### presentationProtectedHeader.nonce ⇒ string \| undefined +For replay attacks. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + + +### presentationProtectedHeader.nonce +For replay attacks. + +**Kind**: instance property of [PresentationProtectedHeader](#PresentationProtectedHeader) + +| Param | Type | +| --- | --- | +| [arg0] | string \| undefined | + ## Proof @@ -5326,6 +5907,146 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## ProofUpdateCtx +**Kind**: global class + +* [ProofUpdateCtx](#ProofUpdateCtx) + * [.old_start_validity_timeframe](#ProofUpdateCtx+old_start_validity_timeframe) ⇒ Uint8Array + * [.old_start_validity_timeframe](#ProofUpdateCtx+old_start_validity_timeframe) + * [.new_start_validity_timeframe](#ProofUpdateCtx+new_start_validity_timeframe) ⇒ Uint8Array + * [.new_start_validity_timeframe](#ProofUpdateCtx+new_start_validity_timeframe) + * [.old_end_validity_timeframe](#ProofUpdateCtx+old_end_validity_timeframe) ⇒ Uint8Array + * [.old_end_validity_timeframe](#ProofUpdateCtx+old_end_validity_timeframe) + * [.new_end_validity_timeframe](#ProofUpdateCtx+new_end_validity_timeframe) ⇒ Uint8Array + * [.new_end_validity_timeframe](#ProofUpdateCtx+new_end_validity_timeframe) + * [.index_start_validity_timeframe](#ProofUpdateCtx+index_start_validity_timeframe) ⇒ number + * [.index_start_validity_timeframe](#ProofUpdateCtx+index_start_validity_timeframe) + * [.index_end_validity_timeframe](#ProofUpdateCtx+index_end_validity_timeframe) ⇒ number + * [.index_end_validity_timeframe](#ProofUpdateCtx+index_end_validity_timeframe) + * [.number_of_signed_messages](#ProofUpdateCtx+number_of_signed_messages) ⇒ number + * [.number_of_signed_messages](#ProofUpdateCtx+number_of_signed_messages) + + + +### proofUpdateCtx.old\_start\_validity\_timeframe ⇒ Uint8Array +Old `startValidityTimeframe` value + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.old\_start\_validity\_timeframe +Old `startValidityTimeframe` value + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | Uint8Array | + + + +### proofUpdateCtx.new\_start\_validity\_timeframe ⇒ Uint8Array +New `startValidityTimeframe` value to be signed + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.new\_start\_validity\_timeframe +New `startValidityTimeframe` value to be signed + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | Uint8Array | + + + +### proofUpdateCtx.old\_end\_validity\_timeframe ⇒ Uint8Array +Old `endValidityTimeframe` value + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.old\_end\_validity\_timeframe +Old `endValidityTimeframe` value + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | Uint8Array | + + + +### proofUpdateCtx.new\_end\_validity\_timeframe ⇒ Uint8Array +New `endValidityTimeframe` value to be signed + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.new\_end\_validity\_timeframe +New `endValidityTimeframe` value to be signed + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | Uint8Array | + + + +### proofUpdateCtx.index\_start\_validity\_timeframe ⇒ number +Index of `startValidityTimeframe` claim inside the array of Claims + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.index\_start\_validity\_timeframe +Index of `startValidityTimeframe` claim inside the array of Claims + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | number | + + + +### proofUpdateCtx.index\_end\_validity\_timeframe ⇒ number +Index of `endValidityTimeframe` claim inside the array of Claims + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.index\_end\_validity\_timeframe +Index of `endValidityTimeframe` claim inside the array of Claims + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | number | + + + +### proofUpdateCtx.number\_of\_signed\_messages ⇒ number +Number of signed messages, number of payloads in a JWP + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + + +### proofUpdateCtx.number\_of\_signed\_messages +Number of signed messages, number of payloads in a JWP + +**Kind**: instance property of [ProofUpdateCtx](#ProofUpdateCtx) + +| Param | Type | +| --- | --- | +| arg0 | number | + ## Resolver @@ -5509,7 +6230,7 @@ Information used to determine the current status of a [Credential](#Credential). * [.startValidityTimeframe()](#RevocationTimeframeStatus+startValidityTimeframe) ⇒ [Timestamp](#Timestamp) * [.endValidityTimeframe()](#RevocationTimeframeStatus+endValidityTimeframe) ⇒ [Timestamp](#Timestamp) * [.id()](#RevocationTimeframeStatus+id) ⇒ string - * [.index()](#RevocationTimeframeStatus+index) ⇒ number + * [.index()](#RevocationTimeframeStatus+index) ⇒ number \| undefined * _static_ * [.fromJSON(json)](#RevocationTimeframeStatus.fromJSON) ⇒ [RevocationTimeframeStatus](#RevocationTimeframeStatus) @@ -5558,8 +6279,8 @@ Return the URL fo the `RevocationBitmapStatus`. **Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) -### revocationTimeframeStatus.index() ⇒ number -Return the index of the credential in the issuer's revocation bitmap if it can be decoded. +### revocationTimeframeStatus.index() ⇒ number \| undefined +Return the index of the credential in the issuer's revocation bitmap **Kind**: instance method of [RevocationTimeframeStatus](#RevocationTimeframeStatus) @@ -5910,6 +6631,93 @@ If path is an empty slice, decoys will be added to the top level. | path | string | | number_of_decoys | number | + + +## SelectiveDisclosurePresentation +Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes +- @context MUST NOT be blinded +- id MUST be blinded +- type MUST NOT be blinded +- issuer MUST NOT be blinded +- issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used) +- expirationDate MUST be blinded (if Timeframe Revocation mechanism is used) +- credentialSubject (User have to choose which attribute must be blinded) +- credentialSchema MUST NOT be blinded +- credentialStatus MUST NOT be blinded +- refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism) +- termsOfUse NO reason to use it in ZK VC (will be in any case blinded) +- evidence (User have to choose which attribute must be blinded) + +**Kind**: global class + +* [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) + * [new SelectiveDisclosurePresentation(issued_jwp)](#new_SelectiveDisclosurePresentation_new) + * [.concealInSubject(path)](#SelectiveDisclosurePresentation+concealInSubject) + * [.concealInEvidence(path)](#SelectiveDisclosurePresentation+concealInEvidence) + * [.setPresentationHeader(header)](#SelectiveDisclosurePresentation+setPresentationHeader) + + + +### new SelectiveDisclosurePresentation(issued_jwp) +Initialize a presentation starting from an Issued JWP. +The properties `jti`, `nbf`, `issuanceDate`, `expirationDate` and `termsOfUse` are concealed by default. + + +| Param | Type | +| --- | --- | +| issued_jwp | [JwpIssued](#JwpIssued) | + + + +### selectiveDisclosurePresentation.concealInSubject(path) +Selectively disclose "credentialSubject" attributes. +# Example +``` +{ + "id": 1234, + "name": "Alice", + "mainCourses": ["Object-oriented Programming", "Mathematics"], + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", +} +``` +If you want to undisclose for example the Mathematics course and the name of the degree: +``` +undisclose_subject("mainCourses[1]"); +undisclose_subject("degree.name"); +``` + +**Kind**: instance method of [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) + +| Param | Type | +| --- | --- | +| path | string | + + + +### selectiveDisclosurePresentation.concealInEvidence(path) +Undiscloses "evidence" attributes. + +**Kind**: instance method of [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) + +| Param | Type | +| --- | --- | +| path | string | + + + +### selectiveDisclosurePresentation.setPresentationHeader(header) +Sets presentation protected header. + +**Kind**: instance method of [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation) + +| Param | Type | +| --- | --- | +| header | [PresentationProtectedHeader](#PresentationProtectedHeader) | + ## Service @@ -6697,41 +7505,13 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | - - -## FailFast -Declares when validation should return if an error occurs. - -**Kind**: global variable - - -## AllErrors -Return all errors that occur during validation. - -**Kind**: global variable - - -## FirstError -Return after the first error occurs. + +## PresentationProofAlgorithm **Kind**: global variable - - -## MethodRelationship -**Kind**: global variable - - -## CredentialStatus -**Kind**: global variable - - -## StateMetadataEncoding -**Kind**: global variable - - -## StatusPurpose -Purpose of a [StatusList2021](#StatusList2021). + +## ProofAlgorithm **Kind**: global variable @@ -6763,6 +7543,14 @@ Validate the status if supported, skip any unsupported ## SkipAll Skip all status checks. +**Kind**: global variable + + +## SerializationType +**Kind**: global variable + + +## MethodRelationship **Kind**: global variable @@ -6791,28 +7579,42 @@ The holder must match the subject only for credentials where the [`nonTransferab The holder is not required to have any kind of relationship to any credential subject. **Kind**: global variable - + -## encodeB64(data) ⇒ string -Encode the given bytes in url-safe base64. +## CredentialStatus +**Kind**: global variable + -**Kind**: global function +## StatusPurpose +Purpose of a [StatusList2021](#StatusList2021). -| Param | Type | -| --- | --- | -| data | Uint8Array | +**Kind**: global variable + - +## StateMetadataEncoding +**Kind**: global variable + -## decodeB64(data) ⇒ Uint8Array -Decode the given url-safe base64-encoded slice into its raw bytes. +## FailFast +Declares when validation should return if an error occurs. -**Kind**: global function +**Kind**: global variable + -| Param | Type | -| --- | --- | -| data | Uint8Array | +## AllErrors +Return all errors that occur during validation. + +**Kind**: global variable + + +## FirstError +Return after the first error occurs. +**Kind**: global variable + + +## PayloadType +**Kind**: global variable ## verifyEd25519(alg, signingInput, decodedSignature, publicKey) @@ -6841,3 +7643,25 @@ prior to calling the function. Initializes the console error panic hook for better error messages **Kind**: global function + + +## encodeB64(data) ⇒ string +Encode the given bytes in url-safe base64. + +**Kind**: global function + +| Param | Type | +| --- | --- | +| data | Uint8Array | + + + +## decodeB64(data) ⇒ Uint8Array +Decode the given url-safe base64-encoded slice into its raw bytes. + +**Kind**: global function + +| Param | Type | +| --- | --- | +| data | Uint8Array | + diff --git a/bindings/wasm/examples/src/1_advanced/8_zkp.ts b/bindings/wasm/examples/src/1_advanced/8_zkp.ts new file mode 100644 index 0000000000..55d0c82fca --- /dev/null +++ b/bindings/wasm/examples/src/1_advanced/8_zkp.ts @@ -0,0 +1,226 @@ +import { + Credential, + FailFast, + IotaDID, + IotaDocument, + IotaIdentityClient, + JptCredentialValidationOptions, + JptCredentialValidator, + JptCredentialValidatorUtils, + JptPresentationValidationOptions, + JptPresentationValidator, + JptPresentationValidatorUtils, + JwkMemStore, + JwpCredentialOptions, + JwpPresentationOptions, + KeyIdMemStore, + MethodScope, + ProofAlgorithm, + SelectiveDisclosurePresentation, + Storage, +} from "@iota/identity-wasm/node"; +import { + type Address, + AliasOutput, + Client, + MnemonicSecretManager, + SecretManager, + SecretManagerType, + Utils, +} from "@iota/sdk-wasm/node"; +import { API_ENDPOINT, ensureAddressHasFunds } from "../util"; + +/** Creates a DID Document and publishes it in a new Alias Output. + +Its functionality is equivalent to the "create DID" example +and exists for convenient calling from the other examples. */ +export async function createDid(client: Client, secretManager: SecretManagerType, storage: Storage): Promise<{ + address: Address; + document: IotaDocument; + fragment: string; +}> { + const didClient = new IotaIdentityClient(client); + const networkHrp: string = await didClient.getNetworkHrp(); + + const secretManagerInstance = new SecretManager(secretManager); + const walletAddressBech32 = (await secretManagerInstance.generateEd25519Addresses({ + accountIndex: 0, + range: { + start: 0, + end: 1, + }, + bech32Hrp: networkHrp, + }))[0]; + + console.log("Wallet address Bech32:", walletAddressBech32); + + await ensureAddressHasFunds(client, walletAddressBech32); + + const address: Address = Utils.parseBech32Address(walletAddressBech32); + + // Create a new DID document with a placeholder DID. + // The DID will be derived from the Alias Id of the Alias Output after publishing. + const document = new IotaDocument(networkHrp); + + const fragment = await document.generateMethodJwp( + storage, + ProofAlgorithm.BLS12381_SHA256, + undefined, + MethodScope.VerificationMethod(), + ); + // Construct an Alias Output containing the DID document, with the wallet address + // set as both the state controller and governor. + const aliasOutput: AliasOutput = await didClient.newDidOutput(address, document); + + // Publish the Alias Output and get the published DID document. + const published = await didClient.publishDidOutput(secretManager, aliasOutput); + + return { address, document: published, fragment }; +} +export async function zkp() { + // =========================================================================== + // Step 1: Create identity for the issuer. + // =========================================================================== + + // Create a new client to interact with the IOTA ledger. + const client = new Client({ + primaryNode: API_ENDPOINT, + localPow: true, + }); + + // Creates a new wallet and identity (see "0_create_did" example). + const issuerSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const issuerStorage: Storage = new Storage( + new JwkMemStore(), + new KeyIdMemStore(), + ); + let { document: issuerDocument, fragment: issuerFragment } = await createDid( + client, + issuerSecretManager, + issuerStorage, + ); + + // =========================================================================== + // Step 2: Issuer creates and signs a Verifiable Credential with BBS algorithm. + // =========================================================================== + + // Create a credential subject indicating the degree earned by Alice. + const subject = { + name: "Alice", + mainCourses: ["Object-oriented Programming", "Mathematics"], + degree: { + type: "BachelorDegree", + name: "Bachelor of Science and Arts", + }, + GPA: 4.0, + }; + + // Build credential using the above subject and issuer. + const credential = new Credential({ + id: "https:/example.edu/credentials/3732", + issuer: issuerDocument.id(), + type: "UniversityDegreeCredential", + credentialSubject: subject, + }); + const credentialJpt = await issuerDocument + .createCredentialJpt( + credential, + issuerStorage, + issuerFragment, + new JwpCredentialOptions(), + ); + // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure, + // that the issuance date is not in the future and that the expiration date is not in the past: + const decodedJpt = JptCredentialValidator.validate( + credentialJpt, + issuerDocument, + new JptCredentialValidationOptions(), + FailFast.FirstError, + ); + + // =========================================================================== + // Step 3: Issuer sends the Verifiable Credential to the holder. + // =========================================================================== + console.log("Sending credential (as JPT) to the holder: " + credentialJpt.toString()); + + // ============================================================================================ + // Step 4: Holder resolve Issuer's DID, retrieve Issuer's document and validate the Credential + // ============================================================================================ + const identityClient = new IotaIdentityClient(client); + + // Holder resolves issuer's DID. + let issuerDid = IotaDID.parse(JptCredentialValidatorUtils.extractIssuerFromIssuedJpt(credentialJpt).toString()); + let issuerDoc = await identityClient.resolveDid(issuerDid); + + // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented + let decodedCredential = JptCredentialValidator.validate( + credentialJpt, + issuerDoc, + new JptCredentialValidationOptions(), + FailFast.FirstError, + ); + + // =========================================================================== + // Step 5: Verifier sends the holder a challenge and requests a Presentation. + // + // Please be aware that when we mention "Presentation," we are not alluding to the Verifiable Presentation standard as defined by W3C (https://www.w3.org/TR/vc-data-model/#presentations). + // Instead, our reference is to a JWP Presentation (https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form), which differs from the W3C standard. + // =========================================================================== + + // A unique random challenge generated by the requester per presentation can mitigate replay attacks. + const challenge = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + // ========================================================================================================= + // Step 6: Holder engages in the Selective Disclosure of credential's attributes. + // ========================================================================================================= + const methodId = decodedCredential + .decodedJwp() + .getIssuerProtectedHeader() + .kid!; + const selectiveDisclosurePresentation = new SelectiveDisclosurePresentation(decodedCredential.decodedJwp()); + selectiveDisclosurePresentation.concealInSubject("mainCourses[1]"); + selectiveDisclosurePresentation.concealInSubject("degree.name"); + + // ======================================================================================================================================= + // Step 7: Holder needs Issuer's Public Key to compute the Signature Proof of Knowledge and construct the Presentation + // JPT. + // ======================================================================================================================================= + + // Construct a JPT(JWP in the Presentation form) representing the Selectively Disclosed Verifiable Credential + const presentationOptions = new JwpPresentationOptions(); + presentationOptions.nonce = challenge; + const presentationJpt = await issuerDoc + .createPresentationJpt( + selectiveDisclosurePresentation, + methodId, + presentationOptions, + ); + + // =========================================================================== + // Step 8: Holder sends a Presentation JPT to the Verifier. + // =========================================================================== + + console.log("Sending presentation (as JPT) to the verifier: " + presentationJpt.toString()); + + // =========================================================================== + // Step 9: Verifier receives the Presentation and verifies it. + // =========================================================================== + + // Verifier resolve Issuer DID + const issuerDidV = IotaDID.parse( + JptPresentationValidatorUtils.extractIssuerFromPresentedJpt(presentationJpt).toString(), + ); + const issuerDocV = await identityClient.resolveDid(issuerDidV); + + const presentationValidationOptions = new JptPresentationValidationOptions({ nonce: challenge }); + const decodedPresentedCredential = JptPresentationValidator.validate( + presentationJpt, + issuerDocV, + presentationValidationOptions, + FailFast.FirstError, + ); + + console.log("Presented credential successfully validated: " + decodedPresentedCredential.credential()); +} diff --git a/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts b/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts new file mode 100644 index 0000000000..e8c3d586a1 --- /dev/null +++ b/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts @@ -0,0 +1,281 @@ +import { + Credential, + Duration, + FailFast, + IotaDID, + IotaDocument, + IotaIdentityClient, + JptCredentialValidationOptions, + JptCredentialValidator, + JptCredentialValidatorUtils, + JptPresentationValidationOptions, + JptPresentationValidator, + JptPresentationValidatorUtils, + JwkMemStore, + JwpCredentialOptions, + JwpPresentationOptions, + KeyIdMemStore, + MethodScope, + ProofAlgorithm, + RevocationBitmap, + RevocationTimeframeStatus, + SelectiveDisclosurePresentation, + Status, + StatusCheck, + Storage, + Timestamp, +} from "@iota/identity-wasm/node"; +import { + type Address, + AliasOutput, + Client, + MnemonicSecretManager, + SecretManager, + SecretManagerType, + Utils, +} from "@iota/sdk-wasm/node"; +import { API_ENDPOINT, ensureAddressHasFunds } from "../util"; + +/** Creates a DID Document and publishes it in a new Alias Output. + +Its functionality is equivalent to the "create DID" example +and exists for convenient calling from the other examples. */ +export async function createDid(client: Client, secretManager: SecretManagerType, storage: Storage): Promise<{ + address: Address; + document: IotaDocument; + fragment: string; +}> { + const didClient = new IotaIdentityClient(client); + const networkHrp: string = await didClient.getNetworkHrp(); + + const secretManagerInstance = new SecretManager(secretManager); + const walletAddressBech32 = (await secretManagerInstance.generateEd25519Addresses({ + accountIndex: 0, + range: { + start: 0, + end: 1, + }, + bech32Hrp: networkHrp, + }))[0]; + + console.log("Wallet address Bech32:", walletAddressBech32); + + await ensureAddressHasFunds(client, walletAddressBech32); + + const address: Address = Utils.parseBech32Address(walletAddressBech32); + + // Create a new DID document with a placeholder DID. + // The DID will be derived from the Alias Id of the Alias Output after publishing. + const document = new IotaDocument(networkHrp); + + const fragment = await document.generateMethodJwp( + storage, + ProofAlgorithm.BLS12381_SHA256, + undefined, + MethodScope.VerificationMethod(), + ); + const revocationBitmap = new RevocationBitmap(); + const serviceId = document.id().toUrl().join("#my-revocation-service"); + const service = revocationBitmap.toService(serviceId); + + document.insertService(service); + // Construct an Alias Output containing the DID document, with the wallet address + // set as both the state controller and governor. + const aliasOutput: AliasOutput = await didClient.newDidOutput(address, document); + + // Publish the Alias Output and get the published DID document. + const published = await didClient.publishDidOutput(secretManager, aliasOutput); + + return { address, document: published, fragment }; +} +export async function zkp_revocation() { + // Create a new client to interact with the IOTA ledger. + const client = new Client({ + primaryNode: API_ENDPOINT, + localPow: true, + }); + + // Creates a new wallet and identity (see "0_create_did" example). + const issuerSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const issuerStorage: Storage = new Storage( + new JwkMemStore(), + new KeyIdMemStore(), + ); + let { document: issuerDocument, fragment: issuerFragment } = await createDid( + client, + issuerSecretManager, + issuerStorage, + ); + const holderSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const holderStorage: Storage = new Storage( + new JwkMemStore(), + new KeyIdMemStore(), + ); + let { document: holderDocument, fragment: holderFragment } = await createDid( + client, + holderSecretManager, + holderStorage, + ); + // ========================================================================================= + // Step 1: Create a new RevocationTimeframeStatus containing the current validityTimeframe + // ======================================================================================= + + const timeframeId = issuerDocument.id().toUrl().join("#my-revocation-service"); + let revocationTimeframeStatus = new RevocationTimeframeStatus( + timeframeId.toString(), + 5, + Duration.minutes(1), + Timestamp.nowUTC(), + ); + + // Create a credential subject indicating the degree earned by Alice. + const subject = { + name: "Alice", + mainCourses: ["Object-oriented Programming", "Mathematics"], + degree: { + type: "BachelorDegree", + name: "Bachelor of Science and Arts", + }, + GPA: 4.0, + }; + + // Build credential using the above subject and issuer. + const credential = new Credential({ + id: "https:/example.edu/credentials/3732", + issuer: issuerDocument.id(), + type: "UniversityDegreeCredential", + credentialSubject: subject, + credentialStatus: revocationTimeframeStatus as any as Status, + }); + const credentialJpt = await issuerDocument + .createCredentialJpt( + credential, + issuerStorage, + issuerFragment, + new JwpCredentialOptions(), + ); + // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure, + // that the issuance date is not in the future and that the expiration date is not in the past: + const decodedJpt = JptCredentialValidator.validate( + credentialJpt, + issuerDocument, + new JptCredentialValidationOptions(), + FailFast.FirstError, + ); + + console.log("Sending credential (as JPT) to the holder: " + credentialJpt.toString()); + + // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented + let decodedCredential = JptCredentialValidator.validate( + credentialJpt, + issuerDocument, + new JptCredentialValidationOptions(), + FailFast.FirstError, + ); + + // =========================================================================== + // Credential's Status check + // =========================================================================== + JptCredentialValidatorUtils.checkTimeframesAndRevocationWithValidityTimeframe2024( + decodedCredential.credential(), + issuerDocument, + undefined, + StatusCheck.Strict, + ); + + // A unique random challenge generated by the requester per presentation can mitigate replay attacks. + const challenge = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + const methodId = decodedCredential + .decodedJwp() + .getIssuerProtectedHeader() + .kid!; + + const selectiveDisclosurePresentation = new SelectiveDisclosurePresentation(decodedCredential.decodedJwp()); + selectiveDisclosurePresentation.concealInSubject("mainCourses[1]"); + selectiveDisclosurePresentation.concealInSubject("degree.name"); + + // Construct a JPT(JWP in the Presentation form) representing the Selectively Disclosed Verifiable Credential + const presentationOptions = new JwpPresentationOptions(); + presentationOptions.nonce = challenge; + const presentationJpt = await issuerDocument + .createPresentationJpt( + selectiveDisclosurePresentation, + methodId, + presentationOptions, + ); + + console.log("Sending presentation (as JPT) to the verifier: " + presentationJpt.toString()); + + // =========================================================================== + // Step 2: Verifier receives the Presentation and verifies it. + // =========================================================================== + + const presentationValidationOptions = new JptPresentationValidationOptions({ nonce: challenge }); + const decodedPresentedCredential = JptPresentationValidator.validate( + presentationJpt, + issuerDocument, + presentationValidationOptions, + FailFast.FirstError, + ); + + JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024( + decodedPresentedCredential.credential(), + undefined, + StatusCheck.Strict, + ); + + console.log("Presented credential successfully validated: " + decodedPresentedCredential.credential()); + + // =========================================================================== + // Step 2b: Waiting for the next validityTimeframe, will result in the Credential timeframe interval NOT valid + // =========================================================================== + + try { + const now = new Date(); + const timeInTwoMinutes = new Date(now.setMinutes(now.getMinutes() + 2)); + JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024( + decodedPresentedCredential.credential(), + Timestamp.parse(timeInTwoMinutes.toISOString()), + StatusCheck.Strict, + ); + } catch (_) { + console.log("successfully expired!"); + } + + // =========================================================================== + // Issuer decides to Revoke Holder's Credential + // =========================================================================== + + console.log("Issuer decides to revoke the Credential"); + + const identityClient = new IotaIdentityClient(client); + + // Update the RevocationBitmap service in the issuer's DID Document. + // This revokes the credential's unique index. + issuerDocument.revokeCredentials("my-revocation-service", 5); + let aliasOutput = await identityClient.updateDidOutput(issuerDocument); + const rent = await identityClient.getRentStructure(); + aliasOutput = await client.buildAliasOutput({ + ...aliasOutput, + amount: Utils.computeStorageDeposit(aliasOutput, rent), + aliasId: aliasOutput.getAliasId(), + unlockConditions: aliasOutput.getUnlockConditions(), + }); + issuerDocument = await identityClient.publishDidOutput(issuerSecretManager, aliasOutput); + + // Holder checks if his credential has been revoked by the Issuer + try { + JptCredentialValidatorUtils.checkRevocationWithValidityTimeframe2024( + decodedCredential.credential(), + issuerDocument, + StatusCheck.Strict, + ); + } catch (_) { + console.log("Credential revoked!"); + } +} diff --git a/bindings/wasm/examples/src/main.ts b/bindings/wasm/examples/src/main.ts index 145980e649..0a074d3fd2 100644 --- a/bindings/wasm/examples/src/main.ts +++ b/bindings/wasm/examples/src/main.ts @@ -17,6 +17,8 @@ import { customResolution } from "./1_advanced/4_custom_resolution"; import { domainLinkage } from "./1_advanced/5_domain_linkage"; import { sdJwt } from "./1_advanced/6_sd_jwt"; import { statusList2021 } from "./1_advanced/7_status_list_2021"; +import { zkp } from "./1_advanced/8_zkp"; +import { zkp_revocation } from "./1_advanced/9_zkp_revocation"; async function main() { // Extract example name. @@ -58,6 +60,10 @@ async function main() { return await sdJwt(); case "7_status_list_2021": return await statusList2021(); + case "8_zkp": + return await zkp(); + case "9_zkp_revocation": + return await zkp_revocation(); default: throw "Unknown example name: '" + argument + "'"; } diff --git a/bindings/wasm/examples/src/tests/8_zkp.ts b/bindings/wasm/examples/src/tests/8_zkp.ts new file mode 100644 index 0000000000..52d5b72bc4 --- /dev/null +++ b/bindings/wasm/examples/src/tests/8_zkp.ts @@ -0,0 +1,8 @@ +import { zkp } from "../1_advanced/8_zkp"; + +// Only verifies that no uncaught exceptions are thrown, including syntax errors etc. +describe("Test node examples", function() { + it("zkp", async () => { + await zkp(); + }); +}); diff --git a/bindings/wasm/examples/src/tests/9_zkp_revocation.ts b/bindings/wasm/examples/src/tests/9_zkp_revocation.ts new file mode 100644 index 0000000000..96075765f3 --- /dev/null +++ b/bindings/wasm/examples/src/tests/9_zkp_revocation.ts @@ -0,0 +1,8 @@ +import { zkp_revocation } from "../1_advanced/9_zkp_revocation"; + +// Only verifies that no uncaught exceptions are thrown, including syntax errors etc. +describe("Test node examples", function() { + it("zkp_revocation", async () => { + await zkp_revocation(); + }); +}); diff --git a/bindings/wasm/lib/jwk_storage.ts b/bindings/wasm/lib/jwk_storage.ts index 2c1156e5ac..235abcc8ce 100644 --- a/bindings/wasm/lib/jwk_storage.ts +++ b/bindings/wasm/lib/jwk_storage.ts @@ -1,5 +1,5 @@ import * as ed from "@noble/ed25519"; -import { decodeB64, encodeB64, Jwk, JwkGenOutput, JwkStorage } from "~identity_wasm"; +import { decodeB64, encodeB64, Jwk, JwkGenOutput, JwkStorage, ProofAlgorithm, ProofUpdateCtx } from "~identity_wasm"; import { EdCurve, JwkType, JwsAlgorithm } from "./jose"; type Ed25519PrivateKey = Uint8Array; @@ -18,6 +18,10 @@ export class JwkMemStore implements JwkStorage { return "Ed25519"; } + private _get_key(keyId: string): Jwk | undefined { + return this._keys.get(keyId); + } + public async generate(keyType: string, algorithm: JwsAlgorithm): Promise { if (keyType !== JwkMemStore.ed25519KeyType()) { throw new Error(`unsupported key type ${keyType}`); @@ -126,6 +130,23 @@ function decodeJwk(jwk: Jwk): [Ed25519PrivateKey, Ed25519PublicKey] { } } +export interface JwkStorageBBSPlusExt { + // Generate a new BLS12381 key represented as a JSON Web Key. + generateBBS: (algorithm: ProofAlgorithm) => Promise; + /** Signs a chunk of data together with an optional header + * using the private key corresponding to the given `keyId` and according + * to `publicKey`'s requirements. + */ + signBBS: (keyId: string, data: Uint8Array[], publicKey: Jwk, header?: Uint8Array) => Promise; + // Updates the timeframe validity period information of a given signature. + updateBBSSignature: ( + keyId: string, + publicKey: Jwk, + signature: Uint8Array, + proofCtx: ProofUpdateCtx, + ) => Promise; +} + // Returns a random number between `min` and `max` (inclusive). // SAFETY NOTE: This is not cryptographically secure randomness and thus not suitable for production use. // It suffices for our testing implementation however and avoids an external dependency. diff --git a/bindings/wasm/src/credential/jpt.rs b/bindings/wasm/src/credential/jpt.rs index 090110b018..ad0b3def10 100644 --- a/bindings/wasm/src/credential/jpt.rs +++ b/bindings/wasm/src/credential/jpt.rs @@ -34,3 +34,9 @@ impl From for Jpt { value.0 } } + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Promise")] + pub type PromiseJpt; +} diff --git a/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs b/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs index 9cd56b2d23..04a776345e 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs @@ -5,6 +5,7 @@ use wasm_bindgen::prelude::*; use crate::common::MapStringAny; use crate::credential::WasmCredential; use crate::error::Result; +use crate::jpt::WasmJwpIssued; #[wasm_bindgen(js_name = DecodedJptCredential)] pub struct WasmDecodedJptCredential(pub(crate) DecodedJptCredential); @@ -27,6 +28,12 @@ impl WasmDecodedJptCredential { None => Ok(MapStringAny::default()), } } + + // The decoded and verified issued JWP, will be used to construct the presented JWP. + #[wasm_bindgen(js_name = decodedJwp)] + pub fn decoded_jwp(&self) -> WasmJwpIssued { + WasmJwpIssued(self.0.decoded_jwp.clone()) + } } impl From for WasmDecodedJptCredential { diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs index 5ee75daf8d..b64f2348de 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs @@ -3,7 +3,7 @@ use crate::credential::WasmDecodedJptCredential; use crate::credential::WasmFailFast; use crate::credential::WasmJpt; use crate::credential::WasmJptCredentialValidationOptions; -use crate::did::WasmCoreDocument; +use crate::did::IToCoreDocument; use crate::error::Result; use crate::error::WasmResult; use identity_iota::credential::JptCredentialValidator; @@ -17,13 +17,13 @@ impl WasmJptCredentialValidator { #[wasm_bindgen] pub fn validate( credential_jpt: &WasmJpt, - issuer: WasmCoreDocument, + issuer: &IToCoreDocument, options: &WasmJptCredentialValidationOptions, fail_fast: WasmFailFast, ) -> Result { - let issuer_doc = ImportedDocumentLock::Core(issuer.0); - let doc = issuer_doc.try_read()?; - JptCredentialValidator::validate(&credential_jpt.0, &doc, &options.0, fail_fast.into()) + let issuer_lock = ImportedDocumentLock::from(issuer); + let issuer_guard = issuer_lock.try_read()?; + JptCredentialValidator::validate(&credential_jpt.0, &issuer_guard, &options.0, fail_fast.into()) .wasm_result() .map(WasmDecodedJptCredential) } diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs index 18e27cf4fd..8cbb9f6ac3 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs @@ -3,8 +3,8 @@ use crate::common::WasmTimestamp; use crate::credential::options::WasmStatusCheck; use crate::credential::WasmCredential; use crate::credential::WasmJpt; +use crate::did::IToCoreDocument; use crate::did::WasmCoreDID; -use crate::did::WasmCoreDocument; use crate::error::Result; use crate::error::WasmResult; use identity_iota::core::Object; @@ -63,13 +63,17 @@ impl WasmJptCredentialValidatorUtils { #[wasm_bindgen(js_name = "checkRevocationWithValidityTimeframe2024")] pub fn check_revocation_with_validity_timeframe_2024( credential: &WasmCredential, - issuer: WasmCoreDocument, + issuer: &IToCoreDocument, status_check: WasmStatusCheck, ) -> Result<()> { - let issuer_doc = ImportedDocumentLock::Core(issuer.0); - let doc = issuer_doc.try_read()?; - JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024(&credential.0, &doc, status_check.into()) - .wasm_result() + let issuer_lock = ImportedDocumentLock::from(issuer); + let issuer_guard = issuer_lock.try_read()?; + JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024( + &credential.0, + &issuer_guard, + status_check.into(), + ) + .wasm_result() } /// Checks whether the credential status has been revoked or the timeframe interval is INVALID @@ -78,15 +82,15 @@ impl WasmJptCredentialValidatorUtils { #[wasm_bindgen(js_name = "checkTimeframesAndRevocationWithValidityTimeframe2024")] pub fn check_timeframes_and_revocation_with_validity_timeframe_2024( credential: &WasmCredential, - issuer: WasmCoreDocument, + issuer: &IToCoreDocument, validity_timeframe: Option, status_check: WasmStatusCheck, ) -> Result<()> { - let issuer_doc = ImportedDocumentLock::Core(issuer.0); - let doc = issuer_doc.try_read()?; + let issuer_lock = ImportedDocumentLock::from(issuer); + let issuer_guard = issuer_lock.try_read()?; JptCredentialValidatorUtils::check_timeframes_and_revocation_with_validity_timeframe_2024( &credential.0, - &doc, + &issuer_guard, validity_timeframe.map(|t| t.0), status_check.into(), ) diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs index fd37ad52aa..ca284eac97 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs @@ -5,15 +5,13 @@ use serde::Deserialize; use serde::Serialize; use wasm_bindgen::prelude::*; -#[wasm_bindgen(inspectable)] +#[wasm_bindgen(js_name = JwpCredentialOptions, getter_with_clone, inspectable)] #[derive(Serialize, Deserialize, Default)] pub struct WasmJwpCredentialOptions { - #[wasm_bindgen(getter_with_clone)] - #[serde(skip_serializing_if = "Option::is_none")] pub kid: Option, } -#[wasm_bindgen] +#[wasm_bindgen(js_class = JwpCredentialOptions)] impl WasmJwpCredentialOptions { #[wasm_bindgen(constructor)] pub fn new() -> WasmJwpCredentialOptions { diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs index e059d99f68..8f55969c4d 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs @@ -3,7 +3,7 @@ use crate::credential::WasmDecodedJptPresentation; use crate::credential::WasmFailFast; use crate::credential::WasmJpt; use crate::credential::WasmJptPresentationValidationOptions; -use crate::did::WasmCoreDocument; +use crate::did::IToCoreDocument; use crate::error::Result; use crate::error::WasmResult; use identity_iota::credential::JptPresentationValidator; @@ -25,13 +25,13 @@ impl WasmJptPresentationValidator { #[wasm_bindgen] pub fn validate( presentation_jpt: &WasmJpt, - issuer: WasmCoreDocument, + issuer: &IToCoreDocument, options: &WasmJptPresentationValidationOptions, fail_fast: WasmFailFast, ) -> Result { - let issuer_doc = ImportedDocumentLock::Core(issuer.0); - let doc = issuer_doc.try_read()?; - JptPresentationValidator::validate(&presentation_jpt.0, &doc, &options.0, fail_fast.into()) + let issuer_lock = ImportedDocumentLock::from(issuer); + let issuer_guard = issuer_lock.try_read()?; + JptPresentationValidator::validate(&presentation_jpt.0, &issuer_guard, &options.0, fail_fast.into()) .wasm_result() .map(WasmDecodedJptPresentation) } diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs new file mode 100644 index 0000000000..8ead5045d6 --- /dev/null +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs @@ -0,0 +1,34 @@ +use identity_iota::core::Url; +use identity_iota::credential::JwpPresentationOptions; +use wasm_bindgen::prelude::*; + +/// Options to be set in the JWT claims of a verifiable presentation. +#[wasm_bindgen(js_name = JwpPresentationOptions, inspectable, getter_with_clone)] +#[derive(Default, Clone)] +pub struct WasmJwpPresentationOptions { + /// Sets the audience for presentation (`aud` property in JWP Presentation Header). + pub audience: Option, + /// The nonce to be placed in the Presentation Protected Header. + pub nonce: Option, +} + +#[wasm_bindgen(js_class = JwpPresentationOptions)] +impl WasmJwpPresentationOptions { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmJwpPresentationOptions { + Self::default() + } +} + +impl TryFrom for JwpPresentationOptions { + type Error = JsError; + fn try_from(value: WasmJwpPresentationOptions) -> Result { + let WasmJwpPresentationOptions { audience, nonce } = value; + let audience = audience + .map(Url::parse) + .transpose() + .map_err(|e| JsError::new(&e.to_string()))?; + + Ok(JwpPresentationOptions { audience, nonce }) + } +} diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs index a39c129027..9ebfe02925 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs @@ -2,8 +2,10 @@ mod decoded_jpt_presentation; mod jpt_presentation_validation_options; mod jpt_presentation_validator; mod jpt_presentation_validator_utils; +mod jwp_presentation_options; pub use decoded_jpt_presentation::*; pub use jpt_presentation_validation_options::*; pub use jpt_presentation_validator::*; pub use jpt_presentation_validator_utils::*; +pub use jwp_presentation_options::*; diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs index 469716f3ec..363475b1c7 100644 --- a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs +++ b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs @@ -1,5 +1,5 @@ +use identity_iota::core::Url; use identity_iota::credential::RevocationTimeframeStatus; -use identity_iota::did::DIDUrl; use wasm_bindgen::prelude::*; use crate::common::WasmDuration; @@ -27,7 +27,7 @@ impl WasmRevocationTimeframeStatus { RevocationTimeframeStatus::new( start_validity.map(|t| t.0), duration.0, - DIDUrl::parse(id).wasm_result()?, + Url::parse(id).wasm_result()?, index, ) .wasm_result() @@ -36,26 +36,26 @@ impl WasmRevocationTimeframeStatus { /// Get startValidityTimeframe value. #[wasm_bindgen(js_name = "startValidityTimeframe")] - pub fn start_validity_timeframe(&self) -> Result { - self.0.start_validity_timeframe().wasm_result().map(WasmTimestamp) + pub fn start_validity_timeframe(&self) -> WasmTimestamp { + self.0.start_validity_timeframe().into() } /// Get endValidityTimeframe value. #[wasm_bindgen(js_name = "endValidityTimeframe")] - pub fn end_validity_timeframe(&self) -> Result { - self.0.end_validity_timeframe().wasm_result().map(WasmTimestamp) + pub fn end_validity_timeframe(&self) -> WasmTimestamp { + self.0.end_validity_timeframe().into() } /// Return the URL fo the `RevocationBitmapStatus`. #[wasm_bindgen] - pub fn id(&self) -> Result { - self.0.id().wasm_result().map(|url| url.to_string()) + pub fn id(&self) -> String { + self.0.id().to_string() } - /// Return the index of the credential in the issuer's revocation bitmap if it can be decoded. + /// Return the index of the credential in the issuer's revocation bitmap #[wasm_bindgen] - pub fn index(&self) -> Result { - self.0.index().wasm_result() + pub fn index(&self) -> Option { + self.0.index() } } diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index 8d004422ad..777a00e679 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -44,13 +44,16 @@ use crate::common::RecordStringAny; use crate::common::UDIDUrlQuery; use crate::common::UOneOrManyNumber; use crate::common::WasmTimestamp; +use crate::credential::PromiseJpt; use crate::credential::UnknownCredential; use crate::credential::WasmCredential; +use crate::credential::WasmJpt; +use crate::credential::WasmJwpCredentialOptions; +use crate::credential::WasmJwpPresentationOptions; use crate::credential::WasmJws; use crate::credential::WasmJwt; use crate::credential::WasmPresentation; use crate::did::CoreDocumentLock; - use crate::did::PromiseJws; use crate::did::PromiseJwt; use crate::did::WasmCoreDocument; @@ -65,6 +68,9 @@ use crate::iota::WasmIotaDocumentMetadata; use crate::iota::WasmStateMetadataEncoding; use crate::jose::WasmDecodedJws; use crate::jose::WasmJwsAlgorithm; +use crate::jpt::WasmJptClaims; +use crate::jpt::WasmProofAlgorithm; +use crate::jpt::WasmSelectiveDisclosurePresentation; use crate::storage::WasmJwsSignatureOptions; use crate::storage::WasmJwtPresentationOptions; use crate::storage::WasmStorage; @@ -75,6 +81,7 @@ use crate::verification::WasmJwsVerifier; use crate::verification::WasmMethodRelationship; use crate::verification::WasmMethodScope; use crate::verification::WasmVerificationMethod; +use identity_iota::storage::JwpDocumentExt; pub(crate) struct IotaDocumentLock(tokio::sync::RwLock); @@ -852,6 +859,140 @@ impl WasmIotaDocument { }); Ok(promise.unchecked_into()) } + + #[wasm_bindgen(js_name = generateMethodJwp)] + pub fn generate_method_jwp( + &self, + storage: &WasmStorage, + alg: WasmProofAlgorithm, + fragment: Option, + scope: WasmMethodScope, + ) -> Result { + let document_lock_clone: Rc = self.0.clone(); + let storage_clone: Rc = storage.0.clone(); + let promise: Promise = future_to_promise(async move { + let method_fragment: String = document_lock_clone + .write() + .await + .generate_method_jwp( + &storage_clone, + KeyType::from_static_str("BLS12381"), + alg.into(), + fragment.as_deref(), + scope.0, + ) + .await + .wasm_result()?; + Ok(JsValue::from(method_fragment)) + }); + + Ok(promise.unchecked_into()) + } + + #[wasm_bindgen(js_name = createIssuedJwp)] + pub fn create_issued_jwp( + &self, + storage: &WasmStorage, + fragment: String, + jpt_claims: WasmJptClaims, + options: WasmJwpCredentialOptions, + ) -> Result { + let document_lock_clone: Rc = self.0.clone(); + let jpt_claims = jpt_claims.into_serde().wasm_result()?; + let storage_clone: Rc = storage.0.clone(); + let options = options.into(); + let promise: Promise = future_to_promise(async move { + let jwp: String = document_lock_clone + .write() + .await + .create_issued_jwp(&storage_clone, fragment.as_str(), &jpt_claims, &options) + .await + .wasm_result()?; + Ok(JsValue::from(jwp)) + }); + + Ok(promise.unchecked_into()) + } + + #[wasm_bindgen(js_name = createPresentedJwp)] + pub fn create_presented_jwp( + &self, + presentation: WasmSelectiveDisclosurePresentation, + method_id: String, + options: WasmJwpPresentationOptions, + ) -> Result { + let document_lock_clone: Rc = self.0.clone(); + let options = options.try_into()?; + let promise: Promise = future_to_promise(async move { + let mut presentation = presentation.0; + let jwp: String = document_lock_clone + .write() + .await + .create_presented_jwp(&mut presentation, method_id.as_str(), &options) + .await + .wasm_result()?; + Ok(JsValue::from(jwp)) + }); + + Ok(promise.unchecked_into()) + } + + #[wasm_bindgen(js_name = createCredentialJpt)] + pub fn create_credential_jpt( + &self, + credential: WasmCredential, + storage: &WasmStorage, + fragment: String, + options: WasmJwpCredentialOptions, + custom_claims: Option, + ) -> Result { + let document_lock_clone: Rc = self.0.clone(); + let storage_clone: Rc = storage.0.clone(); + let options = options.into(); + let custom_claims = custom_claims.and_then(|claims| claims.into_serde().ok()); + let promise: Promise = future_to_promise(async move { + let jpt = document_lock_clone + .write() + .await + .create_credential_jpt( + &credential.0, + &storage_clone, + fragment.as_str(), + &options, + custom_claims, + ) + .await + .map(WasmJpt) + .wasm_result()?; + Ok(JsValue::from(jpt)) + }); + + Ok(promise.unchecked_into()) + } + + #[wasm_bindgen(js_name = createPresentationJpt)] + pub fn create_presentation_jpt( + &self, + presentation: WasmSelectiveDisclosurePresentation, + method_id: String, + options: WasmJwpPresentationOptions, + ) -> Result { + let document_lock_clone: Rc = self.0.clone(); + let options = options.try_into()?; + let promise: Promise = future_to_promise(async move { + let mut presentation = presentation.0; + let jpt = document_lock_clone + .write() + .await + .create_presentation_jpt(&mut presentation, method_id.as_str(), &options) + .await + .map(WasmJpt) + .wasm_result()?; + Ok(JsValue::from(jpt)) + }); + + Ok(promise.unchecked_into()) + } } impl From for WasmIotaDocument { diff --git a/bindings/wasm/src/jpt/issuer_protected_header.rs b/bindings/wasm/src/jpt/issuer_protected_header.rs new file mode 100644 index 0000000000..8a825cfc19 --- /dev/null +++ b/bindings/wasm/src/jpt/issuer_protected_header.rs @@ -0,0 +1,49 @@ +use crate::jpt::WasmProofAlgorithm; +use jsonprooftoken::jwp::header::IssuerProtectedHeader; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = IssuerProtectedHeader, getter_with_clone, inspectable)] +pub struct WasmIssuerProtectedHeader { + /// JWP type (JPT). + pub typ: Option, + /// Algorithm used for the JWP. + pub alg: WasmProofAlgorithm, + /// ID for the key used for the JWP. + pub kid: Option, + /// Not handled for now. Will be used in the future to resolve external claims + pub cid: Option, + /// Claims. + claims: Vec, +} + +#[wasm_bindgen(js_class = IssuerProtectedHeader)] +impl WasmIssuerProtectedHeader { + #[wasm_bindgen] + pub fn claims(&self) -> Vec { + self.claims.clone() + } +} + +impl From for IssuerProtectedHeader { + fn from(value: WasmIssuerProtectedHeader) -> Self { + let WasmIssuerProtectedHeader { typ, alg, kid, cid, .. } = value; + let mut header = IssuerProtectedHeader::new(alg.into()); + header.set_typ(typ); + header.set_kid(kid); + header.set_cid(cid); + + header + } +} + +impl From for WasmIssuerProtectedHeader { + fn from(value: IssuerProtectedHeader) -> Self { + WasmIssuerProtectedHeader { + typ: value.typ().cloned(), + alg: value.alg().into(), + kid: value.kid().cloned(), + cid: value.cid().cloned(), + claims: value.claims().map(|claims| claims.clone().0).unwrap_or_default(), + } + } +} diff --git a/bindings/wasm/src/jpt/jpt_claims.rs b/bindings/wasm/src/jpt/jpt_claims.rs new file mode 100644 index 0000000000..c04f07095d --- /dev/null +++ b/bindings/wasm/src/jpt/jpt_claims.rs @@ -0,0 +1,28 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "JptClaims")] + pub type WasmJptClaims; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_JPT_CLAIMS: &'static str = r#" +/** JPT claims */ + +interface JptClaims { + /** Who issued the JWP*/ + readonly iss?: string; + /** Subject of the JPT. */ + readonly sub?: string; + /** Expiration time. */ + readonly exp?: number; + /** Issuance date. */ + readonly iat?: number; + /** Time before which the JPT MUST NOT be accepted */ + readonly nbf?: number; + /** Unique ID for the JPT. */ + readonly jti?: string; + /** Custom claims. */ + readonly [properties: string]: any; +}"#; diff --git a/bindings/wasm/src/jpt/jwp_issued.rs b/bindings/wasm/src/jpt/jwp_issued.rs index 80ab7e33bd..89c32a6822 100644 --- a/bindings/wasm/src/jpt/jwp_issued.rs +++ b/bindings/wasm/src/jpt/jwp_issued.rs @@ -5,6 +5,7 @@ use super::WasmPayloads; use super::WasmSerializationType; use crate::error::Result; use crate::error::WasmResult; +use crate::jpt::WasmIssuerProtectedHeader; #[wasm_bindgen(js_name = JwpIssued)] pub struct WasmJwpIssued(pub(crate) JwpIssued); @@ -38,4 +39,9 @@ impl WasmJwpIssued { pub fn set_payloads(&mut self, payloads: WasmPayloads) { self.0.set_payloads(payloads.into()) } + + #[wasm_bindgen(js_name = getIssuerProtectedHeader)] + pub fn get_issuer_protected_header(&self) -> WasmIssuerProtectedHeader { + self.0.get_issuer_protected_header().clone().into() + } } diff --git a/bindings/wasm/src/jpt/mod.rs b/bindings/wasm/src/jpt/mod.rs index ed756e382a..3683114019 100644 --- a/bindings/wasm/src/jpt/mod.rs +++ b/bindings/wasm/src/jpt/mod.rs @@ -1,4 +1,6 @@ mod encoding; +mod issuer_protected_header; +mod jpt_claims; mod jwp_issued; mod jwp_presentation_builder; mod payload; @@ -6,6 +8,8 @@ mod presentation_protected_header; mod proof_algorithm; pub use encoding::*; +pub use issuer_protected_header::*; +pub use jpt_claims::*; pub use jwp_issued::*; pub use jwp_presentation_builder::*; pub use payload::*; diff --git a/bindings/wasm/src/jpt/payload.rs b/bindings/wasm/src/jpt/payload.rs index 518a2c43e6..80c542d7bb 100644 --- a/bindings/wasm/src/jpt/payload.rs +++ b/bindings/wasm/src/jpt/payload.rs @@ -36,10 +36,10 @@ impl From for WasmPayloadType { } } -#[wasm_bindgen] +#[wasm_bindgen(js_name = PayloadEntry)] pub struct WasmPayloadEntry(JsValue, pub WasmPayloadType); -#[wasm_bindgen] +#[wasm_bindgen(js_class = PayloadEntry)] impl WasmPayloadEntry { #[wasm_bindgen(setter)] pub fn set_value(&mut self, value: JsValue) { @@ -69,7 +69,7 @@ impl WasmPayloads { .map(WasmPayloads) } - #[wasm_bindgen(constructor)] + #[wasm_bindgen(js_name = newFromValues)] pub fn new_from_values(values: Vec) -> Result { let values = values .into_iter() diff --git a/bindings/wasm/src/jpt/proof_algorithm.rs b/bindings/wasm/src/jpt/proof_algorithm.rs index 1eec61259d..be8ff4e504 100644 --- a/bindings/wasm/src/jpt/proof_algorithm.rs +++ b/bindings/wasm/src/jpt/proof_algorithm.rs @@ -3,7 +3,7 @@ use wasm_bindgen::prelude::*; #[allow(non_camel_case_types)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[wasm_bindgen] +#[wasm_bindgen(js_name = ProofAlgorithm)] pub enum WasmProofAlgorithm { BLS12381_SHA256, BLS12381_SHAKE256, diff --git a/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs b/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs new file mode 100644 index 0000000000..d8745567bb --- /dev/null +++ b/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs @@ -0,0 +1,66 @@ +use identity_iota::storage::ProofUpdateCtx; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = ProofUpdateCtx, inspectable, getter_with_clone)] +pub struct WasmProofUpdateCtx { + /// Old `startValidityTimeframe` value + pub old_start_validity_timeframe: Vec, + /// New `startValidityTimeframe` value to be signed + pub new_start_validity_timeframe: Vec, + /// Old `endValidityTimeframe` value + pub old_end_validity_timeframe: Vec, + /// New `endValidityTimeframe` value to be signed + pub new_end_validity_timeframe: Vec, + /// Index of `startValidityTimeframe` claim inside the array of Claims + pub index_start_validity_timeframe: usize, + /// Index of `endValidityTimeframe` claim inside the array of Claims + pub index_end_validity_timeframe: usize, + /// Number of signed messages, number of payloads in a JWP + pub number_of_signed_messages: usize, +} + +impl From for WasmProofUpdateCtx { + fn from(value: ProofUpdateCtx) -> Self { + let ProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } = value; + Self { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } + } +} + +impl From for ProofUpdateCtx { + fn from(value: WasmProofUpdateCtx) -> Self { + let WasmProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } = value; + Self { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } + } +} diff --git a/bindings/wasm/src/storage/jwk_storage.rs b/bindings/wasm/src/storage/jwk_storage.rs index 4616def075..6adf78845b 100644 --- a/bindings/wasm/src/storage/jwk_storage.rs +++ b/bindings/wasm/src/storage/jwk_storage.rs @@ -50,6 +50,9 @@ extern "C" { #[wasm_bindgen(method)] pub fn exists(this: &WasmJwkStorage, key_id: String) -> PromiseBool; + + #[wasm_bindgen(method)] + pub(crate) fn _get_key(this: &WasmJwkStorage, key_id: &str) -> Option; } #[async_trait::async_trait(?Send)] diff --git a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs new file mode 100644 index 0000000000..c6d0a2dafd --- /dev/null +++ b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs @@ -0,0 +1,357 @@ +use std::str::FromStr; + +use crate::error::Result as WasmResult; +use crate::error::WasmResult as _; +use crate::jose::WasmJwk; +use crate::jpt::WasmProofAlgorithm; + +use super::WasmJwkGenOutput; +use super::WasmJwkStorage; +use super::WasmProofUpdateCtx; + +use identity_iota::storage::JwkGenOutput; +use identity_iota::storage::JwkStorage; +use identity_iota::storage::JwkStorageExt; +use identity_iota::storage::KeyId; +use identity_iota::storage::KeyStorageError; +use identity_iota::storage::KeyStorageErrorKind; +use identity_iota::storage::KeyStorageResult; +use identity_iota::storage::KeyType; +use identity_iota::storage::ProofUpdateCtx; +use identity_iota::verification::jwk::BlsCurve; +use identity_iota::verification::jwk::Jwk; +use identity_iota::verification::jwk::JwkParamsEc; +use identity_iota::verification::jwu; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use wasm_bindgen::prelude::*; +use zkryptium::bbsplus::ciphersuites::BbsCiphersuite; +use zkryptium::bbsplus::ciphersuites::Bls12381Sha256; +use zkryptium::bbsplus::ciphersuites::Bls12381Shake256; +use zkryptium::bbsplus::keys::BBSplusPublicKey; +use zkryptium::bbsplus::keys::BBSplusSecretKey; +use zkryptium::bbsplus::signature::BBSplusSignature; +use zkryptium::keys::pair::KeyPair; +use zkryptium::schemes::algorithms::BBSplus; +use zkryptium::schemes::algorithms::BbsBls12381Sha256; +use zkryptium::schemes::algorithms::BbsBls12381Shake256; +use zkryptium::schemes::generics::Signature; + +fn generate_bbs_keypair() -> Result<(BBSplusSecretKey, BBSplusPublicKey), KeyStorageError> { + let keypair = KeyPair::>::random() + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; + let sk = keypair.private_key().clone(); + let pk = keypair.public_key().clone(); + Ok((sk, pk)) +} + +fn encode_bls_jwk(private_key: &BBSplusSecretKey, public_key: &BBSplusPublicKey) -> Jwk { + let (x, y) = public_key.to_coordinates(); + let x = jwu::encode_b64(x); + let y = jwu::encode_b64(y); + + let d = jwu::encode_b64(private_key.to_bytes()); + let mut params = JwkParamsEc::new(); + params.x = x; + params.y = y; + params.d = Some(d); + params.crv = BlsCurve::BLS12381G2.name().to_owned(); + Jwk::from_params(params) +} + +fn expand_bls_jwk(jwk: &Jwk) -> Result<(BBSplusSecretKey, BBSplusPublicKey), KeyStorageError> { + let params: &JwkParamsEc = jwk.try_ec_params().unwrap(); + + if params + .try_bls_curve() + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(err))? + != BlsCurve::BLS12381G2 + { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("expected an {} key", BlsCurve::BLS12381G2.name())), + ); + } + + let sk: BBSplusSecretKey = params + .d + .as_deref() + .map(jwu::decode_b64) + .ok_or_else(|| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("expected Jwk `d` param to be present") + })? + .map(|v| BBSplusSecretKey::from_bytes(&v)) + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `d` param") + .with_source(err) + })? + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid BBS+ secret key".to_owned()) + })?; + + let x: [u8; BBSplusPublicKey::COORDINATE_LEN] = jwu::decode_b64(¶ms.x) + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `x` param") + .with_source(err) + })? + .try_into() + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected key of length {}", BBSplusPublicKey::COORDINATE_LEN)) + })?; + + let y: [u8; BBSplusPublicKey::COORDINATE_LEN] = jwu::decode_b64(¶ms.y) + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `y` param") + .with_source(err) + })? + .try_into() + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected key of length {}", BBSplusPublicKey::COORDINATE_LEN)) + })?; + + let pk = BBSplusPublicKey::from_coordinates(&x, &y).map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid BBS+ public key".to_owned()) + })?; + + Ok((sk, pk)) +} + +fn update_bbs_signature( + sig: &[u8; 80], + sk: &BBSplusSecretKey, + update_ctx: &ProofUpdateCtx, +) -> Result<[u8; 80], KeyStorageError> +where + A: BbsCiphersuite, +{ + let sig = Signature::>::from_bytes(sig) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))?; + let ProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } = update_ctx; + let half_updated = sig + .update_signature( + sk, + old_start_validity_timeframe, + new_start_validity_timeframe, + *index_start_validity_timeframe, + *number_of_signed_messages, + ) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + })?; + half_updated + .update_signature( + sk, + old_end_validity_timeframe, + new_end_validity_timeframe, + *index_end_validity_timeframe, + *number_of_signed_messages, + ) + .map(|sig| sig.to_bytes()) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed")) +} + +fn decode_sk_jwt(jwk: &Jwk) -> Result { + let params = jwk.try_ec_params().map_err(|_| KeyStorageErrorKind::Unspecified)?; + BBSplusSecretKey::from_bytes( + ¶ms + .d + .as_deref() + .map(jwu::decode_b64) + .ok_or_else(|| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("expected Jwk `d` param to be present") + })? + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `d` param") + .with_source(err) + })?, + ) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("key not valid")) +} + +#[wasm_bindgen(js_class = JwkStorage)] +impl WasmJwkStorage { + #[wasm_bindgen(js_name = generateBBS)] + /// Generates a new BBS+ keypair. + pub async fn _generate_bbs(&self, alg: WasmProofAlgorithm) -> WasmResult { + self + .generate_bbs(KeyType::from_static_str("BLS12381"), alg.into()) + .await + .map(WasmJwkGenOutput::from) + .wasm_result() + } + + #[wasm_bindgen(js_name = signBBS)] + pub async fn _sign_bbs( + &self, + key_id: String, + data: Vec, + public_key: WasmJwk, + header: Option>, + ) -> WasmResult { + let key_id = KeyId::new(key_id); + let data = data.into_iter().map(|arr| arr.to_vec()).collect::>(); + let header = header.unwrap_or_default(); + self + .sign_bbs(&key_id, &data, header.as_slice(), &public_key.into()) + .await + .map(|v| js_sys::Uint8Array::from(v.as_slice())) + .wasm_result() + } + + #[wasm_bindgen(js_name = updateBBSSignature)] + pub async fn _update_signature( + &self, + key_id: String, + public_key: &WasmJwk, + signature: Vec, + ctx: WasmProofUpdateCtx, + ) -> WasmResult { + let key_id = KeyId::new(key_id); + let signature = signature + .try_into() + .map_err(|_| JsError::new("Invalid signature length"))?; + self + .update_signature(&key_id, &public_key.0, &signature, ctx.into()) + .await + .map(|sig| js_sys::Uint8Array::from(sig.as_slice())) + .wasm_result() + } +} + +#[async_trait::async_trait(?Send)] +impl JwkStorageExt for WasmJwkStorage { + async fn generate_bbs(&self, _key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + let (sk, pk) = match alg { + ProofAlgorithm::BLS12381_SHA256 => generate_bbs_keypair::(), + ProofAlgorithm::BLS12381_SHAKE256 => generate_bbs_keypair::(), + other => Err( + KeyStorageError::new(KeyStorageErrorKind::KeyAlgorithmMismatch) + .with_custom_message(format!("cannot validate proof with {}", other)), + ), + }?; + + let mut jwk = encode_bls_jwk(&sk, &pk); + jwk.set_alg(alg.to_string()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + + let public_jwk = jwk.to_public().expect("kty != oct"); + let kid = ::insert(self, jwk).await?; + + Ok(JwkGenOutput::new(kid, public_jwk)) + } + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult> { + let Some(private_jwk) = WasmJwkStorage::_get_key(self, key_id.as_str()).map(Jwk::from) else { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + }; + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm)) + .map_err(KeyStorageError::new)?; + + if matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { + let ec_params = public_key.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) + .with_source(err) + })?; + if ec_params.crv != BlsCurve::BLS12381G2.to_string() { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "expected Jwk with EC {} crv in order to generate the proof with {alg}", + BlsCurve::BLS12381G2 + )), + ); + } + } else { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{alg} is not supported")), + ); + } + let (sk, pk) = expand_bls_jwk(&private_jwk)?; + match alg { + ProofAlgorithm::BLS12381_SHA256 => { + Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) + } + ProofAlgorithm::BLS12381_SHAKE256 => { + Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) + } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{other} is not supported")), + ); + } + } + .map(|bytes| bytes.to_vec()) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("signature failed".to_owned()) + }) + } + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8; BBSplusSignature::BYTES], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm)) + .map_err(KeyStorageError::new)?; + + if matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { + let ec_params = public_key.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) + .with_source(err) + })?; + if ec_params.crv != BlsCurve::BLS12381G2.to_string() { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "expected Jwk with EC {} crv in order to generate the proof with {alg}", + BlsCurve::BLS12381G2 + )), + ); + } + } else { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{alg} is not supported")), + ); + } + let Some(private_jwk) = WasmJwkStorage::_get_key(self, key_id.as_str()).map(Jwk::from) else { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + }; + let sk = decode_sk_jwt(&private_jwk)?; + match alg { + ProofAlgorithm::BLS12381_SHA256 => update_bbs_signature::(signature, &sk, &ctx), + ProofAlgorithm::BLS12381_SHAKE256 => update_bbs_signature::(signature, &sk, &ctx), + _ => unreachable!(), + } + } +} diff --git a/bindings/wasm/src/storage/mod.rs b/bindings/wasm/src/storage/mod.rs index 8295d95e88..fe54110e9d 100644 --- a/bindings/wasm/src/storage/mod.rs +++ b/bindings/wasm/src/storage/mod.rs @@ -1,14 +1,17 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod jpt_timeframe_revocation_ext; mod jwk_gen_output; mod jwk_storage; +mod jwk_storage_bbs_plus_ext; mod jwt_presentation_options; mod key_id_storage; mod method_digest; mod signature_options; mod wasm_storage; +pub use jpt_timeframe_revocation_ext::*; pub use jwk_gen_output::*; pub use jwk_storage::*; pub use jwt_presentation_options::*; diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs index 744395c3cc..d683d4378d 100644 --- a/examples/1_advanced/9_zkp.rs +++ b/examples/1_advanced/9_zkp.rs @@ -158,17 +158,17 @@ async fn main() -> anyhow::Result<()> { ); // ============================================================================================ - // Step 4: Holder resolve Issuer's DID, retrieve Issuer's document and validate the Credential + // Step 4: Holder resolves Issuer's DID, retrieve Issuer's document and validate the Credential // ============================================================================================ let mut resolver: Resolver = Resolver::new(); resolver.attach_iota_handler(client); - // Holder resolve Issuer DID + // Holder resolves issuer's DID let issuer: CoreDID = JptCredentialValidatorUtils::extract_issuer_from_issued_jpt(&credential_jpt).unwrap(); let issuer_document: IotaDocument = resolver.resolve(&issuer).await?; - // Holder validate the credential and retrieve the JwpIssued, needed to construct the JwpPresented + // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented let decoded_credential = JptCredentialValidator::validate::<_, Object>( &credential_jpt, &issuer_document, diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 53c15c483a..843bf10cff 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] anyhow = "1.0.62" identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } -identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021"] } +identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus"] } identity_stronghold = { path = "../identity_stronghold", default-features = false } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } json-proof-token.workspace = true diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index c56a657199..f26a448ba3 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -21,7 +21,6 @@ identity_document = { version = "=1.1.1", path = "../identity_document", default identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } -json-proof-token.workspace = true once_cell = { version = "1.18", default-features = false, features = ["std"] } reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true } roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true } @@ -33,7 +32,8 @@ serde_repr = { version = "0.1", default-features = false, optional = true } strum.workspace = true thiserror.workspace = true url = { version = "2.5", default-features = false } -zkryptium.workspace = true +zkryptium = { workspace = true, optional = true } +json-proof-token = { workspace = true, optional = true } [dev-dependencies] anyhow = "1.0.62" @@ -57,4 +57,5 @@ status-list-2021 = ["revocation-bitmap"] validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation"] domain-linkage = ["validator"] domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] -sd-jwt = ["credential", "validator", "sd-jwt-payload"] +sd-jwt = ["credential", "validator", "dep:sd-jwt-payload"] +jpt-bbs-plus = ["credential", "validator", "dep:zkryptium", "dep:json-proof-token"] diff --git a/identity_credential/src/credential/credential.rs b/identity_credential/src/credential/credential.rs index bd7860c4e3..03c482c6f6 100644 --- a/identity_credential/src/credential/credential.rs +++ b/identity_credential/src/credential/credential.rs @@ -5,6 +5,7 @@ use core::fmt::Display; use core::fmt::Formatter; use identity_core::convert::ToJson; +#[cfg(feature = "jpt-bbs-plus")] use jsonprooftoken::jpt::claims::JptClaims; use once_cell::sync::Lazy; use serde::Deserialize; @@ -177,6 +178,7 @@ impl Credential { } ///Serializes the [`Credential`] as a JPT claims set + #[cfg(feature = "jpt-bbs-plus")] pub fn serialize_jpt(&self, custom_claims: Option) -> Result where T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index 29c1e0fb86..6ce3c60b67 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; +#[cfg(feature = "jpt-bbs-plus")] use jsonprooftoken::jpt::claims::JptClaims; use serde::Deserialize; use serde::Serialize; @@ -361,6 +362,7 @@ where proof: Option>, } +#[cfg(feature = "jpt-bbs-plus")] impl<'credential, T> From> for JptClaims where T: ToOwned + Serialize, diff --git a/identity_credential/src/presentation/mod.rs b/identity_credential/src/presentation/mod.rs index 8202e03c8e..76adc145c6 100644 --- a/identity_credential/src/presentation/mod.rs +++ b/identity_credential/src/presentation/mod.rs @@ -5,17 +5,21 @@ #![allow(clippy::module_inception)] +#[cfg(feature = "jpt-bbs-plus")] mod jwp_presentation_builder; +#[cfg(feature = "jpt-bbs-plus")] mod jwp_presentation_options; mod jwt_presentation_options; mod jwt_serialization; mod presentation; mod presentation_builder; +#[cfg(feature = "jpt-bbs-plus")] pub use self::jwp_presentation_builder::SelectiveDisclosurePresentation; pub use self::jwt_presentation_options::JwtPresentationOptions; pub use self::presentation::Presentation; pub use self::presentation_builder::PresentationBuilder; +#[cfg(feature = "jpt-bbs-plus")] pub use jwp_presentation_options::JwpPresentationOptions; #[cfg(feature = "validator")] diff --git a/identity_credential/src/revocation/mod.rs b/identity_credential/src/revocation/mod.rs index f98ad3f89b..1553022c74 100644 --- a/identity_credential/src/revocation/mod.rs +++ b/identity_credential/src/revocation/mod.rs @@ -9,9 +9,11 @@ mod revocation_bitmap_2022; #[cfg(feature = "status-list-2021")] pub mod status_list_2021; +#[cfg(feature = "jpt-bbs-plus")] pub mod validity_timeframe_2024; pub use self::error::RevocationError; pub use self::error::RevocationResult; pub use revocation_bitmap_2022::*; +#[cfg(feature = "jpt-bbs-plus")] pub use validity_timeframe_2024::*; diff --git a/identity_credential/src/validator/jwt_credential_validation/error.rs b/identity_credential/src/validator/jwt_credential_validation/error.rs index 3cc6a163fa..a531f088d7 100644 --- a/identity_credential/src/validator/jwt_credential_validation/error.rs +++ b/identity_credential/src/validator/jwt_credential_validation/error.rs @@ -101,21 +101,21 @@ pub enum JwtValidationError { /// Indicates that the credential has been revoked. #[error("credential has been revoked")] Revoked, - + /// Indicates that the credential has been suspended. + #[error("credential has been suspended")] + Suspended, /// Indicates that the credential's timeframe interval is not valid + #[cfg(feature = "jpt-bbs-plus")] #[error("timeframe interval not valid")] OutsideTimeframe, - /// Indicates that the JWP representation of an issued credential or presentation could not be decoded. + #[cfg(feature = "jpt-bbs-plus")] #[error("could not decode jwp")] JwpDecodingError(#[source] jsonprooftoken::errors::CustomError), /// Indicates that the verification of the JWP has failed + #[cfg(feature = "jpt-bbs-plus")] #[error("could not verify jwp")] JwpProofVerificationError(#[source] jsonprooftoken::errors::CustomError), - - /// Indicates that the credential has been suspended. - #[error("credential has been suspended")] - Suspended, } /// Specifies whether an error is related to a credential issuer or the presentation holder. diff --git a/identity_credential/src/validator/mod.rs b/identity_credential/src/validator/mod.rs index 11434071e6..2266618ddd 100644 --- a/identity_credential/src/validator/mod.rs +++ b/identity_credential/src/validator/mod.rs @@ -3,7 +3,9 @@ //! Verifiable Credential and Presentation validators. +#[cfg(feature = "jpt-bbs-plus")] pub use self::jpt_credential_validation::*; +#[cfg(feature = "jpt-bbs-plus")] pub use self::jpt_presentation_validation::*; pub use self::jwt_credential_validation::*; pub use self::jwt_presentation_validation::*; @@ -13,7 +15,9 @@ pub use self::options::SubjectHolderRelationship; #[cfg(feature = "sd-jwt")] pub use self::sd_jwt::*; +#[cfg(feature = "jpt-bbs-plus")] mod jpt_credential_validation; +#[cfg(feature = "jpt-bbs-plus")] mod jpt_presentation_validation; mod jwt_credential_validation; mod jwt_presentation_validation; diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index bd5aa25125..ada3ad97bd 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -64,6 +64,9 @@ memstore = ["identity_storage/memstore"] # Enables selective disclosure features. sd-jwt = ["identity_credential/sd-jwt"] +# Enables zero knowledge selective disclosurable VCs +jpt-bbs-plus = ["identity_storage/jpt-bbs-plus", "identity_credential/jpt-bbs-plus"] + [package.metadata.docs.rs] # To build locally: # RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --workspace --open diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index b5743e2d75..e8bc5ec654 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -15,20 +15,20 @@ description = "Abstractions over storage for cryptographic keys used in DID Docu async-trait = { version = "0.1.64", default-features = false } futures = { version = "0.3.27", default-features = false, features = ["async-await"] } identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.1", path = "../identity_credential", default-features = false, features = ["credential", "presentation"] } +identity_credential = { version = "=1.1.1", path = "../identity_credential", default-features = false, features = ["credential", "presentation", "revocation-bitmap"] } identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } identity_iota_core = { version = "=1.1.1", path = "../identity_iota_core", default-features = false, optional = true } identity_verification = { version = "=1.1.1", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"], optional = true } -json-proof-token.workspace = true rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default_features = false } serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"], optional = true } -zkryptium.workspace = true +zkryptium = { workspace = true, optional = true } +json-proof-token = { workspace = true, optional = true } [dev-dependencies] identity_credential = { version = "=1.1.1", path = "../identity_credential", features = ["revocation-bitmap"] } @@ -44,3 +44,5 @@ memstore = ["dep:tokio", "dep:rand", "dep:iota-crypto"] send-sync-storage = [] # Implements the JwkStorageDocumentExt trait for IotaDocument iota-document = ["dep:identity_iota_core"] +# Enables JSON Proof Token & BBS+ related features +jpt-bbs-plus = ["identity_credential/jpt-bbs-plus", "dep:zkryptium", "dep:json-proof-token"] diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs index 11d27f475d..bf92959694 100644 --- a/identity_storage/src/key_storage/bls.rs +++ b/identity_storage/src/key_storage/bls.rs @@ -37,7 +37,7 @@ pub(crate) fn expand_bls_jwk(jwk: &Jwk) -> KeyStorageResult<(BBSplusSecretKey, B .with_source(err) })? .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!("invalid BBS+ secret key")) + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid BBS+ secret key".to_owned()) })?; let x: [u8; BBSplusPublicKey::COORDINATE_LEN] = jwu::decode_b64(¶ms.x) @@ -65,7 +65,7 @@ pub(crate) fn expand_bls_jwk(jwk: &Jwk) -> KeyStorageResult<(BBSplusSecretKey, B })?; let pk = BBSplusPublicKey::from_coordinates(&x, &y).map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!("invalid BBS+ public key")) + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid BBS+ public key".to_owned()) })?; Ok((sk, pk)) diff --git a/identity_storage/src/key_storage/jwk_storage.rs b/identity_storage/src/key_storage/jwk_storage.rs index 1e0fa4055a..4f1918934c 100644 --- a/identity_storage/src/key_storage/jwk_storage.rs +++ b/identity_storage/src/key_storage/jwk_storage.rs @@ -4,12 +4,9 @@ use crate::key_storage::KeyId; use crate::key_storage::KeyStorageError; use crate::key_storage::KeyType; -use crate::ProofUpdateCtx; use async_trait::async_trait; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jws::JwsAlgorithm; -use jsonprooftoken::jpa::algs::ProofAlgorithm; -use zkryptium::bbsplus::signature::BBSplusSignature; use super::jwk_gen_output::JwkGenOutput; @@ -65,30 +62,3 @@ pub trait JwkStorage: storage_sub_trait::StorageSendSyncMaybe { /// Returns `true` if the key with the given `key_id` exists in storage, `false` otherwise. async fn exists(&self, key_id: &KeyId) -> KeyStorageResult; } - -/// Extension to the JwkStorage to handle BBS+ keys -#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] -#[cfg_attr(feature = "send-sync-storage", async_trait)] -pub trait JwkStorageExt: JwkStorage { - /// Generates a JWK representing a BBS+ signature - async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult; - - /// Sign the provided `data` and `header` using the private key identified by `key_id` according to the requirements - /// of the corresponding `public_key` (see [`Jwk::alg`](Jwk::alg()) etc.). - async fn sign_bbs( - &self, - key_id: &KeyId, - data: &[Vec], - header: &[u8], - public_key: &Jwk, - ) -> KeyStorageResult>; - - /// Update proof functionality for timeframe revocation mechanism - async fn update_signature( - &self, - key_id: &KeyId, - public_key: &Jwk, - signature: &[u8; BBSplusSignature::BYTES], - ctx: ProofUpdateCtx, - ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]>; -} diff --git a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs new file mode 100644 index 0000000000..4725f43b8f --- /dev/null +++ b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs @@ -0,0 +1,38 @@ +use async_trait::async_trait; +use identity_verification::jwk::Jwk; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use zkryptium::bbsplus::signature::BBSplusSignature; + +use crate::JwkGenOutput; +use crate::JwkStorage; +use crate::KeyId; +use crate::KeyStorageResult; +use crate::KeyType; +use crate::ProofUpdateCtx; + +/// Extension to the JwkStorage to handle BBS+ keys +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait JwkStorageExt: JwkStorage { + /// Generates a JWK representing a BBS+ signature + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult; + + /// Sign the provided `data` and `header` using the private key identified by `key_id` according to the requirements + /// of the corresponding `public_key` (see [`Jwk::alg`](Jwk::alg()) etc.). + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult>; + + /// Update proof functionality for timeframe revocation mechanism + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8; BBSplusSignature::BYTES], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]>; +} diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index d264144a9a..9c49a4cf58 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -13,21 +13,11 @@ use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jwk::JwkType; use identity_verification::jose::jws::JwsAlgorithm; use identity_verification::jwk::BlsCurve; -use identity_verification::jwu; -use jsonprooftoken::jpa::algs::ProofAlgorithm; use rand::distributions::DistString; use shared::Shared; use tokio::sync::RwLockReadGuard; use tokio::sync::RwLockWriteGuard; -use zkryptium::bbsplus::keys::BBSplusSecretKey; -use zkryptium::bbsplus::signature::BBSplusSignature; -use zkryptium::keys::pair::KeyPair; -use zkryptium::schemes::algorithms::BbsBls12381Sha256; -use zkryptium::schemes::algorithms::BbsBls12381Shake256; -use zkryptium::schemes::generics::Signature; - -use super::bls::encode_bls_jwk; -use super::bls::expand_bls_jwk; + use super::ed25519::encode_jwk; use super::ed25519::expand_secret_jwk; use super::jwk_gen_output::JwkGenOutput; @@ -37,8 +27,6 @@ use super::KeyStorageErrorKind; use super::KeyStorageResult; use super::KeyType; use crate::key_storage::JwkStorage; -use crate::JwkStorageExt; -use crate::ProofUpdateCtx; /// The map from key ids to JWKs. type JwkKeyStore = HashMap; @@ -313,286 +301,316 @@ fn check_key_alg_compatibility(key_type: MemStoreKeyType, alg: JwsAlgorithm) -> } } -/// Check that the key type can be used with the algorithm. -fn check_key_proof_alg_compatibility(key_type: MemStoreKeyType, alg: ProofAlgorithm) -> KeyStorageResult<()> { - match (key_type, alg) { - (MemStoreKeyType::BLS12381G2, ProofAlgorithm::BLS12381_SHA256) => Ok(()), - (MemStoreKeyType::BLS12381G2, ProofAlgorithm::BLS12381_SHAKE256) => Ok(()), - (key_type, alg) => Err( - KeyStorageError::new(crate::key_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) - .with_custom_message(format!("`cannot use key type `{key_type}` with algorithm `{alg}`")), - ), +#[cfg(feature = "jpt-bbs-plus")] +mod bbs_plus_impl { + use std::str::FromStr as _; + + use crate::key_storage::bls::encode_bls_jwk; + use crate::key_storage::bls::expand_bls_jwk; + use crate::JwkGenOutput; + use crate::JwkMemStore; + use crate::JwkStorageExt; + use crate::KeyId; + use crate::KeyStorageError; + use crate::KeyStorageErrorKind; + use crate::KeyStorageResult; + use crate::KeyType; + use crate::ProofUpdateCtx; + use async_trait::async_trait; + use identity_verification::jwk::BlsCurve; + use identity_verification::jwk::Jwk; + use identity_verification::jwu; + use jsonprooftoken::jpa::algs::ProofAlgorithm; + use zkryptium::bbsplus::keys::BBSplusSecretKey; + use zkryptium::bbsplus::signature::BBSplusSignature; + use zkryptium::keys::pair::KeyPair; + use zkryptium::schemes::algorithms::BbsBls12381Sha256; + use zkryptium::schemes::algorithms::BbsBls12381Shake256; + use zkryptium::schemes::generics::Signature; + + use super::random_key_id; + use super::MemStoreKeyType; + + /// Check that the key type can be used with the algorithm. + fn check_key_proof_alg_compatibility(key_type: MemStoreKeyType, alg: ProofAlgorithm) -> KeyStorageResult<()> { + match (key_type, alg) { + (MemStoreKeyType::BLS12381G2, ProofAlgorithm::BLS12381_SHA256) => Ok(()), + (MemStoreKeyType::BLS12381G2, ProofAlgorithm::BLS12381_SHAKE256) => Ok(()), + (key_type, alg) => Err( + KeyStorageError::new(crate::key_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) + .with_custom_message(format!("`cannot use key type `{key_type}` with algorithm `{alg}`")), + ), + } } -} - -/// JwkStorageExt implementation for JwkMemStore -#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] -#[cfg_attr(feature = "send-sync-storage", async_trait)] -impl JwkStorageExt for JwkMemStore { - async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { - let key_type: MemStoreKeyType = MemStoreKeyType::try_from(&key_type)?; - check_key_proof_alg_compatibility(key_type, alg)?; - - let (private_key, public_key) = match key_type { - MemStoreKeyType::BLS12381G2 => match alg { - ProofAlgorithm::BLS12381_SHA256 => { - let keypair = KeyPair::::random() - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; - let sk = keypair.private_key().clone(); - let pk = keypair.public_key().clone(); - (sk, pk) - } - ProofAlgorithm::BLS12381_SHAKE256 => { - let keypair = KeyPair::::random() - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; - let sk = keypair.private_key().clone(); - let pk = keypair.public_key().clone(); - (sk, pk) - } + /// JwkStorageExt implementation for JwkMemStore + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl JwkStorageExt for JwkMemStore { + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + let key_type: MemStoreKeyType = MemStoreKeyType::try_from(&key_type)?; + + check_key_proof_alg_compatibility(key_type, alg)?; + + let (private_key, public_key) = match key_type { + MemStoreKeyType::BLS12381G2 => match alg { + ProofAlgorithm::BLS12381_SHA256 => { + let keypair = KeyPair::::random() + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; + let sk = keypair.private_key().clone(); + let pk = keypair.public_key().clone(); + (sk, pk) + } + ProofAlgorithm::BLS12381_SHAKE256 => { + let keypair = KeyPair::::random() + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; + let sk = keypair.private_key().clone(); + let pk = keypair.public_key().clone(); + (sk, pk) + } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{other} is not supported")), + ); + } + }, other => { return Err( KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) .with_custom_message(format!("{other} is not supported")), ); } - }, - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("{other} is not supported")), - ); - } - }; + }; - let kid: KeyId = random_key_id(); - - let mut jwk: Jwk = encode_bls_jwk(&private_key, &public_key); - jwk.set_alg(alg.to_string()); - jwk.set_kid(jwk.thumbprint_sha256_b64()); - let public_jwk: Jwk = jwk.to_public().expect("should only panic if kty == oct"); + let kid: KeyId = random_key_id(); - let mut jwk_store: RwLockWriteGuard<'_, JwkKeyStore> = self.jwk_store.write().await; - jwk_store.insert(kid.clone(), jwk); - - Ok(JwkGenOutput::new(kid, public_jwk)) - } + let mut jwk: Jwk = encode_bls_jwk(&private_key, &public_key); + jwk.set_alg(alg.to_string()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + let public_jwk: Jwk = jwk.to_public().expect("should only panic if kty == oct"); - async fn sign_bbs( - &self, - key_id: &KeyId, - data: &[Vec], - header: &[u8], - public_key: &Jwk, - ) -> KeyStorageResult> { - let jwk_store: RwLockReadGuard<'_, JwkKeyStore> = self.jwk_store.read().await; + let mut jwk_store = self.jwk_store.write().await; + jwk_store.insert(kid.clone(), jwk); - // Extract the required alg from the given public key - let alg = public_key - .alg() - .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .and_then(|alg_str| { - ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) - })?; + Ok(JwkGenOutput::new(kid, public_jwk)) + } - match alg { - ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256 => { - let ec_params = public_key.try_ec_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) - .with_source(err) + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult> { + let jwk_store = self.jwk_store.read().await; + + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) })?; - if ec_params.crv != BlsCurve::BLS12381G2.to_string() { + + match alg { + ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256 => { + let ec_params = public_key.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) + .with_source(err) + })?; + if ec_params.crv != BlsCurve::BLS12381G2.to_string() { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "expected Jwk with EC {} crv in order to generate the proof with {alg}", + BlsCurve::BLS12381G2 + )), + ); + } + } + other => { return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "expected Jwk with EC {} crv in order to generate the proof with {alg}", - BlsCurve::BLS12381G2 - )), + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{other} is not supported")), ); } } - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{other} is not supported")), - ); - } - } - - // Obtain the corresponding private key and sign `data`. - let jwk: &Jwk = jwk_store - .get(key_id) - .ok_or_else(|| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound))?; - - let (sk, pk) = expand_bls_jwk(jwk)?; - - let signature = match alg { - ProofAlgorithm::BLS12381_SHA256 => { - Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) - } - ProofAlgorithm::BLS12381_SHAKE256 => { - Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) - } - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{other} is not supported")), - ); - } - } - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!("signature failed")) - })?; - Ok(signature.to_vec()) - } + // Obtain the corresponding private key and sign `data`. + let jwk: &Jwk = jwk_store + .get(key_id) + .ok_or_else(|| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound))?; - async fn update_signature( - &self, - key_id: &KeyId, - public_key: &Jwk, - signature: &[u8; BBSplusSignature::BYTES], - ctx: ProofUpdateCtx, - ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { - let jwk_store: RwLockReadGuard<'_, JwkKeyStore> = self.jwk_store.read().await; - - let ProofUpdateCtx { - old_start_validity_timeframe, - new_start_validity_timeframe, - old_end_validity_timeframe, - new_end_validity_timeframe, - index_start_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - } = ctx; - - // Extract the required alg from the given public key - let alg = public_key - .alg() - .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .and_then(|alg_str| { - ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) - })?; + let (sk, pk) = expand_bls_jwk(jwk)?; - match alg { - ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256 => { - let ec_params = public_key.try_ec_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) - .with_source(err) - })?; - if ec_params.crv != BlsCurve::BLS12381G2.to_string() { + let signature = match alg { + ProofAlgorithm::BLS12381_SHA256 => { + Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) + } + ProofAlgorithm::BLS12381_SHAKE256 => { + Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) + } + other => { return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "expected Jwk with EC {} crv in order to generate the proof with {alg}", - BlsCurve::BLS12381G2 - )), + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{other} is not supported")), ); } } - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{other} is not supported")), - ); - } - } + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("signature failed".to_owned()) + })?; - // Obtain the corresponding private key and sign `data`. - let jwk: &Jwk = jwk_store - .get(key_id) - .ok_or_else(|| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound))?; + Ok(signature.to_vec()) + } - let params = jwk.try_ec_params().map_err(|_| KeyStorageErrorKind::Unspecified)?; + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8; BBSplusSignature::BYTES], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { + let jwk_store = self.jwk_store.read().await; + + let ProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } = ctx; + + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; - let sk = BBSplusSecretKey::from_bytes( - ¶ms - .d - .as_deref() - .map(jwu::decode_b64) - .ok_or_else(|| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("expected Jwk `d` param to be present") - })? - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("unable to decode `d` param") - .with_source(err) - })?, - ) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("key not valid"))?; - - let new_proof = match alg { - ProofAlgorithm::BLS12381_SHA256 => { - let signature = Signature::::from_bytes(signature) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid") - })? - .update_signature( - &sk, - &old_start_validity_timeframe, - &new_start_validity_timeframe, - index_start_validity_timeframe, - number_of_signed_messages, - ) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + match alg { + ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256 => { + let ec_params = public_key.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) + .with_source(err) })?; - - signature - .update_signature( - &sk, - &old_end_validity_timeframe, - &new_end_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - ) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") - })? - .to_bytes() + if ec_params.crv != BlsCurve::BLS12381G2.to_string() { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "expected Jwk with EC {} crv in order to generate the proof with {alg}", + BlsCurve::BLS12381G2 + )), + ); + } + } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{other} is not supported")), + ); + } } - ProofAlgorithm::BLS12381_SHAKE256 => { - let proof = Signature::::from_bytes(signature) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid") - })? - .update_signature( - &sk, - &old_start_validity_timeframe, - &new_start_validity_timeframe, - index_start_validity_timeframe, - number_of_signed_messages, - ) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") - })?; - proof - .update_signature( - &sk, - &old_end_validity_timeframe, - &new_end_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - ) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + // Obtain the corresponding private key and sign `data`. + let jwk: &Jwk = jwk_store + .get(key_id) + .ok_or_else(|| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound))?; + + let params = jwk.try_ec_params().map_err(|_| KeyStorageErrorKind::Unspecified)?; + + let sk = BBSplusSecretKey::from_bytes( + ¶ms + .d + .as_deref() + .map(jwu::decode_b64) + .ok_or_else(|| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("expected Jwk `d` param to be present") })? - .to_bytes() - } - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{other} is not supported")), - ); - } - }; + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `d` param") + .with_source(err) + })?, + ) + .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("key not valid"))?; + + let new_proof = match alg { + ProofAlgorithm::BLS12381_SHA256 => { + let signature = Signature::::from_bytes(signature) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid") + })? + .update_signature( + &sk, + &old_start_validity_timeframe, + &new_start_validity_timeframe, + index_start_validity_timeframe, + number_of_signed_messages, + ) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + })?; + + signature + .update_signature( + &sk, + &old_end_validity_timeframe, + &new_end_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + ) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + })? + .to_bytes() + } + ProofAlgorithm::BLS12381_SHAKE256 => { + let proof = Signature::::from_bytes(signature) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid") + })? + .update_signature( + &sk, + &old_start_validity_timeframe, + &new_start_validity_timeframe, + index_start_validity_timeframe, + number_of_signed_messages, + ) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + })?; + + proof + .update_signature( + &sk, + &old_end_validity_timeframe, + &new_end_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + ) + .map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") + })? + .to_bytes() + } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{other} is not supported")), + ); + } + }; - Ok(new_proof) + Ok(new_proof) + } } } - pub(crate) mod shared { use core::fmt::Debug; use core::fmt::Formatter; diff --git a/identity_storage/src/key_storage/mod.rs b/identity_storage/src/key_storage/mod.rs index 5daf049bbc..72047e70c6 100644 --- a/identity_storage/src/key_storage/mod.rs +++ b/identity_storage/src/key_storage/mod.rs @@ -6,12 +6,14 @@ //! This module provides the [`JwkStorage`] trait that //! abstracts over storages that store JSON Web Keys. -#[cfg(feature = "memstore")] +#[cfg(all(feature = "memstore", feature = "jpt-bbs-plus"))] mod bls; #[cfg(feature = "memstore")] mod ed25519; mod jwk_gen_output; mod jwk_storage; +#[cfg(feature = "jpt-bbs-plus")] +mod jwk_storage_bbs_plus_ext; mod key_id; mod key_storage_error; mod key_type; @@ -23,6 +25,8 @@ pub(crate) mod tests; pub use jwk_gen_output::*; pub use jwk_storage::*; +#[cfg(feature = "jpt-bbs-plus")] +pub use jwk_storage_bbs_plus_ext::*; pub use key_id::*; pub use key_storage_error::*; pub use key_type::*; diff --git a/identity_storage/src/storage/mod.rs b/identity_storage/src/storage/mod.rs index fe0c10a532..7643c41a95 100644 --- a/identity_storage/src/storage/mod.rs +++ b/identity_storage/src/storage/mod.rs @@ -6,8 +6,10 @@ mod error; #[macro_use] mod jwk_document_ext; +#[cfg(feature = "jpt-bbs-plus")] mod jwp_document_ext; mod signature_options; +#[cfg(feature = "jpt-bbs-plus")] mod timeframe_revocation_ext; #[cfg(all(test, feature = "memstore"))] @@ -16,8 +18,10 @@ pub(crate) mod tests; pub use error::*; pub use jwk_document_ext::*; +#[cfg(feature = "jpt-bbs-plus")] pub use jwp_document_ext::*; pub use signature_options::*; +#[cfg(feature = "jpt-bbs-plus")] pub use timeframe_revocation_ext::*; /// A type wrapping a key and key id storage, typically used with [`JwkStorage`](crate::key_storage::JwkStorage) and diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index e45a6d4eb7..b82df069b7 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -17,10 +17,14 @@ identity_storage = { version = "=1.1.1", path = "../identity_storage", default_f identity_verification = { version = "=1.1.1", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"] } iota-sdk = { version = "1.0.2", default-features = false, features = ["client", "stronghold"] } -iota_stronghold = { version = "2.0", default-features = false } +iota_stronghold = { version = "2.0.0", default-features = false } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"] } zeroize = { version = "1.6.0", default_features = false } +anyhow = "1.0.82" +zkryptium = { workspace = true, optional = true } +json-proof-token = { workspace = true, optional = true } + [dev-dependencies] identity_did = { version = "=1.1.1", path = "../identity_did", default_features = false } @@ -30,3 +34,4 @@ tokio = { version = "1.29.0", default-features = false, features = ["macros", "s default = [] # Enables `Send` + `Sync` bounds for the trait implementations on `StrongholdStorage`. send-sync-storage = ["identity_storage/send-sync-storage"] +bbs-plus = ["identity_storage/jpt-bbs-plus", "dep:zkryptium", "dep:json-proof-token"] diff --git a/identity_stronghold/src/lib.rs b/identity_stronghold/src/lib.rs index decb2c4c00..5fb8d59786 100644 --- a/identity_stronghold/src/lib.rs +++ b/identity_stronghold/src/lib.rs @@ -3,8 +3,12 @@ pub(crate) mod ed25519; mod stronghold_jwk_storage; +#[cfg(feature = "bbs-plus")] +mod stronghold_jwk_storage_ext; mod stronghold_key_id; +pub(crate) mod stronghold_key_type; #[cfg(test)] mod tests; +pub(crate) mod utils; pub use stronghold_jwk_storage::*; diff --git a/identity_stronghold/src/stronghold_jwk_storage.rs b/identity_stronghold/src/stronghold_jwk_storage.rs index 16f25584b6..24caf83813 100644 --- a/identity_stronghold/src/stronghold_jwk_storage.rs +++ b/identity_stronghold/src/stronghold_jwk_storage.rs @@ -14,7 +14,6 @@ use identity_storage::KeyType; use identity_verification::jwk::EdCurve; use identity_verification::jwk::Jwk; use identity_verification::jwk::JwkParamsOkp; -use identity_verification::jwk::JwkType; use identity_verification::jws::JwsAlgorithm; use identity_verification::jwu; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; @@ -23,24 +22,15 @@ 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::ClientError; use iota_stronghold::Location; use iota_stronghold::Stronghold; -use rand::distributions::DistString; -use std::fmt::Display; use std::str::FromStr; use std::sync::Arc; use tokio::sync::MutexGuard; use crate::ed25519; - -const ED25519_KEY_TYPE_STR: &str = "Ed25519"; -static IDENTITY_VAULT_PATH: &str = "iota_identity_vault"; -pub(crate) static IDENTITY_CLIENT_PATH: &[u8] = b"iota_identity_client"; - -/// The Ed25519 key type. -pub const ED25519_KEY_TYPE: &KeyType = &KeyType::from_static_str(ED25519_KEY_TYPE_STR); +use crate::stronghold_key_type::StrongholdKeyType; +use crate::utils::*; /// Wrapper around a [`StrongholdSecretManager`] that implements the [`KeyIdStorage`](crate::KeyIdStorage) /// and [`JwkStorage`](crate::JwkStorage) interfaces. @@ -110,6 +100,9 @@ impl JwkStorage for StrongholdStorage { let keytype: ProceduresKeyType = match key_type { StrongholdKeyType::Ed25519 => ProceduresKeyType::Ed25519, + StrongholdKeyType::BLS12381G2 => { + todo!("return an error that instruct the user to call the BBS+ flavor for this function.") + } }; let key_id: KeyId = random_key_id(); @@ -157,7 +150,7 @@ impl JwkStorage for StrongholdStorage { } async fn insert(&self, jwk: Jwk) -> KeyStorageResult { - let key_type: StrongholdKeyType = StrongholdKeyType::try_from(&jwk)?; + let key_type = StrongholdKeyType::try_from(&jwk)?; if !jwk.is_private() { return Err( KeyStorageError::new(KeyStorageErrorKind::Unspecified) @@ -290,134 +283,3 @@ impl JwkStorage for StrongholdStorage { Ok(exists) } } - -/// Generate a random alphanumeric string of len 32. -fn random_key_id() -> KeyId { - KeyId::new(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)) -} - -/// Check that the key type can be used with the algorithm. -fn check_key_alg_compatibility(key_type: StrongholdKeyType, alg: JwsAlgorithm) -> KeyStorageResult<()> { - match (key_type, alg) { - (StrongholdKeyType::Ed25519, JwsAlgorithm::EdDSA) => Ok(()), - (key_type, alg) => Err( - KeyStorageError::new(identity_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) - .with_custom_message(format!("cannot use key type `{key_type}` with algorithm `{alg}`")), - ), - } -} - -fn get_client(stronghold: &Stronghold) -> KeyStorageResult { - let client = stronghold.get_client(IDENTITY_CLIENT_PATH); - match client { - Ok(client) => Ok(client), - Err(ClientError::ClientDataNotPresent) => load_or_create_client(stronghold), - Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), - } -} - -fn load_or_create_client(stronghold: &Stronghold) -> KeyStorageResult { - match stronghold.load_client(IDENTITY_CLIENT_PATH) { - Ok(client) => Ok(client), - Err(ClientError::ClientDataNotPresent) => stronghold - .create_client(IDENTITY_CLIENT_PATH) - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), - Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), - } -} - -async fn persist_changes( - secret_manager: &StrongholdStorage, - stronghold: MutexGuard<'_, Stronghold>, -) -> KeyStorageResult<()> { - stronghold.write_client(IDENTITY_CLIENT_PATH).map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("stronghold write client error") - .with_source(err) - })?; - // Must be dropped since `write_stronghold_snapshot` needs to acquire the stronghold lock. - drop(stronghold); - - match secret_manager.as_secret_manager() { - iota_sdk::client::secret::SecretManager::Stronghold(stronghold_manager) => { - stronghold_manager - .write_stronghold_snapshot(None) - .await - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("writing to stronghold snapshot failed") - .with_source(err) - })?; - } - _ => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("secret manager is not of type stronghold"), - ) - } - }; - Ok(()) -} - -/// Key Types supported by the stronghold storage implementation. -#[derive(Debug, Copy, Clone)] -enum StrongholdKeyType { - Ed25519, -} - -impl StrongholdKeyType { - /// String representation of the key type. - const fn name(&self) -> &'static str { - match self { - StrongholdKeyType::Ed25519 => "Ed25519", - } - } -} - -impl Display for StrongholdKeyType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.name()) - } -} - -impl TryFrom<&KeyType> for StrongholdKeyType { - type Error = KeyStorageError; - - fn try_from(value: &KeyType) -> Result { - match value.as_str() { - ED25519_KEY_TYPE_STR => Ok(StrongholdKeyType::Ed25519), - _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), - } - } -} - -impl TryFrom<&Jwk> for StrongholdKeyType { - type Error = KeyStorageError; - - fn try_from(jwk: &Jwk) -> Result { - match jwk.kty() { - JwkType::Okp => { - let okp_params = jwk.try_okp_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message("expected Okp parameters for a JWK with `kty` Okp") - .with_source(err) - })?; - match okp_params.try_ed_curve().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message("only Ed curves are supported for signing") - .with_source(err) - })? { - EdCurve::Ed25519 => Ok(StrongholdKeyType::Ed25519), - curve => Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("{curve} not supported")), - ), - } - } - other => Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("Jwk `kty` {other} not supported")), - ), - } - } -} diff --git a/identity_stronghold/src/stronghold_jwk_storage_ext.rs b/identity_stronghold/src/stronghold_jwk_storage_ext.rs new file mode 100644 index 0000000000..a56e2e49c6 --- /dev/null +++ b/identity_stronghold/src/stronghold_jwk_storage_ext.rs @@ -0,0 +1,330 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). + +use anyhow::Context; +use async_trait::async_trait; +use identity_storage::key_storage::JwkStorage; +use identity_storage::JwkGenOutput; +use identity_storage::JwkStorageExt; +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_storage::KeyType; +use identity_storage::ProofUpdateCtx; +use identity_verification::jwk::BlsCurve; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkParamsEc; +use identity_verification::jwu; +use iota_stronghold::procedures::FatalProcedureError; +use iota_stronghold::procedures::Products; +use iota_stronghold::procedures::Runner as _; +use iota_stronghold::Location; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use std::str::FromStr; +use zeroize::Zeroizing; +use zkryptium::bbsplus::keys::BBSplusPublicKey; +use zkryptium::bbsplus::keys::BBSplusSecretKey; +use zkryptium::bbsplus::signature::BBSplusSignature; +use zkryptium::keys::pair::KeyPair; +use zkryptium::schemes::algorithms::BbsBls12381Sha256; +use zkryptium::schemes::algorithms::BbsBls12381Shake256; +use zkryptium::schemes::generics::Signature; + +use crate::stronghold_key_type::*; +use crate::utils::*; +use crate::StrongholdStorage; + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl JwkStorageExt for StrongholdStorage { + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + let key_type = StrongholdKeyType::try_from(&key_type)?; + + if !matches!(key_type, StrongholdKeyType::BLS12381G2) { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{key_type} is not supported")), + ); + } + + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let kid: KeyId = random_key_id(); + let target_key_location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + kid.to_string().as_bytes().to_vec(), + ); + let jwk = client + .exec_proc([], &target_key_location, |_| { + let (sk, pk) = generate_bbs_plus_key_pair(alg).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let mut jwk = encode_bls_jwk(&sk, &pk); + jwk.set_alg(alg.to_string()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + // Safety: jkw.kty can only be "ec". + let public_jwk = jwk.to_public().expect("should only panic if kty == oct"); + + Ok(Products { + output: public_jwk, + secret: Zeroizing::new(sk.to_bytes().to_vec()), + }) + }) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Failed to execute stronghold procedure") + .with_source(e) + })?; + + persist_changes(self, stronghold).await?; + + Ok(JwkGenOutput::new(kid, jwk)) + } + + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult> { + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + if matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { + let ec_params = public_key.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) + .with_source(err) + })?; + if ec_params.crv != BlsCurve::BLS12381G2.to_string() { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "expected Jwk with EC {} crv in order to generate the proof with {alg}", + BlsCurve::BLS12381G2 + )), + ); + } + } else { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{alg} is not supported")), + ); + } + + // Check `key_id` exists in store. + if !self.exists(key_id).await? { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + } + + let pk = jwk_to_bbs_plus_pk(public_key) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))?; + + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), + record_path: key_id.to_string().as_bytes().to_vec(), + }; + + client + .get_guards([sk_location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let signature_result = match alg { + ProofAlgorithm::BLS12381_SHA256 => { + Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) + } + ProofAlgorithm::BLS12381_SHAKE256 => { + Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) + } + // Safety: Already checked it's either of the two handled variants + _ => unreachable!(), + } + .map_err(|e| FatalProcedureError::from(e.to_string()))?; + Ok(signature_result) + }) + .map(|sig| sig.to_vec()) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature failed") + .with_source(e) + }) + } + + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8; BBSplusSignature::BYTES], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let ProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } = ctx; + + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + if matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { + let ec_params = public_key.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) + .with_source(err) + })?; + if ec_params.crv != BlsCurve::BLS12381G2.to_string() { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "expected Jwk with EC {} crv in order to generate the proof with {alg}", + BlsCurve::BLS12381G2 + )), + ); + } + } else { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .with_custom_message(format!("{alg} is not supported")), + ); + } + + // Check `key_id` exists in store. + if !self.exists(key_id).await? { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + } + + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), + record_path: key_id.to_string().as_bytes().to_vec(), + }; + + client + .get_guards([sk_location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + match alg { + ProofAlgorithm::BLS12381_SHA256 => Signature::::from_bytes(signature) + .and_then(|sig| { + sig.update_signature( + &sk, + &old_start_validity_timeframe, + &new_start_validity_timeframe, + index_start_validity_timeframe, + number_of_signed_messages, + ) + }) + .and_then(|sig| { + sig.update_signature( + &sk, + &old_end_validity_timeframe, + &new_end_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + ) + }) + .map_err(|e| FatalProcedureError::from(e.to_string())) + .map(|sig| sig.to_bytes()), + ProofAlgorithm::BLS12381_SHAKE256 => Signature::::from_bytes(signature) + .and_then(|sig| { + sig.update_signature( + &sk, + &old_start_validity_timeframe, + &new_start_validity_timeframe, + index_start_validity_timeframe, + number_of_signed_messages, + ) + }) + .and_then(|sig| { + sig.update_signature( + &sk, + &old_end_validity_timeframe, + &new_end_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + ) + }) + .map_err(|e| FatalProcedureError::from(e.to_string())) + .map(|sig| sig.to_bytes()), + _ => unreachable!(), + } + }) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature update failed") + .with_source(e) + }) + } +} + +fn jwk_to_bbs_plus_pk(jwk: &Jwk) -> anyhow::Result { + // Safety: only called after checking `jwk`. + let params = jwk.try_ec_params().unwrap(); + let x = jwu::decode_b64(params.x.as_bytes())? + .try_into() + .map_err(|_| anyhow::anyhow!("Invalid coordinate length"))?; + let y = jwu::decode_b64(params.y.as_bytes())? + .try_into() + .map_err(|_| anyhow::anyhow!("Invalid coordinate length"))?; + + BBSplusPublicKey::from_coordinates(&x, &y).context("Failed to create BBS+ public key with the given coordinates") +} + +fn generate_bbs_plus_key_pair(alg: ProofAlgorithm) -> KeyStorageResult<(BBSplusSecretKey, BBSplusPublicKey)> { + match alg { + ProofAlgorithm::BLS12381_SHA256 => { + let keypair = KeyPair::::random() + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; + let sk = keypair.private_key().clone(); + let pk = keypair.public_key().clone(); + + Ok((sk, pk)) + } + ProofAlgorithm::BLS12381_SHAKE256 => { + let keypair = KeyPair::::random() + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; + let sk = keypair.private_key().clone(); + let pk = keypair.public_key().clone(); + + Ok((sk, pk)) + } + other => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm).with_custom_message(format!( + "`{other}` is not supported with key type `{}`", + StrongholdKeyType::BLS12381G2 + )), + ), + } +} + +fn encode_bls_jwk(private_key: &BBSplusSecretKey, public_key: &BBSplusPublicKey) -> Jwk { + let (x, y) = public_key.to_coordinates(); + let x = jwu::encode_b64(x); + let y = jwu::encode_b64(y); + + let d = jwu::encode_b64(private_key.to_bytes()); + let mut params = JwkParamsEc::new(); + params.x = x; + params.y = y; + params.d = Some(d); + params.crv = BlsCurve::BLS12381G2.name().to_owned(); + Jwk::from_params(params) +} diff --git a/identity_stronghold/src/stronghold_key_id.rs b/identity_stronghold/src/stronghold_key_id.rs index dcd3755cab..f7b7aa6436 100644 --- a/identity_stronghold/src/stronghold_key_id.rs +++ b/identity_stronghold/src/stronghold_key_id.rs @@ -1,7 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use crate::stronghold_jwk_storage::IDENTITY_CLIENT_PATH; +use crate::utils::IDENTITY_CLIENT_PATH; use crate::StrongholdStorage; use async_trait::async_trait; use identity_storage::key_id_storage::KeyIdStorage; diff --git a/identity_stronghold/src/stronghold_key_type.rs b/identity_stronghold/src/stronghold_key_type.rs new file mode 100644 index 0000000000..df52b7c44e --- /dev/null +++ b/identity_stronghold/src/stronghold_key_type.rs @@ -0,0 +1,98 @@ +use std::fmt::Display; + +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyType; +use identity_verification::jwk::BlsCurve; +use identity_verification::jwk::EdCurve; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkType; + +/// The Ed25519 key type. +const ED25519_KEY_TYPE_STR: &str = "Ed25519"; +/// The BLS12381G2 key type +const BLS12381G2_KEY_TYPE_STR: &str = "BLS12381G2"; + +/// Key Types supported by the stronghold storage implementation. +#[derive(Debug, Copy, Clone)] +pub enum StrongholdKeyType { + Ed25519, + BLS12381G2, +} + +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, + } + } +} + +impl Display for StrongholdKeyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.name()) + } +} + +impl TryFrom<&KeyType> for StrongholdKeyType { + type Error = KeyStorageError; + + fn try_from(value: &KeyType) -> Result { + match value.as_str() { + ED25519_KEY_TYPE_STR => Ok(StrongholdKeyType::Ed25519), + BLS12381G2_KEY_TYPE_STR => Ok(StrongholdKeyType::BLS12381G2), + _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), + } + } +} + +impl TryFrom<&Jwk> for StrongholdKeyType { + type Error = KeyStorageError; + + fn try_from(jwk: &Jwk) -> Result { + match jwk.kty() { + JwkType::Okp => { + let okp_params = jwk.try_okp_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("expected Okp parameters for a JWK with `kty` Okp") + .with_source(err) + })?; + match okp_params.try_ed_curve().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("only Ed curves are supported for signing") + .with_source(err) + })? { + EdCurve::Ed25519 => Ok(StrongholdKeyType::Ed25519), + curve => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{curve} not supported")), + ), + } + } + JwkType::Ec => { + let ec_params = jwk.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .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")), + ), + } + } + other => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("Jwk `kty` {other} not supported")), + ), + } + } +} diff --git a/identity_stronghold/src/utils.rs b/identity_stronghold/src/utils.rs new file mode 100644 index 0000000000..b539963ca9 --- /dev/null +++ b/identity_stronghold/src/utils.rs @@ -0,0 +1,84 @@ +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_verification::jws::JwsAlgorithm; +use iota_stronghold::Client; +use iota_stronghold::ClientError; +use iota_stronghold::Stronghold; +use rand::distributions::DistString as _; +use tokio::sync::MutexGuard; + +use crate::stronghold_key_type::StrongholdKeyType; +use crate::StrongholdStorage; + +pub static IDENTITY_VAULT_PATH: &str = "iota_identity_vault"; +pub static IDENTITY_CLIENT_PATH: &[u8] = b"iota_identity_client"; + +/// Generate a random alphanumeric string of len 32. +pub fn random_key_id() -> KeyId { + KeyId::new(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)) +} + +/// Check that the key type can be used with the algorithm. +pub fn check_key_alg_compatibility(key_type: StrongholdKeyType, alg: JwsAlgorithm) -> KeyStorageResult<()> { + match (key_type, alg) { + (StrongholdKeyType::Ed25519, JwsAlgorithm::EdDSA) => Ok(()), + (key_type, alg) => Err( + KeyStorageError::new(identity_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) + .with_custom_message(format!("cannot use key type `{key_type}` with algorithm `{alg}`")), + ), + } +} + +pub fn get_client(stronghold: &Stronghold) -> KeyStorageResult { + let client = stronghold.get_client(IDENTITY_CLIENT_PATH); + match client { + Ok(client) => Ok(client), + Err(ClientError::ClientDataNotPresent) => load_or_create_client(stronghold), + Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), + } +} + +fn load_or_create_client(stronghold: &Stronghold) -> KeyStorageResult { + match stronghold.load_client(IDENTITY_CLIENT_PATH) { + Ok(client) => Ok(client), + Err(ClientError::ClientDataNotPresent) => stronghold + .create_client(IDENTITY_CLIENT_PATH) + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), + Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), + } +} + +pub async fn persist_changes( + secret_manager: &StrongholdStorage, + stronghold: MutexGuard<'_, Stronghold>, +) -> KeyStorageResult<()> { + stronghold.write_client(IDENTITY_CLIENT_PATH).map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("stronghold write client error") + .with_source(err) + })?; + // Must be dropped since `write_stronghold_snapshot` needs to acquire the stronghold lock. + drop(stronghold); + + match secret_manager.as_secret_manager() { + iota_sdk::client::secret::SecretManager::Stronghold(stronghold_manager) => { + stronghold_manager + .write_stronghold_snapshot(None) + .await + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("writing to stronghold snapshot failed") + .with_source(err) + })?; + } + _ => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("secret manager is not of type stronghold"), + ) + } + }; + Ok(()) +} From ae8e02268688f35fd0279a40da55903e10a42ac2 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 24 Apr 2024 14:05:56 +0200 Subject: [PATCH 12/33] rename JwkStorageExt to JwkStorageBbsPlusExt --- .../src/storage/jwk_storage_bbs_plus_ext.rs | 4 +-- .../presentation/jwp_presentation_builder.rs | 4 +-- .../revocation_timeframe_status.rs | 2 +- .../key_storage/jwk_storage_bbs_plus_ext.rs | 2 +- identity_storage/src/key_storage/memstore.rs | 6 ++-- .../src/storage/jwp_document_ext.rs | 30 +++++++++---------- .../src/storage/timeframe_revocation_ext.rs | 10 +++---- .../src/stronghold_jwk_storage_ext.rs | 4 +-- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs index c6d0a2dafd..fb2c5a322e 100644 --- a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs +++ b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs @@ -11,7 +11,7 @@ use super::WasmProofUpdateCtx; use identity_iota::storage::JwkGenOutput; use identity_iota::storage::JwkStorage; -use identity_iota::storage::JwkStorageExt; +use identity_iota::storage::JwkStorageBbsPlusExt; use identity_iota::storage::KeyId; use identity_iota::storage::KeyStorageError; use identity_iota::storage::KeyStorageErrorKind; @@ -233,7 +233,7 @@ impl WasmJwkStorage { } #[async_trait::async_trait(?Send)] -impl JwkStorageExt for WasmJwkStorage { +impl JwkStorageBbsPlusExt for WasmJwkStorage { async fn generate_bbs(&self, _key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { let (sk, pk) = match alg { ProofAlgorithm::BLS12381_SHA256 => generate_bbs_keypair::(), diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs index c1ad0c45a4..e7ba9f6859 100644 --- a/identity_credential/src/presentation/jwp_presentation_builder.rs +++ b/identity_credential/src/presentation/jwp_presentation_builder.rs @@ -49,7 +49,7 @@ impl SelectiveDisclosurePresentation { /// Selectively disclose "credentialSubject" attributes. /// # Example - /// ``` + /// ```ignore /// { /// "id": 1234, /// "name": "Alice", @@ -62,7 +62,7 @@ impl SelectiveDisclosurePresentation { /// } /// ``` /// If you want to undisclose for example the Mathematics course and the name of the degree: - /// ``` + /// ```ignore /// undisclose_subject("mainCourses[1]"); /// undisclose_subject("degree.name"); /// ``` diff --git a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs index d961746d55..b7ccea6690 100644 --- a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs +++ b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs @@ -175,7 +175,7 @@ mod tests { "startValidityTimeframe": "2024-03-19T13:57:50Z", "endValidityTimeframe": "2024-03-19T13:58:50Z", "revocationBitmapIndex": "5", - "type": "revocationBitmapIndex" + "type": "RevocationTimeframe2024" }"#; fn get_example_status() -> anyhow::Result { diff --git a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs index 4725f43b8f..423bd1b875 100644 --- a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs +++ b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs @@ -13,7 +13,7 @@ use crate::ProofUpdateCtx; /// Extension to the JwkStorage to handle BBS+ keys #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] -pub trait JwkStorageExt: JwkStorage { +pub trait JwkStorageBbsPlusExt: JwkStorage { /// Generates a JWK representing a BBS+ signature async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult; diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index 9c49a4cf58..815cb35741 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -309,7 +309,7 @@ mod bbs_plus_impl { use crate::key_storage::bls::expand_bls_jwk; use crate::JwkGenOutput; use crate::JwkMemStore; - use crate::JwkStorageExt; + use crate::JwkStorageBbsPlusExt; use crate::KeyId; use crate::KeyStorageError; use crate::KeyStorageErrorKind; @@ -343,10 +343,10 @@ mod bbs_plus_impl { } } - /// JwkStorageExt implementation for JwkMemStore + /// JwkStorageBbsPlusExt implementation for JwkMemStore #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] - impl JwkStorageExt for JwkMemStore { + impl JwkStorageBbsPlusExt for JwkMemStore { async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { let key_type: MemStoreKeyType = MemStoreKeyType::try_from(&key_type)?; diff --git a/identity_storage/src/storage/jwp_document_ext.rs b/identity_storage/src/storage/jwp_document_ext.rs index 5da1608af5..747d6e6570 100644 --- a/identity_storage/src/storage/jwp_document_ext.rs +++ b/identity_storage/src/storage/jwp_document_ext.rs @@ -2,7 +2,7 @@ use super::JwkStorageDocumentError as Error; use crate::key_id_storage::MethodDigest; use crate::try_undo_key_generation; use crate::JwkGenOutput; -use crate::JwkStorageExt; +use crate::JwkStorageBbsPlusExt; use crate::KeyIdStorage; use crate::KeyType; use crate::Storage; @@ -45,7 +45,7 @@ pub trait JwpDocumentExt { scope: MethodScope, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage; /// Compute a JWP in the Issued form representing the Verifiable Credential @@ -58,7 +58,7 @@ pub trait JwpDocumentExt { options: &JwpCredentialOptions, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage; /// Compute a JWP in the Presented form representing the presented Verifiable Credential after the Selective @@ -80,7 +80,7 @@ pub trait JwpDocumentExt { custom_claims: Option, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage, T: ToOwned + Serialize + DeserializeOwned + Sync; @@ -100,8 +100,8 @@ pub trait JwpDocumentExt { generate_method_for_document_type!( CoreDocument, ProofAlgorithm, - JwkStorageExt, - JwkStorageExt::generate_bbs, + JwkStorageBbsPlusExt, + JwkStorageBbsPlusExt::generate_bbs, generate_method_core_document ); @@ -117,7 +117,7 @@ impl JwpDocumentExt for CoreDocument { scope: MethodScope, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage, { generate_method_core_document(self, storage, key_type, alg, fragment, scope).await @@ -131,7 +131,7 @@ impl JwpDocumentExt for CoreDocument { options: &JwpCredentialOptions, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage, { // Obtain the method corresponding to the given fragment. @@ -177,7 +177,7 @@ impl JwpDocumentExt for CoreDocument { |p| p.to_bytes().map_err(|_| Error::JwpBuildingError), )?; - let signature = ::sign_bbs(storage.key_storage(), &key_id, &data, &header, jwk) + let signature = ::sign_bbs(storage.key_storage(), &key_id, &data, &header, jwk) .await .map_err(Error::KeyStorageError)?; @@ -235,7 +235,7 @@ impl JwpDocumentExt for CoreDocument { custom_claims: Option, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage, T: ToOwned + Serialize + DeserializeOwned + Sync, { @@ -273,8 +273,8 @@ mod iota_document { generate_method_for_document_type!( IotaDocument, ProofAlgorithm, - JwkStorageExt, - JwkStorageExt::generate_bbs, + JwkStorageBbsPlusExt, + JwkStorageBbsPlusExt::generate_bbs, generate_method_iota_document ); @@ -290,7 +290,7 @@ mod iota_document { scope: MethodScope, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage, { generate_method_iota_document(self, storage, key_type, alg, fragment, scope).await @@ -304,7 +304,7 @@ mod iota_document { options: &JwpCredentialOptions, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage, { self @@ -334,7 +334,7 @@ mod iota_document { custom_claims: Option, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage, T: ToOwned + Serialize + DeserializeOwned + Sync, { diff --git a/identity_storage/src/storage/timeframe_revocation_ext.rs b/identity_storage/src/storage/timeframe_revocation_ext.rs index 766eff7acc..2469e5b218 100644 --- a/identity_storage/src/storage/timeframe_revocation_ext.rs +++ b/identity_storage/src/storage/timeframe_revocation_ext.rs @@ -1,5 +1,5 @@ use super::JwkStorageDocumentError as Error; -use crate::JwkStorageExt; +use crate::JwkStorageBbsPlusExt; use crate::KeyIdStorage; use crate::MethodDigest; use crate::Storage; @@ -50,7 +50,7 @@ pub trait TimeframeRevocationExtension { credential_jwp: &mut JwpIssued, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage; } @@ -70,7 +70,7 @@ impl TimeframeRevocationExtension for CoreDocument { credential_jwp: &mut JwpIssued, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage, { // Obtain the method corresponding to the given fragment. @@ -148,7 +148,7 @@ impl TimeframeRevocationExtension for CoreDocument { }; let new_proof = - ::update_signature(storage.key_storage(), &key_id, jwk, &proof, proof_update_ctx) + ::update_signature(storage.key_storage(), &key_id, jwk, &proof, proof_update_ctx) .await .map_err(Error::KeyStorageError)?; @@ -183,7 +183,7 @@ mod iota_document { credential_jwp: &mut JwpIssued, ) -> StorageResult where - K: JwkStorageExt, + K: JwkStorageBbsPlusExt, I: KeyIdStorage, { self diff --git a/identity_stronghold/src/stronghold_jwk_storage_ext.rs b/identity_stronghold/src/stronghold_jwk_storage_ext.rs index a56e2e49c6..730226452a 100644 --- a/identity_stronghold/src/stronghold_jwk_storage_ext.rs +++ b/identity_stronghold/src/stronghold_jwk_storage_ext.rs @@ -7,7 +7,7 @@ use anyhow::Context; use async_trait::async_trait; use identity_storage::key_storage::JwkStorage; use identity_storage::JwkGenOutput; -use identity_storage::JwkStorageExt; +use identity_storage::JwkStorageBbsPlusExt; use identity_storage::KeyId; use identity_storage::KeyStorageError; use identity_storage::KeyStorageErrorKind; @@ -39,7 +39,7 @@ use crate::StrongholdStorage; #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] -impl JwkStorageExt for StrongholdStorage { +impl JwkStorageBbsPlusExt for StrongholdStorage { async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { let key_type = StrongholdKeyType::try_from(&key_type)?; From 4a152d0da407d72fa7ff47ba72ba90767898b856 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 25 Apr 2024 11:22:36 +0200 Subject: [PATCH 13/33] JwkStorageBbsPlusExt impl refactor for Stronghold, MemStore, WasmStore --- .../src/storage/jwk_storage_bbs_plus_ext.rs | 256 +--------------- examples/Cargo.toml | 2 +- identity_iota/src/lib.rs | 16 +- identity_storage/Cargo.toml | 1 + identity_storage/src/key_storage/bls.rs | 233 +++++++++++---- identity_storage/src/key_storage/memstore.rs | 278 +++--------------- identity_storage/src/key_storage/mod.rs | 28 +- identity_storage/src/lib.rs | 2 +- .../src/stronghold_jwk_storage_ext.rs | 188 +----------- 9 files changed, 270 insertions(+), 734 deletions(-) diff --git a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs index fb2c5a322e..c6d57444a0 100644 --- a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs +++ b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs @@ -9,6 +9,11 @@ use super::WasmJwkGenOutput; use super::WasmJwkStorage; use super::WasmProofUpdateCtx; +use identity_iota::storage::bls::encode_bls_jwk; +use identity_iota::storage::bls::expand_bls_jwk; +use identity_iota::storage::bls::generate_bbs_keypair; +use identity_iota::storage::bls::sign_bbs; +use identity_iota::storage::bls::update_bbs_signature; use identity_iota::storage::JwkGenOutput; use identity_iota::storage::JwkStorage; use identity_iota::storage::JwkStorageBbsPlusExt; @@ -18,169 +23,10 @@ use identity_iota::storage::KeyStorageErrorKind; use identity_iota::storage::KeyStorageResult; use identity_iota::storage::KeyType; use identity_iota::storage::ProofUpdateCtx; -use identity_iota::verification::jwk::BlsCurve; use identity_iota::verification::jwk::Jwk; -use identity_iota::verification::jwk::JwkParamsEc; -use identity_iota::verification::jwu; use jsonprooftoken::jpa::algs::ProofAlgorithm; use wasm_bindgen::prelude::*; -use zkryptium::bbsplus::ciphersuites::BbsCiphersuite; -use zkryptium::bbsplus::ciphersuites::Bls12381Sha256; -use zkryptium::bbsplus::ciphersuites::Bls12381Shake256; -use zkryptium::bbsplus::keys::BBSplusPublicKey; -use zkryptium::bbsplus::keys::BBSplusSecretKey; use zkryptium::bbsplus::signature::BBSplusSignature; -use zkryptium::keys::pair::KeyPair; -use zkryptium::schemes::algorithms::BBSplus; -use zkryptium::schemes::algorithms::BbsBls12381Sha256; -use zkryptium::schemes::algorithms::BbsBls12381Shake256; -use zkryptium::schemes::generics::Signature; - -fn generate_bbs_keypair() -> Result<(BBSplusSecretKey, BBSplusPublicKey), KeyStorageError> { - let keypair = KeyPair::>::random() - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; - let sk = keypair.private_key().clone(); - let pk = keypair.public_key().clone(); - Ok((sk, pk)) -} - -fn encode_bls_jwk(private_key: &BBSplusSecretKey, public_key: &BBSplusPublicKey) -> Jwk { - let (x, y) = public_key.to_coordinates(); - let x = jwu::encode_b64(x); - let y = jwu::encode_b64(y); - - let d = jwu::encode_b64(private_key.to_bytes()); - let mut params = JwkParamsEc::new(); - params.x = x; - params.y = y; - params.d = Some(d); - params.crv = BlsCurve::BLS12381G2.name().to_owned(); - Jwk::from_params(params) -} - -fn expand_bls_jwk(jwk: &Jwk) -> Result<(BBSplusSecretKey, BBSplusPublicKey), KeyStorageError> { - let params: &JwkParamsEc = jwk.try_ec_params().unwrap(); - - if params - .try_bls_curve() - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(err))? - != BlsCurve::BLS12381G2 - { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("expected an {} key", BlsCurve::BLS12381G2.name())), - ); - } - - let sk: BBSplusSecretKey = params - .d - .as_deref() - .map(jwu::decode_b64) - .ok_or_else(|| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("expected Jwk `d` param to be present") - })? - .map(|v| BBSplusSecretKey::from_bytes(&v)) - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("unable to decode `d` param") - .with_source(err) - })? - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid BBS+ secret key".to_owned()) - })?; - - let x: [u8; BBSplusPublicKey::COORDINATE_LEN] = jwu::decode_b64(¶ms.x) - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("unable to decode `x` param") - .with_source(err) - })? - .try_into() - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected key of length {}", BBSplusPublicKey::COORDINATE_LEN)) - })?; - - let y: [u8; BBSplusPublicKey::COORDINATE_LEN] = jwu::decode_b64(¶ms.y) - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("unable to decode `y` param") - .with_source(err) - })? - .try_into() - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected key of length {}", BBSplusPublicKey::COORDINATE_LEN)) - })?; - - let pk = BBSplusPublicKey::from_coordinates(&x, &y).map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid BBS+ public key".to_owned()) - })?; - - Ok((sk, pk)) -} - -fn update_bbs_signature( - sig: &[u8; 80], - sk: &BBSplusSecretKey, - update_ctx: &ProofUpdateCtx, -) -> Result<[u8; 80], KeyStorageError> -where - A: BbsCiphersuite, -{ - let sig = Signature::>::from_bytes(sig) - .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))?; - let ProofUpdateCtx { - old_start_validity_timeframe, - new_start_validity_timeframe, - old_end_validity_timeframe, - new_end_validity_timeframe, - index_start_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - } = update_ctx; - let half_updated = sig - .update_signature( - sk, - old_start_validity_timeframe, - new_start_validity_timeframe, - *index_start_validity_timeframe, - *number_of_signed_messages, - ) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") - })?; - half_updated - .update_signature( - sk, - old_end_validity_timeframe, - new_end_validity_timeframe, - *index_end_validity_timeframe, - *number_of_signed_messages, - ) - .map(|sig| sig.to_bytes()) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed")) -} - -fn decode_sk_jwt(jwk: &Jwk) -> Result { - let params = jwk.try_ec_params().map_err(|_| KeyStorageErrorKind::Unspecified)?; - BBSplusSecretKey::from_bytes( - ¶ms - .d - .as_deref() - .map(jwu::decode_b64) - .ok_or_else(|| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("expected Jwk `d` param to be present") - })? - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("unable to decode `d` param") - .with_source(err) - })?, - ) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("key not valid")) -} #[wasm_bindgen(js_class = JwkStorage)] impl WasmJwkStorage { @@ -235,20 +81,9 @@ impl WasmJwkStorage { #[async_trait::async_trait(?Send)] impl JwkStorageBbsPlusExt for WasmJwkStorage { async fn generate_bbs(&self, _key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { - let (sk, pk) = match alg { - ProofAlgorithm::BLS12381_SHA256 => generate_bbs_keypair::(), - ProofAlgorithm::BLS12381_SHAKE256 => generate_bbs_keypair::(), - other => Err( - KeyStorageError::new(KeyStorageErrorKind::KeyAlgorithmMismatch) - .with_custom_message(format!("cannot validate proof with {}", other)), - ), - }?; + let (sk, pk) = generate_bbs_keypair(alg)?; - let mut jwk = encode_bls_jwk(&sk, &pk); - jwk.set_alg(alg.to_string()); - jwk.set_kid(jwk.thumbprint_sha256_b64()); - - let public_jwk = jwk.to_public().expect("kty != oct"); + let (jwk, public_jwk) = encode_bls_jwk(&sk, &pk, alg); let kid = ::insert(self, jwk).await?; Ok(JwkGenOutput::new(kid, public_jwk)) @@ -267,48 +102,12 @@ impl JwkStorageBbsPlusExt for WasmJwkStorage { let alg = public_key .alg() .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .and_then(|alg_str| ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm)) - .map_err(KeyStorageError::new)?; - - if matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { - let ec_params = public_key.try_ec_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) - .with_source(err) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) })?; - if ec_params.crv != BlsCurve::BLS12381G2.to_string() { - return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "expected Jwk with EC {} crv in order to generate the proof with {alg}", - BlsCurve::BLS12381G2 - )), - ); - } - } else { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{alg} is not supported")), - ); - } + let (sk, pk) = expand_bls_jwk(&private_jwk)?; - match alg { - ProofAlgorithm::BLS12381_SHA256 => { - Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) - } - ProofAlgorithm::BLS12381_SHAKE256 => { - Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) - } - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{other} is not supported")), - ); - } - } - .map(|bytes| bytes.to_vec()) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("signature failed".to_owned()) - }) + sign_bbs(alg, data, &sk.expect("jwk was private"), &pk, header) } async fn update_signature( &self, @@ -321,37 +120,14 @@ impl JwkStorageBbsPlusExt for WasmJwkStorage { let alg = public_key .alg() .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .and_then(|alg_str| ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm)) - .map_err(KeyStorageError::new)?; - - if matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { - let ec_params = public_key.try_ec_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) - .with_source(err) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) })?; - if ec_params.crv != BlsCurve::BLS12381G2.to_string() { - return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "expected Jwk with EC {} crv in order to generate the proof with {alg}", - BlsCurve::BLS12381G2 - )), - ); - } - } else { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{alg} is not supported")), - ); - } + let Some(private_jwk) = WasmJwkStorage::_get_key(self, key_id.as_str()).map(Jwk::from) else { return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); }; - let sk = decode_sk_jwt(&private_jwk)?; - match alg { - ProofAlgorithm::BLS12381_SHA256 => update_bbs_signature::(signature, &sk, &ctx), - ProofAlgorithm::BLS12381_SHAKE256 => update_bbs_signature::(signature, &sk, &ctx), - _ => unreachable!(), - } + let sk = expand_bls_jwk(&private_jwk)?.0.expect("jwk is private"); + update_bbs_signature(alg, signature, &sk, &ctx) } } diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 843bf10cff..31e6191978 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -9,7 +9,7 @@ publish = false anyhow = "1.0.62" identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus"] } -identity_stronghold = { path = "../identity_stronghold", default-features = false } +identity_stronghold = { path = "../identity_stronghold", default-features = false, features = ["bbs-plus"] } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } json-proof-token.workspace = true primitive-types = "0.12.1" diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index 24a20359eb..bd067bd13b 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -105,7 +105,21 @@ pub mod verification { pub mod storage { //! Storage traits. - pub use identity_storage::*; + /// KeyIdStorage types and functionalities. + pub mod key_id_storage { + pub use identity_storage::key_id_storage::*; + } + /// KeyStorage types and functionalities. + pub mod key_storage { + pub use identity_storage::key_storage::public_modules::*; + } + /// Storage types and functionalities. + pub mod storage { + pub use identity_storage::storage::*; + } + pub use identity_storage::key_id_storage::*; + pub use identity_storage::key_storage::*; + pub use identity_storage::storage::*; } #[cfg(feature = "sd-jwt")] diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index e8bc5ec654..9e07433f74 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -29,6 +29,7 @@ thiserror.workspace = true tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"], optional = true } zkryptium = { workspace = true, optional = true } json-proof-token = { workspace = true, optional = true } +anyhow = "1.0.82" [dev-dependencies] identity_credential = { version = "=1.1.1", path = "../identity_credential", features = ["revocation-bitmap"] } diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs index bf92959694..7cb6cee2de 100644 --- a/identity_storage/src/key_storage/bls.rs +++ b/identity_storage/src/key_storage/bls.rs @@ -1,87 +1,190 @@ +use anyhow::Context; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jwu; use identity_verification::jwk::BlsCurve; use identity_verification::jwk::JwkParamsEc; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use zkryptium::bbsplus::ciphersuites::BbsCiphersuite; +use zkryptium::bbsplus::ciphersuites::Bls12381Sha256; +use zkryptium::bbsplus::ciphersuites::Bls12381Shake256; use zkryptium::bbsplus::keys::BBSplusPublicKey; use zkryptium::bbsplus::keys::BBSplusSecretKey; +use zkryptium::keys::pair::KeyPair; +use zkryptium::schemes::algorithms::BBSplus; +use zkryptium::schemes::generics::Signature; use crate::key_storage::KeyStorageError; use crate::key_storage::KeyStorageErrorKind; use crate::key_storage::KeyStorageResult; +use crate::ProofUpdateCtx; -pub(crate) fn expand_bls_jwk(jwk: &Jwk) -> KeyStorageResult<(BBSplusSecretKey, BBSplusPublicKey)> { - let params: &JwkParamsEc = jwk.try_ec_params().unwrap(); - - if params - .try_bls_curve() - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(err))? - != BlsCurve::BLS12381G2 - { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("expected an {} key", BlsCurve::BLS12381G2.name())), - ); +fn random_bbs_keypair() -> Result<(BBSplusSecretKey, BBSplusPublicKey), zkryptium::errors::Error> +where + S: BbsCiphersuite, +{ + let key_pair = KeyPair::>::random()?; + Ok((key_pair.private_key().clone(), key_pair.public_key().clone())) +} + +/// Generates a new BBS+ keypair using either `BLS12381-SHA256` or `BLS12381-SHAKE256`. +pub fn generate_bbs_keypair(alg: ProofAlgorithm) -> KeyStorageResult<(BBSplusSecretKey, BBSplusPublicKey)> { + match alg { + ProofAlgorithm::BLS12381_SHA256 => random_bbs_keypair::(), + ProofAlgorithm::BLS12381_SHAKE256 => random_bbs_keypair::(), + _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), } + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)) +} + +/// Encodes a private BBS+ key into JWK. +pub fn encode_bls_jwk( + private_key: &BBSplusSecretKey, + public_key: &BBSplusPublicKey, + alg: ProofAlgorithm, +) -> (Jwk, Jwk) { + let (x, y) = public_key.to_coordinates(); + let x = jwu::encode_b64(x); + let y = jwu::encode_b64(y); + + let d = jwu::encode_b64(private_key.to_bytes()); + let params = JwkParamsEc { + x, + y, + d: Some(d), + crv: BlsCurve::BLS12381G2.name().to_owned(), + }; + + let mut jwk = Jwk::from_params(params); + + jwk.set_alg(alg.to_string()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + let public_jwk = jwk.to_public().expect("kty != oct"); + + (jwk, public_jwk) +} + +/// Attempts to decode JWK into a BBS+ keypair. +pub fn expand_bls_jwk(jwk: &Jwk) -> KeyStorageResult<(Option, BBSplusPublicKey)> { + // Check the provided JWK represents a BLS12381G2 key. + let params = jwk + .try_ec_params() + .ok() + .filter(|params| params.try_bls_curve().is_ok_and(|curve| curve == BlsCurve::BLS12381G2)) + .context(format!("not a {} curve key", BlsCurve::BLS12381G2)) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(e))?; - let sk: BBSplusSecretKey = params + let sk = params .d .as_deref() - .map(jwu::decode_b64) - .ok_or_else(|| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("expected Jwk `d` param to be present") - })? - .map(|v| BBSplusSecretKey::from_bytes(&v)) - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("unable to decode `d` param") - .with_source(err) - })? - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid BBS+ secret key".to_owned()) - })?; - - let x: [u8; BBSplusPublicKey::COORDINATE_LEN] = jwu::decode_b64(¶ms.x) - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("unable to decode `x` param") - .with_source(err) - })? - .try_into() - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected key of length {}", BBSplusPublicKey::COORDINATE_LEN)) - })?; - - let y: [u8; BBSplusPublicKey::COORDINATE_LEN] = jwu::decode_b64(¶ms.y) - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("unable to decode `y` param") - .with_source(err) - })? - .try_into() - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected key of length {}", BBSplusPublicKey::COORDINATE_LEN)) - })?; - - let pk = BBSplusPublicKey::from_coordinates(&x, &y).map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid BBS+ public key".to_owned()) + .map(|d| { + jwu::decode_b64(d) + .context("`d` parameter is not base64 encoded") + .and_then(|bytes| BBSplusSecretKey::from_bytes(&bytes).context("invalid key size")) + }) + .transpose() + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))?; + + let x = jwu::decode_b64(¶ms.x) + .context("`x` parameter is not base64 encoded") + .and_then(|bytes| bytes.try_into().ok().context("invalid coordinate size")) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(e))?; + let y = jwu::decode_b64(¶ms.y) + .context("`y` parameter is not base64 encoded") + .and_then(|bytes| bytes.try_into().ok().context("invalid coordinate size")) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(e))?; + + let pk = BBSplusPublicKey::from_coordinates(&x, &y).map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_source(e) + .with_custom_message("invalid BBS+ public key".to_owned()) })?; Ok((sk, pk)) } -#[cfg(any(test, feature = "memstore"))] -pub(crate) fn encode_bls_jwk(private_key: &BBSplusSecretKey, public_key: &BBSplusPublicKey) -> Jwk { - let (x, y) = public_key.to_coordinates(); - let x = jwu::encode_b64(x); - let y = jwu::encode_b64(y); +fn _sign_bbs( + data: &[Vec], + sk: &BBSplusSecretKey, + pk: &BBSplusPublicKey, + header: &[u8], +) -> Result, zkryptium::errors::Error> +where + S: BbsCiphersuite, +{ + Signature::>::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes().to_vec()) +} - let d = jwu::encode_b64(private_key.to_bytes()); - let mut params = JwkParamsEc::new(); - params.x = x; - params.y = y; - params.d = Some(d); - params.crv = BlsCurve::BLS12381G2.name().to_owned(); - Jwk::from_params(params) +/// Signs data and header using the given keys. +pub fn sign_bbs( + alg: ProofAlgorithm, + data: &[Vec], + sk: &BBSplusSecretKey, + pk: &BBSplusPublicKey, + header: &[u8], +) -> KeyStorageResult> { + match alg { + ProofAlgorithm::BLS12381_SHA256 => _sign_bbs::(data, sk, pk, header), + ProofAlgorithm::BLS12381_SHAKE256 => _sign_bbs::(data, sk, pk, header), + _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), + } + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_source(e) + .with_custom_message("signature failed".to_owned()) + }) +} + +fn _update_bbs_signature( + sig: &[u8; 80], + sk: &BBSplusSecretKey, + update_ctx: &ProofUpdateCtx, +) -> Result<[u8; 80], zkryptium::errors::Error> +where + S: BbsCiphersuite, +{ + let sig = Signature::>::from_bytes(sig)?; + let ProofUpdateCtx { + old_start_validity_timeframe, + new_start_validity_timeframe, + old_end_validity_timeframe, + new_end_validity_timeframe, + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages, + } = update_ctx; + let half_updated = sig.update_signature( + sk, + old_start_validity_timeframe, + new_start_validity_timeframe, + *index_start_validity_timeframe, + *number_of_signed_messages, + )?; + half_updated + .update_signature( + sk, + old_end_validity_timeframe, + new_end_validity_timeframe, + *index_end_validity_timeframe, + *number_of_signed_messages, + ) + .map(|sig| sig.to_bytes()) +} + +/// Updates BBS+ signature's timeframe data. +pub fn update_bbs_signature( + alg: ProofAlgorithm, + sig: &[u8; 80], + sk: &BBSplusSecretKey, + update_ctx: &ProofUpdateCtx, +) -> Result<[u8; 80], KeyStorageError> { + match alg { + ProofAlgorithm::BLS12381_SHA256 => _update_bbs_signature::(sig, sk, update_ctx), + ProofAlgorithm::BLS12381_SHAKE256 => _update_bbs_signature::(sig, sk, update_ctx), + _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), + } + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("signature failed") + .with_source(e) + }) } diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index 815cb35741..d9a65ae818 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -206,8 +206,8 @@ impl JwkMemStore { impl MemStoreKeyType { const fn name(&self) -> &'static str { match self { - MemStoreKeyType::Ed25519 => "Ed25519", - MemStoreKeyType::BLS12381G2 => "BLS12381G2", + MemStoreKeyType::Ed25519 => JwkMemStore::ED25519_KEY_TYPE_STR, + MemStoreKeyType::BLS12381G2 => JwkMemStore::BLS12381G2_KEY_TYPE_STR, } } } @@ -307,6 +307,9 @@ mod bbs_plus_impl { use crate::key_storage::bls::encode_bls_jwk; use crate::key_storage::bls::expand_bls_jwk; + use crate::key_storage::bls::generate_bbs_keypair; + use crate::key_storage::bls::sign_bbs; + use crate::key_storage::bls::update_bbs_signature; use crate::JwkGenOutput; use crate::JwkMemStore; use crate::JwkStorageBbsPlusExt; @@ -319,77 +322,27 @@ mod bbs_plus_impl { use async_trait::async_trait; use identity_verification::jwk::BlsCurve; use identity_verification::jwk::Jwk; - use identity_verification::jwu; use jsonprooftoken::jpa::algs::ProofAlgorithm; - use zkryptium::bbsplus::keys::BBSplusSecretKey; use zkryptium::bbsplus::signature::BBSplusSignature; - use zkryptium::keys::pair::KeyPair; - use zkryptium::schemes::algorithms::BbsBls12381Sha256; - use zkryptium::schemes::algorithms::BbsBls12381Shake256; - use zkryptium::schemes::generics::Signature; use super::random_key_id; - use super::MemStoreKeyType; - - /// Check that the key type can be used with the algorithm. - fn check_key_proof_alg_compatibility(key_type: MemStoreKeyType, alg: ProofAlgorithm) -> KeyStorageResult<()> { - match (key_type, alg) { - (MemStoreKeyType::BLS12381G2, ProofAlgorithm::BLS12381_SHA256) => Ok(()), - (MemStoreKeyType::BLS12381G2, ProofAlgorithm::BLS12381_SHAKE256) => Ok(()), - (key_type, alg) => Err( - KeyStorageError::new(crate::key_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) - .with_custom_message(format!("`cannot use key type `{key_type}` with algorithm `{alg}`")), - ), - } - } /// JwkStorageBbsPlusExt implementation for JwkMemStore #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] impl JwkStorageBbsPlusExt for JwkMemStore { async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { - let key_type: MemStoreKeyType = MemStoreKeyType::try_from(&key_type)?; - - check_key_proof_alg_compatibility(key_type, alg)?; - - let (private_key, public_key) = match key_type { - MemStoreKeyType::BLS12381G2 => match alg { - ProofAlgorithm::BLS12381_SHA256 => { - let keypair = KeyPair::::random() - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; - let sk = keypair.private_key().clone(); - let pk = keypair.public_key().clone(); - (sk, pk) - } - ProofAlgorithm::BLS12381_SHAKE256 => { - let keypair = KeyPair::::random() - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; - let sk = keypair.private_key().clone(); - let pk = keypair.public_key().clone(); - (sk, pk) - } - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("{other} is not supported")), - ); - } - }, - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("{other} is not supported")), - ); - } - }; - - let kid: KeyId = random_key_id(); + if key_type != JwkMemStore::BLS12381G2_KEY_TYPE { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("unsupported key type {key_type}")), + ); + } - let mut jwk: Jwk = encode_bls_jwk(&private_key, &public_key); - jwk.set_alg(alg.to_string()); - jwk.set_kid(jwk.thumbprint_sha256_b64()); - let public_jwk: Jwk = jwk.to_public().expect("should only panic if kty == oct"); + let (private_key, public_key) = generate_bbs_keypair(alg)?; + let (jwk, public_jwk) = encode_bls_jwk(&private_key, &public_key, alg); + let kid: KeyId = random_key_id(); let mut jwk_store = self.jwk_store.write().await; jwk_store.insert(kid.clone(), jwk); @@ -408,61 +361,25 @@ mod bbs_plus_impl { // Extract the required alg from the given public key let alg = public_key .alg() - .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .and_then(|alg_str| { - ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) - })?; - - match alg { - ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256 => { - let ec_params = public_key.try_ec_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) - .with_source(err) - })?; - if ec_params.crv != BlsCurve::BLS12381G2.to_string() { - return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "expected Jwk with EC {} crv in order to generate the proof with {alg}", - BlsCurve::BLS12381G2 - )), - ); - } - } - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{other} is not supported")), - ); - } + .and_then(|alg_str| ProofAlgorithm::from_str(alg_str).ok()) + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm)?; + + // Check the provided JWK represents a BLS12381G2 key. + if !public_key + .try_ec_params() + .is_ok_and(|ec| ec.crv == BlsCurve::BLS12381G2.to_string()) + { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("expected a key from the {} curve", BlsCurve::BLS12381G2)), + ); } - // Obtain the corresponding private key and sign `data`. - let jwk: &Jwk = jwk_store - .get(key_id) - .ok_or_else(|| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound))?; - + // Obtain the corresponding private key. + let jwk: &Jwk = jwk_store.get(key_id).ok_or(KeyStorageErrorKind::KeyNotFound)?; let (sk, pk) = expand_bls_jwk(jwk)?; - let signature = match alg { - ProofAlgorithm::BLS12381_SHA256 => { - Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) - } - ProofAlgorithm::BLS12381_SHAKE256 => { - Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) - } - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{other} is not supported")), - ); - } - } - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("signature failed".to_owned()) - })?; - - Ok(signature.to_vec()) + sign_bbs(alg, data, &sk.expect("jwk is private"), &pk, header) } async fn update_signature( @@ -474,16 +391,6 @@ mod bbs_plus_impl { ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { let jwk_store = self.jwk_store.read().await; - let ProofUpdateCtx { - old_start_validity_timeframe, - new_start_validity_timeframe, - old_end_validity_timeframe, - new_end_validity_timeframe, - index_start_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - } = ctx; - // Extract the required alg from the given public key let alg = public_key .alg() @@ -492,122 +399,23 @@ mod bbs_plus_impl { ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) })?; - match alg { - ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256 => { - let ec_params = public_key.try_ec_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) - .with_source(err) - })?; - if ec_params.crv != BlsCurve::BLS12381G2.to_string() { - return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "expected Jwk with EC {} crv in order to generate the proof with {alg}", - BlsCurve::BLS12381G2 - )), - ); - } - } - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{other} is not supported")), - ); - } + // Check the provided JWK represents a BLS12381G2 key. + if !public_key + .try_ec_params() + .is_ok_and(|ec| ec.crv == BlsCurve::BLS12381G2.to_string()) + { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("expected a key from the {} curve", BlsCurve::BLS12381G2)), + ); } - // Obtain the corresponding private key and sign `data`. - let jwk: &Jwk = jwk_store - .get(key_id) - .ok_or_else(|| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound))?; - - let params = jwk.try_ec_params().map_err(|_| KeyStorageErrorKind::Unspecified)?; - - let sk = BBSplusSecretKey::from_bytes( - ¶ms - .d - .as_deref() - .map(jwu::decode_b64) - .ok_or_else(|| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("expected Jwk `d` param to be present") - })? - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("unable to decode `d` param") - .with_source(err) - })?, - ) - .map_err(|_| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("key not valid"))?; - - let new_proof = match alg { - ProofAlgorithm::BLS12381_SHA256 => { - let signature = Signature::::from_bytes(signature) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid") - })? - .update_signature( - &sk, - &old_start_validity_timeframe, - &new_start_validity_timeframe, - index_start_validity_timeframe, - number_of_signed_messages, - ) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") - })?; - - signature - .update_signature( - &sk, - &old_end_validity_timeframe, - &new_end_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - ) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") - })? - .to_bytes() - } - ProofAlgorithm::BLS12381_SHAKE256 => { - let proof = Signature::::from_bytes(signature) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature not valid") - })? - .update_signature( - &sk, - &old_start_validity_timeframe, - &new_start_validity_timeframe, - index_start_validity_timeframe, - number_of_signed_messages, - ) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") - })?; - - proof - .update_signature( - &sk, - &old_end_validity_timeframe, - &new_end_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - ) - .map_err(|_| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("Signature update failed") - })? - .to_bytes() - } - other => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{other} is not supported")), - ); - } - }; + // Obtain the corresponding private key. + let jwk = jwk_store.get(key_id).ok_or(KeyStorageErrorKind::KeyNotFound)?; + let sk = expand_bls_jwk(jwk)?.0.expect("jwk is private"); - Ok(new_proof) + // Update the signature. + update_bbs_signature(alg, signature, &sk, &ctx) } } } diff --git a/identity_storage/src/key_storage/mod.rs b/identity_storage/src/key_storage/mod.rs index 72047e70c6..f54f9d5233 100644 --- a/identity_storage/src/key_storage/mod.rs +++ b/identity_storage/src/key_storage/mod.rs @@ -6,8 +6,9 @@ //! This module provides the [`JwkStorage`] trait that //! abstracts over storages that store JSON Web Keys. -#[cfg(all(feature = "memstore", feature = "jpt-bbs-plus"))] -mod bls; +#[cfg(feature = "jpt-bbs-plus")] +/// BLS12381 utils. +pub mod bls; #[cfg(feature = "memstore")] mod ed25519; mod jwk_gen_output; @@ -23,12 +24,17 @@ mod memstore; #[cfg(test)] pub(crate) mod tests; -pub use jwk_gen_output::*; -pub use jwk_storage::*; -#[cfg(feature = "jpt-bbs-plus")] -pub use jwk_storage_bbs_plus_ext::*; -pub use key_id::*; -pub use key_storage_error::*; -pub use key_type::*; -#[cfg(feature = "memstore")] -pub use memstore::*; +/// All modules that should be made available to end-users. +pub mod public_modules { + pub use super::jwk_gen_output::*; + pub use super::jwk_storage::*; + #[cfg(feature = "jpt-bbs-plus")] + pub use super::jwk_storage_bbs_plus_ext::*; + pub use super::key_id::*; + pub use super::key_storage_error::*; + pub use super::key_type::*; + #[cfg(feature = "memstore")] + pub use super::memstore::*; +} + +pub use public_modules::*; diff --git a/identity_storage/src/lib.rs b/identity_storage/src/lib.rs index da1b0b66f4..643e1e7444 100644 --- a/identity_storage/src/lib.rs +++ b/identity_storage/src/lib.rs @@ -19,5 +19,5 @@ pub mod key_storage; pub mod storage; pub use key_id_storage::*; -pub use key_storage::*; +pub use key_storage::public_modules::*; pub use storage::*; diff --git a/identity_stronghold/src/stronghold_jwk_storage_ext.rs b/identity_stronghold/src/stronghold_jwk_storage_ext.rs index 730226452a..b27be8f4cb 100644 --- a/identity_stronghold/src/stronghold_jwk_storage_ext.rs +++ b/identity_stronghold/src/stronghold_jwk_storage_ext.rs @@ -3,8 +3,8 @@ //! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). -use anyhow::Context; use async_trait::async_trait; +use identity_storage::key_storage::bls::*; use identity_storage::key_storage::JwkStorage; use identity_storage::JwkGenOutput; use identity_storage::JwkStorageBbsPlusExt; @@ -14,10 +14,7 @@ use identity_storage::KeyStorageErrorKind; use identity_storage::KeyStorageResult; use identity_storage::KeyType; use identity_storage::ProofUpdateCtx; -use identity_verification::jwk::BlsCurve; use identity_verification::jwk::Jwk; -use identity_verification::jwk::JwkParamsEc; -use identity_verification::jwu; use iota_stronghold::procedures::FatalProcedureError; use iota_stronghold::procedures::Products; use iota_stronghold::procedures::Runner as _; @@ -25,13 +22,8 @@ use iota_stronghold::Location; use jsonprooftoken::jpa::algs::ProofAlgorithm; use std::str::FromStr; use zeroize::Zeroizing; -use zkryptium::bbsplus::keys::BBSplusPublicKey; use zkryptium::bbsplus::keys::BBSplusSecretKey; use zkryptium::bbsplus::signature::BBSplusSignature; -use zkryptium::keys::pair::KeyPair; -use zkryptium::schemes::algorithms::BbsBls12381Sha256; -use zkryptium::schemes::algorithms::BbsBls12381Shake256; -use zkryptium::schemes::generics::Signature; use crate::stronghold_key_type::*; use crate::utils::*; @@ -60,12 +52,8 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { ); let jwk = client .exec_proc([], &target_key_location, |_| { - let (sk, pk) = generate_bbs_plus_key_pair(alg).map_err(|e| FatalProcedureError::from(e.to_string()))?; - let mut jwk = encode_bls_jwk(&sk, &pk); - jwk.set_alg(alg.to_string()); - jwk.set_kid(jwk.thumbprint_sha256_b64()); - // Safety: jkw.kty can only be "ec". - let public_jwk = jwk.to_public().expect("should only panic if kty == oct"); + let (sk, pk) = generate_bbs_keypair(alg).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let public_jwk = encode_bls_jwk(&sk, &pk, alg).1; Ok(Products { output: public_jwk, @@ -101,34 +89,14 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) })?; - if matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { - let ec_params = public_key.try_ec_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) - .with_source(err) - })?; - if ec_params.crv != BlsCurve::BLS12381G2.to_string() { - return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "expected Jwk with EC {} crv in order to generate the proof with {alg}", - BlsCurve::BLS12381G2 - )), - ); - } - } else { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{alg} is not supported")), - ); - } - // Check `key_id` exists in store. if !self.exists(key_id).await? { return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); } - let pk = jwk_to_bbs_plus_pk(public_key) - .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))?; + let pk = expand_bls_jwk(public_key) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))? + .1; let sk_location = Location::Generic { vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), @@ -138,18 +106,7 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { client .get_guards([sk_location], |[sk]| { let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; - let signature_result = match alg { - ProofAlgorithm::BLS12381_SHA256 => { - Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) - } - ProofAlgorithm::BLS12381_SHAKE256 => { - Signature::::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes()) - } - // Safety: Already checked it's either of the two handled variants - _ => unreachable!(), - } - .map_err(|e| FatalProcedureError::from(e.to_string()))?; - Ok(signature_result) + sign_bbs(alg, data, &sk, &pk, header).map_err(|e| FatalProcedureError::from(e.to_string())) }) .map(|sig| sig.to_vec()) .map_err(|e| { @@ -169,16 +126,6 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { let stronghold = self.get_stronghold().await; let client = get_client(&stronghold)?; - let ProofUpdateCtx { - old_start_validity_timeframe, - new_start_validity_timeframe, - old_end_validity_timeframe, - new_end_validity_timeframe, - index_start_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - } = ctx; - // Extract the required alg from the given public key let alg = public_key .alg() @@ -187,27 +134,6 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) })?; - if matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { - let ec_params = public_key.try_ec_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message(format!("expected a Jwk with EC params in order to sign with {alg}")) - .with_source(err) - })?; - if ec_params.crv != BlsCurve::BLS12381G2.to_string() { - return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( - "expected Jwk with EC {} crv in order to generate the proof with {alg}", - BlsCurve::BLS12381G2 - )), - ); - } - } else { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .with_custom_message(format!("{alg} is not supported")), - ); - } - // Check `key_id` exists in store. if !self.exists(key_id).await? { return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); @@ -221,51 +147,7 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { client .get_guards([sk_location], |[sk]| { let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; - match alg { - ProofAlgorithm::BLS12381_SHA256 => Signature::::from_bytes(signature) - .and_then(|sig| { - sig.update_signature( - &sk, - &old_start_validity_timeframe, - &new_start_validity_timeframe, - index_start_validity_timeframe, - number_of_signed_messages, - ) - }) - .and_then(|sig| { - sig.update_signature( - &sk, - &old_end_validity_timeframe, - &new_end_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - ) - }) - .map_err(|e| FatalProcedureError::from(e.to_string())) - .map(|sig| sig.to_bytes()), - ProofAlgorithm::BLS12381_SHAKE256 => Signature::::from_bytes(signature) - .and_then(|sig| { - sig.update_signature( - &sk, - &old_start_validity_timeframe, - &new_start_validity_timeframe, - index_start_validity_timeframe, - number_of_signed_messages, - ) - }) - .and_then(|sig| { - sig.update_signature( - &sk, - &old_end_validity_timeframe, - &new_end_validity_timeframe, - index_end_validity_timeframe, - number_of_signed_messages, - ) - }) - .map_err(|e| FatalProcedureError::from(e.to_string())) - .map(|sig| sig.to_bytes()), - _ => unreachable!(), - } + update_bbs_signature(alg, signature, &sk, &ctx).map_err(|e| FatalProcedureError::from(e.to_string())) }) .map_err(|e| { KeyStorageError::new(KeyStorageErrorKind::Unspecified) @@ -274,57 +156,3 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { }) } } - -fn jwk_to_bbs_plus_pk(jwk: &Jwk) -> anyhow::Result { - // Safety: only called after checking `jwk`. - let params = jwk.try_ec_params().unwrap(); - let x = jwu::decode_b64(params.x.as_bytes())? - .try_into() - .map_err(|_| anyhow::anyhow!("Invalid coordinate length"))?; - let y = jwu::decode_b64(params.y.as_bytes())? - .try_into() - .map_err(|_| anyhow::anyhow!("Invalid coordinate length"))?; - - BBSplusPublicKey::from_coordinates(&x, &y).context("Failed to create BBS+ public key with the given coordinates") -} - -fn generate_bbs_plus_key_pair(alg: ProofAlgorithm) -> KeyStorageResult<(BBSplusSecretKey, BBSplusPublicKey)> { - match alg { - ProofAlgorithm::BLS12381_SHA256 => { - let keypair = KeyPair::::random() - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; - let sk = keypair.private_key().clone(); - let pk = keypair.public_key().clone(); - - Ok((sk, pk)) - } - ProofAlgorithm::BLS12381_SHAKE256 => { - let keypair = KeyPair::::random() - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err))?; - let sk = keypair.private_key().clone(); - let pk = keypair.public_key().clone(); - - Ok((sk, pk)) - } - other => Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedProofAlgorithm).with_custom_message(format!( - "`{other}` is not supported with key type `{}`", - StrongholdKeyType::BLS12381G2 - )), - ), - } -} - -fn encode_bls_jwk(private_key: &BBSplusSecretKey, public_key: &BBSplusPublicKey) -> Jwk { - let (x, y) = public_key.to_coordinates(); - let x = jwu::encode_b64(x); - let y = jwu::encode_b64(y); - - let d = jwu::encode_b64(private_key.to_bytes()); - let mut params = JwkParamsEc::new(); - params.x = x; - params.y = y; - params.d = Some(d); - params.crv = BlsCurve::BLS12381G2.name().to_owned(); - Jwk::from_params(params) -} From be022b0daabc634055121961dfa1f40d220806fc Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 25 Apr 2024 12:01:28 +0200 Subject: [PATCH 14/33] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 30c9bf2458fd2e202e7ace71c693e08a3bac8d9c Author: Foorack / Max Faxälv Date: Tue Apr 2 10:32:48 2024 +0200 inherit `repository` in identity_verification (#1348) commit 1e9c9a31257a0f430cb9acd22d2911e949137453 Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed Mar 27 15:35:29 2024 +0100 Release wasm-v1.2.0 (#1345) commit 84a630dbf82376d7b6abac8acedfa99acc47bd60 Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed Mar 27 15:32:19 2024 +0100 Release v1.2.0 (#1347) commit 1aba4b5fb575936dd59ac6fb6be0e29b866a5a51 Author: Eike Haß Date: Wed Mar 27 13:13:27 2024 +0100 removed dev_dep version commit 0352b840f0ef0b8f57b343151b9d6ee08c716f74 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Wed Mar 27 10:44:43 2024 +0100 Support %-encoded characters in DID method id (#1303) commit e68538f95787a73ec9ae3d8fdf0746b61c6910db Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue Mar 26 11:58:35 2024 +0100 gRPC bindings (#1264) commit e53561e3b8dabc9ec80653d21c459d8f0205ad40 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue Mar 26 11:18:14 2024 +0100 allow large result err variants (#1342) commit 4a144a36990f3318e869c9192356a7ec06f10d54 Author: Eike Haß Date: Tue Mar 19 09:51:52 2024 +0100 fix readme links (#1336) commit 0af29fc8a630c0c698bc745f9434fab69320aa74 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Mon Mar 18 17:16:57 2024 +0100 Feat/custom verification method (#1334) * Add support for arbitrary (custom) verification method data * wasm bindings * custom method type + wasm * workaround serde's issue * Update bindings/wasm/src/verification/wasm_method_data.rs Co-authored-by: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> * review comments * fmt * review comment --------- Co-authored-by: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> commit edb91501e9ec933471ea4ff9b416e19273c02082 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue Mar 12 14:45:04 2024 +0100 use latest release of sd-jwt-payload (#1333) * use latest release of sd-jwt-payload * make clippy happy commit 0794379be3c18894745e5acad09488bdb3c773c6 Author: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Wed Mar 6 14:16:00 2024 +0100 Wasm bindings for `BlockChainAccountId` verification method. (#1326) commit 59d38f77e8460c1b5da55d751eec0cb88f315d9d Author: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Wed Mar 6 10:56:23 2024 +0100 Add constructor for VerificationMethod in TS (#1321) --- .dockerignore | 3 + .github/workflows/build-and-test-grpc.yml | 41 ++ .../workflows/grpc-publish-to-dockerhub.yml | 52 +++ CHANGELOG.md | 24 +- Cargo.toml | 7 +- README.md | 7 +- bindings/grpc/Cargo.toml | 43 ++ bindings/grpc/Dockerfile | 20 + bindings/grpc/README.md | 130 ++++++ bindings/grpc/build.rs | 14 + bindings/grpc/proto/credentials.proto | 61 +++ bindings/grpc/proto/document.proto | 24 ++ bindings/grpc/proto/domain_linkage.proto | 63 +++ bindings/grpc/proto/health_check.proto | 15 + bindings/grpc/proto/sd_jwt.proto | 30 ++ bindings/grpc/proto/status_list_2021.proto | 50 +++ bindings/grpc/src/lib.rs | 7 + bindings/grpc/src/main.rs | 47 +++ bindings/grpc/src/server.rs | 33 ++ bindings/grpc/src/services/credential/jwt.rs | 85 ++++ bindings/grpc/src/services/credential/mod.rs | 16 + .../src/services/credential/revocation.rs | 161 ++++++++ .../src/services/credential/validation.rs | 135 +++++++ bindings/grpc/src/services/document.rs | 115 ++++++ bindings/grpc/src/services/domain_linkage.rs | 377 ++++++++++++++++++ bindings/grpc/src/services/health_check.rs | 36 ++ bindings/grpc/src/services/mod.rs | 26 ++ bindings/grpc/src/services/sd_jwt.rs | 164 ++++++++ .../grpc/src/services/status_list_2021.rs | 170 ++++++++ .../tests/api/credential_revocation_check.rs | 99 +++++ .../grpc/tests/api/credential_validation.rs | 151 +++++++ .../grpc/tests/api/did_document_creation.rs | 43 ++ bindings/grpc/tests/api/domain_linkage.rs | 174 ++++++++ bindings/grpc/tests/api/health_check.rs | 24 ++ bindings/grpc/tests/api/helpers.rs | 336 ++++++++++++++++ bindings/grpc/tests/api/jwt.rs | 54 +++ bindings/grpc/tests/api/main.rs | 12 + bindings/grpc/tests/api/sd_jwt_validation.rs | 165 ++++++++ bindings/grpc/tests/api/status_list_2021.rs | 94 +++++ .../.well-known/did-configuration.json | 6 + bindings/grpc/tooling/start-http-server.sh | 4 + bindings/grpc/tooling/start-rpc-server.sh | 7 + bindings/wasm/CHANGELOG.md | 12 + bindings/wasm/Cargo.toml | 2 +- bindings/wasm/docs/api-reference.md | 17 + bindings/wasm/package-lock.json | 4 +- bindings/wasm/package.json | 2 +- examples/Cargo.toml | 2 +- identity_core/Cargo.toml | 5 +- identity_credential/Cargo.toml | 12 +- .../src/credential/linked_domain_service.rs | 5 + identity_credential/src/error.rs | 2 +- .../revocation/status_list_2021/credential.rs | 52 ++- .../jwt_credential_validator_utils.rs | 2 +- identity_did/Cargo.toml | 9 +- identity_did/src/did.rs | 31 +- identity_did/src/did_url.rs | 2 +- identity_did/src/error.rs | 16 +- identity_did/src/lib.rs | 2 +- identity_document/Cargo.toml | 13 +- identity_eddsa_verifier/Cargo.toml | 7 +- identity_iota/Cargo.toml | 21 +- identity_iota/README.md | 4 +- identity_iota_core/Cargo.toml | 15 +- identity_jose/Cargo.toml | 7 +- identity_resolver/Cargo.toml | 17 +- identity_storage/Cargo.toml | 21 +- identity_stronghold/Cargo.toml | 11 +- identity_verification/Cargo.toml | 12 +- 69 files changed, 3327 insertions(+), 103 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/build-and-test-grpc.yml create mode 100644 .github/workflows/grpc-publish-to-dockerhub.yml create mode 100644 bindings/grpc/Cargo.toml create mode 100644 bindings/grpc/Dockerfile create mode 100644 bindings/grpc/README.md create mode 100644 bindings/grpc/build.rs create mode 100644 bindings/grpc/proto/credentials.proto create mode 100644 bindings/grpc/proto/document.proto create mode 100644 bindings/grpc/proto/domain_linkage.proto create mode 100644 bindings/grpc/proto/health_check.proto create mode 100644 bindings/grpc/proto/sd_jwt.proto create mode 100644 bindings/grpc/proto/status_list_2021.proto create mode 100644 bindings/grpc/src/lib.rs create mode 100644 bindings/grpc/src/main.rs create mode 100644 bindings/grpc/src/server.rs create mode 100644 bindings/grpc/src/services/credential/jwt.rs create mode 100644 bindings/grpc/src/services/credential/mod.rs create mode 100644 bindings/grpc/src/services/credential/revocation.rs create mode 100644 bindings/grpc/src/services/credential/validation.rs create mode 100644 bindings/grpc/src/services/document.rs create mode 100644 bindings/grpc/src/services/domain_linkage.rs create mode 100644 bindings/grpc/src/services/health_check.rs create mode 100644 bindings/grpc/src/services/mod.rs create mode 100644 bindings/grpc/src/services/sd_jwt.rs create mode 100644 bindings/grpc/src/services/status_list_2021.rs create mode 100644 bindings/grpc/tests/api/credential_revocation_check.rs create mode 100644 bindings/grpc/tests/api/credential_validation.rs create mode 100644 bindings/grpc/tests/api/did_document_creation.rs create mode 100644 bindings/grpc/tests/api/domain_linkage.rs create mode 100644 bindings/grpc/tests/api/health_check.rs create mode 100644 bindings/grpc/tests/api/helpers.rs create mode 100644 bindings/grpc/tests/api/jwt.rs create mode 100644 bindings/grpc/tests/api/main.rs create mode 100644 bindings/grpc/tests/api/sd_jwt_validation.rs create mode 100644 bindings/grpc/tests/api/status_list_2021.rs create mode 100644 bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json create mode 100644 bindings/grpc/tooling/start-http-server.sh create mode 100755 bindings/grpc/tooling/start-rpc-server.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..115fe4a561 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +target/ +bindings/wasm/ +bindings/grpc/target/ diff --git a/.github/workflows/build-and-test-grpc.yml b/.github/workflows/build-and-test-grpc.yml new file mode 100644 index 0000000000..80311728c8 --- /dev/null +++ b/.github/workflows/build-and-test-grpc.yml @@ -0,0 +1,41 @@ +name: Build and run grpc tests + +on: + push: + branches: + - main + pull_request: + types: [ opened, synchronize, reopened, ready_for_review ] + branches: + - main + - 'epic/**' + - 'support/**' + paths: + - '.github/workflows/build-and-test.yml' + - '.github/actions/**' + - '**.rs' + - '**.toml' + - 'bindings/grpc/**' + +jobs: + check-for-run-condition: + runs-on: ubuntu-latest + outputs: + should-run: ${{ !github.event.pull_request || github.event.pull_request.draft == false }} + steps: + - run: | + # this run step does nothing, but is needed to get the job output + + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Build Docker image + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + with: + context: . + file: bindings/grpc/Dockerfile + push: false + labels: iotaledger/identity-grpc:latest \ No newline at end of file diff --git a/.github/workflows/grpc-publish-to-dockerhub.yml b/.github/workflows/grpc-publish-to-dockerhub.yml new file mode 100644 index 0000000000..d72fe20702 --- /dev/null +++ b/.github/workflows/grpc-publish-to-dockerhub.yml @@ -0,0 +1,52 @@ +name: gRPC publish to dockerhub + +on: + workflow_dispatch: + inputs: + tag: + description: 'Tag to publish under, defaults to latest' + required: false + default: latest + branch: + description: 'Branch to run publish from' + required: true + dry-run: + description: 'Run in dry-run mode' + type: boolean + required: false + default: true + +jobs: + push_to_registry: + environment: release + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch }} + + - name: Log in to Docker Hub + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a + with: + username: ${{ secrets.IOTALEDGER_DOCKER_USERNAME }} + password: ${{ secrets.IOTALEDGER_DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671 + with: + context: . + file: bindings/grpc/Dockerfile + push: ${{ !inputs.dry-run }} + labels: iotaledger/identity-grpc:${{ inputs.tag }} + + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae + with: + username: ${{ secrets.IOTALEDGER_DOCKER_USERNAME }} + password: ${{ secrets.IOTALEDGER_DOCKER_PASSWORD }} + repository: iotaledger/identity-grpc + readme-filepath: ./bindigns/grpc/README.md + short-description: ${{ github.event.repository.description }} + diff --git a/CHANGELOG.md b/CHANGELOG.md index 94cc55d62b..979805a0d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,26 @@ # Changelog +## [v1.2.0](https://github.com/iotaledger/identity.rs/tree/v1.2.0) (2024-03-27) + +[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v1.1.1...v1.2.0) + +### Added +- Add `get_public_key` for `StrongholdStorage` [\#1311](https://github.com/iotaledger/identity.rs/pull/1311) +- Support multiple IOTA networks in the Resolver [\#1304](https://github.com/iotaledger/identity.rs/pull/1304) +- Allow setting additional controllers for `IotaDocument` [\#1314](https://github.com/iotaledger/identity.rs/pull/1314) +- use latest release of sd-jwt-payload `IotaDocument` [\#1333](https://github.com/iotaledger/identity.rs/pull/1333) +- Allow arbitrary verification methods [\#1334](https://github.com/iotaledger/identity.rs/pull/1334) + +### Patch +- Support %-encoded characters in DID method id [\#1303](https://github.com/iotaledger/identity.rs/pull/1303) + ## [v1.1.1](https://github.com/iotaledger/identity.rs/tree/v1.1.1) (2024-02-19) [Full Changelog](https://github.com/iotaledger/identity.rs/compare/v1.1.0...v1.1.1) ### Patch -- Fix compilation error caused by the roaring crate [\#1306](https://github.com/iotaledger/identity.rs/pull/1306) +- Fix compilation error caused by the `roaring` crate [\#1306](https://github.com/iotaledger/identity.rs/pull/1306) ## [v1.1.0](https://github.com/iotaledger/identity.rs/tree/v1.1.0) (2024-02-07) @@ -14,15 +28,15 @@ ### Added -- Support Selective Disclosure SD-JWT [\#1268](https://github.com/iotaledger/identity.rs/pull/1268) +- Update `sd-jwt-payload` dependency [\#1296](https://github.com/iotaledger/identity.rs/pull/1296) - Add support for StatusList2021 [\#1273](https://github.com/iotaledger/identity.rs/pull/1273) -- Update sd-jwt-payload dependency [\#1296](https://github.com/iotaledger/identity.rs/pull/1296) +- Support Selective Disclosure SD-JWT [\#1268](https://github.com/iotaledger/identity.rs/pull/1268) ### Patch -- Validate domain-linkage URL making sure they only include an origin [\#1267](https://github.com/iotaledger/identity.rs/pull/1267) -- Credentials cannot be unrevoked with StatusList2021 [\#1284](https://github.com/iotaledger/identity.rs/pull/1284) - Fix RevocationBitmap2022 encoding bug [\#1292](https://github.com/iotaledger/identity.rs/pull/1292) +- Credentials cannot be unrevoked with StatusList2021 [\#1284](https://github.com/iotaledger/identity.rs/pull/1284) +- Validate domain-linkage URL making sure they only include an origin [\#1267](https://github.com/iotaledger/identity.rs/pull/1267) ## [v1.0.0](https://github.com/iotaledger/identity.rs/tree/v1.0.0) (2023-11-02) diff --git a/Cargo.toml b/Cargo.toml index d3f8600a06..6a5f4a11a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ members = [ "examples", ] -exclude = ["bindings/wasm"] +exclude = ["bindings/wasm", "bindings/grpc"] [workspace.dependencies] serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } @@ -35,4 +35,7 @@ repository = "https://github.com/iotaledger/identity.rs" rust-version = "1.65" [patch.crates-io] -iota_stronghold = {git = "https://github.com/tensor-programming/stronghold.rs.git", branch = "feat/expose_runner"} \ No newline at end of file +iota_stronghold = {git = "https://github.com/tensor-programming/stronghold.rs.git", branch = "feat/expose_runner"} + +[workspace.lints.clippy] +result_large_err = "allow" diff --git a/README.md b/README.md index 41ba9d8519..72d0f89c2d 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentra - [Web Assembly](https://github.com/iotaledger/identity.rs/blob/HEAD/bindings/wasm/) (JavaScript/TypeScript) +## gRPC + +We provide a collection of experimental [gRPC services](https://github.com/iotaledger/identity.rs/blob/HEAD/bindings/grpc/) ## Documentation and Resources - API References: @@ -51,7 +54,7 @@ If you want to include IOTA Identity in your project, simply add it as a depende ```toml [dependencies] -identity_iota = { version = "1.1.1" } +identity_iota = { version = "1.2.0" } ``` To try out the [examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples), you can also do this: @@ -85,7 +88,7 @@ version = "1.0.0" edition = "2021" [dependencies] -identity_iota = { version = "1.1.1", features = ["memstore"] } +identity_iota = { version = "1.2.0", features = ["memstore"] } iota-sdk = { version = "1.0.2", default-features = true, features = ["tls", "client", "stronghold"] } tokio = { version = "1", features = ["full"] } anyhow = "1.0.62" diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml new file mode 100644 index 0000000000..f594dc56d4 --- /dev/null +++ b/bindings/grpc/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "identity-grpc" +version = "0.1.0" +authors = ["IOTA Stiftung"] +edition = "2021" +homepage = "https://www.iota.org" +license = "Apache-2.0" +repository = "https://github.com/iotaledger/identity.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +path = "src/lib.rs" + +[[bin]] +name = "identity-grpc" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.75" +futures = { version = "0.3" } +identity_eddsa_verifier = { path = "../../identity_eddsa_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.2", features = ["stronghold"] } +openssl = { version = "0.10", features = ["vendored"] } +prost = "0.12" +rand = "0.8.5" +serde = { version = "1.0.193", features = ["derive", "alloc"] } +serde_json = { version = "1.0.108", features = ["alloc"] } +thiserror = "1.0.50" +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +tokio-stream = { version = "0.1.14", features = ["net"] } +tonic = "0.10" +tracing = { version = "0.1.40", features = ["async-await"] } +tracing-subscriber = "0.3.18" +url = { version = "2.5", default-features = false } + +[dev-dependencies] +identity_storage = { path = "../../identity_storage", features = ["memstore"] } + +[build-dependencies] +tonic-build = "0.10" diff --git a/bindings/grpc/Dockerfile b/bindings/grpc/Dockerfile new file mode 100644 index 0000000000..b7faca7c63 --- /dev/null +++ b/bindings/grpc/Dockerfile @@ -0,0 +1,20 @@ +FROM rust:bookworm as builder + +# install protobuf +RUN apt-get update && apt-get install -y protobuf-compiler libprotobuf-dev musl-tools + +COPY . /usr/src/app/ +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 + +# get binary +COPY --from=builder /usr/src/app/bindings/grpc/target/x86_64-unknown-linux-musl/release/identity-grpc / + +# set run env +EXPOSE 50051 + +# run it +CMD ["/identity-grpc"] \ No newline at end of file diff --git a/bindings/grpc/README.md b/bindings/grpc/README.md new file mode 100644 index 0000000000..814e82a7f8 --- /dev/null +++ b/bindings/grpc/README.md @@ -0,0 +1,130 @@ +# Identity.rs gRPC Bindings +This project provides the functionalities of [Identity.rs](https://github.com/iotaledger/identity.rs) in a language-agnostic way through a [gRPC](https://grpc.io) server. + +The server can easily be run with docker using [this dockerfile](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/Dockerfile). + +## Build +Run `docker build -f bindings/grpc/Dockerfile -t iotaleger/identity-grpc .` from the project root. + +### Dockerimage env variables and volume binds +The provided docker image requires the following variables to be set in order to properly work: +- `API_ENDPOINT`: IOTA node address. +- `STRONGHOLD_PWD`: Stronghold password. +- `SNAPSHOT_PATH`: Stronghold's snapshot location. + +Make sure to provide a valid stronghold snapshot at the provided `SNAPSHOT_PATH` prefilled with all the needed key material. + +### Available services +| Service description | Service Id | Proto File | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------| +| Credential Revocation Checking | `credentials/CredentialRevocation.check` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | +| SD-JWT Validation | `sd_jwt/Verification.verify` | [sd_jwt.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/sd_jwt.proto) | +| Credential JWT creation | `credentials/Jwt.create` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | +| Credential JWT validation | `credentials/VcValidation.validate` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | +| DID Document Creation | `document/DocumentService.create` | [document.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/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/grpc-bindings/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/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) | +| Domain Linkage - validate endpoints in DID, let server fetch did-configuration | `domain_linkage/DomainLinkage.validate_did` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) | +| Domain Linkage - validate endpoints in DID, pass did-configuration to service | `domain_linkage/DomainLinkage.validate_did_against_did_configurations` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) | +| `StatusList2021Credential` creation | `status_list_2021/StatusList2021Svc.create` | [status_list_2021.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/status_list_2021.proto) | +| `StatusList2021Credential` update | `status_list_2021/StatusList2021Svc.update` | [status_list_2021.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/status_list_2021.proto) | + +## Testing + +### Domain Linkage + +#### Http server +In order to test domain linkage, you need access to a server that is reachable via HTTPS. If you already have one, you can ignore the server setup steps here and and provide the `did-configuration.json` on your server. + +1. create a folder with did configuration in it, e.g. (you can also use the template in `./tooling/domain-linkage-test-server`) + ```raw + test-server/ + └── .well-known + └── did-configuration.json + ``` + + the `did-configuration` should look like this for now: + + ```json + { + "@context": "https://identity.foundation/.well-known/did-configuration/v1", + "linked_dids": [ + "add your domain linkage credential here" + ] + } + ``` +1. start a server that will serve this folder, e.g. with a NodeJs "http-server": `http-server ./test-server/`, in this example the server should now be running on local port 8080 +1. tunnel your server's port (here 8080) to a public domain with https, e.g. with ngrok: + `ngrok http http://127.0.0.1:8080` + the output should now have a line like + `Forwarding https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app -> http://127.0.0.1:8080` + check that the https url is reachable, this will be used in the next step. You can also start ngrok with a static domain, which means you don't have to update credentials after each http server restart +1. for convenience, you can find a script to start the HTTP server, that you can adjust in `tooling/start-http-server.sh`, don't forget to insert your static domain or to remove the `--domain` parameter + +#### Domain linkage credential +1. copy the public url and insert it into [6_domain_linkage.rs](../../examples/1_advanced/6_domain_linkage.rs) as domain 1, e.g. `let domain_1: Url = Url::parse("https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app")?;` +.1 run the example with `cargo run --release --example 6_domain_linkage` + +#### GRPC server +1. grab the configuration resource from the log and replace the contents of your `did-configuration.json` with it +1. you now have a publicly reachable (sub)domain, that serves a `did-configuration` file containing a credential pointing to your DID +1. to verify this, run the server via Docker or with the following command, remember to replace the placeholders ;) `API_ENDPOINT=replace_me STRONGHOLD_PWD=replace_me SNAPSHOT_PATH=replace_me cargo run --release` +The arguments can be taken from examples, e.g. after running a `6_domain_linkage.rs`, which also logs snapshot path passed to secret manager (`let snapshot_path = random_stronghold_path(); dbg!(&snapshot_path.to_str());`), for example + - API_ENDPOINT: `"http://localhost"` + - STRONGHOLD_PWD: `"secure_password"` + - SNAPSHOT_PATH: `"/var/folders/41/s1sm86jx0xl4x435t81j81440000gn/T/test_strongholds/8o2Nyiv5ENBi7Ik3dEDq9gNzSrqeUdqi.stronghold"` +1. for convenience, you can find a script to start the GRPC server, that you can adjust in `tooling/start-rpc-server.sh`, don't forget to insert the env variables as described above + +#### Calling the endpoints +1. call the `validate_domain` endpoint with your domain, e.g with: + + ```json + { + "domain": "https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app" + } + ``` + + you should now receive a response like this: + + ```json + { + "linked_dids": [ + { + "document": "... (compact JWT domain linkage credential)", + "status": "ok" + } + ] + } + ``` + +1. to call the `validate_did` endpoint, you need a DID to check, you can find a testable in you domain linkage credential. for this just decode it (e.g. on jwt.io) and get the `iss` value, then you can submit as "did" like following + + ```json + { + "did": "did:iota:snd:0x967bf8f0c7487f61378611b6a1c6a59cb99e65b839681ee70be691b09a024ab9" + } + ``` + + you should not receive a response like this: + + ```json + { + "service": [ + { + "service_endpoint": [ + { + "valid": true, + "document": "eyJraWQiOiJkaWQ6aW90YTpzbmQ6MHg5NjdiZjhmMGM3NDg3ZjYxMzc4NjExYjZhMWM2YTU5Y2I5OWU2NWI4Mzk2ODFlZTcwYmU2OTFiMDlhMDI0YWI5IzA3QjVWRkxBa0FabkRhaC1OTnYwYUN3TzJ5ZnRzX09ZZ0YzNFNudUloMlUiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJleHAiOjE3NDE2NzgyNzUsImlzcyI6ImRpZDppb3RhOnNuZDoweDk2N2JmOGYwYzc0ODdmNjEzNzg2MTFiNmExYzZhNTljYjk5ZTY1YjgzOTY4MWVlNzBiZTY5MWIwOWEwMjRhYjkiLCJuYmYiOjE3MTAxNDIyNzUsInN1YiI6ImRpZDppb3RhOnNuZDoweDk2N2JmOGYwYzc0ODdmNjEzNzg2MTFiNmExYzZhNTljYjk5ZTY1YjgzOTY4MWVlNzBiZTY5MWIwOWEwMjRhYjkiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsib3JpZ2luIjoiaHR0cHM6Ly9ob3QtYnVsbGRvZy1wcm9mb3VuZC5uZ3Jvay1mcmVlLmFwcC8ifX19.69e7T0DbRw9Kz7eEQ96P9E5HWbEo5F1fLuMjyQN6_Oa1lwBdbfj0wLlhS1j_d8AuNmvu60lMdLVixjMZJLQ5AA" + }, + { + "valid": false, + "error": "domain linkage error: error sending request for url (https://bar.example.com/.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known" + } + ], + "id": "did:iota:snd:0x967bf8f0c7487f61378611b6a1c6a59cb99e65b839681ee70be691b09a024ab9" + } + ] + } + ``` + + Which tells us that it found a DID document with one matching service with a serviceEndpoint, that contains two domains. Out of these domains one links back to the given DID, the other domain could not be resolved. diff --git a/bindings/grpc/build.rs b/bindings/grpc/build.rs new file mode 100644 index 0000000000..c48bbdce41 --- /dev/null +++ b/bindings/grpc/build.rs @@ -0,0 +1,14 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +fn main() -> Result<(), Box> { + let proto_files = std::fs::read_dir("./proto")? + .filter_map(|entry| entry.ok().map(|e| e.path())) + .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("proto")); + + for proto in proto_files { + tonic_build::compile_protos(proto)?; + } + + Ok(()) +} diff --git a/bindings/grpc/proto/credentials.proto b/bindings/grpc/proto/credentials.proto new file mode 100644 index 0000000000..ae34c7b4b6 --- /dev/null +++ b/bindings/grpc/proto/credentials.proto @@ -0,0 +1,61 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; +package credentials; + +// -- CREDENTIALS REVOCATION --------------------------------------------- + +// The States a credential can be in. +enum RevocationStatus { + REVOKED = 0; + SUSPENDED = 1; + VALID = 2; +} + +message RevocationCheckRequest { + string type = 1; + string url = 2; + map properties = 3; +} + +message RevocationCheckResponse { + RevocationStatus status = 1; +} + +service CredentialRevocation { + // Checks whether a credential has been revoked with `RevocationBitmap2022`. + rpc check(RevocationCheckRequest) returns (RevocationCheckResponse); +} + +message JwtCreationRequest { + string credential_json = 1; + string issuer_fragment = 2; +} + +message JwtCreationResponse { + string jwt = 1; +} + +service Jwt { + // Encodes a given JSON credential into JWT, using the issuer's fragment to fetch the key from stronghold. + rpc create(JwtCreationRequest) returns (JwtCreationResponse); +} + +message VcValidationRequest { + // JWT encoded credential. + string credential_jwt = 1; + // JSON encoded `StatusList2021Credential`, used for status checking. + // If missing, status checking will be performed with `RevocationBitmap2022`. + optional string status_list_credential_json = 2; +} + +message VcValidationResponse { + // JSON encoded credential (extracted from request's JWT). + string credential_json = 1; +} + +service VcValidation { + // Performs encoding, syntax, signature, time constraints and status checking on the provided credential. + rpc validate(VcValidationRequest) returns (VcValidationResponse); +} \ No newline at end of file diff --git a/bindings/grpc/proto/document.proto b/bindings/grpc/proto/document.proto new file mode 100644 index 0000000000..d25558c243 --- /dev/null +++ b/bindings/grpc/proto/document.proto @@ -0,0 +1,24 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; +package document; + +message CreateDIDRequest { + // An IOTA's bech32 encoded address. + string bech32_address = 1; +} + +message CreateDIDResponse { + // The created DID document, encoded as JSON. + string document_json = 1; + // The stronghold's fragment for the generated document's auth method. + string fragment = 2; + // The DID of the created document. + string did = 3; +} + +service DocumentService { + /// Creates a new DID document stored on Tangle. + rpc create(CreateDIDRequest) returns (CreateDIDResponse); +} \ No newline at end of file diff --git a/bindings/grpc/proto/domain_linkage.proto b/bindings/grpc/proto/domain_linkage.proto new file mode 100644 index 0000000000..f2fe3426df --- /dev/null +++ b/bindings/grpc/proto/domain_linkage.proto @@ -0,0 +1,63 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; +package domain_linkage; + +message ValidateDomainRequest { + // domain to validate + string domain = 1; +} + +message ValidateDomainAgainstDidConfigurationRequest { + // domain to validate + string domain = 1; + // already resolved domain linkage config + string did_configuration = 2; +} + +message LinkedDidValidationStatus { + // validation succeeded or not, `error` property is added for `false` cases + bool valid = 1; + // credential from `linked_dids` as compact JWT domain linkage credential if it could be retrieved + optional string document = 2; + // an error message, that occurred when validated, omitted if valid + optional string error = 3; +} + +message ValidateDomainResponse { + // list of JWT domain linkage credential, uses the same order as the `did-configuration.json` file for domain + repeated LinkedDidValidationStatus linked_dids = 1; +} + +message LinkedDidEndpointValidationStatus { + // id of service endpoint entry + string id = 1; + // list of JWT domain linkage credential, uses the same order as the `did-configuration.json` file for domain + repeated LinkedDidValidationStatus service_endpoint = 2; +} + +message ValidateDidRequest { + // DID to validate + string did = 1; +} + +message ValidateDidAgainstDidConfigurationsRequest { + // DID to validate + string did = 1; + // already resolved domain linkage configs + repeated ValidateDomainAgainstDidConfigurationRequest did_configurations = 2; +} + +message ValidateDidResponse { + // mapping of service entries from DID with validation status for endpoint URLs + repeated LinkedDidEndpointValidationStatus service = 1; +} + +service DomainLinkage { + rpc validate_domain(ValidateDomainRequest) returns (ValidateDomainResponse); + rpc validate_domain_against_did_configuration(ValidateDomainAgainstDidConfigurationRequest) returns (ValidateDomainResponse); + + rpc validate_did(ValidateDidRequest) returns (ValidateDidResponse); + rpc validate_did_against_did_configurations(ValidateDidAgainstDidConfigurationsRequest) returns (ValidateDidResponse); +} \ No newline at end of file diff --git a/bindings/grpc/proto/health_check.proto b/bindings/grpc/proto/health_check.proto new file mode 100644 index 0000000000..0c4bee8ba5 --- /dev/null +++ b/bindings/grpc/proto/health_check.proto @@ -0,0 +1,15 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; +package health_check; + +message HealthCheckRequest {} + +message HealthCheckResponse { + string status = 1; +} + +service HealthCheck { + rpc Check(HealthCheckRequest) returns (HealthCheckResponse); +} \ No newline at end of file diff --git a/bindings/grpc/proto/sd_jwt.proto b/bindings/grpc/proto/sd_jwt.proto new file mode 100644 index 0000000000..86d6b5f7fe --- /dev/null +++ b/bindings/grpc/proto/sd_jwt.proto @@ -0,0 +1,30 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; +package sd_jwt; + +message KeyBindingOptions { + optional string nonce = 1; + optional string aud = 2; + // TODO: add JWS validation options + optional string earliest_issuance_date = 3; + optional string latest_issuance_date = 4; + string holder_did = 5; +} + +message VerificationRequest { + // SD-JWT encoded credential. + string jwt = 1; + optional KeyBindingOptions kb_options = 2; +} + +message VerificationResponse { + // JSON encoded credential, extracted from the request's SD-JWT. + string credential = 1; +} + +service Verification { + // Performs all validation steps on a SD-JWT encoded credential. + rpc verify(VerificationRequest) returns (VerificationResponse); +} \ No newline at end of file diff --git a/bindings/grpc/proto/status_list_2021.proto b/bindings/grpc/proto/status_list_2021.proto new file mode 100644 index 0000000000..f84eb738b1 --- /dev/null +++ b/bindings/grpc/proto/status_list_2021.proto @@ -0,0 +1,50 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; +package status_list_2021; + +enum Purpose { + REVOCATION = 0; + SUSPENSION = 1; +} + +message CreateRequest { + // Whether this status list will be used for revoking or suspending credentials. + Purpose purpose = 1; + // Amount of entries in the status list (a minimum of 131072 entries is required). + optional uint64 length = 2; + // The URL that identifies the credential. + optional string id = 3; + // Timestamp representing the expiration date for this credential, if it has to expire. + optional string expiration_date = 4; + // A list of credential's contexts, used to fill the credential's "@context" property. + // "https://www.w3.org/2018/credentials/v1" is provided by default. + repeated string contexts = 5; + // A list of credential's types, used to fill the credential's "type" property. + // "VerifiableCredential" is provided by default. + repeated string types = 6; + // The issuer DID URL. + string issuer = 7; +} + +message StatusListCredential { + // JSON encoded `StatusList2021Credential`. + string credential_json = 1; +} + +message UpdateRequest { + // JSON encoded `StatusList2021Credential`. + string credential_json = 1; + // Changes to apply to the status list represented as the map "entry-index -> bool value" + // where `true` means that the entry at the given index is revoked/suspended depending on + // the list's purpose. + map entries = 2; +} + +service StatusList2021Svc { + // Creates a new `StatusList2021Credential`. + rpc create(CreateRequest) returns(StatusListCredential); + // Sets the value for a list of entries in the provided `StatusList2021Credential`. + rpc update(UpdateRequest) returns(StatusListCredential); +} diff --git a/bindings/grpc/src/lib.rs b/bindings/grpc/src/lib.rs new file mode 100644 index 0000000000..d26756e597 --- /dev/null +++ b/bindings/grpc/src/lib.rs @@ -0,0 +1,7 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#![allow(clippy::blocks_in_conditions)] + +pub mod server; +pub mod services; diff --git a/bindings/grpc/src/main.rs b/bindings/grpc/src/main.rs new file mode 100644 index 0000000000..4e6e3e11fa --- /dev/null +++ b/bindings/grpc/src/main.rs @@ -0,0 +1,47 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_grpc::server::GRpcServer; +use identity_stronghold::StrongholdStorage; +use iota_sdk::client::stronghold::StrongholdAdapter; +use iota_sdk::client::Client; + +#[tokio::main] +#[tracing::instrument(err)] +async fn main() -> anyhow::Result<()> { + tracing::subscriber::set_global_default(tracing_subscriber::fmt().compact().finish()) + .expect("Failed to setup global tracing subscriber."); + + let api_endpoint = std::env::var("API_ENDPOINT")?; + + let client: Client = Client::builder() + .with_primary_node(&api_endpoint, None)? + .finish() + .await?; + let stronghold = init_stronghold()?; + + let addr = "0.0.0.0:50051".parse()?; + tracing::info!("gRPC server listening on {}", addr); + GRpcServer::new(client, stronghold).serve(addr).await?; + + Ok(()) +} + +#[tracing::instrument] +fn init_stronghold() -> anyhow::Result { + let stronghold_password = std::env::var("STRONGHOLD_PWD")?; + let snapshot_path = std::env::var("SNAPSHOT_PATH")?; + + // Check for snapshot file at specified path + let metadata = std::fs::metadata(&snapshot_path)?; + if !metadata.is_file() { + return Err(anyhow::anyhow!("No snapshot at provided path \"{}\"", &snapshot_path)); + } + + Ok( + StrongholdAdapter::builder() + .password(stronghold_password) + .build(snapshot_path) + .map(StrongholdStorage::new)?, + ) +} diff --git a/bindings/grpc/src/server.rs b/bindings/grpc/src/server.rs new file mode 100644 index 0000000000..c7fa5b527c --- /dev/null +++ b/bindings/grpc/src/server.rs @@ -0,0 +1,33 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::net::SocketAddr; + +use identity_stronghold::StrongholdStorage; +use iota_sdk::client::Client; +use tonic::transport::server::Router; +use tonic::transport::server::Server; + +use crate::services; + +#[derive(Debug)] +pub struct GRpcServer { + router: Router, + stronghold: StrongholdStorage, +} + +impl GRpcServer { + pub fn new(client: Client, stronghold: StrongholdStorage) -> Self { + let router = Server::builder().add_routes(services::routes(&client, &stronghold)); + Self { router, stronghold } + } + pub async fn serve(self, addr: SocketAddr) -> Result<(), tonic::transport::Error> { + self.router.serve(addr).await + } + pub fn into_router(self) -> Router { + self.router + } + pub fn stronghold(&self) -> StrongholdStorage { + self.stronghold.clone() + } +} diff --git a/bindings/grpc/src/services/credential/jwt.rs b/bindings/grpc/src/services/credential/jwt.rs new file mode 100644 index 0000000000..6cfb3368e6 --- /dev/null +++ b/bindings/grpc/src/services/credential/jwt.rs @@ -0,0 +1,85 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use _credentials::jwt_server::Jwt as JwtSvc; +use identity_iota::core::FromJson; +use identity_iota::core::Object; +use identity_iota::credential::Credential; +use identity_iota::iota::IotaDID; +use identity_iota::iota::IotaDocument; +use identity_iota::resolver::Resolver; +use identity_iota::storage::JwkDocumentExt; +use identity_iota::storage::JwsSignatureOptions; +use identity_iota::storage::Storage; +use identity_stronghold::StrongholdStorage; +use iota_sdk::client::Client; +use tonic::Request; +use tonic::Response; +use tonic::Status; + +use self::_credentials::jwt_server::JwtServer; +use self::_credentials::JwtCreationRequest; +use self::_credentials::JwtCreationResponse; + +mod _credentials { + tonic::include_proto!("credentials"); +} + +pub struct JwtService { + resolver: Resolver, + storage: Storage, +} + +impl JwtService { + pub fn new(client: &Client, stronghold: &StrongholdStorage) -> Self { + let mut resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + Self { + resolver, + storage: Storage::new(stronghold.clone(), stronghold.clone()), + } + } +} + +#[tonic::async_trait] +impl JwtSvc for JwtService { + #[tracing::instrument( + name = "create_jwt_credential", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn create(&self, req: Request) -> Result, Status> { + let JwtCreationRequest { + credential_json, + issuer_fragment, + } = req.into_inner(); + let credential = + Credential::::from_json(credential_json.as_str()).map_err(|e| Status::invalid_argument(e.to_string()))?; + let issuer_did = + IotaDID::parse(credential.issuer.url().as_str()).map_err(|e| Status::invalid_argument(e.to_string()))?; + let issuer_document = self + .resolver + .resolve(&issuer_did) + .await + .map_err(|e| Status::not_found(e.to_string()))?; + + let jwt = issuer_document + .create_credential_jwt( + &credential, + &self.storage, + &issuer_fragment, + &JwsSignatureOptions::default(), + None, + ) + .await + .map_err(|e| Status::internal(e.to_string()))?; + + Ok(Response::new(JwtCreationResponse { jwt: jwt.into() })) + } +} + +pub fn service(client: &Client, stronghold: &StrongholdStorage) -> JwtServer { + JwtServer::new(JwtService::new(client, stronghold)) +} diff --git a/bindings/grpc/src/services/credential/mod.rs b/bindings/grpc/src/services/credential/mod.rs new file mode 100644 index 0000000000..8d71ccacee --- /dev/null +++ b/bindings/grpc/src/services/credential/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod jwt; +pub mod revocation; +pub mod validation; + +use identity_stronghold::StrongholdStorage; +use iota_sdk::client::Client; +use tonic::transport::server::RoutesBuilder; + +pub fn init_services(routes: &mut RoutesBuilder, client: &Client, stronghold: &StrongholdStorage) { + routes.add_service(revocation::service(client)); + routes.add_service(jwt::service(client, stronghold)); + routes.add_service(validation::service(client)); +} diff --git a/bindings/grpc/src/services/credential/revocation.rs b/bindings/grpc/src/services/credential/revocation.rs new file mode 100644 index 0000000000..d637bce22e --- /dev/null +++ b/bindings/grpc/src/services/credential/revocation.rs @@ -0,0 +1,161 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use credential_verification::credential_revocation_server::CredentialRevocation; +use credential_verification::credential_revocation_server::CredentialRevocationServer; +use credential_verification::RevocationCheckRequest; +use credential_verification::RevocationCheckResponse; +use credential_verification::RevocationStatus; +use identity_iota::credential::JwtCredentialValidatorUtils; +use identity_iota::credential::JwtValidationError; +use identity_iota::credential::RevocationBitmapStatus; +use identity_iota::credential::{self}; +use identity_iota::prelude::IotaDocument; +use identity_iota::prelude::Resolver; +use iota_sdk::client::Client; +use prost::bytes::Bytes; +use serde::Deserialize; +use serde::Serialize; + +use thiserror::Error; +use tonic::Request; +use tonic::Response; +use tonic::{self}; + +mod credential_verification { + use super::RevocationCheckError; + use identity_iota::credential::RevocationBitmapStatus; + use identity_iota::credential::Status; + + tonic::include_proto!("credentials"); + + impl TryFrom for Status { + type Error = RevocationCheckError; + fn try_from(req: RevocationCheckRequest) -> Result { + use identity_iota::core::Object; + use identity_iota::core::Url; + + if req.r#type.as_str() != RevocationBitmapStatus::TYPE { + Err(Self::Error::UnknownRevocationType(req.r#type)) + } else { + let parsed_url = req + .url + .parse::() + .map_err(|_| Self::Error::InvalidRevocationUrl(req.url))?; + let properties = req + .properties + .into_iter() + .map(|(k, v)| serde_json::to_value(v).map(|v| (k, v))) + .collect::>() + .map_err(|_| Self::Error::MalformedPropertiesObject)?; + + Ok(Status { + id: parsed_url, + type_: req.r#type, + properties, + }) + } + } + } +} + +#[derive(Debug, Error, Serialize, Deserialize)] +#[serde(tag = "error_type", content = "reason")] +#[serde(rename_all = "snake_case")] +pub enum RevocationCheckError { + #[error("Unknown revocation type {0}")] + UnknownRevocationType(String), + #[error("Could not parse {0} into a valid URL")] + InvalidRevocationUrl(String), + #[error("Properties isn't a valid JSON object")] + MalformedPropertiesObject, + #[error("Invalid credential status: {0}")] + InvalidCredentialStatus(String), + #[error("Issuer's DID resolution error: {0}")] + ResolutionError(String), + #[error("Revocation map not found")] + RevocationMapNotFound, +} + +impl From for tonic::Status { + fn from(e: RevocationCheckError) -> Self { + let message = e.to_string(); + let code = match &e { + RevocationCheckError::InvalidCredentialStatus(_) + | RevocationCheckError::MalformedPropertiesObject + | RevocationCheckError::UnknownRevocationType(_) + | RevocationCheckError::InvalidRevocationUrl(_) => tonic::Code::InvalidArgument, + RevocationCheckError::ResolutionError(_) => tonic::Code::Internal, + RevocationCheckError::RevocationMapNotFound => tonic::Code::NotFound, + }; + let error_json = serde_json::to_vec(&e).unwrap_or_default(); + + tonic::Status::with_details(code, message, Bytes::from(error_json)) + } +} + +impl TryFrom for RevocationCheckError { + type Error = (); + fn try_from(value: tonic::Status) -> Result { + serde_json::from_slice(value.details()).map_err(|_| ()) + } +} + +#[derive(Debug)] +pub struct CredentialVerifier { + resolver: Resolver, +} + +impl CredentialVerifier { + pub fn new(client: &Client) -> Self { + let mut resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + Self { resolver } + } +} + +#[tonic::async_trait] +impl CredentialRevocation for CredentialVerifier { + #[tracing::instrument( + name = "credential_check", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn check( + &self, + req: Request, + ) -> Result, tonic::Status> { + let credential_revocation_status = { + let revocation_status = credential::Status::try_from(req.into_inner())?; + RevocationBitmapStatus::try_from(revocation_status) + .map_err(|e| RevocationCheckError::InvalidCredentialStatus(e.to_string()))? + }; + let issuer_did = credential_revocation_status.id().unwrap(); // Safety: already parsed as a valid URL + let issuer_doc = self + .resolver + .resolve(issuer_did.did()) + .await + .map_err(|e| RevocationCheckError::ResolutionError(e.to_string()))?; + + if let Err(e) = + JwtCredentialValidatorUtils::check_revocation_bitmap_status(&issuer_doc, credential_revocation_status) + { + match &e { + JwtValidationError::Revoked => Ok(Response::new(RevocationCheckResponse { + status: RevocationStatus::Revoked.into(), + })), + _ => Err(RevocationCheckError::RevocationMapNotFound.into()), + } + } else { + Ok(Response::new(RevocationCheckResponse { + status: RevocationStatus::Valid.into(), + })) + } + } +} + +pub fn service(client: &Client) -> CredentialRevocationServer { + CredentialRevocationServer::new(CredentialVerifier::new(client)) +} diff --git a/bindings/grpc/src/services/credential/validation.rs b/bindings/grpc/src/services/credential/validation.rs new file mode 100644 index 0000000000..fb218b727b --- /dev/null +++ b/bindings/grpc/src/services/credential/validation.rs @@ -0,0 +1,135 @@ +// 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; +use identity_iota::credential::status_list_2021::StatusList2021Credential; +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::JwtValidationError; +use identity_iota::credential::StatusCheck; +use identity_iota::iota::IotaDID; +use identity_iota::resolver; +use identity_iota::resolver::Resolver; +use iota_sdk::client::Client; + +use _credentials::vc_validation_server::VcValidation; +use _credentials::vc_validation_server::VcValidationServer; +use _credentials::VcValidationRequest; +use _credentials::VcValidationResponse; +use tonic::Code; +use tonic::Request; +use tonic::Response; +use tonic::Status; + +mod _credentials { + tonic::include_proto!("credentials"); +} + +#[derive(Debug, thiserror::Error)] +pub enum VcValidationError { + #[error(transparent)] + JwtValidationError(#[from] JwtValidationError), + #[error("DID resolution error")] + DidResolutionError(#[source] resolver::Error), + #[error("Provided an invalid StatusList2021Credential")] + InvalidStatusList2021Credential(#[source] identity_iota::core::Error), + #[error("The provided credential has been revoked")] + RevokedCredential, + #[error("The provided credential has expired")] + ExpiredCredential, + #[error("The provided credential has been suspended")] + SuspendedCredential, +} + +impl From for Status { + fn from(error: VcValidationError) -> Self { + let code = match &error { + VcValidationError::InvalidStatusList2021Credential(_) => Code::InvalidArgument, + _ => Code::Internal, + }; + + Status::new(code, error.to_string()) + } +} + +pub struct VcValidator { + resolver: Resolver, +} + +impl VcValidator { + pub fn new(client: &Client) -> Self { + let mut resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + Self { resolver } + } +} + +#[tonic::async_trait] +impl VcValidation for VcValidator { + #[tracing::instrument( + name = "validate_jwt_credential", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, +)] + async fn validate(&self, req: Request) -> Result, Status> { + let VcValidationRequest { + credential_jwt, + status_list_credential_json, + } = req.into_inner(); + let jwt = Jwt::new(credential_jwt); + let issuer_did = JwtCredentialValidatorUtils::extract_issuer_from_jwt::(&jwt) + .map_err(VcValidationError::JwtValidationError)?; + let issuer_doc = self + .resolver + .resolve(&issuer_did) + .await + .map_err(VcValidationError::DidResolutionError)?; + + let mut validation_option = JwtCredentialValidationOptions::default(); + if status_list_credential_json.is_some() { + validation_option = validation_option.status_check(StatusCheck::SkipAll); + } + + let validator = JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::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) { + JwtValidationError::Revoked => VcValidationError::RevokedCredential, + JwtValidationError::ExpirationDate | JwtValidationError::IssuanceDate => VcValidationError::ExpiredCredential, + e => VcValidationError::JwtValidationError(e), + })?; + + if let Some(status_list_json) = status_list_credential_json { + let status_list = StatusList2021Credential::from_json(&status_list_json) + .map_err(VcValidationError::InvalidStatusList2021Credential)?; + JwtCredentialValidatorUtils::check_status_with_status_list_2021( + &decoded_credential.credential, + &status_list, + StatusCheck::Strict, + ) + .map_err(|e| match e { + JwtValidationError::Revoked => VcValidationError::RevokedCredential, + JwtValidationError::Suspended => VcValidationError::SuspendedCredential, + e => VcValidationError::JwtValidationError(e), + })?; + } + + let response = Response::new(VcValidationResponse { + credential_json: decoded_credential.credential.to_json().unwrap(), + }); + + Ok(response) + } +} + +pub fn service(client: &Client) -> VcValidationServer { + VcValidationServer::new(VcValidator::new(client)) +} diff --git a/bindings/grpc/src/services/document.rs b/bindings/grpc/src/services/document.rs new file mode 100644 index 0000000000..0ed1298637 --- /dev/null +++ b/bindings/grpc/src/services/document.rs @@ -0,0 +1,115 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use _document::document_service_server::DocumentService; +use _document::document_service_server::DocumentServiceServer; +use _document::CreateDidRequest; +use _document::CreateDidResponse; +use identity_iota::core::ToJson; +use identity_iota::iota::IotaClientExt; +use identity_iota::iota::IotaDocument; +use identity_iota::iota::IotaIdentityClientExt; +use identity_iota::storage::JwkDocumentExt; +use identity_iota::storage::JwkStorageDocumentError; +use identity_iota::storage::Storage; +use identity_iota::verification::jws::JwsAlgorithm; +use identity_iota::verification::MethodScope; +use identity_stronghold::StrongholdStorage; +use identity_stronghold::ED25519_KEY_TYPE; +use iota_sdk::client::Client; +use iota_sdk::types::block::address::Address; +use std::error::Error as _; +use tonic::Code; +use tonic::Request; +use tonic::Response; +use tonic::Status; + +mod _document { + tonic::include_proto!("document"); +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("The provided address is not a valid bech32 encoded address")] + InvalidAddress, + #[error(transparent)] + IotaClientError(identity_iota::iota::Error), + #[error(transparent)] + StorageError(JwkStorageDocumentError), +} + +impl From for Status { + fn from(value: Error) -> Self { + let code = match &value { + Error::InvalidAddress => Code::InvalidArgument, + _ => Code::Internal, + }; + Status::new(code, value.to_string()) + } +} + +pub struct DocumentSvc { + storage: Storage, + client: Client, +} + +impl DocumentSvc { + pub fn new(client: &Client, stronghold: &StrongholdStorage) -> Self { + Self { + storage: Storage::new(stronghold.clone(), stronghold.clone()), + client: client.clone(), + } + } +} + +#[tonic::async_trait] +impl DocumentService for DocumentSvc { + #[tracing::instrument( + name = "create_did_document", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn create(&self, req: Request) -> Result, Status> { + let CreateDidRequest { bech32_address } = req.into_inner(); + let address = Address::try_from_bech32(&bech32_address).map_err(|_| Error::InvalidAddress)?; + let network_name = self.client.network_name().await.map_err(Error::IotaClientError)?; + + let mut document = IotaDocument::new(&network_name); + let fragment = document + .generate_method( + &self.storage, + ED25519_KEY_TYPE.clone(), + JwsAlgorithm::EdDSA, + None, + MethodScope::VerificationMethod, + ) + .await + .map_err(Error::StorageError)?; + + let alias_output = self + .client + .new_did_output(address, document, None) + .await + .map_err(Error::IotaClientError)?; + + let document = self + .client + .publish_did_output(self.storage.key_storage().as_secret_manager(), alias_output) + .await + .map_err(Error::IotaClientError) + .inspect_err(|e| tracing::error!("{:?}", e.source()))?; + let did = document.id(); + + Ok(Response::new(CreateDidResponse { + document_json: document.to_json().unwrap(), + fragment, + did: did.to_string(), + })) + } +} + +pub fn service(client: &Client, stronghold: &StrongholdStorage) -> DocumentServiceServer { + DocumentServiceServer::new(DocumentSvc::new(client, stronghold)) +} diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs new file mode 100644 index 0000000000..3c3935a413 --- /dev/null +++ b/bindings/grpc/src/services/domain_linkage.rs @@ -0,0 +1,377 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::error::Error; + +use domain_linkage::domain_linkage_server::DomainLinkage; +use domain_linkage::domain_linkage_server::DomainLinkageServer; +use domain_linkage::LinkedDidEndpointValidationStatus; +use domain_linkage::LinkedDidValidationStatus; +use domain_linkage::ValidateDidAgainstDidConfigurationsRequest; +use domain_linkage::ValidateDidRequest; +use domain_linkage::ValidateDidResponse; +use domain_linkage::ValidateDomainAgainstDidConfigurationRequest; +use domain_linkage::ValidateDomainRequest; +use domain_linkage::ValidateDomainResponse; +use futures::stream::FuturesOrdered; +use futures::TryStreamExt; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota::core::FromJson; +use identity_iota::core::Url; +use identity_iota::credential::DomainLinkageConfiguration; +use identity_iota::credential::JwtCredentialValidationOptions; +use identity_iota::credential::JwtDomainLinkageValidator; +use identity_iota::credential::LinkedDomainService; +use identity_iota::did::CoreDID; +use identity_iota::iota::IotaDID; +use identity_iota::iota::IotaDocument; +use identity_iota::resolver::Resolver; +use iota_sdk::client::Client; +use serde::Deserialize; +use serde::Serialize; +use thiserror::Error; +use tonic::Request; +use tonic::Response; +use tonic::Status; +use url::Origin; + +#[allow(clippy::module_inception)] +mod domain_linkage { + tonic::include_proto!("domain_linkage"); +} + +#[derive(Debug, Error, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "error", content = "reason")] +enum DomainLinkageError { + #[error("domain argument invalid: {0}")] + DomainParsing(String), + #[error("did configuration argument invalid: {0}")] + DidConfigurationParsing(String), + #[error("did resolving failed: {0}")] + DidResolving(String), +} + +impl From for tonic::Status { + fn from(value: DomainLinkageError) -> Self { + let code = match &value { + DomainLinkageError::DomainParsing(_) => tonic::Code::InvalidArgument, + DomainLinkageError::DidConfigurationParsing(_) => tonic::Code::InvalidArgument, + DomainLinkageError::DidResolving(_) => tonic::Code::Internal, + }; + let message = value.to_string(); + let error_json = serde_json::to_vec(&value).expect("plenty of memory!"); // ? + + tonic::Status::with_details(code, message, error_json.into()) + } +} + +/// Helper struct that allows to convert `ValidateDomainAgainstDidConfigurationRequest` input struct +/// with `String` config to a struct with `DomainLinkageService` config. +struct DomainValidationConfig { + domain: Url, + config: DomainLinkageConfiguration, +} + +impl DomainValidationConfig { + /// Parses did-configuration inputs from: + /// + /// - `validate_domain_against_did_configuration` + /// - `validate_did_against_did_configurations` + pub fn try_parse(request_config: &ValidateDomainAgainstDidConfigurationRequest) -> Result { + Ok(Self { + domain: Url::parse(&request_config.domain).map_err(|e| DomainLinkageError::DomainParsing(e.to_string()))?, + config: DomainLinkageConfiguration::from_json(&request_config.did_configuration).map_err(|err| { + DomainLinkageError::DidConfigurationParsing(format!("could not parse given DID configuration; {}", &err)) + })?, + }) + } +} + +/// Builds a validation status for a failed validation from an `Error`. +fn get_validation_failed_status(message: &str, err: &impl Error) -> LinkedDidValidationStatus { + LinkedDidValidationStatus { + valid: false, + document: None, + error: Some(format!("{}; {}", message, &err.to_string())), + } +} + +#[derive(Debug)] +pub struct DomainLinkageService { + resolver: Resolver, +} + +impl DomainLinkageService { + pub fn new(client: &Client) -> Self { + let mut resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + Self { resolver } + } + + /// Validates a DID' `LinkedDomains` service endpoints. Pre-fetched did-configurations can be passed to skip fetching + /// them on server. + /// + /// Arguments: + /// + /// * `did`: DID to validate + /// * `did_configurations`: A list of domains and their did-configuration, if omitted config will be fetched + async fn validate_did_with_optional_configurations( + &self, + did: &IotaDID, + did_configurations: Option>, + ) -> Result, DomainLinkageError> { + // fetch DID document for given DID + let did_document = self + .resolver + .resolve(did) + .await + .map_err(|e| DomainLinkageError::DidResolving(e.to_string()))?; + + let services: Vec = did_document + .service() + .iter() + .cloned() + .filter_map(|service| LinkedDomainService::try_from(service).ok()) + .collect(); + + let config_map: HashMap = match did_configurations { + Some(configurations) => configurations + .into_iter() + .map(|value| (value.domain.origin(), value.config)) + .collect::>(), + None => HashMap::new(), + }; + + // check validation for all services and endpoints in them + let mut service_futures = FuturesOrdered::new(); + for service in services { + let service_id: CoreDID = did.clone().into(); + let domains: Vec = service.domains().into(); + let local_config_map = config_map.clone(); + service_futures.push_back(async move { + let mut domain_futures = FuturesOrdered::new(); + for domain in domains { + let config = local_config_map.get(&domain.origin()).map(|value| value.to_owned()); + domain_futures.push_back(self.validate_domains_with_optional_configuration( + domain.clone(), + Some(did.clone().into()), + config, + )); + } + domain_futures + .try_collect::>>() + .await + .map(|value| LinkedDidEndpointValidationStatus { + id: service_id.to_string(), + service_endpoint: value.into_iter().flatten().collect(), + }) + }); + } + let endpoint_validation_status = service_futures + .try_collect::>() + .await?; + + Ok(endpoint_validation_status) + } + + /// Validates domain linkage for given origin. + /// + /// Arguments: + /// + /// * `domain`: An origin to validate domain linkage for + /// * `did`: A DID to restrict validation to, if omitted all DIDs from config will be validated + /// * `config`: A domain linkage configuration can be passed if already loaded, if omitted config will be fetched from + /// origin + async fn validate_domains_with_optional_configuration( + &self, + domain: Url, + did: Option, + config: Option, + ) -> Result, DomainLinkageError> { + // get domain linkage config + let domain_linkage_configuration: DomainLinkageConfiguration = if let Some(config_value) = config { + config_value + } else { + match DomainLinkageConfiguration::fetch_configuration(domain.clone()).await { + Ok(value) => value, + Err(err) => { + return Ok(vec![get_validation_failed_status( + "could not get domain linkage config", + &err, + )]); + } + } + }; + + // get issuers of `linked_dids` credentials + let linked_dids: Vec = if let Some(issuer_did) = did { + vec![issuer_did] + } else { + match domain_linkage_configuration.issuers() { + Ok(value) => value, + Err(err) => { + return Ok(vec![get_validation_failed_status( + "could not get issuers from domain linkage config credential", + &err, + )]); + } + } + }; + + // resolve all issuers + let resolved = match self.resolver.resolve_multiple(&linked_dids).await { + Ok(value) => value, + Err(err) => { + return Ok(vec![get_validation_failed_status( + "could not resolve linked DIDs from domain linkage config", + &err, + )]); + } + }; + + // check linked DIDs separately + let errors: Vec> = resolved + .values() + .map(|issuer_did_doc| { + JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default()) + .validate_linkage( + &issuer_did_doc, + &domain_linkage_configuration, + &domain.clone(), + &JwtCredentialValidationOptions::default(), + ) + .err() + .map(|err| err.to_string()) + }) + .collect(); + + // collect resolved documents and their validation status into array following the order of `linked_dids` + let status_infos = domain_linkage_configuration + .linked_dids() + .iter() + .zip(errors.iter()) + .map(|(credential, error)| LinkedDidValidationStatus { + valid: error.is_none(), + document: Some(credential.as_str().to_string()), + error: error.clone(), + }) + .collect(); + + Ok(status_infos) + } +} + +#[tonic::async_trait] +impl DomainLinkage for DomainLinkageService { + #[tracing::instrument( + name = "validate_domain", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn validate_domain( + &self, + req: Request, + ) -> Result, Status> { + let request_data = &req.into_inner(); + // parse given domain + let domain: Url = + Url::parse(&request_data.domain).map_err(|err| DomainLinkageError::DomainParsing(err.to_string()))?; + + // get validation status for all issuer dids + let status_infos = self + .validate_domains_with_optional_configuration(domain, None, None) + .await?; + + Ok(Response::new(ValidateDomainResponse { + linked_dids: status_infos, + })) + } + + #[tracing::instrument( + name = "validate_domain_against_did_configuration", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn validate_domain_against_did_configuration( + &self, + req: Request, + ) -> Result, Status> { + let request_data = &req.into_inner(); + // parse given domain + let domain: Url = + Url::parse(&request_data.domain).map_err(|err| DomainLinkageError::DomainParsing(err.to_string()))?; + // parse config + let config = DomainLinkageConfiguration::from_json(&request_data.did_configuration.to_string()).map_err(|err| { + DomainLinkageError::DidConfigurationParsing(format!("could not parse given DID configuration; {}", &err)) + })?; + + // get validation status for all issuer dids + let status_infos = self + .validate_domains_with_optional_configuration(domain, None, Some(config)) + .await?; + + Ok(Response::new(ValidateDomainResponse { + linked_dids: status_infos, + })) + } + + #[tracing::instrument( + name = "validate_did", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn validate_did(&self, req: Request) -> Result, Status> { + // fetch DID document for given DID + let did: IotaDID = IotaDID::parse(req.into_inner().did).map_err(|e| Status::internal(e.to_string()))?; + + let endpoint_validation_status = self.validate_did_with_optional_configurations(&did, None).await?; + + let response = ValidateDidResponse { + service: endpoint_validation_status, + }; + + Ok(Response::new(response)) + } + + #[tracing::instrument( + name = "validate_did_against_did_configurations", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn validate_did_against_did_configurations( + &self, + req: Request, + ) -> Result, Status> { + let request_data = &req.into_inner(); + let did: IotaDID = IotaDID::parse(&request_data.did).map_err(|e| Status::internal(e.to_string()))?; + let did_configurations = request_data + .did_configurations + .iter() + .map(DomainValidationConfig::try_parse) + .collect::, DomainLinkageError>>()?; + + let endpoint_validation_status = self + .validate_did_with_optional_configurations(&did, Some(did_configurations)) + .await?; + + let response = ValidateDidResponse { + service: endpoint_validation_status, + }; + + Ok(Response::new(response)) + } +} + +pub fn service(client: &Client) -> DomainLinkageServer { + DomainLinkageServer::new(DomainLinkageService::new(client)) +} diff --git a/bindings/grpc/src/services/health_check.rs b/bindings/grpc/src/services/health_check.rs new file mode 100644 index 0000000000..27cf808c4f --- /dev/null +++ b/bindings/grpc/src/services/health_check.rs @@ -0,0 +1,36 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use health_check::health_check_server::HealthCheck; +use health_check::health_check_server::HealthCheckServer; +use health_check::HealthCheckRequest; +use health_check::HealthCheckResponse; +use tonic::Request; +use tonic::Response; +use tonic::Status; + +#[allow(clippy::module_inception)] +mod health_check { + tonic::include_proto!("health_check"); +} + +#[derive(Debug, Default)] +pub struct HealthChecker {} + +#[tonic::async_trait] +impl HealthCheck for HealthChecker { + #[tracing::instrument( + name = "health_check", + skip_all, + fields(request = ?_req.get_ref()) + ret, + err, + )] + async fn check(&self, _req: Request) -> Result, Status> { + Ok(Response::new(HealthCheckResponse { status: "OK".into() })) + } +} + +pub fn service() -> HealthCheckServer { + HealthCheckServer::new(HealthChecker::default()) +} diff --git a/bindings/grpc/src/services/mod.rs b/bindings/grpc/src/services/mod.rs new file mode 100644 index 0000000000..f632feb91a --- /dev/null +++ b/bindings/grpc/src/services/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod credential; +pub mod document; +pub mod domain_linkage; +pub mod health_check; +pub mod sd_jwt; +pub mod status_list_2021; + +use identity_stronghold::StrongholdStorage; +use iota_sdk::client::Client; +use tonic::transport::server::Routes; +use tonic::transport::server::RoutesBuilder; + +pub fn routes(client: &Client, stronghold: &StrongholdStorage) -> Routes { + let mut routes = RoutesBuilder::default(); + routes.add_service(health_check::service()); + credential::init_services(&mut routes, client, stronghold); + routes.add_service(sd_jwt::service(client)); + routes.add_service(domain_linkage::service(client)); + routes.add_service(document::service(client, stronghold)); + routes.add_service(status_list_2021::service()); + + routes.routes() +} diff --git a/bindings/grpc/src/services/sd_jwt.rs b/bindings/grpc/src/services/sd_jwt.rs new file mode 100644 index 0000000000..af792e51f6 --- /dev/null +++ b/bindings/grpc/src/services/sd_jwt.rs @@ -0,0 +1,164 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +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; +use identity_iota::credential::FailFast; +use identity_iota::credential::Jwt; +use identity_iota::credential::JwtCredentialValidationOptions; +use identity_iota::credential::JwtCredentialValidatorUtils; +use identity_iota::credential::KeyBindingJWTValidationOptions; +use identity_iota::credential::SdJwtCredentialValidator; +use identity_iota::iota::IotaDID; +use identity_iota::iota::IotaDocument; +use identity_iota::resolver::Resolver; +use identity_iota::sd_jwt_payload::SdJwt; +use identity_iota::sd_jwt_payload::SdObjectDecoder; +use iota_sdk::client::Client; +use serde::Deserialize; +use serde::Serialize; +use thiserror::Error; + +use self::_sd_jwt::KeyBindingOptions; + +mod _sd_jwt { + tonic::include_proto!("sd_jwt"); +} + +impl From for KeyBindingJWTValidationOptions { + fn from(value: KeyBindingOptions) -> Self { + let mut kb_options = Self::default(); + kb_options.nonce = value.nonce; + kb_options.aud = value.aud; + kb_options.earliest_issuance_date = value + .earliest_issuance_date + .and_then(|t| Timestamp::parse(t.as_str()).ok()); + kb_options.latest_issuance_date = value + .latest_issuance_date + .and_then(|t| Timestamp::parse(t.as_str()).ok()); + + kb_options + } +} + +#[derive(Debug, Error, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "error", content = "reason")] +enum SdJwtVerificationError { + #[error("Failed to parse SD-JWT: {0}")] + DeserializationError(String), + #[error("Failed to parse JWT: {0}")] + JwtError(String), + #[error("Credential verification failed: {0}")] + VerificationError(String), + #[error("Failed to resolve DID Document: {0}")] + DidResolutionError(String), + #[error("Missing \"kb_options\".")] + MissingKbOptions, + #[error("{0}")] + KeyBindingJwtError(String), + #[error("Provided an invalid holder's id.")] + InvalidHolderDid, +} + +impl From for tonic::Status { + fn from(value: SdJwtVerificationError) -> Self { + let code = match &value { + SdJwtVerificationError::DeserializationError(_) => tonic::Code::InvalidArgument, + SdJwtVerificationError::JwtError(_) => tonic::Code::InvalidArgument, + SdJwtVerificationError::VerificationError(_) => tonic::Code::InvalidArgument, + SdJwtVerificationError::DidResolutionError(_) => tonic::Code::NotFound, + SdJwtVerificationError::MissingKbOptions => tonic::Code::InvalidArgument, + SdJwtVerificationError::KeyBindingJwtError(_) => tonic::Code::Internal, + SdJwtVerificationError::InvalidHolderDid => tonic::Code::InvalidArgument, + }; + let message = value.to_string(); + let error_json = serde_json::to_vec(&value).expect("plenty of memory!"); + + tonic::Status::with_details(code, message, error_json.into()) + } +} + +#[derive(Debug)] +pub struct SdJwtService { + resolver: Resolver, +} + +impl SdJwtService { + pub fn new(client: &Client) -> Self { + let mut resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + Self { resolver } + } +} + +#[tonic::async_trait] +impl Verification for SdJwtService { + #[tracing::instrument( + name = "sd_jwt_verification", + skip_all, + fields(request = ?request.get_ref()) + ret, + err, + )] + async fn verify( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let VerificationRequest { jwt, kb_options } = request.into_inner(); + let mut sd_jwt = SdJwt::parse(&jwt).map_err(|e| SdJwtVerificationError::DeserializationError(e.to_string()))?; + let jwt = Jwt::new(sd_jwt.jwt); + + let issuer_did = JwtCredentialValidatorUtils::extract_issuer_from_jwt::(&jwt) + .map_err(|e| SdJwtVerificationError::VerificationError(e.to_string()))?; + let issuer_document = self + .resolver + .resolve(&issuer_did) + .await + .map_err(|e| SdJwtVerificationError::DidResolutionError(e.to_string()))?; + sd_jwt.jwt = jwt.into(); + + let decoder = SdObjectDecoder::new_with_sha256(); + let validator = SdJwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default(), decoder); + let credential = validator + .validate_credential::<_, Object>( + &sd_jwt, + &issuer_document, + &JwtCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .map_err(|e| SdJwtVerificationError::VerificationError(e.to_string()))?; + + if sd_jwt.key_binding_jwt.is_some() { + let Some(kb_options) = kb_options else { + return Err(SdJwtVerificationError::MissingKbOptions.into()); + }; + let holder = { + let did = + IotaDID::parse(kb_options.holder_did.as_str()).map_err(|_| SdJwtVerificationError::InvalidHolderDid)?; + self + .resolver + .resolve(&did) + .await + .map_err(|e| SdJwtVerificationError::DidResolutionError(e.to_string()))? + }; + let _ = validator + .validate_key_binding_jwt(&sd_jwt, &holder, &kb_options.into()) + .map_err(|e| SdJwtVerificationError::KeyBindingJwtError(e.to_string()))?; + } + + Ok(tonic::Response::new(VerificationResponse { + credential: credential.credential.to_json().unwrap(), + })) + } +} + +pub fn service(client: &Client) -> VerificationServer { + VerificationServer::new(SdJwtService::new(client)) +} diff --git a/bindings/grpc/src/services/status_list_2021.rs b/bindings/grpc/src/services/status_list_2021.rs new file mode 100644 index 0000000000..be0595c9a4 --- /dev/null +++ b/bindings/grpc/src/services/status_list_2021.rs @@ -0,0 +1,170 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashSet; + +use identity_iota::core::Context; +use identity_iota::core::FromJson; +use identity_iota::core::Timestamp; +use identity_iota::core::ToJson; +use identity_iota::core::Url; +use identity_iota::credential::status_list_2021::StatusList2021; +use identity_iota::credential::status_list_2021::StatusList2021Credential; +use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder; +use identity_iota::credential::status_list_2021::StatusList2021CredentialError; +use identity_iota::credential::status_list_2021::StatusPurpose; +use identity_iota::credential::Issuer; +use identity_iota::credential::{self}; + +use _status_list_2021::status_list2021_svc_server::StatusList2021Svc; +use _status_list_2021::status_list2021_svc_server::StatusList2021SvcServer; +use _status_list_2021::CreateRequest; +use _status_list_2021::Purpose; +use _status_list_2021::StatusListCredential; +use _status_list_2021::UpdateRequest; +use tonic::Code; +use tonic::Request; +use tonic::Response; +use tonic::Status; + +mod _status_list_2021 { + use identity_iota::credential::status_list_2021::StatusPurpose; + + tonic::include_proto!("status_list_2021"); + + impl From for StatusPurpose { + fn from(value: Purpose) -> Self { + match value { + Purpose::Revocation => StatusPurpose::Revocation, + Purpose::Suspension => StatusPurpose::Suspension, + } + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("A valid status list must have at least 16KB entries")] + InvalidStatusListLength, + #[error("\"{0}\" is not a valid context")] + InvalidContext(String), + #[error("\"{0}\" is not a valid issuer")] + InvalidIssuer(String), + #[error("\"{0}\" is not a valid timestamp")] + InvalidTimestamp(String), + #[error("\"{0}\" is not a valid id")] + InvalidId(String), + #[error("Failed to deserialize into a valid StatusList2021Credential")] + CredentialDeserializationError(#[source] identity_iota::core::Error), + #[error(transparent)] + CredentialError(#[from] credential::Error), + #[error(transparent)] + StatusListError(StatusList2021CredentialError), +} + +impl From for Status { + fn from(value: Error) -> Self { + let code = match &value { + Error::InvalidStatusListLength + | Error::InvalidContext(_) + | Error::InvalidIssuer(_) + | Error::InvalidTimestamp(_) => Code::InvalidArgument, + _ => Code::Internal, + }; + + Status::new(code, value.to_string()) + } +} + +pub struct StatusList2021Service; + +#[tonic::async_trait] +impl StatusList2021Svc for StatusList2021Service { + #[tracing::instrument( + name = "create_status_list_credential", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, +)] + async fn create(&self, req: Request) -> Result, Status> { + let CreateRequest { + purpose, + length, + id, + expiration_date, + contexts, + types, + issuer, + } = req.into_inner(); + let status_list = length + .map(|entries| StatusList2021::new(entries as usize)) + .unwrap_or(Ok(StatusList2021::default())) + .map_err(|_| Error::InvalidStatusListLength)?; + + let mut builder = StatusList2021CredentialBuilder::new(status_list); + let contexts = contexts.into_iter().collect::>(); + for ctx in contexts { + let url = Url::parse(&ctx).map_err(move |_| Error::InvalidContext(ctx))?; + builder = builder.context(Context::Url(url)); + } + + let types = types.into_iter().collect::>(); + for t in types { + builder = builder.add_type(t); + } + let issuer = Url::parse(&issuer) + .map_err(move |_| Error::InvalidIssuer(issuer)) + .map(Issuer::Url)?; + builder = builder.issuer(issuer); + builder = builder.purpose(StatusPurpose::from(Purpose::try_from(purpose).unwrap())); + if let Some(exp) = expiration_date { + let exp = Timestamp::parse(&exp).map_err(move |_| Error::InvalidTimestamp(exp))?; + builder = builder.expiration_date(exp); + } + if let Some(id) = id { + let id = Url::parse(&id).map_err(move |_| Error::InvalidId(id))?; + builder = builder.subject_id(id); + } + let status_list_credential = builder.build().map_err(Error::CredentialError)?; + let res = StatusListCredential { + credential_json: status_list_credential.to_json().unwrap(), + }; + + Ok(Response::new(res)) + } + + #[tracing::instrument( + name = "update_status_list_credential", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn update(&self, req: Request) -> Result, Status> { + let UpdateRequest { + credential_json, + entries, + } = req.into_inner(); + let mut status_list_credential = + StatusList2021Credential::from_json(&credential_json).map_err(Error::CredentialDeserializationError)?; + + status_list_credential + .update(move |status_list| { + for (idx, value) in entries { + status_list.set_entry(idx as usize, value)? + } + + Ok(()) + }) + .map_err(Error::StatusListError)?; + + Ok(Response::new(StatusListCredential { + credential_json: status_list_credential.to_json().unwrap(), + })) + } +} + +pub fn service() -> StatusList2021SvcServer { + StatusList2021SvcServer::new(StatusList2021Service) +} diff --git a/bindings/grpc/tests/api/credential_revocation_check.rs b/bindings/grpc/tests/api/credential_revocation_check.rs new file mode 100644 index 0000000000..9e92197c72 --- /dev/null +++ b/bindings/grpc/tests/api/credential_revocation_check.rs @@ -0,0 +1,99 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use credentials::credential_revocation_client::CredentialRevocationClient; +use credentials::RevocationStatus; +use identity_iota::credential::RevocationBitmap; +use identity_iota::credential::RevocationBitmapStatus; +use identity_iota::credential::{self}; +use identity_iota::did::DID; +use serde_json::json; + +use crate::credential_revocation_check::credentials::RevocationCheckRequest; +use crate::helpers::Entity; +use crate::helpers::TestServer; + +mod credentials { + tonic::include_proto!("credentials"); +} + +#[tokio::test] +async fn checking_status_of_credential_works() -> anyhow::Result<()> { + let server = TestServer::new().await; + let client = server.client(); + let mut issuer = Entity::new(); + issuer.create_did(client).await?; + + let mut subject = Entity::new(); + subject.create_did(client).await?; + + let service_id = issuer + .document() + .unwrap() // Safety: `create_did` didn't fail + .id() + .to_url() + .join("#my-revocation-service")?; + + // Add a revocation service to the issuer's DID document + issuer + .update_document(client, |mut doc| { + let service = RevocationBitmap::new().to_service(service_id.clone()).unwrap(); + + doc.insert_service(service).ok().map(|_| doc) + }) + .await?; + + let credential_status: credential::Status = RevocationBitmapStatus::new(service_id, 3).into(); + + let mut grpc_client = CredentialRevocationClient::connect(server.endpoint()).await?; + let req = RevocationCheckRequest { + r#type: credential_status.type_, + url: credential_status.id.into_string(), + properties: credential_status + .properties + .into_iter() + .map(|(k, v)| (k, v.to_string().trim_matches('"').to_owned())) + .collect(), + }; + let res = grpc_client.check(tonic::Request::new(req.clone())).await?.into_inner(); + + assert_eq!(res.status(), RevocationStatus::Valid); + + // Revoke credential + issuer + .update_document(&client, |mut doc| { + doc.revoke_credentials("my-revocation-service", &[3]).ok().map(|_| doc) + }) + .await?; + + let res = grpc_client.check(tonic::Request::new(req)).await?.into_inner(); + assert_eq!(res.status(), RevocationStatus::Revoked); + + Ok(()) +} + +#[tokio::test] +async fn checking_status_of_valid_but_unresolvable_url_fails() -> anyhow::Result<()> { + use identity_grpc::services::credential::revocation::RevocationCheckError; + let server = TestServer::new().await; + + let mut grpc_client = CredentialRevocationClient::connect(server.endpoint()).await?; + let properties = json!({ + "revocationBitmapIndex": "3" + }); + let req = RevocationCheckRequest { + r#type: RevocationBitmap::TYPE.to_owned(), + url: "did:example:1234567890#my-revocation-service".to_owned(), + properties: properties + .as_object() + .unwrap() + .into_iter() + .map(|(k, v)| (k.clone(), v.to_string().trim_matches('"').to_owned())) + .collect(), + }; + let res_error = grpc_client.check(tonic::Request::new(req.clone())).await; + + assert!(res_error.is_err_and(|e| matches!(e.try_into().unwrap(), RevocationCheckError::ResolutionError(_)))); + + Ok(()) +} diff --git a/bindings/grpc/tests/api/credential_validation.rs b/bindings/grpc/tests/api/credential_validation.rs new file mode 100644 index 0000000000..f1bfedf100 --- /dev/null +++ b/bindings/grpc/tests/api/credential_validation.rs @@ -0,0 +1,151 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use _credentials::vc_validation_client::VcValidationClient; +use _credentials::VcValidationRequest; +use identity_iota::core::FromJson; +use identity_iota::core::ToJson; +use identity_iota::core::Url; +use identity_iota::credential::status_list_2021::StatusList2021; +use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder; +use identity_iota::credential::status_list_2021::StatusPurpose; +use identity_iota::credential::Credential; +use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::Issuer; +use identity_iota::credential::Subject; +use identity_iota::did::DID; +use identity_storage::JwkDocumentExt; +use identity_storage::JwsSignatureOptions; +use identity_stronghold::StrongholdStorage; +use serde_json::json; + +use crate::helpers::make_stronghold; +use crate::helpers::Entity; +use crate::helpers::TestServer; + +mod _credentials { + tonic::include_proto!("credentials"); +} + +#[tokio::test] +async fn credential_validation() -> anyhow::Result<()> { + let stronghold = StrongholdStorage::new(make_stronghold()); + let server = TestServer::new_with_stronghold(stronghold.clone()).await; + let api_client = server.client(); + + let mut issuer = Entity::new_with_stronghold(stronghold); + issuer.create_did(api_client).await?; + + let mut holder = Entity::new(); + holder.create_did(api_client).await?; + + let subject = Subject::from_json_value(json!({ + "id": holder.document().unwrap().id().as_str(), + "name": "Alice", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", + }))?; + + // Build credential using subject above and issuer. + let credential: Credential = CredentialBuilder::default() + .id(Url::parse("https://example.edu/credentials/3732")?) + .issuer(Url::parse(issuer.document().unwrap().id().as_str())?) + .type_("UniversityDegreeCredential") + .subject(subject) + .build()?; + + let credential_jwt = issuer + .document() + .unwrap() + .create_credential_jwt( + &credential, + &issuer.storage(), + &issuer.fragment().unwrap(), + &JwsSignatureOptions::default(), + None, + ) + .await? + .into(); + + let mut grpc_client = VcValidationClient::connect(server.endpoint()).await?; + let decoded_cred = grpc_client + .validate(VcValidationRequest { + credential_jwt, + status_list_credential_json: None, + }) + .await? + .into_inner() + .credential_json; + + let decoded_cred = serde_json::from_str::(&decoded_cred)?; + assert_eq!(decoded_cred, credential); + + Ok(()) +} + +#[tokio::test] +async fn revoked_credential_validation() -> anyhow::Result<()> { + let stronghold = StrongholdStorage::new(make_stronghold()); + let server = TestServer::new_with_stronghold(stronghold.clone()).await; + let api_client = server.client(); + + let mut issuer = Entity::new_with_stronghold(stronghold); + issuer.create_did(api_client).await?; + + let mut holder = Entity::new(); + holder.create_did(api_client).await?; + + let subject = Subject::from_json_value(json!({ + "id": holder.document().unwrap().id().as_str(), + "name": "Alice", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", + }))?; + + let mut status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default()) + .issuer(Issuer::Url(Url::parse(issuer.document().unwrap().id().as_str())?)) + .purpose(StatusPurpose::Revocation) + .subject_id(Url::parse("https://example.edu/credentials/status/1")?) + .build()?; + + // Build credential using subject above and issuer. + let mut credential: Credential = CredentialBuilder::default() + .id(Url::parse("https://example.edu/credentials/3732")?) + .issuer(Url::parse(issuer.document().unwrap().id().as_str())?) + .type_("UniversityDegreeCredential") + .subject(subject) + .build()?; + status_list_credential.set_credential_status(&mut credential, 0, true)?; + + let credential_jwt = issuer + .document() + .unwrap() + .create_credential_jwt( + &credential, + &issuer.storage(), + &issuer.fragment().unwrap(), + &JwsSignatureOptions::default(), + None, + ) + .await? + .into(); + + let mut grpc_client = VcValidationClient::connect(server.endpoint()).await?; + let error = grpc_client + .validate(VcValidationRequest { + credential_jwt, + status_list_credential_json: Some(status_list_credential.to_json()?), + }) + .await + .unwrap_err(); + + assert!(error.message().contains("revoked")); + + Ok(()) +} diff --git a/bindings/grpc/tests/api/did_document_creation.rs b/bindings/grpc/tests/api/did_document_creation.rs new file mode 100644 index 0000000000..394217e7a3 --- /dev/null +++ b/bindings/grpc/tests/api/did_document_creation.rs @@ -0,0 +1,43 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_stronghold::StrongholdStorage; +use iota_sdk::types::block::address::ToBech32Ext; +use tonic::Request; + +use crate::helpers::get_address_with_funds; +use crate::helpers::make_stronghold; +use crate::helpers::Entity; +use crate::helpers::TestServer; +use crate::helpers::FAUCET_ENDPOINT; +use _document::document_service_client::DocumentServiceClient; +use _document::CreateDidRequest; + +mod _document { + tonic::include_proto!("document"); +} + +#[tokio::test] +async fn did_document_creation() -> anyhow::Result<()> { + let stronghold = StrongholdStorage::new(make_stronghold()); + let server = TestServer::new_with_stronghold(stronghold.clone()).await; + let api_client = server.client(); + let hrp = api_client.get_bech32_hrp().await?; + + let user = Entity::new_with_stronghold(stronghold); + let user_address = get_address_with_funds( + api_client, + user.storage().key_storage().as_secret_manager(), + FAUCET_ENDPOINT, + ) + .await?; + + let mut grpc_client = DocumentServiceClient::connect(server.endpoint()).await?; + grpc_client + .create(Request::new(CreateDidRequest { + bech32_address: user_address.to_bech32(hrp).to_string(), + })) + .await?; + + Ok(()) +} diff --git a/bindings/grpc/tests/api/domain_linkage.rs b/bindings/grpc/tests/api/domain_linkage.rs new file mode 100644 index 0000000000..a79b732d58 --- /dev/null +++ b/bindings/grpc/tests/api/domain_linkage.rs @@ -0,0 +1,174 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Duration; +use identity_iota::core::Object; +use identity_iota::core::OrderedSet; +use identity_iota::core::Timestamp; +use identity_iota::core::Url; +use identity_iota::credential::Credential; +use identity_iota::credential::DomainLinkageConfiguration; +use identity_iota::credential::DomainLinkageCredentialBuilder; +use identity_iota::credential::Jwt; +use identity_iota::credential::LinkedDomainService; +use identity_iota::did::DIDUrl; +use identity_iota::did::DID; +use identity_storage::JwkDocumentExt; +use identity_storage::JwsSignatureOptions; +use identity_stronghold::StrongholdStorage; + +use crate::domain_linkage::_credentials::domain_linkage_client::DomainLinkageClient; +use crate::domain_linkage::_credentials::LinkedDidEndpointValidationStatus; +use crate::domain_linkage::_credentials::LinkedDidValidationStatus; +use crate::domain_linkage::_credentials::ValidateDidAgainstDidConfigurationsRequest; +use crate::domain_linkage::_credentials::ValidateDidResponse; +use crate::domain_linkage::_credentials::ValidateDomainAgainstDidConfigurationRequest; +use crate::domain_linkage::_credentials::ValidateDomainResponse; +use crate::helpers::make_stronghold; +use crate::helpers::Entity; +use crate::helpers::TestServer; + +mod _credentials { + tonic::include_proto!("domain_linkage"); +} + +/// Prepares basically the same test setup as in test `examples/1_advanced/6_domain_linkage.rs`. +async fn prepare_test() -> anyhow::Result<(TestServer, Url, String, Jwt)> { + let stronghold = StrongholdStorage::new(make_stronghold()); + let server = TestServer::new_with_stronghold(stronghold.clone()).await; + let api_client = server.client(); + + let mut issuer = Entity::new_with_stronghold(stronghold); + issuer.create_did(api_client).await?; + let did = issuer + .document() + .ok_or_else(|| anyhow::anyhow!("no DID document for issuer"))? + .id(); + let did_string = did.to_string(); + // ===================================================== + // Create Linked Domain service + // ===================================================== + + // The DID should be linked to the following domains. + let domain_1: Url = Url::parse("https://foo.example.com")?; + let domain_2: Url = Url::parse("https://bar.example.com")?; + + let mut domains: OrderedSet = OrderedSet::new(); + domains.append(domain_1.clone()); + domains.append(domain_2.clone()); + + // Create a Linked Domain Service to enable the discovery of the linked domains through the DID Document. + // This is optional since it is not a hard requirement by the specs. + let service_url: DIDUrl = did.clone().join("#domain-linkage")?; + let linked_domain_service: LinkedDomainService = LinkedDomainService::new(service_url, domains, Object::new())?; + issuer + .update_document(&api_client, |mut doc| { + doc.insert_service(linked_domain_service.into()).ok().map(|_| doc) + }) + .await?; + let updated_did_document = issuer + .document() + .ok_or_else(|| anyhow::anyhow!("no DID document for issuer"))?; + + println!("DID document with linked domain service: {updated_did_document:#}"); + + // ===================================================== + // Create DID Configuration resource + // ===================================================== + + // Create the Domain Linkage Credential. + let domain_linkage_credential: Credential = DomainLinkageCredentialBuilder::new() + .issuer(updated_did_document.id().clone().into()) + .origin(domain_1.clone()) + .issuance_date(Timestamp::now_utc()) + // Expires after a year. + .expiration_date( + Timestamp::now_utc() + .checked_add(Duration::days(365)) + .ok_or_else(|| anyhow::anyhow!("calculation should not overflow"))?, + ) + .build()?; + + let jwt: Jwt = updated_did_document + .create_credential_jwt( + &domain_linkage_credential, + &issuer.storage(), + &issuer + .fragment() + .ok_or_else(|| anyhow::anyhow!("no fragment for issuer"))?, + &JwsSignatureOptions::default(), + None, + ) + .await?; + + Ok((server, domain_1, did_string, jwt)) +} + +#[tokio::test] +async fn can_validate_domain() -> anyhow::Result<()> { + let (server, linked_domain, _, jwt) = prepare_test().await?; + let configuration_resource: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt.clone()]); + let mut grpc_client = DomainLinkageClient::connect(server.endpoint()).await?; + + let response = grpc_client + .validate_domain_against_did_configuration(ValidateDomainAgainstDidConfigurationRequest { + domain: linked_domain.to_string(), + did_configuration: configuration_resource.to_string(), + }) + .await?; + + assert_eq!( + response.into_inner(), + ValidateDomainResponse { + linked_dids: vec![LinkedDidValidationStatus { + valid: true, + document: Some(jwt.as_str().to_string()), + error: None, + }], + } + ); + + Ok(()) +} + +#[tokio::test] +async fn can_validate_did() -> anyhow::Result<()> { + let (server, linked_domain, issuer_did, jwt) = prepare_test().await?; + let configuration_resource: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt.clone()]); + let mut grpc_client = DomainLinkageClient::connect(server.endpoint()).await?; + + let response = grpc_client + .validate_did_against_did_configurations(ValidateDidAgainstDidConfigurationsRequest { + did: issuer_did.clone(), + did_configurations: vec![ValidateDomainAgainstDidConfigurationRequest { + domain: linked_domain.to_string(), + did_configuration: configuration_resource.to_string(), + }], + }) + .await?; + + assert_eq!( + response.into_inner(), + ValidateDidResponse { + service: vec![ + LinkedDidEndpointValidationStatus { + id: issuer_did, + service_endpoint: vec![ + LinkedDidValidationStatus { + valid: true, + document: Some(jwt.as_str().to_string()), + error: None, + }, + LinkedDidValidationStatus { + valid: false, + document: None, + error: Some("could not get domain linkage config; domain linkage error: error sending request for url (https://bar.example.com/.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known".to_string()), + } + ], + } + ] + } + ); + + Ok(()) +} diff --git a/bindings/grpc/tests/api/health_check.rs b/bindings/grpc/tests/api/health_check.rs new file mode 100644 index 0000000000..d8ea486269 --- /dev/null +++ b/bindings/grpc/tests/api/health_check.rs @@ -0,0 +1,24 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use health_check::health_check_client::HealthCheckClient; +use health_check::HealthCheckRequest; +use health_check::HealthCheckResponse; + +use crate::helpers::TestServer; + +mod health_check { + tonic::include_proto!("health_check"); +} + +#[tokio::test] +async fn health_check() -> anyhow::Result<()> { + let server = TestServer::new().await; + let mut grpc_client = HealthCheckClient::connect(server.endpoint()).await?; + let request = tonic::Request::new(HealthCheckRequest {}); + + let response = grpc_client.check(request).await?; + assert_eq!(response.into_inner(), HealthCheckResponse { status: "OK".into() }); + + Ok(()) +} diff --git a/bindings/grpc/tests/api/helpers.rs b/bindings/grpc/tests/api/helpers.rs new file mode 100644 index 0000000000..c307213db7 --- /dev/null +++ b/bindings/grpc/tests/api/helpers.rs @@ -0,0 +1,336 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Context; +use identity_iota::iota::IotaClientExt; +use identity_iota::iota::IotaDocument; +use identity_iota::iota::IotaIdentityClientExt; +use identity_iota::iota::NetworkName; +use identity_iota::verification::jws::JwsAlgorithm; +use identity_iota::verification::MethodScope; +use identity_storage::key_id_storage::KeyIdMemstore; +use identity_storage::key_storage::JwkMemStore; +use identity_storage::JwkDocumentExt; +use identity_storage::JwkStorage; +use identity_storage::KeyIdStorage; +use identity_storage::Storage; +use identity_stronghold::StrongholdStorage; +use iota_sdk::client::api::GetAddressesOptions; +use iota_sdk::client::node_api::indexer::query_parameters::QueryParameter; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; +use iota_sdk::client::stronghold::StrongholdAdapter; +use iota_sdk::client::Client; +use iota_sdk::client::Password; +use iota_sdk::crypto::keys::bip39; +use iota_sdk::types::block::address::Address; +use iota_sdk::types::block::address::Bech32Address; +use iota_sdk::types::block::address::Hrp; +use iota_sdk::types::block::output::AliasOutputBuilder; +use rand::distributions::Alphanumeric; +use rand::distributions::DistString; +use rand::thread_rng; +use std::net::SocketAddr; +use std::path::PathBuf; +use tokio::net::TcpListener; +use tokio::task::JoinHandle; +use tonic::transport::Uri; + +pub type MemStorage = Storage; + +pub const API_ENDPOINT: &str = "http://localhost"; +pub const FAUCET_ENDPOINT: &str = "http://localhost/faucet/api/enqueue"; + +#[derive(Debug)] +pub struct TestServer { + client: Client, + addr: SocketAddr, + _handle: JoinHandle>, +} + +impl TestServer { + pub async fn new() -> Self { + let stronghold = StrongholdSecretManager::builder() + .password(random_password(18)) + .build(random_stronghold_path()) + .map(StrongholdStorage::new) + .expect("Failed to create temporary stronghold"); + + Self::new_with_stronghold(stronghold).await + } + + pub async fn new_with_stronghold(stronghold: StrongholdStorage) -> Self { + let _ = tracing::subscriber::set_global_default(tracing_subscriber::fmt().compact().finish()); + + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind to random OS's port"); + let addr = listener.local_addr().unwrap(); + + let client: Client = Client::builder() + .with_primary_node(API_ENDPOINT, None) + .unwrap() + .finish() + .await + .expect("Failed to connect to API's endpoint"); + + let server = identity_grpc::server::GRpcServer::new(client.clone(), stronghold) + .into_router() + .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)); + TestServer { + _handle: tokio::spawn(server), + addr, + client, + } + } + + pub fn endpoint(&self) -> Uri { + format!("https://{}", self.addr) + .parse() + .expect("Failed to parse server's URI") + } + + pub fn client(&self) -> &Client { + &self.client + } +} + +pub async fn create_did( + client: &Client, + secret_manager: &mut SecretManager, + storage: &Storage, +) -> anyhow::Result<(Address, IotaDocument, String)> +where + K: JwkStorage, + I: KeyIdStorage, +{ + let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT) + .await + .context("failed to get address with funds")?; + + let network_name = client.network_name().await?; + let (document, fragment): (IotaDocument, String) = create_did_document(&network_name, storage).await?; + let alias_output = client.new_did_output(address, document, None).await?; + + let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?; + + Ok((address, document, fragment)) +} + +/// Creates an example DID document with the given `network_name`. +/// +/// Its functionality is equivalent to the "create DID" example +/// and exists for convenient calling from the other examples. +pub async fn create_did_document( + network_name: &NetworkName, + storage: &Storage, +) -> anyhow::Result<(IotaDocument, String)> +where + I: KeyIdStorage, + K: JwkStorage, +{ + let mut document: IotaDocument = IotaDocument::new(network_name); + + let fragment: String = document + .generate_method( + storage, + JwkMemStore::ED25519_KEY_TYPE, + JwsAlgorithm::EdDSA, + None, + MethodScope::VerificationMethod, + ) + .await?; + + Ok((document, fragment)) +} + +/// Generates an address from the given [`SecretManager`] and adds funds from the faucet. +pub async fn get_address_with_funds( + client: &Client, + stronghold: &SecretManager, + faucet_endpoint: &str, +) -> anyhow::Result
{ + let address = get_address(client, stronghold).await?; + + request_faucet_funds(client, address, faucet_endpoint) + .await + .context("failed to request faucet funds")?; + + Ok(*address) +} + +/// Initializes the [`SecretManager`] with a new mnemonic, if necessary, +/// and generates an address from the given [`SecretManager`]. +pub async fn get_address(client: &Client, secret_manager: &SecretManager) -> anyhow::Result { + let random: [u8; 32] = rand::random(); + let mnemonic = bip39::wordlist::encode(random.as_ref(), &bip39::wordlist::ENGLISH) + .map_err(|err| anyhow::anyhow!(format!("{err:?}")))?; + + if let SecretManager::Stronghold(ref stronghold) = secret_manager { + match stronghold.store_mnemonic(mnemonic).await { + Ok(()) => (), + Err(iota_sdk::client::stronghold::Error::MnemonicAlreadyStored) => (), + Err(err) => anyhow::bail!(err), + } + } else { + anyhow::bail!("expected a `StrongholdSecretManager`"); + } + + let bech32_hrp: Hrp = client.get_bech32_hrp().await?; + let address: Bech32Address = secret_manager + .generate_ed25519_addresses( + GetAddressesOptions::default() + .with_range(0..1) + .with_bech32_hrp(bech32_hrp), + ) + .await?[0]; + + Ok(address) +} + +/// Requests funds from the faucet for the given `address`. +async fn request_faucet_funds(client: &Client, address: Bech32Address, faucet_endpoint: &str) -> anyhow::Result<()> { + iota_sdk::client::request_funds_from_faucet(faucet_endpoint, &address).await?; + + tokio::time::timeout(std::time::Duration::from_secs(45), async { + loop { + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + let balance = get_address_balance(client, &address) + .await + .context("failed to get address balance")?; + if balance > 0 { + break; + } + } + Ok::<(), anyhow::Error>(()) + }) + .await + .context("maximum timeout exceeded")??; + + Ok(()) +} + +pub struct Entity { + secret_manager: SecretManager, + storage: Storage, + did: Option<(Address, IotaDocument, String)>, +} + +pub fn random_password(len: usize) -> Password { + let mut rng = thread_rng(); + Alphanumeric.sample_string(&mut rng, len).into() +} + +pub fn random_stronghold_path() -> PathBuf { + let mut file = std::env::temp_dir(); + file.push("test_strongholds"); + file.push(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)); + file.set_extension("stronghold"); + file.to_owned() +} + +impl Default for Entity { + fn default() -> Self { + let secret_manager = SecretManager::Stronghold(make_stronghold()); + let storage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + + Self { + secret_manager, + storage, + did: None, + } + } +} + +impl Entity { + pub fn new() -> Self { + Self::default() + } +} + +impl Entity { + pub fn new_with_stronghold(s: StrongholdStorage) -> Self { + let secret_manager = SecretManager::Stronghold(make_stronghold()); + let storage = Storage::new(s.clone(), s); + + Self { + secret_manager, + storage, + did: None, + } + } +} + +impl Entity { + pub async fn create_did(&mut self, client: &Client) -> anyhow::Result<()> { + let Entity { + secret_manager, + storage, + did, + } = self; + *did = Some(create_did(client, secret_manager, storage).await?); + + Ok(()) + } + + pub fn storage(&self) -> &Storage { + &self.storage + } + + pub fn document(&self) -> Option<&IotaDocument> { + self.did.as_ref().map(|(_, doc, _)| doc) + } + + pub fn fragment(&self) -> Option<&str> { + self.did.as_ref().map(|(_, _, frag)| frag.as_ref()) + } + + pub async fn update_document(&mut self, client: &Client, f: F) -> anyhow::Result<()> + where + F: FnOnce(IotaDocument) -> Option, + { + let (address, doc, fragment) = self.did.take().context("Missing doc")?; + let mut new_doc = f(doc.clone()); + if let Some(doc) = new_doc.take() { + let alias_output = client.update_did_output(doc.clone()).await?; + let rent_structure = client.get_rent_structure().await?; + let alias_output = AliasOutputBuilder::from(&alias_output) + .with_minimum_storage_deposit(rent_structure) + .finish()?; + + new_doc = Some(client.publish_did_output(&self.secret_manager, alias_output).await?); + } + + self.did = Some((address, new_doc.unwrap_or(doc), fragment)); + + Ok(()) + } +} +/// Returns the balance of the given Bech32-encoded `address`. +async fn get_address_balance(client: &Client, address: &Bech32Address) -> anyhow::Result { + let output_ids = client + .basic_output_ids(vec![ + QueryParameter::Address(address.to_owned()), + QueryParameter::HasExpiration(false), + QueryParameter::HasTimelock(false), + QueryParameter::HasStorageDepositReturn(false), + ]) + .await?; + + let outputs = client.get_outputs(&output_ids).await?; + + let mut total_amount = 0; + for output_response in outputs { + total_amount += output_response.output().amount(); + } + + Ok(total_amount) +} + +pub fn make_stronghold() -> StrongholdAdapter { + StrongholdAdapter::builder() + .password(random_password(18)) + .build(random_stronghold_path()) + .expect("Failed to create temporary stronghold") +} diff --git a/bindings/grpc/tests/api/jwt.rs b/bindings/grpc/tests/api/jwt.rs new file mode 100644 index 0000000000..927027b300 --- /dev/null +++ b/bindings/grpc/tests/api/jwt.rs @@ -0,0 +1,54 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use _credentials::jwt_client::JwtClient; +use _credentials::JwtCreationRequest; +use identity_iota::core::Object; +use identity_iota::core::Timestamp; +use identity_iota::core::ToJson; +use identity_iota::credential::CredentialBuilder; +use identity_iota::did::DID; +use identity_stronghold::StrongholdStorage; +use iota_sdk::Url; +use serde_json::json; + +use crate::helpers::make_stronghold; +use crate::helpers::Entity; +use crate::helpers::TestServer; + +mod _credentials { + tonic::include_proto!("credentials"); +} + +#[tokio::test] +async fn jwt_creation() -> anyhow::Result<()> { + let stronghold = StrongholdStorage::new(make_stronghold()); + let server = TestServer::new_with_stronghold(stronghold.clone()).await; + let api_client = server.client(); + + let mut issuer = Entity::new_with_stronghold(stronghold); + issuer.create_did(api_client).await?; + + let mut holder = Entity::new(); + holder.create_did(api_client).await?; + + let credential = CredentialBuilder::::default() + .issuance_date(Timestamp::now_utc()) + .issuer(Url::parse(issuer.document().unwrap().id().as_str())?) + .subject(serde_json::from_value(json!({ + "id": holder.document().unwrap().id().as_str(), + "type": "UniversityDegree", + "gpa": "4.0", + }))?) + .build()?; + + let mut grpc_client = JwtClient::connect(server.endpoint()).await?; + let _ = grpc_client + .create(JwtCreationRequest { + credential_json: credential.to_json()?, + issuer_fragment: issuer.fragment().unwrap().to_owned(), + }) + .await?; + + Ok(()) +} diff --git a/bindings/grpc/tests/api/main.rs b/bindings/grpc/tests/api/main.rs new file mode 100644 index 0000000000..e187cf7f1c --- /dev/null +++ b/bindings/grpc/tests/api/main.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod credential_revocation_check; +mod credential_validation; +mod did_document_creation; +mod domain_linkage; +mod health_check; +mod helpers; +mod jwt; +mod sd_jwt_validation; +mod status_list_2021; diff --git a/bindings/grpc/tests/api/sd_jwt_validation.rs b/bindings/grpc/tests/api/sd_jwt_validation.rs new file mode 100644 index 0000000000..e746e930c3 --- /dev/null +++ b/bindings/grpc/tests/api/sd_jwt_validation.rs @@ -0,0 +1,165 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use _sd_jwt::verification_client::VerificationClient; +use _sd_jwt::KeyBindingOptions; +use _sd_jwt::VerificationRequest; +use identity_iota::core::FromJson; +use identity_iota::core::Timestamp; +use identity_iota::core::ToJson; +use identity_iota::core::Url; +use identity_iota::credential::Credential; +use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::Jws; +use identity_iota::credential::Subject; +use identity_iota::did::DID; +use identity_iota::sd_jwt_payload::KeyBindingJwtClaims; +use identity_iota::sd_jwt_payload::SdJwt; +use identity_iota::sd_jwt_payload::SdObjectEncoder; +use identity_iota::sd_jwt_payload::Sha256Hasher; +use identity_storage::JwkDocumentExt; +use identity_storage::JwsSignatureOptions; + +use crate::helpers::Entity; +use crate::helpers::TestServer; + +mod _sd_jwt { + tonic::include_proto!("sd_jwt"); +} + +#[tokio::test] +async fn sd_jwt_validation_works() -> anyhow::Result<()> { + let server = TestServer::new().await; + let client = server.client(); + let mut issuer = Entity::new(); + issuer.create_did(client).await?; + + let mut holder = Entity::new(); + holder.create_did(client).await?; + + // Create an address credential subject. + let subject = Subject::from_json_value(serde_json::json!({ + "id": holder.document().unwrap().id().as_str(), + "name": "Alice", + "address": { + "locality": "Maxstadt", + "postal_code": "12344", + "country": "DE", + "street_address": "Weidenstraße 22" + } + }))?; + // Build credential using subject above and issuer. + let credential: Credential = CredentialBuilder::default() + .id(Url::parse("https://example.com/credentials/3732")?) + .issuer(Url::parse(issuer.document().unwrap().id().as_str())?) + .type_("AddressCredential") + .subject(subject) + .build()?; + + // In Order to create an selective disclosure JWT, the plain text JWT + // claims set must be created first. + let payload = credential.serialize_jwt(None)?; + + // Using the crate `sd-jwt` properties of the claims can be made selectively disclosable. + // The default sha-256 hasher will be used to create the digests. + // Read more in https://github.com/iotaledger/sd-jwt-payload . + let mut encoder = SdObjectEncoder::new(&payload)?; + + // Make "locality", "postal_code" and "street_address" selectively disclosable while keeping + // other properties in plain text. + let disclosures = vec![ + encoder.conceal("/vc/credentialSubject/address/locality", None)?, + encoder.conceal("/vc/credentialSubject/address/postal_code", None)?, + encoder.conceal("/vc/credentialSubject/address/street_address", None)?, + ]; + + // Add the `_sd_alg` property. + encoder.add_sd_alg_property(); + let encoded_payload = encoder.try_to_string()?; + + // Create the signed JWT. + let jwt: Jws = issuer + .document() + .unwrap() + .create_jws( + issuer.storage(), + issuer.fragment().unwrap(), + encoded_payload.as_bytes(), + &JwsSignatureOptions::default(), + ) + .await?; + + // One way to send the JWT and the disclosures, is by creating an SD-JWT with all the + // disclosures. + let disclosures: Vec = disclosures + .into_iter() + .map(|disclosure| disclosure.to_string()) + .collect(); + let sd_jwt_str = SdJwt::new(jwt.into(), disclosures, None).presentation(); + + const VERIFIER_DID: &str = "did:example:verifier"; + // A unique random challenge generated by the requester per presentation can mitigate replay attacks. + let nonce: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + // =========================================================================== + // Step 5: Holder creates an SD-JWT to be presented to a verifier. + // =========================================================================== + + let sd_jwt = SdJwt::parse(&sd_jwt_str)?; + + // The holder only wants to present "locality" and "postal_code" but not "street_address". + let disclosures = vec![ + sd_jwt.disclosures.first().unwrap().clone(), + sd_jwt.disclosures.get(1).unwrap().clone(), + ]; + + // Optionally, the holder can add a Key Binding JWT (KB-JWT). This is dependent on the verifier's policy. + // Issuing the KB-JWT is done by creating the claims set and setting the header `typ` value + // with the help of `KeyBindingJwtClaims`. + let binding_claims = KeyBindingJwtClaims::new( + &Sha256Hasher::new(), + sd_jwt.jwt.as_str().to_string(), + disclosures.clone(), + nonce.to_string(), + VERIFIER_DID.to_string(), + Timestamp::now_utc().to_unix(), + ) + .to_json()?; + + // Setting the `typ` in the header is required. + let options = JwsSignatureOptions::new().typ(KeyBindingJwtClaims::KB_JWT_HEADER_TYP); + + // Create the KB-JWT. + let kb_jwt: Jws = holder + .document() + .unwrap() + .create_jws( + holder.storage(), + holder.fragment().unwrap(), + binding_claims.as_bytes(), + &options, + ) + .await?; + + // Create the final SD-JWT. + let sd_jwt_obj = SdJwt::new(sd_jwt.jwt, disclosures, Some(kb_jwt.into())); + + // Holder presents the SD-JWT to the verifier. + let sd_jwt_presentation: String = sd_jwt_obj.presentation(); + + // Verify the JWT. + let mut sd_jwt_verification_client = VerificationClient::connect(server.endpoint()).await?; + let _ = sd_jwt_verification_client + .verify(VerificationRequest { + jwt: sd_jwt_presentation, + kb_options: Some(KeyBindingOptions { + nonce: Some(nonce.to_owned()), + aud: Some(VERIFIER_DID.to_owned()), + holder_did: holder.document().unwrap().id().to_string(), + ..Default::default() + }), + }) + .await?; + + Ok(()) +} diff --git a/bindings/grpc/tests/api/status_list_2021.rs b/bindings/grpc/tests/api/status_list_2021.rs new file mode 100644 index 0000000000..67ad31b34d --- /dev/null +++ b/bindings/grpc/tests/api/status_list_2021.rs @@ -0,0 +1,94 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::TestServer; +use _status_list_2021::status_list2021_svc_client::StatusList2021SvcClient; +use _status_list_2021::CreateRequest; +use _status_list_2021::Purpose; +use _status_list_2021::UpdateRequest; +use identity_iota::core::FromJson; +use identity_iota::core::ToJson; +use identity_iota::core::Url; +use identity_iota::credential::status_list_2021::StatusList2021; +use identity_iota::credential::status_list_2021::StatusList2021Credential; +use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder; +use identity_iota::credential::status_list_2021::StatusPurpose; +use identity_iota::credential::Issuer; +use tonic::Request; + +mod _status_list_2021 { + tonic::include_proto!("status_list_2021"); +} + +#[tokio::test] +async fn status_list_2021_credential_creation() -> anyhow::Result<()> { + let server = TestServer::new().await; + + let id = Url::parse("http://example.com/credentials/status/1").unwrap(); + let issuer = Issuer::Url(Url::parse("http://example.com/issuers/1").unwrap()); + let status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default()) + .purpose(StatusPurpose::Revocation) + .subject_id(id.clone()) + .issuer(issuer.clone()) + .build() + .unwrap(); + + let mut grpc_client = StatusList2021SvcClient::connect(server.endpoint()).await?; + let res = grpc_client + .create(Request::new(CreateRequest { + id: Some(id.into_string()), + issuer: issuer.url().to_string(), + purpose: Purpose::Revocation as i32, + length: None, + expiration_date: None, + contexts: vec![], + types: vec![], + })) + .await? + .into_inner() + .credential_json; + let grpc_credential = StatusList2021Credential::from_json(&res)?; + + assert_eq!(status_list_credential, grpc_credential); + Ok(()) +} + +#[tokio::test] +async fn status_list_2021_credential_update() -> anyhow::Result<()> { + let server = TestServer::new().await; + + let id = Url::parse("http://example.com/credentials/status/1").unwrap(); + let issuer = Issuer::Url(Url::parse("http://example.com/issuers/1").unwrap()); + let mut status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default()) + .purpose(StatusPurpose::Revocation) + .subject_id(id) + .issuer(issuer) + .build() + .unwrap(); + + let entries_to_set = [0_u64, 42, 420, 4200]; + let entries = entries_to_set.iter().map(|i| (*i, true)).collect(); + + let mut grpc_client = StatusList2021SvcClient::connect(server.endpoint()).await?; + let grpc_credential = grpc_client + .update(Request::new(UpdateRequest { + credential_json: status_list_credential.to_json().unwrap(), + entries, + })) + .await + .map(|res| res.into_inner().credential_json) + .map(|credential_json| StatusList2021Credential::from_json(&credential_json).unwrap()) + .unwrap(); + + status_list_credential.update(|status_list| { + for idx in entries_to_set { + if let Err(e) = status_list.set_entry(idx as usize, true) { + return Err(e); + } + } + Ok(()) + })?; + + assert_eq!(status_list_credential, grpc_credential); + Ok(()) +} diff --git a/bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json b/bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json new file mode 100644 index 0000000000..802f453e3e --- /dev/null +++ b/bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json @@ -0,0 +1,6 @@ +{ + "@context": "https://identity.foundation/.well-known/did-configuration/v1", + "linked_dids": [ + "add your domain linkage credential here" + ] +} \ No newline at end of file diff --git a/bindings/grpc/tooling/start-http-server.sh b/bindings/grpc/tooling/start-http-server.sh new file mode 100644 index 0000000000..4cebbf82d2 --- /dev/null +++ b/bindings/grpc/tooling/start-http-server.sh @@ -0,0 +1,4 @@ +#!/bin/sh +http-server ./domain-linkage-test-server & +# replace or omint the --domain parameter if you don't have a static domain or don't want to use it +ngrok http --domain=example-static-domain.ngrok-free.app 8080 \ No newline at end of file diff --git a/bindings/grpc/tooling/start-rpc-server.sh b/bindings/grpc/tooling/start-rpc-server.sh new file mode 100755 index 0000000000..69c207f6cf --- /dev/null +++ b/bindings/grpc/tooling/start-rpc-server.sh @@ -0,0 +1,7 @@ +#!/bin/sh +cd .. + +API_ENDPOINT=replace_me \ +STRONGHOLD_PWD=replace_me \ +SNAPSHOT_PATH=replace_me \ +cargo +nightly run --release diff --git a/bindings/wasm/CHANGELOG.md b/bindings/wasm/CHANGELOG.md index 7f25322057..fb0c4f8f46 100644 --- a/bindings/wasm/CHANGELOG.md +++ b/bindings/wasm/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [wasm-v1.2.0](https://github.com/iotaledger/identity.rs/tree/wasm-v1.2.0) (2024-03-27) + +[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v1.1.0...wasm-v1.2.0) + +### Added +- Allow setting additional controllers for `IotaDocument` [\#1314](https://github.com/iotaledger/identity.rs/pull/1314) +- use latest release of sd-jwt-payload `IotaDocument` [\#1333](https://github.com/iotaledger/identity.rs/pull/1333) +- Allow arbitrary verification methods [\#1334](https://github.com/iotaledger/identity.rs/pull/1334) + +### Patch +- Support %-encoded characters in DID method id [\#1303](https://github.com/iotaledger/identity.rs/pull/1303) + ## [wasm-v1.1.0](https://github.com/iotaledger/identity.rs/tree/wasm-v1.1.0) (2024-02-06) [Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v1.0.0...wasm-v1.1.0) diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 4d3178de83..b3db2773ac 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_wasm" -version = "1.1.0" +version = "1.2.0" authors = ["IOTA Stiftung"] edition = "2021" homepage = "https://www.iota.org" diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index 309c070aa7..1972af3649 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -331,6 +331,9 @@ prior to calling the function.

decodeB64(data)Uint8Array

Decode the given url-safe base64-encoded slice into its raw bytes.

+
start()
+

Initializes the console error panic hook for better error messages

+
@@ -7513,6 +7516,14 @@ Deserializes an instance from a JSON object. ## ProofAlgorithm **Kind**: global variable + + +## MethodRelationship +**Kind**: global variable + + +## CredentialStatus +**Kind**: global variable ## StatusCheck @@ -7665,3 +7676,9 @@ Decode the given url-safe base64-encoded slice into its raw bytes. | --- | --- | | data | Uint8Array | + + +## start() +Initializes the console error panic hook for better error messages + +**Kind**: global function diff --git a/bindings/wasm/package-lock.json b/bindings/wasm/package-lock.json index ef07af4968..e84427a5f0 100644 --- a/bindings/wasm/package-lock.json +++ b/bindings/wasm/package-lock.json @@ -1,12 +1,12 @@ { "name": "@iota/identity-wasm", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@iota/identity-wasm", - "version": "1.1.0", + "version": "1.2.0", "license": "Apache-2.0", "dependencies": { "@noble/ed25519": "^1.7.3", diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index 060b03b33d..193e761136 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@iota/identity-wasm", - "version": "1.1.0", + "version": "1.2.0", "description": "WASM bindings for IOTA Identity - A Self Sovereign Identity Framework implementing the DID and VC standards from W3C. To be used in Javascript/Typescript", "repository": { "type": "git", diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 31e6191978..4e107cb042 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "examples" -version = "1.1.1" +version = "1.2.0" authors = ["IOTA Stiftung"] edition = "2021" publish = false diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml index 120d6dc9be..02d46938f8 100644 --- a/identity_core/Cargo.toml +++ b/identity_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_core" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -35,3 +35,6 @@ quickcheck_macros = { version = "1.0" } # RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --workspace --open all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index f26a448ba3..7efd5e9685 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_credential" -version = "1.1.1" +version = "1.2.0" authors = ["IOTA Stiftung"] edition = "2021" homepage.workspace = true @@ -15,10 +15,10 @@ description = "An implementation of the Verifiable Credentials standard." async-trait = { version = "0.1.64", default-features = false } flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true } futures = { version = "0.3", default-features = false, optional = true } -identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } -identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } -identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } -identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } +identity_core = { version = "=1.2.0", path = "../identity_core", default-features = false } +identity_did = { version = "=1.2.0", path = "../identity_did", default-features = false } +identity_document = { version = "=1.2.0", path = "../identity_document", default-features = false } +identity_verification = { version = "=1.2.0", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } once_cell = { version = "1.18", default-features = false, features = ["std"] } @@ -59,3 +59,5 @@ domain-linkage = ["validator"] domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] sd-jwt = ["credential", "validator", "dep:sd-jwt-payload"] jpt-bbs-plus = ["credential", "validator", "dep:zkryptium", "dep:json-proof-token"] +[lints] +workspace = true diff --git a/identity_credential/src/credential/linked_domain_service.rs b/identity_credential/src/credential/linked_domain_service.rs index c6efbae255..3a76b10eb5 100644 --- a/identity_credential/src/credential/linked_domain_service.rs +++ b/identity_credential/src/credential/linked_domain_service.rs @@ -144,6 +144,11 @@ impl LinkedDomainService { .as_slice(), } } + + /// Returns a reference to the `Service` id. + pub fn id(&self) -> &DIDUrl { + self.service.id() + } } #[cfg(test)] diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index cf4f9c5066..468370e460 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -35,7 +35,7 @@ pub enum Error { #[error("invalid credential status: {0}")] InvalidStatus(String), /// Caused when constructing an invalid `LinkedDomainService` or `DomainLinkageConfiguration`. - #[error("domain linkage error")] + #[error("domain linkage error: {0}")] DomainLinkageError(#[source] Box), /// Caused when attempting to encode a `Credential` containing multiple subjects as a JWT. #[error("could not create JWT claim set from verifiable credential: more than one subject")] diff --git a/identity_credential/src/revocation/status_list_2021/credential.rs b/identity_credential/src/revocation/status_list_2021/credential.rs index e3f0875216..cc52916967 100644 --- a/identity_credential/src/revocation/status_list_2021/credential.rs +++ b/identity_credential/src/revocation/status_list_2021/credential.rs @@ -143,6 +143,21 @@ impl StatusList2021Credential { Ok(entry) } + /// Apply `update_fn` to the status list encoded in this credential. + pub fn update(&mut self, update_fn: F) -> Result<(), StatusList2021CredentialError> + where + F: FnOnce(&mut MutStatusList) -> Result<(), StatusList2021CredentialError>, + { + let mut encapsuled_status_list = MutStatusList { + status_list: self.status_list()?, + purpose: self.purpose(), + }; + update_fn(&mut encapsuled_status_list)?; + + self.subject.encoded_list = encapsuled_status_list.status_list.into_encoded_str(); + Ok(()) + } + /// Sets the `index`-th entry to `value` pub(crate) fn set_entry(&mut self, index: usize, value: bool) -> Result<(), StatusList2021CredentialError> { let mut status_list = self.status_list()?; @@ -167,6 +182,25 @@ impl StatusList2021Credential { } } +/// A wrapper over the [`StatusList2021`] contained in a [`StatusList2021Credential`] +/// that allows for its mutation. +pub struct MutStatusList { + status_list: StatusList2021, + purpose: StatusPurpose, +} + +impl MutStatusList { + /// Sets the value of the `index`-th entry in the status list. + pub fn set_entry(&mut self, index: usize, value: bool) -> Result<(), StatusList2021CredentialError> { + let entry_status = self.status_list.get(index)?; + if self.purpose == StatusPurpose::Revocation && !value && entry_status { + return Err(StatusList2021CredentialError::UnreversibleRevocation); + } + self.status_list.set(index, value)?; + Ok(()) + } +} + /// The status of a credential referenced inside a [`StatusList2021Credential`] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum CredentialStatus { @@ -241,7 +275,7 @@ impl From for Subject { impl StatusList2021CredentialSubject { /// Parse a StatusListCredentialSubject out of a credential, without copying. fn try_from_credential(credential: &mut Credential) -> Result { - let OneOrMany::One(subject) = &mut credential.credential_subject else { + let OneOrMany::One(mut subject) = std::mem::take(&mut credential.credential_subject) else { return Err(StatusList2021CredentialError::MultipleCredentialSubject); }; if let Some(subject_type) = subject.properties.get("type") { @@ -283,7 +317,7 @@ impl StatusList2021CredentialSubject { .map(std::mem::take)?; Ok(StatusList2021CredentialSubject { - id: std::mem::take(&mut subject.id), + id: subject.id, encoded_list, status_purpose, }) @@ -363,11 +397,17 @@ impl StatusList2021CredentialBuilder { .inner_builder .type_(CREDENTIAL_TYPE) .issuance_date(Timestamp::now_utc()) - .subject(self.credential_subject.clone().into()) + .subject(Subject { + id: self.credential_subject.id.clone(), + ..Default::default() + }) .build() - .map(|credential| StatusList2021Credential { - subject: self.credential_subject, - inner: credential, + .map(|mut credential| { + credential.credential_subject = OneOrMany::default(); + StatusList2021Credential { + subject: self.credential_subject, + inner: credential, + } }) } } diff --git a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs index e7a43bcdab..d454122c15 100644 --- a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs +++ b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs @@ -171,7 +171,7 @@ impl JwtCredentialValidatorUtils { /// Check the given `status` against the matching [`RevocationBitmap`] service in the /// issuer's DID Document. #[cfg(feature = "revocation-bitmap")] - fn check_revocation_bitmap_status + ?Sized>( + pub fn check_revocation_bitmap_status + ?Sized>( issuer: &DOC, status: crate::credential::RevocationBitmapStatus, ) -> ValidationUnitResult { diff --git a/identity_did/Cargo.toml b/identity_did/Cargo.toml index bed6f9012a..92a5e7fa30 100644 --- a/identity_did/Cargo.toml +++ b/identity_did/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_did" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition = "2021" homepage.workspace = true @@ -11,9 +11,9 @@ repository.workspace = true description = "Agnostic implementation of the Decentralized Identifiers (DID) standard." [dependencies] -did_url = { version = "0.1", default-features = false, features = ["std", "serde"] } +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.1.1", path = "../identity_core" } +identity_core = { version = "=1.2.0", path = "../identity_core" } serde.workspace = true strum.workspace = true thiserror.workspace = true @@ -27,3 +27,6 @@ serde_json.workspace = true # RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --workspace --open all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/identity_did/src/did.rs b/identity_did/src/did.rs index eb07f61a08..de9cf61183 100644 --- a/identity_did/src/did.rs +++ b/identity_did/src/did.rs @@ -8,7 +8,7 @@ use core::fmt::Formatter; use core::str::FromStr; use std::hash::Hash; -use did_url::DID as BaseDIDUrl; +use did_url_parser::DID as BaseDIDUrl; use identity_core::common::KeyComparable; @@ -111,14 +111,7 @@ impl CoreDID { /// /// Returns `Err` if the input is not a valid [`DID`]. pub fn parse(input: impl AsRef) -> Result { - let base_did_url: BaseDIDUrl = BaseDIDUrl::parse(input).map_err(Error::from)?; - Self::try_from_base_did(base_did_url) - } - - /// Try convert a [`BaseDIDUrl`] into a [`CoreDID`]. - fn try_from_base_did(base_did_url: BaseDIDUrl) -> Result { - Self::check_validity(&base_did_url)?; - Ok(Self(base_did_url)) + BaseDIDUrl::parse(input).map(Self).map_err(Error::from) } /// Set the method name of the [`DID`]. @@ -145,9 +138,23 @@ impl CoreDID { /// Validates whether a string is a valid [`DID`] method-id. pub fn valid_method_id(value: &str) -> Result<(), Error> { - if !value.chars().all(is_char_method_id) { - return Err(Error::InvalidMethodId); + // if !value.chars().all(is_char_method_id) { + // return Err(Error::InvalidMethodId); + // } + let mut chars = value.chars(); + while let Some(c) = chars.next() { + match c { + '%' => { + let digits = chars.clone().take(2).collect::(); + u8::from_str_radix(&digits, 16).map_err(|_| Error::InvalidMethodId)?; + chars.next(); + chars.next(); + } + c if is_char_method_id(c) => (), + _ => return Err(Error::InvalidMethodId), + } } + Ok(()) } @@ -185,7 +192,7 @@ impl TryFrom for CoreDID { type Error = Error; fn try_from(base_did_url: BaseDIDUrl) -> Result { - Self::try_from_base_did(base_did_url) + Ok(Self(base_did_url)) } } diff --git a/identity_did/src/did_url.rs b/identity_did/src/did_url.rs index 60c7d6c84e..0e8eebcace 100644 --- a/identity_did/src/did_url.rs +++ b/identity_did/src/did_url.rs @@ -10,7 +10,7 @@ use std::cmp::Ordering; use std::hash::Hash; use std::hash::Hasher; -use did_url::DID as BaseDIDUrl; +use did_url_parser::DID as BaseDIDUrl; use identity_core::common::KeyComparable; use identity_core::common::Url; diff --git a/identity_did/src/error.rs b/identity_did/src/error.rs index b2c1ab1469..e9bf4f51b4 100644 --- a/identity_did/src/error.rs +++ b/identity_did/src/error.rs @@ -23,15 +23,15 @@ pub enum Error { Other(&'static str), } -impl From for Error { - fn from(error: did_url::Error) -> Self { +impl From for Error { + fn from(error: did_url_parser::Error) -> Self { match error { - did_url::Error::InvalidFragment => Self::InvalidFragment, - did_url::Error::InvalidMethodId => Self::InvalidMethodId, - did_url::Error::InvalidMethodName => Self::InvalidMethodName, - did_url::Error::InvalidPath => Self::InvalidPath, - did_url::Error::InvalidQuery => Self::InvalidQuery, - did_url::Error::InvalidScheme => Self::InvalidScheme, + did_url_parser::Error::InvalidFragment => Self::InvalidFragment, + did_url_parser::Error::InvalidMethodId => Self::InvalidMethodId, + did_url_parser::Error::InvalidMethodName => Self::InvalidMethodName, + did_url_parser::Error::InvalidPath => Self::InvalidPath, + did_url_parser::Error::InvalidQuery => Self::InvalidQuery, + did_url_parser::Error::InvalidScheme => Self::InvalidScheme, error => Self::Other(error.as_str()), } } diff --git a/identity_did/src/lib.rs b/identity_did/src/lib.rs index f6fb2c368c..9289419211 100644 --- a/identity_did/src/lib.rs +++ b/identity_did/src/lib.rs @@ -23,7 +23,7 @@ mod error; pub use crate::did_url::DIDUrl; pub use crate::did_url::RelativeDIDUrl; -pub use ::did_url::DID as BaseDIDUrl; +pub use ::did_url_parser::DID as BaseDIDUrl; pub use did::CoreDID; pub use did::DID; pub use error::Error; diff --git a/identity_document/Cargo.toml b/identity_document/Cargo.toml index bebbd8f070..3c2779e04b 100644 --- a/identity_document/Cargo.toml +++ b/identity_document/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_document" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,10 +12,10 @@ rust-version.workspace = true description = "Method-agnostic implementation of the Decentralized Identifiers (DID) standard." [dependencies] -did_url = { version = "0.1", default-features = false, features = ["std", "serde"] } -identity_core = { version = "=1.1.1", path = "../identity_core" } -identity_did = { version = "=1.1.1", path = "../identity_did" } -identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } +did_url_parser = { version = "0.2.0", features = ["std", "serde"] } +identity_core = { version = "=1.2.0", path = "../identity_core" } +identity_did = { version = "=1.2.0", path = "../identity_did" } +identity_verification = { version = "=1.2.0", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } serde.workspace = true strum.workspace = true @@ -28,3 +28,6 @@ serde_json.workspace = true [[bench]] name = "deserialize_document" harness = false + +[lints] +workspace = true diff --git a/identity_eddsa_verifier/Cargo.toml b/identity_eddsa_verifier/Cargo.toml index 257fa5d5a4..b8a3cff943 100644 --- a/identity_eddsa_verifier/Cargo.toml +++ b/identity_eddsa_verifier/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_eddsa_verifier" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,9 +12,12 @@ rust-version.workspace = true description = "JWS EdDSA signature verification for IOTA Identity" [dependencies] -identity_jose = { version = "=1.1.1", path = "../identity_jose", default-features = false } +identity_jose = { version = "=1.2.0", path = "../identity_jose", default-features = false } iota-crypto = { version = "0.23", default-features = false, features = ["std"] } [features] ed25519 = ["iota-crypto/ed25519"] default = ["ed25519"] + +[lints] +workspace = true diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index ada3ad97bd..38ffb10842 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_iota" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,14 +12,14 @@ rust-version.workspace = true description = "Framework for Self-Sovereign Identity with IOTA DID." [dependencies] -identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.1", path = "../identity_credential", features = ["validator"], default-features = false } -identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } -identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } -identity_iota_core = { version = "=1.1.1", path = "../identity_iota_core", default-features = false } -identity_resolver = { version = "=1.1.1", path = "../identity_resolver", default-features = false, optional = true } -identity_storage = { version = "=1.1.1", path = "../identity_storage", default-features = false, features = ["iota-document"] } -identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } +identity_core = { version = "=1.2.0", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.2.0", path = "../identity_credential", features = ["validator"], default-features = false } +identity_did = { version = "=1.2.0", path = "../identity_did", default-features = false } +identity_document = { version = "=1.2.0", path = "../identity_document", default-features = false } +identity_iota_core = { version = "=1.2.0", path = "../identity_iota_core", default-features = false } +identity_resolver = { version = "=1.2.0", path = "../identity_resolver", default-features = false, optional = true } +identity_storage = { version = "=1.2.0", path = "../identity_storage", default-features = false, features = ["iota-document"] } +identity_verification = { version = "=1.2.0", path = "../identity_verification", default-features = false } [dev-dependencies] anyhow = "1.0.64" @@ -72,3 +72,6 @@ jpt-bbs-plus = ["identity_storage/jpt-bbs-plus", "identity_credential/jpt-bbs-pl # RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --workspace --open all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true diff --git a/identity_iota/README.md b/identity_iota/README.md index 69d68defd8..ea2c38b2ec 100644 --- a/identity_iota/README.md +++ b/identity_iota/README.md @@ -51,7 +51,7 @@ If you want to include IOTA Identity in your project, simply add it as a depende ```toml [dependencies] -identity_iota = { version = "1.1.1" } +identity_iota = { version = "1.2.0" } ``` To try out the [examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples), you can also do this: @@ -74,7 +74,7 @@ version = "1.0.0" edition = "2021" [dependencies] -identity_iota = { version = "1.1.1", features = ["memstore"] } +identity_iota = { version = "1.2.0", features = ["memstore"] } iota-sdk = { version = "1.0.2", default-features = true, features = ["tls", "client", "stronghold"] } tokio = { version = "1", features = ["full"] } anyhow = "1.0.62" diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index c5fbfa1f89..4e55ae36f4 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_iota_core" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -14,11 +14,11 @@ description = "An IOTA Ledger integration for the IOTA DID Method." [dependencies] async-trait = { version = "0.1.56", default-features = false, optional = true } futures = { version = "0.3", default-features = false } -identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.1", path = "../identity_credential", default-features = false, features = ["validator"] } -identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } -identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } -identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } +identity_core = { version = "=1.2.0", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.2.0", path = "../identity_credential", default-features = false, features = ["validator"] } +identity_did = { version = "=1.2.0", path = "../identity_did", default-features = false } +identity_document = { version = "=1.2.0", path = "../identity_document", default-features = false } +identity_verification = { version = "=1.2.0", path = "../identity_verification", default-features = false } iota-sdk = { version = "1.0.2", default-features = false, features = ["serde", "std"], optional = true } num-derive = { version = "0.4", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["std"] } @@ -53,3 +53,6 @@ revocation-bitmap = ["identity_credential/revocation-bitmap"] send-sync-client-ext = [] # Disables the blanket implementation of `IotaIdentityClientExt`. test = ["client"] + +[lints] +workspace = true diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index 6781362356..014ee4de7c 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_jose" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,7 +12,7 @@ rust-version.workspace = true description = "A library for JOSE (JSON Object Signing and Encryption)" [dependencies] -identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } +identity_core = { version = "=1.2.0", path = "../identity_core", default-features = false } iota-crypto = { version = "0.23", default-features = false, features = ["std", "sha"] } json-proof-token.workspace = true serde.workspace = true @@ -30,3 +30,6 @@ signature = { version = "2", default-features = false } [[example]] name = "jws_encoding_decoding" test = true + +[lints] +workspace = true diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index bd28b248ad..26f4fd6290 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_resolver" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -15,23 +15,23 @@ description = "DID Resolution utilities for the identity.rs library." # This is currently necessary for the ResolutionHandler trait. This can be made an optional dependency if alternative ways of attaching handlers are introduced. async-trait = { version = "0.1", default-features = false } futures = { version = "0.3" } -identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.1", path = "../identity_credential", default-features = false, features = ["validator"] } -identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } -identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } +identity_core = { version = "=1.2.0", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.2.0", path = "../identity_credential", default-features = false, features = ["validator"] } +identity_did = { version = "=1.2.0", path = "../identity_did", default-features = false } +identity_document = { version = "=1.2.0", path = "../identity_document", default-features = false } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } strum.workspace = true thiserror = { version = "1.0", default-features = false } [dependencies.identity_iota_core] -version = "=1.1.1" +version = "=1.2.0" path = "../identity_iota_core" default-features = false features = ["send-sync-client-ext", "iota-client"] optional = true [dev-dependencies] -identity_iota_core = { version = "=1.1.1", path = "../identity_iota_core", features = ["test"] } +identity_iota_core = { path = "../identity_iota_core", features = ["test"] } iota-sdk = { version = "1.0.2" } tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } @@ -40,3 +40,6 @@ default = ["revocation-bitmap", "iota"] revocation-bitmap = ["identity_credential/revocation-bitmap", "identity_iota_core?/revocation-bitmap"] # Enables the IOTA integration for the resolver. iota = ["dep:identity_iota_core"] + +[lints] +workspace = true diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index 9e07433f74..0f32069722 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_storage" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -14,12 +14,12 @@ description = "Abstractions over storage for cryptographic keys used in DID Docu [dependencies] async-trait = { version = "0.1.64", default-features = false } futures = { version = "0.3.27", default-features = false, features = ["async-await"] } -identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.1", path = "../identity_credential", default-features = false, features = ["credential", "presentation", "revocation-bitmap"] } -identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } -identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } -identity_iota_core = { version = "=1.1.1", path = "../identity_iota_core", default-features = false, optional = true } -identity_verification = { version = "=1.1.1", path = "../identity_verification", default_features = false } +identity_core = { version = "=1.2.0", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.2.0", path = "../identity_credential", default-features = false, features = ["credential", "presentation", "revocation-bitmap"] } +identity_did = { version = "=1.2.0", path = "../identity_did", default-features = false } +identity_document = { version = "=1.2.0", path = "../identity_document", default-features = false } +identity_iota_core = { version = "=1.2.0", path = "../identity_iota_core", default-features = false, optional = true } +identity_verification = { version = "=1.2.0", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"], optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default_features = false } @@ -32,8 +32,8 @@ json-proof-token = { workspace = true, optional = true } anyhow = "1.0.82" [dev-dependencies] -identity_credential = { version = "=1.1.1", path = "../identity_credential", features = ["revocation-bitmap"] } -identity_eddsa_verifier = { version = "=1.1.1", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } +identity_credential = { version = "=1.2.0", path = "../identity_credential", features = ["revocation-bitmap"] } +identity_eddsa_verifier = { version = "=1.2.0", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } once_cell = { version = "1.18", default-features = false } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } @@ -47,3 +47,6 @@ send-sync-storage = [] iota-document = ["dep:identity_iota_core"] # Enables JSON Proof Token & BBS+ related features jpt-bbs-plus = ["identity_credential/jpt-bbs-plus", "dep:zkryptium", "dep:json-proof-token"] + +[lints] +workspace = true diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index b82df069b7..810ee209d1 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_stronghold" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -13,8 +13,8 @@ description = "Secure JWK storage with Stronghold for IOTA Identity" [dependencies] async-trait = { version = "0.1.64", default-features = false } -identity_storage = { version = "=1.1.1", path = "../identity_storage", default_features = false } -identity_verification = { version = "=1.1.1", path = "../identity_verification", default_features = false } +identity_storage = { version = "=1.2.0", path = "../identity_storage", default_features = false } +identity_verification = { version = "=1.2.0", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"] } iota-sdk = { version = "1.0.2", default-features = false, features = ["client", "stronghold"] } iota_stronghold = { version = "2.0.0", default-features = false } @@ -27,7 +27,7 @@ json-proof-token = { workspace = true, optional = true } [dev-dependencies] -identity_did = { version = "=1.1.1", path = "../identity_did", default_features = false } +identity_did = { version = "=1.2.0", path = "../identity_did", default_features = false } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } [features] @@ -35,3 +35,6 @@ default = [] # Enables `Send` + `Sync` bounds for the trait implementations on `StrongholdStorage`. send-sync-storage = ["identity_storage/send-sync-storage"] bbs-plus = ["identity_storage/jpt-bbs-plus", "dep:zkryptium", "dep:json-proof-token"] + +[lints] +workspace = true diff --git a/identity_verification/Cargo.toml b/identity_verification/Cargo.toml index e400aeebeb..812cfe43ec 100644 --- a/identity_verification/Cargo.toml +++ b/identity_verification/Cargo.toml @@ -1,20 +1,24 @@ [package] name = "identity_verification" -version = "1.1.1" +version = "1.2.0" authors.workspace = true edition.workspace = true homepage.workspace = true license.workspace = true +repository.workspace = true rust-version.workspace = true description = "Verification data types and functionality for identity.rs" [dependencies] -identity_core = { version = "=1.1.1", path = "./../identity_core", default-features = false } -identity_did = { version = "=1.1.1", path = "./../identity_did", default-features = false } -identity_jose = { version = "=1.1.1", path = "./../identity_jose", default-features = false } +identity_core = { version = "=1.2.0", path = "./../identity_core", default-features = false } +identity_did = { version = "=1.2.0", path = "./../identity_did", default-features = false } +identity_jose = { version = "=1.2.0", path = "./../identity_jose", default-features = false } serde.workspace = true serde_json.workspace = true strum.workspace = true thiserror.workspace = true [dev-dependencies] + +[lints] +workspace = true From 94565c9a7e801c883cf7ee4b800da4494bc6ccf4 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 25 Apr 2024 12:13:59 +0200 Subject: [PATCH 15/33] clippy --- identity_iota/src/lib.rs | 1 + identity_storage/src/key_storage/bls.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index bd067bd13b..9ab2e53805 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -114,6 +114,7 @@ pub mod storage { pub use identity_storage::key_storage::public_modules::*; } /// Storage types and functionalities. + #[allow(clippy::module_inception)] pub mod storage { pub use identity_storage::storage::*; } diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs index 7cb6cee2de..a1eb9926e2 100644 --- a/identity_storage/src/key_storage/bls.rs +++ b/identity_storage/src/key_storage/bls.rs @@ -111,7 +111,7 @@ fn _sign_bbs( where S: BbsCiphersuite, { - Signature::>::sign(Some(data), &sk, &pk, Some(header)).map(|s| s.to_bytes().to_vec()) + Signature::>::sign(Some(data), sk, pk, Some(header)).map(|s| s.to_bytes().to_vec()) } /// Signs data and header using the given keys. From 47386057b352b11d6e2fca9ed7ab7f0c2701a1e2 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 25 Apr 2024 12:24:12 +0200 Subject: [PATCH 16/33] fmt --- Cargo.toml | 2 +- identity_credential/Cargo.toml | 2 +- identity_storage/Cargo.toml | 4 ++-- identity_stronghold/Cargo.toml | 5 ++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6a5f4a11a7..fcc0af432f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ repository = "https://github.com/iotaledger/identity.rs" rust-version = "1.65" [patch.crates-io] -iota_stronghold = {git = "https://github.com/tensor-programming/stronghold.rs.git", branch = "feat/expose_runner"} +iota_stronghold = { git = "https://github.com/tensor-programming/stronghold.rs.git", branch = "feat/expose_runner" } [workspace.lints.clippy] result_large_err = "allow" diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 1b1df9160e..91834c6aba 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -21,6 +21,7 @@ identity_document = { version = "=1.2.0", path = "../identity_document", default identity_verification = { version = "=1.2.0", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } +json-proof-token = { workspace = true, optional = true } once_cell = { version = "1.18", default-features = false, features = ["std"] } reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true } roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true } @@ -33,7 +34,6 @@ strum.workspace = true thiserror.workspace = true url = { version = "2.5", default-features = false } zkryptium = { workspace = true, optional = true } -json-proof-token = { workspace = true, optional = true } [dev-dependencies] anyhow = "1.0.62" diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index 0f32069722..590d532485 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -12,6 +12,7 @@ rust-version.workspace = true description = "Abstractions over storage for cryptographic keys used in DID Documents" [dependencies] +anyhow = "1.0.82" async-trait = { version = "0.1.64", default-features = false } futures = { version = "0.3.27", default-features = false, features = ["async-await"] } identity_core = { version = "=1.2.0", path = "../identity_core", default-features = false } @@ -21,6 +22,7 @@ identity_document = { version = "=1.2.0", path = "../identity_document", default identity_iota_core = { version = "=1.2.0", path = "../identity_iota_core", default-features = false, optional = true } identity_verification = { version = "=1.2.0", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"], optional = true } +json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default_features = false } serde.workspace = true @@ -28,8 +30,6 @@ serde_json.workspace = true thiserror.workspace = true tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"], optional = true } zkryptium = { workspace = true, optional = true } -json-proof-token = { workspace = true, optional = true } -anyhow = "1.0.82" [dev-dependencies] identity_credential = { version = "=1.2.0", path = "../identity_credential", features = ["revocation-bitmap"] } diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index 810ee209d1..a74bc13e51 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -12,19 +12,18 @@ rust-version.workspace = true description = "Secure JWK storage with Stronghold for IOTA Identity" [dependencies] +anyhow = "1.0.82" async-trait = { version = "0.1.64", default-features = false } identity_storage = { version = "=1.2.0", path = "../identity_storage", default_features = false } identity_verification = { version = "=1.2.0", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"] } iota-sdk = { version = "1.0.2", default-features = false, features = ["client", "stronghold"] } iota_stronghold = { version = "2.0.0", default-features = false } +json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"] } zeroize = { version = "1.6.0", default_features = false } -anyhow = "1.0.82" zkryptium = { workspace = true, optional = true } -json-proof-token = { workspace = true, optional = true } - [dev-dependencies] identity_did = { version = "=1.2.0", path = "../identity_did", default_features = false } From 6a32429cc962e52c0bf3cf576e00fb49548c8769 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 25 Apr 2024 15:41:17 +0200 Subject: [PATCH 17/33] add stronghold bbs+ tests --- identity_stronghold/Cargo.toml | 3 + identity_stronghold/src/lib.rs | 2 +- .../src/stronghold_jwk_storage_ext.rs | 14 +-- .../src/stronghold_key_type.rs | 6 ++ identity_stronghold/src/tests/mod.rs | 1 + identity_stronghold/src/tests/test_bbs_ext.rs | 90 +++++++++++++++++++ 6 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 identity_stronghold/src/tests/test_bbs_ext.rs diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index a74bc13e51..77cf48e02e 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -27,7 +27,10 @@ zkryptium = { workspace = true, optional = true } [dev-dependencies] identity_did = { version = "=1.2.0", path = "../identity_did", default_features = false } +identity_storage = { version = "=1.2.0", path = "../identity_storage", default_features = false, features = ["jpt-bbs-plus"] } +json-proof-token = { workspace = true } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } +zkryptium = { workspace = true } [features] default = [] diff --git a/identity_stronghold/src/lib.rs b/identity_stronghold/src/lib.rs index 5fb8d59786..c2a0dd7f82 100644 --- a/identity_stronghold/src/lib.rs +++ b/identity_stronghold/src/lib.rs @@ -3,7 +3,7 @@ pub(crate) mod ed25519; mod stronghold_jwk_storage; -#[cfg(feature = "bbs-plus")] +#[cfg(any(feature = "bbs-plus", test))] mod stronghold_jwk_storage_ext; mod stronghold_key_id; pub(crate) mod stronghold_key_type; diff --git a/identity_stronghold/src/stronghold_jwk_storage_ext.rs b/identity_stronghold/src/stronghold_jwk_storage_ext.rs index b27be8f4cb..6720e216fa 100644 --- a/identity_stronghold/src/stronghold_jwk_storage_ext.rs +++ b/identity_stronghold/src/stronghold_jwk_storage_ext.rs @@ -42,6 +42,10 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { ); } + if !matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { + return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()); + } + let stronghold = self.get_stronghold().await; let client = get_client(&stronghold)?; @@ -78,9 +82,6 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { header: &[u8], public_key: &Jwk, ) -> KeyStorageResult> { - let stronghold = self.get_stronghold().await; - let client = get_client(&stronghold)?; - // Extract the required alg from the given public key let alg = public_key .alg() @@ -103,6 +104,8 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { record_path: key_id.to_string().as_bytes().to_vec(), }; + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; client .get_guards([sk_location], |[sk]| { let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; @@ -123,9 +126,6 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { signature: &[u8; BBSplusSignature::BYTES], ctx: ProofUpdateCtx, ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { - let stronghold = self.get_stronghold().await; - let client = get_client(&stronghold)?; - // Extract the required alg from the given public key let alg = public_key .alg() @@ -143,6 +143,8 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), record_path: key_id.to_string().as_bytes().to_vec(), }; + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; client .get_guards([sk_location], |[sk]| { diff --git a/identity_stronghold/src/stronghold_key_type.rs b/identity_stronghold/src/stronghold_key_type.rs index df52b7c44e..af0b868831 100644 --- a/identity_stronghold/src/stronghold_key_type.rs +++ b/identity_stronghold/src/stronghold_key_type.rs @@ -48,6 +48,12 @@ impl TryFrom<&KeyType> for StrongholdKeyType { } } +impl From for KeyType { + fn from(key_type: StrongholdKeyType) -> KeyType { + KeyType::from_static_str(key_type.name()) + } +} + impl TryFrom<&Jwk> for StrongholdKeyType { type Error = KeyStorageError; diff --git a/identity_stronghold/src/tests/mod.rs b/identity_stronghold/src/tests/mod.rs index 96db03f0aa..54c5488c05 100644 --- a/identity_stronghold/src/tests/mod.rs +++ b/identity_stronghold/src/tests/mod.rs @@ -1,6 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod test_bbs_ext; mod test_jwk_storage; mod test_key_id_storage; pub(crate) mod utils; diff --git a/identity_stronghold/src/tests/test_bbs_ext.rs b/identity_stronghold/src/tests/test_bbs_ext.rs new file mode 100644 index 0000000000..385b7ce627 --- /dev/null +++ b/identity_stronghold/src/tests/test_bbs_ext.rs @@ -0,0 +1,90 @@ +use identity_storage::key_storage::bls::expand_bls_jwk; +use identity_storage::key_storage::bls::sign_bbs; +use identity_storage::JwkGenOutput; +use identity_storage::JwkStorage; +use identity_storage::JwkStorageBbsPlusExt; +use identity_storage::KeyStorageErrorKind; +use iota_stronghold::procedures::Runner; +use iota_stronghold::Location; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use rand::RngCore; +use zkryptium::bbsplus::keys::BBSplusSecretKey; + +use crate::stronghold_key_type::StrongholdKeyType; +use crate::tests::utils::create_stronghold_secret_manager; +use crate::utils::get_client; +use crate::utils::IDENTITY_VAULT_PATH; +use crate::StrongholdStorage; + +#[tokio::test] +async fn stronghold_bbs_keypair_gen_works() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let JwkGenOutput { key_id, jwk, .. } = stronghold_storage + .generate_bbs(StrongholdKeyType::BLS12381G2.into(), ProofAlgorithm::BLS12381_SHA256) + .await?; + + assert!(jwk.is_public()); + assert!(stronghold_storage.exists(&key_id).await?); + + Ok(()) +} + +#[tokio::test] +async fn stronghold_bbs_keypair_gen_fails_with_wrong_key_type() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let error = stronghold_storage + .generate_bbs(StrongholdKeyType::Ed25519.into(), ProofAlgorithm::BLS12381_SHA256) + .await + .unwrap_err(); + assert!(matches!(error.kind(), KeyStorageErrorKind::UnsupportedKeyType)); + + Ok(()) +} + +#[tokio::test] +async fn stronghold_bbs_keypair_gen_fails_with_wrong_alg() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let error = stronghold_storage + .generate_bbs(StrongholdKeyType::BLS12381G2.into(), ProofAlgorithm::MAC_H256) + .await + .unwrap_err(); + + assert!(matches!(error.kind(), KeyStorageErrorKind::UnsupportedProofAlgorithm)); + + Ok(()) +} + +#[tokio::test] +async fn stronghold_sign_bbs_works() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let JwkGenOutput { key_id, jwk, .. } = stronghold_storage + .generate_bbs(StrongholdKeyType::BLS12381G2.into(), ProofAlgorithm::BLS12381_SHA256) + .await?; + let pk = expand_bls_jwk(&jwk)?.1; + let sk = { + let stronghold = stronghold_storage.get_stronghold().await; + let client = get_client(&stronghold)?; + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_owned(), + record_path: key_id.as_str().as_bytes().to_owned(), + }; + client + .get_guards([sk_location], |[sk]| Ok(BBSplusSecretKey::from_bytes(&sk.borrow()))) + .unwrap() + }?; + + let mut data = vec![0; 1024]; + rand::thread_rng().fill_bytes(&mut data); + let expected_signature = sign_bbs( + ProofAlgorithm::BLS12381_SHA256, + std::slice::from_ref(&data), + &sk, + &pk, + &[], + )?; + + let signature = stronghold_storage.sign_bbs(&key_id, &[data], &[], &jwk).await?; + assert_eq!(signature, expected_signature); + + Ok(()) +} From 23a2f461996260369b38ad0a82aef9810ca3b9c2 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 25 Apr 2024 16:24:39 +0200 Subject: [PATCH 18/33] review comments --- identity_storage/src/key_storage/bls.rs | 14 ++++++++++---- .../key_storage/jwk_storage_bbs_plus_ext.rs | 5 ++--- identity_storage/src/key_storage/memstore.rs | 5 ++--- .../src/stronghold_jwk_storage_ext.rs | 19 +++++++++++++------ 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs index a1eb9926e2..e9fc62d478 100644 --- a/identity_storage/src/key_storage/bls.rs +++ b/identity_storage/src/key_storage/bls.rs @@ -173,15 +173,21 @@ where /// Updates BBS+ signature's timeframe data. pub fn update_bbs_signature( alg: ProofAlgorithm, - sig: &[u8; 80], + sig: &[u8], sk: &BBSplusSecretKey, update_ctx: &ProofUpdateCtx, -) -> Result<[u8; 80], KeyStorageError> { +) -> KeyStorageResult> { + let exact_size_signature = sig.try_into().map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid signature size".to_owned()) + })?; match alg { - ProofAlgorithm::BLS12381_SHA256 => _update_bbs_signature::(sig, sk, update_ctx), - ProofAlgorithm::BLS12381_SHAKE256 => _update_bbs_signature::(sig, sk, update_ctx), + ProofAlgorithm::BLS12381_SHA256 => _update_bbs_signature::(exact_size_signature, sk, update_ctx), + ProofAlgorithm::BLS12381_SHAKE256 => { + _update_bbs_signature::(exact_size_signature, sk, update_ctx) + } _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), } + .map(Vec::from) .map_err(|e| { KeyStorageError::new(KeyStorageErrorKind::Unspecified) .with_custom_message("signature failed") diff --git a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs index 423bd1b875..d041448ba8 100644 --- a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs +++ b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs @@ -1,7 +1,6 @@ use async_trait::async_trait; use identity_verification::jwk::Jwk; use jsonprooftoken::jpa::algs::ProofAlgorithm; -use zkryptium::bbsplus::signature::BBSplusSignature; use crate::JwkGenOutput; use crate::JwkStorage; @@ -32,7 +31,7 @@ pub trait JwkStorageBbsPlusExt: JwkStorage { &self, key_id: &KeyId, public_key: &Jwk, - signature: &[u8; BBSplusSignature::BYTES], + signature: &[u8], ctx: ProofUpdateCtx, - ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]>; + ) -> KeyStorageResult>; } diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index d9a65ae818..036f92ce7c 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -323,7 +323,6 @@ mod bbs_plus_impl { use identity_verification::jwk::BlsCurve; use identity_verification::jwk::Jwk; use jsonprooftoken::jpa::algs::ProofAlgorithm; - use zkryptium::bbsplus::signature::BBSplusSignature; use super::random_key_id; @@ -386,9 +385,9 @@ mod bbs_plus_impl { &self, key_id: &KeyId, public_key: &Jwk, - signature: &[u8; BBSplusSignature::BYTES], + signature: &[u8], ctx: ProofUpdateCtx, - ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { + ) -> KeyStorageResult> { let jwk_store = self.jwk_store.read().await; // Extract the required alg from the given public key diff --git a/identity_stronghold/src/stronghold_jwk_storage_ext.rs b/identity_stronghold/src/stronghold_jwk_storage_ext.rs index 6720e216fa..a3418027b5 100644 --- a/identity_stronghold/src/stronghold_jwk_storage_ext.rs +++ b/identity_stronghold/src/stronghold_jwk_storage_ext.rs @@ -23,7 +23,6 @@ use jsonprooftoken::jpa::algs::ProofAlgorithm; use std::str::FromStr; use zeroize::Zeroizing; use zkryptium::bbsplus::keys::BBSplusSecretKey; -use zkryptium::bbsplus::signature::BBSplusSignature; use crate::stronghold_key_type::*; use crate::utils::*; @@ -46,10 +45,14 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()); } + // Get a key id that's not already used. + let mut kid = random_key_id(); + while self.exists(&kid).await? { + kid = random_key_id(); + } + let stronghold = self.get_stronghold().await; let client = get_client(&stronghold)?; - - let kid: KeyId = random_key_id(); let target_key_location = Location::generic( IDENTITY_VAULT_PATH.as_bytes().to_vec(), kid.to_string().as_bytes().to_vec(), @@ -109,7 +112,11 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { client .get_guards([sk_location], |[sk]| { let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; - sign_bbs(alg, data, &sk, &pk, header).map_err(|e| FatalProcedureError::from(e.to_string())) + let signature_result = + sign_bbs(alg, data, &sk, &pk, header).map_err(|e| FatalProcedureError::from(e.to_string())); + // clean up `sk` to avoid leaking. + drop(Zeroizing::new(sk.to_bytes())); + signature_result }) .map(|sig| sig.to_vec()) .map_err(|e| { @@ -123,9 +130,9 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { &self, key_id: &KeyId, public_key: &Jwk, - signature: &[u8; BBSplusSignature::BYTES], + signature: &[u8], ctx: ProofUpdateCtx, - ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { + ) -> KeyStorageResult> { // Extract the required alg from the given public key let alg = public_key .alg() From e0147283903102091c1d87066ed9f41d4a99d520 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 25 Apr 2024 16:35:35 +0200 Subject: [PATCH 19/33] add license header --- bindings/wasm/src/credential/jpt.rs | 3 +++ .../jpt_credential_validator/decoded_jpt_credential.rs | 3 +++ .../jpt_credential_validation_options.rs | 3 +++ .../jpt_credential_validator/jpt_credential_validator.rs | 3 +++ .../jpt_credential_validator/jpt_credential_validator_utils.rs | 3 +++ .../jpt_credential_validator/jwp_credential_options.rs | 3 +++ .../jpt_credential_validator/jwp_verification_options.rs | 3 +++ bindings/wasm/src/credential/jpt_credential_validator/mod.rs | 3 +++ .../jpt_presentiation_validation/decoded_jpt_presentation.rs | 3 +++ .../jpt_presentation_validation_options.rs | 3 +++ .../jpt_presentiation_validation/jpt_presentation_validator.rs | 3 +++ .../jpt_presentation_validator_utils.rs | 3 +++ .../jpt_presentiation_validation/jwp_presentation_options.rs | 3 +++ .../wasm/src/credential/jpt_presentiation_validation/mod.rs | 3 +++ .../src/credential/revocation/validity_timeframe_2024/mod.rs | 3 +++ .../credential/revocation/validity_timeframe_2024/status.rs | 3 +++ bindings/wasm/src/jpt/encoding.rs | 3 +++ bindings/wasm/src/jpt/issuer_protected_header.rs | 3 +++ bindings/wasm/src/jpt/jpt_claims.rs | 3 +++ bindings/wasm/src/jpt/jwp_issued.rs | 3 +++ bindings/wasm/src/jpt/jwp_presentation_builder.rs | 3 +++ bindings/wasm/src/jpt/mod.rs | 3 +++ bindings/wasm/src/jpt/payload.rs | 3 +++ bindings/wasm/src/jpt/presentation_protected_header.rs | 3 +++ bindings/wasm/src/jpt/proof_algorithm.rs | 3 +++ bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs | 3 +++ bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs | 3 +++ examples/1_advanced/10_zkp_revocation.rs | 3 +++ examples/1_advanced/9_zkp.rs | 3 +++ identity_credential/src/credential/jpt.rs | 3 +++ identity_credential/src/credential/jwp_credential_options.rs | 3 +++ .../src/presentation/jwp_presentation_builder.rs | 3 +++ .../src/presentation/jwp_presentation_options.rs | 3 +++ .../src/revocation/validity_timeframe_2024/mod.rs | 3 +++ .../validity_timeframe_2024/revocation_timeframe_status.rs | 3 +++ .../jpt_credential_validation/decoded_jpt_credential.rs | 3 +++ .../jpt_credential_validation_options.rs | 3 +++ .../jpt_credential_validation/jpt_credential_validator.rs | 3 +++ .../jpt_credential_validator_utils.rs | 3 +++ .../src/validator/jpt_credential_validation/mod.rs | 3 +++ .../jpt_presentation_validation/decoded_jpt_presentation.rs | 3 +++ .../jpt_presentation_validation_options.rs | 3 +++ .../jpt_presentation_validation/jpt_presentation_validator.rs | 3 +++ .../jpt_presentation_validator_utils.rs | 3 +++ .../src/validator/jpt_presentation_validation/mod.rs | 3 +++ identity_document/src/verifiable/jwp_verification_options.rs | 3 +++ identity_jose/src/jwk/curve/bls.rs | 3 +++ identity_jose/src/jwk/jwk_ext.rs | 3 +++ identity_storage/src/key_storage/bls.rs | 3 +++ identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs | 3 +++ identity_storage/src/storage/jwp_document_ext.rs | 3 +++ identity_storage/src/storage/timeframe_revocation_ext.rs | 3 +++ identity_stronghold/src/stronghold_jwk_storage_ext.rs | 2 +- identity_stronghold/src/stronghold_key_type.rs | 3 +++ identity_stronghold/src/tests/test_bbs_ext.rs | 3 +++ identity_stronghold/src/utils.rs | 3 +++ 56 files changed, 166 insertions(+), 1 deletion(-) diff --git a/bindings/wasm/src/credential/jpt.rs b/bindings/wasm/src/credential/jpt.rs index ad0b3def10..e3e3daab2b 100644 --- a/bindings/wasm/src/credential/jpt.rs +++ b/bindings/wasm/src/credential/jpt.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::credential::Jpt; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs b/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs index 04a776345e..46c999a40f 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::core::Object; use identity_iota::credential::DecodedJptCredential; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs index 55ff8bd0b2..aefc6ec443 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::credential::JptCredentialValidationOptions; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs index b64f2348de..10876fe96f 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::common::ImportedDocumentLock; use crate::credential::WasmDecodedJptCredential; use crate::credential::WasmFailFast; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs index 8cbb9f6ac3..cfdf9c6e9e 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::common::ImportedDocumentLock; use crate::common::WasmTimestamp; use crate::credential::options::WasmStatusCheck; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs index ca284eac97..907e793996 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::error::Result; use crate::error::WasmResult; use identity_iota::credential::JwpCredentialOptions; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs index 2f107276fc..d7ef8b5b89 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::error::Result; use crate::error::WasmResult; use identity_iota::document::verifiable::JwpVerificationOptions; diff --git a/bindings/wasm/src/credential/jpt_credential_validator/mod.rs b/bindings/wasm/src/credential/jpt_credential_validator/mod.rs index 4b6ab97483..7da2b15114 100644 --- a/bindings/wasm/src/credential/jpt_credential_validator/mod.rs +++ b/bindings/wasm/src/credential/jpt_credential_validator/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + mod decoded_jpt_credential; mod jpt_credential_validation_options; mod jpt_credential_validator; diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs index 2fdfe94dcc..698b9e3410 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::core::Object; use identity_iota::credential::DecodedJptPresentation; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs index 478bf7b5b7..2576437ed4 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::credential::JptPresentationValidationOptions; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs index 8f55969c4d..3843b48b81 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::common::ImportedDocumentLock; use crate::credential::WasmDecodedJptPresentation; use crate::credential::WasmFailFast; diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs index 3e8e05d97b..d3d927b82f 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::common::WasmTimestamp; use crate::credential::options::WasmStatusCheck; use crate::credential::WasmCredential; diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs index 8ead5045d6..7bc30851a5 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::core::Url; use identity_iota::credential::JwpPresentationOptions; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs index 9ebfe02925..8a2663c85f 100644 --- a/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs +++ b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + mod decoded_jpt_presentation; mod jpt_presentation_validation_options; mod jpt_presentation_validator; diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs index 2da8555a22..36474c70bb 100644 --- a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs +++ b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + mod status; pub use status::*; diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs index 363475b1c7..fb85bbeee3 100644 --- a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs +++ b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::core::Url; use identity_iota::credential::RevocationTimeframeStatus; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/jpt/encoding.rs b/bindings/wasm/src/jpt/encoding.rs index bb647762a8..e36a5307a5 100644 --- a/bindings/wasm/src/jpt/encoding.rs +++ b/bindings/wasm/src/jpt/encoding.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use jsonprooftoken::encoding::SerializationType; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/jpt/issuer_protected_header.rs b/bindings/wasm/src/jpt/issuer_protected_header.rs index 8a825cfc19..4499d42b69 100644 --- a/bindings/wasm/src/jpt/issuer_protected_header.rs +++ b/bindings/wasm/src/jpt/issuer_protected_header.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::jpt::WasmProofAlgorithm; use jsonprooftoken::jwp::header::IssuerProtectedHeader; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/jpt/jpt_claims.rs b/bindings/wasm/src/jpt/jpt_claims.rs index c04f07095d..ae9a6e0822 100644 --- a/bindings/wasm/src/jpt/jpt_claims.rs +++ b/bindings/wasm/src/jpt/jpt_claims.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use wasm_bindgen::prelude::*; #[wasm_bindgen] diff --git a/bindings/wasm/src/jpt/jwp_issued.rs b/bindings/wasm/src/jpt/jwp_issued.rs index 89c32a6822..e2c3826621 100644 --- a/bindings/wasm/src/jpt/jwp_issued.rs +++ b/bindings/wasm/src/jpt/jwp_issued.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use jsonprooftoken::jwp::issued::JwpIssued; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/jpt/jwp_presentation_builder.rs b/bindings/wasm/src/jpt/jwp_presentation_builder.rs index 23d15be410..64797ee4cf 100644 --- a/bindings/wasm/src/jpt/jwp_presentation_builder.rs +++ b/bindings/wasm/src/jpt/jwp_presentation_builder.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use super::WasmJwpIssued; use super::WasmPresentationProtectedHeader; use crate::error::Result; diff --git a/bindings/wasm/src/jpt/mod.rs b/bindings/wasm/src/jpt/mod.rs index 3683114019..631572003b 100644 --- a/bindings/wasm/src/jpt/mod.rs +++ b/bindings/wasm/src/jpt/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + mod encoding; mod issuer_protected_header; mod jpt_claims; diff --git a/bindings/wasm/src/jpt/payload.rs b/bindings/wasm/src/jpt/payload.rs index 80c542d7bb..cdb06638b3 100644 --- a/bindings/wasm/src/jpt/payload.rs +++ b/bindings/wasm/src/jpt/payload.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::error::Result; use crate::error::WasmError; use crate::error::WasmResult; diff --git a/bindings/wasm/src/jpt/presentation_protected_header.rs b/bindings/wasm/src/jpt/presentation_protected_header.rs index a2e907c29b..398870da4c 100644 --- a/bindings/wasm/src/jpt/presentation_protected_header.rs +++ b/bindings/wasm/src/jpt/presentation_protected_header.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use jsonprooftoken::jpa::algs::PresentationProofAlgorithm; use jsonprooftoken::jwp::header::PresentationProtectedHeader; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/jpt/proof_algorithm.rs b/bindings/wasm/src/jpt/proof_algorithm.rs index be8ff4e504..0f7f6986f1 100644 --- a/bindings/wasm/src/jpt/proof_algorithm.rs +++ b/bindings/wasm/src/jpt/proof_algorithm.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use jsonprooftoken::jpa::algs::ProofAlgorithm; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs b/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs index d8745567bb..9529128e20 100644 --- a/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs +++ b/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::storage::ProofUpdateCtx; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs index c6d57444a0..b8355e4e8b 100644 --- a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs +++ b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use std::str::FromStr; use crate::error::Result as WasmResult; diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs index db30d71d71..3ec9af89e8 100644 --- a/examples/1_advanced/10_zkp_revocation.rs +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use examples::get_address_with_funds; use examples::random_stronghold_path; use examples::MemStorage; diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs index d683d4378d..67ad69ef30 100644 --- a/examples/1_advanced/9_zkp.rs +++ b/examples/1_advanced/9_zkp.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use examples::get_address_with_funds; use examples::random_stronghold_path; use examples::MemStorage; diff --git a/identity_credential/src/credential/jpt.rs b/identity_credential/src/credential/jpt.rs index 0ee1355734..328bcb2ef1 100644 --- a/identity_credential/src/credential/jpt.rs +++ b/identity_credential/src/credential/jpt.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use serde::Deserialize; use serde::Serialize; diff --git a/identity_credential/src/credential/jwp_credential_options.rs b/identity_credential/src/credential/jwp_credential_options.rs index acecc499b5..8b3349b01e 100644 --- a/identity_credential/src/credential/jwp_credential_options.rs +++ b/identity_credential/src/credential/jwp_credential_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + /// Options for creating a JSON Web Proof. #[non_exhaustive] #[derive(Debug, Default, serde::Serialize, serde::Deserialize, Eq, PartialEq, Clone)] diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs index e7ba9f6859..517c86ae44 100644 --- a/identity_credential/src/presentation/jwp_presentation_builder.rs +++ b/identity_credential/src/presentation/jwp_presentation_builder.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::error::Error; use crate::error::Result; use jsonprooftoken::jwp::header::PresentationProtectedHeader; diff --git a/identity_credential/src/presentation/jwp_presentation_options.rs b/identity_credential/src/presentation/jwp_presentation_options.rs index 927be649f8..67c14d6130 100644 --- a/identity_credential/src/presentation/jwp_presentation_options.rs +++ b/identity_credential/src/presentation/jwp_presentation_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_core::common::Url; use serde::Deserialize; use serde::Serialize; diff --git a/identity_credential/src/revocation/validity_timeframe_2024/mod.rs b/identity_credential/src/revocation/validity_timeframe_2024/mod.rs index d4f1ab0614..61fd3717cb 100644 --- a/identity_credential/src/revocation/validity_timeframe_2024/mod.rs +++ b/identity_credential/src/revocation/validity_timeframe_2024/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + //! Implementation of a new Revocation mechanism for ZK Verifiable Credentials. mod revocation_timeframe_status; diff --git a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs index b7ccea6690..02acd02287 100644 --- a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs +++ b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::credential::Status; use crate::error::Error; use crate::error::Result; diff --git a/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs b/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs index 1a970e018a..60036c9947 100644 --- a/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs +++ b/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_core::common::Object; use jsonprooftoken::jwp::issued::JwpIssued; diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs index 918afd749a..256178390e 100644 --- a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::validator::SubjectHolderRelationship; use identity_core::common::Timestamp; use identity_core::common::Url; diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs index 617ea7c5f7..a0f27288cf 100644 --- a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_core::convert::FromJson; use identity_core::convert::ToJson; use identity_did::CoreDID; diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs index e1f292f57a..55fa649f44 100644 --- a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use crate::credential::Credential; use crate::revocation::RevocationDocumentExt; use crate::revocation::RevocationTimeframeStatus; diff --git a/identity_credential/src/validator/jpt_credential_validation/mod.rs b/identity_credential/src/validator/jpt_credential_validation/mod.rs index bc27a9f43a..7ed480186b 100644 --- a/identity_credential/src/validator/jpt_credential_validation/mod.rs +++ b/identity_credential/src/validator/jpt_credential_validation/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + mod decoded_jpt_credential; mod jpt_credential_validation_options; mod jpt_credential_validator; diff --git a/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs b/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs index 6693018dd4..bff5e8a4fe 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_core::common::Object; use identity_core::common::Url; use jsonprooftoken::jwp::presented::JwpPresented; diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs index af5f6f7b0a..488c6ab45e 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_document::verifiable::JwpVerificationOptions; use serde::Deserialize; use serde::Serialize; diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs index 8fb6e925df..25327ae3db 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use std::str::FromStr; use identity_core::common::Url; diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs index dfb5ad280a..1f880320a0 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use std::str::FromStr; use identity_core::common::Object; diff --git a/identity_credential/src/validator/jpt_presentation_validation/mod.rs b/identity_credential/src/validator/jpt_presentation_validation/mod.rs index a39c129027..6a12563fe7 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/mod.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + mod decoded_jpt_presentation; mod jpt_presentation_validation_options; mod jpt_presentation_validator; diff --git a/identity_document/src/verifiable/jwp_verification_options.rs b/identity_document/src/verifiable/jwp_verification_options.rs index 0b7ceec064..65667968ea 100644 --- a/identity_document/src/verifiable/jwp_verification_options.rs +++ b/identity_document/src/verifiable/jwp_verification_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_did::DIDUrl; use identity_verification::MethodScope; diff --git a/identity_jose/src/jwk/curve/bls.rs b/identity_jose/src/jwk/curve/bls.rs index aa241bee21..4c468002fb 100644 --- a/identity_jose/src/jwk/curve/bls.rs +++ b/identity_jose/src/jwk/curve/bls.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use core::fmt::Display; use core::fmt::Formatter; use core::fmt::Result; diff --git a/identity_jose/src/jwk/jwk_ext.rs b/identity_jose/src/jwk/jwk_ext.rs index 97f1d8a6f1..1b6ede06a6 100644 --- a/identity_jose/src/jwk/jwk_ext.rs +++ b/identity_jose/src/jwk/jwk_ext.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use super::Jwk; use super::JwkOperation; use super::JwkParams; diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs index e9fc62d478..b32f5ec86e 100644 --- a/identity_storage/src/key_storage/bls.rs +++ b/identity_storage/src/key_storage/bls.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use anyhow::Context; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jwu; diff --git a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs index d041448ba8..09f65788ab 100644 --- a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs +++ b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use async_trait::async_trait; use identity_verification::jwk::Jwk; use jsonprooftoken::jpa::algs::ProofAlgorithm; diff --git a/identity_storage/src/storage/jwp_document_ext.rs b/identity_storage/src/storage/jwp_document_ext.rs index 747d6e6570..8d5629461c 100644 --- a/identity_storage/src/storage/jwp_document_ext.rs +++ b/identity_storage/src/storage/jwp_document_ext.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use super::JwkStorageDocumentError as Error; use crate::key_id_storage::MethodDigest; use crate::try_undo_key_generation; diff --git a/identity_storage/src/storage/timeframe_revocation_ext.rs b/identity_storage/src/storage/timeframe_revocation_ext.rs index 2469e5b218..2b6ffd0f34 100644 --- a/identity_storage/src/storage/timeframe_revocation_ext.rs +++ b/identity_storage/src/storage/timeframe_revocation_ext.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use super::JwkStorageDocumentError as Error; use crate::JwkStorageBbsPlusExt; use crate::KeyIdStorage; diff --git a/identity_stronghold/src/stronghold_jwk_storage_ext.rs b/identity_stronghold/src/stronghold_jwk_storage_ext.rs index a3418027b5..d4a5359770 100644 --- a/identity_stronghold/src/stronghold_jwk_storage_ext.rs +++ b/identity_stronghold/src/stronghold_jwk_storage_ext.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). diff --git a/identity_stronghold/src/stronghold_key_type.rs b/identity_stronghold/src/stronghold_key_type.rs index af0b868831..ee903ec811 100644 --- a/identity_stronghold/src/stronghold_key_type.rs +++ b/identity_stronghold/src/stronghold_key_type.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use std::fmt::Display; use identity_storage::KeyStorageError; diff --git a/identity_stronghold/src/tests/test_bbs_ext.rs b/identity_stronghold/src/tests/test_bbs_ext.rs index 385b7ce627..a2b023f36e 100644 --- a/identity_stronghold/src/tests/test_bbs_ext.rs +++ b/identity_stronghold/src/tests/test_bbs_ext.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_storage::key_storage::bls::expand_bls_jwk; use identity_storage::key_storage::bls::sign_bbs; use identity_storage::JwkGenOutput; diff --git a/identity_stronghold/src/utils.rs b/identity_stronghold/src/utils.rs index b539963ca9..e4d595e0cf 100644 --- a/identity_stronghold/src/utils.rs +++ b/identity_stronghold/src/utils.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_storage::KeyId; use identity_storage::KeyStorageError; use identity_storage::KeyStorageErrorKind; From 614719b9adc09fb0119542d12919fb34f9ab9cd9 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Fri, 26 Apr 2024 11:28:44 +0200 Subject: [PATCH 20/33] fix wasm bindings --- bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs index b8355e4e8b..d92f12e607 100644 --- a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs +++ b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs @@ -29,7 +29,6 @@ use identity_iota::storage::ProofUpdateCtx; use identity_iota::verification::jwk::Jwk; use jsonprooftoken::jpa::algs::ProofAlgorithm; use wasm_bindgen::prelude::*; -use zkryptium::bbsplus::signature::BBSplusSignature; #[wasm_bindgen(js_class = JwkStorage)] impl WasmJwkStorage { @@ -70,9 +69,6 @@ impl WasmJwkStorage { ctx: WasmProofUpdateCtx, ) -> WasmResult { let key_id = KeyId::new(key_id); - let signature = signature - .try_into() - .map_err(|_| JsError::new("Invalid signature length"))?; self .update_signature(&key_id, &public_key.0, &signature, ctx.into()) .await @@ -116,9 +112,9 @@ impl JwkStorageBbsPlusExt for WasmJwkStorage { &self, key_id: &KeyId, public_key: &Jwk, - signature: &[u8; BBSplusSignature::BYTES], + signature: &[u8], ctx: ProofUpdateCtx, - ) -> KeyStorageResult<[u8; BBSplusSignature::BYTES]> { + ) -> KeyStorageResult> { // Extract the required alg from the given public key let alg = public_key .alg() From 096bb303cc1f8e4bacd9a12fbd8e228f6b59136a Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Fri, 26 Apr 2024 12:28:02 +0200 Subject: [PATCH 21/33] Persist Stronghold's changes only when its handle is dropped --- .../src/stronghold_jwk_storage.rs | 17 ++++++++++++++--- .../src/stronghold_jwk_storage_ext.rs | 2 -- identity_stronghold/src/utils.rs | 6 +++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/identity_stronghold/src/stronghold_jwk_storage.rs b/identity_stronghold/src/stronghold_jwk_storage.rs index 24caf83813..4f4e153bb5 100644 --- a/identity_stronghold/src/stronghold_jwk_storage.rs +++ b/identity_stronghold/src/stronghold_jwk_storage.rs @@ -136,7 +136,6 @@ impl JwkStorage for StrongholdStorage { .with_custom_message("stronghold public key procedure failed") .with_source(err) })?; - persist_changes(self, stronghold).await?; let public_key: Vec = procedure_result.into(); let mut params = JwkParamsOkp::new(); @@ -188,7 +187,6 @@ impl JwkStorage for StrongholdStorage { .with_custom_message("stronghold write secret failed") .with_source(err) })?; - persist_changes(self, stronghold).await?; Ok(key_id) } @@ -263,7 +261,6 @@ impl JwkStorage for StrongholdStorage { if !deleted { return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); } - persist_changes(self, stronghold).await?; Ok(()) } @@ -283,3 +280,17 @@ impl JwkStorage for StrongholdStorage { Ok(exists) } } + +/// Calls `persist_changes` when `StrongholdStorage` gets dropped. +impl Drop for StrongholdStorage { + fn drop(&mut self) { + let secret_manager = std::mem::replace(&mut self.0, Arc::new(SecretManager::Placeholder)); + tokio::spawn(async move { + let SecretManager::Stronghold(stronghold) = secret_manager.as_ref() else { + return; + }; + let stronghold = stronghold.inner().await; + let _ = persist_changes(&secret_manager, stronghold).await; + }); + } +} diff --git a/identity_stronghold/src/stronghold_jwk_storage_ext.rs b/identity_stronghold/src/stronghold_jwk_storage_ext.rs index d4a5359770..d29381d19f 100644 --- a/identity_stronghold/src/stronghold_jwk_storage_ext.rs +++ b/identity_stronghold/src/stronghold_jwk_storage_ext.rs @@ -73,8 +73,6 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { .with_source(e) })?; - persist_changes(self, stronghold).await?; - Ok(JwkGenOutput::new(kid, jwk)) } diff --git a/identity_stronghold/src/utils.rs b/identity_stronghold/src/utils.rs index e4d595e0cf..3a9ae72842 100644 --- a/identity_stronghold/src/utils.rs +++ b/identity_stronghold/src/utils.rs @@ -6,6 +6,7 @@ use identity_storage::KeyStorageError; use identity_storage::KeyStorageErrorKind; use identity_storage::KeyStorageResult; use identity_verification::jws::JwsAlgorithm; +use iota_sdk::client::secret::SecretManager; use iota_stronghold::Client; use iota_stronghold::ClientError; use iota_stronghold::Stronghold; @@ -13,7 +14,6 @@ use rand::distributions::DistString as _; use tokio::sync::MutexGuard; use crate::stronghold_key_type::StrongholdKeyType; -use crate::StrongholdStorage; pub static IDENTITY_VAULT_PATH: &str = "iota_identity_vault"; pub static IDENTITY_CLIENT_PATH: &[u8] = b"iota_identity_client"; @@ -54,7 +54,7 @@ fn load_or_create_client(stronghold: &Stronghold) -> KeyStorageResult { } pub async fn persist_changes( - secret_manager: &StrongholdStorage, + secret_manager: &SecretManager, stronghold: MutexGuard<'_, Stronghold>, ) -> KeyStorageResult<()> { stronghold.write_client(IDENTITY_CLIENT_PATH).map_err(|err| { @@ -65,7 +65,7 @@ pub async fn persist_changes( // Must be dropped since `write_stronghold_snapshot` needs to acquire the stronghold lock. drop(stronghold); - match secret_manager.as_secret_manager() { + match secret_manager { iota_sdk::client::secret::SecretManager::Stronghold(stronghold_manager) => { stronghold_manager .write_stronghold_snapshot(None) From b347a1ef4fbb6194f28a3689201438aba9be118f Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Mon, 29 Apr 2024 13:21:25 +0200 Subject: [PATCH 22/33] Fix StrongholdStorage::get_public_key --- Cargo.toml | 2 +- identity_storage/src/key_storage/bls.rs | 3 +- identity_stronghold/src/lib.rs | 2 +- .../src/stronghold_jwk_storage.rs | 75 +++++++- .../src/stronghold_jwk_storage_ext.rs | 165 ------------------ .../src/stronghold_key_type.rs | 8 +- identity_stronghold/src/tests/test_bbs_ext.rs | 6 +- .../src/tests/test_jwk_storage.rs | 5 +- 8 files changed, 87 insertions(+), 179 deletions(-) delete mode 100644 identity_stronghold/src/stronghold_jwk_storage_ext.rs diff --git a/Cargo.toml b/Cargo.toml index fcc0af432f..e9aab70af4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ thiserror = { version = "1.0", default-features = false } strum = { version = "0.25", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0", default-features = false } json-proof-token = { version = "0.3.5" } -zkryptium = { version = "0.2.1", default-features = false, features = ["bbsplus"] } +zkryptium = { version = "0.2.2", default-features = false, features = ["bbsplus"] } [workspace.package] authors = ["IOTA Stiftung"] diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs index b32f5ec86e..0dfb342a92 100644 --- a/identity_storage/src/key_storage/bls.rs +++ b/identity_storage/src/key_storage/bls.rs @@ -25,8 +25,7 @@ fn random_bbs_keypair() -> Result<(BBSplusSecretKey, BBSplusPublicKey), zkryp where S: BbsCiphersuite, { - let key_pair = KeyPair::>::random()?; - Ok((key_pair.private_key().clone(), key_pair.public_key().clone())) + KeyPair::>::random().map(KeyPair::into_parts) } /// Generates a new BBS+ keypair using either `BLS12381-SHA256` or `BLS12381-SHAKE256`. diff --git a/identity_stronghold/src/lib.rs b/identity_stronghold/src/lib.rs index c2a0dd7f82..07f4d4dc96 100644 --- a/identity_stronghold/src/lib.rs +++ b/identity_stronghold/src/lib.rs @@ -4,7 +4,7 @@ pub(crate) mod ed25519; mod stronghold_jwk_storage; #[cfg(any(feature = "bbs-plus", test))] -mod stronghold_jwk_storage_ext; +mod stronghold_jwk_storage_bbs_plus_ext; mod stronghold_key_id; pub(crate) mod stronghold_key_type; #[cfg(test)] diff --git a/identity_stronghold/src/stronghold_jwk_storage.rs b/identity_stronghold/src/stronghold_jwk_storage.rs index 4f4e153bb5..831003962b 100644 --- a/identity_stronghold/src/stronghold_jwk_storage.rs +++ b/identity_stronghold/src/stronghold_jwk_storage.rs @@ -4,6 +4,7 @@ //! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). use async_trait::async_trait; +use identity_storage::key_storage::bls::encode_bls_jwk; use identity_storage::key_storage::JwkStorage; use identity_storage::JwkGenOutput; use identity_storage::KeyId; @@ -19,14 +20,19 @@ use identity_verification::jwu; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; use iota_sdk::client::secret::SecretManager; use iota_stronghold::procedures::Ed25519Sign; +use iota_stronghold::procedures::FatalProcedureError; use iota_stronghold::procedures::GenerateKey; use iota_stronghold::procedures::KeyType as ProceduresKeyType; +use iota_stronghold::procedures::Runner; use iota_stronghold::procedures::StrongholdProcedure; use iota_stronghold::Location; use iota_stronghold::Stronghold; +use jsonprooftoken::jpa::algs::ProofAlgorithm; use std::str::FromStr; use std::sync::Arc; use tokio::sync::MutexGuard; +use zeroize::Zeroizing; +use zkryptium::bbsplus::keys::BBSplusSecretKey; use crate::ed25519; use crate::stronghold_key_type::StrongholdKeyType; @@ -56,7 +62,68 @@ impl StrongholdStorage { } } + async fn get_ed25519_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 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::KeyNotFound).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_owned(); + let mut jwk: Jwk = Jwk::from_params(params); + jwk.set_alg(JwsAlgorithm::EdDSA.name()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + + Ok(jwk) + } + + async fn get_bls12381g2_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(), + ); + + client + .get_guards([location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let pk = sk.public_key(); + let public_jwk = encode_bls_jwk(&sk, &pk, ProofAlgorithm::BLS12381_SHA256).1; + + drop(Zeroizing::new(sk.to_bytes())); + Ok(public_jwk) + }) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).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::Bls12381G2 => self.get_bls12381g2_public_key(key_id).await, + } + } + /// Retrieve the public key corresponding to `key_id`. + #[deprecated(since = "1.3.0", note = "use `get_public_key_with_type` instead")] pub async fn get_public_key(&self, key_id: &KeyId) -> KeyStorageResult { let stronghold = self.get_stronghold().await; let client = get_client(&stronghold)?; @@ -100,8 +167,12 @@ impl JwkStorage for StrongholdStorage { let keytype: ProceduresKeyType = match key_type { StrongholdKeyType::Ed25519 => ProceduresKeyType::Ed25519, - StrongholdKeyType::BLS12381G2 => { - todo!("return an error that instruct the user to call the BBS+ flavor for this function.") + StrongholdKeyType::Bls12381G2 => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "`{key_type}` is supported but `JwkStorageBbsPlusExt::generate_bbs` should be called instead." + )), + ) } }; diff --git a/identity_stronghold/src/stronghold_jwk_storage_ext.rs b/identity_stronghold/src/stronghold_jwk_storage_ext.rs deleted file mode 100644 index d29381d19f..0000000000 --- a/identity_stronghold/src/stronghold_jwk_storage_ext.rs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2020-2024 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -//! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). - -use async_trait::async_trait; -use identity_storage::key_storage::bls::*; -use identity_storage::key_storage::JwkStorage; -use identity_storage::JwkGenOutput; -use identity_storage::JwkStorageBbsPlusExt; -use identity_storage::KeyId; -use identity_storage::KeyStorageError; -use identity_storage::KeyStorageErrorKind; -use identity_storage::KeyStorageResult; -use identity_storage::KeyType; -use identity_storage::ProofUpdateCtx; -use identity_verification::jwk::Jwk; -use iota_stronghold::procedures::FatalProcedureError; -use iota_stronghold::procedures::Products; -use iota_stronghold::procedures::Runner as _; -use iota_stronghold::Location; -use jsonprooftoken::jpa::algs::ProofAlgorithm; -use std::str::FromStr; -use zeroize::Zeroizing; -use zkryptium::bbsplus::keys::BBSplusSecretKey; - -use crate::stronghold_key_type::*; -use crate::utils::*; -use crate::StrongholdStorage; - -#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] -#[cfg_attr(feature = "send-sync-storage", async_trait)] -impl JwkStorageBbsPlusExt for StrongholdStorage { - async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { - let key_type = StrongholdKeyType::try_from(&key_type)?; - - if !matches!(key_type, StrongholdKeyType::BLS12381G2) { - return Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("{key_type} is not supported")), - ); - } - - if !matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { - return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()); - } - - // Get a key id that's not already used. - let mut kid = random_key_id(); - while self.exists(&kid).await? { - kid = random_key_id(); - } - - let stronghold = self.get_stronghold().await; - let client = get_client(&stronghold)?; - let target_key_location = Location::generic( - IDENTITY_VAULT_PATH.as_bytes().to_vec(), - kid.to_string().as_bytes().to_vec(), - ); - let jwk = client - .exec_proc([], &target_key_location, |_| { - let (sk, pk) = generate_bbs_keypair(alg).map_err(|e| FatalProcedureError::from(e.to_string()))?; - let public_jwk = encode_bls_jwk(&sk, &pk, alg).1; - - Ok(Products { - output: public_jwk, - secret: Zeroizing::new(sk.to_bytes().to_vec()), - }) - }) - .map_err(|e| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("Failed to execute stronghold procedure") - .with_source(e) - })?; - - Ok(JwkGenOutput::new(kid, jwk)) - } - - async fn sign_bbs( - &self, - key_id: &KeyId, - data: &[Vec], - header: &[u8], - public_key: &Jwk, - ) -> KeyStorageResult> { - // Extract the required alg from the given public key - let alg = public_key - .alg() - .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .and_then(|alg_str| { - ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) - })?; - - // Check `key_id` exists in store. - if !self.exists(key_id).await? { - return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); - } - - let pk = expand_bls_jwk(public_key) - .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))? - .1; - - let sk_location = Location::Generic { - vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), - record_path: key_id.to_string().as_bytes().to_vec(), - }; - - let stronghold = self.get_stronghold().await; - let client = get_client(&stronghold)?; - client - .get_guards([sk_location], |[sk]| { - let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; - let signature_result = - sign_bbs(alg, data, &sk, &pk, header).map_err(|e| FatalProcedureError::from(e.to_string())); - // clean up `sk` to avoid leaking. - drop(Zeroizing::new(sk.to_bytes())); - signature_result - }) - .map(|sig| sig.to_vec()) - .map_err(|e| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("Signature failed") - .with_source(e) - }) - } - - async fn update_signature( - &self, - key_id: &KeyId, - public_key: &Jwk, - signature: &[u8], - ctx: ProofUpdateCtx, - ) -> KeyStorageResult> { - // Extract the required alg from the given public key - let alg = public_key - .alg() - .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) - .and_then(|alg_str| { - ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) - })?; - - // Check `key_id` exists in store. - if !self.exists(key_id).await? { - return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); - } - - let sk_location = Location::Generic { - vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), - record_path: key_id.to_string().as_bytes().to_vec(), - }; - let stronghold = self.get_stronghold().await; - let client = get_client(&stronghold)?; - - client - .get_guards([sk_location], |[sk]| { - let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; - update_bbs_signature(alg, signature, &sk, &ctx).map_err(|e| FatalProcedureError::from(e.to_string())) - }) - .map_err(|e| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("Signature update failed") - .with_source(e) - }) - } -} diff --git a/identity_stronghold/src/stronghold_key_type.rs b/identity_stronghold/src/stronghold_key_type.rs index ee903ec811..890fce0531 100644 --- a/identity_stronghold/src/stronghold_key_type.rs +++ b/identity_stronghold/src/stronghold_key_type.rs @@ -20,7 +20,7 @@ const BLS12381G2_KEY_TYPE_STR: &str = "BLS12381G2"; #[derive(Debug, Copy, Clone)] pub enum StrongholdKeyType { Ed25519, - BLS12381G2, + Bls12381G2, } impl StrongholdKeyType { @@ -28,7 +28,7 @@ impl StrongholdKeyType { const fn name(&self) -> &'static str { match self { StrongholdKeyType::Ed25519 => ED25519_KEY_TYPE_STR, - StrongholdKeyType::BLS12381G2 => BLS12381G2_KEY_TYPE_STR, + StrongholdKeyType::Bls12381G2 => BLS12381G2_KEY_TYPE_STR, } } } @@ -45,7 +45,7 @@ impl TryFrom<&KeyType> for StrongholdKeyType { fn try_from(value: &KeyType) -> Result { match value.as_str() { ED25519_KEY_TYPE_STR => Ok(StrongholdKeyType::Ed25519), - BLS12381G2_KEY_TYPE_STR => Ok(StrongholdKeyType::BLS12381G2), + BLS12381G2_KEY_TYPE_STR => Ok(StrongholdKeyType::Bls12381G2), _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), } } @@ -91,7 +91,7 @@ impl TryFrom<&Jwk> for StrongholdKeyType { .with_custom_message("only Ed curves are supported for signing") .with_source(err) })? { - BlsCurve::BLS12381G2 => Ok(StrongholdKeyType::BLS12381G2), + BlsCurve::BLS12381G2 => Ok(StrongholdKeyType::Bls12381G2), curve => Err( KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) .with_custom_message(format!("{curve} not supported")), diff --git a/identity_stronghold/src/tests/test_bbs_ext.rs b/identity_stronghold/src/tests/test_bbs_ext.rs index a2b023f36e..efa71f3cc2 100644 --- a/identity_stronghold/src/tests/test_bbs_ext.rs +++ b/identity_stronghold/src/tests/test_bbs_ext.rs @@ -23,7 +23,7 @@ use crate::StrongholdStorage; async fn stronghold_bbs_keypair_gen_works() -> anyhow::Result<()> { let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); let JwkGenOutput { key_id, jwk, .. } = stronghold_storage - .generate_bbs(StrongholdKeyType::BLS12381G2.into(), ProofAlgorithm::BLS12381_SHA256) + .generate_bbs(StrongholdKeyType::Bls12381G2.into(), ProofAlgorithm::BLS12381_SHA256) .await?; assert!(jwk.is_public()); @@ -48,7 +48,7 @@ async fn stronghold_bbs_keypair_gen_fails_with_wrong_key_type() -> anyhow::Resul async fn stronghold_bbs_keypair_gen_fails_with_wrong_alg() -> anyhow::Result<()> { let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); let error = stronghold_storage - .generate_bbs(StrongholdKeyType::BLS12381G2.into(), ProofAlgorithm::MAC_H256) + .generate_bbs(StrongholdKeyType::Bls12381G2.into(), ProofAlgorithm::MAC_H256) .await .unwrap_err(); @@ -61,7 +61,7 @@ async fn stronghold_bbs_keypair_gen_fails_with_wrong_alg() -> anyhow::Result<()> async fn stronghold_sign_bbs_works() -> anyhow::Result<()> { let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); let JwkGenOutput { key_id, jwk, .. } = stronghold_storage - .generate_bbs(StrongholdKeyType::BLS12381G2.into(), ProofAlgorithm::BLS12381_SHA256) + .generate_bbs(StrongholdKeyType::Bls12381G2.into(), ProofAlgorithm::BLS12381_SHA256) .await?; let pk = expand_bls_jwk(&jwk)?.1; let sk = { diff --git a/identity_stronghold/src/tests/test_jwk_storage.rs b/identity_stronghold/src/tests/test_jwk_storage.rs index 61e25af808..ca775c95aa 100644 --- a/identity_stronghold/src/tests/test_jwk_storage.rs +++ b/identity_stronghold/src/tests/test_jwk_storage.rs @@ -33,7 +33,10 @@ async fn retrieve() { .unwrap(); let key_id = &generate.key_id; - let pub_key: Jwk = stronghold_storage.get_public_key(key_id).await.unwrap(); + let pub_key: Jwk = stronghold_storage + .get_public_key_with_type(key_id, crate::stronghold_key_type::StrongholdKeyType::Ed25519) + .await + .unwrap(); assert_eq!(generate.jwk, pub_key); } From 453103c77a842e4eafdde7391775c97a1785c00a Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Mon, 29 Apr 2024 13:22:19 +0200 Subject: [PATCH 23/33] rename stronghold_jwk_storage_ext --- .../stronghold_jwk_storage_bbs_plus_ext.rs | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 identity_stronghold/src/stronghold_jwk_storage_bbs_plus_ext.rs diff --git a/identity_stronghold/src/stronghold_jwk_storage_bbs_plus_ext.rs b/identity_stronghold/src/stronghold_jwk_storage_bbs_plus_ext.rs new file mode 100644 index 0000000000..583d5f0841 --- /dev/null +++ b/identity_stronghold/src/stronghold_jwk_storage_bbs_plus_ext.rs @@ -0,0 +1,174 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). + +use async_trait::async_trait; +use identity_storage::key_storage::bls::*; +use identity_storage::key_storage::JwkStorage; +use identity_storage::JwkGenOutput; +use identity_storage::JwkStorageBbsPlusExt; +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_storage::KeyType; +use identity_storage::ProofUpdateCtx; +use identity_verification::jwk::Jwk; +use iota_stronghold::procedures::FatalProcedureError; +use iota_stronghold::procedures::Products; +use iota_stronghold::procedures::Runner as _; +use iota_stronghold::Location; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use std::str::FromStr; +use zeroize::Zeroizing; +use zkryptium::bbsplus::keys::BBSplusSecretKey; + +use crate::stronghold_key_type::*; +use crate::utils::*; +use crate::StrongholdStorage; + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl JwkStorageBbsPlusExt for StrongholdStorage { + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + let key_type = StrongholdKeyType::try_from(&key_type)?; + + if !matches!(key_type, StrongholdKeyType::Bls12381G2) { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{key_type} is not supported")), + ); + } + + if !matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { + return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()); + } + + // Get a key id that's not already used. + let mut kid = random_key_id(); + while self.exists(&kid).await? { + kid = random_key_id(); + } + + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + let target_key_location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + kid.to_string().as_bytes().to_vec(), + ); + let jwk = client + .exec_proc([], &target_key_location, |_| { + let (sk, pk) = generate_bbs_keypair(alg).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let public_jwk = encode_bls_jwk(&sk, &pk, alg).1; + + Ok(Products { + output: public_jwk, + secret: Zeroizing::new(sk.to_bytes().to_vec()), + }) + }) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Failed to execute stronghold procedure") + .with_source(e) + })?; + + Ok(JwkGenOutput::new(kid, jwk)) + } + + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult> { + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + // Check `key_id` exists in store. + if !self.exists(key_id).await? { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + } + + let pk = expand_bls_jwk(public_key) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))? + .1; + + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), + record_path: key_id.to_string().as_bytes().to_vec(), + }; + + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + client + .get_guards([sk_location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + // Ensure `sk` and `pk` matches. + if sk.public_key() != pk { + return Err(FatalProcedureError::from( + "`public_key` is not the public key of key with id `key_id`".to_owned(), + )); + } + let signature_result = + sign_bbs(alg, data, &sk, &pk, header).map_err(|e| FatalProcedureError::from(e.to_string())); + // clean up `sk` to avoid leaking. + drop(Zeroizing::new(sk.to_bytes())); + signature_result + }) + .map(|sig| sig.to_vec()) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature failed") + .with_source(e) + }) + } + + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult> { + // Extract the required alg from the given public key + let alg = public_key + .alg() + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm) + .and_then(|alg_str| { + ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm) + })?; + + // Check `key_id` exists in store. + if !self.exists(key_id).await? { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + } + + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), + record_path: key_id.to_string().as_bytes().to_vec(), + }; + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + client + .get_guards([sk_location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let signature_update_result = + update_bbs_signature(alg, signature, &sk, &ctx).map_err(|e| FatalProcedureError::from(e.to_string())); + drop(Zeroizing::new(sk.to_bytes())); + signature_update_result + }) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature update failed") + .with_source(e) + }) + } +} From 9632e8db75ae5afcfd1d7307b43fddfb45f3ea94 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Mon, 29 Apr 2024 14:34:02 +0200 Subject: [PATCH 24/33] Add inx-faucet profile in CI --- .github/actions/iota-sandbox/setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/iota-sandbox/setup/action.yml b/.github/actions/iota-sandbox/setup/action.yml index b62c175e7f..8b32b8608d 100644 --- a/.github/actions/iota-sandbox/setup/action.yml +++ b/.github/actions/iota-sandbox/setup/action.yml @@ -23,7 +23,7 @@ runs: # Start Tangle sudo ./bootstrap.sh - docker compose up -d + docker compose --profile inx-faucet up -d - name: Wait for tangle to start shell: bash run: wget -qO- https://raw.githubusercontent.com/eficode/wait-for/$WAIT_FOR_VERSION/wait-for | sh -s -- -t 60 http://localhost/health -- echo "Tangle is up" From 479f4bcaabc3d9e46e91ae7e983473432d5ca3ef Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 30 Apr 2024 11:07:01 +0200 Subject: [PATCH 25/33] change stronghold crate's structure, revert persist changes on drop --- identity_stronghold/src/lib.rs | 7 +- identity_stronghold/src/storage/mod.rs | 163 ++++++++++++++++++ .../{ => storage}/stronghold_jwk_storage.rs | 148 +--------------- .../stronghold_jwk_storage_bbs_plus_ext.rs | 4 +- .../src/{ => storage}/stronghold_key_id.rs | 0 5 files changed, 173 insertions(+), 149 deletions(-) create mode 100644 identity_stronghold/src/storage/mod.rs rename identity_stronghold/src/{ => storage}/stronghold_jwk_storage.rs (59%) rename identity_stronghold/src/{ => storage}/stronghold_jwk_storage_bbs_plus_ext.rs (98%) rename identity_stronghold/src/{ => storage}/stronghold_key_id.rs (100%) diff --git a/identity_stronghold/src/lib.rs b/identity_stronghold/src/lib.rs index 07f4d4dc96..9de8ca6472 100644 --- a/identity_stronghold/src/lib.rs +++ b/identity_stronghold/src/lib.rs @@ -2,13 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod ed25519; -mod stronghold_jwk_storage; -#[cfg(any(feature = "bbs-plus", test))] -mod stronghold_jwk_storage_bbs_plus_ext; -mod stronghold_key_id; +mod storage; pub(crate) mod stronghold_key_type; #[cfg(test)] mod tests; pub(crate) mod utils; -pub use stronghold_jwk_storage::*; +pub use storage::*; diff --git a/identity_stronghold/src/storage/mod.rs b/identity_stronghold/src/storage/mod.rs new file mode 100644 index 0000000000..7ad8b26ffa --- /dev/null +++ b/identity_stronghold/src/storage/mod.rs @@ -0,0 +1,163 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod stronghold_jwk_storage; +#[cfg(any(feature = "bbs-plus", test))] +mod stronghold_jwk_storage_bbs_plus_ext; +mod stronghold_key_id; + +use std::sync::Arc; + +#[cfg(feature = "bbs-plus")] +use identity_storage::key_storage::bls::encode_bls_jwk; +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_verification::jwk::EdCurve; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkParamsOkp; +use identity_verification::jws::JwsAlgorithm; +use identity_verification::jwu; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; +#[cfg(feature = "bbs-plus")] +use iota_stronghold::procedures::FatalProcedureError; +use iota_stronghold::procedures::KeyType as ProceduresKeyType; +#[cfg(feature = "bbs-plus")] +use iota_stronghold::procedures::Runner as _; +use iota_stronghold::procedures::StrongholdProcedure; +use iota_stronghold::Location; +use iota_stronghold::Stronghold; +#[cfg(feature = "bbs-plus")] +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use tokio::sync::MutexGuard; +#[cfg(feature = "bbs-plus")] +use zeroize::Zeroizing; +#[cfg(feature = "bbs-plus")] +use zkryptium::bbsplus::keys::BBSplusSecretKey; + +use crate::stronghold_key_type::StrongholdKeyType; +use crate::utils::get_client; +use crate::utils::IDENTITY_VAULT_PATH; + +/// Wrapper around a [`StrongholdSecretManager`] that implements the [`KeyIdStorage`](crate::KeyIdStorage) +/// and [`JwkStorage`](crate::JwkStorage) interfaces. +#[derive(Clone, Debug)] +pub struct StrongholdStorage(Arc); + +impl StrongholdStorage { + /// Creates a new [`StrongholdStorage`]. + pub fn new(stronghold_secret_manager: StrongholdSecretManager) -> Self { + Self(Arc::new(SecretManager::Stronghold(stronghold_secret_manager))) + } + + /// Shared reference to the inner [`SecretManager`]. + pub fn as_secret_manager(&self) -> &SecretManager { + self.0.as_ref() + } + + /// Acquire lock of the inner [`Stronghold`]. + pub(crate) async fn get_stronghold(&self) -> MutexGuard<'_, Stronghold> { + match *self.0 { + SecretManager::Stronghold(ref stronghold) => stronghold.inner().await, + _ => unreachable!("secret manager can be only constructed from stronghold"), + } + } + + async fn get_ed25519_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 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::KeyNotFound).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_owned(); + let mut jwk: Jwk = Jwk::from_params(params); + jwk.set_alg(JwsAlgorithm::EdDSA.name()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + + Ok(jwk) + } + + #[cfg(feature = "bbs-plus")] + async fn get_bls12381g2_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(), + ); + + client + .get_guards([location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let pk = sk.public_key(); + let public_jwk = encode_bls_jwk(&sk, &pk, ProofAlgorithm::BLS12381_SHA256).1; + + drop(Zeroizing::new(sk.to_bytes())); + Ok(public_jwk) + }) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).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, + #[cfg(feature = "bbs-plus")] + StrongholdKeyType::Bls12381G2 => self.get_bls12381g2_public_key(key_id).await, + #[allow(unreachable_patterns)] + _ => Err(KeyStorageErrorKind::UnsupportedKeyType.into()), + } + } + + /// Retrieve the public key corresponding to `key_id`. + #[deprecated(since = "1.3.0", note = "use `get_public_key_with_type` instead")] + pub async fn get_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 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::KeyNotFound).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_owned(); + let mut jwk: Jwk = Jwk::from_params(params); + jwk.set_alg(JwsAlgorithm::EdDSA.name()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + + Ok(jwk) + } +} diff --git a/identity_stronghold/src/stronghold_jwk_storage.rs b/identity_stronghold/src/storage/stronghold_jwk_storage.rs similarity index 59% rename from identity_stronghold/src/stronghold_jwk_storage.rs rename to identity_stronghold/src/storage/stronghold_jwk_storage.rs index 831003962b..1ab55225fc 100644 --- a/identity_stronghold/src/stronghold_jwk_storage.rs +++ b/identity_stronghold/src/storage/stronghold_jwk_storage.rs @@ -4,7 +4,6 @@ //! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). use async_trait::async_trait; -use identity_storage::key_storage::bls::encode_bls_jwk; use identity_storage::key_storage::JwkStorage; use identity_storage::JwkGenOutput; use identity_storage::KeyId; @@ -17,143 +16,17 @@ use identity_verification::jwk::Jwk; use identity_verification::jwk::JwkParamsOkp; use identity_verification::jws::JwsAlgorithm; use identity_verification::jwu; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; use iota_stronghold::procedures::Ed25519Sign; -use iota_stronghold::procedures::FatalProcedureError; use iota_stronghold::procedures::GenerateKey; use iota_stronghold::procedures::KeyType as ProceduresKeyType; -use iota_stronghold::procedures::Runner; use iota_stronghold::procedures::StrongholdProcedure; use iota_stronghold::Location; -use iota_stronghold::Stronghold; -use jsonprooftoken::jpa::algs::ProofAlgorithm; use std::str::FromStr; -use std::sync::Arc; -use tokio::sync::MutexGuard; -use zeroize::Zeroizing; -use zkryptium::bbsplus::keys::BBSplusSecretKey; use crate::ed25519; use crate::stronghold_key_type::StrongholdKeyType; use crate::utils::*; - -/// Wrapper around a [`StrongholdSecretManager`] that implements the [`KeyIdStorage`](crate::KeyIdStorage) -/// and [`JwkStorage`](crate::JwkStorage) interfaces. -#[derive(Clone, Debug)] -pub struct StrongholdStorage(Arc); - -impl StrongholdStorage { - /// Creates a new [`StrongholdStorage`]. - pub fn new(stronghold_secret_manager: StrongholdSecretManager) -> Self { - Self(Arc::new(SecretManager::Stronghold(stronghold_secret_manager))) - } - - /// Shared reference to the inner [`SecretManager`]. - pub fn as_secret_manager(&self) -> &SecretManager { - self.0.as_ref() - } - - /// Acquire lock of the inner [`Stronghold`]. - pub(crate) async fn get_stronghold(&self) -> MutexGuard<'_, Stronghold> { - match *self.0 { - SecretManager::Stronghold(ref stronghold) => stronghold.inner().await, - _ => unreachable!("secret manager can be only constructed from stronghold"), - } - } - - async fn get_ed25519_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 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::KeyNotFound).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_owned(); - let mut jwk: Jwk = Jwk::from_params(params); - jwk.set_alg(JwsAlgorithm::EdDSA.name()); - jwk.set_kid(jwk.thumbprint_sha256_b64()); - - Ok(jwk) - } - - async fn get_bls12381g2_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(), - ); - - client - .get_guards([location], |[sk]| { - let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; - let pk = sk.public_key(); - let public_jwk = encode_bls_jwk(&sk, &pk, ProofAlgorithm::BLS12381_SHA256).1; - - drop(Zeroizing::new(sk.to_bytes())); - Ok(public_jwk) - }) - .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).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::Bls12381G2 => self.get_bls12381g2_public_key(key_id).await, - } - } - - /// Retrieve the public key corresponding to `key_id`. - #[deprecated(since = "1.3.0", note = "use `get_public_key_with_type` instead")] - pub async fn get_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 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::KeyNotFound).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_owned(); - let mut jwk: Jwk = Jwk::from_params(params); - jwk.set_alg(JwsAlgorithm::EdDSA.name()); - jwk.set_kid(jwk.thumbprint_sha256_b64()); - - Ok(jwk) - } -} +use crate::StrongholdStorage; #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] @@ -208,6 +81,7 @@ impl JwkStorage for StrongholdStorage { .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); @@ -259,6 +133,8 @@ impl JwkStorage for StrongholdStorage { .with_source(err) })?; + persist_changes(self.as_secret_manager(), stronghold).await?; + Ok(key_id) } @@ -333,6 +209,8 @@ impl JwkStorage for StrongholdStorage { return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); } + persist_changes(self.as_secret_manager(), stronghold).await?; + Ok(()) } @@ -351,17 +229,3 @@ impl JwkStorage for StrongholdStorage { Ok(exists) } } - -/// Calls `persist_changes` when `StrongholdStorage` gets dropped. -impl Drop for StrongholdStorage { - fn drop(&mut self) { - let secret_manager = std::mem::replace(&mut self.0, Arc::new(SecretManager::Placeholder)); - tokio::spawn(async move { - let SecretManager::Stronghold(stronghold) = secret_manager.as_ref() else { - return; - }; - let stronghold = stronghold.inner().await; - let _ = persist_changes(&secret_manager, stronghold).await; - }); - } -} diff --git a/identity_stronghold/src/stronghold_jwk_storage_bbs_plus_ext.rs b/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs similarity index 98% rename from identity_stronghold/src/stronghold_jwk_storage_bbs_plus_ext.rs rename to identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs index 583d5f0841..10fbe7faa0 100644 --- a/identity_stronghold/src/stronghold_jwk_storage_bbs_plus_ext.rs +++ b/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs @@ -1,8 +1,6 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! Wrapper around [`StrongholdSecretManager`](StrongholdSecretManager). - use async_trait::async_trait; use identity_storage::key_storage::bls::*; use identity_storage::key_storage::JwkStorage; @@ -73,6 +71,8 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { .with_source(e) })?; + persist_changes(self.as_secret_manager(), stronghold).await?; + Ok(JwkGenOutput::new(kid, jwk)) } diff --git a/identity_stronghold/src/stronghold_key_id.rs b/identity_stronghold/src/storage/stronghold_key_id.rs similarity index 100% rename from identity_stronghold/src/stronghold_key_id.rs rename to identity_stronghold/src/storage/stronghold_key_id.rs From 87a744ccb2be3678a27dfc6b2948e6a5e504cef8 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 30 Apr 2024 12:00:02 +0200 Subject: [PATCH 26/33] review comments --- identity_credential/src/credential/mod.rs | 4 ++ .../presentation/jwp_presentation_builder.rs | 48 ++++++++++++------- .../jpt_presentation_validator.rs | 2 +- .../src/storage/jwp_document_ext.rs | 8 ++-- identity_stronghold/Cargo.toml | 2 +- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 77c65f74af..72f3b5d7a8 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -9,7 +9,9 @@ mod builder; mod credential; mod evidence; mod issuer; +#[cfg(feature = "jpt-bbs-plus")] mod jpt; +#[cfg(feature = "jpt-bbs-plus")] mod jwp_credential_options; mod jws; mod jwt; @@ -28,7 +30,9 @@ pub use self::builder::CredentialBuilder; pub use self::credential::Credential; pub use self::evidence::Evidence; pub use self::issuer::Issuer; +#[cfg(feature = "jpt-bbs-plus")] pub use self::jpt::Jpt; +#[cfg(feature = "jpt-bbs-plus")] pub use self::jwp_credential_options::JwpCredentialOptions; pub use self::jws::Jws; pub use self::jwt::Jwt; diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs index 517c86ae44..f245bb1ad5 100644 --- a/identity_credential/src/presentation/jwp_presentation_builder.rs +++ b/identity_credential/src/presentation/jwp_presentation_builder.rs @@ -7,19 +7,19 @@ use jsonprooftoken::jwp::header::PresentationProtectedHeader; use jsonprooftoken::jwp::issued::JwpIssued; use jsonprooftoken::jwp::presented::JwpPresentedBuilder; -/// Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes -/// - @context MUST NOT be blinded -/// - id MUST be blinded -/// - type MUST NOT be blinded -/// - issuer MUST NOT be blinded -/// - issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used) -/// - expirationDate MUST be blinded (if Timeframe Revocation mechanism is used) -/// - credentialSubject (User have to choose which attribute must be blinded) -/// - credentialSchema MUST NOT be blinded -/// - credentialStatus MUST NOT be blinded -/// - refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism) -/// - termsOfUse NO reason to use it in ZK VC (will be in any case blinded) -/// - evidence (User have to choose which attribute must be blinded) +/// Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes. +// - @context MUST NOT be blinded +// - id MUST be blinded +// - type MUST NOT be blinded +// - issuer MUST NOT be blinded +// - issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used) +// - expirationDate MUST be blinded (if Timeframe Revocation mechanism is used) +// - credentialSubject (Users have to choose which attribute must be blinded) +// - credentialSchema MUST NOT be blinded +// - credentialStatus MUST NOT be blinded +// - refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism) +// - termsOfUse NO reason to use it in ZK VC (will be in any case blinded) +// - evidence (Users have to choose which attribute must be blinded) pub struct SelectiveDisclosurePresentation { jwp_builder: JwpPresentedBuilder, } @@ -50,7 +50,7 @@ impl SelectiveDisclosurePresentation { Self { jwp_builder } } - /// Selectively disclose "credentialSubject" attributes. + /// Selectively conceal "credentialSubject" attributes. /// # Example /// ```ignore /// { @@ -66,8 +66,8 @@ impl SelectiveDisclosurePresentation { /// ``` /// If you want to undisclose for example the Mathematics course and the name of the degree: /// ```ignore - /// undisclose_subject("mainCourses[1]"); - /// undisclose_subject("degree.name"); + /// presentation_builder.conceal_in_subject("mainCourses[1]"); + /// presentation_builder.conceal_in_subject("degree.name"); /// ``` pub fn conceal_in_subject(&mut self, path: &str) -> Result<(), Error> { let _ = self @@ -78,6 +78,22 @@ impl SelectiveDisclosurePresentation { } /// Undisclose "evidence" attributes. + /// # Example + /// ```ignore + /// { + /// "id": "https://example.edu/evidence/f2aeec97-fc0d-42bf-8ca7-0548192d4231", + /// "type": ["DocumentVerification"], + /// "verifier": "https://example.edu/issuers/14", + /// "evidenceDocument": "DriversLicense", + /// "subjectPresence": "Physical", + /// "documentPresence": "Physical", + /// "licenseNumber": "123AB4567" + /// } + /// ``` + /// To conceal the `licenseNumber` field: + /// ```ignore + /// presentation_builder.conceal_in_evidence("licenseNumber"); + /// ``` pub fn conceal_in_evidence(&mut self, path: &str) -> Result<(), Error> { let _ = self .jwp_builder diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs index 25327ae3db..da3b70ca92 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs @@ -171,7 +171,7 @@ impl JptPresentationValidator { where T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, { - //Verify Jwp proof + // Verify Jwp proof let decoded_jwp = decoded .verify(public_key) .map_err(JwtValidationError::JwpProofVerificationError)?; diff --git a/identity_storage/src/storage/jwp_document_ext.rs b/identity_storage/src/storage/jwp_document_ext.rs index 8d5629461c..318cff6782 100644 --- a/identity_storage/src/storage/jwp_document_ext.rs +++ b/identity_storage/src/storage/jwp_document_ext.rs @@ -33,12 +33,12 @@ use jsonprooftoken::jwp::issued::JwpIssuedBuilder; use serde::de::DeserializeOwned; use serde::Serialize; -///New trait to handle JWP-based operations on DID Documents +/// Handle JWP-based operations on DID Documents. #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] pub trait JwpDocumentExt { /// Generate new key material in the given `storage` and insert a new verification method with the corresponding - /// public key material into the DID document. This support BBS+ keys. + /// public key material into the DID document. This supports BBS+ keys. async fn generate_method_jwp( &mut self, storage: &Storage, @@ -52,7 +52,7 @@ pub trait JwpDocumentExt { I: KeyIdStorage; /// Compute a JWP in the Issued form representing the Verifiable Credential - /// See [JSON Web Proof draft section 4.1](https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-issued-form) + /// See [JSON Web Proof draft](https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-issued-form) async fn create_issued_jwp( &self, storage: &Storage, @@ -65,7 +65,7 @@ pub trait JwpDocumentExt { I: KeyIdStorage; /// Compute a JWP in the Presented form representing the presented Verifiable Credential after the Selective - /// Disclosure of attributes See [JSON Web Proof draft section 4.2](https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form) + /// Disclosure of attributes See [JSON Web Proof draft](https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form) async fn create_presented_jwp( &self, presentation: &mut SelectiveDisclosurePresentation, diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index 77cf48e02e..6fcf93c4e4 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -12,7 +12,6 @@ rust-version.workspace = true description = "Secure JWK storage with Stronghold for IOTA Identity" [dependencies] -anyhow = "1.0.82" async-trait = { version = "0.1.64", default-features = false } identity_storage = { version = "=1.2.0", path = "../identity_storage", default_features = false } identity_verification = { version = "=1.2.0", path = "../identity_verification", default_features = false } @@ -26,6 +25,7 @@ zeroize = { version = "1.6.0", default_features = false } zkryptium = { workspace = true, optional = true } [dev-dependencies] +anyhow = "1.0.82" identity_did = { version = "=1.2.0", path = "../identity_did", default_features = false } identity_storage = { version = "=1.2.0", path = "../identity_storage", default_features = false, features = ["jpt-bbs-plus"] } json-proof-token = { workspace = true } From ef85f1e02469fee22217967e19f9b88acb2fbcc1 Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue, 30 Apr 2024 12:01:02 +0200 Subject: [PATCH 27/33] Update identity_credential/src/presentation/jwp_presentation_builder.rs Co-authored-by: wulfraem --- .../src/presentation/jwp_presentation_builder.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs index f245bb1ad5..84d29f818e 100644 --- a/identity_credential/src/presentation/jwp_presentation_builder.rs +++ b/identity_credential/src/presentation/jwp_presentation_builder.rs @@ -26,7 +26,17 @@ pub struct SelectiveDisclosurePresentation { impl SelectiveDisclosurePresentation { /// Initialize a presentation starting from an Issued JWP. - /// The properties `jti`, `nbf`, `issuanceDate`, `expirationDate` and `termsOfUse` are concealed by default. + /// The following properties are concealed by default: + /// + /// - `exp` + /// - `expirationDate` + /// - `issuanceDate` + /// - `jti` + /// - `nbf` + /// - `sub` + /// - `termsOfUse` + /// - `vc.credentialStatus.revocationBitmapIndex` + /// - `vc.credentialSubject.id` pub fn new(issued_jwp: &JwpIssued) -> Self { let mut jwp_builder = JwpPresentedBuilder::new(issued_jwp); From 041fbab39e3228373050a38e068657ce35389da2 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 30 Apr 2024 13:36:08 +0200 Subject: [PATCH 28/33] fix wasm bindings --- bindings/wasm/Cargo.toml | 2 +- bindings/wasm/package.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index b3db2773ac..819da13d7b 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -30,7 +30,7 @@ serde_repr = { version = "0.1", default-features = false } tokio = { version = "1.29", default-features = false, features = ["sync"] } wasm-bindgen = { version = "0.2.85", features = ["serde-serialize"] } wasm-bindgen-futures = { version = "0.4", default-features = false } -zkryptium = "0.2.1" +zkryptium = "0.2.2" [dependencies.identity_iota] path = "../../identity_iota" diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index 193e761136..b9f3404439 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -15,7 +15,7 @@ "bundle:web": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target web --out-dir web && node ./build/web && tsc --project ./lib/tsconfig.web.json && node ./build/replace_paths ./lib/tsconfig.web.json web", "build:nodejs": "npm run build:src && npm run bundle:nodejs && wasm-opt -O node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm", "build:web": "npm run build:src && npm run bundle:web && wasm-opt -O web/identity_wasm_bg.wasm -o web/identity_wasm_bg.wasm", - "build:docs": "node ./build/docs", + "build:docs": "npm run fix_js_doc && node ./build/docs", "build:examples:web": "tsc --project ./examples/tsconfig.web.json && node ./build/replace_paths ./examples/tsconfig.web.json ./examples/dist resolve", "build": "npm run build:web && npm run build:nodejs && npm run build:docs", "example:node": "ts-node --project tsconfig.node.json -r tsconfig-paths/register ./examples/src/main.ts", @@ -28,7 +28,8 @@ "test:readme:rust": "mocha ./tests/txm_readme_rust.js --retries 3 --timeout 360000 --exit", "test:unit:node": "ts-mocha -p tsconfig.node.json ./tests/*.ts --parallel --exit", "cypress": "cypress open", - "fmt": "dprint fmt" + "fmt": "dprint fmt", + "fix_js_doc": "sed -Ei 's/\\((.*)\\)\\[\\]/\\1\\[\\]/' ./node/identity_wasm.js" }, "config": { "CYPRESS_VERIFY_TIMEOUT": 100000 From 5135f0b14953471122c704d797f36e79a121b9d6 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 30 Apr 2024 14:11:03 +0200 Subject: [PATCH 29/33] expose stronghold's key types --- .../src/presentation/jwp_presentation_builder.rs | 6 +++--- identity_stronghold/src/lib.rs | 1 + identity_stronghold/src/stronghold_key_type.rs | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs index 84d29f818e..62a37e92c1 100644 --- a/identity_credential/src/presentation/jwp_presentation_builder.rs +++ b/identity_credential/src/presentation/jwp_presentation_builder.rs @@ -34,9 +34,9 @@ impl SelectiveDisclosurePresentation { /// - `jti` /// - `nbf` /// - `sub` - /// - `termsOfUse` - /// - `vc.credentialStatus.revocationBitmapIndex` - /// - `vc.credentialSubject.id` + /// - `termsOfUse` + /// - `vc.credentialStatus.revocationBitmapIndex` + /// - `vc.credentialSubject.id` pub fn new(issued_jwp: &JwpIssued) -> Self { let mut jwp_builder = JwpPresentedBuilder::new(issued_jwp); diff --git a/identity_stronghold/src/lib.rs b/identity_stronghold/src/lib.rs index 9de8ca6472..ae8f8aef5b 100644 --- a/identity_stronghold/src/lib.rs +++ b/identity_stronghold/src/lib.rs @@ -9,3 +9,4 @@ mod tests; pub(crate) mod utils; pub use storage::*; +pub use stronghold_key_type::*; diff --git a/identity_stronghold/src/stronghold_key_type.rs b/identity_stronghold/src/stronghold_key_type.rs index 890fce0531..c78deb4d3a 100644 --- a/identity_stronghold/src/stronghold_key_type.rs +++ b/identity_stronghold/src/stronghold_key_type.rs @@ -11,10 +11,12 @@ use identity_verification::jwk::EdCurve; use identity_verification::jwk::Jwk; use identity_verification::jwk::JwkType; +pub const ED25519_KEY_TYPE_STR: &str = "Ed25519"; /// The Ed25519 key type. -const ED25519_KEY_TYPE_STR: &str = "Ed25519"; +pub const ED25519_KEY_TYPE: KeyType = KeyType::from_static_str(ED25519_KEY_TYPE_STR); +pub const BLS12381G2_KEY_TYPE_STR: &str = "BLS12381G2"; /// The BLS12381G2 key type -const BLS12381G2_KEY_TYPE_STR: &str = "BLS12381G2"; +pub const BLS12381G2_KEY_TYPE: KeyType = KeyType::from_static_str(BLS12381G2_KEY_TYPE_STR); /// Key Types supported by the stronghold storage implementation. #[derive(Debug, Copy, Clone)] From 63df8d406ee93eed3e7faeb2783c3405c9f98ad6 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 8 May 2024 11:21:51 +0200 Subject: [PATCH 30/33] revert last commit --- .../revocation/status_list_2021/credential.rs | 6 ++++- identity_jose/src/jwk/key_params.rs | 24 +++++++++++++++++++ identity_jose/src/jwt/header.rs | 6 +++++ identity_jose/src/jwu/serde.rs | 4 ++-- identity_storage/src/key_storage/bls.rs | 7 +++++- identity_storage/src/key_storage/ed25519.rs | 2 +- identity_storage/src/key_storage/memstore.rs | 6 +++-- identity_stronghold/src/storage/mod.rs | 4 ++-- .../src/storage/stronghold_jwk_storage.rs | 2 +- 9 files changed, 51 insertions(+), 10 deletions(-) diff --git a/identity_credential/src/revocation/status_list_2021/credential.rs b/identity_credential/src/revocation/status_list_2021/credential.rs index cc52916967..e2ef0950cd 100644 --- a/identity_credential/src/revocation/status_list_2021/credential.rs +++ b/identity_credential/src/revocation/status_list_2021/credential.rs @@ -279,7 +279,11 @@ impl StatusList2021CredentialSubject { return Err(StatusList2021CredentialError::MultipleCredentialSubject); }; if let Some(subject_type) = subject.properties.get("type") { - if !subject_type.as_str().is_some_and(|t| t == CREDENTIAL_SUBJECT_TYPE) { + if !subject_type + .as_str() + .map(|t| t == CREDENTIAL_SUBJECT_TYPE) + .unwrap_or(false) + { return Err(StatusList2021CredentialError::InvalidProperty("credentialSubject.type")); } } else { diff --git a/identity_jose/src/jwk/key_params.rs b/identity_jose/src/jwk/key_params.rs index ba3ca23059..9d1437637a 100644 --- a/identity_jose/src/jwk/key_params.rs +++ b/identity_jose/src/jwk/key_params.rs @@ -105,6 +105,12 @@ pub struct JwkParamsEc { pub d: Option, // ECC Private Key } +impl Default for JwkParamsEc { + fn default() -> Self { + Self::new() + } +} + impl JwkParamsEc { /// Creates new JWK EC Params. pub const fn new() -> Self { @@ -251,6 +257,12 @@ pub struct JwkParamsRsaPrime { pub t: String, // Factor CRT Coefficient } +impl Default for JwkParamsRsa { + fn default() -> Self { + Self::new() + } +} + impl JwkParamsRsa { /// Creates new JWK RSA Params. pub const fn new() -> Self { @@ -333,6 +345,12 @@ pub struct JwkParamsOct { pub k: String, // Key Value } +impl Default for JwkParamsOct { + fn default() -> Self { + Self::new() + } +} + impl JwkParamsOct { /// Creates new JWK Oct Params. pub const fn new() -> Self { @@ -382,6 +400,12 @@ pub struct JwkParamsOkp { pub d: Option, // Private Key } +impl Default for JwkParamsOkp { + fn default() -> Self { + Self::new() + } +} + impl JwkParamsOkp { /// Creates new JWK OKP Params. pub const fn new() -> Self { diff --git a/identity_jose/src/jwt/header.rs b/identity_jose/src/jwt/header.rs index 631bd151c3..ca87211c84 100644 --- a/identity_jose/src/jwt/header.rs +++ b/identity_jose/src/jwt/header.rs @@ -105,6 +105,12 @@ pub struct JwtHeader { nonce: Option, } +impl Default for JwtHeader { + fn default() -> Self { + Self::new() + } +} + impl JwtHeader { /// Create a new `JwtHeader`. pub const fn new() -> Self { diff --git a/identity_jose/src/jwu/serde.rs b/identity_jose/src/jwu/serde.rs index a5e6c1f84d..cd80a1c949 100644 --- a/identity_jose/src/jwu/serde.rs +++ b/identity_jose/src/jwu/serde.rs @@ -24,10 +24,10 @@ pub(crate) fn parse_utf8(slice: &(impl AsRef<[u8]> + ?Sized)) -> Result<&str> { str::from_utf8(slice.as_ref()).map_err(Error::InvalidUtf8) } -pub(crate) fn filter_non_empty_bytes<'a, T, U: 'a>(value: T) -> Option<&'a [u8]> +pub(crate) fn filter_non_empty_bytes<'a, T, U>(value: T) -> Option<&'a [u8]> where T: Into>, - U: AsRef<[u8]> + ?Sized, + U: AsRef<[u8]> + ?Sized + 'a, { value.into().map(AsRef::as_ref).filter(|value| !value.is_empty()) } diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs index 0dfb342a92..f828f28ea7 100644 --- a/identity_storage/src/key_storage/bls.rs +++ b/identity_storage/src/key_storage/bls.rs @@ -71,7 +71,12 @@ pub fn expand_bls_jwk(jwk: &Jwk) -> KeyStorageResult<(Option, let params = jwk .try_ec_params() .ok() - .filter(|params| params.try_bls_curve().is_ok_and(|curve| curve == BlsCurve::BLS12381G2)) + .filter(|params| { + params + .try_bls_curve() + .map(|curve| curve == BlsCurve::BLS12381G2) + .unwrap_or(false) + }) .context(format!("not a {} curve key", BlsCurve::BLS12381G2)) .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(e))?; diff --git a/identity_storage/src/key_storage/ed25519.rs b/identity_storage/src/key_storage/ed25519.rs index c8750e1f39..8e807af6c9 100644 --- a/identity_storage/src/key_storage/ed25519.rs +++ b/identity_storage/src/key_storage/ed25519.rs @@ -53,6 +53,6 @@ pub(crate) fn encode_jwk(private_key: &SecretKey, public_key: &crypto::signature let mut params = JwkParamsOkp::new(); params.x = x; params.d = Some(d); - params.crv = EdCurve::Ed25519.name().to_owned(); + EdCurve::Ed25519.name().clone_into(&mut params.crv); Jwk::from_params(params) } diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index 036f92ce7c..e1f463705d 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -366,7 +366,8 @@ mod bbs_plus_impl { // Check the provided JWK represents a BLS12381G2 key. if !public_key .try_ec_params() - .is_ok_and(|ec| ec.crv == BlsCurve::BLS12381G2.to_string()) + .map(|ec| ec.crv == BlsCurve::BLS12381G2.to_string()) + .unwrap_or(false) { return Err( KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) @@ -401,7 +402,8 @@ mod bbs_plus_impl { // Check the provided JWK represents a BLS12381G2 key. if !public_key .try_ec_params() - .is_ok_and(|ec| ec.crv == BlsCurve::BLS12381G2.to_string()) + .map(|ec| ec.crv == BlsCurve::BLS12381G2.to_string()) + .unwrap_or(false) { return Err( KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) diff --git a/identity_stronghold/src/storage/mod.rs b/identity_stronghold/src/storage/mod.rs index 7ad8b26ffa..cb02b9274b 100644 --- a/identity_stronghold/src/storage/mod.rs +++ b/identity_stronghold/src/storage/mod.rs @@ -87,7 +87,7 @@ impl StrongholdStorage { let mut params = JwkParamsOkp::new(); params.x = jwu::encode_b64(public_key); - params.crv = EdCurve::Ed25519.name().to_owned(); + EdCurve::Ed25519.name().clone_into(&mut params.crv); let mut jwk: Jwk = Jwk::from_params(params); jwk.set_alg(JwsAlgorithm::EdDSA.name()); jwk.set_kid(jwk.thumbprint_sha256_b64()); @@ -153,7 +153,7 @@ impl StrongholdStorage { let mut params = JwkParamsOkp::new(); params.x = jwu::encode_b64(public_key); - params.crv = EdCurve::Ed25519.name().to_owned(); + EdCurve::Ed25519.name().clone_into(&mut params.crv); let mut jwk: Jwk = Jwk::from_params(params); jwk.set_alg(JwsAlgorithm::EdDSA.name()); jwk.set_kid(jwk.thumbprint_sha256_b64()); diff --git a/identity_stronghold/src/storage/stronghold_jwk_storage.rs b/identity_stronghold/src/storage/stronghold_jwk_storage.rs index 1ab55225fc..63535293b1 100644 --- a/identity_stronghold/src/storage/stronghold_jwk_storage.rs +++ b/identity_stronghold/src/storage/stronghold_jwk_storage.rs @@ -85,7 +85,7 @@ impl JwkStorage for StrongholdStorage { let mut params = JwkParamsOkp::new(); params.x = jwu::encode_b64(public_key); - params.crv = EdCurve::Ed25519.name().to_owned(); + EdCurve::Ed25519.name().clone_into(&mut params.crv); let mut jwk: Jwk = Jwk::from_params(params); jwk.set_alg(alg.name()); jwk.set_kid(jwk.thumbprint_sha256_b64()); From b2f62773b6b1e400edd618b4d9fbd696b04fc4b8 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Mon, 13 May 2024 17:41:16 +0200 Subject: [PATCH 31/33] Add "Fondazione Links" to license header --- examples/1_advanced/10_zkp_revocation.rs | 2 +- examples/1_advanced/9_zkp.rs | 2 +- identity_credential/src/credential/jpt.rs | 2 +- identity_credential/src/credential/jwp_credential_options.rs | 2 +- .../src/presentation/jwp_presentation_builder.rs | 2 +- .../src/presentation/jwp_presentation_options.rs | 2 +- .../src/revocation/validity_timeframe_2024/mod.rs | 2 +- .../validity_timeframe_2024/revocation_timeframe_status.rs | 2 +- .../jpt_credential_validation/decoded_jpt_credential.rs | 2 +- .../jpt_credential_validation_options.rs | 2 +- .../jpt_credential_validation/jpt_credential_validator.rs | 2 +- .../jpt_credential_validation/jpt_credential_validator_utils.rs | 2 +- .../src/validator/jpt_credential_validation/mod.rs | 2 +- .../jpt_presentation_validation/decoded_jpt_presentation.rs | 2 +- .../jpt_presentation_validation_options.rs | 2 +- .../jpt_presentation_validation/jpt_presentation_validator.rs | 2 +- .../jpt_presentation_validator_utils.rs | 2 +- .../src/validator/jpt_presentation_validation/mod.rs | 2 +- identity_jose/src/jwk/curve/bls.rs | 2 +- identity_jose/src/jwk/jwk_ext.rs | 2 +- identity_storage/src/key_storage/bls.rs | 2 +- identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs | 2 +- identity_storage/src/key_storage/memstore.rs | 2 +- identity_storage/src/storage/jwp_document_ext.rs | 2 +- identity_storage/src/storage/timeframe_revocation_ext.rs | 2 +- 25 files changed, 25 insertions(+), 25 deletions(-) diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs index 3ec9af89e8..a78dea0e76 100644 --- a/examples/1_advanced/10_zkp_revocation.rs +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use examples::get_address_with_funds; diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs index 67ad69ef30..eeb4246280 100644 --- a/examples/1_advanced/9_zkp.rs +++ b/examples/1_advanced/9_zkp.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use examples::get_address_with_funds; diff --git a/identity_credential/src/credential/jpt.rs b/identity_credential/src/credential/jpt.rs index 328bcb2ef1..feab003949 100644 --- a/identity_credential/src/credential/jpt.rs +++ b/identity_credential/src/credential/jpt.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use serde::Deserialize; diff --git a/identity_credential/src/credential/jwp_credential_options.rs b/identity_credential/src/credential/jwp_credential_options.rs index 8b3349b01e..f607c2f68e 100644 --- a/identity_credential/src/credential/jwp_credential_options.rs +++ b/identity_credential/src/credential/jwp_credential_options.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 /// Options for creating a JSON Web Proof. diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs index 62a37e92c1..e6919058a2 100644 --- a/identity_credential/src/presentation/jwp_presentation_builder.rs +++ b/identity_credential/src/presentation/jwp_presentation_builder.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use crate::error::Error; diff --git a/identity_credential/src/presentation/jwp_presentation_options.rs b/identity_credential/src/presentation/jwp_presentation_options.rs index 67c14d6130..fba35a7f1f 100644 --- a/identity_credential/src/presentation/jwp_presentation_options.rs +++ b/identity_credential/src/presentation/jwp_presentation_options.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use identity_core::common::Url; diff --git a/identity_credential/src/revocation/validity_timeframe_2024/mod.rs b/identity_credential/src/revocation/validity_timeframe_2024/mod.rs index 61fd3717cb..179d5696ec 100644 --- a/identity_credential/src/revocation/validity_timeframe_2024/mod.rs +++ b/identity_credential/src/revocation/validity_timeframe_2024/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 //! Implementation of a new Revocation mechanism for ZK Verifiable Credentials. diff --git a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs index 02acd02287..0a70589112 100644 --- a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs +++ b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use crate::credential::Status; diff --git a/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs b/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs index 60036c9947..b574abfa13 100644 --- a/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs +++ b/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use identity_core::common::Object; diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs index 256178390e..2cbaafac28 100644 --- a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use crate::validator::SubjectHolderRelationship; diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs index a0f27288cf..3639d1a229 100644 --- a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use identity_core::convert::FromJson; diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs index 55fa649f44..258df619d4 100644 --- a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs +++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use crate::credential::Credential; diff --git a/identity_credential/src/validator/jpt_credential_validation/mod.rs b/identity_credential/src/validator/jpt_credential_validation/mod.rs index 7ed480186b..60455ba606 100644 --- a/identity_credential/src/validator/jpt_credential_validation/mod.rs +++ b/identity_credential/src/validator/jpt_credential_validation/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 mod decoded_jpt_credential; diff --git a/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs b/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs index bff5e8a4fe..fb62181057 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use identity_core::common::Object; diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs index 488c6ab45e..302b45f8c4 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use identity_document::verifiable::JwpVerificationOptions; diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs index da3b70ca92..ac32e9878f 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use std::str::FromStr; diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs index 1f880320a0..3bdf17a00e 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use std::str::FromStr; diff --git a/identity_credential/src/validator/jpt_presentation_validation/mod.rs b/identity_credential/src/validator/jpt_presentation_validation/mod.rs index 6a12563fe7..1cab953dc5 100644 --- a/identity_credential/src/validator/jpt_presentation_validation/mod.rs +++ b/identity_credential/src/validator/jpt_presentation_validation/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 mod decoded_jpt_presentation; diff --git a/identity_jose/src/jwk/curve/bls.rs b/identity_jose/src/jwk/curve/bls.rs index 4c468002fb..97b68bf678 100644 --- a/identity_jose/src/jwk/curve/bls.rs +++ b/identity_jose/src/jwk/curve/bls.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use core::fmt::Display; diff --git a/identity_jose/src/jwk/jwk_ext.rs b/identity_jose/src/jwk/jwk_ext.rs index 1b6ede06a6..39fc02fa93 100644 --- a/identity_jose/src/jwk/jwk_ext.rs +++ b/identity_jose/src/jwk/jwk_ext.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use super::Jwk; diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs index f828f28ea7..2a3b38a0a7 100644 --- a/identity_storage/src/key_storage/bls.rs +++ b/identity_storage/src/key_storage/bls.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use anyhow::Context; diff --git a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs index 09f65788ab..276c39d4cb 100644 --- a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs +++ b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index e1f463705d..203ced2e64 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2023 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use core::fmt::Debug; diff --git a/identity_storage/src/storage/jwp_document_ext.rs b/identity_storage/src/storage/jwp_document_ext.rs index 318cff6782..21ef7fafaa 100644 --- a/identity_storage/src/storage/jwp_document_ext.rs +++ b/identity_storage/src/storage/jwp_document_ext.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use super::JwkStorageDocumentError as Error; diff --git a/identity_storage/src/storage/timeframe_revocation_ext.rs b/identity_storage/src/storage/timeframe_revocation_ext.rs index 2b6ffd0f34..f53f2a9639 100644 --- a/identity_storage/src/storage/timeframe_revocation_ext.rs +++ b/identity_storage/src/storage/timeframe_revocation_ext.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use super::JwkStorageDocumentError as Error; From 4cf455d62deae9a05d5f727508ef6241771ddbcd Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 21 May 2024 16:55:10 +0200 Subject: [PATCH 32/33] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 9abdb3868d76ccb39da2145346e201640448870a Author: Sven Date: Tue May 14 09:16:09 2024 +0200 Add EcDSA verifier (#1353) * add ecdsa verifier * add identity_ecdsa_verifier to workspace, add license headers * Update identity_ecdsa_verifier/Cargo.toml Co-authored-by: wulfraem * Update identity_ecdsa_verifier/src/secp256k1.rs Co-authored-by: wulfraem * Update identity_ecdsa_verifier/Cargo.toml Co-authored-by: wulfraem * Update identity_ecdsa_verifier/src/secp256k1.rs Co-authored-by: wulfraem * Update identity_ecdsa_verifier/src/secp256r1.rs Co-authored-by: wulfraem * add feedback * add OpenSSL installation to windows runner in CI * update license headers and authors for ecdsa verifier * update license template to allow multiple contributors --------- Co-authored-by: Sebastian Wolfram commit 149bfac98e8d9d8ca3d890e413291e384447c62b Author: wulfraem Date: Mon May 13 10:44:09 2024 +0200 Fix findings after clippy update (#1365) * fix clippy findings * fix formatting * refactor .clone_into calls into .to_string * fix previous edit * disable empty_docs for wasm binding for now * fix missing newline * disable self update from rust setup in ci for now * update self update skip to skip only for windows build commit 51aedd51be086e333744b020e867c0348833a083 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue Apr 30 16:16:36 2024 +0200 Use STRONGHOLD_PWD_FILE env variable to pass stronghold's password (#1363) commit edec26c18782ad75a20ca6bebd7c66959eadb91d Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue Apr 30 15:40:55 2024 +0200 Arbitrary data signing service (#1350) commit f59e75a57df05971aef00549c8880b83c6600f2b Author: Eike Haß Date: Tue Apr 30 15:34:40 2024 +0200 Fix dockerhub workflow (#1343) commit 993cfec8a698f668f2891f46e6c94f2584c50c05 Author: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Fri Apr 26 13:39:29 2024 +0200 add inx-faucet profile (#1356) --- .github/actions/rust/rust-setup/action.yml | 19 ++- .github/workflows/build-and-test.yml | 6 + .../workflows/grpc-publish-to-dockerhub.yml | 5 +- .license_template | 2 +- Cargo.toml | 1 + bindings/grpc/README.md | 26 ++-- bindings/grpc/proto/utils.proto | 23 ++++ bindings/grpc/src/main.rs | 22 +++- bindings/grpc/src/services/mod.rs | 2 + bindings/grpc/src/services/utils.rs | 67 +++++++++++ bindings/grpc/tests/api/main.rs | 1 + bindings/grpc/tests/api/utils.rs | 48 ++++++++ bindings/wasm/Cargo.toml | 5 + identity_credential/src/credential/proof.rs | 4 +- .../revocation/status_list_2021/credential.rs | 6 +- .../src/validator/test_utils.rs | 2 +- identity_ecdsa_verifier/Cargo.toml | 32 +++++ identity_ecdsa_verifier/README.md | 3 + .../src/ecdsa_jws_verifier.rs | 34 ++++++ identity_ecdsa_verifier/src/lib.rs | 29 +++++ identity_ecdsa_verifier/src/secp256k1.rs | 93 +++++++++++++++ identity_ecdsa_verifier/src/secp256r1.rs | 89 ++++++++++++++ identity_ecdsa_verifier/src/tests/mod.rs | 5 + identity_ecdsa_verifier/src/tests/secp256.rs | 77 ++++++++++++ identity_ecdsa_verifier/src/tests/secp256k.rs | 112 ++++++++++++++++++ identity_iota_core/src/document/test_utils.rs | 2 +- identity_storage/src/key_storage/ed25519.rs | 2 +- identity_storage/src/key_storage/memstore.rs | 8 +- .../src/key_storage/tests/utils.rs | 8 +- .../src/storage/tests/test_utils.rs | 2 +- identity_stronghold/src/ed25519.rs | 2 +- .../src/storage/stronghold_jwk_storage.rs | 2 +- .../src/tests/test_jwk_storage.rs | 8 +- identity_stronghold/src/tests/utils.rs | 2 +- 34 files changed, 702 insertions(+), 47 deletions(-) create mode 100644 bindings/grpc/proto/utils.proto create mode 100644 bindings/grpc/src/services/utils.rs create mode 100644 bindings/grpc/tests/api/utils.rs create mode 100644 identity_ecdsa_verifier/Cargo.toml create mode 100644 identity_ecdsa_verifier/README.md create mode 100644 identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs create mode 100644 identity_ecdsa_verifier/src/lib.rs create mode 100644 identity_ecdsa_verifier/src/secp256k1.rs create mode 100644 identity_ecdsa_verifier/src/secp256r1.rs create mode 100644 identity_ecdsa_verifier/src/tests/mod.rs create mode 100644 identity_ecdsa_verifier/src/tests/secp256.rs create mode 100644 identity_ecdsa_verifier/src/tests/secp256k.rs diff --git a/.github/actions/rust/rust-setup/action.yml b/.github/actions/rust/rust-setup/action.yml index b7b16a352a..5f783a98cc 100644 --- a/.github/actions/rust/rust-setup/action.yml +++ b/.github/actions/rust/rust-setup/action.yml @@ -48,7 +48,16 @@ runs: shell: bash run: | - if ! rustup self update; then + # self update is currently broken on Windows runners: + # https://github.com/rust-lang/rustup/issues/3709 + # so we'll skip self update for windows + OS=${{ inputs.os }} + IS_WINDOWS=false; [[ $OS =~ ^[wW]indows ]] && IS_WINDOWS=true + + if [[ $IS_WINDOWS = true ]] ; + then + echo "skipping self update on windows runner due to https://github.com/rust-lang/rustup/issues/3709" + elif ! rustup self update; then echo "rustup self update failed" fi @@ -57,7 +66,13 @@ runs: rustup target add $TARGET fi - rustup update + if [[ $IS_WINDOWS = true ]] ; + then + echo "skipping self update on windows runner due to https://github.com/rust-lang/rustup/issues/3709" + rustup update --no-self-update + else + rustup update + fi TOOLCHAIN=${{ inputs.toolchain }} if [[ $TOOLCHAIN != 'stable' ]]; then diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a58316790f..206c534962 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -82,6 +82,12 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Ensure, OpenSSL is available in Windows + if: matrix.os == 'windows-latest' + run: | + echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append + vcpkg install openssl:x64-windows-static-md + - name: Setup Rust and cache uses: './.github/actions/rust/rust-setup' with: diff --git a/.github/workflows/grpc-publish-to-dockerhub.yml b/.github/workflows/grpc-publish-to-dockerhub.yml index d72fe20702..348bf8c564 100644 --- a/.github/workflows/grpc-publish-to-dockerhub.yml +++ b/.github/workflows/grpc-publish-to-dockerhub.yml @@ -39,14 +39,15 @@ jobs: context: . file: bindings/grpc/Dockerfile push: ${{ !inputs.dry-run }} - labels: iotaledger/identity-grpc:${{ inputs.tag }} + tags: iotaledger/identity-grpc:${{ inputs.tag }} - name: Docker Hub Description uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae + if: ${{ !inputs.dry-run }} with: username: ${{ secrets.IOTALEDGER_DOCKER_USERNAME }} password: ${{ secrets.IOTALEDGER_DOCKER_PASSWORD }} repository: iotaledger/identity-grpc - readme-filepath: ./bindigns/grpc/README.md + readme-filepath: ./bindings/grpc/README.md short-description: ${{ github.event.repository.description }} diff --git a/.license_template b/.license_template index 30334ddc0c..a437281e00 100644 --- a/.license_template +++ b/.license_template @@ -1,2 +1,2 @@ -// Copyright {20\d{2}(-20\d{2})?} IOTA Stiftung +// Copyright {20\d{2}(-20\d{2})?} IOTA Stiftung{(?:, .+)?} // SPDX-License-Identifier: Apache-2.0 diff --git a/Cargo.toml b/Cargo.toml index e9aab70af4..e7eda3efc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "identity_verification", "identity_stronghold", "identity_jose", + "identity_ecdsa_verifier", "identity_eddsa_verifier", "examples", ] diff --git a/bindings/grpc/README.md b/bindings/grpc/README.md index 814e82a7f8..f94f0add17 100644 --- a/bindings/grpc/README.md +++ b/bindings/grpc/README.md @@ -1,7 +1,7 @@ # Identity.rs gRPC Bindings This project provides the functionalities of [Identity.rs](https://github.com/iotaledger/identity.rs) in a language-agnostic way through a [gRPC](https://grpc.io) server. -The server can easily be run with docker using [this dockerfile](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/Dockerfile). +The server can easily be run with docker using [this dockerfile](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/Dockerfile). ## Build Run `docker build -f bindings/grpc/Dockerfile -t iotaleger/identity-grpc .` from the project root. @@ -17,17 +17,17 @@ Make sure to provide a valid stronghold snapshot at the provided `SNAPSHOT_PATH` ### Available services | Service description | Service Id | Proto File | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------| -| Credential Revocation Checking | `credentials/CredentialRevocation.check` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | -| SD-JWT Validation | `sd_jwt/Verification.verify` | [sd_jwt.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/sd_jwt.proto) | -| Credential JWT creation | `credentials/Jwt.create` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | -| Credential JWT validation | `credentials/VcValidation.validate` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) | -| DID Document Creation | `document/DocumentService.create` | [document.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/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/grpc-bindings/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/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) | -| Domain Linkage - validate endpoints in DID, let server fetch did-configuration | `domain_linkage/DomainLinkage.validate_did` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) | -| Domain Linkage - validate endpoints in DID, pass did-configuration to service | `domain_linkage/DomainLinkage.validate_did_against_did_configurations` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) | -| `StatusList2021Credential` creation | `status_list_2021/StatusList2021Svc.create` | [status_list_2021.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/status_list_2021.proto) | -| `StatusList2021Credential` update | `status_list_2021/StatusList2021Svc.update` | [status_list_2021.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/status_list_2021.proto) | +| Credential Revocation Checking | `credentials/CredentialRevocation.check` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/credentials.proto) | +| 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) | +| 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) | +| Domain Linkage - validate endpoints in DID, let server fetch did-configuration | `domain_linkage/DomainLinkage.validate_did` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/domain_linkage.proto) | +| Domain Linkage - validate endpoints in DID, pass did-configuration to service | `domain_linkage/DomainLinkage.validate_did_against_did_configurations` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/domain_linkage.proto) | +| `StatusList2021Credential` creation | `status_list_2021/StatusList2021Svc.create` | [status_list_2021.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/status_list_2021.proto) | +| `StatusList2021Credential` update | `status_list_2021/StatusList2021Svc.update` | [status_list_2021.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/status_list_2021.proto) | ## Testing @@ -62,7 +62,7 @@ In order to test domain linkage, you need access to a server that is reachable v 1. for convenience, you can find a script to start the HTTP server, that you can adjust in `tooling/start-http-server.sh`, don't forget to insert your static domain or to remove the `--domain` parameter #### Domain linkage credential -1. copy the public url and insert it into [6_domain_linkage.rs](../../examples/1_advanced/6_domain_linkage.rs) as domain 1, e.g. `let domain_1: Url = Url::parse("https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app")?;` +1. copy the public url and insert it into [6_domain_linkage.rs](https://github.com/iotaledger/identity.rs/blob/main/examples/1_advanced/6_domain_linkage.rs) as domain 1, e.g. `let domain_1: Url = Url::parse("https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app")?;` .1 run the example with `cargo run --release --example 6_domain_linkage` #### GRPC server diff --git a/bindings/grpc/proto/utils.proto b/bindings/grpc/proto/utils.proto new file mode 100644 index 0000000000..87ea3f7054 --- /dev/null +++ b/bindings/grpc/proto/utils.proto @@ -0,0 +1,23 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; +package utils; + +message DataSigningRequest { + // Raw data that will be signed. + bytes data = 1; + // Signing key's ID. + string key_id = 2; +} + +message DataSigningResponse { + // Raw data signature. + bytes signature = 1; +} + +// Service that handles signing operations on raw data. +service Signing { + rpc sign(DataSigningRequest) returns (DataSigningResponse); +} + diff --git a/bindings/grpc/src/main.rs b/bindings/grpc/src/main.rs index 4e6e3e11fa..04927b1c9c 100644 --- a/bindings/grpc/src/main.rs +++ b/bindings/grpc/src/main.rs @@ -1,6 +1,7 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use anyhow::Context; use identity_grpc::server::GRpcServer; use identity_stronghold::StrongholdStorage; use iota_sdk::client::stronghold::StrongholdAdapter; @@ -29,11 +30,18 @@ async fn main() -> anyhow::Result<()> { #[tracing::instrument] fn init_stronghold() -> anyhow::Result { - let stronghold_password = std::env::var("STRONGHOLD_PWD")?; - let snapshot_path = std::env::var("SNAPSHOT_PATH")?; + use std::env; + use std::fs; + let stronghold_password = env::var("STRONGHOLD_PWD_FILE") + .context("Unset \"STRONGHOLD_PWD_FILE\" env variable") + .and_then(|path| fs::read_to_string(&path).context(format!("{path} does not exists"))) + .map(sanitize_pwd) + .or(env::var("STRONGHOLD_PWD")) + .context("No password for stronghold was provided")?; + let snapshot_path = env::var("SNAPSHOT_PATH")?; // Check for snapshot file at specified path - let metadata = std::fs::metadata(&snapshot_path)?; + let metadata = fs::metadata(&snapshot_path)?; if !metadata.is_file() { return Err(anyhow::anyhow!("No snapshot at provided path \"{}\"", &snapshot_path)); } @@ -45,3 +53,11 @@ fn init_stronghold() -> anyhow::Result { .map(StrongholdStorage::new)?, ) } + +/// Remove any trailing whitespace in-place. +fn sanitize_pwd(mut pwd: String) -> String { + let trimmed = pwd.trim_end(); + pwd.truncate(trimmed.len()); + pwd.shrink_to_fit(); + pwd +} diff --git a/bindings/grpc/src/services/mod.rs b/bindings/grpc/src/services/mod.rs index f632feb91a..00abe17ce1 100644 --- a/bindings/grpc/src/services/mod.rs +++ b/bindings/grpc/src/services/mod.rs @@ -7,6 +7,7 @@ pub mod domain_linkage; pub mod health_check; pub mod sd_jwt; pub mod status_list_2021; +pub mod utils; use identity_stronghold::StrongholdStorage; use iota_sdk::client::Client; @@ -21,6 +22,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)); routes.routes() } diff --git a/bindings/grpc/src/services/utils.rs b/bindings/grpc/src/services/utils.rs new file mode 100644 index 0000000000..0e7d2fc570 --- /dev/null +++ b/bindings/grpc/src/services/utils.rs @@ -0,0 +1,67 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use _utils::signing_server::Signing as SigningSvc; +use _utils::signing_server::SigningServer; +use _utils::DataSigningRequest; +use _utils::DataSigningResponse; +use identity_iota::storage::JwkStorage; +use identity_iota::storage::KeyId; +use identity_iota::storage::KeyStorageError; +use identity_stronghold::StrongholdStorage; +use tonic::Request; +use tonic::Response; +use tonic::Status; + +mod _utils { + tonic::include_proto!("utils"); +} + +#[derive(Debug, thiserror::Error)] +#[error("Key storage error: {0}")] +pub struct Error(#[from] KeyStorageError); + +impl From for Status { + fn from(value: Error) -> Self { + Status::internal(value.to_string()) + } +} + +pub struct SigningService { + storage: StrongholdStorage, +} + +impl SigningService { + pub fn new(stronghold: &StrongholdStorage) -> Self { + Self { + storage: stronghold.clone(), + } + } +} + +#[tonic::async_trait] +impl SigningSvc for SigningService { + #[tracing::instrument( + name = "utils/sign", + skip_all, + fields(request = ?req.get_ref()) + ret, + err, + )] + async fn sign(&self, req: Request) -> Result, Status> { + let DataSigningRequest { data, key_id } = 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 signature = self + .storage + .sign(&key_id, &data, &public_key_jwk) + .await + .map_err(Error)?; + + Ok(Response::new(DataSigningResponse { signature })) + } +} + +pub fn service(stronghold: &StrongholdStorage) -> SigningServer { + SigningServer::new(SigningService::new(stronghold)) +} diff --git a/bindings/grpc/tests/api/main.rs b/bindings/grpc/tests/api/main.rs index e187cf7f1c..af4929bfae 100644 --- a/bindings/grpc/tests/api/main.rs +++ b/bindings/grpc/tests/api/main.rs @@ -10,3 +10,4 @@ mod helpers; mod jwt; mod sd_jwt_validation; mod status_list_2021; +mod utils; diff --git a/bindings/grpc/tests/api/utils.rs b/bindings/grpc/tests/api/utils.rs new file mode 100644 index 0000000000..9c863bf3de --- /dev/null +++ b/bindings/grpc/tests/api/utils.rs @@ -0,0 +1,48 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use _utils::signing_client::SigningClient; +use _utils::DataSigningRequest; +use identity_iota::verification::jws::JwsAlgorithm; +use identity_storage::JwkStorage; +use identity_storage::KeyType; +use identity_stronghold::StrongholdStorage; + +use crate::helpers::make_stronghold; +use crate::helpers::TestServer; + +mod _utils { + tonic::include_proto!("utils"); +} + +const SAMPLE_SIGNING_DATA: &'static [u8] = b"I'm just some random data to be signed :)"; + +#[tokio::test] +async fn raw_data_signing_works() -> anyhow::Result<()> { + let stronghold = StrongholdStorage::new(make_stronghold()); + let server = TestServer::new_with_stronghold(stronghold.clone()).await; + + let key_id = stronghold + .generate(KeyType::from_static_str("Ed25519"), JwsAlgorithm::EdDSA) + .await? + .key_id; + + let expected_signature = { + let public_key_jwk = stronghold.get_public_key(&key_id).await?; + stronghold.sign(&key_id, SAMPLE_SIGNING_DATA, &public_key_jwk).await? + }; + + let mut grpc_client = SigningClient::connect(server.endpoint()).await?; + let signature = grpc_client + .sign(DataSigningRequest { + data: SAMPLE_SIGNING_DATA.to_owned(), + key_id: key_id.to_string(), + }) + .await? + .into_inner() + .signature; + + assert_eq!(signature, expected_signature); + + Ok(()) +} diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 819da13d7b..74bee6d945 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -47,3 +47,8 @@ instant = { version = "0.1", default-features = false, features = ["wasm-bindgen [profile.release] opt-level = 's' lto = true + +[lints.clippy] +# can be removed as soon as fix has been added to clippy +# see https://github.com/rust-lang/rust-clippy/issues/12377 +empty_docs = "allow" diff --git a/identity_credential/src/credential/proof.rs b/identity_credential/src/credential/proof.rs index 03e4bca663..ab779014a2 100644 --- a/identity_credential/src/credential/proof.rs +++ b/identity_credential/src/credential/proof.rs @@ -52,7 +52,7 @@ mod tests { assert_eq!(proof.type_, "test-proof"); let value = proof .properties - .get(&"signature".to_owned()) + .get("signature") .expect("property in proof doesn't exist"); assert_eq!(value, "abc123"); } @@ -88,7 +88,7 @@ mod tests { assert_eq!(proof.type_, "RsaSignature2018"); let value = proof .properties - .get(&"proofPurpose".to_owned()) + .get("proofPurpose") .expect("property in proof doesn't exist"); assert_eq!(value, "assertionMethod"); assert_eq!(proof.properties.len(), 4); diff --git a/identity_credential/src/revocation/status_list_2021/credential.rs b/identity_credential/src/revocation/status_list_2021/credential.rs index e2ef0950cd..4402283e1a 100644 --- a/identity_credential/src/revocation/status_list_2021/credential.rs +++ b/identity_credential/src/revocation/status_list_2021/credential.rs @@ -279,11 +279,7 @@ impl StatusList2021CredentialSubject { return Err(StatusList2021CredentialError::MultipleCredentialSubject); }; if let Some(subject_type) = subject.properties.get("type") { - if !subject_type - .as_str() - .map(|t| t == CREDENTIAL_SUBJECT_TYPE) - .unwrap_or(false) - { + if subject_type.as_str() != Some(CREDENTIAL_SUBJECT_TYPE) { return Err(StatusList2021CredentialError::InvalidProperty("credentialSubject.type")); } } else { diff --git a/identity_credential/src/validator/test_utils.rs b/identity_credential/src/validator/test_utils.rs index 9febb41a1f..22a18a7605 100644 --- a/identity_credential/src/validator/test_utils.rs +++ b/identity_credential/src/validator/test_utils.rs @@ -19,7 +19,7 @@ pub(crate) fn encode_public_ed25519_jwk(public_key: &PublicKey) -> Jwk { let mut params = JwkParamsOkp::new(); params.x = x; params.d = None; - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); let mut jwk = Jwk::from_params(params); jwk.set_alg(JwsAlgorithm::EdDSA.name()); jwk diff --git a/identity_ecdsa_verifier/Cargo.toml b/identity_ecdsa_verifier/Cargo.toml new file mode 100644 index 0000000000..c72ffa089c --- /dev/null +++ b/identity_ecdsa_verifier/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "identity_ecdsa_verifier" +version = "0.1.0" +authors = ["IOTA Stiftung", "Filancore GmbH"] +edition.workspace = true +homepage.workspace = true +keywords = ["iota", "identity", "jose", "jwk", "jws"] +license.workspace = true +readme = "./README.md" +repository.workspace = true +rust-version.workspace = true +description = "JWS ECDSA signature verification for IOTA Identity" + +[lints] +workspace = true + +[dependencies] +identity_verification = { version = "=1.2.0", path = "../identity_verification", default-features = false } +k256 = { version = "0.13.3", default-features = false, features = ["std", "ecdsa", "ecdsa-core"], optional = true } +p256 = { version = "0.13.2", default-features = false, features = ["std", "ecdsa", "ecdsa-core"], optional = true } +signature = { version = "2", default-features = false } + +[dev-dependencies] +josekit = "0.8.6" +serde_json.workspace = true + +[features] +default = ["es256", "es256k"] +# Enables the EcDSAJwsVerifier to verify JWS with alg = ES256. +es256 = ["dep:p256"] +# Enables the EcDSAJwsVerifier to verify JWS with alg = ES256K. +es256k = ["dep:k256"] diff --git a/identity_ecdsa_verifier/README.md b/identity_ecdsa_verifier/README.md new file mode 100644 index 0000000000..4ccb0f36b9 --- /dev/null +++ b/identity_ecdsa_verifier/README.md @@ -0,0 +1,3 @@ +# ECDSA Verifier + +This crate implements a `JwsVerifier` capable of verifying EcDSA signatures with algorithms `ES256` and `ES256K`. diff --git a/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs b/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs new file mode 100644 index 0000000000..6371b40b78 --- /dev/null +++ b/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs @@ -0,0 +1,34 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +use identity_verification::jws::JwsAlgorithm; +use identity_verification::jws::JwsVerifier; +use identity_verification::jws::SignatureVerificationErrorKind; + +/// An implementor of [`JwsVerifier`](identity_verification::jws::JwsVerifier) +/// that can handle a selection of EcDSA algorithms. +/// +/// The following algorithms are supported, if the respective feature on the +/// crate is activated: +/// +/// - [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256). +/// - [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K). +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct EcDSAJwsVerifier {} + +impl JwsVerifier for EcDSAJwsVerifier { + fn verify( + &self, + input: identity_verification::jws::VerificationInput, + public_key: &identity_verification::jwk::Jwk, + ) -> Result<(), identity_verification::jws::SignatureVerificationError> { + match input.alg { + #[cfg(feature = "es256")] + JwsAlgorithm::ES256 => crate::Secp256R1Verifier::verify(&input, public_key), + #[cfg(feature = "es256k")] + JwsAlgorithm::ES256K => crate::Secp256K1Verifier::verify(&input, public_key), + _ => Err(SignatureVerificationErrorKind::UnsupportedAlg.into()), + } + } +} diff --git a/identity_ecdsa_verifier/src/lib.rs b/identity_ecdsa_verifier/src/lib.rs new file mode 100644 index 0000000000..6136a3eae1 --- /dev/null +++ b/identity_ecdsa_verifier/src/lib.rs @@ -0,0 +1,29 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +#![doc = include_str!("./../README.md")] +#![warn( + rust_2018_idioms, + unreachable_pub, + missing_docs, + rustdoc::missing_crate_level_docs, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::private_doc_tests, + clippy::missing_safety_doc +)] + +mod ecdsa_jws_verifier; +#[cfg(feature = "es256k")] +mod secp256k1; +#[cfg(feature = "es256")] +mod secp256r1; + +pub use ecdsa_jws_verifier::*; +#[cfg(feature = "es256k")] +pub use secp256k1::*; +#[cfg(feature = "es256")] +pub use secp256r1::*; + +#[cfg(test)] +mod tests; diff --git a/identity_ecdsa_verifier/src/secp256k1.rs b/identity_ecdsa_verifier/src/secp256k1.rs new file mode 100644 index 0000000000..9c77412cc8 --- /dev/null +++ b/identity_ecdsa_verifier/src/secp256k1.rs @@ -0,0 +1,93 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +use identity_verification::jwk::JwkParamsEc; +use identity_verification::jws::SignatureVerificationError; +use identity_verification::jws::SignatureVerificationErrorKind; +use identity_verification::jwu::{self}; +use k256::ecdsa::Signature; +use k256::ecdsa::VerifyingKey; +use k256::elliptic_curve::sec1::FromEncodedPoint; +use k256::elliptic_curve::subtle::CtOption; +use k256::EncodedPoint; +use k256::PublicKey; + +/// A verifier that can handle the +/// [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K) +/// algorithm. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Secp256K1Verifier {} + +impl Secp256K1Verifier { + /// Verify a JWS signature secured with the + /// [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K) + /// algorithm. + /// + /// This function is useful when one is building a + /// [`JwsVerifier`](identity_verification::jws::JwsVerifier) that + /// handles the + /// [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K) + /// in the same manner as the [`Secp256K1Verifier`] hence extending its + /// capabilities. + /// + /// # Warning + /// + /// This function does not check whether `alg = ES256K` in the protected + /// header. Callers are expected to assert this prior to calling the + /// function. + pub fn verify( + input: &identity_verification::jws::VerificationInput, + public_key: &identity_verification::jwk::Jwk, + ) -> Result<(), SignatureVerificationError> { + // Obtain a K256 public key. + let params: &JwkParamsEc = public_key + .try_ec_params() + .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; + + // Concatenate x and y coordinates as required by + // EncodedPoint::from_untagged_bytes. + let public_key_bytes = jwu::decode_b64(¶ms.x) + .map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })? + .into_iter() + .chain(jwu::decode_b64(¶ms.y).map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })?) + .collect(); + + // The JWK contains the uncompressed x and y coordinates, so we can create the + // encoded point directly without prefixing an SEC1 tag. + let encoded_point: EncodedPoint = EncodedPoint::from_untagged_bytes(&public_key_bytes); + let public_key: PublicKey = { + let opt_public_key: CtOption = PublicKey::from_encoded_point(&encoded_point); + if opt_public_key.is_none().into() { + return Err(SignatureVerificationError::new( + SignatureVerificationErrorKind::KeyDecodingFailure, + )); + } else { + opt_public_key.unwrap() + } + }; + + let verifying_key: VerifyingKey = VerifyingKey::from(public_key); + + let mut signature: Signature = Signature::try_from(input.decoded_signature.deref()).map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err) + })?; + + if let Some(normalized) = signature.normalize_s() { + signature = normalized; + } + + match signature::Verifier::verify(&verifying_key, &input.signing_input, &signature) { + Ok(()) => Ok(()), + Err(err) => { + Err(SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err)) + } + } + } +} diff --git a/identity_ecdsa_verifier/src/secp256r1.rs b/identity_ecdsa_verifier/src/secp256r1.rs new file mode 100644 index 0000000000..09201570d0 --- /dev/null +++ b/identity_ecdsa_verifier/src/secp256r1.rs @@ -0,0 +1,89 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +use identity_verification::jwk::JwkParamsEc; +use identity_verification::jws::SignatureVerificationError; +use identity_verification::jws::SignatureVerificationErrorKind; +use identity_verification::jwu::{self}; +use p256::ecdsa::Signature; +use p256::ecdsa::VerifyingKey; +use p256::elliptic_curve::sec1::FromEncodedPoint; +use p256::elliptic_curve::subtle::CtOption; +use p256::EncodedPoint; +use p256::PublicKey; + +/// A verifier that can handle the +/// [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256) +/// algorithm. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Secp256R1Verifier {} + +impl Secp256R1Verifier { + /// Verify a JWS signature secured with the + /// [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256) + /// algorithm. + /// + /// This function is useful when one is building a + /// [`JwsVerifier`](identity_verification::jws::JwsVerifier) that + /// handles the + /// [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256) + /// in the same manner as the [`Secp256R1Verifier`] hence extending its + /// capabilities. + /// + /// # Warning + /// + /// This function does not check whether `alg = ES256` in the protected + /// header. Callers are expected to assert this prior to calling the + /// function. + pub fn verify( + input: &identity_verification::jws::VerificationInput, + public_key: &identity_verification::jwk::Jwk, + ) -> Result<(), SignatureVerificationError> { + // Obtain a P256 public key. + let params: &JwkParamsEc = public_key + .try_ec_params() + .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; + + // Concatenate x and y coordinates as required by + // EncodedPoint::from_untagged_bytes. + let public_key_bytes = jwu::decode_b64(¶ms.x) + .map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })? + .into_iter() + .chain(jwu::decode_b64(¶ms.y).map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })?) + .collect(); + + // The JWK contains the uncompressed x and y coordinates, so we can create the + // encoded point directly without prefixing an SEC1 tag. + let encoded_point: EncodedPoint = EncodedPoint::from_untagged_bytes(&public_key_bytes); + let public_key: PublicKey = { + let opt_public_key: CtOption = PublicKey::from_encoded_point(&encoded_point); + if opt_public_key.is_none().into() { + return Err(SignatureVerificationError::new( + SignatureVerificationErrorKind::KeyDecodingFailure, + )); + } else { + opt_public_key.unwrap() + } + }; + + let verifying_key: VerifyingKey = VerifyingKey::from(public_key); + + let signature: Signature = Signature::try_from(input.decoded_signature.deref()).map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err) + })?; + + match signature::Verifier::verify(&verifying_key, &input.signing_input, &signature) { + Ok(()) => Ok(()), + Err(err) => { + Err(SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err)) + } + } + } +} diff --git a/identity_ecdsa_verifier/src/tests/mod.rs b/identity_ecdsa_verifier/src/tests/mod.rs new file mode 100644 index 0000000000..63e508fa33 --- /dev/null +++ b/identity_ecdsa_verifier/src/tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +mod secp256; +mod secp256k; diff --git a/identity_ecdsa_verifier/src/tests/secp256.rs b/identity_ecdsa_verifier/src/tests/secp256.rs new file mode 100644 index 0000000000..c6700a85e4 --- /dev/null +++ b/identity_ecdsa_verifier/src/tests/secp256.rs @@ -0,0 +1,77 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +mod es256 { + use identity_verification::jwk::EcCurve; + use identity_verification::jwk::Jwk; + use identity_verification::jwk::JwkParamsEc; + use identity_verification::jwu; + use p256::ecdsa::Signature; + use p256::ecdsa::SigningKey; + use p256::SecretKey; + + pub(crate) fn expand_p256_jwk(jwk: &Jwk) -> SecretKey { + let params: &JwkParamsEc = jwk.try_ec_params().unwrap(); + + if params.try_ec_curve().unwrap() != EcCurve::P256 { + panic!("expected a P256 curve"); + } + + let sk_bytes = params.d.as_ref().map(jwu::decode_b64).unwrap().unwrap(); + SecretKey::from_slice(&sk_bytes).unwrap() + } + + pub(crate) fn sign(message: &[u8], private_key: &Jwk) -> impl AsRef<[u8]> { + let sk: SecretKey = expand_p256_jwk(private_key); + let signing_key: SigningKey = SigningKey::from(sk); + let signature: Signature = signature::Signer::sign(&signing_key, message); + signature.to_bytes() + } +} + +use identity_verification::jwk::Jwk; +use identity_verification::jws; +use identity_verification::jws::JwsHeader; + +use crate::EcDSAJwsVerifier; + +#[test] +fn test_es256_rfc7515() { + // Test Vector taken from https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.3. + let tv_header: &str = r#"{"alg":"ES256"}"#; + let tv_claims: &[u8] = &[ + 123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, + 56, 49, 57, 51, 56, 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, + 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125, + ]; + let tv_encoded: &[u8] = b"eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.e4ZrhZdbFQ7630Tq51E6RQiJaae9bFNGJszIhtusEwzvO21rzH76Wer6yRn2Zb34VjIm3cVRl0iQctbf4uBY3w"; + let tv_private_key: &str = r#" + { + "kty": "EC", + "crv": "P-256", + "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", + "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", + "d": "jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI" + } + "#; + + let header: JwsHeader = serde_json::from_str(tv_header).unwrap(); + let jwk: Jwk = serde_json::from_str(tv_private_key).unwrap(); + let encoder: jws::CompactJwsEncoder<'_> = jws::CompactJwsEncoder::new(tv_claims, &header).unwrap(); + let signing_input: &[u8] = encoder.signing_input(); + let encoded: String = { + let signature = es256::sign(signing_input, &jwk); + encoder.into_jws(signature.as_ref()) + }; + assert_eq!(encoded.as_bytes(), tv_encoded); + + let jws_verifier = EcDSAJwsVerifier::default(); + let decoder = jws::Decoder::new(); + let token = decoder + .decode_compact_serialization(tv_encoded, None) + .and_then(|decoded| decoded.verify(&jws_verifier, &jwk)) + .unwrap(); + + assert_eq!(token.protected, header); + assert_eq!(token.claims, tv_claims); +} diff --git a/identity_ecdsa_verifier/src/tests/secp256k.rs b/identity_ecdsa_verifier/src/tests/secp256k.rs new file mode 100644 index 0000000000..49f234a3c7 --- /dev/null +++ b/identity_ecdsa_verifier/src/tests/secp256k.rs @@ -0,0 +1,112 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +mod es256k1 { + use identity_verification::jwk::EcCurve; + use identity_verification::jwk::Jwk; + use identity_verification::jwk::JwkParamsEc; + use identity_verification::jwu; + use k256::ecdsa::Signature; + use k256::ecdsa::SigningKey; + use k256::SecretKey; + + pub(crate) fn expand_k256_jwk(jwk: &Jwk) -> SecretKey { + let params: &JwkParamsEc = jwk.try_ec_params().unwrap(); + + if params.try_ec_curve().unwrap() != EcCurve::Secp256K1 { + panic!("expected a Secp256K1 curve"); + } + + let sk_bytes = params.d.as_ref().map(jwu::decode_b64).unwrap().unwrap(); + SecretKey::from_slice(&sk_bytes).unwrap() + } + + pub(crate) fn sign(message: &[u8], private_key: &Jwk) -> impl AsRef<[u8]> { + let sk: SecretKey = expand_k256_jwk(private_key); + let signing_key: SigningKey = SigningKey::from(sk); + let signature: Signature = signature::Signer::sign(&signing_key, message); + signature.to_bytes() + } +} + +use identity_verification::jwk::Jwk; +use identity_verification::jws; +use identity_verification::jws::JwsHeader; + +use crate::EcDSAJwsVerifier; + +#[test] +fn test_es256k_verifier() { + let tv_header: &str = r#"{ + "typ": "JWT", + "alg":"ES256K" + }"#; + let tv_private_key: &str = r#" + { + "kty":"EC", + "crv":"secp256k1", + "d":"y0zUV7bLeUG_kDOvACFHnSmtH7j8MSJek25R2wJbWWg", + "x":"BBobbZkiC8E4C4EYekPNJkcXFCsMNHhh0AV2USy_xSs", + "y":"VQcPHjIQClX0b5TLluFl6jpIf9U-norWC0oEvIQRNyU" + }"#; + let tv_claims: &[u8] = br#"{"key":"value"}"#; + + let header: JwsHeader = serde_json::from_str(tv_header).unwrap(); + let jwk: Jwk = serde_json::from_str(tv_private_key).unwrap(); + let encoder: jws::CompactJwsEncoder<'_> = jws::CompactJwsEncoder::new(tv_claims, &header).unwrap(); + let signing_input: &[u8] = encoder.signing_input(); + let encoded: String = { + let signature = es256k1::sign(signing_input, &jwk); + encoder.into_jws(signature.as_ref()) + }; + + let jws_verifier = EcDSAJwsVerifier::default(); + let jwk: Jwk = serde_json::from_str(tv_private_key).unwrap(); + let decoder = jws::Decoder::new(); + assert!(decoder + .decode_compact_serialization(encoded.as_bytes(), None) + .and_then(|decoded| decoded.verify(&jws_verifier, &jwk)) + .is_ok()); +} + +/// In the absence of official test vectors for secp256k1, +/// this ensures we can verify JWTs created by other libraries. +mod test_es256k_josekit { + use identity_verification::jws; + use josekit::jwk::alg::ec::EcKeyPair; + use josekit::jwk::Jwk; + use josekit::jws::JwsHeader; + use josekit::jwt::JwtPayload; + + use crate::EcDSAJwsVerifier; + + #[test] + fn test_es256k_josekit() { + let alg = josekit::jws::ES256K; + + let private_key: &str = r#" + { + "kty":"EC", + "crv":"secp256k1", + "d":"y0zUV7bLeUG_kDOvACFHnSmtH7j8MSJek25R2wJbWWg", + "x":"BBobbZkiC8E4C4EYekPNJkcXFCsMNHhh0AV2USy_xSs", + "y":"VQcPHjIQClX0b5TLluFl6jpIf9U-norWC0oEvIQRNyU" + }"#; + let josekit_jwk: Jwk = serde_json::from_str(private_key).unwrap(); + let mut src_header = JwsHeader::new(); + src_header.set_token_type("JWT"); + let mut src_payload = JwtPayload::new(); + src_payload.set_claim("key", Some("value".into())).unwrap(); + let eckp = EcKeyPair::from_jwk(&josekit_jwk).unwrap(); + let signer = alg.signer_from_jwk(&eckp.to_jwk_key_pair()).unwrap(); + let jwt_string = josekit::jwt::encode_with_signer(&src_payload, &src_header, &signer).unwrap(); + + let jws_verifier = EcDSAJwsVerifier::default(); + let decoder = jws::Decoder::new(); + let jwk: identity_verification::jwk::Jwk = serde_json::from_str(private_key).unwrap(); + assert!(decoder + .decode_compact_serialization(jwt_string.as_bytes(), None) + .and_then(|decoded| decoded.verify(&jws_verifier, &jwk)) + .is_ok()); + } +} diff --git a/identity_iota_core/src/document/test_utils.rs b/identity_iota_core/src/document/test_utils.rs index b8c48cadf4..b45d418751 100644 --- a/identity_iota_core/src/document/test_utils.rs +++ b/identity_iota_core/src/document/test_utils.rs @@ -24,7 +24,7 @@ fn encode_public_ed25519_jwk(public_key: &[u8]) -> Jwk { let mut params = JwkParamsOkp::new(); params.x = x; params.d = None; - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); let mut jwk = Jwk::from_params(params); jwk.set_alg(JwsAlgorithm::EdDSA.name()); jwk diff --git a/identity_storage/src/key_storage/ed25519.rs b/identity_storage/src/key_storage/ed25519.rs index 8e807af6c9..619493c35d 100644 --- a/identity_storage/src/key_storage/ed25519.rs +++ b/identity_storage/src/key_storage/ed25519.rs @@ -53,6 +53,6 @@ pub(crate) fn encode_jwk(private_key: &SecretKey, public_key: &crypto::signature let mut params = JwkParamsOkp::new(); params.x = x; params.d = Some(d); - EdCurve::Ed25519.name().clone_into(&mut params.crv); + params.crv = EdCurve::Ed25519.name().to_string(); Jwk::from_params(params) } diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index 203ced2e64..9bf4e6ea9a 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -513,10 +513,10 @@ mod tests { let store: JwkMemStore = JwkMemStore::new(); let mut ec_params = JwkParamsEc::new(); - ec_params.crv = EcCurve::P256.name().to_owned(); - ec_params.x = "".to_owned(); - ec_params.y = "".to_owned(); - ec_params.d = Some("".to_owned()); + ec_params.crv = EcCurve::P256.name().to_string(); + ec_params.x = String::new(); + ec_params.y = String::new(); + ec_params.d = Some(String::new()); let jwk_ec = Jwk::from_params(ec_params); let err = store.insert(jwk_ec).await.unwrap_err(); diff --git a/identity_storage/src/key_storage/tests/utils.rs b/identity_storage/src/key_storage/tests/utils.rs index 379df562b4..b5ca210301 100644 --- a/identity_storage/src/key_storage/tests/utils.rs +++ b/identity_storage/src/key_storage/tests/utils.rs @@ -45,10 +45,10 @@ pub(crate) async fn test_incompatible_key_alg(store: impl JwkStorage) { pub(crate) async fn test_incompatible_key_type(store: impl JwkStorage) { let mut ec_params = JwkParamsEc::new(); - ec_params.crv = EcCurve::P256.name().to_owned(); - ec_params.x = "".to_owned(); - ec_params.y = "".to_owned(); - ec_params.d = Some("".to_owned()); + ec_params.crv = EcCurve::P256.name().to_string(); + ec_params.x = String::new(); + ec_params.y = String::new(); + ec_params.d = Some(String::new()); let jwk_ec = Jwk::from_params(ec_params); let err = store.insert(jwk_ec).await.unwrap_err(); diff --git a/identity_storage/src/storage/tests/test_utils.rs b/identity_storage/src/storage/tests/test_utils.rs index ebc0660147..77b1a92072 100644 --- a/identity_storage/src/storage/tests/test_utils.rs +++ b/identity_storage/src/storage/tests/test_utils.rs @@ -192,7 +192,7 @@ pub(crate) fn encode_public_ed25519_jwk(public_key: &PublicKey) -> Jwk { let mut params = JwkParamsOkp::new(); params.x = x; params.d = None; - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); let mut jwk = Jwk::from_params(params); jwk.set_alg(JwsAlgorithm::EdDSA.name()); jwk diff --git a/identity_stronghold/src/ed25519.rs b/identity_stronghold/src/ed25519.rs index 13c3135bb0..933983cdfc 100644 --- a/identity_stronghold/src/ed25519.rs +++ b/identity_stronghold/src/ed25519.rs @@ -53,6 +53,6 @@ pub(crate) fn encode_jwk(private_key: &SecretKey, public_key: &crypto::signature let mut params = JwkParamsOkp::new(); params.x = x; params.d = Some(d); - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); Jwk::from_params(params) } diff --git a/identity_stronghold/src/storage/stronghold_jwk_storage.rs b/identity_stronghold/src/storage/stronghold_jwk_storage.rs index 63535293b1..b0400c8f65 100644 --- a/identity_stronghold/src/storage/stronghold_jwk_storage.rs +++ b/identity_stronghold/src/storage/stronghold_jwk_storage.rs @@ -85,7 +85,7 @@ impl JwkStorage for StrongholdStorage { let mut params = JwkParamsOkp::new(); params.x = jwu::encode_b64(public_key); - EdCurve::Ed25519.name().clone_into(&mut params.crv); + 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()); diff --git a/identity_stronghold/src/tests/test_jwk_storage.rs b/identity_stronghold/src/tests/test_jwk_storage.rs index ca775c95aa..e7ccbb2a05 100644 --- a/identity_stronghold/src/tests/test_jwk_storage.rs +++ b/identity_stronghold/src/tests/test_jwk_storage.rs @@ -171,10 +171,10 @@ mod jwk_storage_tests { pub(crate) async fn test_incompatible_key_type(store: impl JwkStorage) { let mut ec_params = JwkParamsEc::new(); - ec_params.crv = EcCurve::P256.name().to_owned(); - ec_params.x = "".to_owned(); - ec_params.y = "".to_owned(); - ec_params.d = Some("".to_owned()); + ec_params.crv = EcCurve::P256.name().to_string(); + ec_params.x = String::new(); + ec_params.y = String::new(); + ec_params.d = Some(String::new()); let jwk_ec = Jwk::from_params(ec_params); let err = store.insert(jwk_ec).await.unwrap_err(); diff --git a/identity_stronghold/src/tests/utils.rs b/identity_stronghold/src/tests/utils.rs index 9fec954f0f..5113c95f28 100644 --- a/identity_stronghold/src/tests/utils.rs +++ b/identity_stronghold/src/tests/utils.rs @@ -28,7 +28,7 @@ pub(crate) fn encode_public_ed25519_jwk(public_key: &PublicKey) -> Jwk { let mut params = JwkParamsOkp::new(); params.x = x; params.d = None; - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); let mut jwk = Jwk::from_params(params); jwk.set_alg(JwsAlgorithm::EdDSA.name()); jwk From 5cc7f3b4349b403a108f6bf269ffcc8d2fe23a0b Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 22 May 2024 10:40:36 +0200 Subject: [PATCH 33/33] update stronghold and sdk --- Cargo.toml | 3 --- bindings/grpc/Cargo.toml | 2 +- identity_iota/Cargo.toml | 2 +- identity_iota_core/Cargo.toml | 2 +- identity_resolver/Cargo.toml | 2 +- identity_stronghold/Cargo.toml | 4 ++-- 6 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e7eda3efc5..a0375aa810 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,8 +35,5 @@ license = "Apache-2.0" repository = "https://github.com/iotaledger/identity.rs" rust-version = "1.65" -[patch.crates-io] -iota_stronghold = { git = "https://github.com/tensor-programming/stronghold.rs.git", branch = "feat/expose_runner" } - [workspace.lints.clippy] result_large_err = "allow" diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml index f594dc56d4..2b542712db 100644 --- a/bindings/grpc/Cargo.toml +++ b/bindings/grpc/Cargo.toml @@ -22,7 +22,7 @@ futures = { version = "0.3" } identity_eddsa_verifier = { path = "../../identity_eddsa_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.2", features = ["stronghold"] } +iota-sdk = { version = "1.1.5", features = ["stronghold"] } openssl = { version = "0.10", features = ["vendored"] } prost = "0.12" rand = "0.8.5" diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index 38ffb10842..0183994b24 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -23,7 +23,7 @@ identity_verification = { version = "=1.2.0", path = "../identity_verification", [dev-dependencies] anyhow = "1.0.64" -iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client"] } +iota-sdk = { version = "1.1.5", default-features = false, features = ["tls", "client"] } rand = "0.8.5" tokio = { version = "1.29.0", features = ["full"] } diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index 4e55ae36f4..8e0e7070e2 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -19,7 +19,7 @@ identity_credential = { version = "=1.2.0", path = "../identity_credential", def identity_did = { version = "=1.2.0", path = "../identity_did", default-features = false } identity_document = { version = "=1.2.0", path = "../identity_document", default-features = false } identity_verification = { version = "=1.2.0", path = "../identity_verification", default-features = false } -iota-sdk = { version = "1.0.2", default-features = false, features = ["serde", "std"], optional = true } +iota-sdk = { version = "1.1.5", default-features = false, features = ["serde", "std"], optional = true } num-derive = { version = "0.4", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["std"] } once_cell = { version = "1.18", default-features = false, features = ["std"] } diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index 26f4fd6290..d176fc5437 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -32,7 +32,7 @@ optional = true [dev-dependencies] identity_iota_core = { path = "../identity_iota_core", features = ["test"] } -iota-sdk = { version = "1.0.2" } +iota-sdk = { version = "1.1.5" } tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } [features] diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index 6fcf93c4e4..40f549e5d2 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -16,8 +16,8 @@ async-trait = { version = "0.1.64", default-features = false } identity_storage = { version = "=1.2.0", path = "../identity_storage", default_features = false } identity_verification = { version = "=1.2.0", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"] } -iota-sdk = { version = "1.0.2", default-features = false, features = ["client", "stronghold"] } -iota_stronghold = { version = "2.0.0", default-features = false } +iota-sdk = { version = "1.1.5", default-features = false, features = ["client", "stronghold"] } +iota_stronghold = { version = "2.1.0", default-features = false } json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"] }