-
Notifications
You must be signed in to change notification settings - Fork 200
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add Ed25519Signature2020 support (#2029)
Signed-off-by: Daniel Bluhm <[email protected]>
- Loading branch information
Showing
5 changed files
with
353 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
237 changes: 237 additions & 0 deletions
237
packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2020.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<boolean>} 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 }) | ||
} |
100 changes: 100 additions & 0 deletions
100
packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/context2020.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 } |
1 change: 1 addition & 0 deletions
1
packages/core/src/modules/vc/data-integrity/signature-suites/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './ed25519/Ed25519Signature2018' | ||
export * from './ed25519/Ed25519Signature2020' | ||
export * from './JwsLinkedDataSignature' |