UNPKG

@arcblock/vc

Version:

TypeScript lib to work with ArcBlock Verifiable Credentials

295 lines (293 loc) 14.1 kB
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs'); const require_package = require('./package.cjs'); let _arcblock_did = require("@arcblock/did"); let _ocap_mcrypto = require("@ocap/mcrypto"); let _ocap_util = require("@ocap/util"); let _ocap_wallet = require("@ocap/wallet"); let debug = require("debug"); debug = require_rolldown_runtime.__toESM(debug); let is_absolute_url = require("is-absolute-url"); is_absolute_url = require_rolldown_runtime.__toESM(is_absolute_url); let json_stable_stringify = require("json-stable-stringify"); json_stable_stringify = require_rolldown_runtime.__toESM(json_stable_stringify); let lodash_cloneDeep = require("lodash/cloneDeep"); lodash_cloneDeep = require_rolldown_runtime.__toESM(lodash_cloneDeep); //#region src/index.ts /** * @fileOverview Utility functions to create/verify vc * * @module @arcblock/vc * @requires @arcblock/did * @requires @ocap/util */ const debug$1 = (0, debug.default)(require_package.name); const proofTypes = { [_ocap_mcrypto.types.KeyType.ED25519]: "Ed25519Signature", [_ocap_mcrypto.types.KeyType.SECP256K1]: "Secp256k1Signature", [_ocap_mcrypto.types.KeyType.ETHEREUM]: "EthereumSignature" }; /** * Create a valid verifiable credential * * @param params * @param params.type - The type of credential * @param params.subject - The content of credential * @param params.issuer - The issuer name and wallet * @param params.issuanceDate * @param params.expirationDate * @param params.endpoint - Status endpoint url * @param params.endpointScope - Endpoint scope, either be public or private * @returns Promise<object> */ async function create({ type, subject, issuer, issuanceDate, expirationDate, tag = "", endpoint = "", endpointScope = "public" }) { const typeArray = Array.isArray(type) ? [...type] : type ? [type] : []; if (typeArray.length === 0 || typeArray.some((t) => !t)) throw new Error("Can not create verifiable credential without empty type"); if (!typeArray.includes("VerifiableCredential")) typeArray.unshift("VerifiableCredential"); if (!subject) throw new Error("Can not create verifiable credential from empty subject"); if (!subject.id) throw new Error("Can not create verifiable credential without holder"); if (!(0, _arcblock_did.isValid)(subject.id)) throw new Error("Can not create verifiable credential invalid holder did"); if (endpoint && (0, is_absolute_url.default)(endpoint) === false) throw new Error("VC Endpoint must be absolute url"); if (endpointScope && ["public", "private"].includes(endpointScope) === false) throw new Error("VC Endpoint scope must be either public or private"); const { wallet, name: issuerName } = issuer; const issuerDid = wallet.address; const typeInfo = (0, _arcblock_did.toTypeInfo)(issuerDid); const vcType = { ...typeInfo, role: _ocap_mcrypto.types.RoleType.ROLE_VC }; const vcDid = (0, _arcblock_did.fromPublicKeyHash)(wallet.hash((0, json_stable_stringify.default)(subject)), vcType); const pkType = typeInfo.pk; if (!proofTypes[pkType]) throw new Error("Unsupported signer type when create verifiable credential"); const issuanceDateValue = issuanceDate || (/* @__PURE__ */ new Date()).toISOString(); const issuerPk = (0, _ocap_util.toBase58)(wallet.publicKey); const vcObj = { "@context": ["https://www.w3.org/2018/credentials/v1", "https://schema.arcblock.io/v0.1/context.jsonld"], id: vcDid, type: typeArray, issuer: { id: issuerDid, pk: issuerPk, name: issuerName || issuerDid }, issuanceDate: issuanceDateValue, expirationDate, credentialSubject: subject }; if (tag) vcObj.tag = tag; if (endpoint) vcObj.credentialStatus = { id: endpoint, type: "NFTStatusList2021", scope: endpointScope || "public" }; const signature = await wallet.sign((0, json_stable_stringify.default)(vcObj)); const result = { proof: { type: proofTypes[pkType], created: issuanceDateValue, proofPurpose: "assertionMethod", id: crypto.randomUUID(), signer: issuerDid, pk: issuerPk, jws: (0, _ocap_util.toBase64)(signature) }, ...vcObj }; debug$1("create", result); if (await verify({ vc: result, ownerDid: subject.id, trustedIssuers: [issuerDid] })) return result; return null; } /** * Verify that the verifiable credential is valid * - It is signed by a whitelist of issuers * - It is owned by the vc.subject.id * - It has valid signature by the issuer * - It is not expired * * @param vc - the verifiable credential object * @param ownerDid - vc holder/owner did * @param trustedIssuers - list of issuer did * @throws Error * @returns Promise<boolean> */ async function verify({ vc, ownerDid, trustedIssuers, ignoreExpired = false }) { if (!vc) throw new Error("Empty verifiable credential object"); if (!vc.issuer || !vc.issuer.id || !vc.issuer.pk || !(0, _arcblock_did.isValid)(vc.issuer.id)) throw new Error("Invalid verifiable credential issuer"); if (!vc.credentialSubject || !vc.credentialSubject.id || !(0, _arcblock_did.isValid)(vc.credentialSubject.id)) throw new Error("Invalid verifiable credential subject"); const proofList = Array.isArray(vc.proof) ? vc.proof : vc.proof ? [vc.proof] : []; if (proofList.length === 0 || proofList.some((p) => !p || !p.jws)) throw new Error("Invalid verifiable credential proof"); if (vc.issuanceDate === void 0) throw Error("Invalid verifiable credential issue date"); if (new Date(vc.issuanceDate).getTime() > Date.now()) throw Error("Verifiable credential has not take effect"); if (!ignoreExpired && vc.expirationDate !== void 0 && new Date(vc.expirationDate).getTime() < Date.now()) throw Error("Verifiable credential has expired"); const issuers = Array.isArray(trustedIssuers) ? trustedIssuers : [trustedIssuers]; if (!(0, _arcblock_did.isFromPublicKey)(vc.issuer.id, vc.issuer.pk)) throw new Error("Verifiable credential not issuer pk not match with issuer did"); if (ownerDid !== vc.credentialSubject.id) throw new Error("Verifiable credential not owned by specified owner did"); for (const proof of proofList) { const proofPk = proof.pk || vc.issuer.pk; const proofSigner = proof.signer || vc.issuer.id; if (!issuers.includes(proofSigner)) throw new Error("Proof signer not in trusted issuers"); if (proof.signer && proof.pk && !(0, _arcblock_did.isFromPublicKey)(proof.signer, proof.pk)) throw new Error("Proof pk does not match signer"); const clone = (0, lodash_cloneDeep.default)(vc); delete clone.signature; const prevProofRefs = proof.previousProof; if (prevProofRefs && (typeof prevProofRefs === "string" || Array.isArray(prevProofRefs) && prevProofRefs.length > 0)) { const prevIds = Array.isArray(prevProofRefs) ? prevProofRefs : [prevProofRefs]; const matchingProofs = proofList.filter((p) => p.id && prevIds.includes(p.id)); if (matchingProofs.length !== prevIds.length) throw new Error("Referenced previous proof not found"); clone.proof = matchingProofs; } else delete clone.proof; const signedContent = (0, json_stable_stringify.default)(clone); if (await (0, _ocap_wallet.fromPublicKey)(proofPk, (0, _arcblock_did.toTypeInfo)(proofSigner)).verify(signedContent, (0, _ocap_util.fromBase64)(proof.jws)) !== true) throw Error("Verifiable credential signature not valid"); } return true; } /** * Counter-sign an existing verifiable credential * Adds a new proof from a different signer to the VC * * @param params * @param params.vc - The already-signed verifiable credential * @param params.wallet - The counter-signer's wallet * @param params.mode - 'set' (independent proof) or 'chain' (references previous proofs) * @returns Promise<VerifiableCredential> */ async function counterSign({ vc, wallet, mode = "set" }) { if (!vc) throw new Error("Cannot counter-sign empty verifiable credential"); const existingProofs = Array.isArray(vc.proof) ? vc.proof : vc.proof ? [vc.proof] : []; if (existingProofs.length === 0) throw new Error("Cannot counter-sign verifiable credential without existing proof"); const signerDid = wallet.address; const pkType = (0, _arcblock_did.toTypeInfo)(signerDid).pk; if (!proofTypes[pkType]) throw new Error("Unsupported signer type when counter-signing verifiable credential"); const signerPk = (0, _ocap_util.toBase58)(wallet.publicKey); const proofsWithIds = existingProofs.map((p) => p.id ? p : { ...p, id: crypto.randomUUID() }); const clone = (0, lodash_cloneDeep.default)(vc); delete clone.signature; const newProof = { type: proofTypes[pkType], created: (/* @__PURE__ */ new Date()).toISOString(), proofPurpose: "assertionMethod", id: crypto.randomUUID(), signer: signerDid, pk: signerPk, jws: "" }; if (mode === "chain") { clone.proof = proofsWithIds; const prevIds = proofsWithIds.map((p) => p.id); newProof.previousProof = prevIds.length === 1 ? prevIds[0] : prevIds; } else delete clone.proof; const signedContent = (0, json_stable_stringify.default)(clone); newProof.jws = (0, _ocap_util.toBase64)(await wallet.sign(signedContent)); return { ...vc, proof: [...proofsWithIds, newProof] }; } /** * Verify that the Presentation is valid * - It is signed by VC's owner * - It contain challenge * - It has valid signature by the issuer * - It is not expired * * @param presentation - the presentation object * @param trustedIssuers - list of issuer did * @param challenge - Random byte you want * @throws Error * @returns Promise<boolean> */ async function verifyPresentation({ presentation, trustedIssuers, challenge, ignoreExpired = false }) { if (!presentation.challenge || challenge !== presentation.challenge) throw Error("Invalid challenge included on vc presentation"); const vcList = Array.isArray(presentation.verifiableCredential) ? presentation.verifiableCredential : [presentation.verifiableCredential]; const proofList = Array.isArray(presentation.proof) ? presentation.proof : [presentation.proof]; const clone = (0, lodash_cloneDeep.default)(presentation); delete clone.proof; await Promise.all(vcList.map(async (vcStr) => { const vcObj = JSON.parse(vcStr); const proof = proofList.find((x) => (0, _arcblock_did.isFromPublicKey)(vcObj.credentialSubject.id, x.pk)); if (!proof) throw Error(`VC does not have corresponding proof: ${vcStr}`); const signatureStr = proof.jws; if (await (0, _ocap_wallet.fromPublicKey)((0, _ocap_util.fromBase58)(proof.pk), (0, _arcblock_did.toTypeInfo)(vcObj.credentialSubject.id)).verify((0, json_stable_stringify.default)(clone), (0, _ocap_util.fromBase64)(signatureStr)) !== true) throw Error("Presentation signature invalid"); await verify({ vc: vcObj, ownerDid: vcObj.credentialSubject.id, trustedIssuers, ignoreExpired }); })); return true; } async function createCredentialList({ claims, issuer, issuanceDate }) { if (!claims || !Array.isArray(claims)) throw new Error("Can not create credential list with empty claim list"); if (!issuer || !issuer.wallet || !issuer.name) throw new Error("Can not create credential list with empty issuer name or wallet"); if (typeof issuer.wallet.sign !== "function") throw new Error("Can not create credential list with invalid issuer wallet"); const { wallet, name: name$1 } = issuer; const issuerDid = wallet.address; const typeInfo = (0, _arcblock_did.toTypeInfo)(issuerDid); const vcType = { ...typeInfo, role: _ocap_mcrypto.types.RoleType.ROLE_VC }; const issued = issuanceDate || (/* @__PURE__ */ new Date()).toISOString(); const pkType = typeInfo.pk; const issuerPk = (0, _ocap_util.toBase58)(wallet.publicKey); return await Promise.all(claims.map(async (x) => { const vc = { claim: x }; vc.id = (0, _arcblock_did.fromPublicKeyHash)(wallet.hash((0, json_stable_stringify.default)(vc.claim)), vcType); vc.issued = issued; vc.issuer = { id: issuerDid, pk: issuerPk, name: name$1 || issuerDid }; const signature = await wallet.sign((0, json_stable_stringify.default)(vc)); vc.proof = { type: proofTypes[pkType], created: issued, proofPurpose: "assertionMethod", id: crypto.randomUUID(), signer: issuerDid, pk: issuerPk, jws: (0, _ocap_util.toBase64)(signature) }; return vc; })); } async function verifyCredentialList({ credentials, trustedIssuers }) { if (!credentials || !Array.isArray(credentials)) throw new Error("Can not verify with empty credentials list"); return Promise.all(credentials.map(async (x) => { const issuers = Array.isArray(trustedIssuers) ? trustedIssuers : [trustedIssuers]; const issuerDid = issuers.find((d) => d === x.issuer.id); if (!issuerDid) throw new Error("Credential not issued by trusted issuers"); if (!(0, _arcblock_did.isFromPublicKey)(issuerDid, x.issuer.pk)) throw new Error("Credential not issuer pk not match with issuer did"); const proofList = Array.isArray(x.proof) ? x.proof : x.proof ? [x.proof] : []; if (proofList.length === 0 || proofList.some((p) => !p || !p.jws)) throw new Error("Invalid credential proof"); const clone = (0, lodash_cloneDeep.default)(x); delete clone.proof; const signedContent = (0, json_stable_stringify.default)(clone); for (const proof of proofList) { const proofPk = proof.pk || x.issuer.pk; const proofSigner = proof.signer || x.issuer.id; if (!issuers.includes(proofSigner)) throw new Error("Proof signer not in trusted issuers"); if (proof.signer && proof.pk && !(0, _arcblock_did.isFromPublicKey)(proof.signer, proof.pk)) throw new Error("Proof pk does not match signer"); if (await (0, _ocap_wallet.fromPublicKey)(proofPk, (0, _arcblock_did.toTypeInfo)(proofSigner)).verify(signedContent, (0, _ocap_util.fromBase64)(proof.jws)) !== true) throw Error("Status credential signature not valid"); } return x.claim; })); } const stableStringify = json_stable_stringify.default; //#endregion exports.counterSign = counterSign; exports.create = create; exports.createCredentialList = createCredentialList; exports.proofTypes = proofTypes; exports.stableStringify = stableStringify; exports.verify = verify; exports.verifyCredentialList = verifyCredentialList; exports.verifyPresentation = verifyPresentation;