@veramo/credential-eip712
Version:
Veramo plugin for working with EIP712 Verifiable Credentials & Presentations.
247 lines • 11.4 kB
JavaScript
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 { 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 {
methods;
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} */
async createVerifiableCredentialEIP712(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);
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 = {
...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;
}
/** {@inheritdoc ICredentialIssuerEIP712.verifyCredentialEIP712} */
async verifyCredentialEIP712(args, context) {
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, 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) {
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;
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;
}
/** {@inheritdoc ICredentialIssuerEIP712.verifyPresentationEIP712} */
async verifyPresentationEIP712(args, context) {
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;
}
}
//# sourceMappingURL=CredentialEIP712.js.map