@veramo/credential-eip712
Version:
Veramo plugin for working with EIP712 Verifiable Credentials & Presentations.
303 lines • 13.4 kB
JavaScript
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