UNPKG

@veramo/credential-eip712

Version:

Veramo plugin for working with EIP712 Verifiable Credentials & Presentations.

342 lines (286 loc) 11.1 kB
import { CredentialPayload, IAgentPlugin, IIdentifier, PresentationPayload, VerifiableCredential, VerifiablePresentation, } from '@veramo/core-types' import { extractIssuer, getChainIdForDidEthr, getEthereumAddress, isDefined, MANDATORY_CREDENTIAL_CONTEXT, mapIdentifierKeysToDoc, processEntryToArray, resolveDidOrThrow, } from '@veramo/utils' import schema from "../plugin.schema.json" assert { type: 'json' } import { recoverTypedSignature, SignTypedDataVersion } from '@metamask/eth-sig-util' import { ICreateVerifiableCredentialEIP712Args, ICreateVerifiablePresentationEIP712Args, ICredentialIssuerEIP712, IRequiredContext, IVerifyCredentialEIP712Args, IVerifyPresentationEIP712Args, } from '../types/ICredentialEIP712' import { getEthTypesFromInputDoc } from 'eip-712-types-generation' /** * A Veramo plugin that implements the {@link ICredentialIssuerEIP712} methods. * * @beta This API may change without a BREAKING CHANGE notice. */ export class CredentialIssuerEIP712 implements IAgentPlugin { readonly methods: ICredentialIssuerEIP712 readonly schema = schema.ICredentialIssuerEIP712 constructor() { this.methods = { createVerifiableCredentialEIP712: this.createVerifiableCredentialEIP712.bind(this), createVerifiablePresentationEIP712: this.createVerifiablePresentationEIP712.bind(this), verifyCredentialEIP712: this.verifyCredentialEIP712.bind(this), verifyPresentationEIP712: this.verifyPresentationEIP712.bind(this), } } /** {@inheritdoc ICredentialIssuerEIP712.createVerifiableCredentialEIP712} */ public async createVerifiableCredentialEIP712( args: ICreateVerifiableCredentialEIP712Args, context: IRequiredContext, ): 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) 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) 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 = 1 if (identifier.did.split(':')[1] === 'ethr') chainId = getChainIdForDidEthr(extendedKey.meta.verificationMethod) const credential: CredentialPayload = { ...args?.credential, '@context': credentialContext, type: credentialType, issuanceDate, proof: { verificationMethod: extendedKey.meta.verificationMethod.id, created: issuanceDate, proofPurpose: 'assertionMethod', type: 'EthereumEip712Signature2021', }, } 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 }) const signature = await context.agent.keyManagerSign({ keyRef, data, algorithm: 'eth_signTypedData' }) credential['proof']['proofValue'] = signature credential['proof']['eip712'] = { domain, types: allTypes, primaryType, } return credential as VerifiableCredential } /** {@inheritdoc ICredentialIssuerEIP712.verifyCredentialEIP712} */ private async verifyCredentialEIP712( args: IVerifyCredentialEIP712Args, context: IRequiredContext, ): Promise<boolean> { const { credential } = args 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) if (didDocument.verificationMethod) { for (const verificationMethod of didDocument.verificationMethod) { if (getEthereumAddress(verificationMethod)?.toLowerCase() === recovered.toLowerCase()) { return true } } } else { throw new Error('resolver_error: issuer DIDDocument does not contain any verificationMethods') } return false } /** {@inheritdoc ICredentialIssuerEIP712.createVerifiablePresentationEIP712} */ async createVerifiablePresentationEIP712( args: ICreateVerifiablePresentationEIP712Args, context: IRequiredContext, ): 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) { const credentials = args.presentation.verifiableCredential.map((cred) => { // map JWT credentials to their canonical form if (typeof cred !== 'string' && cred.proof.jwt) { return cred.proof.jwt } else { return JSON.stringify(cred) } }) presentation.verifiableCredential = credentials } let identifier: IIdentifier try { identifier = await context.agent.didManagerGet({ did: presentation.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) 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 = 1 if (identifier.did.split(':')[1] === 'ethr') chainId = getChainIdForDidEthr(extendedKey.meta.verificationMethod) presentation['proof'] = { verificationMethod: extendedKey.meta.verificationMethod.id, created: issuanceDate, proofPurpose: 'assertionMethod', type: 'EthereumEip712Signature2021', } 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 }) const signature = await context.agent.keyManagerSign({ keyRef, data, algorithm: 'eth_signTypedData' }) presentation.proof.proofValue = signature presentation.proof.eip712 = { domain, types: allTypes, primaryType, } return presentation as VerifiablePresentation } /** {@inheritdoc ICredentialIssuerEIP712.verifyPresentationEIP712} */ private async verifyPresentationEIP712( args: IVerifyPresentationEIP712Args, context: IRequiredContext, ): Promise<boolean> { const { presentation } = args 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) if (didDocument.verificationMethod) { for (const verificationMethod of didDocument.verificationMethod) { if (getEthereumAddress(verificationMethod)?.toLowerCase() === recovered.toLowerCase()) { return true } } } else { throw new Error('resolver_error: holder DIDDocument does not contain any verificationMethods') } return false } }