Skip to content
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

feat: add Ed25519Signature2020 support #2029

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/core/src/modules/vc/W3cCredentialsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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],
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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, {
Expand All @@ -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],
})
})
})
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)
}
dbluhm marked this conversation as resolved.
Show resolved Hide resolved

// -- 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 })
}
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 }
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'
Loading