UNPKG

@veramo/credential-eip712

Version:

Veramo plugin for working with EIP712 Verifiable Credentials & Presentations.

303 lines 13.4 kB
import { PROOF_FORMAT, } from '@veramo/core-types'; import { extractIssuer, getChainId, getEthereumAddress, intersect, isDefined, MANDATORY_CREDENTIAL_CONTEXT, mapIdentifierKeysToDoc, processEntryToArray, removeDIDParameters, resolveDidOrThrow, } from '@veramo/utils'; import { recoverTypedSignature, SignTypedDataVersion } from '@metamask/eth-sig-util'; import { getEthTypesFromInputDoc } from 'eip-712-types-generation'; /** * A Veramo Credential sub-plugin that implements * a {@link @veramo/credential-w3c#ICredentialProvider | ICredentialProvider} with support for * EthereumEIP712Signature2021 proofs. * * @beta This API may change without a BREAKING CHANGE notice. * @see {@link https://w3c-ccg.github.io/ethereum-eip712-signature-2021-spec/ | EthereumEIP712Signature2021 spec } * @see {@link https://www.w3.org/TR/vc-data-model-1.1/ | VC 1.1 data model}. */ export class CredentialProviderEIP712 { /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.getProofFormatsSupportedForKey} */ getProofFormatsSupportedForKey(key) { if (this.matchKeyForEIP712(key)) { return [PROOF_FORMAT.ETHEREUM_EIP712_SIGNATURE_2021]; } return []; } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.canIssueCredentialType} */ canIssueProofFormat(query) { return query.proofFormat === PROOF_FORMAT.ETHEREUM_EIP712_SIGNATURE_2021; } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.canVerifyDocumentType} */ canVerifyDocumentType(query) { const { document } = query; return document?.proof?.type === PROOF_FORMAT.ETHEREUM_EIP712_SIGNATURE_2021; } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.createVerifiableCredential} */ async createVerifiableCredential(args, context) { const credentialContext = processEntryToArray(args?.credential?.['@context'], MANDATORY_CREDENTIAL_CONTEXT); const credentialType = processEntryToArray(args?.credential?.type, 'VerifiableCredential'); let issuanceDate = args?.credential?.issuanceDate || new Date().toISOString(); if (issuanceDate instanceof Date) { issuanceDate = issuanceDate.toISOString(); } const issuer = extractIssuer(args.credential, { removeParameters: true }); if (!issuer || typeof issuer === 'undefined') { throw new Error('invalid_argument: credential.issuer must not be empty'); } let keyRef = args.keyRef; const identifier = await context.agent.didManagerGet({ did: issuer }); if (!keyRef) { const key = identifier.keys.find((k) => k.type === 'Secp256k1' && k.meta?.algorithms?.includes('eth_signTypedData')); if (!key) throw Error('key_not_found: No suitable signing key is known for ' + identifier.did); keyRef = key.kid; } const extendedKeys = await mapIdentifierKeysToDoc(identifier, 'verificationMethod', context, args.resolutionOptions); const extendedKey = extendedKeys.find((key) => key.kid === keyRef); if (!extendedKey) throw Error('key_not_found: The signing key is not available in the issuer DID document'); let chainId; try { chainId = getChainId(extendedKey.meta.verificationMethod); } catch (e) { chainId = 1; } const credential = { ...args?.credential, '@context': credentialContext, type: credentialType, issuanceDate, proof: { verificationMethod: extendedKey.meta.verificationMethod.id, created: issuanceDate, proofPurpose: 'assertionMethod', type: PROOF_FORMAT.ETHEREUM_EIP712_SIGNATURE_2021, }, }; const message = credential; const domain = { chainId, name: 'VerifiableCredential', version: '1', }; const primaryType = 'VerifiableCredential'; const allTypes = getEthTypesFromInputDoc(credential, primaryType); const types = { ...allTypes }; const data = JSON.stringify({ domain, types, message, primaryType }); credential['proof']['proofValue'] = await context.agent.keyManagerSign({ keyRef, data, algorithm: 'eth_signTypedData', }); credential['proof']['eip712'] = { domain, types: allTypes, primaryType, }; return credential; } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.verifyCredential} */ async verifyCredential(args, context) { const credential = args.credential; if (!credential.proof || !credential.proof.proofValue) throw new Error('invalid_argument: proof is undefined'); const { proof, ...signingInput } = credential; const { proofValue, eip712, eip712Domain, ...verifyInputProof } = proof; const verificationMessage = { ...signingInput, proof: verifyInputProof, }; const compat = { ...eip712Domain, ...eip712, }; compat.types = compat.types || compat.messageSchema; if (!compat.primaryType || !compat.types || !compat.domain) throw new Error('invalid_argument: proof is missing expected properties'); const objectToVerify = { message: verificationMessage, domain: compat.domain, types: compat.types, primaryType: compat.primaryType, }; const recovered = recoverTypedSignature({ data: objectToVerify, signature: proofValue, version: SignTypedDataVersion.V4, }); const issuer = extractIssuer(credential); if (!issuer || typeof issuer === 'undefined') { throw new Error('invalid_argument: credential.issuer must not be empty'); } const didDocument = await resolveDidOrThrow(issuer, context, args.resolutionOptions); if (didDocument.verificationMethod) { for (const verificationMethod of didDocument.verificationMethod) { if (getEthereumAddress(verificationMethod)?.toLowerCase() === recovered.toLowerCase()) { return { verified: true, }; } } } else { throw new Error('resolver_error: issuer DIDDocument does not contain any verificationMethods'); } return { verified: false, error: { message: 'invalid_signature: The signature does not match any of the issuer signing keys', errorCode: 'invalid_signature', }, }; } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.createVerifiablePresentation} */ async createVerifiablePresentation(args, context) { const presentationContext = processEntryToArray(args?.presentation?.['@context'], MANDATORY_CREDENTIAL_CONTEXT); const presentationType = processEntryToArray(args?.presentation?.type, 'VerifiablePresentation'); let issuanceDate = args?.presentation?.issuanceDate || new Date().toISOString(); if (issuanceDate instanceof Date) { issuanceDate = issuanceDate.toISOString(); } const presentation = { ...args?.presentation, '@context': presentationContext, type: presentationType, issuanceDate, }; if (!isDefined(args.presentation.holder)) { throw new Error('invalid_argument: presentation.holder must not be empty'); } if (args.presentation.verifiableCredential) { // EIP712 arrays must use a single data type, so we map all credentials to strings. presentation.verifiableCredential = args.presentation.verifiableCredential.map((cred) => { // map JWT credentials to their canonical form if (typeof cred === 'string') { return cred; } else if (cred.proof.jwt) { return cred.proof.jwt; } else { return JSON.stringify(cred); } }); } const holder = removeDIDParameters(presentation.holder); let identifier; try { identifier = await context.agent.didManagerGet({ did: holder }); } catch (e) { throw new Error('invalid_argument: presentation.holder must be a DID managed by this agent'); } let keyRef = args.keyRef; if (!keyRef) { const key = identifier.keys.find((k) => k.type === 'Secp256k1' && k.meta?.algorithms?.includes('eth_signTypedData')); if (!key) throw Error('key_not_found: No suitable signing key is known for ' + identifier.did); keyRef = key.kid; } const extendedKeys = await mapIdentifierKeysToDoc(identifier, 'verificationMethod', context, args.resolutionOptions); const extendedKey = extendedKeys.find((key) => key.kid === keyRef); if (!extendedKey) throw Error('key_not_found: The signing key is not available in the issuer DID document'); let chainId; try { chainId = getChainId(extendedKey.meta.verificationMethod); } catch (e) { chainId = 1; } presentation['proof'] = { verificationMethod: extendedKey.meta.verificationMethod.id, created: issuanceDate, proofPurpose: 'assertionMethod', type: PROOF_FORMAT.ETHEREUM_EIP712_SIGNATURE_2021, }; const message = presentation; const domain = { chainId, name: 'VerifiablePresentation', version: '1', }; const primaryType = 'VerifiablePresentation'; const allTypes = getEthTypesFromInputDoc(presentation, primaryType); const types = { ...allTypes }; const data = JSON.stringify({ domain, types, message }); presentation.proof.proofValue = await context.agent.keyManagerSign({ keyRef, data, algorithm: 'eth_signTypedData', }); presentation.proof.eip712 = { domain, types: allTypes, primaryType, }; return presentation; } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.verifyPresentation} */ async verifyPresentation(args, context) { const presentation = args.presentation; if (!presentation.proof || !presentation.proof.proofValue) throw new Error('Proof is undefined'); const { proof, ...signingInput } = presentation; const { proofValue, eip712, eip712Domain, ...verifyInputProof } = proof; const verificationMessage = { ...signingInput, proof: verifyInputProof, }; const compat = { ...eip712Domain, ...eip712, }; compat.types = compat.types || compat.messageSchema; if (!compat.primaryType || !compat.types || !compat.domain) throw new Error('invalid_argument: presentation proof is missing expected properties'); const objectToVerify = { message: verificationMessage, domain: compat.domain, types: compat.types, primaryType: compat.primaryType, }; const recovered = recoverTypedSignature({ data: objectToVerify, signature: proofValue, version: SignTypedDataVersion.V4, }); const issuer = extractIssuer(presentation); if (!issuer || typeof issuer === 'undefined') { throw new Error('invalid_argument: args.presentation.issuer must not be empty'); } const didDocument = await resolveDidOrThrow(issuer, context, args.resolutionOptions); if (didDocument.verificationMethod) { for (const verificationMethod of didDocument.verificationMethod) { if (getEthereumAddress(verificationMethod)?.toLowerCase() === recovered.toLowerCase()) { return { verified: true, }; } } } else { throw new Error('resolver_error: holder DIDDocument does not contain any verificationMethods'); } return { verified: false, error: { message: 'invalid_signature: The signature does not match any of the holder signing keys', errorCode: 'invalid_signature', }, }; } /** * Checks if a key is suitable for signing EIP712 payloads. * This relies on the metadata set by the key management system to determine if this key can sign EIP712 payloads. * * @param k - the key to check * * @internal */ matchKeyForEIP712(k) { return (intersect(k.meta?.algorithms ?? [], ['eth_signTypedData', PROOF_FORMAT.ETHEREUM_EIP712_SIGNATURE_2021]) .length > 0); } } //# sourceMappingURL=CredentialProviderEIP712.js.map