import jsonld from 'jsonld';
import jsigs from 'jsonld-signatures';
import { statusTypeMatches, checkStatus } from '@digitalcredentials/vc-status-list';
import base64url from 'base64url';
import { CredentialBuilder, CredentialSchema } from '@docknetwork/crypto-wasm-ts';
import CredentialIssuancePurpose from './CredentialIssuancePurpose';
import defaultDocumentLoader from './document-loader';
import { getAndValidateSchemaIfPresent } from './schema';
import {
checkRevocationRegistryStatus, DockRevRegQualifier,
getCredentialStatus, isAccumulatorRevocationStatus,
isRegistryRevocationStatus, RevRegType,
} from '../revocation';
import { Resolver } from "../../resolver"; // eslint-disable-line
import {
getSuiteFromKeyDoc,
expandJSONLD,
getKeyFromDIDDocument, processIfKvac,
} from './helpers';
import {
DEFAULT_CONTEXT_V1_URL,
credentialContextField,
PrivateStatusList2021EntryType,
DockStatusList2021Qualifier,
StatusList2021EntryType,
PrivateStatusList2021Qualifier,
} from './constants';
import { ensureValidDatetime } from '../type-helpers';
import {
EcdsaSecp256k1Signature2019,
Ed25519Signature2018,
Ed25519Signature2020,
Sr25519Signature2020,
Bls12381PSSignatureDock2023,
Bls12381PSSignatureProofDock2023,
Bls12381BBSSignatureDock2022,
Bls12381BBSSignatureProofDock2022,
Bls12381BBSSignatureDock2023,
Bls12381BBSSignatureProofDock2023,
Bls12381BBS23SigProofDockSigName,
Bls12381PSSigProofDockSigName,
JsonWebSignature2020,
Bls12381PSSigDockSigName,
Bls12381BBSSigDockSigName,
Bls12381BBSSigProofDockSigName,
Bls12381BBS23SigDockSigName,
Bls12381BDDT16MacDockName,
Bls12381BDDT16MacProofDockName,
} from './custom_crypto';
import { signJWS } from './jws';
import Bls12381BDDT16MACProofDock2024 from './crypto/Bls12381BDDT16MACProofDock2024';
export const VC_ISSUE_TYPE_JSONLD = 'jsonld';
export const VC_ISSUE_TYPE_PROOFVALUE = 'proofValue';
export const VC_ISSUE_TYPE_JWT = 'jwt';
export const VC_ISSUE_TYPE_DEFAULT = VC_ISSUE_TYPE_JSONLD;
/**
* @param {string|object} obj - Object with ID property or a string
* @returns {string|undefined} Object's id property or the initial string value
* @private
*/
function getId(obj) {
if (!obj) {
return undefined;
}
if (typeof obj === 'string') {
return obj;
}
return obj.id;
}
function dateStringToTimestamp(dateStr) {
return Math.floor(Date.parse(dateStr) / 1000);
}
export function isAnoncredsProofType(verifiableCredential) {
const proofType = verifiableCredential.proof && verifiableCredential.proof.type;
return (
proofType === Bls12381BBSSigDockSigName
|| proofType === Bls12381BBSSigProofDockSigName
|| proofType === Bls12381BBS23SigProofDockSigName
|| proofType === Bls12381PSSigProofDockSigName
|| proofType === Bls12381BBS23SigDockSigName
|| proofType === Bls12381BDDT16MacDockName
|| proofType === Bls12381BDDT16MacProofDockName
|| proofType === Bls12381PSSigDockSigName
);
}
export function formatToJWTPayload(keyDoc, cred) {
const kid = keyDoc.id;
const credentialIssuer = cred.issuer;
const subject = cred.credentialSubject.id;
const { issuanceDate, expirationDate } = cred;
// NOTE: Expecting validFrom here for future spec support
const validFrom = cred.validFrom || issuanceDate;
// References: https://www.w3.org/TR/vc-data-model/#jwt-encoding
// https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6
const vcJwtPayload = {
jti: cred.id,
sub: subject || '',
iss: credentialIssuer.id || credentialIssuer,
iat: dateStringToTimestamp(issuanceDate),
vc: cred,
};
if (validFrom) {
vcJwtPayload.nbf = dateStringToTimestamp(validFrom);
}
if (expirationDate) {
vcJwtPayload.exp = dateStringToTimestamp(expirationDate);
}
const vcJwtHeader = {
typ: 'JWT',
kid,
};
return [vcJwtHeader, vcJwtPayload];
}
/**
* @param {object} credential - An object that could be a VerifiableCredential.
* @throws {Error}
*/
export function checkCredentialJSONLD(credential) {
// Ensure VerifiableCredential is listed in credential types
if (!jsonld.getValues(credential, 'type').includes('VerifiableCredential')) {
throw new Error('"type" must include `VerifiableCredential`.');
}
// Ensure issuanceDate cardinality
if (jsonld.getValues(credential, 'issuanceDate').length > 1) {
throw new Error('"issuanceDate" property can only have one value.');
}
// Ensure issuer cardinality
if (jsonld.getValues(credential, 'issuer').length > 1) {
throw new Error('"issuer" property can only have one value.');
}
// Ensure evidences are URIs
jsonld.getValues(credential, 'evidence').forEach((evidence) => {
const evidenceId = getId(evidence);
if (evidenceId && !evidenceId.includes(':')) {
throw new Error(`"evidence" id must be a URL: ${evidence}`);
}
});
}
/**
* @param {object} credential - An object that could be a VerifiableCredential.
* @throws {Error}
*/
export function checkCredentialRequired(credential) {
// Ensure first context is DEFAULT_CONTEXT_V1_URL
if (credential['@context'][0] !== DEFAULT_CONTEXT_V1_URL) {
throw new Error(
`"${DEFAULT_CONTEXT_V1_URL}" needs to be first in the contexts array.`,
);
}
// Ensure type property exists
if (!credential.type) {
throw new Error('"type" property is required.');
}
// Ensure credential has subject
if (!credential.credentialSubject) {
throw new Error('"credentialSubject" property is required.');
}
// Ensure issuer is valid
const issuer = getId(credential.issuer);
if (!issuer) {
throw new Error(
`"issuer" must be an object with ID property or a string. Got: ${credential.issuer}`,
);
} else if (!issuer.includes(':')) {
throw new Error('"issuer" id must be in URL format.');
}
// Ensure there is an issuance date, if exists
if (!credential.issuanceDate) {
throw new Error('"issuanceDate" property is required.');
} else {
ensureValidDatetime(credential.issuanceDate);
}
}
/**
* @param {object} credential - An object that could be a VerifiableCredential.
* @throws {Error}
*/
export function checkCredentialOptional(credential) {
// Ensure credential status is valid, if exists
if ('credentialStatus' in credential) {
if (!credential.credentialStatus.id) {
throw new Error('"credentialStatus" must include an id.');
}
if (!credential.credentialStatus.type) {
throw new Error('"credentialStatus" must include a type.');
}
}
// Ensure expiration date is valid, if exists
if ('expirationDate' in credential) {
ensureValidDatetime(credential.expirationDate);
}
}
/**
* @param {object} credential - An object that could be a VerifiableCredential.
* @throws {Error}
*/
export function checkCredential(credential) {
checkCredentialRequired(credential);
checkCredentialOptional(credential);
checkCredentialJSONLD(credential);
}
/**
* @typedef {object} VerifiableParams The Options to verify credentials and presentations.
* @property {string} [challenge] - proof challenge Required.
* @property {string} [domain] - proof domain (optional)
* @property {string} [controller] - controller (optional)
* @property {Resolver} [resolver] - Resolver to resolve the `DID`s/`StatusList`s/`Blob`s/Revocation registries (optional)
* @property {boolean} [unsignedPresentation] - Whether to verify the proof or not
* @property {boolean} [compactProof] - Whether to compact the JSON-LD or not.
* @property {boolean} [skipRevocationCheck=false] - Disables revocation check.
* **Warning, setting `skipRevocationCheck` to `true` can allow false positives when verifying revocable credentials.**
* @property {boolean} [skipSchemaCheck=false] - Disables schema check.
* **Warning, setting `skipSchemaCheck` to `true` can allow false positives when verifying revocable credentials.**
* @property {boolean} [verifyMatchingIssuersForRevocation=true] - ensure that status list credential issuer is same as credential issuer.
* **Will be used only if credential doesn't have `StatusList2021Entry` in `credentialStatus`.**
* @property {object} [purpose] - A purpose other than the default CredentialIssuancePurpose
* @property {object} [documentLoader] - A document loader, can be null and use the default
*/
/**
* Verify a Verifiable Credential. Returns the verification status and error in an object
* @param {object} [vcJSONorString] The VCDM Credential as JSON-LD or JWT string
* @param {VerifiableParams} options Verify parameters, this object is passed down to jsonld-signatures calls
* @return {Promise<object>} verification result. The returned object will have a key `verified` which is true if the
* credential is valid and not revoked and false otherwise. The `error` will describe the error if any.
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
export async function verifyCredential(
vcJSONorString,
{
resolver = null,
compactProof = true,
skipRevocationCheck = false,
skipSchemaCheck = false,
verifyMatchingIssuersForRevocation = true,
documentLoader = null,
purpose = null,
controller = null,
suite = [],
verifyDates = true,
// Anoncreds params
predicateParams = null,
accumulatorPublicKeys = null,
circomOutputs = null,
blindedAttributesCircomOutputs = null,
} = {},
) {
// Set document loader
const docLoader = getDocLoader(documentLoader, resolver);
const isJWT = typeof vcJSONorString === 'string';
const credential = isJWT
? JSON.parse(base64url.decode(vcJSONorString.split('.')[1])).vc
: vcJSONorString;
if (!credential) {
throw new TypeError('A "credential" property is required for verifying.');
}
// Check credential is valid
checkCredential(credential);
// Check expiration date
if (verifyDates && 'expirationDate' in credential) {
const expirationDate = new Date(credential.expirationDate);
const currentDate = new Date();
if (currentDate > expirationDate) {
const error = new Error('Credential has expired');
return {
verified: false,
error,
results: [
{
verified: false,
expirationDate,
error: {
name: error.name,
message: error.message,
},
},
],
};
}
}
// Expand credential JSON-LD
const expandedCredential = await expandJSONLD(credential, {
documentLoader: docLoader,
});
// Determine if we should validate the schema when verifying
// NOTE: derived anoncreds do not need JSON schema validation as the anoncreds library validates it
// and it can fail when required attributes are not revealed
const isAnoncredsDerived = isAnoncredsProofType(credential);
if (!skipSchemaCheck && !isAnoncredsDerived) {
await getAndValidateSchemaIfPresent(
expandedCredential,
credential[credentialContextField],
docLoader,
);
}
// JWT formatted credential?
if (isJWT) {
const jwtSplit = vcJSONorString.split('.');
if (jwtSplit.length !== 3) {
throw new Error('Malformed JWT');
}
const header = JSON.parse(base64url.decode(jwtSplit[0]).toString());
if (!header.kid) {
throw new Error('No kid in JWT header');
}
const { document: didDocument } = await docLoader(header.kid);
const keyDocument = getKeyFromDIDDocument(didDocument, header.kid);
const keyDocSuite = await getSuiteFromKeyDoc(keyDocument, false, {
detached: false,
header,
});
const verified = await keyDocSuite.verifySignature({
verifyData: new Uint8Array(Buffer.from(jwtSplit[1], 'utf8')),
verificationMethod: keyDocument,
proof: {
jws: vcJSONorString,
},
});
return { verified };
}
// Process and return the result if a KVAC credential
const r = processIfKvac(credential);
if (r) {
return r;
}
// Specify certain parameters for anoncreds
const anoncredsParams = {
accumulatorPublicKeys, predicateParams, circomOutputs, blindedAttributesCircomOutputs,
};
const fullSuite = [
new Ed25519Signature2018(),
new Ed25519Signature2020(),
new EcdsaSecp256k1Signature2019(),
new Sr25519Signature2020(),
new JsonWebSignature2020(),
new Bls12381BBSSignatureDock2022(anoncredsParams),
new Bls12381BBSSignatureProofDock2022(anoncredsParams),
new Bls12381BBSSignatureDock2023(anoncredsParams),
new Bls12381BBSSignatureProofDock2023(anoncredsParams),
new Bls12381PSSignatureDock2023(anoncredsParams),
new Bls12381PSSignatureProofDock2023(anoncredsParams),
// Only BDDT16MACProof is present and not BDDT16MAC since those aren't verified by the following
new Bls12381BDDT16MACProofDock2024(anoncredsParams),
...suite,
];
// Verify with jsonld-signatures otherwise
const result = await jsigs.verify(credential, {
purpose:
purpose
|| new CredentialIssuancePurpose({
controller,
}),
// TODO: support more key types, see digitalbazaar github
suite: fullSuite,
documentLoader: docLoader,
compactProof,
});
// Check for revocation only if the credential is verified and revocation check is needed.
if (result.verified && !skipRevocationCheck) {
const status = getCredentialStatus(expandedCredential);
if (status) {
const isStatusList2021Status = statusTypeMatches({ credential });
if (isStatusList2021Status) {
const revResult = await checkStatus({
credential,
suite: fullSuite,
documentLoader: docLoader,
verifyStatusListCredential: true,
verifyMatchingIssuers: verifyMatchingIssuersForRevocation,
});
// If revocation check fails, return the error else return the result of credential verification to avoid data loss.
if (!revResult.verified) {
if (!revResult.error) {
revResult.error = 'Credential was revoked (or suspended) according to the status list referenced in `credentialStatus`';
}
return revResult;
}
}
const isRegRevStatus = isRegistryRevocationStatus(status);
if (isRegRevStatus) {
const revResult = await checkRevocationRegistryStatus(
expandedCredential,
docLoader,
);
// If revocation check fails, return the error else return the result of credential verification to avoid data loss.
if (!revResult.verified) {
return revResult;
}
}
// Is using private status list or not
const isPrivStatus = getPrivateStatus(credential) !== undefined;
// For credentials supporting revocation with accumulator, the revocation check happens using witness which is not
// part of the credential and evolves over time
const isAccumStatus = isAccumulatorRevocationStatus(status);
if (!isStatusList2021Status && !isRegRevStatus && !isPrivStatus && !isAccumStatus) {
throw new Error(`Unsupported \`credentialStatus\`: \`${status}\``);
}
}
}
return result;
}
/**
* Issue a Verifiable credential
* @param {object} keyDoc - key document containing `id`, `controller`, `type`, `privateKeyBase58` and `publicKeyBase58`
* @param {object} credential - Credential to be signed.
* @param {boolean} [compactProof] - Whether to compact the JSON-LD or not.
* @param documentLoader
* @param purpose
* @param expansionMap
* @param {object} [issuerObject] - Optional issuer object to assign
* @param {boolean} [addSuiteContext] - Toggles the default
* behavior of each signature suite enforcing the presence of its own
* `@context` (if it is not present, it's added to the context list).
* @param {(jsonld|jwt|proofValue)} [type] - Optional format/type of the credential (JSON-LD, JWT, proofValue)
* @param resolver
* @return {Promise<object>} The signed credential object.
*/
export async function issueCredential(
keyDoc,
credential,
compactProof = true,
documentLoader = null,
purpose = null,
expansionMap,
issuerObject = null,
addSuiteContext = true,
type = VC_ISSUE_TYPE_DEFAULT,
resolver = null,
) {
// Set document loader
const docLoader = getDocLoader(documentLoader, resolver);
const useProofValue = type === VC_ISSUE_TYPE_PROOFVALUE;
const useJWT = type === VC_ISSUE_TYPE_JWT;
// Clone the credential object to prevent mutation
const issuerId = credential.issuer || keyDoc.controller;
const cred = {
...credential,
issuer: issuerObject
? {
...issuerObject,
id: issuerId,
}
: issuerId,
};
// Ensure credential is valid
checkCredential(cred);
// Should use JWT format?
if (useJWT) {
// Format to VC JWT spec
const [vcJwtHeader, vcJwtPayload] = formatToJWTPayload(keyDoc, cred);
// Get suite from keyDoc parameter
const jwtOpts = { detached: false, header: vcJwtHeader };
const suite = await getSuiteFromKeyDoc(keyDoc, false, jwtOpts);
return signJWS(suite.signer || suite, suite.alg, jwtOpts, vcJwtPayload);
}
// Get suite from keyDoc parameter
const suite = await getSuiteFromKeyDoc(keyDoc, useProofValue);
if (!suite.verificationMethod) {
throw new TypeError('"suite.verificationMethod" property is required.');
}
if (suite.requireCredentialSchema) {
// BBS+, BBS and PS require `cryptoVersion` key to be set. This version wont be overwritten but doesn't matter if it
// does, we only want to set the key
cred.cryptoVersion = CredentialBuilder.VERSION;
// Some suites (such as Dock BBS+) require a schema to exist
// we intentionally dont set the ID here because it will be auto generated on signing
if (!cred.credentialSchema) {
cred.credentialSchema = {
id: '',
type: 'JsonSchemaValidator2018',
};
}
}
// Sign and return the credential with jsonld-signatures otherwise
return jsigs.sign(cred, {
purpose: purpose || new CredentialIssuancePurpose(),
documentLoader: docLoader,
suite,
compactProof,
expansionMap,
addSuiteContext,
});
}
/**
* Get JSON-schema from the credential.
* @param credential
* @param full - when set to true, returns the JSON schema with properties. This might be a fetched schema
* @returns {IEmbeddedJsonSchema | IJsonSchema}
*/
export function getJsonSchemaFromCredential(credential, full = false) {
if (credential.credentialSchema === undefined) {
throw new Error('`credentialSchema` key must be defined in the credential');
}
if (typeof credential.credentialSchema.id !== 'string') {
throw new Error(`credentialSchema was expected to be string but was ${typeof credential.credentialSchema}`);
}
// eslint-disable-next-line no-nested-ternary
const key = full ? (credential.credentialSchema.fullJsonSchema !== undefined ? 'fullJsonSchema' : 'id') : 'id';
return CredentialSchema.convertFromDataUri(credential.credentialSchema[key]);
}
/**
* Get status of a credential issued with revocation type of private status list.
* @param credential
* @returns {*|Object|undefined}
*/
export function getPrivateStatus(credential) {
return credential.credentialStatus && credential.credentialStatus.type === PrivateStatusList2021EntryType ? credential.credentialStatus : undefined;
}
/**
* Verify the credential status given the private status list credential
* @param credentialStatus - `credentialStatus` field of the credential
* @param privateStatusListCredential
* @param documentLoader
* @param suite
* @param verifyStatusListCredential - Whether to verify the status list credential. This isn't necessary when the caller
* of this function got the credential directly from the issuer.
* @param expectedIssuer - Checks whether the issuer of the private status list credential matches the given
* @returns {Promise<{verified: boolean}>}
*/
export async function verifyPrivateStatus(credentialStatus, privateStatusListCredential, {
documentLoader = null, suite = [], verifyStatusListCredential = true, expectedIssuer = null,
}) {
const fullSuite = [
new Ed25519Signature2018(),
new EcdsaSecp256k1Signature2019(),
new Sr25519Signature2020(),
new JsonWebSignature2020(),
...suite,
];
const { statusPurpose: credentialStatusPurpose } = credentialStatus;
const { statusPurpose: slCredentialStatusPurpose } = privateStatusListCredential.credentialSubject;
if (slCredentialStatusPurpose !== credentialStatusPurpose) {
throw new Error(
`The status purpose "${slCredentialStatusPurpose}" of the status list credential does not match the status purpose "${credentialStatusPurpose}" in the credential.`,
);
}
// ensure that the issuer of the verifiable credential matches
if (typeof expectedIssuer === 'string') {
const issuer = typeof privateStatusListCredential.issuer === 'object' ? privateStatusListCredential.issuer.id : privateStatusListCredential.issuer;
if (issuer !== expectedIssuer) {
throw new Error(`Expected issuer to be ${expectedIssuer} but found ${issuer}`);
}
}
// verify VC
if (verifyStatusListCredential) {
const verifyResult = await verifyCredential(privateStatusListCredential.toJSON(), {
suite: fullSuite,
documentLoader,
});
if (!verifyResult.verified) {
throw new Error(`Status list credential failed to verify with error: ${verifyResult.error}`);
}
}
// check VC's SL index for the status
const { statusListIndex } = credentialStatus;
const index = parseInt(statusListIndex, 10);
const list = await privateStatusListCredential.decodedStatusList();
const verified = !list.getStatus(index);
return { verified };
}
/**
* Add revocation registry id to credential
* @param cred
* @param regId
* @returns {*}
*/
export function addRevRegIdToCredential(cred, regId) {
const newCred = { ...cred };
newCred.credentialStatus = {
id: `${DockRevRegQualifier}${regId}`,
type: RevRegType,
};
return newCred;
}
/**
* For setting revocation type of credential to status list 21
* @param cred
* @param statusListCredentialId
* @param statusListCredentialIndex
* @param purpose
* @returns {Object}
*/
export function addStatusList21EntryToCredential(
cred,
statusListCredentialId,
statusListCredentialIndex,
purpose,
) {
validateStatusPurpose(purpose);
return {
...cred,
credentialStatus: {
id: `${DockStatusList2021Qualifier}${statusListCredentialId}#${statusListCredentialIndex}`,
type: StatusList2021EntryType,
statusListIndex: String(statusListCredentialIndex),
statusListCredential: `${DockStatusList2021Qualifier}${statusListCredentialId}`,
statusPurpose: purpose,
},
};
}
/**
* For setting revocation type of credential to private status list 21
* @param cred
* @param statusListCredentialId
* @param statusListCredentialIndex
* @param purpose
* @returns {Object}
*/
export function addPrivateStatusListEntryToCredential(
cred,
statusListCredentialId,
statusListCredentialIndex,
purpose,
) {
validateStatusPurpose(purpose);
return {
...cred,
credentialStatus: {
id: `${PrivateStatusList2021Qualifier}${statusListCredentialId}#${statusListCredentialIndex}`,
type: PrivateStatusList2021EntryType,
statusListIndex: String(statusListCredentialIndex),
statusListCredential: `${PrivateStatusList2021Qualifier}${statusListCredentialId}`,
statusPurpose: purpose,
},
};
}
function validateStatusPurpose(purpose) {
if (purpose !== 'suspension' && purpose !== 'revocation') {
throw new Error(`statusPurpose must 'suspension' or 'revocation' but was '${purpose}'`);
}
}
function getDocLoader(documentLoader, resolver) {
if (documentLoader && resolver) {
throw new Error(
'Passing resolver and documentLoader results in resolver being ignored, please re-factor.',
);
}
return documentLoader || defaultDocumentLoader(resolver);
}