-
Notifications
You must be signed in to change notification settings - Fork 238
feat: add Ed25519Signature2020 support #2029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
TimoGlastra
merged 7 commits into
openwallet-foundation:main
from
dbluhm:feature/ed25519-signature-2020
Sep 27, 2024
Merged
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
214fcac
feat: add ed25519 signature 2020 support
dbluhm 7bf6607
fix: straggling 2018 references and key parsing
dbluhm fc920f1
fix: w3c module config test
dbluhm ed952f1
style: fix prettier errors
dbluhm 93f571e
refactor: use Key.fromFingerprint for key transform
dbluhm ea4f724
Merge branch 'main' into feature/ed25519-signature-2020
TimoGlastra 4fc9854
Merge branch 'main' into feature/ed25519-signature-2020
TimoGlastra File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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
239 changes: 239 additions & 0 deletions
239
packages/core/src/modules/vc/data-integrity/signature-suites/ed25519/Ed25519Signature2020.ts
This file contains hidden or 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,239 @@ | ||
import type { DocumentLoader, JsonLdDoc, Proof, VerificationMethod } from '../../jsonldUtil' | ||
import type { JwsLinkedDataSignatureOptions } from '../JwsLinkedDataSignature' | ||
|
||
import { MultiBaseEncoder, TypedArrayEncoder } 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 -- | ||
let publicKeyBuffer = MultiBaseEncoder.decode(verificationMethod.publicKeyMultibase).data | ||
if ((verificationMethod.publicKeyMultibase as string).startsWith('z6Mk')) { | ||
publicKeyBuffer = publicKeyBuffer.slice(2) | ||
} | ||
|
||
// -- update type | ||
verificationMethod.type = 'Ed25519VerificationKey2018' | ||
|
||
verificationMethod = { | ||
...verificationMethod, | ||
publicKeyMultibase: undefined, | ||
publicKeyBase58: TypedArrayEncoder.toBase58(publicKeyBuffer), | ||
} | ||
} | ||
|
||
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 hidden or 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 hidden or 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' |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.