UNPKG

@veramo/credential-eip712

Version:

Veramo plugin for working with EIP712 Verifiable Credentials & Presentations.

409 lines (351 loc) 13.1 kB
import { CredentialPayload, ICreateVerifiableCredentialArgs, ICreateVerifiablePresentationArgs, IIdentifier, IKey, IssuerAgentContext, IVerifyCredentialArgs, IVerifyPresentationArgs, IVerifyResult, PresentationPayload, PROOF_FORMAT, ProofFormat, VerifiableCredential, VerifiablePresentation, VerifierAgentContext, } from '@veramo/core-types' import { extractIssuer, getChainId, getEthereumAddress, intersect, isDefined, MANDATORY_CREDENTIAL_CONTEXT, mapIdentifierKeysToDoc, processEntryToArray, removeDIDParameters, resolveDidOrThrow, } from '@veramo/utils' import { ICredentialProvider, ProofFormatQuery, TentativeVerificationQuery } from '@veramo/credential-w3c' 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 implements ICredentialProvider { /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.getProofFormatsSupportedForKey} */ getProofFormatsSupportedForKey(key: IKey): ProofFormat[] { if (this.matchKeyForEIP712(key)) { return [PROOF_FORMAT.ETHEREUM_EIP712_SIGNATURE_2021] } return [] } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.canIssueCredentialType} */ canIssueProofFormat(query: ProofFormatQuery): boolean { return query.proofFormat === PROOF_FORMAT.ETHEREUM_EIP712_SIGNATURE_2021 } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.canVerifyDocumentType} */ canVerifyDocumentType(query: TentativeVerificationQuery): boolean { const { document } = query return (<VerifiableCredential>document)?.proof?.type === PROOF_FORMAT.ETHEREUM_EIP712_SIGNATURE_2021 } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.createVerifiableCredential} */ async createVerifiableCredential( args: ICreateVerifiableCredentialArgs, context: IssuerAgentContext, ): Promise<VerifiableCredential> { 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: CredentialPayload = { ...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 as VerifiableCredential } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.verifyCredential} */ async verifyCredential(args: IVerifyCredentialArgs, context: VerifierAgentContext): Promise<IVerifyResult> { const credential = args.credential as VerifiableCredential 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: ICreateVerifiablePresentationArgs, context: IssuerAgentContext, ): Promise<VerifiablePresentation> { 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: PresentationPayload = { ...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: IIdentifier 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 as VerifiablePresentation } /** {@inheritdoc @veramo/credential-w3c#ICredentialProvider.verifyPresentation} */ async verifyPresentation( args: IVerifyPresentationArgs, context: VerifierAgentContext, ): Promise<IVerifyResult> { const presentation = args.presentation as VerifiablePresentation 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: IKey): boolean { return ( intersect(k.meta?.algorithms ?? [], ['eth_signTypedData', PROOF_FORMAT.ETHEREUM_EIP712_SIGNATURE_2021]) .length > 0 ) } }