import jsonld from 'jsonld';
import jsigs from 'jsonld-signatures';
import {
BBSPlusPublicKeyG2,
BBSPublicKey,
PSPublicKey,
Presentation,
CredentialSchema,
} from '@docknetwork/crypto-wasm-ts';
import b58 from 'bs58';
import { getPrivateStatus, verifyCredential } from './credentials';
import DIDResolver from "../../resolver/did/did-resolver"; // eslint-disable-line
import defaultDocumentLoader from './document-loader';
import { getSuiteFromKeyDoc } from './helpers';
import {
Bls12381BBSSigDockSigName,
Bls12381PSSigDockSigName,
Bls12381BBS23SigDockSigName,
Bls12381BBSDockVerKeyName,
Bls12381PSDockVerKeyName,
Bls12381BBS23DockVerKeyName, Bls12381BDDT16DockVerKeyName, Bls12381BDDT16MacDockName,
} from './crypto/constants';
import { DEFAULT_CONTEXT_V1_URL } from './constants';
import {
EcdsaSecp256k1Signature2019,
Ed25519Signature2018,
Sr25519Signature2020,
JsonWebSignature2020,
Bls12381BBSSignatureDock2022,
Bls12381BBSSignatureDock2023,
Bls12381PSSignatureDock2023,
} from './custom_crypto';
const { AuthenticationProofPurpose } = jsigs.purposes;
/**
* @typedef {object} VerifiablePresentation Representation of a Verifiable Presentation.
*/
/**
* @param {object} presentation - An object that could be a presentation.
* @throws {Error}
* @private
*/
function checkPresentation(presentation) {
// Normalize to an array to allow the common case of context being a string
const context = Array.isArray(presentation['@context'])
? presentation['@context']
: [presentation['@context']];
// Ensure first context is 'https://www.w3.org/2018/credentials/v1'
if (context[0] !== DEFAULT_CONTEXT_V1_URL) {
throw new Error(
`"${DEFAULT_CONTEXT_V1_URL}" needs to be first in the `
+ 'list of contexts.',
);
}
// Ensure VerifiablePresentation exists in types
const types = jsonld.getValues(presentation, 'type');
if (!types.includes('VerifiablePresentation')) {
throw new Error('"type" must include "VerifiablePresentation".');
}
}
export async function verifyPresentationCredentials(
presentation,
options = {},
) {
let verified = true;
let credentialResults = [];
// Get presentation credentials
const credentials = jsonld.getValues(presentation, 'verifiableCredential');
if (credentials.length > 0) {
// Verify all credentials in list
credentialResults = await Promise.all(
credentials.map((credential) => verifyCredential(credential, { ...options })),
);
// Assign credentialId property to all credential results
for (const [i, credentialResult] of credentialResults.entries()) {
credentialResult.credentialId = credentials[i].id;
}
// Check all credentials passed verification
const allCredentialsVerified = credentialResults.every((r) => r.verified);
if (!allCredentialsVerified) {
verified = false;
}
}
return {
verified,
credentialResults,
};
}
/**
* @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 {DIDResolver} [resolver] - Resolver to resolve the issuer DID (optional)
* @property {Boolean} [unsignedPresentation] - Whether to verify the proof or not
* @property {Boolean} [compactProof] - Whether to compact the JSON-LD or not.
* @property {object} [presentationPurpose] - A purpose other than the default AuthenticationProofPurpose
* @property {object} [documentLoader] - A document loader, can be null and use the default
*/
/**
* Verify a Verifiable Presentation. Returns the verification status and error in an object
* @param {object} presentation The verifiable presentation
* @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
* presentation is valid and all the credentials are valid and not revoked and false otherwise. The `error` will
* describe the error if any.
*/
export async function verifyPresentation(presentation, options = {}) {
if (options.documentLoader && options.resolver) {
throw new Error(
'Passing resolver and documentLoader results in resolver being ignored, please re-factor.',
);
}
// Ensure presentation is passed
if (!presentation) {
throw new TypeError('"presentation" property is required');
}
if (isAnoncreds(presentation)) {
return verifyAnoncreds(presentation, options);
}
// Ensure presentation is valid
checkPresentation(presentation);
// Extract parameters
const {
challenge,
domain,
resolver,
unsignedPresentation = false,
presentationPurpose,
controller,
suite = [],
} = options;
// Build verification options
const verificationOptions = {
documentLoader: options.documentLoader || defaultDocumentLoader(resolver),
...options,
resolver: null,
suite: [
new Ed25519Signature2018(),
new EcdsaSecp256k1Signature2019(),
new Sr25519Signature2020(),
new JsonWebSignature2020(),
...suite,
],
};
// TODO: verify proof then credentials
const { verified, credentialResults } = await verifyPresentationCredentials(
presentation,
verificationOptions,
);
try {
// Skip proof validation for unsigned
if (unsignedPresentation) {
return { verified, results: [presentation], credentialResults };
}
// Early out incase credentials arent verified
if (!verified) {
return { verified, results: [presentation], credentialResults };
}
// Get proof purpose
if (!presentationPurpose && !challenge) {
throw new Error(
'A "challenge" param is required for AuthenticationProofPurpose.',
);
}
// Set purpose and verify
const purpose = presentationPurpose
|| new AuthenticationProofPurpose({ controller, domain, challenge });
const presentationResult = await jsigs.verify(presentation, {
purpose,
...verificationOptions,
});
// Return results
return {
presentationResult,
credentialResults,
verified: verified && presentationResult.verified,
error: presentationResult.error,
};
} catch (error) {
// Error occured when verifying presentation, catch and return error
return {
verified: false,
results: [{ verified: false, error }],
error,
};
}
}
/**
* Sign a Verifiable Presentation
* @param {object} presentation - the one to be signed
* @param {object} keyDoc - key document containing `id`, `controller`, `type`, `privateKeyBase58` and `publicKeyBase58`
* @param {string} challenge - proof challenge Required.
* @param {string} domain - proof domain (optional)
* @param {DIDResolver} [resolver] - Resolver for DIDs.
* @param {Boolean} [compactProof] - Whether to compact the JSON-LD or not.
* @param {object} [presentationPurpose] - Optional presentation purpose to override default AuthenticationProofPurpose
* @return {Promise<VerifiablePresentation>} A VerifiablePresentation with a proof.
*/
export async function signPresentation(
presentation,
keyDoc,
challenge,
domain,
resolver = null,
compactProof = true,
presentationPurpose = null,
addSuiteContext = true,
) {
const suite = await getSuiteFromKeyDoc(keyDoc);
const purpose = presentationPurpose
|| new AuthenticationProofPurpose({
domain,
challenge,
});
const documentLoader = defaultDocumentLoader(resolver);
const signed = await jsigs.sign(presentation, {
purpose,
documentLoader,
domain,
challenge,
compactProof,
suite,
addSuiteContext,
});
// Sometimes jsigs returns proof like [null, { proof }]
// check for that case here and if there's only 1 proof store object instead
if (Array.isArray(signed.proof)) {
const validProofs = signed.proof.filter((p) => !!p);
if (validProofs.length === 1) {
signed.proof = validProofs.pop();
}
}
return signed;
}
export function isAnoncreds(presentation) {
// Since there is no type parameter present we have to guess by checking field types
// these wont exist in a standard VP
return (
typeof presentation.version === 'string'
&& typeof presentation.proof === 'string'
&& typeof presentation.spec !== 'undefined'
&& typeof presentation.spec.credentials !== 'undefined'
);
}
/**
* Verify an anoncreds presentation given in JSON format
* @param presentation - object
* @param options - Any accumulator public keys, predicate params, etc required to verify the presentation.
* @returns {Promise<VerifyResult>}
*/
export async function verifyAnoncreds(presentation, options = {}) {
const documentLoader = options.documentLoader || defaultDocumentLoader(options.resolver);
const {
predicateParams, accumulatorPublicKeys,
circomOutputs, blindedAttributesCircomOutputs,
} = options;
const keyDocuments = await Promise.all(
presentation.spec.credentials.map((c, idx) => {
const { proof } = c.revealedAttributes;
if (!proof) {
throw new Error(
`Presentation credential does not reveal its proof for index ${idx}`,
);
}
let sigClass;
switch (proof.type) {
case Bls12381BBSSigDockSigName:
sigClass = Bls12381BBSSignatureDock2022;
break;
case Bls12381BBS23SigDockSigName:
sigClass = Bls12381BBSSignatureDock2023;
break;
case Bls12381PSSigDockSigName:
sigClass = Bls12381PSSignatureDock2023;
break;
case Bls12381BDDT16MacDockName:
return { type: Bls12381BDDT16DockVerKeyName };
default:
throw new Error(`Invalid proof type ${proof.type}`);
}
return sigClass.getVerificationMethod({ proof, documentLoader });
}),
);
const recreatedPres = Presentation.fromJSON(presentation);
const pks = new Map();
keyDocuments.forEach((keyDocument, i) => {
if (!keyDocument.type) {
throw new Error(`No type provided for key document ${JSON.stringify(keyDocument)}`);
}
// Question: Why would keyDocument.type start with `did:`
const keyType = keyDocument.type.startsWith('did:') ? keyDocument.type.slice(4) : keyDocument.type;
let Cls;
switch (keyType) {
case Bls12381BBSDockVerKeyName:
Cls = BBSPlusPublicKeyG2;
break;
case Bls12381BBS23DockVerKeyName:
Cls = BBSPublicKey;
break;
case Bls12381PSDockVerKeyName:
Cls = PSPublicKey;
break;
case Bls12381BDDT16DockVerKeyName:
return;
default:
throw new Error(`Invalid key document type: ${keyType}`);
}
const pkRaw = b58.decode(keyDocument.publicKeyBase58);
pks.set(i, new Cls(pkRaw));
});
return recreatedPres.verify(pks, accumulatorPublicKeys, predicateParams, circomOutputs, blindedAttributesCircomOutputs);
}
export function getDelegatedProofsFromVerifiedPresentation(presentation) {
if (!isAnoncreds(presentation)) {
throw new Error('Only anoncreds presentation supported');
}
const presObject = Presentation.fromJSON(presentation);
return presObject.getDelegatedProofs();
}
/**
* Get JSON-schemas of all credentials in the presentation
* @param presentation
* @param full - when set to true, returns the JSON schema of each credential with properties. This might be a fetched schema
* @returns {*}
*/
export function getJsonSchemasFromPresentation(presentation, full = false) {
return presentation.spec.credentials.map((cred) => {
const schema = CredentialSchema.fromJSON(JSON.parse(cred.schema));
// eslint-disable-next-line no-nested-ternary
const key = full ? (schema.fullJsonSchema !== undefined ? 'fullJsonSchema' : 'jsonSchema') : 'jsonSchema';
return schema[key];
});
}
/**
* Get status of all credentials from the presentation with revocation type of private status list.
* @param presentation
* @returns {Object[]}
*/
export function getPrivateStatuses(presentation) {
const credentials = jsonld.getValues(presentation, 'verifiableCredential');
return credentials.map((c) => getPrivateStatus(c));
}