From 715be31e3b8ad80f3f1be69ea5cdab8df3f32e24 Mon Sep 17 00:00:00 2001 From: Daniel Bluhm Date: Fri, 27 Sep 2024 14:05:22 -0400 Subject: [PATCH] feat: add Ed25519Signature2020 support (#2029) Signed-off-by: Daniel Bluhm --- .../src/modules/vc/W3cCredentialsModule.ts | 8 +- .../vc/__tests__/W3CredentialsModule.test.ts | 10 +- .../ed25519/Ed25519Signature2020.ts | 237 ++++++++++++++++++ .../signature-suites/ed25519/context2020.ts | 100 ++++++++ .../data-integrity/signature-suites/index.ts | 1 + 5 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2020.ts create mode 100644 packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/context2020.ts diff --git a/packages/core/src/modules/vc/W3cCredentialsModule.ts b/packages/core/src/modules/vc/W3cCredentialsModule.ts index 3b1fd7da8b..bf7397b64b 100644 --- a/packages/core/src/modules/vc/W3cCredentialsModule.ts +++ b/packages/core/src/modules/vc/W3cCredentialsModule.ts @@ -12,7 +12,7 @@ import { W3cCredentialsApi } from './W3cCredentialsApi' import { W3cCredentialsModuleConfig } from './W3cCredentialsModuleConfig' import { SignatureSuiteRegistry, SignatureSuiteToken } from './data-integrity/SignatureSuiteRegistry' import { W3cJsonLdCredentialService } from './data-integrity/W3cJsonLdCredentialService' -import { Ed25519Signature2018 } from './data-integrity/signature-suites' +import { Ed25519Signature2018, Ed25519Signature2020 } from './data-integrity/signature-suites' import { W3cJwtCredentialService } from './jwt-vc' import { W3cCredentialRepository } from './repository/W3cCredentialRepository' @@ -48,5 +48,11 @@ export class W3cCredentialsModule implements Module { ], keyTypes: [KeyType.Ed25519], }) + dependencyManager.registerInstance(SignatureSuiteToken, { + suiteClass: Ed25519Signature2020, + proofType: 'Ed25519Signature2020', + verificationMethodTypes: [VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020], + keyTypes: [KeyType.Ed25519], + }) } } diff --git a/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts b/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts index 263d9f03ee..73e0181b30 100644 --- a/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts +++ b/packages/core/src/modules/vc/__tests__/W3CredentialsModule.test.ts @@ -5,7 +5,7 @@ import { W3cCredentialsModule } from '../W3cCredentialsModule' import { W3cCredentialsModuleConfig } from '../W3cCredentialsModuleConfig' import { SignatureSuiteRegistry, SignatureSuiteToken } from '../data-integrity/SignatureSuiteRegistry' import { W3cJsonLdCredentialService } from '../data-integrity/W3cJsonLdCredentialService' -import { Ed25519Signature2018 } from '../data-integrity/signature-suites' +import { Ed25519Signature2018, Ed25519Signature2020 } from '../data-integrity/signature-suites' import { W3cJwtCredentialService } from '../jwt-vc' import { W3cCredentialRepository } from '../repository' @@ -27,7 +27,7 @@ describe('W3cCredentialsModule', () => { expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(W3cCredentialRepository) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SignatureSuiteRegistry) - expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(2) + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(3) expect(dependencyManager.registerInstance).toHaveBeenCalledWith(W3cCredentialsModuleConfig, module.config) expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, { @@ -36,5 +36,11 @@ describe('W3cCredentialsModule', () => { proofType: 'Ed25519Signature2018', keyTypes: [KeyType.Ed25519], }) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith(SignatureSuiteToken, { + suiteClass: Ed25519Signature2020, + verificationMethodTypes: ['Ed25519VerificationKey2020'], + proofType: 'Ed25519Signature2020', + keyTypes: [KeyType.Ed25519], + }) }) }) diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2020.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2020.ts new file mode 100644 index 0000000000..9a7f419544 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2020.ts @@ -0,0 +1,237 @@ +import type { DocumentLoader, JsonLdDoc, Proof, VerificationMethod } from '../../jsonldUtil' +import type { JwsLinkedDataSignatureOptions } from '../JwsLinkedDataSignature' + +import { Key } from '../../../../../crypto' +import { MultiBaseEncoder } from '../../../../../utils' +import { CREDENTIALS_CONTEXT_V1_URL, SECURITY_CONTEXT_URL } from '../../../constants' +import { _includesContext } from '../../jsonldUtil' +import jsonld from '../../libraries/jsonld' +import { JwsLinkedDataSignature } from '../JwsLinkedDataSignature' + +import { ED25519_SUITE_CONTEXT_URL_2020 } from './constants' +import { ed25519Signature2020Context } from './context2020' + +type Ed25519Signature2020Options = Pick< + JwsLinkedDataSignatureOptions, + 'key' | 'proof' | 'date' | 'useNativeCanonize' | 'LDKeyClass' +> + +export class Ed25519Signature2020 extends JwsLinkedDataSignature { + public static CONTEXT_URL = ED25519_SUITE_CONTEXT_URL_2020 + public static CONTEXT = ed25519Signature2020Context.get(ED25519_SUITE_CONTEXT_URL_2020) + + /** + * @param {object} options - Options hashmap. + * + * Either a `key` OR at least one of `signer`/`verifier` is required. + * + * @param {object} [options.key] - An optional key object (containing an + * `id` property, and either `signer` or `verifier`, depending on the + * intended operation. Useful for when the application is managing keys + * itself (when using a KMS, you never have access to the private key, + * and so should use the `signer` param instead). + * @param {Function} [options.signer] - Signer function that returns an + * object with an async sign() method. This is useful when interfacing + * with a KMS (since you don't get access to the private key and its + * `signer()`, the KMS client gives you only the signer function to use). + * @param {Function} [options.verifier] - Verifier function that returns + * an object with an async `verify()` method. Useful when working with a + * KMS-provided verifier function. + * + * Advanced optional parameters and overrides. + * + * @param {object} [options.proof] - A JSON-LD document with options to use + * for the `proof` node. Any other custom fields can be provided here + * using a context different from security-v2). + * @param {string|Date} [options.date] - Signing date to use if not passed. + * @param {boolean} [options.useNativeCanonize] - Whether to use a native + * canonize algorithm. + */ + public constructor(options: Ed25519Signature2020Options) { + super({ + type: 'Ed25519Signature2020', + algorithm: 'EdDSA', + LDKeyClass: options.LDKeyClass, + contextUrl: ED25519_SUITE_CONTEXT_URL_2020, + key: options.key, + proof: options.proof, + date: options.date, + useNativeCanonize: options.useNativeCanonize, + }) + this.requiredKeyType = 'Ed25519VerificationKey2020' + } + + public async assertVerificationMethod(document: JsonLdDoc) { + if (!_includesCompatibleContext({ document: document })) { + // For DID Documents, since keys do not have their own contexts, + // the suite context is usually provided by the documentLoader logic + throw new TypeError( + `The '@context' of the verification method (key) MUST contain the context url "${this.contextUrl}".` + ) + } + + if (!_isEd2020Key(document)) { + const verificationMethodType = jsonld.getValues(document, 'type')[0] + throw new Error( + `Unsupported verification method type '${verificationMethodType}'. Verification method type MUST be 'Ed25519VerificationKey2020'.` + ) + } else if (_isEd2020Key(document) && !_includesEd2020Context(document)) { + throw new Error( + `For verification method type 'Ed25519VerificationKey2020' the '@context' MUST contain the context url "${ED25519_SUITE_CONTEXT_URL_2020}".` + ) + } + + // ensure verification method has not been revoked + if (document.revoked !== undefined) { + throw new Error('The verification method has been revoked.') + } + } + + public async getVerificationMethod(options: { proof: Proof; documentLoader?: DocumentLoader }) { + let verificationMethod = await super.getVerificationMethod({ + proof: options.proof, + documentLoader: options.documentLoader, + }) + + // convert Ed25519VerificationKey2020 to Ed25519VerificationKey2018 + if (_isEd2020Key(verificationMethod) && _includesEd2020Context(verificationMethod)) { + // -- convert multibase to base58 -- + const publicKeyBase58 = Key.fromFingerprint(verificationMethod.publicKeyMultibase).publicKeyBase58 + + // -- update type + verificationMethod.type = 'Ed25519VerificationKey2018' + + verificationMethod = { + ...verificationMethod, + publicKeyMultibase: undefined, + publicKeyBase58, + } + } + + return verificationMethod + } + + /** + * Ensures the document to be signed contains the required signature suite + * specific `@context`, by either adding it (if `addSuiteContext` is true), + * or throwing an error if it's missing. + * + * @override + * + * @param {object} options - Options hashmap. + * @param {object} options.document - JSON-LD document to be signed. + * @param {boolean} options.addSuiteContext - Add suite context? + */ + public ensureSuiteContext(options: { document: JsonLdDoc; addSuiteContext: boolean }) { + if (_includesCompatibleContext({ document: options.document })) { + return + } + + super.ensureSuiteContext({ document: options.document, addSuiteContext: options.addSuiteContext }) + } + + /** + * Checks whether a given proof exists in the document. + * + * @override + * + * @param {object} options - Options hashmap. + * @param {object} options.proof - A proof. + * @param {object} options.document - A JSON-LD document. + * @param {object} options.purpose - A jsonld-signatures ProofPurpose + * instance (e.g. AssertionProofPurpose, AuthenticationProofPurpose, etc). + * @param {Function} options.documentLoader - A secure document loader (it is + * recommended to use one that provides static known documents, instead of + * fetching from the web) for returning contexts, controller documents, + * keys, and other relevant URLs needed for the proof. + * + * @returns {Promise} Whether a match for the proof was found. + */ + public async matchProof(options: { + proof: Proof + document: VerificationMethod + // eslint-disable-next-line @typescript-eslint/no-explicit-any + purpose: any + documentLoader?: DocumentLoader + }) { + if (!_includesCompatibleContext({ document: options.document })) { + return false + } + return super.matchProof({ + proof: options.proof, + document: options.document, + purpose: options.purpose, + documentLoader: options.documentLoader, + }) + } + + /** + * @param options - Options hashmap. + * @param options.verifyData - The data to sign. + * @param options.proof - A JSON-LD document with options to use + * for the `proof` node. Any other custom fields can be provided here + * using a context different from `security-v2`. + * + * @returns The proof containing the signature value. + */ + public async sign(options: { verifyData: Uint8Array; proof: Proof }) { + if (!(this.signer && typeof this.signer.sign === 'function')) { + throw new Error('A signer API has not been specified.') + } + const signature = await this.signer.sign({ data: options.verifyData }) + const encodedSignature = MultiBaseEncoder.encode(signature, 'base58btc') + + // create detached content signature + options.proof.proofValue = encodedSignature + return options.proof + } + + /** + * @param options - Options hashmap. + * @param options.verifyData - The data to verify. + * @param options.verificationMethod - A verification method. + * @param options.proof - The proof to be verified. + * + * @returns Resolves with the verification result. + */ + public async verifySignature(options: { + verifyData: Uint8Array + verificationMethod: VerificationMethod + proof: Proof + }) { + if (!(options.proof.proofValue && typeof options.proof.proofValue === 'string')) { + throw new TypeError('The proof does not include a valid "proofValue" property.') + } + const signature = MultiBaseEncoder.decode(options.proof.proofValue).data + + let { verifier } = this + if (!verifier) { + const key = await this.LDKeyClass.from(options.verificationMethod) + verifier = key.verifier() + } + return verifier.verify({ data: options.verifyData, signature }) + } +} + +function _includesCompatibleContext(options: { document: JsonLdDoc }) { + // Handle the unfortunate Ed25519Signature2018 / credentials/v1 collision + const hasEd2020 = _includesContext({ + document: options.document, + contextUrl: ED25519_SUITE_CONTEXT_URL_2020, + }) + const hasCred = _includesContext({ document: options.document, contextUrl: CREDENTIALS_CONTEXT_V1_URL }) + const hasSecV2 = _includesContext({ document: options.document, contextUrl: SECURITY_CONTEXT_URL }) + + // Either one by itself is fine, for this suite + return hasEd2020 || hasCred || hasSecV2 +} + +function _isEd2020Key(verificationMethod: JsonLdDoc) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - .hasValue is not part of the public API + return jsonld.hasValue(verificationMethod, 'type', 'Ed25519VerificationKey2020') +} + +function _includesEd2020Context(document: JsonLdDoc) { + return _includesContext({ document, contextUrl: ED25519_SUITE_CONTEXT_URL_2020 }) +} diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/context2020.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/context2020.ts new file mode 100644 index 0000000000..9561932dc4 --- /dev/null +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/context2020.ts @@ -0,0 +1,100 @@ +import { ED25519_SUITE_CONTEXT_URL_2020 } from './constants' + +export const context = { + '@context': { + id: '@id', + type: '@type', + '@protected': true, + proof: { + '@id': 'https://w3id.org/security#proof', + '@type': '@id', + '@container': '@graph', + }, + Ed25519VerificationKey2020: { + '@id': 'https://w3id.org/security#Ed25519VerificationKey2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + revoked: { + '@id': 'https://w3id.org/security#revoked', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + publicKeyMultibase: { + '@id': 'https://w3id.org/security#publicKeyMultibase', + '@type': 'https://w3id.org/security#multibase', + }, + }, + }, + Ed25519Signature2020: { + '@id': 'https://w3id.org/security#Ed25519Signature2020', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + challenge: 'https://w3id.org/security#challenge', + created: { + '@id': 'http://purl.org/dc/terms/created', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + domain: 'https://w3id.org/security#domain', + expires: { + '@id': 'https://w3id.org/security#expiration', + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + }, + nonce: 'https://w3id.org/security#nonce', + proofPurpose: { + '@id': 'https://w3id.org/security#proofPurpose', + '@type': '@vocab', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + }, + }, + proofValue: { + '@id': 'https://w3id.org/security#proofValue', + '@type': 'https://w3id.org/security#multibase', + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + }, +} + +const ed25519Signature2020Context = new Map() +ed25519Signature2020Context.set(ED25519_SUITE_CONTEXT_URL_2020, context) + +export { ed25519Signature2020Context } diff --git a/packages/core/src/modules/vc/data-integrity/signature-suites/index.ts b/packages/core/src/modules/vc/data-integrity/signature-suites/index.ts index 7eecb7ef25..c2e2b49ed1 100644 --- a/packages/core/src/modules/vc/data-integrity/signature-suites/index.ts +++ b/packages/core/src/modules/vc/data-integrity/signature-suites/index.ts @@ -1,2 +1,3 @@ export * from './ed25519/Ed25519Signature2018' +export * from './ed25519/Ed25519Signature2020' export * from './JwsLinkedDataSignature'