diff --git a/Cargo.toml b/Cargo.toml index 6dc4d7dae2..25fe6950be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "identity_jose", "identity_ecdsa_verifier", "identity_eddsa_verifier", + "identity_pqc_verifier", "examples", ] @@ -25,8 +26,10 @@ 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.5" } -zkryptium = { version = "0.2.2", default-features = false, features = ["bbsplus"] } +json-proof-token = { version = "0.4.1" } +zkryptium = { version = "0.4.0", default-features = false, features = ["bbsplus"] } +oqs = {version = "0.10.0", default-features = false, features = ["sigs", "std", "vendored"] } + [workspace.package] authors = ["IOTA Stiftung"] diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index be95f2aded..253f9b27c7 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -24,7 +24,7 @@ futures = { version = "0.3" } identity_ecdsa_verifier = { path = "../../identity_ecdsa_verifier", default-features = false, features = ["es256", "es256k"] } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } js-sys = { version = "0.3.61" } -json-proof-token = "0.3.4" +json-proof-token = "0.4.1" proc_typescript = { version = "0.1.0", path = "./proc_typescript" } serde = { version = "1.0", features = ["derive"] } serde-wasm-bindgen = "0.6.5" @@ -34,7 +34,7 @@ serde_repr = { version = "0.1", default-features = false } tokio = { version = "1.29", default-features = false, features = ["sync"] } wasm-bindgen = { version = "0.2.100", features = ["serde-serialize"] } wasm-bindgen-futures = { version = "0.4", default-features = false } -zkryptium = "0.2.2" +zkryptium = "0.4.0" [dependencies.identity_iota] path = "../../identity_iota" @@ -48,6 +48,8 @@ features = [ "status-list-2021", "jpt-bbs-plus", "sd-jwt-vc", + "pqc", + "hybrid" ] [dev-dependencies] diff --git a/bindings/wasm/examples/src/1_advanced/8_zkp.ts b/bindings/wasm/examples/src/1_advanced/8_zkp.ts index 55d0c82fca..127b3fcbb0 100644 --- a/bindings/wasm/examples/src/1_advanced/8_zkp.ts +++ b/bindings/wasm/examples/src/1_advanced/8_zkp.ts @@ -64,7 +64,7 @@ export async function createDid(client: Client, secretManager: SecretManagerType const fragment = await document.generateMethodJwp( storage, - ProofAlgorithm.BLS12381_SHA256, + ProofAlgorithm.BBS, undefined, MethodScope.VerificationMethod(), ); diff --git a/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts b/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts index e8c3d586a1..7d34a19cb7 100644 --- a/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts +++ b/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts @@ -70,7 +70,7 @@ export async function createDid(client: Client, secretManager: SecretManagerType const fragment = await document.generateMethodJwp( storage, - ProofAlgorithm.BLS12381_SHA256, + ProofAlgorithm.BBS, undefined, MethodScope.VerificationMethod(), ); diff --git a/bindings/wasm/examples/src/1_advanced/hybrid.ts b/bindings/wasm/examples/src/1_advanced/hybrid.ts new file mode 100644 index 0000000000..0c4fcf7add --- /dev/null +++ b/bindings/wasm/examples/src/1_advanced/hybrid.ts @@ -0,0 +1,281 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +import { + CoreDID, + Credential, + Duration, + FailFast, + IotaDocument, + IotaIdentityClient, + JwkPqMemStore, + JwsSignatureOptions, + JwsVerificationOptions, + Jwt, + PQJwsVerifier, + EdDSAJwsVerifier, + JwtCredentialValidationOptions, + JwtCredentialValidator, + JwtPresentationOptions, + JwtPresentationValidationOptions, + JwtPresentationValidator, + KeyIdMemStore, + MethodScope, + Presentation, + Resolver, + Storage, + SubjectHolderRelationship, + Timestamp, + CompositeAlgId, + JwtCredentialValidatorHybrid, + JwtPresentationValidatorHybrid, +} from "@iota/identity-wasm/node"; +import { Address, AliasOutput, Client, MnemonicSecretManager, SecretManager, SecretManagerType, Utils } from "@iota/sdk-wasm/node"; +import { API_ENDPOINT, ensureAddressHasFunds } from "../util"; + +async function createHybridDid(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); + + // Create a new method with PQ/T algorithm. + const fragment = await document.generateMethodHybrid( + storage, + CompositeAlgId.IdMldsa44Ed25519, + "#0", + 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 }; +} + +/** + * This example shows how to create an hybrid Verifiable Presentation and validate it + */ +export async function hybrid() { + // =========================================================================== + // Step 1: Create identities for the issuer and the holder. + // =========================================================================== + + const client = new Client({ + primaryNode: API_ENDPOINT, + localPow: true, + }); + const didClient = new IotaIdentityClient(client); + + // Creates a new wallet and identity (see "0_create_did" example). + const issuerSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const issuerStorage: Storage = new Storage( + new JwkPqMemStore(), + new KeyIdMemStore(), + ); + let { document: issuerDocument, fragment: issuerFragment } = await createHybridDid( + client, + issuerSecretManager, + issuerStorage, + ); + + // Create an identity for the holder, in this case also the subject. + const aliceSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const aliceStorage: Storage = new Storage( + new JwkPqMemStore(), + new KeyIdMemStore(), + ); + let { document: aliceDocument, fragment: aliceFragment } = await createHybridDid( + client, + aliceSecretManager, + aliceStorage, + ); + + // =========================================================================== + // Step 2: Issuer creates and signs a Verifiable Credential. + // =========================================================================== + + const subject = { + id: aliceDocument.id(), + name: "Alice", + degreeName: "Bachelor of Science and Arts", + degreeType: "BachelorDegree", + GPA: "4.0", + }; + + // Create an unsigned `UniversityDegree` credential for Alice + const unsignedVc = new Credential({ + id: "https://example.edu/credentials/3732", + type: "UniversityDegreeCredential", + issuer: issuerDocument.id(), + credentialSubject: subject, + }); + + // Create a Credential JWT with the issuer's hybrid verification method. + const credentialJwt = await issuerDocument.createCredentialJwtHybrid( + issuerStorage, + issuerFragment, + unsignedVc, + new JwsSignatureOptions(), + ); + + + const res = new JwtCredentialValidatorHybrid(new EdDSAJwsVerifier, new PQJwsVerifier()).validate( + credentialJwt, + issuerDocument, + new JwtCredentialValidationOptions(), + FailFast.FirstError, + ); + console.log("credentialjwt validation", res.intoCredential()); + + // =========================================================================== + // Step 3: Issuer sends the Verifiable Credential to the holder. + // =========================================================================== + + // The credential is then serialized to JSON and transmitted to the holder in a secure manner. + // Note that the credential is NOT published to the IOTA Tangle. It is sent and stored off-chain. + console.log(`Sending credential (as JWT) to the holder`, unsignedVc.toJSON()); + + // =========================================================================== + // Step 4: Verifier 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. + const nonce = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + // The verifier and holder also agree that the signature should have an expiry date + // 10 minutes from now. + const expires = Timestamp.nowUTC().checkedAdd(Duration.minutes(10)); + + // =========================================================================== + // Step 5: Holder creates a verifiable presentation from the issued credential for the verifier to validate. + // =========================================================================== + + // Create a Verifiable Presentation from the Credential + const unsignedVp = new Presentation({ + holder: aliceDocument.id(), + verifiableCredential: [credentialJwt], + }); + + // Create a Hybrid JWT verifiable presentation using the holder's verification method + // and include the requested challenge and expiry timestamp. + const presentationJwt = await aliceDocument.createPresentationJwtHybrid( + aliceStorage, + aliceFragment, + unsignedVp, + new JwsSignatureOptions({ nonce }), + new JwtPresentationOptions({ expirationDate: expires }), + ); + + // =========================================================================== + // Step 6: Holder sends a verifiable presentation to the verifier. + // =========================================================================== + console.log( + `Sending presentation (as JWT) to the verifier`, + unsignedVp.toJSON(), + ); + + // =========================================================================== + // Step 7: Verifier receives the Verifiable Presentation and verifies it. + // =========================================================================== + + // The verifier wants the following requirements to be satisfied: + // - JWT verification of the presentation (including checking the requested challenge to mitigate replay attacks) + // - JWT verification of the credentials. + // - The presentation holder must always be the subject, regardless of the presence of the nonTransferable property + // - The issuance date must not be in the future. + + const jwtPresentationValidationOptions = new JwtPresentationValidationOptions( + { + presentationVerifierOptions: new JwsVerificationOptions({ nonce }), + }, + ); + + const resolver = new Resolver({ + client: didClient, + }); + // Resolve the presentation holder. + const presentationHolderDID: CoreDID = JwtPresentationValidator.extractHolder(presentationJwt); + const resolvedHolder = await resolver.resolve( + presentationHolderDID.toString(), + ); + + // Validate presentation. Note that this doesn't validate the included credentials. + let decodedPresentation = new JwtPresentationValidatorHybrid(new EdDSAJwsVerifier, new PQJwsVerifier()).validate( + presentationJwt, + resolvedHolder, + jwtPresentationValidationOptions, + ); + + // Validate the hybrid credentials in the presentation. + let credentialValidator = new JwtCredentialValidatorHybrid(new EdDSAJwsVerifier, new PQJwsVerifier()); + let validationOptions = new JwtCredentialValidationOptions({ + subjectHolderRelationship: [ + presentationHolderDID.toString(), + SubjectHolderRelationship.AlwaysSubject, + ], + }); + + let jwtCredentials: Jwt[] = decodedPresentation + .presentation() + .verifiableCredential() + .map((credential) => { + const jwt = credential.tryIntoJwt(); + if (!jwt) { + throw new Error("expected a JWT credential"); + } else { + return jwt; + } + }); + + // Concurrently resolve the issuers' documents. + let issuers: string[] = []; + for (let jwtCredential of jwtCredentials) { + let issuer = JwtCredentialValidator.extractIssuerFromJwt(jwtCredential); + issuers.push(issuer.toString()); + } + let resolvedIssuers = await resolver.resolveMultiple(issuers); + + // Validate the credentials in the presentation. + for (let i = 0; i < jwtCredentials.length; i++) { + credentialValidator.validate( + jwtCredentials[i], + resolvedIssuers[i], + validationOptions, + FailFast.FirstError, + ); + } + + // Since no errors were thrown we know that the validation was successful. + console.log(`VP successfully validated`); +} diff --git a/bindings/wasm/examples/src/1_advanced/pq.ts b/bindings/wasm/examples/src/1_advanced/pq.ts new file mode 100644 index 0000000000..c7229d44d8 --- /dev/null +++ b/bindings/wasm/examples/src/1_advanced/pq.ts @@ -0,0 +1,278 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +import { + CoreDID, + Credential, + Duration, + FailFast, + IotaDocument, + IotaIdentityClient, + JwkPqMemStore, + JwsAlgorithm, + JwsSignatureOptions, + JwsVerificationOptions, + Jwt, + PQJwsVerifier, + JwtCredentialValidationOptions, + JwtCredentialValidator, + JwtPresentationOptions, + JwtPresentationValidationOptions, + JwtPresentationValidator, + KeyIdMemStore, + MethodScope, + Presentation, + Resolver, + Storage, + SubjectHolderRelationship, + Timestamp, +} from "@iota/identity-wasm/node"; +import { Address, AliasOutput, Client, MnemonicSecretManager, SecretManager, SecretManagerType, Utils } from "@iota/sdk-wasm/node"; +import { API_ENDPOINT, ensureAddressHasFunds } from "../util"; + +async function createPQDid(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); + + // Create a new method with PQ algorithm. + const fragment = await document.generateMethodPQC( + storage, + JwkPqMemStore.mldsaKeyType(), + JwsAlgorithm.MLDSA44, + "#0", + 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 }; +} + +/** + * This example shows how to create a PQ Verifiable Presentation and validate it + */ +export async function pq() { + // =========================================================================== + // Step 1: Create identities for the issuer and the holder. + // =========================================================================== + + const client = new Client({ + primaryNode: API_ENDPOINT, + localPow: true, + }); + const didClient = new IotaIdentityClient(client); + + // Creates a new wallet and identity (see "0_create_did" example). + const issuerSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const issuerStorage: Storage = new Storage( + new JwkPqMemStore(), + new KeyIdMemStore(), + ); + let { document: issuerDocument, fragment: issuerFragment } = await createPQDid( + client, + issuerSecretManager, + issuerStorage, + ); + + // Create an identity for the holder, in this case also the subject. + const aliceSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const aliceStorage: Storage = new Storage( + new JwkPqMemStore(), + new KeyIdMemStore(), + ); + let { document: aliceDocument, fragment: aliceFragment } = await createPQDid( + client, + aliceSecretManager, + aliceStorage, + ); + + // =========================================================================== + // Step 2: Issuer creates and signs a Verifiable Credential. + // =========================================================================== + + const subject = { + id: aliceDocument.id(), + name: "Alice", + degreeName: "Bachelor of Science and Arts", + degreeType: "BachelorDegree", + GPA: "4.0", + }; + + // Create an unsigned `UniversityDegree` credential for Alice + const unsignedVc = new Credential({ + id: "https://example.edu/credentials/3732", + type: "UniversityDegreeCredential", + issuer: issuerDocument.id(), + credentialSubject: subject, + }); + + // Create a Credential JWT with the issuer's PQ verification method. + const credentialJwt = await issuerDocument.createCredentialJwtPqc( + issuerStorage, + issuerFragment, + unsignedVc, + new JwsSignatureOptions(), + ); + + const res = new JwtCredentialValidator(new PQJwsVerifier()).validate( + credentialJwt, + issuerDocument, + new JwtCredentialValidationOptions(), + FailFast.FirstError, + ); + console.log("credentialjwt validation", res.intoCredential()); + + // =========================================================================== + // Step 3: Issuer sends the Verifiable Credential to the holder. + // =========================================================================== + + // The credential is then serialized to JSON and transmitted to the holder in a secure manner. + // Note that the credential is NOT published to the IOTA Tangle. It is sent and stored off-chain. + console.log(`Sending credential (as JWT) to the holder`, unsignedVc.toJSON()); + + // =========================================================================== + // Step 4: Verifier 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. + const nonce = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + // The verifier and holder also agree that the signature should have an expiry date + // 10 minutes from now. + const expires = Timestamp.nowUTC().checkedAdd(Duration.minutes(10)); + + // =========================================================================== + // Step 5: Holder creates a verifiable presentation from the issued credential for the verifier to validate. + // =========================================================================== + + // Create a Verifiable Presentation from the Credential + const unsignedVp = new Presentation({ + holder: aliceDocument.id(), + verifiableCredential: [credentialJwt], + }); + + // Create a PQ JWT verifiable presentation using the holder's verification method + // and include the requested challenge and expiry timestamp. + const presentationJwt = await aliceDocument.createPresentationJwtPqc( + aliceStorage, + aliceFragment, + unsignedVp, + new JwsSignatureOptions({ nonce }), + new JwtPresentationOptions({ expirationDate: expires }), + ); + + // =========================================================================== + // Step 6: Holder sends a verifiable presentation to the verifier. + // =========================================================================== + console.log( + `Sending presentation (as JWT) to the verifier`, + unsignedVp.toJSON(), + ); + + // =========================================================================== + // Step 7: Verifier receives the Verifiable Presentation and verifies it. + // =========================================================================== + + // The verifier wants the following requirements to be satisfied: + // - JWT verification of the presentation (including checking the requested challenge to mitigate replay attacks) + // - JWT verification of the credentials. + // - The presentation holder must always be the subject, regardless of the presence of the nonTransferable property + // - The issuance date must not be in the future. + + const jwtPresentationValidationOptions = new JwtPresentationValidationOptions( + { + presentationVerifierOptions: new JwsVerificationOptions({ nonce }), + }, + ); + + const resolver = new Resolver({ + client: didClient, + }); + // Resolve the presentation holder. + const presentationHolderDID: CoreDID = JwtPresentationValidator.extractHolder(presentationJwt); + const resolvedHolder = await resolver.resolve( + presentationHolderDID.toString(), + ); + + // Validate presentation. Note that this doesn't validate the included credentials. + let decodedPresentation = new JwtPresentationValidator(new PQJwsVerifier()).validate( + presentationJwt, + resolvedHolder, + jwtPresentationValidationOptions, + ); + + // Validate the credentials in the presentation. + let credentialValidator = new JwtCredentialValidator(new PQJwsVerifier()); + let validationOptions = new JwtCredentialValidationOptions({ + subjectHolderRelationship: [ + presentationHolderDID.toString(), + SubjectHolderRelationship.AlwaysSubject, + ], + }); + + let jwtCredentials: Jwt[] = decodedPresentation + .presentation() + .verifiableCredential() + .map((credential) => { + const jwt = credential.tryIntoJwt(); + if (!jwt) { + throw new Error("expected a JWT credential"); + } else { + return jwt; + } + }); + + // Concurrently resolve the issuers' documents. + let issuers: string[] = []; + for (let jwtCredential of jwtCredentials) { + let issuer = JwtCredentialValidator.extractIssuerFromJwt(jwtCredential); + issuers.push(issuer.toString()); + } + let resolvedIssuers = await resolver.resolveMultiple(issuers); + + // Validate the credentials in the presentation. + for (let i = 0; i < jwtCredentials.length; i++) { + credentialValidator.validate( + jwtCredentials[i], + resolvedIssuers[i], + validationOptions, + FailFast.FirstError, + ); + } + + // Since no errors were thrown we know that the validation was successful. + console.log(`VP successfully validated`); +} diff --git a/bindings/wasm/examples/src/main.ts b/bindings/wasm/examples/src/main.ts index c88ee419c0..0a28208978 100644 --- a/bindings/wasm/examples/src/main.ts +++ b/bindings/wasm/examples/src/main.ts @@ -1,5 +1,8 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ import { createIdentity } from "./0_basic/0_create_did"; import { updateIdentity } from "./0_basic/1_update_did"; @@ -11,6 +14,8 @@ import { createVP } from "./0_basic/6_create_vp"; import { revokeVC } from "./0_basic/7_revoke_vc"; import { didControlsDid } from "./1_advanced/0_did_controls_did"; import { sdJwtVc } from "./1_advanced/10_sd_jwt_vc"; +import { pq } from "./1_advanced/pq"; +import { hybrid } from "./1_advanced/hybrid"; import { didIssuesNft } from "./1_advanced/1_did_issues_nft"; import { nftOwnsDid } from "./1_advanced/2_nft_owns_did"; import { didIssuesTokens } from "./1_advanced/3_did_issues_tokens"; @@ -67,6 +72,10 @@ async function main() { return await zkp_revocation(); case "10_sd_jwt_vc": return await sdJwtVc(); + case "pq": + return await pq(); + case "hybrid": + return await hybrid(); default: throw "Unknown example name: '" + argument + "'"; } diff --git a/bindings/wasm/lib/index.ts b/bindings/wasm/lib/index.ts index dd46503528..e6b76d9b1c 100644 --- a/bindings/wasm/lib/index.ts +++ b/bindings/wasm/lib/index.ts @@ -1,10 +1,13 @@ // Copyright 2021-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 - +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ import "./append_functions.js"; export * from "./iota_identity_client.js"; export * from "./jose"; export * from "./jwk_storage"; +export * from "./jwk_storage_pq"; export * from "./key_id_storage"; - +export * from "./pq_verifier"; export * from "~identity_wasm"; diff --git a/bindings/wasm/lib/jose/composite_jwk.ts b/bindings/wasm/lib/jose/composite_jwk.ts new file mode 100644 index 0000000000..54ba4b13b8 --- /dev/null +++ b/bindings/wasm/lib/jose/composite_jwk.ts @@ -0,0 +1,11 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +export const enum CompositeAlgId { + /** DER encoded value in hex = 060B6086480186FA6B5008013E */ + IdMldsa44Ed25519 = "id-MLDSA44-Ed25519", + + /** /// DER encoded value in hex = 060B6086480186FA6B50080147 */ + IdMldsa65Ed25519 = "id-MLDSA65-Ed25519" + +} \ No newline at end of file diff --git a/bindings/wasm/lib/jose/index.ts b/bindings/wasm/lib/jose/index.ts index f033c4f110..44c7c2c890 100644 --- a/bindings/wasm/lib/jose/index.ts +++ b/bindings/wasm/lib/jose/index.ts @@ -4,3 +4,4 @@ export * from "./jwk_operation"; export * from "./jwk_type"; export * from "./jwk_use"; export * from "./jws_algorithm"; +export * from "./composite_jwk"; \ No newline at end of file diff --git a/bindings/wasm/lib/jose/jwk_type.ts b/bindings/wasm/lib/jose/jwk_type.ts index 1707c3295f..820308bd3b 100644 --- a/bindings/wasm/lib/jose/jwk_type.ts +++ b/bindings/wasm/lib/jose/jwk_type.ts @@ -10,4 +10,6 @@ export const enum JwkType { Oct = "oct", /** Octet string key pairs. */ Okp = "OKP", + /** Algorithm key pair. */ + Akp = "AKP", } diff --git a/bindings/wasm/lib/jose/jws_algorithm.ts b/bindings/wasm/lib/jose/jws_algorithm.ts index 54cd306a27..edfa545a06 100644 --- a/bindings/wasm/lib/jose/jws_algorithm.ts +++ b/bindings/wasm/lib/jose/jws_algorithm.ts @@ -32,4 +32,15 @@ export const enum JwsAlgorithm { NONE = "none", /** EdDSA signature algorithms */ EdDSA = "EdDSA", + /** ML-DSA-44 */ + MLDSA44 = "ML-DSA-44", + /** ML-DSA-65 */ + MLDSA65 = "ML-DSA-65", + /** ML-DSA-87 */ + MLDSA87 = "ML-DSA-87", + /** ML-DSA-44 in hybrid signature*/ + IdMldsa44Ed25519 = "id-MLDSA44-Ed25519", + /** ML-DSA-65 in hybrid signature*/ + IdMldsa65Ed25519 = "id-MLDSA65-Ed25519", + } diff --git a/bindings/wasm/lib/jwk_storage_pq.ts b/bindings/wasm/lib/jwk_storage_pq.ts new file mode 100644 index 0000000000..04058ec027 --- /dev/null +++ b/bindings/wasm/lib/jwk_storage_pq.ts @@ -0,0 +1,271 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 +import * as ed from "@noble/ed25519"; +import { ml_dsa44, ml_dsa65, ml_dsa87 } from '@noble/post-quantum/ml-dsa'; +import { decodeB64, encodeB64, Jwk, JwkGenOutput, JwkStoragePQ, JwkStorage } from "~identity_wasm"; +import { EdCurve, JwkType, JwsAlgorithm} from "./jose"; + +type Ed25519PrivateKey = Uint8Array; +type Ed25519PublicKey = Uint8Array; + + +//JkwStorage for PQ and PQ/T examples +export class JwkPqMemStore implements JwkStorage, JwkStoragePQ{ + /** The map from key identifiers to Jwks. */ + private _keys: Map; + + /** Creates a new, empty `MemStore` instance. */ + constructor() { + this._keys = new Map(); + } + + public static mldsaKeyType(): string { + return "AKP"; + } + + public static ed25519KeyType(): string { + return "Ed25519"; + } + + + private _get_key(keyId: string): Jwk | undefined { + return this._keys.get(keyId); + } + + public async generate(keyType: string, algorithm: JwsAlgorithm): Promise { + if (keyType !== JwkPqMemStore.ed25519KeyType()) { + throw new Error(`unsupported key type ${keyType}`); + } + + if (algorithm !== JwsAlgorithm.EdDSA) { + throw new Error(`unsupported algorithm`); + } + + const keyId = randomKeyId(); + const privKey: Ed25519PrivateKey = ed.utils.randomPrivateKey(); + + const publicKey: Ed25519PublicKey = await ed.getPublicKey(privKey); + const jwk = await encodeJwk(privKey, publicKey, algorithm); + + this._keys.set(keyId, jwk); + + const publicJWK = jwk?.toPublic(); + if (!publicJWK) { + throw new Error(`JWK is not a public key`); + } + + return new JwkGenOutput(keyId, publicJWK); + } + + public async generatePQKey(keyType: String, algorithm: JwsAlgorithm): Promise { + + if (keyType !== JwkPqMemStore.mldsaKeyType()) { + throw new Error(`unsupported key type ${keyType}`); + } + + const seed = new TextEncoder().encode(randomKeyId()) + let keys; + if (algorithm === JwsAlgorithm.MLDSA44) { + keys = ml_dsa44.keygen(seed); + } else if(algorithm === JwsAlgorithm.MLDSA65) { + keys = ml_dsa65.keygen(seed); + } else if(algorithm === JwsAlgorithm.MLDSA87) { + keys = ml_dsa87.keygen(seed); + } else { + throw new Error(`unsupported algorithm`); + } + + const keyId = randomKeyId(); + + const jwk = await encodeJwk(keys.secretKey, keys.publicKey, algorithm); + + if(jwk == undefined) + throw new Error("Unexpected error: await encodeJwk(privKey, publicKey, algorithm)"); + + this._keys.set(keyId, jwk); + + const publicJWK = jwk?.toPublic(); + if (!publicJWK) { + throw new Error(`JWK is not a public key`); + } + + return new JwkGenOutput(keyId, publicJWK); + + } + + public async sign(keyId: string, data: Uint8Array, publicKey: Jwk): Promise { + let alg = publicKey.alg(); + let signature = null; + + if(alg === undefined) { + throw new Error("expected a Jwk with an `alg` parameter"); + } + + if (alg !== JwsAlgorithm.EdDSA ) { + throw new Error("unsupported JWS algorithm"); + } else { + if (publicKey.paramsOkp()?.crv !== (EdCurve.Ed25519 as string)) + { + throw new Error("unsupported Okp parameter"); + } + } + + const jwk = this._keys.get(keyId); + + if (jwk) { + const [privateKey, _] = decodeJwk(jwk); + signature = await ed.sign(data, privateKey); + + } else { + throw new Error(`key with id ${keyId} not found`); + } + return signature; + } + + public async signPQ(keyId: string, data: Uint8Array, publicKey: Jwk, ctx: Uint8Array|undefined ): Promise { + let alg = publicKey.alg(); + let signature = null; + + if(alg === undefined) { + throw new Error("expected a Jwk with an `alg` parameter"); + } + + if (alg !== JwsAlgorithm.MLDSA44 && alg !== JwsAlgorithm.MLDSA65 && alg !== JwsAlgorithm.MLDSA87) { + throw new Error("unsupported JWS algorithm"); + } + + const jwk = this._keys.get(keyId); + + if (jwk) { + + const [privateKey, _] = decodeJwk(jwk); + + if(alg == JwsAlgorithm.MLDSA44) + signature = ml_dsa44.sign(privateKey, data, ctx); + else if(alg == JwsAlgorithm.MLDSA65) + signature = ml_dsa65.sign(privateKey, data, ctx); + else if(alg == JwsAlgorithm.MLDSA87) + signature = ml_dsa87.sign(privateKey, data, ctx); + else + throw new Error("unsupported algorithm"); + + } else { + throw new Error(`key with id ${keyId} not found`); + } + return signature; + } + + public async insert(jwk: Jwk): Promise { + const keyId = randomKeyId(); + + if (!jwk.isPrivate) { + throw new Error("expected a JWK with all private key components set"); + } + + if (!jwk.alg()) { + throw new Error("expected a Jwk with an `alg` parameter"); + } + + this._keys.set(keyId, jwk); + + return keyId; + } + + public async delete(keyId: string): Promise { + this._keys.delete(keyId); + } + + public async exists(keyId: string): Promise { + return this._keys.has(keyId); + } + + public count(): number { + return this._keys.size; + } +} + +// Encodes a Ed25519 keypair into a Jwk. +async function encodeJwk(privateKey: Uint8Array, publicKey: Uint8Array, alg: JwsAlgorithm): Promise { + let pub = encodeB64(publicKey); + let priv = encodeB64(privateKey); + + if (alg === JwsAlgorithm.EdDSA) { + return new Jwk({ + "kty": JwkType.Okp, + crv: "Ed25519", + d: priv, + x: pub, + alg, + }); + } else { + return new Jwk({ + "kty": JwkType.Akp, + pub: pub, + priv: priv, + alg, + }); + } + +} + +function decodeJwk(jwk: Jwk): [Uint8Array, Uint8Array] { + if (jwk.alg()! !== JwsAlgorithm.MLDSA44 && + jwk.alg()! !== JwsAlgorithm.MLDSA65 && + jwk.alg()! !== JwsAlgorithm.MLDSA87 && + jwk.alg()! !== JwsAlgorithm.EdDSA) { + throw new Error("unsupported `alg`"); + } + if (jwk.alg()! === JwsAlgorithm.EdDSA) { + const paramsOkp = jwk.paramsOkp(); + if (paramsOkp) { + const d = paramsOkp.d; + + if (d) { + const textEncoder = new TextEncoder(); + const privateKey = decodeB64(textEncoder.encode(d)); + const publicKey = decodeB64(textEncoder.encode(paramsOkp.x)); + return [privateKey, publicKey]; + } else { + throw new Error("missing private key component"); + } + } else { + throw new Error("expected Okp params"); + } + } else { + const paramsPQ = jwk.paramsAkp(); + + if (paramsPQ) { + const priv = paramsPQ.priv; + + if (priv) { + let textEncoder = new TextEncoder(); + const privateKey = decodeB64(textEncoder.encode(priv)); + const publicKey = decodeB64(textEncoder.encode(paramsPQ.pub)); + return [privateKey, publicKey]; + } else { + throw new Error("missing private key component"); + } + } else { + throw new Error("expected Okp params"); + } + + } + +} + +// 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. +function getRandomNumber(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +// Returns a random key id. +function randomKeyId(): string { + const randomness = new Uint8Array(20); + for (let index = 0; index < randomness.length; index++) { + randomness[index] = getRandomNumber(0, 255); + } + + return encodeB64(randomness); +} diff --git a/bindings/wasm/lib/pq_verifier.ts b/bindings/wasm/lib/pq_verifier.ts new file mode 100644 index 0000000000..9fa5582d58 --- /dev/null +++ b/bindings/wasm/lib/pq_verifier.ts @@ -0,0 +1,61 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +import { ml_dsa44, ml_dsa65, ml_dsa87 } from '@noble/post-quantum/ml-dsa'; +import { decodeB64, Jwk, IJwsVerifier} from "~identity_wasm"; +import { JwsAlgorithm } from "./jose"; + +export class PQJwsVerifier implements IJwsVerifier{ + + public verify (alg: JwsAlgorithm, signingInput: Uint8Array, decodedSignature: Uint8Array, publicKey: Jwk): void{ + let res = false; + let ctx = undefined; + + if (alg !== JwsAlgorithm.MLDSA44 && + alg !== JwsAlgorithm.MLDSA65 && + alg !== JwsAlgorithm.MLDSA87 && + alg !== JwsAlgorithm.IdMldsa44Ed25519 && + alg !== JwsAlgorithm.IdMldsa65Ed25519) { + throw new Error("unsupported JWS algorithm"); + } + + const pubKey = decodeJwk(publicKey); + + //Domain separator for hybrid signatures + if (alg === JwsAlgorithm.IdMldsa44Ed25519) { + ctx = Uint8Array.from([6, 11, 96, 134, 72, 1, 134, 250, 107, 80, 8, 1, 62]); + } else if (alg === JwsAlgorithm.IdMldsa65Ed25519) { + ctx = Uint8Array.from([6, 11, 96, 134, 72, 1, 134, 250, 107, 80, 8, 1, 71]); + } + + if (alg === JwsAlgorithm.MLDSA44 || alg === JwsAlgorithm.IdMldsa44Ed25519) { + res = ml_dsa44.verify(pubKey, signingInput, decodedSignature, ctx); + } else if (alg === JwsAlgorithm.MLDSA65 || alg === JwsAlgorithm.IdMldsa65Ed25519) { + res = ml_dsa65.verify(pubKey, signingInput, decodedSignature, ctx); + } else if (alg === JwsAlgorithm.MLDSA87) { + res = ml_dsa87.verify(pubKey, signingInput, decodedSignature); + } + if (!res) { + throw new Error("signature verification failed"); + } + } + +} + +function decodeJwk(jwk: Jwk): Uint8Array { + if (jwk.alg()! !== JwsAlgorithm.MLDSA44 && jwk.alg()! !== JwsAlgorithm.MLDSA65 && jwk.alg()! !== JwsAlgorithm.MLDSA87) { + throw new Error("unsupported `alg`"); + } + + const paramsPQ = jwk.paramsAkp(); + + if (paramsPQ) { + let textEncoder = new TextEncoder(); + return decodeB64(textEncoder.encode(paramsPQ.pub)); + } else { + throw new Error("expected Okp params"); + } +} + + + diff --git a/bindings/wasm/package-lock.json b/bindings/wasm/package-lock.json index a002e5845f..0c6d032ecd 100644 --- a/bindings/wasm/package-lock.json +++ b/bindings/wasm/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@noble/ed25519": "^1.7.3", + "@noble/post-quantum": "^0.2.0", "@types/node-fetch": "^2.6.2", "base64-arraybuffer": "^1.0.2", "jose": "^5.9.6", @@ -334,6 +335,28 @@ } ] }, + "node_modules/@noble/hashes": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/post-quantum": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.2.1.tgz", + "integrity": "sha512-ImgfMp9notXSEocz464o1AefYfFWEkkszKMGO+ZiTn73yIBFeNyEHKQUMS+SheJwSNymldSts6YyVcQDjcnVVg==", + "dependencies": { + "@noble/hashes": "1.6.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6535,6 +6558,19 @@ "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", "integrity": "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==" }, + "@noble/hashes": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==" + }, + "@noble/post-quantum": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.2.1.tgz", + "integrity": "sha512-ImgfMp9notXSEocz464o1AefYfFWEkkszKMGO+ZiTn73yIBFeNyEHKQUMS+SheJwSNymldSts6YyVcQDjcnVVg==", + "requires": { + "@noble/hashes": "1.6.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index 779075102c..d26068bf80 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -78,6 +78,7 @@ }, "dependencies": { "@noble/ed25519": "^1.7.3", + "@noble/post-quantum": "^0.2.0", "@types/node-fetch": "^2.6.2", "base64-arraybuffer": "^1.0.2", "jose": "^5.9.6", diff --git a/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator_hybrid.rs b/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator_hybrid.rs new file mode 100644 index 0000000000..db99373a99 --- /dev/null +++ b/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator_hybrid.rs @@ -0,0 +1,211 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Object; +use identity_iota::core::Url; +use identity_iota::credential::JwtCredentialValidatorHybrid; +use identity_iota::credential::JwtCredentialValidatorUtils; +use identity_iota::credential::StatusCheck; +use identity_iota::did::CoreDID; + +use super::options::WasmJwtCredentialValidationOptions; +use crate::common::ImportedDocumentLock; +use crate::common::ImportedDocumentReadGuard; +use crate::common::WasmTimestamp; +use crate::credential::options::WasmStatusCheck; +use crate::credential::revocation::status_list_2021::WasmStatusList2021Credential; +use crate::credential::WasmCredential; +use crate::credential::WasmDecodedJwtCredential; +use crate::credential::WasmFailFast; +use crate::credential::WasmJwt; +use crate::credential::WasmSubjectHolderRelationship; +use crate::did::ArrayIToCoreDocument; +use crate::did::IToCoreDocument; +use crate::did::WasmCoreDID; +use crate::did::WasmJwsVerificationOptions; +use crate::error::Result; +use crate::error::WasmResult; +use crate::verification::IJwsVerifier; +use crate::verification::WasmJwsVerifier; + +use wasm_bindgen::prelude::*; + +/// A type for decoding and validating PQ/T {@link Credential}. +#[wasm_bindgen(js_name = JwtCredentialValidatorHybrid)] +pub struct WasmJwtCredentialValidatorHybrid(JwtCredentialValidatorHybrid); + +#[wasm_bindgen(js_class = JwtCredentialValidatorHybrid)] +impl WasmJwtCredentialValidatorHybrid { + /// Creates a new {@link JwtCredentialValidatorHybrid}. + #[wasm_bindgen(constructor)] + #[allow(non_snake_case)] + pub fn new(traditionalSignatureVerifier: Option, pqSignatureVerifier: Option) -> WasmJwtCredentialValidatorHybrid { + let traditional_signature_verifier = WasmJwsVerifier::new(traditionalSignatureVerifier); + let pq_signature_verifier = WasmJwsVerifier::new(pqSignatureVerifier); + WasmJwtCredentialValidatorHybrid(JwtCredentialValidatorHybrid::with_signature_verifiers(traditional_signature_verifier, pq_signature_verifier)) + } + + /// Decodes and validates a {@link Credential} issued as a JWS. A {@link DecodedJwtCredential} is returned upon + /// success. + /// + /// The following properties are validated according to `options`: + /// - the issuer's signature on the JWS, + /// - the expiration date, + /// - the issuance date, + /// - the semantic structure. + /// + /// # Warning + /// The lack of an error returned from this method is in of itself not enough to conclude that the credential can be + /// trusted. This section contains more information on additional checks that should be carried out before and after + /// calling this method. + /// + /// ## The state of the issuer's DID Document + /// The caller must ensure that `issuer` represents an up-to-date DID Document. + /// + /// ## Properties that are not validated + /// There are many properties defined in [The Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/) that are **not** validated, such as: + /// `proof`, `credentialStatus`, `type`, `credentialSchema`, `refreshService` **and more**. + /// These should be manually checked after validation, according to your requirements. + /// + /// # Errors + /// An error is returned whenever a validated condition is not satisfied. + #[wasm_bindgen] + pub fn validate( + &self, + credential_jwt: &WasmJwt, + issuer: &IToCoreDocument, + options: &WasmJwtCredentialValidationOptions, + fail_fast: WasmFailFast, + ) -> Result { + let issuer_lock = ImportedDocumentLock::from(issuer); + let issuer_guard = issuer_lock.try_read()?; + + self + .0 + .validate(&credential_jwt.0, &issuer_guard, &options.0, fail_fast.into()) + .wasm_result() + .map(WasmDecodedJwtCredential) + } + + /// Decode and verify the JWS signature of a {@link Credential} issued as a JWT using the DID Document of a trusted + /// issuer. + /// + /// A {@link DecodedJwtCredential} is returned upon success. + /// + /// # Warning + /// The caller must ensure that the DID Documents of the trusted issuers are up-to-date. + /// + /// ## Proofs + /// Only the JWS signature is verified. If the {@link Credential} contains a `proof` property this will not be + /// verified by this method. + /// + /// # Errors + /// This method immediately returns an error if + /// the credential issuer' url cannot be parsed to a DID belonging to one of the trusted issuers. Otherwise an attempt + /// to verify the credential's signature will be made and an error is returned upon failure. + #[wasm_bindgen(js_name = verifySignature)] + #[allow(non_snake_case)] + pub fn verify_signature( + &self, + credential: &WasmJwt, + trustedIssuers: &ArrayIToCoreDocument, + options: &WasmJwsVerificationOptions, + ) -> Result { + let issuer_locks: Vec = trustedIssuers.into(); + let trusted_issuers: Vec> = issuer_locks + .iter() + .map(ImportedDocumentLock::try_read) + .collect::>>>( + )?; + + self + .0 + .verify_signature(&credential.0, &trusted_issuers, &options.0) + .wasm_result() + .map(WasmDecodedJwtCredential) + } + + /// Validate that the credential expires on or after the specified timestamp. + #[wasm_bindgen(js_name = checkExpiresOnOrAfter)] + pub fn check_expires_on_or_after(credential: &WasmCredential, timestamp: &WasmTimestamp) -> Result<()> { + JwtCredentialValidatorUtils::check_expires_on_or_after(&credential.0, timestamp.0).wasm_result() + } + + /// Validate that the credential is issued on or before the specified timestamp. + #[wasm_bindgen(js_name = checkIssuedOnOrBefore)] + pub fn check_issued_on_or_before(credential: &WasmCredential, timestamp: &WasmTimestamp) -> Result<()> { + JwtCredentialValidatorUtils::check_issued_on_or_before(&credential.0, timestamp.0).wasm_result() + } + + /// Validate that the relationship between the `holder` and the credential subjects is in accordance with + /// `relationship`. The `holder` parameter is expected to be the URL of the holder. + #[wasm_bindgen(js_name = checkSubjectHolderRelationship)] + pub fn check_subject_holder_relationship( + credential: &WasmCredential, + holder: &str, + relationship: WasmSubjectHolderRelationship, + ) -> Result<()> { + let holder: Url = Url::parse(holder).wasm_result()?; + JwtCredentialValidatorUtils::check_subject_holder_relationship(&credential.0, &holder, relationship.into()) + .wasm_result() + } + + /// Checks whether the credential status has been revoked. + /// + /// Only supports `RevocationBitmap2022`. + #[wasm_bindgen(js_name = checkStatus)] + #[allow(non_snake_case)] + pub fn check_status( + credential: &WasmCredential, + trustedIssuers: &ArrayIToCoreDocument, + statusCheck: WasmStatusCheck, + ) -> Result<()> { + let issuer_locks: Vec = trustedIssuers.into(); + let trusted_issuers: Vec> = issuer_locks + .iter() + .map(ImportedDocumentLock::try_read) + .collect::>>>( + )?; + let status_check: StatusCheck = statusCheck.into(); + JwtCredentialValidatorUtils::check_status(&credential.0, &trusted_issuers, status_check).wasm_result() + } + + /// Checks wheter the credential status has been revoked using `StatusList2021`. + #[wasm_bindgen(js_name = checkStatusWithStatusList2021)] + pub fn check_status_with_status_list_2021( + credential: &WasmCredential, + status_list: &WasmStatusList2021Credential, + status_check: WasmStatusCheck, + ) -> Result<()> { + JwtCredentialValidatorUtils::check_status_with_status_list_2021( + &credential.0, + &status_list.inner, + status_check.into(), + ) + .wasm_result() + } + + /// 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 { + JwtCredentialValidatorUtils::extract_issuer::(&credential.0) + .map(WasmCoreDID::from) + .wasm_result() + } + + /// Utility for extracting the issuer field of a credential in JWT representation as DID. + /// + /// # Errors + /// + /// If the JWT decoding fails or the issuer field is not a valid DID. + #[wasm_bindgen(js_name = extractIssuerFromJwt)] + pub fn extract_issuer_from_jwt(credential: &WasmJwt) -> Result { + JwtCredentialValidatorUtils::extract_issuer_from_jwt::(&credential.0) + .map(WasmCoreDID::from) + .wasm_result() + } +} diff --git a/bindings/wasm/src/credential/jwt_credential_validation/mod.rs b/bindings/wasm/src/credential/jwt_credential_validation/mod.rs index 826d0388d9..c0c3adc88b 100644 --- a/bindings/wasm/src/credential/jwt_credential_validation/mod.rs +++ b/bindings/wasm/src/credential/jwt_credential_validation/mod.rs @@ -1,5 +1,8 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ mod decoded_jwt_credential; mod jwt_credential_validator; @@ -7,9 +10,11 @@ mod kb_validation_options; mod options; mod sd_jwt_validator; mod unknown_credential; +mod jwt_credential_validator_hybrid; pub use self::decoded_jwt_credential::*; pub use self::jwt_credential_validator::*; +pub use self::jwt_credential_validator_hybrid::*; pub use self::kb_validation_options::*; pub use self::options::*; pub use self::sd_jwt_validator::*; diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator_hybrid.rs b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator_hybrid.rs new file mode 100644 index 0000000000..219943b0cc --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator_hybrid.rs @@ -0,0 +1,95 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use super::decoded_jwt_presentation::WasmDecodedJwtPresentation; +use super::options::WasmJwtPresentationValidationOptions; +use crate::common::ImportedDocumentLock; +use crate::credential::WasmJwt; +use crate::credential::WasmPresentation; +use crate::did::IToCoreDocument; +use crate::did::WasmCoreDID; +use crate::error::Result; +use crate::error::WasmResult; +use crate::verification::IJwsVerifier; +use crate::verification::WasmJwsVerifier; +use identity_iota::credential::JwtPresentationValidatorHybrid; +use identity_iota::credential::JwtPresentationValidatorUtils; +use identity_iota::did::CoreDID; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JwtPresentationValidatorHybrid, inspectable)] +pub struct WasmJwtPresentationValidatorHybrid(JwtPresentationValidatorHybrid); + +#[wasm_bindgen(js_class = JwtPresentationValidatorHybrid)] +impl WasmJwtPresentationValidatorHybrid { + /// Creates a new {@link JwtPresentationValidatorHybrid}. If a `signatureVerifier` is provided it will be used when + /// verifying decoded JWS signatures, otherwise a default verifier capable of handling the `EdDSA`, `ES256`, `ES256K` + /// algorithms will be used. + #[wasm_bindgen(constructor)] + #[allow(non_snake_case)] + pub fn new(traditionalSignatureVerifier: Option, pqSignatureVerifier: Option) -> WasmJwtPresentationValidatorHybrid { + let traditional_signature_verifier = WasmJwsVerifier::new(traditionalSignatureVerifier); + let pq_signature_verifier = WasmJwsVerifier::new(pqSignatureVerifier); + WasmJwtPresentationValidatorHybrid(JwtPresentationValidatorHybrid::with_signature_verifiers(traditional_signature_verifier, pq_signature_verifier)) + } + + /// Validates a {@link Presentation} encoded as a {@link Jwt}. + /// + /// The following properties are validated according to `options`: + /// - the JWT can be decoded into a semantically valid presentation. + /// - the expiration and issuance date contained in the JWT claims. + /// - the holder's signature. + /// + /// Validation is done with respect to the properties set in `options`. + /// + /// # Warning + /// + /// * This method does NOT validate the constituent credentials and therefore also not the relationship between the + /// credentials' subjects and the presentation holder. This can be done with {@link JwtCredentialValidationOptions}. + /// * The lack of an error returned from this method is in of itself not enough to conclude that the presentation can + /// be trusted. This section contains more information on additional checks that should be carried out before and + /// after calling this method. + /// + /// ## The state of the supplied DID Documents. + /// + /// The caller must ensure that the DID Documents in `holder` are up-to-date. + /// + /// # Errors + /// + /// An error is returned whenever a validated condition is not satisfied or when decoding fails. + #[wasm_bindgen] + #[allow(non_snake_case)] + pub fn validate( + &self, + presentationJwt: &WasmJwt, + holder: &IToCoreDocument, + validation_options: &WasmJwtPresentationValidationOptions, + ) -> Result { + let holder_lock = ImportedDocumentLock::from(holder); + let holder_guard = holder_lock.try_read()?; + + self + .0 + .validate(&presentationJwt.0, &holder_guard, &validation_options.0) + .map(WasmDecodedJwtPresentation::from) + .wasm_result() + } + + /// Validates the semantic structure of the {@link Presentation}. + #[wasm_bindgen(js_name = checkStructure)] + pub fn check_structure(presentation: &WasmPresentation) -> Result<()> { + JwtPresentationValidatorUtils::check_structure(&presentation.0).wasm_result()?; + Ok(()) + } + + /// Attempt to extract the holder of the presentation. + /// + /// # Errors: + /// * If deserialization/decoding of the presentation fails. + /// * If the holder can't be parsed as DIDs. + #[wasm_bindgen(js_name = extractHolder)] + pub fn extract_holder(presentation: &WasmJwt) -> Result { + let holder = JwtPresentationValidatorUtils::extract_holder::(&presentation.0).wasm_result()?; + Ok(WasmCoreDID(holder)) + } +} diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs b/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs index 12c556852e..99d774244b 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs @@ -1,10 +1,15 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ mod decoded_jwt_presentation; mod jwt_presentation_validator; mod options; +mod jwt_presentation_validator_hybrid; pub use self::decoded_jwt_presentation::*; pub use self::jwt_presentation_validator::*; pub use self::options::*; +pub use self::jwt_presentation_validator_hybrid::*; \ No newline at end of file diff --git a/bindings/wasm/src/did/did_compositejwk.rs b/bindings/wasm/src/did/did_compositejwk.rs new file mode 100644 index 0000000000..2e71a1cccf --- /dev/null +++ b/bindings/wasm/src/did/did_compositejwk.rs @@ -0,0 +1,105 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::did::DIDCompositeJwk; +use identity_iota::did::DID as _; +use wasm_bindgen::prelude::*; + +use super::wasm_core_did::get_core_did_clone; +use super::IToCoreDID; +use super::WasmCoreDID; +use crate::error::Result; +use crate::error::WasmResult; +use crate::jose::WasmCompositeJwk; + +/// `did:compositejwk` DID. +#[wasm_bindgen(js_name = DIDCompositeJwk)] +pub struct WasmDIDCompositeJwk(pub(crate) DIDCompositeJwk); + +#[wasm_bindgen(js_class = DIDCompositeJwk)] +impl WasmDIDCompositeJwk { + #[wasm_bindgen(constructor)] + /// Creates a new {@link DIDCompositeJwk} from a {@link CoreDID}. + /// + /// ### Errors + /// Throws an error if the given did is not a valid `did:compositejwk` DID. + pub fn new(did: IToCoreDID) -> Result { + let did = get_core_did_clone(&did).0; + DIDCompositeJwk::try_from(did).wasm_result().map(Self) + } + /// Parses a {@link DIDCompositeJwk} from the given `input`. + /// + /// ### Errors + /// + /// Throws an error if the input is not a valid {@link DIDCompositeJwk}. + #[wasm_bindgen] + pub fn parse(input: &str) -> Result { + DIDCompositeJwk::parse(input).wasm_result().map(Self) + } + + /// Returns the JSON WEB KEY (JWK) encoded inside this `did:jwk`. + #[wasm_bindgen] + pub fn composite_jwk(&self) -> WasmCompositeJwk { + self.0.composite_jwk().into() + } + + // =========================================================================== + // DID trait + // =========================================================================== + + /// Returns the {@link CoreDID} scheme. + /// + /// E.g. + /// - `"did:example:12345678" -> "did"` + /// - `"did:iota:smr:12345678" -> "did"` + #[wasm_bindgen] + pub fn scheme(&self) -> String { + self.0.scheme().to_owned() + } + + /// Returns the {@link CoreDID} authority: the method name and method-id. + /// + /// E.g. + /// - `"did:example:12345678" -> "example:12345678"` + /// - `"did:iota:smr:12345678" -> "iota:smr:12345678"` + #[wasm_bindgen] + pub fn authority(&self) -> String { + self.0.authority().to_owned() + } + + /// Returns the {@link CoreDID} method name. + /// + /// E.g. + /// - `"did:example:12345678" -> "example"` + /// - `"did:iota:smr:12345678" -> "iota"` + #[wasm_bindgen] + pub fn method(&self) -> String { + self.0.method().to_owned() + } + + /// Returns the {@link CoreDID} method-specific ID. + /// + /// E.g. + /// - `"did:example:12345678" -> "12345678"` + /// - `"did:iota:smr:12345678" -> "smr:12345678"` + #[wasm_bindgen(js_name = methodId)] + pub fn method_id(&self) -> String { + self.0.method_id().to_owned() + } + + /// Returns the {@link CoreDID} as a string. + #[allow(clippy::inherent_to_string)] + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + self.0.to_string() + } + + // Only intended to be called internally. + #[wasm_bindgen(js_name = toCoreDid, skip_typescript)] + pub fn to_core_did(&self) -> WasmCoreDID { + WasmCoreDID(self.0.clone().into()) + } +} + +impl_wasm_json!(WasmDIDCompositeJwk, DIDCompositeJwk); +impl_wasm_clone!(WasmDIDCompositeJwk, DIDCompositeJwk); diff --git a/bindings/wasm/src/did/mod.rs b/bindings/wasm/src/did/mod.rs index ae2e89bc0c..3209672f3c 100644 --- a/bindings/wasm/src/did/mod.rs +++ b/bindings/wasm/src/did/mod.rs @@ -1,12 +1,16 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 - +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ mod did_jwk; +mod did_compositejwk; mod jws_verification_options; mod service; mod wasm_core_did; mod wasm_core_document; mod wasm_did_url; +mod wasm_did_jwk_document_ext; pub use self::service::IService; pub use self::service::UServiceEndpoint; @@ -21,5 +25,5 @@ pub use self::wasm_core_document::PromiseJwt; pub use self::wasm_core_document::WasmCoreDocument; pub use self::wasm_did_url::WasmDIDUrl; pub use did_jwk::*; - +pub use did_compositejwk::*; pub use self::jws_verification_options::*; diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index fd66c4e7ca..1b9158f83c 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -1,6 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use std::rc::Rc; use super::WasmCoreDID; @@ -25,6 +29,7 @@ use crate::credential::WasmPresentation; use crate::did::service::WasmService; use crate::did::wasm_did_url::WasmDIDUrl; use crate::did::WasmDIDJwk; +use crate::did::WasmDIDCompositeJwk; use crate::error::Result; use crate::error::WasmResult; use crate::jose::WasmDecodedJws; @@ -773,6 +778,12 @@ impl WasmCoreDocument { pub fn expand_did_jwk(did: WasmDIDJwk) -> Result { CoreDocument::expand_did_jwk(did.0).wasm_result().map(Self::from) } + + /// Creates a {@link CoreDocument} from the given {@link DIDCompositeJwk}. + #[wasm_bindgen(js_name = expandDIDCompositeJwk)] + pub fn expand_did_compositejwk(did: WasmDIDCompositeJwk) -> Result { + CoreDocument::expand_did_compositejwk(did.0).wasm_result().map(Self::from) + } } #[wasm_bindgen] diff --git a/bindings/wasm/src/did/wasm_did_jwk_document_ext.rs b/bindings/wasm/src/did/wasm_did_jwk_document_ext.rs new file mode 100644 index 0000000000..bd1f096130 --- /dev/null +++ b/bindings/wasm/src/did/wasm_did_jwk_document_ext.rs @@ -0,0 +1,350 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use std::rc::Rc; +use crate::error::Result; +use crate::error::WasmResult; +use crate::storage::WasmStorageInner; +use crate::jose::WasmJwsAlgorithm; +use crate::jose::WasmCompositeAlgId; +use crate::storage::WasmStorage; +use super::CoreDocumentLock; +use super::WasmCoreDocument; +use crate::credential::WasmCredential; +use crate::common::RecordStringAny; +use crate::credential::WasmJpt; +use crate::credential::PromiseJpt; +use crate::credential::WasmJwpPresentationOptions; +use crate::jpt::WasmSelectiveDisclosurePresentation; +use crate::credential::WasmJwt; +use crate::jpt::WasmProofAlgorithm; +use crate::credential::UnknownCredential; +use crate::did::PromiseJws; +use crate::storage::WasmJwsSignatureOptions; +use crate::credential::WasmJws; +use crate::credential::WasmPresentation; +use crate::storage::WasmJwtPresentationOptions; +use crate::did::PromiseJwt; +use identity_iota::credential::Presentation; +use identity_iota::credential::JwtPresentationOptions; +use identity_iota::storage::JwsSignatureOptions; +use identity_iota::storage::JwpDocumentExt; +use identity_iota::credential::Credential; +use identity_iota::core::Object; +use identity_iota::storage::storage::JwsDocumentExtPQC; +use identity_iota::storage::storage::JwkDocumentExtHybrid; +use identity_iota::storage::DidJwkDocumentExt; +use identity_iota::document::CoreDocument; +use identity_iota::storage::key_storage::KeyType; +use identity_iota::verification::jws::JwsAlgorithm; +use identity_iota::verification::jwk::CompositeAlgId; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use js_sys::Promise; + +#[wasm_bindgen(js_class = CoreDocument)] +impl WasmCoreDocument { + + /// Creates a new DID Document with the given `key_type` and `alg` with the JWK did method. + #[wasm_bindgen(js_name = newDidJwk)] + pub async fn _new_did_jwk( + storage: &WasmStorage, + key_type: String, + alg: WasmJwsAlgorithm, + ) -> Result{ + let storage_clone: Rc = storage.0.clone(); + let alg: JwsAlgorithm = alg.into_serde().wasm_result()?; + CoreDocument::new_did_jwk( + &storage_clone, + KeyType::from(key_type), + alg + ).await + .map(|doc| WasmCoreDocument(Rc::new(CoreDocumentLock::new(doc.0)))) + .wasm_result() + } + + /// Creates a new PQ DID Document with the given `key_type` and `alg` with the JWK did method. + #[wasm_bindgen(js_name = newDidJwkPq)] + pub async fn _new_did_jwk_pqc( + storage: &WasmStorage, + key_type: String, + alg: WasmJwsAlgorithm, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let alg: JwsAlgorithm = alg.into_serde().wasm_result()?; + CoreDocument::new_did_jwk_pqc( + &storage_clone, + KeyType::from(key_type), + alg + ).await + .map(|doc| WasmCoreDocument(Rc::new(CoreDocumentLock::new(doc.0)))) + .wasm_result() + } + + /// Creates a new hybrid DID Document with the given `key_type` and `alg`with the compositeJWK did method. + #[wasm_bindgen(js_name = newDidCompositeJwk)] + pub async fn _new_did_compositejwk( + storage: &WasmStorage, + alg: WasmCompositeAlgId + ) -> Result{ + let storage_clone: Rc = storage.0.clone(); + let alg: CompositeAlgId = alg.into_serde().wasm_result()?; + CoreDocument::new_did_compositejwk( + &storage_clone, + alg + ).await + .map(|doc| WasmCoreDocument(Rc::new(CoreDocumentLock::new(doc.0)))) + .wasm_result() + } + + /// Creates a new zk DID Document with the given `key_type` and `alg` with the JWK did method. + #[wasm_bindgen(js_name = newDidJwkZk)] + pub async fn _new_did_jwk_zk( + storage: &WasmStorage, + alg: WasmProofAlgorithm, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let alg: ProofAlgorithm = alg.into(); + CoreDocument::new_did_jwk_zk( + &storage_clone, + KeyType::from_static_str("BLS12381"), + alg + ).await + .map(|doc| WasmCoreDocument(Rc::new(CoreDocumentLock::new(doc.0)))) + .wasm_result() + } + + #[wasm_bindgen(js_name = fragmentJwk)] + pub fn _fragment(self) -> String { + "0".to_string() + } + /// Produces a PQ JWS, from a document with a PQ method, where the payload is produced from the given `fragment` and `payload`. + #[wasm_bindgen(js_name = createPqJws)] + pub fn _create_pq_jws( + &self, + storage: &WasmStorage, + fragment: String, + payload: String, + options: &WasmJwsSignatureOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_jws_pqc(&storage_clone, &fragment, payload.as_bytes(), &options_clone) + .await + .wasm_result() + .map(WasmJws::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Produces an hybrid JWS, from a document with an hybrid method, where the payload is produced from the given `fragment` and `payload`. + #[wasm_bindgen(js_name = createHybridJws)] + pub fn create_hybrid_jws( + &self, + storage: &WasmStorage, + fragment: String, + payload: String, + options: &WasmJwsSignatureOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_jws(&storage_clone, &fragment, payload.as_bytes(), &options_clone) + .await + .wasm_result() + .map(WasmJws::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Produces a PQ JWT, from a document with a PQ method, where the payload is produced from the given `credential` + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + /// + /// The `custom_claims` can be used to set additional claims on the resulting JWT. + #[wasm_bindgen(js_name = createCredentialJwtPqc)] + pub fn _create_credential_jwt_pqc( + &self, + storage: &WasmStorage, + fragment: String, + credential: &WasmCredential, + options: &WasmJwsSignatureOptions, + custom_claims: Option, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let credential_clone: Credential = credential.0.clone(); + let custom: Option = custom_claims + .map(|claims| claims.into_serde().wasm_result()) + .transpose()?; + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_credential_jwt_pqc(&credential_clone, &storage_clone, &fragment, &options_clone, custom) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Produces an hybrid JWT, from a document with an hybrid method, where the payload is produced from the given `credential` + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + /// + /// The `custom_claims` can be used to set additional claims on the resulting JWT. + #[wasm_bindgen(js_name = createCredentialJwtHybrid)] + pub fn _create_credential_jwt_hybrid( + &self, + storage: &WasmStorage, + fragment: String, + credential: &WasmCredential, + options: &WasmJwsSignatureOptions, + custom_claims: Option, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let credential_clone: Credential = credential.0.clone(); + let custom: Option = custom_claims + .map(|claims| claims.into_serde().wasm_result()) + .transpose()?; + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_credential_jwt_hybrid(&credential_clone, &storage_clone, &fragment, &options_clone, custom) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Produces a PQ JWT, from a document with a PQ method, where the payload is produced from the given presentation. + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + #[wasm_bindgen(js_name = createPresentationJwtPqc)] + pub fn _create_presentation_jwt_pqc( + &self, + storage: &WasmStorage, + fragment: String, + presentation: &WasmPresentation, + signature_options: &WasmJwsSignatureOptions, + presentation_options: &WasmJwtPresentationOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = signature_options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let presentation_clone: Presentation = presentation.0.clone(); + let presentation_options_clone: JwtPresentationOptions = presentation_options.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_presentation_jwt_pqc( + &presentation_clone, + &storage_clone, + &fragment, + &options_clone, + &presentation_options_clone, + ) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Produces an hybrid JWT, from a document with an hybrid method, where the payload is produced from the given presentation. + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + #[wasm_bindgen(js_name = createPresentationJwtHybrid)] + pub fn _create_presentation_jwt_hybrid( + &self, + storage: &WasmStorage, + fragment: String, + presentation: &WasmPresentation, + signature_options: &WasmJwsSignatureOptions, + presentation_options: &WasmJwtPresentationOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = signature_options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let presentation_clone: Presentation = presentation.0.clone(); + let presentation_options_clone: JwtPresentationOptions = presentation_options.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_presentation_jwt_hybrid( + &presentation_clone, + &storage_clone, + &fragment, + &options_clone, + &presentation_options_clone, + ) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + 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()) + } + + +} + diff --git a/bindings/wasm/src/iota/iota_document_ext.rs b/bindings/wasm/src/iota/iota_document_ext.rs new file mode 100644 index 0000000000..8816830ff2 --- /dev/null +++ b/bindings/wasm/src/iota/iota_document_ext.rs @@ -0,0 +1,318 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use std::rc::Rc; + +use identity_iota::core::Object; + +use identity_iota::credential::Credential; +use identity_iota::credential::JwtPresentationOptions; +use identity_iota::credential::Presentation; +use identity_iota::storage::key_storage::KeyType; +use identity_iota::storage::storage::JwsSignatureOptions; +use identity_iota::verification::jose::jws::JwsAlgorithm; +use identity_iota::verification::MethodScope; +use js_sys::Promise; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::future_to_promise; +use crate::common::PromiseString; +use crate::common::RecordStringAny; +use crate::credential::UnknownCredential; +use crate::credential::WasmCredential; +use crate::credential::WasmJws; +use crate::credential::WasmJwt; +use crate::credential::WasmPresentation; +use crate::iota::IotaDocumentLock; +use crate::did::PromiseJws; +use crate::did::PromiseJwt; +use crate::error::Result; +use crate::error::WasmResult; +use crate::jose::WasmJwsAlgorithm; +use crate::storage::WasmJwsSignatureOptions; +use crate::storage::WasmJwtPresentationOptions; +use crate::storage::WasmStorage; +use crate::storage::WasmStorageInner; +use crate::verification::WasmMethodScope; +use crate::jose::WasmCompositeAlgId; +use crate::iota::WasmIotaDocument; +use identity_iota::storage::JwsDocumentExtPQC; +use identity_iota::storage::JwkDocumentExtHybrid; +use identity_iota::verification::jwk::CompositeAlgId; + +#[wasm_bindgen(js_class = IotaDocument)] +impl WasmIotaDocument { + + /// Generate new PQ key material in the given `storage` and insert a new verification method with the corresponding + /// public key material into the DID document. + /// + /// - If no fragment is given the `kid` of the generated JWK is used, if it is set, otherwise an error is returned. + /// - The `keyType` must be compatible with the given `storage`. `Storage`s are expected to export key type constants + /// for that use case. + /// + /// The fragment of the generated method is returned. + #[wasm_bindgen(js_name = generateMethodPQC)] + #[allow(non_snake_case)] + pub fn generate_method_pqc( + &self, + storage: &WasmStorage, + keyType: String, + alg: WasmJwsAlgorithm, + fragment: Option, + scope: WasmMethodScope, + ) -> Result { + let alg: JwsAlgorithm = alg.into_serde().wasm_result()?; + let document_lock_clone: Rc = self.0.clone(); + let storage_clone: Rc = storage.0.clone(); + let scope: MethodScope = scope.0; + + let promise: Promise = future_to_promise(async move { + let method_fragment: String = document_lock_clone + .write() + .await + .generate_method_pqc(&storage_clone, KeyType::from(keyType), alg, fragment.as_deref(), scope) + .await + .wasm_result()?; + Ok(JsValue::from(method_fragment)) + }); + Ok(promise.unchecked_into()) + } + + /// Generate new hybrid key material in the given `storage` and insert a new verification method with the corresponding + /// public key material into the DID document. + /// + /// - If no fragment is given the `kid` of the generated JWK is used, if it is set, otherwise an error is returned. + /// - The `keyType` must be compatible with the given `storage`. `Storage`s are expected to export key type constants + /// for that use case. + /// + /// The fragment of the generated method is returned. + #[wasm_bindgen(js_name = generateMethodHybrid)] + #[allow(non_snake_case)] + pub fn generate_method_hybrid( + &self, + storage: &WasmStorage, + alg: WasmCompositeAlgId, + fragment: Option, + scope: WasmMethodScope, + ) -> Result { + let alg: CompositeAlgId = alg.into_serde().wasm_result()?; + let document_lock_clone: Rc = self.0.clone(); + let storage_clone: Rc = storage.0.clone(); + let scope: MethodScope = scope.0; + + let promise: Promise = future_to_promise(async move { + let method_fragment: String = document_lock_clone + .write() + .await + .generate_method_hybrid(&storage_clone, alg, fragment.as_deref(), scope) + .await + .wasm_result()?; + Ok(JsValue::from(method_fragment)) + }); + Ok(promise.unchecked_into()) + } + + /// Produces a PQ JWS, from a document with a PQ method, where the payload is produced from the given `fragment` and `payload`. + #[wasm_bindgen(js_name = createPqJws)] + pub fn _create_pq_jws( + &self, + storage: &WasmStorage, + fragment: String, + payload: String, + options: &WasmJwsSignatureOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_jws_pqc(&storage_clone, &fragment, payload.as_bytes(), &options_clone) + .await + .wasm_result() + .map(WasmJws::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Produces an hybrid JWS, from a document with an hybrid method, where the payload is produced from the given `fragment` and `payload`. + #[wasm_bindgen(js_name = createHybridJws)] + pub fn create_hybrid_jws( + &self, + storage: &WasmStorage, + fragment: String, + payload: String, + options: &WasmJwsSignatureOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_jws(&storage_clone, &fragment, payload.as_bytes(), &options_clone) + .await + .wasm_result() + .map(WasmJws::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Produces a PQ JWT, from a document with a PQ method, where the payload is produced from the given `credential` + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + /// + /// The `custom_claims` can be used to set additional claims on the resulting JWT. + #[wasm_bindgen(js_name = createCredentialJwtPqc)] + pub fn _create_credential_jwt_pqc( + &self, + storage: &WasmStorage, + fragment: String, + credential: &WasmCredential, + options: &WasmJwsSignatureOptions, + custom_claims: Option, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let credential_clone: Credential = credential.0.clone(); + let custom: Option = custom_claims + .map(|claims| claims.into_serde().wasm_result()) + .transpose()?; + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_credential_jwt_pqc(&credential_clone, &storage_clone, &fragment, &options_clone, custom) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Produces an hybrid JWT, from a document with an hybrid method, where the payload is produced from the given `credential` + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + /// + /// The `custom_claims` can be used to set additional claims on the resulting JWT. + #[wasm_bindgen(js_name = createCredentialJwtHybrid)] + pub fn _create_credential_jwt_hybrid( + &self, + storage: &WasmStorage, + fragment: String, + credential: &WasmCredential, + options: &WasmJwsSignatureOptions, + custom_claims: Option, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let credential_clone: Credential = credential.0.clone(); + let custom: Option = custom_claims + .map(|claims| claims.into_serde().wasm_result()) + .transpose()?; + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_credential_jwt_hybrid(&credential_clone, &storage_clone, &fragment, &options_clone, custom) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Produces a PQ JWT, from a document with a PQ method, where the payload is produced from the given presentation. + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + #[wasm_bindgen(js_name = createPresentationJwtPqc)] + pub fn _create_presentation_jwt_pqc( + &self, + storage: &WasmStorage, + fragment: String, + presentation: &WasmPresentation, + signature_options: &WasmJwsSignatureOptions, + presentation_options: &WasmJwtPresentationOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = signature_options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let presentation_clone: Presentation = presentation.0.clone(); + let presentation_options_clone: JwtPresentationOptions = presentation_options.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_presentation_jwt_pqc( + &presentation_clone, + &storage_clone, + &fragment, + &options_clone, + &presentation_options_clone, + ) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Produces an hybrid JWT, from a document with an hybrid method, where the payload is produced from the given presentation. + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + #[wasm_bindgen(js_name = createPresentationJwtHybrid)] + pub fn _create_presentation_jwt_hybrid( + &self, + storage: &WasmStorage, + fragment: String, + presentation: &WasmPresentation, + signature_options: &WasmJwsSignatureOptions, + presentation_options: &WasmJwtPresentationOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = signature_options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let presentation_clone: Presentation = presentation.0.clone(); + let presentation_options_clone: JwtPresentationOptions = presentation_options.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_presentation_jwt_hybrid( + &presentation_clone, + &storage_clone, + &fragment, + &options_clone, + &presentation_options_clone, + ) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + +} + diff --git a/bindings/wasm/src/iota/mod.rs b/bindings/wasm/src/iota/mod.rs index fa68380d80..a6f50f0027 100644 --- a/bindings/wasm/src/iota/mod.rs +++ b/bindings/wasm/src/iota/mod.rs @@ -1,5 +1,8 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ pub(crate) use identity_client::WasmIotaIdentityClient; pub use identity_client_ext::PromiseIotaDocument; @@ -13,5 +16,6 @@ mod identity_client; mod identity_client_ext; mod iota_did; mod iota_document; +mod iota_document_ext; mod iota_document_metadata; mod iota_metadata_encoding; diff --git a/bindings/wasm/src/jose/compositejwk.rs b/bindings/wasm/src/jose/compositejwk.rs new file mode 100644 index 0000000000..8f307ad41b --- /dev/null +++ b/bindings/wasm/src/jose/compositejwk.rs @@ -0,0 +1,61 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::verification::jose::jwk::CompositeJwk; + +use wasm_bindgen::prelude::*; +use crate::jose::WasmCompositeAlgId; +use crate::jose::WasmJwk; +use crate::jose::IJwkParams; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[wasm_bindgen(js_name = CompositeJwk, inspectable)] +pub struct WasmCompositeJwk(pub(crate) CompositeJwk); + +#[wasm_bindgen(js_class = CompositeJwk)] +impl WasmCompositeJwk { + #[wasm_bindgen(constructor)] + pub fn new(jwk: IJwkParams) -> Self { + let jwk: CompositeJwk = jwk.into_serde().unwrap(); + Self(jwk) + } + + #[wasm_bindgen] + /// Get the `algId` value. + pub fn alg_id(&self) -> WasmCompositeAlgId { + //let alg: CompositeAlgId = alg.into_serde().wasm_result()?; + //JsValue::from(self.0.alg_id()).unchecked_into() + JsValue::from_serde(&self.0.alg_id()).unwrap() + .unchecked_into() + + + } + + /// Get the post-quantum public key in Jwk format. + #[wasm_bindgen] + pub fn pq_public_key(&self) -> WasmJwk { + //JsValue::from(self.0.pq_public_key()).unchecked_into() + todo!() + } + + #[wasm_bindgen] + /// Get the traditional public key in Jwk format. + pub fn traditional_public_key(&self) -> WasmJwk { + //self.0.traditional_public_key().map(JsValue::from) + todo!() + } + +} + +impl From for CompositeJwk { + fn from(value: WasmCompositeJwk) -> Self { + value.0 + } +} + + +impl From for WasmCompositeJwk { + fn from(value: CompositeJwk) -> Self { + WasmCompositeJwk(value) + } +} \ No newline at end of file diff --git a/bindings/wasm/src/jose/jwk.rs b/bindings/wasm/src/jose/jwk.rs index cf20245302..6b0b6a7b37 100644 --- a/bindings/wasm/src/jose/jwk.rs +++ b/bindings/wasm/src/jose/jwk.rs @@ -1,6 +1,8 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 - +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ use identity_iota::verification::jose::jwk::Jwk; use identity_iota::verification::jose::jwk::JwkOperation; use identity_iota::verification::jose::jwk::JwkParams; @@ -15,6 +17,7 @@ use crate::jose::WasmJwkParamsEc; use crate::jose::WasmJwkParamsOct; use crate::jose::WasmJwkParamsOkp; use crate::jose::WasmJwkParamsRsa; +use crate::jose::WasmJwkParamsAkp; use crate::jose::WasmJwkType; use crate::jose::WasmJwkUse; use crate::jose::WasmJwsAlgorithm; @@ -165,6 +168,17 @@ impl WasmJwk { } } + /// If this JWK is of kty AKP, returns those parameters. + #[wasm_bindgen(js_name = paramsAkp)] + pub fn params_akp(&self) -> crate::error::Result> { + if let JwkParams::Akp(params_akp) = self.0.params() { + // WARNING: this does not validate the return type. Check carefully. + Ok(Some(JsValue::from_serde(params_akp).wasm_result()?.unchecked_into())) + } else { + Ok(None) + } + } + /// Returns a clone of the {@link Jwk} with _all_ private key components unset. /// Nothing is returned when `kty = oct` as this key type is not considered public by this library. #[wasm_bindgen(js_name = toPublic)] @@ -202,7 +216,7 @@ impl_wasm_clone!(WasmJwk, Jwk); #[wasm_bindgen(typescript_custom_section)] const I_JWK: &'static str = r#" -type IJwkParams = IJwkEc | IJwkRsa | IJwkOkp | IJwkOct +type IJwkParams = IJwkEc | IJwkRsa | IJwkOkp | IJwkOct | IJwkAkp /** A JSON Web Key with EC params. */ export interface IJwkEc extends IJwk, JwkParamsEc { kty: JwkType.Ec @@ -219,6 +233,10 @@ export interface IJwkOkp extends IJwk, JwkParamsOkp { export interface IJwkOct extends IJwk, JwkParamsOct { kty: JwkType.Oct } +/** A JSON Web Key with AKP params. */ +export interface IJwkAkp extends IJwk, JwkParamsAkp { + kty: JwkType.Akp +} "#; #[wasm_bindgen(typescript_custom_section)] @@ -402,3 +420,14 @@ interface JwkParamsOct { * [More Info](https://tools.ietf.org/html/rfc7518#section-6.4.1) */ k: string }"#; + + +#[wasm_bindgen(typescript_custom_section)] +const IJWK_PARAMS_AKP: &str = r#" +/** Parameters for Algorithm Key Pair (AKP). + * + * [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-dilithium-06#name-algorithm-key-pair-type) */ +interface JwkParamsAkp { + pub: string, + priv?: string +}"#; \ No newline at end of file diff --git a/bindings/wasm/src/jose/mod.rs b/bindings/wasm/src/jose/mod.rs index 9d6075dbb3..24a7024abd 100644 --- a/bindings/wasm/src/jose/mod.rs +++ b/bindings/wasm/src/jose/mod.rs @@ -1,14 +1,20 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + mod decoded_jws; mod jwk; +mod compositejwk; mod jws_header; mod jwu; mod types; pub use decoded_jws::*; pub use jwk::*; +pub use compositejwk::*; pub use jws_header::*; pub use jwu::*; pub use types::*; diff --git a/bindings/wasm/src/jose/types.rs b/bindings/wasm/src/jose/types.rs index b069dd5bb3..c48436a310 100644 --- a/bindings/wasm/src/jose/types.rs +++ b/bindings/wasm/src/jose/types.rs @@ -1,9 +1,13 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ use identity_iota::verification::jws::JwsAlgorithm; use js_sys::JsString; use std::str::FromStr; use wasm_bindgen::prelude::*; +use identity_iota::verification::jwk::CompositeAlgId; #[wasm_bindgen] extern "C" { @@ -25,6 +29,10 @@ extern "C" { pub type WasmJwkParamsRsa; #[wasm_bindgen(typescript_type = "JwkParamsOct")] pub type WasmJwkParamsOct; + #[wasm_bindgen(typescript_type = "JwkParamsAkp")] + pub type WasmJwkParamsAkp; + #[wasm_bindgen(typescript_type = "CompositeAlgId")] + pub type WasmCompositeAlgId; } impl TryFrom for JwsAlgorithm { @@ -38,3 +46,15 @@ impl TryFrom for JwsAlgorithm { } } } + +impl TryFrom for CompositeAlgId { + type Error = JsValue; + fn try_from(value: WasmCompositeAlgId) -> Result { + if let Ok(js_string) = value.dyn_into::() { + CompositeAlgId::from_str(String::from(js_string).as_ref()) + .map_err(|err| js_sys::Error::new(&err.to_string()).into()) + } else { + Err(js_sys::Error::new("invalid CompositeAlgId").into()) + } + } +} diff --git a/bindings/wasm/src/jpt/encoding.rs b/bindings/wasm/src/jpt/encoding.rs index e36a5307a5..de77828e19 100644 --- a/bindings/wasm/src/jpt/encoding.rs +++ b/bindings/wasm/src/jpt/encoding.rs @@ -1,6 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use jsonprooftoken::encoding::SerializationType; use wasm_bindgen::prelude::*; @@ -8,6 +12,7 @@ use wasm_bindgen::prelude::*; pub enum WasmSerializationType { COMPACT = 0, JSON = 1, + CBOR = 2, } impl From for SerializationType { @@ -15,6 +20,7 @@ impl From for SerializationType { match value { WasmSerializationType::COMPACT => SerializationType::COMPACT, WasmSerializationType::JSON => SerializationType::JSON, + WasmSerializationType::CBOR => SerializationType::CBOR, } } } @@ -24,6 +30,7 @@ impl From for WasmSerializationType { match value { SerializationType::COMPACT => WasmSerializationType::COMPACT, SerializationType::JSON => WasmSerializationType::JSON, + SerializationType::CBOR => WasmSerializationType::CBOR, } } } diff --git a/bindings/wasm/src/jpt/presentation_protected_header.rs b/bindings/wasm/src/jpt/presentation_protected_header.rs index 398870da4c..4a1fadea26 100644 --- a/bindings/wasm/src/jpt/presentation_protected_header.rs +++ b/bindings/wasm/src/jpt/presentation_protected_header.rs @@ -1,6 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use jsonprooftoken::jpa::algs::PresentationProofAlgorithm; use jsonprooftoken::jwp::header::PresentationProtectedHeader; use wasm_bindgen::prelude::*; @@ -9,9 +13,11 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen(js_name = PresentationProofAlgorithm)] #[allow(non_camel_case_types)] pub enum WasmPresentationProofAlgorithm { - BLS12381_SHA256_PROOF, - BLS12381_SHAKE256_PROOF, + BBS, + BBS_SHAKE256, SU_ES256, + SU_ES384, + SU_ES512, MAC_H256, MAC_H384, MAC_H512, @@ -23,9 +29,11 @@ pub enum WasmPresentationProofAlgorithm { 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::BBS => PresentationProofAlgorithm::BBS, + WasmPresentationProofAlgorithm::BBS_SHAKE256 => PresentationProofAlgorithm::BBS_SHAKE256, WasmPresentationProofAlgorithm::SU_ES256 => PresentationProofAlgorithm::SU_ES256, + WasmPresentationProofAlgorithm::SU_ES384 => PresentationProofAlgorithm::SU_ES384, + WasmPresentationProofAlgorithm::SU_ES512 => PresentationProofAlgorithm::SU_ES512, WasmPresentationProofAlgorithm::MAC_H256 => PresentationProofAlgorithm::MAC_H256, WasmPresentationProofAlgorithm::MAC_H384 => PresentationProofAlgorithm::MAC_H384, WasmPresentationProofAlgorithm::MAC_H512 => PresentationProofAlgorithm::MAC_H512, @@ -39,9 +47,11 @@ impl From for PresentationProofAlgorithm { 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::BBS => WasmPresentationProofAlgorithm::BBS, + PresentationProofAlgorithm::BBS_SHAKE256 => WasmPresentationProofAlgorithm::BBS_SHAKE256, PresentationProofAlgorithm::SU_ES256 => WasmPresentationProofAlgorithm::SU_ES256, + PresentationProofAlgorithm::SU_ES384 => WasmPresentationProofAlgorithm::SU_ES384, + PresentationProofAlgorithm::SU_ES512 => WasmPresentationProofAlgorithm::SU_ES512, PresentationProofAlgorithm::MAC_H256 => WasmPresentationProofAlgorithm::MAC_H256, PresentationProofAlgorithm::MAC_H384 => WasmPresentationProofAlgorithm::MAC_H384, PresentationProofAlgorithm::MAC_H512 => WasmPresentationProofAlgorithm::MAC_H512, diff --git a/bindings/wasm/src/jpt/proof_algorithm.rs b/bindings/wasm/src/jpt/proof_algorithm.rs index 0f7f6986f1..f5b3e7b44b 100644 --- a/bindings/wasm/src/jpt/proof_algorithm.rs +++ b/bindings/wasm/src/jpt/proof_algorithm.rs @@ -1,6 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use jsonprooftoken::jpa::algs::ProofAlgorithm; use wasm_bindgen::prelude::*; @@ -8,9 +12,11 @@ use wasm_bindgen::prelude::*; #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[wasm_bindgen(js_name = ProofAlgorithm)] pub enum WasmProofAlgorithm { - BLS12381_SHA256, - BLS12381_SHAKE256, + BBS, + BBS_SHAKE256, SU_ES256, + SU_ES384, + SU_ES512, MAC_H256, MAC_H384, MAC_H512, @@ -22,9 +28,11 @@ pub enum WasmProofAlgorithm { impl From for WasmProofAlgorithm { fn from(value: ProofAlgorithm) -> Self { match value { - ProofAlgorithm::BLS12381_SHA256 => WasmProofAlgorithm::BLS12381_SHA256, - ProofAlgorithm::BLS12381_SHAKE256 => WasmProofAlgorithm::BLS12381_SHAKE256, + ProofAlgorithm::BBS => WasmProofAlgorithm::BBS, + ProofAlgorithm::BBS_SHAKE256 => WasmProofAlgorithm::BBS_SHAKE256, ProofAlgorithm::SU_ES256 => WasmProofAlgorithm::SU_ES256, + ProofAlgorithm::SU_ES384 => WasmProofAlgorithm::SU_ES384, + ProofAlgorithm::SU_ES512 => WasmProofAlgorithm::SU_ES512, ProofAlgorithm::MAC_H256 => WasmProofAlgorithm::MAC_H256, ProofAlgorithm::MAC_H384 => WasmProofAlgorithm::MAC_H384, ProofAlgorithm::MAC_H512 => WasmProofAlgorithm::MAC_H512, @@ -38,9 +46,11 @@ impl From for WasmProofAlgorithm { impl From for ProofAlgorithm { fn from(value: WasmProofAlgorithm) -> Self { match value { - WasmProofAlgorithm::BLS12381_SHA256 => ProofAlgorithm::BLS12381_SHA256, - WasmProofAlgorithm::BLS12381_SHAKE256 => ProofAlgorithm::BLS12381_SHAKE256, + WasmProofAlgorithm::BBS => ProofAlgorithm::BBS, + WasmProofAlgorithm::BBS_SHAKE256 => ProofAlgorithm::BBS_SHAKE256, WasmProofAlgorithm::SU_ES256 => ProofAlgorithm::SU_ES256, + WasmProofAlgorithm::SU_ES384 => ProofAlgorithm::SU_ES384, + WasmProofAlgorithm::SU_ES512 => ProofAlgorithm::SU_ES512, WasmProofAlgorithm::MAC_H256 => ProofAlgorithm::MAC_H256, WasmProofAlgorithm::MAC_H384 => ProofAlgorithm::MAC_H384, WasmProofAlgorithm::MAC_H512 => ProofAlgorithm::MAC_H512, diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index b7a0f08ea5..549516005d 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -1,5 +1,8 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ #![allow(deprecated)] #![allow(clippy::upper_case_acronyms)] @@ -43,5 +46,5 @@ pub fn start() -> Result<(), JsValue> { // It appears the import path must be relative to `src`. #[wasm_bindgen(typescript_custom_section)] const CUSTOM_IMPORTS: &'static str = r#" -import { JwsAlgorithm, JwkOperation, JwkUse, JwkType } from '../lib/jose/index'; +import { JwsAlgorithm, JwkOperation, JwkUse, JwkType, CompositeAlgId} from '../lib/jose/index'; "#; diff --git a/bindings/wasm/src/storage/jwk_storage_pqc.rs b/bindings/wasm/src/storage/jwk_storage_pqc.rs new file mode 100644 index 0000000000..aef570adb8 --- /dev/null +++ b/bindings/wasm/src/storage/jwk_storage_pqc.rs @@ -0,0 +1,83 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use super::WasmJwkStorage; +use identity_iota::storage::JwkGenOutput; +use identity_iota::storage::JwkStoragePQ; +use identity_iota::storage::KeyId; +use identity_iota::storage::KeyStorageResult; +use identity_iota::storage::KeyType; +use identity_iota::verification::jwk::Jwk; +use wasm_bindgen::prelude::*; +use crate::error::JsValueResult; +use js_sys::Promise; +use identity_iota::storage::KeyStorageErrorKind; +use identity_iota::verification::jose::jws::JwsAlgorithm; +use identity_iota::storage::KeyStorageError; +use wasm_bindgen_futures::JsFuture; +use super::jwk_storage::PromiseJwkGenOutput; +use js_sys::Array; +use crate::jose::WasmJwk; +use js_sys::Uint8Array; + + +use crate::common::PromiseUint8Array; + +#[wasm_bindgen] +extern "C" { + + #[wasm_bindgen(method, js_name = generatePQKey)] + pub fn _generate_pq_key(this: &WasmJwkStorage, key_type: String, alg: String) -> PromiseJwkGenOutput; + + #[wasm_bindgen(method, js_name = signPQ)] + pub fn _pq_sign(this: &WasmJwkStorage, key_id: String, data: Vec, public_key: WasmJwk, ctx: Option<&[u8]>) -> PromiseUint8Array; + +} + + +#[async_trait::async_trait(?Send)] +impl JwkStoragePQ for WasmJwkStorage { + async fn generate_pq_key(&self, key_type: KeyType, alg: JwsAlgorithm) -> KeyStorageResult { + let promise: Promise = Promise::resolve(&WasmJwkStorage::_generate_pq_key(self, key_type.into(), alg.name().to_owned())); + let result: JsValueResult = JsFuture::from(promise).await.into(); + result.into() + } + + async fn pq_sign(&self, key_id: &KeyId, data: &[u8], public_key: &Jwk, ctx: Option<&[u8]>) -> KeyStorageResult> { + let promise: Promise = Promise::resolve(&WasmJwkStorage::_pq_sign( + self, + key_id.clone().into(), + data.to_owned(), + WasmJwk(public_key.clone(),), + ctx + )); + let result: JsValueResult = JsFuture::from(promise).await.into(); + result.to_key_storage_error().map(uint8array_to_bytes)? + } + +} + +#[wasm_bindgen(typescript_custom_section)] +const JWK_STORAGE_PQ: &'static str = r#" +/** Secure storage for cryptographic keys represented as JWKs. */ +interface JwkStoragePQ { + /** Generate a new PQ key represented as a JSON Web Key. + * + * It's recommend that the implementer exposes constants for the supported key type string. */ + generatePQKey: (keyType: string, algorithm: JwsAlgorithm) => Promise; + + signPQ: (keyId: string, data: Uint8Array, publicKey: Jwk, ctx: Uint8Array|undefined ) => Promise; +}"#; + +fn uint8array_to_bytes(value: JsValue) -> KeyStorageResult> { + if !JsCast::is_instance_of::(&value) { + return Err( + KeyStorageError::new(KeyStorageErrorKind::SerializationError) + .with_custom_message("expected Uint8Array".to_owned()), + ); + } + let array_js_value = JsValue::from(Array::from(&value)); + array_js_value + .into_serde() + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::SerializationError).with_custom_message(e.to_string())) +} \ No newline at end of file diff --git a/bindings/wasm/src/storage/mod.rs b/bindings/wasm/src/storage/mod.rs index fe54110e9d..7a6dd1fad5 100644 --- a/bindings/wasm/src/storage/mod.rs +++ b/bindings/wasm/src/storage/mod.rs @@ -1,5 +1,8 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ mod jpt_timeframe_revocation_ext; mod jwk_gen_output; @@ -10,6 +13,7 @@ mod key_id_storage; mod method_digest; mod signature_options; mod wasm_storage; +mod jwk_storage_pqc; pub use jpt_timeframe_revocation_ext::*; pub use jwk_gen_output::*; diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs index a78dea0e76..3fbffd6eb8 100644 --- a/examples/1_advanced/10_zkp_revocation.rs +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -156,7 +156,7 @@ async fn main() -> anyhow::Result<()> { &storage_issuer, JwkMemStore::BLS12381G2_KEY_TYPE, None, - Some(ProofAlgorithm::BLS12381_SHA256), + Some(ProofAlgorithm::BBS), ) .await?; @@ -344,7 +344,7 @@ async fn main() -> anyhow::Result<()> { // Step 2b: Waiting for the next validityTimeframe, will result in the Credential timeframe interval NOT valid // =========================================================================== - thread::sleep(SleepDuration::from_secs(61)); + thread::sleep(SleepDuration::from_secs(63)); let timeframe_result = JptPresentationValidatorUtils::check_timeframes_with_validity_timeframe_2024( &decoded_presented_credential.credential, diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs index eeb4246280..1b056e334f 100644 --- a/examples/1_advanced/9_zkp.rs +++ b/examples/1_advanced/9_zkp.rs @@ -103,7 +103,7 @@ async fn main() -> anyhow::Result<()> { &secret_manager_issuer, &storage_issuer, JwkMemStore::BLS12381G2_KEY_TYPE, - ProofAlgorithm::BLS12381_SHA256, + ProofAlgorithm::BBS, ) .await?; diff --git a/examples/1_advanced/hybrid.rs b/examples/1_advanced/hybrid.rs new file mode 100644 index 0000000000..f01a5de0ef --- /dev/null +++ b/examples/1_advanced/hybrid.rs @@ -0,0 +1,289 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use examples::get_address_with_funds; +use examples::random_stronghold_path; +use examples::MemStorage; +use identity_eddsa_verifier::EdDSAJwsVerifier; +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::DecodedJwtCredential; +use identity_iota::credential::DecodedJwtPresentation; +use identity_iota::credential::FailFast; +use identity_iota::credential::Jwt; +use identity_iota::credential::JwtCredentialValidationOptions; +use identity_iota::credential::JwtCredentialValidatorHybrid; +use identity_iota::credential::JwtCredentialValidatorUtils; +use identity_iota::credential::JwtPresentationOptions; +use identity_iota::credential::JwtPresentationValidationOptions; +use identity_iota::credential::JwtPresentationValidatorHybrid; +use identity_iota::credential::JwtPresentationValidatorUtils; +use identity_iota::credential::Presentation; +use identity_iota::credential::PresentationBuilder; +use identity_iota::credential::Subject; +use identity_iota::credential::SubjectHolderRelationship; +use identity_iota::did::CoreDID; +use identity_iota::did::DID; +use identity_iota::document::verifiable::JwsVerificationOptions; +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::JwkDocumentExtHybrid; +use identity_iota::storage::JwkMemStore; +use identity_iota::storage::JwsSignatureOptions; +use identity_iota::storage::KeyIdMemstore; +use identity_iota::verification::jwk::CompositeAlgId; +use identity_iota::verification::MethodScope; +use identity_pqc_verifier::PQCJwsVerifier; +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 serde_json::json; + +// // The API endpoint of an IOTA node, e.g. Hornet. +// const API_ENDPOINT: &str = "http://localhost"; +// // The faucet endpoint allows requesting funds for testing purposes. +// const FAUCET_ENDPOINT: &str = "http://localhost/faucet/api/enqueue"; + +const API_ENDPOINT: &str = "https://api.testnet.shimmer.network"; +const FAUCET_ENDPOINT: &str = "https://faucet.testnet.shimmer.network/api/enqueue"; + +async fn create_did( + client: &Client, + secret_manager: &SecretManager, + storage: &MemStorage, + alg_id: CompositeAlgId, +) -> 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 PQC key + let fragment = document + .generate_method_hybrid(storage, alg_id, 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 a DID Document and publish it in a new Alias Output. +/// +/// In this example we connect to a locally running private network, but it can be adapted +/// to run on any IOTA node by setting the network and faucet endpoints. +/// +/// See the following instructions on running your own private network +/// https://github.com/iotaledger/hornet/tree/develop/private_tangle +#[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 (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did( + &client, + &secret_manager_issuer, + &storage_issuer, + CompositeAlgId::IdMldsa65Ed25519, + ) + .await?; + + 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 (_, holder_document, fragment_holder): (Address, IotaDocument, String) = create_did( + &client, + &secret_manager_holder, + &storage_holder, + CompositeAlgId::IdMldsa65Ed25519, + ) + .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", + "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_jwt: Jwt = issuer_document + .create_credential_jwt_hybrid( + &credential, + &storage_issuer, + &fragment_issuer, + &JwsSignatureOptions::default(), + None, + ) + .await?; + + println!("Credential JWT: {}", credential_jwt.as_str()); + + // Before sending this credential to the holder the issuer wants to validate that some properties + // of the credential satisfy their expectations. + + // Validate the credential's signature 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_credential: DecodedJwtCredential = + JwtCredentialValidatorHybrid::with_signature_verifiers(EdDSAJwsVerifier::default(), PQCJwsVerifier::default()) + .validate::<_, Object>( + &credential_jwt, + &issuer_document, + &JwtCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); + + println!("VC successfully validated"); + + println!("Credential JSON > {:#}", decoded_credential.credential); + + // =========================================================================== + // Step 4: Verifier 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 verifier and holder 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(); + + // =========================================================================== + // Step 5: Holder creates and signs a verifiable presentation from the issued credential. + // =========================================================================== + + // Create an unsigned Presentation from the previously issued Verifiable Credential. + let presentation: Presentation = + PresentationBuilder::new(holder_document.id().to_url().into(), Default::default()) + .credential(credential_jwt) + .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_hybrid( + &presentation, + &storage_holder, + &fragment_holder, + &JwsSignatureOptions::default().nonce(challenge.to_owned()), + &JwtPresentationOptions::default().expiration_date(expires), + ) + .await?; + + // =========================================================================== + // Step 6: Holder sends a verifiable presentation to the verifier. + // =========================================================================== + println!( + "Sending presentation (as JWT) to the verifier: {}", + presentation_jwt.as_str() + ); + + // =========================================================================== + // Step 7: Verifier receives the Verifiable Presentation and verifies it. + // =========================================================================== + + // The verifier wants the following requirements to be satisfied: + // - JWT verification of the presentation (including checking the requested challenge to mitigate replay attacks) + // - JWT verification of the credentials. + // - The presentation holder must always be the subject, regardless of the presence of the nonTransferable property + // - The issuance date must not be in the future. + + let presentation_verifier_options: JwsVerificationOptions = + JwsVerificationOptions::default().nonce(challenge.to_owned()); + + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client); + + // 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 = + JwtPresentationValidatorHybrid::with_signature_verifiers(EdDSAJwsVerifier::default(), PQCJwsVerifier::default()) + .validate(&presentation_jwt, &holder, &presentation_validation_options)?; + + // Concurrently resolve the issuers' documents. + let jwt_credentials: &Vec = &presentation.presentation.verifiable_credential; + let issuers: Vec = jwt_credentials + .iter() + .map(JwtCredentialValidatorUtils::extract_issuer_from_jwt) + .collect::, _>>()?; + let issuers_documents: HashMap = resolver.resolve_multiple(&issuers).await?; + + // Validate the credentials in the presentation. + let credential_validator = + JwtCredentialValidatorHybrid::with_signature_verifiers(EdDSAJwsVerifier::default(), PQCJwsVerifier::default()); + let validation_options: JwtCredentialValidationOptions = JwtCredentialValidationOptions::default() + .subject_holder_relationship(holder_did.to_url().into(), SubjectHolderRelationship::AlwaysSubject); + + for (index, jwt_vc) in jwt_credentials.iter().enumerate() { + // SAFETY: Indexing should be fine since we extracted the DID from each credential and resolved it. + let issuer_document: &IotaDocument = &issuers_documents[&issuers[index]]; + + let _decoded_credential: DecodedJwtCredential = credential_validator + .validate::<_, Object>(jwt_vc, issuer_document, &validation_options, FailFast::FirstError) + .unwrap(); + } + + // Since no errors were thrown by `verify_presentation` we know that the validation was successful. + println!("VP successfully validated: {:#?}", presentation.presentation); + + // Note that we did not declare a latest allowed issuance date for credentials. This is because we only want to check + // that the credentials do not have an issuance date in the future which is a default check. + + Ok(()) +} diff --git a/examples/1_advanced/pq.rs b/examples/1_advanced/pq.rs new file mode 100644 index 0000000000..3d4d1c49eb --- /dev/null +++ b/examples/1_advanced/pq.rs @@ -0,0 +1,292 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use examples::get_address_with_funds; +use examples::random_stronghold_path; +use examples::MemStorage; +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::DecodedJwtCredential; +use identity_iota::credential::DecodedJwtPresentation; +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::JwtPresentationOptions; +use identity_iota::credential::JwtPresentationValidationOptions; +use identity_iota::credential::JwtPresentationValidator; +use identity_iota::credential::JwtPresentationValidatorUtils; +use identity_iota::credential::Presentation; +use identity_iota::credential::PresentationBuilder; +use identity_iota::credential::Subject; +use identity_iota::credential::SubjectHolderRelationship; +use identity_iota::did::CoreDID; +use identity_iota::did::DID; +use identity_iota::document::verifiable::JwsVerificationOptions; +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::JwsDocumentExtPQC; +use identity_iota::storage::JwsSignatureOptions; +use identity_iota::storage::KeyIdMemstore; +use identity_iota::storage::KeyType; +use identity_iota::verification::jws::JwsAlgorithm; +use identity_iota::verification::MethodScope; +use identity_pqc_verifier::PQCJwsVerifier; +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 serde_json::json; + +// The API endpoint of an IOTA node, e.g. Hornet. +const API_ENDPOINT: &str = "http://localhost"; +// The faucet endpoint allows requesting funds for testing purposes. +const FAUCET_ENDPOINT: &str = "http://localhost/faucet/api/enqueue"; + +async fn create_did( + client: &Client, + secret_manager: &SecretManager, + storage: &MemStorage, + key_type: KeyType, + alg: JwsAlgorithm, +) -> 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 PQC key + let fragment = document + .generate_method_pqc(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 a Post-Quantum Verifiable Credential. +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // =========================================================================== + // Step 1: Create identitiy 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::PQ_KEY_TYPE, + JwsAlgorithm::ML_DSA_87, + ) + .await?; + + 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 (_, holder_document, fragment_holder): (Address, IotaDocument, String) = create_did( + &client, + &secret_manager_holder, + &storage_holder, + JwkMemStore::PQ_KEY_TYPE, + JwsAlgorithm::SLH_DSA_SHA2_128s, + ) + .await?; + + // ====================================================================================== + // Step 2: Issuer creates and signs a Verifiable Credential with a Post-Quantum algorithm. + // ====================================================================================== + + // 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", + "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_jwt: Jwt = issuer_document + .create_credential_jwt_pqc( + &credential, + &storage_issuer, + &fragment_issuer, + &JwsSignatureOptions::default(), + None, + ) + .await?; + + // Before sending this credential to the holder the issuer wants to validate that some properties + // of the credential satisfy their expectations. + + JwtCredentialValidator::with_signature_verifier(PQCJwsVerifier::default()) + .validate::<_, Object>( + &credential_jwt, + &issuer_document, + &JwtCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); + + println!("VC successfully validated"); + + // =========================================================================== + // Step 3: Issuer sends the Verifiable Credential to the holder. + // =========================================================================== + println!( + "Sending credential (as JWT) to the holder: {}\n", + credential_jwt.as_str() + ); + + // =========================================================================== + // Step 4: Verifier 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 verifier and holder 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(); + + // =========================================================================== + // Step 5: Holder creates and signs a verifiable presentation from the issued credential. + // =========================================================================== + + // Create an unsigned Presentation from the previously issued Verifiable Credential. + let presentation: Presentation = + PresentationBuilder::new(holder_document.id().to_url().into(), Default::default()) + .credential(credential_jwt) + .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_pqc( + &presentation, + &storage_holder, + &fragment_holder, + &JwsSignatureOptions::default().nonce(challenge.to_owned()), + &JwtPresentationOptions::default().expiration_date(expires), + ) + .await?; + + // =========================================================================== + // Step 6: Holder sends a verifiable presentation to the verifier. + // =========================================================================== + println!( + "Sending presentation (as JWT) to the verifier: {}\n", + presentation_jwt.as_str() + ); + + // =========================================================================== + // Step 7: Verifier receives the Verifiable Presentation and verifies it. + // =========================================================================== + + // The verifier wants the following requirements to be satisfied: + // - JWT verification of the presentation (including checking the requested challenge to mitigate replay attacks) + // - JWT verification of the credentials. + // - The presentation holder must always be the subject, regardless of the presence of the nonTransferable property + // - The issuance date must not be in the future. + + let presentation_verifier_options: JwsVerificationOptions = + JwsVerificationOptions::default().nonce(challenge.to_owned()); + + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client); + + // 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( + PQCJwsVerifier::default(), + ) + .validate(&presentation_jwt, &holder, &presentation_validation_options)?; + + // Concurrently resolve the issuers' documents. + let jwt_credentials: &Vec = &presentation.presentation.verifiable_credential; + let issuers: Vec = jwt_credentials + .iter() + .map(JwtCredentialValidatorUtils::extract_issuer_from_jwt) + .collect::, _>>()?; + let issuers_documents: HashMap = resolver.resolve_multiple(&issuers).await?; + + // Validate the credentials in the presentation. + let credential_validator: JwtCredentialValidator = + JwtCredentialValidator::with_signature_verifier(PQCJwsVerifier::default()); + let validation_options: JwtCredentialValidationOptions = JwtCredentialValidationOptions::default() + .subject_holder_relationship(holder_did.to_url().into(), SubjectHolderRelationship::AlwaysSubject); + + println!("--------------------------"); + + for (index, jwt_vc) in jwt_credentials.iter().enumerate() { + // SAFETY: Indexing should be fine since we extracted the DID from each credential and resolved it. + let issuer_document: &IotaDocument = &issuers_documents[&issuers[index]]; + + let _decoded_credential: DecodedJwtCredential = credential_validator + .validate::<_, Object>(jwt_vc, issuer_document, &validation_options, FailFast::FirstError) + .unwrap(); + } + + // Since no errors were thrown by `verify_presentation` we know that the validation was successful. + println!("VP successfully validated: {:#?}", presentation.presentation); + + Ok(()) +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index b699ae34ef..a66a0b3232 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -9,7 +9,7 @@ publish = false anyhow = "1.0.62" bls12_381_plus.workspace = true 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", "resolver"] } +identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus", "hybrid-liboqs", "resolver"] } 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 @@ -18,6 +18,8 @@ rand = "0.8.5" 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"] } +identity_pqc_verifier = { path = "../identity_pqc_verifier", default-features = true } +serde.workspace = true [lib] path = "utils/utils.rs" @@ -105,3 +107,11 @@ name = "10_zkp_revocation" [[example]] path = "1_advanced/11_linked_verifiable_presentation.rs" name = "11_linked_verifiable_presentation" + +[[example]] +path = "1_advanced/pq.rs" +name = "pq" + +[[example]] +path = "1_advanced/hybrid.rs" +name = "hybrid" diff --git a/examples/README.md b/examples/README.md index 8ea9ab2145..6dd319d1ca 100644 --- a/examples/README.md +++ b/examples/README.md @@ -48,4 +48,11 @@ The following advanced examples are available: | [6_domain_linkage](./1_advanced/6_domain_linkage) | Demonstrates how to link a domain and a DID and verify the linkage. | | [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. | | [8_status_list_2021](./1_advanced/8_status_list_2021.rs) | Demonstrates how to revoke a credential using `StatusList2021`. | +| [9_zkp](./1_advanced/9_zkp.rs) | Demonstrates how to generate, present and verify a ZK VC (BBS+) with Selective Disclosure. | +| [10_zkp_revocation](./1_advanced/10_zkp_revocation.rs) | Demonstrates how to revoke a ZK VC (BBS+). | | [11_linked_verifiable_presentation](./1_advanced/11_linked_verifiable_presentation.rs) | Demonstrates how to link a public Verifiable Presentation to an identity and how it can be verified. | +| [12_pq](./1_advanced/12_pq.rs) | Demonstrates how to generate, present and verify a VC with pure PQ signature. | +| [13_hybrid](./1_advanced/13_hybrid.rs) | Demonstrates how to generate, present and verify a VC with PQ/T hybrid signature | + +#### Note: Running the examples with the release flag will be significantly faster due to stronghold performance issues in debug mode. + diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index f84ff4f152..4bc009532b 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -79,6 +79,6 @@ jpt-bbs-plus = [ "dep:json-proof-token", "dep:futures", ] - +hybrid = ["credential", "validator"] [lints] workspace = true diff --git a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_hybrid.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_hybrid.rs new file mode 100644 index 0000000000..090db2bd97 --- /dev/null +++ b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_hybrid.rs @@ -0,0 +1,342 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::convert::FromJson; +use identity_did::CoreDID; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use identity_document::verifiable::JwsVerificationOptions; +use identity_verification::jwk::Jwk; +use identity_verification::jws::DecodedJws; +use identity_verification::jws::Decoder; +use identity_verification::jws::JwsValidationItem; +use identity_verification::jws::JwsVerifier; +use identity_verification::jwk::CompositeJwk; + +use super::CompoundCredentialValidationError; +use super::DecodedJwtCredential; +use super::JwtCredentialValidationOptions; +use super::JwtCredentialValidatorUtils; +use super::JwtValidationError; +use super::SignerContext; +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; +use crate::credential::Jwt; +use crate::validator::FailFast; + +/// A type for decoding and validating [`Credential`]s signed with a PQ/T signature. +#[non_exhaustive] +pub struct JwtCredentialValidatorHybrid(TRV, PQV); + +impl JwtCredentialValidatorHybrid { + /// Create a new [`JwtCredentialValidatorHybrid`] that delegates cryptographic signature verification to the given + /// traditional [`JwsVerifier`] and PQ [`JwsVerifier`]. + pub fn with_signature_verifiers(traditional_signature_verifier: TRV, pq_signature_verifier: PQV) -> Self { + Self(traditional_signature_verifier, pq_signature_verifier) + } + + /// Decodes and validates a [`Credential`] issued as a JWT. A [`DecodedJwtCredential`] is returned upon success. + /// + /// The following properties are validated according to `options`: + /// - the issuer's PQ/T signature on the JWS, + /// - the expiration date, + /// - the issuance date, + /// - the semantic structure. + /// + /// # Warning + /// The lack of an error returned from this method is in of itself not enough to conclude that the credential can be + /// trusted. This section contains more information on additional checks that should be carried out before and after + /// calling this method. + /// + /// ## The state of the issuer's DID Document + /// The caller must ensure that `issuer` represents an up-to-date DID Document. + /// + /// ## Properties that are not validated + /// There are many properties defined in [The Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/) that are **not** validated, such as: + /// `proof`, `credentialStatus`, `type`, `credentialSchema`, `refreshService` **and more**. + /// These should be manually checked after validation, according to your requirements. + /// + /// # Errors + /// An error is returned whenever a validated condition is not satisfied. + pub fn validate( + &self, + credential_jwt: &Jwt, + issuer: &DOC, + options: &JwtCredentialValidationOptions, + fail_fast: FailFast, + ) -> Result, CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + let credential_token = self + .verify_signature( + credential_jwt, + std::slice::from_ref(issuer.as_ref()), + &options.verification_options, + ) + .map_err(|err| CompoundCredentialValidationError { + validation_errors: [err].into(), + })?; + + Self::validate_decoded_credential::( + credential_token, + std::slice::from_ref(issuer.as_ref()), + options, + fail_fast, + ) + } + + /// Decode and verify the PQ/T JWS signature of a [`Credential`] issued as a JWT using the DID Document of a trusted + /// issuer. + /// + /// A [`DecodedJwtCredential`] is returned upon success. + /// + /// # Warning + /// The caller must ensure that the DID Documents of the trusted issuers are up-to-date. + /// + /// ## Proofs + /// Only the PQ/T JWS signature is verified. If the [`Credential`] contains a `proof` property this will not be verified + /// by this method. + /// + /// # Errors + /// This method immediately returns an error if + /// the credential issuer' url cannot be parsed to a DID belonging to one of the trusted issuers. Otherwise an attempt + /// to verify the credential's signature will be made and an error is returned upon failure. + pub fn verify_signature( + &self, + credential: &Jwt, + trusted_issuers: &[DOC], + options: &JwsVerificationOptions, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + Self::verify_signature_with_verifiers(&self.0, &self.1, credential, trusted_issuers, options) + } + + // This method takes a slice of issuer's instead of a single issuer in order to better accommodate presentation + // validation. It also validates the relationship between a holder and the credential subjects when + // `relationship_criterion` is Some. + pub(crate) fn validate_decoded_credential( + credential_token: DecodedJwtCredential, + issuers: &[DOC], + options: &JwtCredentialValidationOptions, + fail_fast: FailFast, + ) -> Result, CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + let credential: &Credential = &credential_token.credential; + // 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_token.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); + + #[cfg(feature = "revocation-bitmap")] + let validation_units_iter = { + let revocation_validation = + std::iter::once_with(|| JwtCredentialValidatorUtils::check_status(credential, issuers, options.status)); + validation_units_iter.chain(revocation_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(credential_token) + } else { + Err(CompoundCredentialValidationError { validation_errors }) + } + } + + pub(crate) fn parse_composite_pk<'a, 'i, DOC>( + jws: &JwsValidationItem<'a>, + trusted_issuers: &'i [DOC], + options: &JwsVerificationOptions, + ) -> Result<(&'a CompositeJwk, DIDUrl), JwtValidationError> + where + DOC: AsRef, + 'i: 'a, + { + let nonce: Option<&str> = options.nonce.as_deref(); + // Validate the nonce + if jws.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.method_id { + Some(method_id) => method_id.clone(), + None => { + let kid: &str = jws.protected_header().and_then(|header| 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, + })? + } + }; + + // locate the corresponding issuer + let issuer: &CoreDocument = trusted_issuers + .iter() + .map(AsRef::as_ref) + .find(|issuer_doc| ::id(issuer_doc) == method_id.did()) + .ok_or(JwtValidationError::DocumentMismatch(SignerContext::Issuer))?; + + // Obtain the public key from the issuer's DID document + issuer + .resolve_method(&method_id, options.method_scope) + .and_then(|method| method.data().composite_public_key()) + .ok_or_else(|| JwtValidationError::MethodDataLookupError { + source: None, + message: "could not extract CompositePublicKey from a method identified by kid", + signer_ctx: SignerContext::Issuer, + }) + .map(move |c: &CompositeJwk| (c, method_id)) + } + + /// Stateless version of [`Self::verify_signature`] + fn verify_signature_with_verifiers( + traditional_signature_verifier: &TRV, + pq_signature_verifier: &PQV, + credential: &Jwt, + trusted_issuers: &[DOC], + options: &JwsVerificationOptions, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef + { + // Note the below steps are necessary because `CoreDocument::verify_jws` decodes the JWS and then searches for a + // method with a fragment (or full DID Url) matching `kid` in the given document. We do not want to carry out + // that process for potentially every document in `trusted_issuers`. + + // Start decoding the credential + let decoded: JwsValidationItem<'_> = Self::decode(credential.as_str())?; + + let (composite, method_id) = Self::parse_composite_pk(&decoded, trusted_issuers, options)?; + + let credential_token = Self::verify_decoded_signature( + decoded, + composite.traditional_public_key(), + composite.pq_public_key(), + traditional_signature_verifier, + pq_signature_verifier, + )?; + + // 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) + } + + /// Decode the credential into a [`JwsValidationItem`]. + pub(crate) fn decode(credential_jws: &str) -> Result, JwtValidationError> { + let decoder: Decoder = Decoder::new(); + + decoder + .decode_compact_serialization(credential_jws.as_bytes(), None) + .map_err(JwtValidationError::JwsDecodingError) + } + + pub(crate) fn verify_signature_raw<'a>( + decoded: JwsValidationItem<'a>, + traditional_pk: &Jwk, + pq_pk: &Jwk, + traditional_verifier: &TRV, + pq_verifier: &PQV, + ) -> Result, JwtValidationError> { + decoded + .verify_hybrid(traditional_verifier, pq_verifier, traditional_pk, pq_pk) + .map_err(|err| JwtValidationError::Signature { + source: err, + signer_ctx: SignerContext::Issuer, + }) + } + + /// Verify the signature using the given `public_key` and `signature_verifier`. + fn verify_decoded_signature( + decoded: JwsValidationItem<'_>, + traditional_pk: &Jwk, + pq_pk: &Jwk, + traditional_verifier: &TRV, + pq_verifier: &PQV, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + // Verify the JWS signature and obtain the decoded token containing the protected header and raw claims + let DecodedJws { protected, claims, .. } = + Self::verify_signature_raw(decoded, traditional_pk, pq_pk, traditional_verifier, pq_verifier)?; + + let credential_claims: CredentialJwtClaims<'_, T> = + CredentialJwtClaims::from_json_slice(&claims).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(DecodedJwtCredential { + credential, + header: Box::new(protected), + custom_claims, + }) + } +} diff --git a/identity_credential/src/validator/jwt_credential_validation/mod.rs b/identity_credential/src/validator/jwt_credential_validation/mod.rs index 25c0d4f494..2bf943ad31 100644 --- a/identity_credential/src/validator/jwt_credential_validation/mod.rs +++ b/identity_credential/src/validator/jwt_credential_validation/mod.rs @@ -1,15 +1,22 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ //! Contains functionality for validating credentials issued as JWTs. mod decoded_jwt_credential; mod error; mod jwt_credential_validation_options; mod jwt_credential_validator; +#[cfg(feature = "hybrid")] +mod jwt_credential_validator_hybrid; mod jwt_credential_validator_utils; pub use decoded_jwt_credential::*; pub use error::*; pub use jwt_credential_validation_options::*; pub use jwt_credential_validator::*; +#[cfg(feature = "hybrid")] +pub use jwt_credential_validator_hybrid::*; pub use jwt_credential_validator_utils::*; diff --git a/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator_hybrid.rs b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator_hybrid.rs new file mode 100644 index 0000000000..ffc9e4ff9f --- /dev/null +++ b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator_hybrid.rs @@ -0,0 +1,168 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_core::convert::FromJson; +use identity_did::CoreDID; +use identity_document::document::CoreDocument; +use identity_verification::jws::DecodedJws; +use identity_verification::jws::JwsVerifier; +use std::str::FromStr; + +use crate::credential::Jwt; +use crate::presentation::Presentation; +use crate::presentation::PresentationJwtClaims; +use crate::validator::jwt_credential_validation::JwtValidationError; +use crate::validator::jwt_credential_validation::SignerContext; + +use super::CompoundJwtPresentationValidationError; +use super::DecodedJwtPresentation; +use super::JwtPresentationValidationOptions; + +/// Struct for validating [`Presentation`] signed with a PQ/T signature. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct JwtPresentationValidatorHybrid(TRV, PQV); + +impl JwtPresentationValidatorHybrid +where + TRV: JwsVerifier, + PQV: JwsVerifier, +{ + /// Creates a new [`JwtPresentationValidatorHybrid`] using a specific traditional [`JwsVerifier`] and a specific PQ [`JwsVerifier`]. + pub fn with_signature_verifiers(traditional_signature_verifier: TRV, pq_signature_verifier: PQV) -> Self { + Self(traditional_signature_verifier, pq_signature_verifier) + } + + /// Validates a [`Presentation`] signed with a PQ/T signature. + /// + /// The following properties are validated according to `options`: + /// - the JWT can be decoded into a semantically valid presentation. + /// - the expiration and issuance date contained in the JWT claims. + /// - the holder's PQ/T signature. + /// + /// Validation is done with respect to the properties set in `options`. + /// + /// # Warning + /// + /// * This method does NOT validate the constituent credentials and therefore also not the relationship between the + /// credentials' subjects and the presentation holder. This can be done with + /// [`JwtCredentialValidationOptions`](crate::validator::JwtCredentialValidationOptions). + /// * The lack of an error returned from this method is in of itself not enough to conclude that the presentation can + /// be trusted. This section contains more information on additional checks that should be carried out before and + /// after calling this method. + /// + /// ## The state of the supplied DID Documents. + /// + /// The caller must ensure that the DID Documents in `holder` and `issuers` are up-to-date. + /// + /// # Errors + /// + /// An error is returned whenever a validated condition is not satisfied or when decoding fails. + pub fn validate( + &self, + presentation: &Jwt, + holder: &HDOC, + options: &JwtPresentationValidationOptions, + ) -> Result, CompoundJwtPresentationValidationError> + where + HDOC: AsRef + ?Sized, + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + CRED: ToOwned + serde::Serialize + serde::de::DeserializeOwned + Clone, + { + // Verify JWS. + let decoded_jws: DecodedJws<'_> = holder + .as_ref() + .verify_jws_hybrid( + presentation.as_str(), + None, + &self.0, + &self.1, + &options.presentation_verifier_options, + ) + .map_err(|err| { + CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::PresentationJwsError(err)) + })?; + + let claims: PresentationJwtClaims<'_, CRED, T> = PresentationJwtClaims::from_json_slice(&decoded_jws.claims) + .map_err(|err| { + CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::PresentationStructure( + crate::Error::JwtClaimsSetDeserializationError(err.into()), + )) + })?; + + // Verify that holder document matches holder in presentation. + let holder_did: CoreDID = CoreDID::from_str(claims.iss.as_str()).map_err(|err| { + CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::SignerUrl { + signer_ctx: SignerContext::Holder, + source: err.into(), + }) + })?; + + if &holder_did != ::id(holder.as_ref()) { + return Err(CompoundJwtPresentationValidationError::one_presentation_error( + JwtValidationError::DocumentMismatch(SignerContext::Holder), + )); + } + + // Check the expiration date. + let expiration_date: Option = claims + .exp + .map(|exp| { + Timestamp::from_unix(exp).map_err(|err| { + CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::PresentationStructure( + crate::Error::JwtClaimsSetDeserializationError(err.into()), + )) + }) + }) + .transpose()?; + + (expiration_date.is_none() || expiration_date >= Some(options.earliest_expiry_date.unwrap_or_default())) + .then_some(()) + .ok_or(CompoundJwtPresentationValidationError::one_presentation_error( + JwtValidationError::ExpirationDate, + ))?; + + // Check issuance date. + let issuance_date: Option = match claims.issuance_date { + Some(iss) => { + if iss.iat.is_some() || iss.nbf.is_some() { + Some(iss.to_issuance_date().map_err(|err| { + CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::PresentationStructure( + crate::Error::JwtClaimsSetDeserializationError(err.into()), + )) + })?) + } else { + None + } + } + None => None, + }; + + (issuance_date.is_none() || issuance_date <= Some(options.latest_issuance_date.unwrap_or_default())) + .then_some(()) + .ok_or(CompoundJwtPresentationValidationError::one_presentation_error( + JwtValidationError::IssuanceDate, + ))?; + + let aud: Option = claims.aud.clone(); + let custom_claims: Option = claims.custom.clone(); + + let presentation: Presentation = claims.try_into_presentation().map_err(|err| { + CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::PresentationStructure(err)) + })?; + + let decoded_jwt_presentation: DecodedJwtPresentation = DecodedJwtPresentation { + presentation, + header: Box::new(decoded_jws.protected), + expiration_date, + issuance_date, + aud, + custom_claims, + }; + + Ok(decoded_jwt_presentation) + } +} diff --git a/identity_credential/src/validator/jwt_presentation_validation/mod.rs b/identity_credential/src/validator/jwt_presentation_validation/mod.rs index a9bb144080..adf42dc3a9 100644 --- a/identity_credential/src/validator/jwt_presentation_validation/mod.rs +++ b/identity_credential/src/validator/jwt_presentation_validation/mod.rs @@ -1,14 +1,21 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ mod decoded_jwt_presentation; mod error; mod jwt_presentation_validation_options; mod jwt_presentation_validator; +#[cfg(feature = "hybrid")] +mod jwt_presentation_validator_hybrid; mod jwt_presentation_validator_utils; pub use decoded_jwt_presentation::*; pub use error::*; pub use jwt_presentation_validation_options::*; pub use jwt_presentation_validator::*; +#[cfg(feature = "hybrid")] +pub use jwt_presentation_validator_hybrid::*; pub use jwt_presentation_validator_utils::*; diff --git a/identity_did/src/did_compositejwk.rs b/identity_did/src/did_compositejwk.rs new file mode 100644 index 0000000000..4689351287 --- /dev/null +++ b/identity_did/src/did_compositejwk.rs @@ -0,0 +1,84 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Debug; +use std::fmt::Display; +use std::str::FromStr; + +use identity_jose::jwk::CompositeJwk; +use identity_jose::jwu::decode_b64_json; + +use crate::CoreDID; +use crate::Error; +use crate::DID; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +#[repr(transparent)] +#[serde(into = "CoreDID", try_from = "CoreDID")] +/// A type representing a `did:compositejwk` DID. +pub struct DIDCompositeJwk(CoreDID); + +impl DIDCompositeJwk { + /// [`DIDCompositeJwk`]'s method. + pub const METHOD: &'static str = "compositejwk"; + + /// Tries to parse a [`DIDCompositeJwk`] from a string. + pub fn parse(s: &str) -> Result { + s.parse() + } + + /// Returns the JWK encoded inside this did:jwk. + pub fn composite_jwk(&self) -> CompositeJwk { + decode_b64_json(self.method_id()).expect("did:compositejwk encodes a valid compositeJwk") + } +} + +impl AsRef for DIDCompositeJwk { + fn as_ref(&self) -> &CoreDID { + &self.0 + } +} + +impl From for CoreDID { + fn from(value: DIDCompositeJwk) -> Self { + value.0 + } +} + +impl<'a> TryFrom<&'a str> for DIDCompositeJwk { + type Error = Error; + fn try_from(value: &'a str) -> Result { + value.parse() + } +} + +impl Display for DIDCompositeJwk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for DIDCompositeJwk { + type Err = Error; + fn from_str(s: &str) -> Result { + s.parse::().and_then(TryFrom::try_from) + } +} + +impl From for String { + fn from(value: DIDCompositeJwk) -> Self { + value.to_string() + } +} + +impl TryFrom for DIDCompositeJwk { + type Error = Error; + fn try_from(value: CoreDID) -> Result { + let Self::METHOD = value.method() else { + return Err(Error::InvalidMethodName); + }; + decode_b64_json::(value.method_id()) + .map(|_| Self(value)) + .map_err(|_| Error::InvalidMethodId) + } +} \ No newline at end of file diff --git a/identity_did/src/lib.rs b/identity_did/src/lib.rs index 62c846847e..e9bb2c5873 100644 --- a/identity_did/src/lib.rs +++ b/identity_did/src/lib.rs @@ -1,6 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + #![forbid(unsafe_code)] #![doc = include_str!("./../README.md")] #![allow(clippy::upper_case_acronyms)] @@ -21,6 +25,7 @@ mod did; mod did_jwk; mod did_url; mod error; +mod did_compositejwk; pub use crate::did_url::DIDUrl; pub use crate::did_url::RelativeDIDUrl; @@ -29,3 +34,4 @@ pub use did::CoreDID; pub use did::DID; pub use did_jwk::*; pub use error::Error; +pub use did_compositejwk::*; \ No newline at end of file diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 1e1a340bb4..d1a5059630 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -1,12 +1,17 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use core::convert::TryInto as _; use core::fmt::Display; use core::fmt::Formatter; use std::collections::HashMap; use std::convert::Infallible; +use identity_did::DIDCompositeJwk; use identity_did::DIDJwk; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jws::DecodedJws; @@ -983,6 +988,63 @@ impl CoreDocument { .verify(signature_verifier, public_key) .map_err(Error::JwsVerificationError) } + + /// Decodes and verifies the provided PQ/T JWS according to the passed [`JwsVerificationOptions`] with a + /// traditional [`JwsVerifier`] and PQ [`JwsVerifier`]. + /// + /// Regardless of which options are passed the following conditions must be met in order for a verification attempt to + /// take place. + /// - The JWS must be encoded according to the JWS compact serialization. + /// - The `kid` value in the protected header must be an identifier of a verification method in this DID document, or + /// set explicitly in the `options`. + // + // NOTE: This is tested in `identity_storage` and `identity_credential`. + pub fn verify_jws_hybrid<'jws, TRV: JwsVerifier, PQV: JwsVerifier>( + &self, + jws: &'jws str, + detached_payload: Option<&'jws [u8]>, + traditional_verifier: &TRV, + pq_verifier: &PQV, + options: &JwsVerificationOptions, + ) -> Result> { + let validation_item = Decoder::new() + .decode_compact_serialization(jws.as_bytes(), detached_payload) + .map_err(Error::JwsVerificationError)?; + + let nonce: Option<&str> = options.nonce.as_deref(); + // Validate the nonce + if validation_item.nonce() != nonce { + return Err(Error::JwsVerificationError( + identity_verification::jose::error::Error::InvalidParam("invalid nonce value"), + )); + } + + let method_url_query: DIDUrlQuery<'_> = match &options.method_id { + Some(method_id) => method_id.into(), + None => validation_item + .kid() + .ok_or(Error::JwsVerificationError( + identity_verification::jose::error::Error::InvalidParam("missing kid value"), + ))? + .into(), + }; + + let composite_public_key = self + .resolve_method(method_url_query, options.method_scope) + .ok_or(Error::MethodNotFound)? + .data() + .try_composite_public_key() + .map_err(Error::InvalidKeyMaterial)?; + + validation_item + .verify_hybrid( + traditional_verifier, + pq_verifier, + composite_public_key.traditional_public_key(), + composite_public_key.pq_public_key(), + ) + .map_err(Error::JwsVerificationError) + } } impl CoreDocument { @@ -1002,6 +1064,23 @@ impl CoreDocument { } } +impl CoreDocument { + /// Creates a [`CoreDocument`] from a did:compositejwk DID. + pub fn expand_did_compositejwk(did_compositejwk: DIDCompositeJwk) -> Result { + let verification_method = VerificationMethod::try_from(did_compositejwk.clone()).map_err(Error::InvalidKeyMaterial)?; + let verification_method_id = verification_method.id().clone(); + + DocumentBuilder::default() + .id(did_compositejwk.into()) + .verification_method(verification_method) + .assertion_method(verification_method_id.clone()) + .authentication(verification_method_id.clone()) + .capability_invocation(verification_method_id.clone()) + .capability_delegation(verification_method_id.clone()) + .build() + } +} + #[cfg(test)] mod tests { use identity_core::convert::FromJson; diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index 39ae087d03..94b512a715 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -68,6 +68,14 @@ sd-jwt-vc = ["identity_credential/sd-jwt-vc"] # Enables zero knowledge selective disclosurable VCs jpt-bbs-plus = ["identity_storage/jpt-bbs-plus", "identity_credential/jpt-bbs-plus"] +# Enables PQC +pqc = ["identity_storage/pqc"] +pqc-liboqs = ["identity_storage/pqc-liboqs"] + +# Enables PQ/T Hybrid +hybrid = ["identity_storage/hybrid", "identity_credential/hybrid"] +hybrid-liboqs = ["identity_storage/hybrid-liboqs", "identity_credential/hybrid"] + [package.metadata.docs.rs] # To build locally: # RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --workspace --open diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index b43233afad..67f97a846d 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -13,7 +13,7 @@ description = "A library for JOSE (JSON Object Signing and Encryption)" [dependencies] bls12_381_plus.workspace = true identity_core = { version = "=1.5.1", path = "../identity_core" } -iota-crypto = { version = "0.23.2", default-features = false, features = ["std", "sha"] } +iota-crypto = { version = "0.23.2", default-features = false, features = ["ed25519", "std", "sha"] } json-proof-token.workspace = true serde.workspace = true serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/identity_jose/src/jwk/composite_jwk.rs b/identity_jose/src/jwk/composite_jwk.rs new file mode 100644 index 0000000000..61735b825a --- /dev/null +++ b/identity_jose/src/jwk/composite_jwk.rs @@ -0,0 +1,73 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use crate::jwk::Jwk; + +/// Mame of algorithms used to generate the hybrid signature. +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub enum CompositeAlgId { + /// DER encoded value in hex = 060B6086480186FA6B5008013E + #[serde(rename = "id-MLDSA44-Ed25519")] + IdMldsa44Ed25519, + /// DER encoded value in hex = 060B6086480186FA6B50080147 + #[serde(rename = "id-MLDSA65-Ed25519")] + IdMldsa65Ed25519, +} + +impl CompositeAlgId { + /// Returns the JWS algorithm as a `str` slice. + pub const fn name(self) -> &'static str { + match self { + Self::IdMldsa44Ed25519 => "id-MLDSA44-Ed25519", + Self::IdMldsa65Ed25519 => "id-MLDSA65-Ed25519", + } + } +} + +/// Represent a combination of a traditional public key and a post-quantum public key both in Jwk format. +#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub struct CompositeJwk { + #[serde(rename = "algId")] + alg_id: CompositeAlgId, + #[serde(rename = "traditionalPublicKey")] + traditional_public_key: Jwk, + #[serde(rename = "pqPublicKey")] + pq_public_key: Jwk, +} + +impl CompositeJwk { + /// Create a new CompositePublicKey structure. + pub fn new(alg_id: CompositeAlgId, traditional_public_key: Jwk, pq_public_key: Jwk) -> Self { + Self { + alg_id, + traditional_public_key, + pq_public_key, + } + } + /// Get the `algId` value. + pub fn alg_id(&self) -> CompositeAlgId { + self.alg_id + } + /// Get the post-quantum public key in Jwk format. + pub fn pq_public_key(&self) -> &Jwk { + &self.pq_public_key + } + /// Get the traditional public key in Jwk format. + pub fn traditional_public_key(&self) -> &Jwk { + &self.traditional_public_key + } +} + +impl FromStr for CompositeAlgId { + type Err = crate::error::Error; + + fn from_str(string: &str) -> std::result::Result { + match string { + "id-MLDSA44-Ed25519" => Ok(Self::IdMldsa44Ed25519), + "id-MLDSA65-Ed25519" => Ok(Self::IdMldsa65Ed25519), + &_ => Err(crate::error::Error::JwsAlgorithmParsingError), + } + } +} diff --git a/identity_jose/src/jwk/jwk_akp.rs b/identity_jose/src/jwk/jwk_akp.rs new file mode 100644 index 0000000000..9d6e943a4a --- /dev/null +++ b/identity_jose/src/jwk/jwk_akp.rs @@ -0,0 +1,64 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + + +// ============================================================================= +// Algorithm Key Pair (AKP) key parameters for Post-quantum algorithm +// ============================================================================= + +use zeroize::Zeroize; + +use super::JwkType; + +/// Parameters for Post-Quantum algorithm keys +/// +/// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-dilithium-06) +#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize, serde::Serialize, Zeroize)] +#[zeroize(drop)] +pub struct JwkParamsAKP { + /// The public key as a base64url-encoded value. + #[serde(rename = "pub")] + pub public: String, // Public Key + /// The private key as a base64url-encoded value. + #[serde(skip_serializing_if = "Option::is_none", rename = "priv")] + pub private: Option, // Private Key +} + +impl Default for JwkParamsAKP { + fn default() -> Self { + Self::new() + } +} + +impl JwkParamsAKP { + /// Creates new JWK AKP Params. + pub const fn new() -> Self { + Self { + public: String::new(), + private: None, + } + } + + /// Returns the key type `kty`. + pub const fn kty(&self) -> JwkType { + JwkType::Akp + } + + /// Returns a clone with _all_ private key components unset. + pub fn to_public(&self) -> Self { + Self { + public: self.public.clone(), + private: None, + } + } + + /// Returns `true` if _all_ private key components of the key are unset, `false` otherwise. + pub fn is_public(&self) -> bool { + self.private.is_none() + } + + /// Returns `true` if _all_ private key components of the key are set, `false` otherwise. + pub fn is_private(&self) -> bool { + self.private.is_some() + } +} diff --git a/identity_jose/src/jwk/key.rs b/identity_jose/src/jwk/key.rs index e2cb05d62d..a1ef5d947a 100644 --- a/identity_jose/src/jwk/key.rs +++ b/identity_jose/src/jwk/key.rs @@ -1,6 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use crypto::hashes::sha::SHA256; use crypto::hashes::sha::SHA256_LEN; use identity_core::common::Url; @@ -17,6 +21,7 @@ use crate::jwk::JwkParamsEc; use crate::jwk::JwkParamsOct; use crate::jwk::JwkParamsOkp; use crate::jwk::JwkParamsRsa; +use crate::jwk::JwkParamsAKP; use crate::jwk::JwkType; use crate::jwk::JwkUse; use crate::jwu::encode_b64; @@ -339,6 +344,22 @@ impl Jwk { } } + /// Returns the [`JwkParamsAkp`] in this JWK if it is of type `Akp`. + pub fn try_akp_params(&self) -> Result<&JwkParamsAKP> { + match self.params() { + JwkParams::Akp(params) => Ok(params), + _ => Err(Error::KeyError("Akp")), + } + } + + /// Returns a mutable reference to the [`JwkParamsAkp`] in this JWK if it is of type `Akp`. + pub fn try_akp_params_mut(&mut self) -> Result<&mut JwkParamsAKP> { + match self.params_mut() { + JwkParams::Akp(params) => Ok(params), + _ => Err(Error::KeyError("Akp")), + } + } + // =========================================================================== // Thumbprint // =========================================================================== @@ -387,6 +408,9 @@ impl Jwk { JwkParams::Okp(JwkParamsOkp { crv, x, .. }) => { format!(r#"{{"crv":"{crv}","kty":"{kty}","x":"{x}"}}"#) } + JwkParams::Akp(JwkParamsAKP { public, .. }) => { + format!(r#"{{"kty":"{kty}","pub":"{public}"}}"#) + } } } @@ -439,6 +463,7 @@ impl Jwk { JwkParams::Rsa(params) => params.is_private(), JwkParams::Oct(_) => true, JwkParams::Okp(params) => params.is_private(), + JwkParams::Akp(params) => params.is_private(), } } diff --git a/identity_jose/src/jwk/key_params.rs b/identity_jose/src/jwk/key_params.rs index 9d1437637a..613f6a84ba 100644 --- a/identity_jose/src/jwk/key_params.rs +++ b/identity_jose/src/jwk/key_params.rs @@ -1,8 +1,14 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use zeroize::Zeroize; +use super::BlsCurve; +use super::JwkParamsAKP; use crate::error::Error; use crate::error::Result; use crate::jwk::EcCurve; @@ -10,8 +16,6 @@ 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) @@ -28,6 +32,8 @@ pub enum JwkParams { Oct(JwkParamsOct), /// Octet Key Pairs parameters. Okp(JwkParamsOkp), + /// Algorithm Key Pair Type parameters + Akp(JwkParamsAKP), } impl JwkParams { @@ -38,6 +44,7 @@ impl JwkParams { JwkType::Rsa => Self::Rsa(JwkParamsRsa::new()), JwkType::Oct => Self::Oct(JwkParamsOct::new()), JwkType::Okp => Self::Okp(JwkParamsOkp::new()), + JwkType::Akp => Self::Akp(JwkParamsAKP::new()) } } @@ -48,6 +55,7 @@ impl JwkParams { Self::Rsa(inner) => inner.kty(), Self::Oct(inner) => inner.kty(), Self::Okp(inner) => inner.kty(), + Self::Akp(_) => JwkType::Akp } } @@ -60,6 +68,7 @@ impl JwkParams { Self::Ec(inner) => Some(Self::Ec(inner.to_public())), Self::Rsa(inner) => Some(Self::Rsa(inner.to_public())), Self::Oct(_) => None, + Self::Akp(inner) => Some(Self::Akp(inner.to_public())) } } @@ -70,6 +79,7 @@ impl JwkParams { Self::Ec(value) => value.is_public(), Self::Rsa(value) => value.is_public(), Self::Oct(value) => value.is_public(), + Self::Akp(value) => value.is_public() } } } diff --git a/identity_jose/src/jwk/key_type.rs b/identity_jose/src/jwk/key_type.rs index 59cdbaa5cc..bbb4f9ed55 100644 --- a/identity_jose/src/jwk/key_type.rs +++ b/identity_jose/src/jwk/key_type.rs @@ -1,6 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use core::fmt::Display; use core::fmt::Formatter; use core::fmt::Result; @@ -22,6 +26,10 @@ pub enum JwkType { /// Octet string key pairs. #[serde(rename = "OKP")] Okp, + /// Algorithm Key Pair, JSON Web Key Type for the ML-DSA and SLH-DSA Algorithm Family. + /// [More Info] (https://datatracker.ietf.org/doc/html/draft-ietf-cose-dilithium-06#name-algorithm-key-pair-type) + #[serde(rename = "AKP")] + Akp, } impl JwkType { @@ -32,6 +40,7 @@ impl JwkType { Self::Rsa => "RSA", Self::Oct => "oct", Self::Okp => "OKP", + Self::Akp => "AKP", } } } diff --git a/identity_jose/src/jwk/mod.rs b/identity_jose/src/jwk/mod.rs index 780c7f9861..609004c5fb 100644 --- a/identity_jose/src/jwk/mod.rs +++ b/identity_jose/src/jwk/mod.rs @@ -1,21 +1,29 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + //! JSON Web Keys ([JWK](https://tools.ietf.org/html/rfc7517)) mod curve; mod jwk_ext; +mod jwk_akp; mod key; mod key_operation; mod key_params; mod key_set; mod key_type; mod key_use; +mod composite_jwk; pub use self::curve::*; +pub use self::jwk_akp::*; pub use self::key::*; pub use self::key_operation::*; pub use self::key_params::*; pub use self::key_set::*; pub use self::key_type::*; pub use self::key_use::*; +pub use self::composite_jwk::*; diff --git a/identity_jose/src/jws/algorithm.rs b/identity_jose/src/jws/algorithm.rs index 1d6b1c319c..83b8c1e714 100644 --- a/identity_jose/src/jws/algorithm.rs +++ b/identity_jose/src/jws/algorithm.rs @@ -1,6 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use core::fmt::Display; use core::fmt::Formatter; use core::fmt::Result; @@ -44,6 +48,67 @@ pub enum JwsAlgorithm { NONE, /// EdDSA signature algorithms EdDSA, + /// JSON Web Signature Algorithm for ML-DSA-44 + /// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-dilithium#name-the-ml-dsa-algorithm-family) + #[serde(rename = "ML-DSA-44")] + ML_DSA_44, + /// JSON Web Signature Algorithm for ML-DSA-44 + /// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-dilithium#name-the-ml-dsa-algorithm-family) + #[serde(rename = "ML-DSA-65")] + ML_DSA_65, + /// JSON Web Signature Algorithm for ML-DSA-44 + /// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-dilithium#name-the-ml-dsa-algorithm-family) + #[serde(rename = "ML-DSA-87")] + ML_DSA_87, + /// JSON Web Signature Algorithm for SLH-DSA-SHA2-128s + /// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-sphincs-plus#name-the-slh-dsa-algorithm-famil) + #[serde(rename = "SLH-DSA-SHA2-128s")] + SLH_DSA_SHA2_128s, + /// JSON Web Signature Algorithm for SLH-DSA-SHAKE-128s + /// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-sphincs-plus#name-the-slh-dsa-algorithm-famil) + #[serde(rename = "SLH-DSA-SHAKE-128s")] + SLH_DSA_SHAKE_128s, + /// JSON Web Signature Algorithm for SLH-DSA-SHA2-128f + /// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-sphincs-plus#name-the-slh-dsa-algorithm-famil) + #[serde(rename = "SLH-DSA-SHA2-128f")] + SLH_DSA_SHA2_128f, + ///SLH_DSA_SHAKE_128f + #[serde(rename = "SLH-DSA-SHAKE-128f")] + SLH_DSA_SHAKE_128f, + ///SLH_DSA_SHA2_192s + #[serde(rename = "SLH-DSA-SHA2-192s")] + SLH_DSA_SHA2_192s, + ///SLH_DSA_SHAKE_192s + #[serde(rename = "SLH-DSA-SHAKE-192s")] + SLH_DSA_SHAKE_192s, + ///SLH-DSA-SHA2-192f + #[serde(rename = "SLH-DSA-SHA2-192f")] + SLH_DSA_SHA2_192f, + ///SLH-DSA-SHAKE-192f + #[serde(rename = "SLH-DSA-SHAKE-192f")] + SLH_DSA_SHAKE_192f, + ///SLH-DSA-SHA2-256s + #[serde(rename = "SLH-DSA-SHA2-256s")] + SLH_DSA_SHA2_256s, + ///SLH-DSA-SHA2-256s + #[serde(rename = "SLH-DSA-SHAKE-256s")] + SLH_DSA_SHAKE_256s, + ///SLH-DSA-SHA2-256f + #[serde(rename = "SLH-DSA-SHA2-256f")] + SLH_DSA_SHA2_256f, + ///SLH-DSA-SHAKE-256f + #[serde(rename = "SLH-DSA-SHAKE-256f")] + SLH_DSA_SHAKE_256f, + ///FALCON512 + FALCON512, + ///FALCON1024 + FALCON1024, + ///id-MLDSA44-Ed25519 + #[serde(rename = "id-MLDSA44-Ed25519")] + IdMldsa44Ed25519, + ///id-MLDSA65-Ed25519 + #[serde(rename = "id-MLDSA65-Ed25519")] + IdMldsa65Ed25519, /// Custom algorithm #[cfg(feature = "custom_alg")] #[serde(untagged)] @@ -73,6 +138,25 @@ impl JwsAlgorithm { Self::ES256K, Self::NONE, Self::EdDSA, + Self::ML_DSA_44, + Self::ML_DSA_65, + Self::ML_DSA_87, + Self::SLH_DSA_SHA2_128s, + Self::SLH_DSA_SHAKE_128s, + Self::SLH_DSA_SHA2_128f, + Self::SLH_DSA_SHAKE_128f, + Self::SLH_DSA_SHA2_192s, + Self::SLH_DSA_SHAKE_192s, + Self::SLH_DSA_SHA2_192f, + Self::SLH_DSA_SHAKE_192f, + Self::SLH_DSA_SHA2_256s, + Self::SLH_DSA_SHAKE_256s, + Self::SLH_DSA_SHA2_256f, + Self::SLH_DSA_SHAKE_256f, + Self::FALCON512, + Self::FALCON1024, + Self::IdMldsa44Ed25519, + Self::IdMldsa65Ed25519, ]; /// Returns the JWS algorithm as a `str` slice. @@ -94,6 +178,25 @@ impl JwsAlgorithm { Self::ES256K => "ES256K", Self::NONE => "none", Self::EdDSA => "EdDSA", + Self::ML_DSA_44 => "ML-DSA-44", + Self::ML_DSA_65 => "ML-DSA-65", + Self::ML_DSA_87 => "ML-DSA-87", + Self::SLH_DSA_SHA2_128s => "SLH-DSA-SHA2-128s", + Self::SLH_DSA_SHAKE_128s => "SLH-DSA-SHAKE-128s", + Self::SLH_DSA_SHA2_128f => "SLH-DSA-SHA2-128f", + Self::SLH_DSA_SHAKE_128f => "SLH-DSA-SHAKE-128f", + Self::SLH_DSA_SHA2_192s => "SLH-DSA-SHA2-192s", + Self::SLH_DSA_SHAKE_192s => "SLH-DSA-SHAKE-192s", + Self::SLH_DSA_SHA2_192f => "SLH-DSA-SHA2-192f", + Self::SLH_DSA_SHAKE_192f => "SLH-DSA-SHAKE-192f", + Self::SLH_DSA_SHA2_256s => "SLH-DSA-SHA2-256s", + Self::SLH_DSA_SHAKE_256s => "SLH-DSA-SHAKE-256s", + Self::SLH_DSA_SHA2_256f => "SLH-DSA-SHA2-256f", + Self::SLH_DSA_SHAKE_256f => "SLH-DSA-SHAKE-256f", + Self::FALCON512 => "FALCON512", + Self::FALCON1024 => "FALCON1024", + Self::IdMldsa44Ed25519 => "id-MLDSA44-Ed25519", + Self::IdMldsa65Ed25519 => "id-MLDSA65-Ed25519", } } @@ -116,6 +219,25 @@ impl JwsAlgorithm { Self::ES256K => "ES256K".to_string(), Self::NONE => "none".to_string(), Self::EdDSA => "EdDSA".to_string(), + Self::ML_DSA_44 => "ML-DSA-44".to_string(), + Self::ML_DSA_65 => "ML-DSA-65".to_string(), + Self::ML_DSA_87 => "ML-DSA-87".to_string(), + Self::SLH_DSA_SHA2_128s => "SLH-DSA-SHA2-128s".to_string(), + Self::SLH_DSA_SHAKE_128s => "SLH-DSA-SHAKE-128s".to_string(), + Self::SLH_DSA_SHA2_128f => "SLH-DSA-SHA2-128f".to_string(), + Self::SLH_DSA_SHAKE_128f => "SLH-DSA-SHAKE-128f".to_string(), + Self::SLH_DSA_SHA2_192s => "SLH-DSA-SHA2-192s".to_string(), + Self::SLH_DSA_SHAKE_192s => "SLH-DSA-SHAKE-192s".to_string(), + Self::SLH_DSA_SHA2_192f => "SLH-DSA-SHA2-192f".to_string(), + Self::SLH_DSA_SHAKE_192f => "SLH-DSA-SHAKE-192f".to_string(), + Self::SLH_DSA_SHA2_256s => "SLH-DSA-SHA2-256s".to_string(), + Self::SLH_DSA_SHAKE_256s => "SLH-DSA-SHAKE-256s".to_string(), + Self::SLH_DSA_SHA2_256f => "SLH-DSA-SHA2-256f".to_string(), + Self::SLH_DSA_SHAKE_256f => "SLH-DSA-SHAKE-256f".to_string(), + Self::FALCON512 => "FALCON512".to_string(), + Self::FALCON1024 => "FALCON1024".to_string(), + Self::IdMldsa44Ed25519 => "id-MLDSA44-Ed25519".to_string(), + Self::IdMldsa65Ed25519 => "id-MLDSA65-Ed25519".to_string(), Self::Custom(name) => name.clone(), } } @@ -141,6 +263,25 @@ impl FromStr for JwsAlgorithm { "ES256K" => Ok(Self::ES256K), "none" => Ok(Self::NONE), "EdDSA" => Ok(Self::EdDSA), + "ML-DSA-44" => Ok(Self::ML_DSA_44), + "ML-DSA-65" => Ok(Self::ML_DSA_65), + "ML-DSA-87" => Ok(Self::ML_DSA_87), + "SLH-DSA-SHA2-128s" => Ok(Self::SLH_DSA_SHA2_128s), + "SLH-DSA-SHAKE-128s" => Ok(Self::SLH_DSA_SHAKE_128s), + "SLH-DSA-SHA2-128f" => Ok(Self::SLH_DSA_SHA2_128f), + "SLH-DSA-SHAKE-128f" => Ok(Self::SLH_DSA_SHAKE_128f), + "SLH-DSA-SHA2-192s" => Ok(Self::SLH_DSA_SHA2_192s), + "SLH-DSA-SHAKE-192s" => Ok(Self::SLH_DSA_SHAKE_192s), + "SLH-DSA-SHA2-192f" => Ok(Self::SLH_DSA_SHA2_192f), + "SLH-DSA-SHAKE-192f" => Ok(Self::SLH_DSA_SHAKE_192f), + "SLH-DSA-SHA2-256s" => Ok(Self::SLH_DSA_SHA2_256s), + "SLH-DSA-SHAKE-256s" => Ok(Self::SLH_DSA_SHAKE_256s), + "SLH-DSA-SHA2-256f" => Ok(Self::SLH_DSA_SHA2_256f), + "SLH-DSA-SHAKE-256f" => Ok(Self::SLH_DSA_SHAKE_256f), + "FALCON512" => Ok(Self::FALCON512), + "FALCON1024" => Ok(Self::FALCON1024), + "id-MLDSA44-Ed25519" => Ok(Self::IdMldsa44Ed25519), + "id-MLDSA65-Ed25519" => Ok(Self::IdMldsa65Ed25519), #[cfg(feature = "custom_alg")] value => Ok(Self::Custom(value.to_string())), #[cfg(not(feature = "custom_alg"))] diff --git a/identity_jose/src/jws/decoder.rs b/identity_jose/src/jws/decoder.rs index c1635c86d3..6db2e9a59e 100644 --- a/identity_jose/src/jws/decoder.rs +++ b/identity_jose/src/jws/decoder.rs @@ -1,6 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use core::str; use std::borrow::Cow; @@ -175,6 +179,114 @@ impl<'a> JwsValidationItem<'a> { claims, }) } + + ///Hybrid signature verify + pub fn verify_hybrid( + self, + traditional_verifier: &TRV, + pq_verifier: &PQV, + traditional_pk: &Jwk, + pq_pk: &Jwk, + ) -> Result> + where + TRV: JwsVerifier, + PQV: JwsVerifier, + { + // Destructure data + let JwsValidationItem { + headers, + claims, + signing_input, + decoded_signature, + } = self; + + let (protected, unprotected): (JwsHeader, Option>) = match headers { + DecodedHeaders::Protected(protected) => (protected, None), + DecodedHeaders::Both { protected, unprotected } => (protected, Some(unprotected)), + DecodedHeaders::Unprotected(_) => return Err(Error::MissingHeader("missing protected header")), + }; + + // Extract and validate alg from the protected header. + let alg: JwsAlgorithm = protected.alg().ok_or(Error::ProtectedHeaderWithoutAlg)?; + + // M' = Prefix || Domain || len(ctx) || ctx || M + let (t_alg, pq_alg, signing_input, traditional_signature_len) = match alg { + JwsAlgorithm::IdMldsa44Ed25519 => { + //Prefix: CompositeAlgorithmSignatures2025 + let mut input = b"CompositeAlgorithmSignatures2025".to_vec(); + + //Domain: id-MLDSA44-Ed25519 + input.extend_from_slice(&[0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x08, 0x01, 0x3E]); + + //len(ctx) = 0 + input.push(0x00); + + //M + input.extend(signing_input); + ( + JwsAlgorithm::EdDSA, + JwsAlgorithm::IdMldsa44Ed25519, + input, + crypto::signatures::ed25519::Signature::LENGTH, + ) + } + JwsAlgorithm::IdMldsa65Ed25519 => { + //Prefix: CompositeAlgorithmSignatures2025 + let mut input = b"CompositeAlgorithmSignatures2025".to_vec(); + + //Domain: id-MLDSA65-Ed25519 + input.extend_from_slice(&[0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x08, 0x01, 0x47]); + + //len(ctx) = 0 + input.push(0x00); + + //M + input.extend(signing_input); + + ( + JwsAlgorithm::EdDSA, + JwsAlgorithm::IdMldsa65Ed25519, + input, + crypto::signatures::ed25519::Signature::LENGTH, + ) + } + _ => return Err(Error::JwsAlgorithmParsingError), + }; + + traditional_pk.check_alg(t_alg.name())?; + + let extracted_signature_t = &decoded_signature[..traditional_signature_len]; + let extracted_signature_pq = &decoded_signature[traditional_signature_len..]; + + // Construct verification input + let input1 = VerificationInput { + alg: t_alg, + signing_input: signing_input.clone().into(), + decoded_signature: extracted_signature_t.into(), + }; + + // Call the traditional verifier + traditional_verifier + .verify(input1, traditional_pk) + .map_err(Error::SignatureVerificationError)?; + + let input2 = VerificationInput { + alg: pq_alg, + signing_input: signing_input.into(), + decoded_signature: extracted_signature_pq.into(), + }; + + // Call the PQ verifier + pq_verifier + .verify(input2, pq_pk) + .map_err(Error::SignatureVerificationError)?; + + Ok(DecodedJws { + protected, + unprotected, + claims, + }) + } } // ============================================================================================= diff --git a/identity_pqc_verifier/Cargo.toml b/identity_pqc_verifier/Cargo.toml new file mode 100644 index 0000000000..d21f520ece --- /dev/null +++ b/identity_pqc_verifier/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "identity_pqc_verifier" +version = "1.5.1" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords = ["iota", "identity", "jose", "jwk", "jws"] +license.workspace = true +readme = "./README.md" +repository.workspace = true + +description = "JWS PQC signature verification for IOTA Identity" + +[dependencies] +identity_jose = { version = "=1.5.1", path = "../identity_jose", default-features = true } +oqs.workspace = true + +[features] +ML_DSA_44 = [] +ML_DSA_65 = [] +ML_DSA_87 = [] +SLH_DSA_SHA2_128s = [] +SLH_DSA_SHA2_128f = [] +SLH_DSA_SHAKE_128s = [] + +SLH_DSA_SHAKE_128f = [] +SLH_DSA_SHA2_192s = [] +SLH_DSA_SHAKE_192s = [] +SLH_DSA_SHA2_192f = [] +SLH_DSA_SHAKE_192f = [] +SLH_DSA_SHA2_256s = [] +SLH_DSA_SHAKE_256s = [] +SLH_DSA_SHA2_256f = [] +SLH_DSA_SHAKE_256f = [] + +FALCON512 = [] +FALCON1024 = [] + +default = [ + "ML_DSA_44", + "ML_DSA_65", + "ML_DSA_87", + "SLH_DSA_SHA2_128s", + "SLH_DSA_SHA2_128f", + "SLH_DSA_SHAKE_128s", + + "SLH_DSA_SHAKE_128f", + "SLH_DSA_SHA2_192s", + "SLH_DSA_SHAKE_192s", + "SLH_DSA_SHA2_192f", + "SLH_DSA_SHAKE_192f", + "SLH_DSA_SHA2_256s", + "SLH_DSA_SHAKE_256s", + "SLH_DSA_SHA2_256f", + "SLH_DSA_SHAKE_256f", + + "FALCON512", + "FALCON1024", + ] diff --git a/identity_pqc_verifier/README.md b/identity_pqc_verifier/README.md new file mode 100644 index 0000000000..676bfc724c --- /dev/null +++ b/identity_pqc_verifier/README.md @@ -0,0 +1,4 @@ +IOTA Identity - PQC Verifier +=== + +This crate implements a `JwsVerifier` capable of verifying Post-Quantum (PQ) signatures, based on liboqs PQ signatures implementation. \ No newline at end of file diff --git a/identity_pqc_verifier/src/lib.rs b/identity_pqc_verifier/src/lib.rs new file mode 100644 index 0000000000..a4cbc5cbed --- /dev/null +++ b/identity_pqc_verifier/src/lib.rs @@ -0,0 +1,8 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +mod oqs_verifier; +mod pqc_verifier; + +pub use oqs_verifier::*; +pub use pqc_verifier::*; diff --git a/identity_pqc_verifier/src/oqs_verifier.rs b/identity_pqc_verifier/src/oqs_verifier.rs new file mode 100644 index 0000000000..337aec83d2 --- /dev/null +++ b/identity_pqc_verifier/src/oqs_verifier.rs @@ -0,0 +1,124 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_jose::jwk::Jwk; +use identity_jose::jwk::JwkParamsAKP; +use identity_jose::jws::SignatureVerificationError; +use identity_jose::jws::SignatureVerificationErrorKind; +use identity_jose::jws::VerificationInput; +use oqs::sig::Algorithm; +use oqs::sig::Sig; +use std::ops::Deref; + +/// A verifier that can handle the [`Algorithm`] PQC algorithms. +#[derive(Debug)] +#[non_exhaustive] +pub struct OQSVerifier; + +impl OQSVerifier { + /// Verify a JWS signature secured with the on the [`Algorithm`] defined in liboqs. + pub fn verify(input: VerificationInput, public_key: &Jwk, alg: Algorithm) -> Result<(), SignatureVerificationError> { + + let params: &JwkParamsAKP = public_key + .try_akp_params() + .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; + + let pk = identity_jose::jwu::decode_b64(params.public.as_str()).map_err(|_| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure) + .with_custom_message("could not decode 'pub' parameter from jwk") + })?; + + oqs::init(); + + let scheme = Sig::new(alg).map_err(|_| { + SignatureVerificationError::new(SignatureVerificationErrorKind::Unspecified) + .with_custom_message("signature scheme init failed") + })?; + + let public_key = scheme + .public_key_from_bytes(&pk) + .ok_or(SignatureVerificationError::new( + SignatureVerificationErrorKind::KeyDecodingFailure, + ))?; + + let signature = scheme + .signature_from_bytes(input.decoded_signature.deref()) + .ok_or(SignatureVerificationErrorKind::InvalidSignature)?; + + Ok( + scheme + .verify(&input.signing_input, signature, public_key) + .map_err(|_| SignatureVerificationErrorKind::InvalidSignature)?, + ) + } + + /// Verify a JWS signature signed with a ctx and secured with the on the [`Algorithm`] defined in liboqs, used in hybrid signature. + /// The ctx value is set as the Domain separator value for binding the signature to the Composite OID + pub fn verify_hybrid_signature(input: VerificationInput, public_key: &Jwk, alg: Algorithm) -> Result<(), SignatureVerificationError> { + + let params: &JwkParamsAKP = public_key + .try_akp_params() + .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; + + let pk = identity_jose::jwu::decode_b64(params.public.as_str()).map_err(|_| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure) + .with_custom_message("could not decode 'pub' parameter from jwk") + })?; + + oqs::init(); + + let scheme = Sig::new(alg).map_err(|_| { + SignatureVerificationError::new(SignatureVerificationErrorKind::Unspecified) + .with_custom_message("signature scheme init failed") + })?; + + let public_key = scheme + .public_key_from_bytes(&pk) + .ok_or(SignatureVerificationError::new( + SignatureVerificationErrorKind::KeyDecodingFailure, + ))?; + + let signature = scheme + .signature_from_bytes(input.decoded_signature.deref()) + .ok_or(SignatureVerificationErrorKind::InvalidSignature)?; + + let ctx = match alg { + Algorithm::MlDsa44 => &[0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x08, 0x01, 0x3E], + Algorithm::MlDsa65 => &[0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x08, 0x01, 0x47], + _ => return Err(SignatureVerificationError::new(SignatureVerificationErrorKind::UnsupportedKeyType)), + }; + + Ok( + scheme + .verify_with_ctx_str(&input.signing_input, signature, ctx, public_key) + .map_err(|_| SignatureVerificationErrorKind::InvalidSignature)?, + ) + } +} + +#[cfg(test)] +mod tests { + use oqs::sig::{Algorithm, Sig}; + + #[test] + fn test_sig_and_verify(){ + oqs::init(); + let scheme = Sig::new(Algorithm::MlDsa44).unwrap(); + let (pk, sk) = scheme.keypair().unwrap(); + let message = b"test_message"; + let signature = scheme.sign(message, &sk).unwrap(); + assert!(scheme.verify(message, &signature, &pk).is_ok()); + } + + #[test] + fn test_sig_and_invalid_verify(){ + oqs::init(); + let scheme = Sig::new(Algorithm::MlDsa87).unwrap(); + let (pk, sk) = scheme.keypair().unwrap(); + let message = b"test_message"; + let wrong_message = b"wrong_message"; + let signature = scheme.sign(message, &sk).unwrap(); + assert!(scheme.verify(wrong_message, &signature, &pk).is_err()); + } +} + diff --git a/identity_pqc_verifier/src/pqc_verifier.rs b/identity_pqc_verifier/src/pqc_verifier.rs new file mode 100644 index 0000000000..72a4a9d9bd --- /dev/null +++ b/identity_pqc_verifier/src/pqc_verifier.rs @@ -0,0 +1,85 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_jose::jwk::Jwk; +use identity_jose::jws::JwsAlgorithm; +use identity_jose::jws::JwsVerifier; +use identity_jose::jws::SignatureVerificationError; +use identity_jose::jws::SignatureVerificationErrorKind; +use identity_jose::jws::VerificationInput; +use oqs::sig::Algorithm; + +use crate::OQSVerifier; + +/// An implementor of [`JwsVerifier`] that can handle the +/// [`JwsAlgorithm::ML_DSA_44`](identity_jose::jws::JwsAlgorithm::ML_DSA_44) +/// | [`JwsAlgorithm::ML_DSA_65`](identity_jose::jws::JwsAlgorithm::ML_DSA_65) +/// | [`JwsAlgorithm::ML_DSA_87`](identity_jose::jws::JwsAlgorithm::ML_DSA_87) algorithms. +#[derive(Debug)] +#[non_exhaustive] +pub struct PQCJwsVerifier; + +impl Default for PQCJwsVerifier { + /// Constructs an [`MLDSAJwsVerifier`]. This is the only way to obtain an [`MLDSAJwsVerifier`]. + fn default() -> Self { + Self + } +} + +impl JwsVerifier for PQCJwsVerifier { + /// This implements verification of JWS signatures signed with the + /// [`JwsAlgorithm::ML_DSA_44`](identity_jose::jws::JwsAlgorithm::ML_DSA_44) + /// | [`JwsAlgorithm::ML_DSA_65`](identity_jose::jws::JwsAlgorithm::ML_DSA_65) + /// | [`JwsAlgorithm::ML_DSA_87`](identity_jose::jws::JwsAlgorithm::ML_DSA_87) + /// | [`JwsAlgorithm::SLH_DSA_SHA2_128s`](identity_jose::jws::JwsAlgorithm::SLH_DSA_SHA2_128s) + /// | [`JwsAlgorithm::SLH_DSA_SHA2_128f`](identity_jose::jws::JwsAlgorithm::SLH_DSA_SHA2_128f) + /// | [`JwsAlgorithm::SLH_DSA_SHAKE_128s`](identity_jose::jws::JwsAlgorithm::SLH_DSA_SHAKE_128s) algorithms. + // Allow unused variables in case of no-default-features. + #[allow(unused_variables)] + fn verify(&self, input: VerificationInput, public_key: &Jwk) -> std::result::Result<(), SignatureVerificationError> { + match input.alg { + #[cfg(feature = "ML_DSA_44")] + JwsAlgorithm::ML_DSA_44 => OQSVerifier::verify(input, public_key, Algorithm::MlDsa44), + #[cfg(feature = "ML_DSA_65")] + JwsAlgorithm::ML_DSA_65 => OQSVerifier::verify(input, public_key, Algorithm::MlDsa65), + #[cfg(feature = "ML_DSA_87")] + JwsAlgorithm::ML_DSA_87 => OQSVerifier::verify(input, public_key, Algorithm::MlDsa87), + #[cfg(feature = "ML_DSA_44")] + JwsAlgorithm::IdMldsa44Ed25519 => OQSVerifier::verify_hybrid_signature(input, public_key, Algorithm::MlDsa44), + #[cfg(feature = "ML_DSA_65")] + JwsAlgorithm::IdMldsa65Ed25519 => OQSVerifier::verify_hybrid_signature(input, public_key, Algorithm::MlDsa65), + + #[cfg(feature = "SLH_DSA_SHA2_128s")] + JwsAlgorithm::SLH_DSA_SHA2_128s => OQSVerifier::verify(input, public_key, Algorithm::SphincsSha2128sSimple), + #[cfg(feature = "SLH_DSA_SHAKE_128s")] + JwsAlgorithm::SLH_DSA_SHAKE_128s => OQSVerifier::verify(input, public_key, Algorithm::SphincsShake128sSimple), + #[cfg(feature = "SLH_DSA_SHA2_128f")] + JwsAlgorithm::SLH_DSA_SHA2_128f => OQSVerifier::verify(input, public_key, Algorithm::SphincsSha2128fSimple), + + #[cfg(feature = "SLH_DSA_SHAKE_128f")] + JwsAlgorithm::SLH_DSA_SHAKE_128f => OQSVerifier::verify(input, public_key, Algorithm::SphincsShake128fSimple), + #[cfg(feature = "SLH_DSA_SHA2_192s")] + JwsAlgorithm::SLH_DSA_SHA2_192s => OQSVerifier::verify(input, public_key, Algorithm::SphincsSha2192sSimple), + #[cfg(feature = "SLH_DSA_SHAKE_192s")] + JwsAlgorithm::SLH_DSA_SHAKE_192s => OQSVerifier::verify(input, public_key, Algorithm::SphincsShake192sSimple), + #[cfg(feature = "SLH_DSA_SHA2_192f")] + JwsAlgorithm::SLH_DSA_SHA2_192f => OQSVerifier::verify(input, public_key, Algorithm::SphincsSha2192fSimple), + #[cfg(feature = "SLH_DSA_SHAKE_192f")] + JwsAlgorithm::SLH_DSA_SHAKE_192f => OQSVerifier::verify(input, public_key, Algorithm::SphincsShake192fSimple), + #[cfg(feature = "SLH_DSA_SHA2_256s")] + JwsAlgorithm::SLH_DSA_SHA2_256s => OQSVerifier::verify(input, public_key, Algorithm::SphincsSha2256sSimple), + #[cfg(feature = "SLH_DSA_SHAKE_256s")] + JwsAlgorithm::SLH_DSA_SHAKE_256s => OQSVerifier::verify(input, public_key, Algorithm::SphincsShake256sSimple), + #[cfg(feature = "SLH_DSA_SHA2_256f")] + JwsAlgorithm::SLH_DSA_SHA2_256f => OQSVerifier::verify(input, public_key, Algorithm::SphincsSha2256fSimple), + #[cfg(feature = "SLH_DSA_SHAKE_256f")] + JwsAlgorithm::SLH_DSA_SHAKE_256f => OQSVerifier::verify(input, public_key, Algorithm::SphincsShake256fSimple), + + #[cfg(feature = "FALCON512")] + JwsAlgorithm::FALCON512 => OQSVerifier::verify(input, public_key, Algorithm::Falcon512), + #[cfg(feature = "FALCON1024")] + JwsAlgorithm::FALCON1024 => OQSVerifier::verify(input, public_key, Algorithm::Falcon1024), + _ => Err(SignatureVerificationErrorKind::UnsupportedAlg.into()), + } + } +} diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index 228a65582b..5e4bdf2fbb 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -1,9 +1,14 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use core::future::Future; use futures::stream::FuturesUnordered; use futures::TryStreamExt; +use identity_did::DIDCompositeJwk; use identity_did::DIDJwk; use identity_did::DID; use std::collections::HashSet; @@ -264,6 +269,22 @@ impl + 'static> Resolver> { } } +impl + 'static> Resolver> { + /// Attaches a handler capable of resolving `did:compositejwk` DIDs. + pub fn attach_did_compositejwk_handler(&mut self) { + let handler = |did_compositejwk: DIDCompositeJwk| async move { CoreDocument::expand_did_compositejwk(did_compositejwk) }; + self.attach_handler(DIDCompositeJwk::METHOD.to_string(), handler) + } +} + +impl + 'static> Resolver> { + /// Attaches a handler capable of resolving `did:compositejwk` DIDs. + pub fn attach_did_compositejwk_handler(&mut self) { + let handler = |did_compositejwk: DIDCompositeJwk| async move { CoreDocument::expand_did_compositejwk(did_compositejwk) }; + self.attach_handler(DIDCompositeJwk::METHOD.to_string(), handler) + } +} + #[cfg(feature = "iota")] mod iota_handler { use crate::ErrorCause; diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index e1a7e01a99..4944e89f4d 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -30,7 +30,7 @@ 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 } - +oqs = { workspace = true, optional = true } [dev-dependencies] identity_credential = { version = "=1.5.1", path = "../identity_credential", features = ["revocation-bitmap"] } identity_eddsa_verifier = { version = "=1.5.1", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } @@ -53,5 +53,11 @@ jpt-bbs-plus = [ "dep:json-proof-token", ] +# Enables PQC (JwkStoragePQ implementation needed) +pqc = [] +pqc-liboqs = ["pqc", "memstore", "dep:oqs"] +hybrid = ["pqc", "dep:iota-crypto"] +hybrid-liboqs = ["hybrid", "pqc-liboqs"] + [lints] workspace = true diff --git a/identity_storage/src/key_id_storage/method_digest.rs b/identity_storage/src/key_id_storage/method_digest.rs index c6eb4fd59d..262843d5d7 100644 --- a/identity_storage/src/key_id_storage/method_digest.rs +++ b/identity_storage/src/key_id_storage/method_digest.rs @@ -1,6 +1,11 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + +use identity_core::convert::ToJson; use identity_verification::MethodData; use identity_verification::VerificationMethod; use seahash::SeaHasher; @@ -56,6 +61,15 @@ impl MethodDigest { match method_data { MethodData::PublicKeyJwk(jwk) => hasher.write(jwk.thumbprint_sha256().as_ref()), + MethodData::CompositeJwk(composite) => { + let algid = composite + .alg_id() + .to_json_vec() + .map_err(|err| MethodDigestConstructionError::new(DataDecodingFailure).with_source(err))?; + hasher.write(&algid); + hasher.write(composite.traditional_public_key().thumbprint_sha256().as_ref()); + hasher.write(composite.pq_public_key().thumbprint_sha256().as_ref()); + } _ => hasher.write( &method_data .try_decode() diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs index 2a3b38a0a7..ed0281d3bf 100644 --- a/identity_storage/src/key_storage/bls.rs +++ b/identity_storage/src/key_storage/bls.rs @@ -31,8 +31,8 @@ where /// 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::(), + ProofAlgorithm::BBS => random_bbs_keypair::(), + ProofAlgorithm::BBS_SHAKE256 => random_bbs_keypair::(), _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), } .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)) @@ -130,8 +130,8 @@ pub fn sign_bbs( header: &[u8], ) -> KeyStorageResult> { match alg { - ProofAlgorithm::BLS12381_SHA256 => _sign_bbs::(data, sk, pk, header), - ProofAlgorithm::BLS12381_SHAKE256 => _sign_bbs::(data, sk, pk, header), + ProofAlgorithm::BBS => _sign_bbs::(data, sk, pk, header), + ProofAlgorithm::BBS_SHAKE256 => _sign_bbs::(data, sk, pk, header), _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), } .map_err(|e| { @@ -188,8 +188,8 @@ pub fn update_bbs_signature( KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid signature size".to_owned()) })?; match alg { - ProofAlgorithm::BLS12381_SHA256 => _update_bbs_signature::(exact_size_signature, sk, update_ctx), - ProofAlgorithm::BLS12381_SHAKE256 => { + ProofAlgorithm::BBS => _update_bbs_signature::(exact_size_signature, sk, update_ctx), + ProofAlgorithm::BBS_SHAKE256 => { _update_bbs_signature::(exact_size_signature, sk, update_ctx) } _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), diff --git a/identity_storage/src/key_storage/jwk_storage_pqc.rs b/identity_storage/src/key_storage/jwk_storage_pqc.rs new file mode 100644 index 0000000000..a058e35c58 --- /dev/null +++ b/identity_storage/src/key_storage/jwk_storage_pqc.rs @@ -0,0 +1,23 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use crate::key_storage::KeyId; +use crate::key_storage::KeyType; +use async_trait::async_trait; +use identity_verification::jose::jwk::Jwk; +use identity_verification::jose::jws::JwsAlgorithm; + +use super::jwk_gen_output::JwkGenOutput; +use super::JwkStorage; +use super::KeyStorageResult; + +/// Extension to the JwkStorage to handle post-quantum keys +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait JwkStoragePQ: JwkStorage { + /// Generates a JWK representing a PQ key + async fn generate_pq_key(&self, key_type: KeyType, alg: JwsAlgorithm) -> KeyStorageResult; + + /// Sign the provided `data` using a PQ algorithm, ctx is optional for the ctx paramter of the algorithm ML-DSA + async fn pq_sign(&self, key_id: &KeyId, data: &[u8], public_key: &Jwk, ctx: Option<&[u8]>) -> KeyStorageResult>; +} \ No newline at end of file diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index 2f51be97ce..f7034a4486 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -1,6 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use core::fmt::Debug; use std::collections::HashMap; use std::fmt::Display; @@ -201,6 +205,11 @@ impl JwkMemStore { 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); + + const PQ_KEY_TYPE_STR: &'static str = "AKP"; + /// ML-DSA algorithms key types; + pub const PQ_KEY_TYPE: KeyType = KeyType::from_static_str(Self::PQ_KEY_TYPE_STR); + } impl MemStoreKeyType { @@ -301,6 +310,247 @@ fn check_key_alg_compatibility(key_type: MemStoreKeyType, alg: &JwsAlgorithm) -> } } +#[cfg(feature = "pqc-liboqs")] +mod pqc_liboqs { + use std::str::FromStr; + use async_trait::async_trait; + use identity_verification::jose::jwk::Jwk; + use identity_verification::jose::jwk::JwkType; + use identity_verification::jose::jws::JwsAlgorithm; + use identity_verification::jwk::JwkParams; + use identity_verification::jwu; + use oqs::sig::Algorithm; + use oqs::sig::Sig; + use tokio::sync::RwLockReadGuard; + use tokio::sync::RwLockWriteGuard; + + use super::random_key_id; + use super::JwkKeyStore; + use super::JwkMemStore; + use super::KeyId; + use super::KeyStorageError; + use super::KeyStorageErrorKind; + use super::KeyStorageResult; + use super::KeyType; + use crate::key_storage::jwk_storage_pqc::JwkStoragePQ; + use crate::JwkGenOutput; + + fn check_pq_alg_compatibility(alg: &JwsAlgorithm) -> KeyStorageResult { + match alg { + JwsAlgorithm::ML_DSA_44 => Ok(Algorithm::MlDsa44), + JwsAlgorithm::ML_DSA_65 => Ok(Algorithm::MlDsa65), + JwsAlgorithm::ML_DSA_87 => Ok(Algorithm::MlDsa87), + JwsAlgorithm::SLH_DSA_SHA2_128s => Ok(Algorithm::SphincsSha2128sSimple), + JwsAlgorithm::SLH_DSA_SHAKE_128s => Ok(Algorithm::SphincsShake128sSimple), + JwsAlgorithm::SLH_DSA_SHA2_128f => Ok(Algorithm::SphincsSha2128fSimple), + + JwsAlgorithm::SLH_DSA_SHAKE_128f => Ok(Algorithm::SphincsShake128fSimple), + JwsAlgorithm::SLH_DSA_SHA2_192s => Ok(Algorithm::SphincsSha2192sSimple), + JwsAlgorithm::SLH_DSA_SHAKE_192s => Ok(Algorithm::SphincsShake192sSimple), + JwsAlgorithm::SLH_DSA_SHA2_192f => Ok(Algorithm::SphincsSha2192fSimple), + JwsAlgorithm::SLH_DSA_SHAKE_192f => Ok(Algorithm::SphincsShake192fSimple), + JwsAlgorithm::SLH_DSA_SHA2_256s => Ok(Algorithm::SphincsSha2256sSimple), + JwsAlgorithm::SLH_DSA_SHAKE_256s => Ok(Algorithm::SphincsShake256sSimple), + JwsAlgorithm::SLH_DSA_SHA2_256f => Ok(Algorithm::SphincsSha2256fSimple), + JwsAlgorithm::SLH_DSA_SHAKE_256f => Ok(Algorithm::SphincsShake256fSimple), + + JwsAlgorithm::FALCON512 => Ok(Algorithm::Falcon512), + JwsAlgorithm::FALCON1024 => Ok(Algorithm::Falcon1024), + other => { + Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedSignatureAlgorithm) + .with_custom_message(format!("{other} is not supported")), + ) + } + } + } + + /// JwkStoragePQ implementation for JwkMemStore based on liboqs + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl JwkStoragePQ for JwkMemStore { + async fn generate_pq_key(&self, key_type: KeyType, alg: JwsAlgorithm) -> KeyStorageResult { + if key_type != JwkMemStore::PQ_KEY_TYPE + { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("unsupported key type {key_type}")), + ); + } + + let oqs_alg = check_pq_alg_compatibility(&alg)?; + oqs::init(); + + let scheme = Sig::new(oqs_alg).map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("signature scheme init failed".to_string()) + .with_source(err) + })?; + let (pk, sk) = scheme.keypair().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("keypair generation failed!".to_string()) + .with_source(err) + })?; + + let kid: KeyId = random_key_id(); + + let public = jwu::encode_b64(pk.into_vec()); + let private = jwu::encode_b64(sk.into_vec()); + + let mut jwk_params = match alg { + JwsAlgorithm::ML_DSA_44 => JwkParams::new(JwkType::Akp), + JwsAlgorithm::ML_DSA_65 => JwkParams::new(JwkType::Akp), + JwsAlgorithm::ML_DSA_87 => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHA2_128s => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHAKE_128s => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHA2_128f => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHAKE_128f => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHA2_192s => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHAKE_192s => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHA2_192f => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHAKE_192f => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHA2_256s => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHAKE_256s => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHA2_256f => JwkParams::new(JwkType::Akp), + JwsAlgorithm::SLH_DSA_SHAKE_256f => JwkParams::new(JwkType::Akp), + JwsAlgorithm::FALCON512 => JwkParams::new(JwkType::Akp), + JwsAlgorithm::FALCON1024 => JwkParams::new(JwkType::Akp), + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedSignatureAlgorithm) + .with_custom_message(format!("{other} is not supported")), + ); + } + }; + + match jwk_params { + JwkParams::Akp(ref mut params) => { + params.public = public; + params.private = Some(private); + } + _ => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_custom_message("Should NOT happen!"), + ) + } + } + + let mut jwk = Jwk::from_params(jwk_params); + + jwk.set_alg(alg.name()); + 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 pq_sign(&self, key_id: &KeyId, data: &[u8], public_key: &Jwk, ctx: Option<&[u8]>) -> 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::UnsupportedSignatureAlgorithm) + .and_then(|alg_str| { + JwsAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedSignatureAlgorithm) + })?; + + let oqs_alg = check_pq_alg_compatibility(&alg)?; + + // Check that `kty` is `AKP`. + match alg { + JwsAlgorithm::ML_DSA_44 + | JwsAlgorithm::ML_DSA_65 + | JwsAlgorithm::ML_DSA_87 + | JwsAlgorithm::SLH_DSA_SHA2_128s + | JwsAlgorithm::SLH_DSA_SHAKE_128s + | JwsAlgorithm::SLH_DSA_SHA2_128f + | JwsAlgorithm::SLH_DSA_SHAKE_128f + | JwsAlgorithm::SLH_DSA_SHA2_192s + | JwsAlgorithm::SLH_DSA_SHAKE_192s + | JwsAlgorithm::SLH_DSA_SHA2_192f + | JwsAlgorithm::SLH_DSA_SHAKE_192f + | JwsAlgorithm::SLH_DSA_SHA2_256s + | JwsAlgorithm::SLH_DSA_SHAKE_256s + | JwsAlgorithm::SLH_DSA_SHA2_256f + | JwsAlgorithm::SLH_DSA_SHAKE_256f + | JwsAlgorithm::FALCON512 + | JwsAlgorithm::FALCON1024 => public_key.try_akp_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message(format!("expected a Jwk with AKP params in order to sign with {alg}")) + .with_source(err) + })?, + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedSignatureAlgorithm) + .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_akp_params().unwrap(); + + let sk_bytes = params + .private + .as_deref() + .map(jwu::decode_b64) + .ok_or_else(|| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("expected Jwk `private` param to be present") + })? + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("unable to decode `private` param") + .with_source(err) + })?; + oqs::init(); + + let scheme = Sig::new(oqs_alg).map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("signature scheme init failed".to_string()) + .with_source(err) + })?; + + let secret_key = scheme.secret_key_from_bytes(&sk_bytes).ok_or( + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("invalid private key".to_string()), + )?; + + let signature: oqs::sig::Signature; + + if let Some(ctx) = ctx { + if !scheme.has_ctx_str_support() { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("signature with ctx is not supported with this algorithm".to_string()) + ); + } + signature = scheme.sign_with_ctx_str(data, ctx, secret_key).map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("signature computation failed".to_string()) + .with_source(err) + })?; + } else { + signature = scheme.sign(data, secret_key).map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("signature computation failed".to_string()) + .with_source(err) + })?; + } + + Ok(signature.into_vec()) + } + } +} + #[cfg(feature = "jpt-bbs-plus")] mod bbs_plus_impl { use std::str::FromStr as _; diff --git a/identity_storage/src/key_storage/mod.rs b/identity_storage/src/key_storage/mod.rs index f54f9d5233..90cd700ecb 100644 --- a/identity_storage/src/key_storage/mod.rs +++ b/identity_storage/src/key_storage/mod.rs @@ -1,6 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + //! A Key Storage is used to securely store private keys. //! //! This module provides the [`JwkStorage`] trait that @@ -15,6 +19,8 @@ mod jwk_gen_output; mod jwk_storage; #[cfg(feature = "jpt-bbs-plus")] mod jwk_storage_bbs_plus_ext; +#[cfg(feature = "pqc")] +mod jwk_storage_pqc; mod key_id; mod key_storage_error; mod key_type; @@ -30,6 +36,8 @@ pub mod public_modules { pub use super::jwk_storage::*; #[cfg(feature = "jpt-bbs-plus")] pub use super::jwk_storage_bbs_plus_ext::*; + #[cfg(feature = "pqc")] + pub use super::jwk_storage_pqc::*; pub use super::key_id::*; pub use super::key_storage_error::*; pub use super::key_type::*; diff --git a/identity_storage/src/storage/did_jwk_document_ext.rs b/identity_storage/src/storage/did_jwk_document_ext.rs new file mode 100644 index 0000000000..b5010baaec --- /dev/null +++ b/identity_storage/src/storage/did_jwk_document_ext.rs @@ -0,0 +1,261 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_did::DIDJwk; +use identity_document::document::CoreDocument; +use identity_verification::{jws::JwsAlgorithm, jwu::encode_b64_json}; +use async_trait::async_trait; +use crate::{JwkGenOutput, JwkStorage, JwkStorageDocumentError as Error, KeyIdStorage, KeyType, MethodDigest}; +#[cfg(feature = "pqc")] +use crate::JwkStoragePQ; +#[cfg(feature = "jpt-bbs-plus")] +use crate::JwkStorageBbsPlusExt; +#[cfg(feature = "jpt-bbs-plus")] +use jsonprooftoken::jpa::algs::ProofAlgorithm; +#[cfg(feature = "hybrid")] +use identity_verification::jwk::{CompositeAlgId, CompositeJwk}; +#[cfg(feature = "hybrid")] +use identity_did::DIDCompositeJwk; +#[cfg(feature = "hybrid")] +use crate::KeyId; + +use super::{Storage, StorageResult}; + +/// Extension trait for creating JWK-based DID documents for traditional, zk, PQ and hybrid keys +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait DidJwkDocumentExt{ + /// Create a JWK-based DID documents with traditional keys. Returns the DID document and the fragment + async fn new_did_jwk( + storage: &Storage, + key_type: KeyType, + alg: JwsAlgorithm, + ) -> StorageResult<(CoreDocument, String)> + where + K: JwkStorage, + I: KeyIdStorage; + /// Create a JWK-based DID documents with PQ keys. Returns the DID document and the fragment + #[cfg(feature = "pqc")] + async fn new_did_jwk_pqc( + storage: &Storage, + key_type: KeyType, + alg: JwsAlgorithm, + ) -> StorageResult<(CoreDocument, String)> + where + K: JwkStoragePQ, + I: KeyIdStorage; + /// Create a JWK-based DID documents with zk keys. Returns the DID document and the fragment + #[cfg(feature = "jpt-bbs-plus")] + async fn new_did_jwk_zk( + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + ) -> StorageResult<(CoreDocument, String)> + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage; + + /// Create a JWK-based DID documents with hybrid keys. Returns the DID document and the fragment + #[cfg(feature = "hybrid")] + async fn new_did_compositejwk( + storage: &Storage, + alg: identity_verification::jwk::CompositeAlgId, + ) -> StorageResult<(CoreDocument, String)> + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage; +} + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl DidJwkDocumentExt for CoreDocument { + async fn new_did_jwk( + storage: &Storage, + key_type: KeyType, + alg: JwsAlgorithm, + ) -> StorageResult<(CoreDocument, String)> + where + K: JwkStorage, + I: KeyIdStorage + { + let JwkGenOutput { key_id, jwk } = K::generate(storage.key_storage(), + key_type, + alg + ).await + .map_err(Error::KeyStorageError)?; + + let b64 = encode_b64_json(&jwk) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let did = DIDJwk::parse(&("did:jwk:".to_string() + &b64)) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let document = CoreDocument::expand_did_jwk(did) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let fragment = "0"; + + let verification_method = document.resolve_method(fragment, None) + .ok_or(identity_verification::Error::MissingIdFragment) + .map_err(Error::VerificationMethodConstructionError)?; + + let method_digest = MethodDigest::new(verification_method) + .map_err(Error::MethodDigestConstructionError)?; + + I::insert_key_id(storage.key_id_storage(), method_digest, key_id.clone()) + .await + .map_err(Error::KeyIdStorageError)?; + + Ok((document, fragment.to_string())) + } + + #[cfg(feature = "pqc")] + async fn new_did_jwk_pqc( + storage: &Storage, + key_type: KeyType, + alg: JwsAlgorithm, + ) -> StorageResult<(CoreDocument, String)> + where + K: JwkStoragePQ, + I: KeyIdStorage + { + + let JwkGenOutput { key_id, jwk } = K::generate_pq_key(storage.key_storage(), + key_type, + alg + ).await + .map_err(Error::KeyStorageError)?; + + let b64 = encode_b64_json(&jwk) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let did = DIDJwk::parse(&("did:jwk:".to_string() + &b64)) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let document = CoreDocument::expand_did_jwk(did) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let fragment = "0"; + + let verification_method = document.resolve_method(fragment, None) + .ok_or(identity_verification::Error::MissingIdFragment) + .map_err(Error::VerificationMethodConstructionError)?; + + let method_digest = MethodDigest::new(verification_method) + .map_err(Error::MethodDigestConstructionError)?; + + I::insert_key_id(storage.key_id_storage(), method_digest, key_id.clone()) + .await + .map_err(Error::KeyIdStorageError)?; + + Ok((document, fragment.to_string())) + } + + #[cfg(feature = "jpt-bbs-plus")] + async fn new_did_jwk_zk( + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + ) -> StorageResult<(CoreDocument, String)> + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage + { + let JwkGenOutput { key_id, jwk } = K::generate_bbs(storage.key_storage(), + key_type, + alg + ).await + .map_err(Error::KeyStorageError)?; + + let b64 = encode_b64_json(&jwk) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let did = DIDJwk::parse(&("did:jwk:".to_string() + &b64)) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let document = CoreDocument::expand_did_jwk(did) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let fragment = "0"; + + let verification_method = document.resolve_method(fragment, None) + .ok_or(identity_verification::Error::MissingIdFragment) + .map_err(Error::VerificationMethodConstructionError)?; + + let method_digest = MethodDigest::new(verification_method) + .map_err(Error::MethodDigestConstructionError)?; + + I::insert_key_id(storage.key_id_storage(), method_digest, key_id.clone()) + .await + .map_err(Error::KeyIdStorageError)?; + + Ok((document, fragment.to_string())) + } + + #[cfg(feature = "hybrid")] + async fn new_did_compositejwk( + storage: &Storage, + alg: CompositeAlgId, + ) -> StorageResult<(CoreDocument, String)> + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage + { + let (pq_key_type, pq_alg, trad_key_type, trad_alg) = match alg { + CompositeAlgId::IdMldsa44Ed25519 => ( + KeyType::from_static_str("AKP"), + JwsAlgorithm::ML_DSA_44, + KeyType::from_static_str("Ed25519"), + JwsAlgorithm::EdDSA, + ), + CompositeAlgId::IdMldsa65Ed25519 => ( + KeyType::from_static_str("AKP"), + JwsAlgorithm::ML_DSA_65, + KeyType::from_static_str("Ed25519"), + JwsAlgorithm::EdDSA, + ), + }; + + let JwkGenOutput { + key_id: t_key_id, + jwk: t_jwk, + } = K::generate(storage.key_storage(), trad_key_type, trad_alg) + .await + .map_err(Error::KeyStorageError)?; + + let JwkGenOutput { + key_id: pq_key_id, + jwk: pq_jwk, + } = K::generate_pq_key(storage.key_storage(), pq_key_type, pq_alg) + .await + .map_err(Error::KeyStorageError)?; + + let key_id = KeyId::new(format!("{}~{}", t_key_id.as_str(), pq_key_id.as_str())); + + let composite_pk = CompositeJwk::new(alg, t_jwk, pq_jwk); + + let b64 = encode_b64_json(&composite_pk) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let did = DIDCompositeJwk::parse(&("did:compositejwk:".to_string() + &b64)) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let document = CoreDocument::expand_did_compositejwk(did) + .map_err(|err| Error::EncodingError(Box::new(err)))?; + + let fragment = "0"; + + let verification_method = document.resolve_method(fragment, None) + .ok_or(identity_verification::Error::MissingIdFragment) + .map_err(Error::VerificationMethodConstructionError)?; + + let method_digest = MethodDigest::new(verification_method) + .map_err(Error::MethodDigestConstructionError)?; + + I::insert_key_id(storage.key_id_storage(), method_digest, key_id.clone()) + .await + .map_err(Error::KeyIdStorageError)?; + + Ok((document, fragment.to_string())) + } +} \ No newline at end of file diff --git a/identity_storage/src/storage/error.rs b/identity_storage/src/storage/error.rs index 51af20ead0..aad90b2d12 100644 --- a/identity_storage/src/storage/error.rs +++ b/identity_storage/src/storage/error.rs @@ -1,5 +1,8 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ use crate::key_id_storage::KeyIdStorageError; use crate::key_id_storage::MethodDigestConstructionError; @@ -24,6 +27,9 @@ pub enum JwkStorageDocumentError { /// Caused by the usage of a non-JWK method where a JWK method is expected. #[error("invalid method data format: expected publicKeyJwk")] NotPublicKeyJwk, + /// Caused by the usage of a non-Composite method where a Composite method is expected. + #[error("invalid method data format: expected compositePublicKey")] + NotCompositePublicKey, /// Caused by an invalid JWS algorithm. #[error("invalid JWS algorithm")] InvalidJwsAlgorithm, diff --git a/identity_storage/src/storage/hybrid_jws_document_ext.rs b/identity_storage/src/storage/hybrid_jws_document_ext.rs new file mode 100644 index 0000000000..f516393180 --- /dev/null +++ b/identity_storage/src/storage/hybrid_jws_document_ext.rs @@ -0,0 +1,521 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use super::JwkStorageDocumentError as Error; +use crate::try_undo_key_generation; +use crate::JwkGenOutput; +use crate::KeyType; +use crate::JwkStorage; +use crate::JwkStoragePQ; +use crate::JwsSignatureOptions; +use crate::KeyId; +use crate::KeyIdStorage; +use crate::KeyIdStorageErrorKind; +use crate::MethodDigest; +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::Jws; +use identity_credential::credential::Jwt; +use identity_credential::presentation::JwtPresentationOptions; +use identity_credential::presentation::Presentation; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use identity_verification::jws::CharSet; +use identity_verification::jws::CompactJwsEncoder; +use identity_verification::jws::CompactJwsEncodingOptions; +use identity_verification::jws::JwsAlgorithm; +use identity_verification::jws::JwsHeader; +use identity_verification::jwk::CompositeAlgId; +use identity_verification::jwk::CompositeJwk; +use identity_verification::MethodData; +use identity_verification::MethodScope; +use identity_verification::VerificationMethod; +use serde::de::DeserializeOwned; +use serde::Serialize; + +macro_rules! generate_method_hybrid_for_document_type { + ($t:ty, $name:ident) => { + async fn $name( + document: &mut $t, + storage: &Storage, + alg_id: CompositeAlgId, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + { + let (pq_key_type, pq_alg, trad_key_type, trad_alg) = match alg_id { + CompositeAlgId::IdMldsa44Ed25519 => ( + KeyType::from_static_str("AKP"), + JwsAlgorithm::ML_DSA_44, + KeyType::from_static_str("Ed25519"), + JwsAlgorithm::EdDSA, + ), + CompositeAlgId::IdMldsa65Ed25519 => ( + KeyType::from_static_str("AKP"), + JwsAlgorithm::ML_DSA_65, + KeyType::from_static_str("Ed25519"), + JwsAlgorithm::EdDSA, + ), + }; + + let JwkGenOutput { + key_id: t_key_id, + jwk: t_jwk, + } = K::generate(storage.key_storage(), trad_key_type, trad_alg) + .await + .map_err(Error::KeyStorageError)?; + + let JwkGenOutput { + key_id: pq_key_id, + jwk: pq_jwk, + } = K::generate_pq_key(storage.key_storage(), pq_key_type, pq_alg) + .await + .map_err(Error::KeyStorageError)?; + + let composite_kid = KeyId::new(format!("{}~{}", t_key_id.as_str(), pq_key_id.as_str())); + + let composite_pk = CompositeJwk::new(alg_id, t_jwk, pq_jwk); + + let method: VerificationMethod = { + match VerificationMethod::new_from_compositejwk(document.id().clone(), composite_pk, fragment) + .map_err(Error::VerificationMethodConstructionError) + { + Ok(method) => method, + Err(source) => { + let error = try_undo_key_generation(storage, &t_key_id, source).await; + let error = try_undo_key_generation(storage, &pq_key_id, error).await; + return Err(error); + } + } + }; + + // Extract data from method before inserting it into the DID document. + let method_digest: MethodDigest = MethodDigest::new(&method).map_err(Error::MethodDigestConstructionError)?; + let method_id: DIDUrl = method.id().clone(); + + // The fragment is always set on a method, so this error will never occur. + let fragment: String = method_id + .fragment() + .ok_or(identity_verification::Error::MissingIdFragment) + .map_err(Error::VerificationMethodConstructionError)? + .to_owned(); + + // Insert method into document and handle error upon failure. + if let Err(error) = document + .insert_method(method, scope) + .map_err(|_| Error::FragmentAlreadyExists) + { + let error = try_undo_key_generation(storage, &t_key_id, error).await; + let error = try_undo_key_generation(storage, &pq_key_id, error).await; + return Err(error); + }; + + // Insert the generated `KeyId` into storage under the computed method digest and handle the error if the + // operation fails. + if let Err(error) = ::insert_key_id(&storage.key_id_storage(), method_digest, composite_kid) + .await + .map_err(Error::KeyIdStorageError) + { + // Remove the method from the document as it can no longer be used. + let _ = document.remove_method(&method_id); + let error = try_undo_key_generation(storage, &t_key_id, error).await; + let error = try_undo_key_generation(storage, &pq_key_id, error).await; + return Err(error); + } + + Ok(fragment) + } + }; +} + +/// Extension trait to handle PQ/T hybrid operations. +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait JwkDocumentExtHybrid { + /// Generate an Verification Method containing a PQ/T hybrid key. + async fn generate_method_hybrid( + &mut self, + storage: &Storage, + alg_id: CompositeAlgId, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage; + + /// Create a PQ/T hybrid JWS. + async fn create_jws( + &self, + storage: &Storage, + fragment: &str, + payload: &[u8], + options: &JwsSignatureOptions, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage; + + /// Create a PQ/T hybrid Verifiable Credential. + async fn create_credential_jwt_hybrid( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwsSignatureOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync; + + /// Create a PQ/T hybrid Verifiable Presentation. + async fn create_presentation_jwt_hybrid( + &self, + presentation: &Presentation, + storage: &Storage, + fragment: &str, + signature_options: &JwsSignatureOptions, + presentation_options: &JwtPresentationOptions, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + CRED: ToOwned + Serialize + DeserializeOwned + Clone + Sync; +} + +generate_method_hybrid_for_document_type!(CoreDocument, generate_method_hybrid_core_document); + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl JwkDocumentExtHybrid for CoreDocument { + async fn generate_method_hybrid( + &mut self, + storage: &Storage, + alg_id: CompositeAlgId, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + { + generate_method_hybrid_core_document(self, storage, alg_id, fragment, scope).await + } + + /// Hybrid signature implementation + async fn create_jws( + &self, + storage: &Storage, + fragment: &str, + payload: &[u8], + options: &JwsSignatureOptions, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + { + // Obtain the method corresponding to the given fragment. + let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?; + let MethodData::CompositeJwk(ref composite) = method.data() else { + return Err(Error::NotCompositePublicKey); + }; + + let alg_id = composite.alg_id(); + let t_jwk = composite.traditional_public_key(); + let pq_jwk = composite.pq_public_key(); + + // Extract JwsAlgorithm. + let alg: JwsAlgorithm = alg_id.name().parse().map_err(|_| Error::InvalidJwsAlgorithm)?; + + // Create JWS header in accordance with options. + let header: JwsHeader = { + let mut header = JwsHeader::new(); + + header.set_alg(alg.clone()); + if let Some(custom) = &options.custom_header_parameters { + header.set_custom(custom.clone()) + } + + if let Some(ref kid) = options.kid { + header.set_kid(kid.clone()); + } else { + header.set_kid(method.id().to_string()); + } + + if let Some(b64) = options.b64 { + // Follow recommendation in https://datatracker.ietf.org/doc/html/rfc7797#section-7. + if !b64 { + header.set_b64(b64); + header.set_crit(["b64"]); + } + }; + + if let Some(typ) = &options.typ { + header.set_typ(typ.clone()) + } else { + // https://www.w3.org/TR/vc-data-model/#jwt-encoding + header.set_typ("JWT") + } + + if let Some(cty) = &options.cty { + header.set_cty(cty.clone()) + }; + + if let Some(url) = &options.url { + header.set_url(url.clone()) + }; + + if let Some(nonce) = &options.nonce { + header.set_nonce(nonce.clone()) + }; + + header + }; + + // 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: KeyId = ::get_key_id(storage.key_id_storage(), &method_digest) + .await + .map_err(Error::KeyIdStorageError)?; + + let (t_key_id, pq_key_id) = key_id + .as_str() + .split_once("~") + .map(|v| (KeyId::new(v.0), KeyId::new(v.1))) + .ok_or(Error::KeyIdStorageError(KeyIdStorageErrorKind::Unspecified.into()))?; + + // Extract Compact JWS encoding options. + let encoding_options: CompactJwsEncodingOptions = if !options.detached_payload { + // We use this as a default and don't provide the extra UrlSafe check for now. + // Applications that require such checks can easily do so after JWS creation. + CompactJwsEncodingOptions::NonDetached { + charset_requirements: CharSet::Default, + } + } else { + CompactJwsEncodingOptions::Detached + }; + + let jws_encoder: CompactJwsEncoder<'_> = CompactJwsEncoder::new_with_options(payload, &header, encoding_options) + .map_err(|err| Error::EncodingError(err.into()))?; + + //M' = Prefix || Domain || len(ctx) || ctx || M + //let prefix = b"CompositeAlgorithmSignatures2025"; + + let (signing_input, ctx) = match alg { + JwsAlgorithm::IdMldsa44Ed25519 => { + //Prefix: CompositeAlgorithmSignatures2025 + let mut input = b"CompositeAlgorithmSignatures2025".to_vec(); + + //Domain: id-MLDSA44-Ed25519 + let domain = &[0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x08, 0x01, 0x3E]; + + input.extend_from_slice(domain); + + //len(ctx) = 0 + input.push(0x00); + + //M + input.extend(jws_encoder.signing_input()); + + (input, domain) + } + JwsAlgorithm::IdMldsa65Ed25519 => { + //Prefix: CompositeAlgorithmSignatures2025 + let mut input = b"CompositeAlgorithmSignatures2025".to_vec(); + + //Domain: id-MLDSA65-Ed25519 + let domain = &[0x06, 0x0B, 0x60, 0x86, 0x48, 0x01, 0x86, 0xFA, 0x6B, 0x50, 0x08, 0x01, 0x47]; + + input.extend_from_slice(domain); + + //len(ctx) = 0 + input.push(0x00); + + //M + input.extend(jws_encoder.signing_input()); + + (input, domain) + } + _ => return Err(Error::InvalidJwsAlgorithm), + }; + + let signature_t = ::sign(storage.key_storage(), &t_key_id, &signing_input, t_jwk) + .await + .map_err(Error::KeyStorageError)?; + + let signature_pq = ::pq_sign(storage.key_storage(), &pq_key_id, &signing_input, pq_jwk, Some(ctx)) + .await + .map_err(Error::KeyStorageError)?; + + let signature = [signature_t, signature_pq].concat(); + + Ok(Jws::new(jws_encoder.into_jws(&signature))) + } + + async fn create_credential_jwt_hybrid( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwsSignatureOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + if options.detached_payload { + return Err(Error::EncodingError(Box::::from( + "cannot use detached payload for credential signing", + ))); + } + + if !options.b64.unwrap_or(true) { + // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7. + return Err(Error::EncodingError(Box::::from( + "cannot use `b64 = false` with JWTs", + ))); + } + + let payload = credential + .serialize_jwt(custom_claims) + .map_err(Error::ClaimsSerializationError)?; + self + .create_jws(storage, fragment, payload.as_bytes(), options) + .await + .map(|jws| Jwt::new(jws.into())) + } + + async fn create_presentation_jwt_hybrid( + &self, + presentation: &Presentation, + storage: &Storage, + fragment: &str, + jws_options: &JwsSignatureOptions, + jwt_options: &JwtPresentationOptions, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + CRED: ToOwned + Serialize + DeserializeOwned + Clone + Sync, + { + if jws_options.detached_payload { + return Err(Error::EncodingError(Box::::from( + "cannot use detached payload for presentation signing", + ))); + } + + if !jws_options.b64.unwrap_or(true) { + // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7. + return Err(Error::EncodingError(Box::::from( + "cannot use `b64 = false` with JWTs", + ))); + } + let payload = presentation + .serialize_jwt(jwt_options) + .map_err(Error::ClaimsSerializationError)?; + self + .create_jws(storage, fragment, payload.as_bytes(), jws_options) + .await + .map(|jws| Jwt::new(jws.into())) + } +} + +// ==================================================================================================================== +// IotaDocument +// ==================================================================================================================== +#[cfg(feature = "iota-document")] +mod iota_document { + use crate::StorageResult; + + use super::*; + use identity_credential::credential::Jwt; + use identity_iota_core::IotaDocument; + + generate_method_hybrid_for_document_type!(IotaDocument, generate_method_hybrid_iota_document); + + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl JwkDocumentExtHybrid for IotaDocument { + async fn generate_method_hybrid( + &mut self, + storage: &Storage, + alg_id: CompositeAlgId, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + { + generate_method_hybrid_iota_document(self, storage, alg_id, fragment, scope).await + } + + async fn create_jws( + &self, + storage: &Storage, + fragment: &str, + payload: &[u8], + options: &JwsSignatureOptions, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + { + self + .core_document() + .create_jws(storage, fragment, payload, options) + .await + } + + async fn create_credential_jwt_hybrid( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwsSignatureOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + self + .core_document() + .create_credential_jwt_hybrid(credential, storage, fragment, options, custom_claims) + .await + } + + async fn create_presentation_jwt_hybrid( + &self, + presentation: &Presentation, + storage: &Storage, + fragment: &str, + options: &JwsSignatureOptions, + jwt_options: &JwtPresentationOptions, + ) -> StorageResult + where + K: JwkStorage + JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + CRED: ToOwned + Serialize + DeserializeOwned + Clone + Sync, + { + self + .core_document() + .create_presentation_jwt_hybrid(presentation, storage, fragment, options, jwt_options) + .await + } + } +} diff --git a/identity_storage/src/storage/mod.rs b/identity_storage/src/storage/mod.rs index 7643c41a95..e8ec970a5a 100644 --- a/identity_storage/src/storage/mod.rs +++ b/identity_storage/src/storage/mod.rs @@ -1,6 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + //! This module provides a type wrapping a key and key id storage. mod error; @@ -11,6 +15,12 @@ mod jwp_document_ext; mod signature_options; #[cfg(feature = "jpt-bbs-plus")] mod timeframe_revocation_ext; +#[cfg(feature = "hybrid")] +mod hybrid_jws_document_ext; +#[cfg(feature = "pqc")] +mod pqc_jws_document_ext; + +mod did_jwk_document_ext; #[cfg(all(test, feature = "memstore"))] pub(crate) mod tests; @@ -23,6 +33,12 @@ pub use jwp_document_ext::*; pub use signature_options::*; #[cfg(feature = "jpt-bbs-plus")] pub use timeframe_revocation_ext::*; +#[cfg(feature = "hybrid")] +pub use hybrid_jws_document_ext::*; +#[cfg(feature = "pqc")] +pub use pqc_jws_document_ext::*; + +pub use did_jwk_document_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/pqc_jws_document_ext.rs b/identity_storage/src/storage/pqc_jws_document_ext.rs new file mode 100644 index 0000000000..01a0e8e85e --- /dev/null +++ b/identity_storage/src/storage/pqc_jws_document_ext.rs @@ -0,0 +1,397 @@ +// Copyright 2024 Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use super::JwkStorageDocumentError as Error; +use crate::key_id_storage::MethodDigest; +use crate::try_undo_key_generation; +use crate::JwkGenOutput; +use crate::JwkStoragePQ; +use crate::JwsSignatureOptions; +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::Jws; +use identity_credential::credential::Jwt; +use identity_credential::presentation::JwtPresentationOptions; +use identity_credential::presentation::Presentation; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use identity_verification::jws::CharSet; +use identity_verification::jws::CompactJwsEncoder; +use identity_verification::jws::CompactJwsEncodingOptions; +use identity_verification::jws::JwsAlgorithm; +use identity_verification::jws::JwsHeader; +use identity_verification::MethodData; +use identity_verification::MethodScope; +use identity_verification::VerificationMethod; +use serde::de::DeserializeOwned; +use serde::Serialize; + +///New trait to handle PQ-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 JwsDocumentExtPQC { + /// Generate new key material in the given `storage` and insert a new verification method with the corresponding PQ + /// public key material into the DID document. + async fn generate_method_pqc( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: JwsAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage; + + /// Create a PQ JWS + async fn create_jws_pqc( + &self, + storage: &Storage, + fragment: &str, + payload: &[u8], + options: &JwsSignatureOptions, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage; + + /// Produces a JWT using PQC algorithms where the payload is produced from the given `credential` + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + /// + /// The `custom_claims` can be used to set additional claims on the resulting JWT. + async fn create_credential_jwt_pqc( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwsSignatureOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync; + + /// Produces a JWT using PQC algorithms where the payload is produced from the given `presentation` + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id` + /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding + /// private key backed by the `storage` in accordance with the passed `options`. + async fn create_presentation_jwt_pqc( + &self, + presentation: &Presentation, + storage: &Storage, + fragment: &str, + signature_options: &JwsSignatureOptions, + presentation_options: &JwtPresentationOptions, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + CRED: ToOwned + Serialize + DeserializeOwned + Clone + Sync; +} + +// ==================================================================================================================== +// CoreDocument +// ==================================================================================================================== + +generate_method_for_document_type!( + CoreDocument, + JwsAlgorithm, + JwkStoragePQ, + JwkStoragePQ::generate_pq_key, + generate_method_core_document +); + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl JwsDocumentExtPQC for CoreDocument { + async fn generate_method_pqc( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: JwsAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage, + { + generate_method_core_document(self, storage, key_type, alg, fragment, scope).await + } + + async fn create_jws_pqc( + &self, + storage: &Storage, + fragment: &str, + payload: &[u8], + options: &JwsSignatureOptions, + ) -> StorageResult + where + K: JwkStoragePQ, + 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: JwsAlgorithm = jwk + .alg() + .unwrap_or("") + .parse() + .map_err(|_| Error::InvalidJwsAlgorithm)?; + + // Create JWS header in accordance with options. + let header: JwsHeader = { + let mut header = JwsHeader::new(); + + header.set_alg(alg); + if let Some(custom) = &options.custom_header_parameters { + header.set_custom(custom.clone()) + } + + if let Some(ref kid) = options.kid { + header.set_kid(kid.clone()); + } else { + header.set_kid(method.id().to_string()); + } + + if options.attach_jwk { + header.set_jwk(jwk.clone()) + }; + + if let Some(b64) = options.b64 { + // Follow recommendation in https://datatracker.ietf.org/doc/html/rfc7797#section-7. + if !b64 { + header.set_b64(b64); + header.set_crit(["b64"]); + } + }; + + if let Some(typ) = &options.typ { + header.set_typ(typ.clone()) + } else { + // https://www.w3.org/TR/vc-data-model/#jwt-encoding + header.set_typ("JWT") + } + + if let Some(cty) = &options.cty { + header.set_cty(cty.clone()) + }; + + if let Some(url) = &options.url { + header.set_url(url.clone()) + }; + + if let Some(nonce) = &options.nonce { + header.set_nonce(nonce.clone()) + }; + + header + }; + + // 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)?; + + // Extract Compact JWS encoding options. + let encoding_options: CompactJwsEncodingOptions = if !options.detached_payload { + // We use this as a default and don't provide the extra UrlSafe check for now. + // Applications that require such checks can easily do so after JWS creation. + CompactJwsEncodingOptions::NonDetached { + charset_requirements: CharSet::Default, + } + } else { + CompactJwsEncodingOptions::Detached + }; + + let jws_encoder: CompactJwsEncoder<'_> = CompactJwsEncoder::new_with_options(payload, &header, encoding_options) + .map_err(|err| Error::EncodingError(err.into()))?; + + let signature = ::pq_sign(storage.key_storage(), &key_id, jws_encoder.signing_input(), jwk, None) + .await + .map_err(Error::KeyStorageError)?; + Ok(Jws::new(jws_encoder.into_jws(&signature))) + } + + async fn create_credential_jwt_pqc( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwsSignatureOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + if options.detached_payload { + return Err(Error::EncodingError(Box::::from( + "cannot use detached payload for credential signing", + ))); + } + + if !options.b64.unwrap_or(true) { + // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7. + return Err(Error::EncodingError(Box::::from( + "cannot use `b64 = false` with JWTs", + ))); + } + + let payload = credential + .serialize_jwt(custom_claims) + .map_err(Error::ClaimsSerializationError)?; + self + .create_jws_pqc(storage, fragment, payload.as_bytes(), options) + .await + .map(|jws| Jwt::new(jws.into())) + } + + async fn create_presentation_jwt_pqc( + &self, + presentation: &Presentation, + storage: &Storage, + fragment: &str, + jws_options: &JwsSignatureOptions, + jwt_options: &JwtPresentationOptions, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + CRED: ToOwned + Serialize + DeserializeOwned + Clone + Sync, + { + if jws_options.detached_payload { + return Err(Error::EncodingError(Box::::from( + "cannot use detached payload for presentation signing", + ))); + } + + if !jws_options.b64.unwrap_or(true) { + // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7. + return Err(Error::EncodingError(Box::::from( + "cannot use `b64 = false` with JWTs", + ))); + } + let payload = presentation + .serialize_jwt(jwt_options) + .map_err(Error::ClaimsSerializationError)?; + self + .create_jws_pqc(storage, fragment, payload.as_bytes(), jws_options) + .await + .map(|jws| Jwt::new(jws.into())) + } +} + +// ==================================================================================================================== +// IotaDocument +// ==================================================================================================================== +#[cfg(feature = "iota-document")] +mod iota_document { + + use super::*; + use identity_iota_core::IotaDocument; + + generate_method_for_document_type!( + IotaDocument, + JwsAlgorithm, + JwkStoragePQ, + JwkStoragePQ::generate_pq_key, + generate_method_iota_document + ); + + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl JwsDocumentExtPQC for IotaDocument { + async fn generate_method_pqc( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: JwsAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage, + { + generate_method_iota_document(self, storage, key_type, alg, fragment, scope).await + } + + async fn create_jws_pqc( + &self, + storage: &Storage, + fragment: &str, + payload: &[u8], + options: &JwsSignatureOptions, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage, + { + self + .core_document() + .create_jws_pqc(storage, fragment, payload, options) + .await + } + + async fn create_credential_jwt_pqc( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwsSignatureOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + self + .core_document() + .create_credential_jwt_pqc(credential, storage, fragment, options, custom_claims) + .await + } + + async fn create_presentation_jwt_pqc( + &self, + presentation: &Presentation, + storage: &Storage, + fragment: &str, + jws_options: &JwsSignatureOptions, + jwt_options: &JwtPresentationOptions, + ) -> StorageResult + where + K: JwkStoragePQ, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + CRED: ToOwned + Serialize + DeserializeOwned + Clone + Sync, + { + self + .core_document() + .create_presentation_jwt_pqc(presentation, storage, fragment, jws_options, jwt_options) + .await + } + } +} diff --git a/identity_stronghold/src/storage/mod.rs b/identity_stronghold/src/storage/mod.rs index 2752f3b077..20cb245ba1 100644 --- a/identity_stronghold/src/storage/mod.rs +++ b/identity_stronghold/src/storage/mod.rs @@ -1,6 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + mod stronghold_jwk_storage; #[cfg(any(feature = "bbs-plus", test))] mod stronghold_jwk_storage_bbs_plus_ext; @@ -109,7 +113,7 @@ impl StrongholdStorage { .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; + let public_jwk = encode_bls_jwk(&sk, &pk, ProofAlgorithm::BBS).1; drop(Zeroizing::new(sk.to_bytes())); Ok(public_jwk) diff --git a/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs b/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs index 10fbe7faa0..62502dcb23 100644 --- a/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs +++ b/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs @@ -1,6 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use async_trait::async_trait; use identity_storage::key_storage::bls::*; use identity_storage::key_storage::JwkStorage; @@ -39,7 +43,7 @@ impl JwkStorageBbsPlusExt for StrongholdStorage { ); } - if !matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { + if !matches!(alg, ProofAlgorithm::BBS | ProofAlgorithm::BBS_SHAKE256) { return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()); } diff --git a/identity_stronghold/src/tests/test_bbs_ext.rs b/identity_stronghold/src/tests/test_bbs_ext.rs index efa71f3cc2..a093c67975 100644 --- a/identity_stronghold/src/tests/test_bbs_ext.rs +++ b/identity_stronghold/src/tests/test_bbs_ext.rs @@ -1,6 +1,10 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use identity_storage::key_storage::bls::expand_bls_jwk; use identity_storage::key_storage::bls::sign_bbs; use identity_storage::JwkGenOutput; @@ -23,7 +27,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::BBS) .await?; assert!(jwk.is_public()); @@ -36,7 +40,7 @@ async fn stronghold_bbs_keypair_gen_works() -> anyhow::Result<()> { 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) + .generate_bbs(StrongholdKeyType::Ed25519.into(), ProofAlgorithm::BBS) .await .unwrap_err(); assert!(matches!(error.kind(), KeyStorageErrorKind::UnsupportedKeyType)); @@ -61,7 +65,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::BBS) .await?; let pk = expand_bls_jwk(&jwk)?.1; let sk = { @@ -79,7 +83,7 @@ async fn stronghold_sign_bbs_works() -> anyhow::Result<()> { let mut data = vec![0; 1024]; rand::thread_rng().fill_bytes(&mut data); let expected_signature = sign_bbs( - ProofAlgorithm::BLS12381_SHA256, + ProofAlgorithm::BBS, std::slice::from_ref(&data), &sk, &pk, diff --git a/identity_verification/src/error.rs b/identity_verification/src/error.rs index 97070de3bf..dedc53d1b0 100644 --- a/identity_verification/src/error.rs +++ b/identity_verification/src/error.rs @@ -1,6 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + //! Errors that may occur when working with Decentralized Identifiers. /// Alias for a [`Result`][::core::result::Result] with the error type [Error]. @@ -39,4 +43,7 @@ pub enum Error { /// Caused by key material that is not a JSON Web Key. #[error("verification material format is not publicKeyJwk")] NotPublicKeyJwk, + /// Caused by key material that is not a Composite Public Key. + #[error("verification material format is not compositePublicKey")] + NotCompositePublicKey, } diff --git a/identity_verification/src/verification_method/material.rs b/identity_verification/src/verification_method/material.rs index 8e881253c5..578f7f8839 100644 --- a/identity_verification/src/verification_method/material.rs +++ b/identity_verification/src/verification_method/material.rs @@ -1,7 +1,12 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use crate::jose::jwk::Jwk; +use crate::jose::jwk::CompositeJwk; use core::fmt::Debug; use core::fmt::Formatter; use identity_core::convert::BaseEncoding; @@ -27,6 +32,8 @@ pub enum MethodData { PublicKeyBase58(String), /// Verification Material in the JSON Web Key format. PublicKeyJwk(Jwk), + /// Verification Material containing two keys in JSON Web Key format, one traditional and one PQ + CompositeJwk(CompositeJwk), /// Arbitrary verification material. #[serde(untagged)] Custom(CustomMethodData), @@ -59,9 +66,9 @@ impl MethodData { /// represented as a vector of bytes. pub fn try_decode(&self) -> Result> { match self { - Self::PublicKeyJwk(_) | Self::Custom(_) => Err(Error::InvalidMethodDataTransformation( - "method data is not base encoded", - )), + Self::PublicKeyJwk(_) | Self::Custom(_) | Self::CompositeJwk(_) => Err( + Error::InvalidMethodDataTransformation("method data is not base encoded"), + ), Self::PublicKeyMultibase(input) => { BaseEncoding::decode_multibase(input).map_err(|_| Error::InvalidKeyDataMultibase) } @@ -69,6 +76,20 @@ impl MethodData { } } + /// Returns the wrapped `CompositePublicKey` if the format is [`MethodData::CompositePublicKey`]. + pub fn composite_public_key(&self) -> Option<&CompositeJwk> { + if let Self::CompositeJwk(ref c) = self { + Some(c) + } else { + None + } + } + + /// Fallible version of [`Self::composite_public_key`](Self::composite_public_key()). + pub fn try_composite_public_key(&self) -> Result<&CompositeJwk> { + self.composite_public_key().ok_or(Error::NotCompositePublicKey) + } + /// Returns the wrapped `Jwk` if the format is [`MethodData::PublicKeyJwk`]. pub fn public_key_jwk(&self) -> Option<&Jwk> { if let Self::PublicKeyJwk(ref jwk) = self { @@ -99,6 +120,7 @@ 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::CompositeJwk(inner) => f.write_fmt(format_args!("CompositePublicKey({inner:#?})")), Self::Custom(CustomMethodData { name, data }) => f.write_fmt(format_args!("{name}({data})")), } } diff --git a/identity_verification/src/verification_method/method.rs b/identity_verification/src/verification_method/method.rs index 084956c3a9..d5286395dc 100644 --- a/identity_verification/src/verification_method/method.rs +++ b/identity_verification/src/verification_method/method.rs @@ -1,11 +1,17 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +/* + * Modifications Copyright 2024 Fondazione LINKS. + */ + use core::fmt::Display; use core::fmt::Formatter; use std::borrow::Cow; +use identity_did::DIDCompositeJwk; use identity_did::DIDJwk; +use identity_jose::jwk::CompositeJwk; use identity_jose::jwk::Jwk; use serde::de; use serde::Deserialize; @@ -227,6 +233,53 @@ impl VerificationMethod { } } + +impl VerificationMethod { + // =========================================================================== + // Constructors + // =========================================================================== + + /// Creates a new [`VerificationMethod`] from the given `did` and [`CompositeJwk`]. If `fragment` is not given + /// the `kid` value of the given `key` will be used, if present, otherwise an error is returned. + pub fn new_from_compositejwk(did: D, key: CompositeJwk, fragment: Option<&str>) -> Result { + let composite_fragment = key.traditional_public_key() + .kid() + .map(|s| s.to_string()) + .or_else(|| key.pq_public_key().kid().map(|s| s.to_string())) + .map(|s| { + if let (Some(str1), Some(str2)) = (key.traditional_public_key().kid(), key.pq_public_key().kid()) { + format!("{}~{}", str1, str2) + } else { + s + } + }); + + + let fragment: Cow<'_, str> = { + let given_fragment: &str = fragment + .or(composite_fragment.as_deref()) + .ok_or(Error::InvalidMethod( + "an explicit fragment or kid is required", + ))?; + // Make sure the fragment starts with "#" + if given_fragment.starts_with('#') { + Cow::Borrowed(given_fragment) + } else { + Cow::Owned(format!("#{given_fragment}")) + } + }; + + let id: DIDUrl = did.to_url().join(fragment).map_err(Error::DIDUrlConstructionError)?; + + MethodBuilder::default() + .id(id) + .type_(MethodType::custom("CompositeJsonWebKey")) + .controller(did.into()) + .data(MethodData::CompositeJwk(key)) + .build() + } +} + impl Display for VerificationMethod { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { self.fmt_json(f) @@ -256,6 +309,14 @@ impl TryFrom for VerificationMethod { } } +impl TryFrom for VerificationMethod { + type Error = Error; + fn try_from(did: DIDCompositeJwk) -> Result { + let jwk = did.composite_jwk(); + Self::new_from_compositejwk(did, jwk, Some("0")) + } +} + // Horrible workaround for a tracked serde issue https://github.com/serde-rs/serde/issues/2200. Serde doesn't "consume" // the input when deserializing flattened enums (MethodData in this case) causing duplication of data (in this case // it ends up in the properties object). This workaround simply removes the duplication. @@ -285,6 +346,7 @@ impl From<_VerificationMethod> for VerificationMethod { MethodData::PublicKeyBase58(_) => "publicKeyBase58", MethodData::PublicKeyJwk(_) => "publicKeyJwk", MethodData::PublicKeyMultibase(_) => "publicKeyMultibase", + MethodData::CompositeJwk(_) => "compositePublicKey", MethodData::Custom(CustomMethodData { name, .. }) => name.as_str(), }; properties.remove(key);