@arcblock/vc
Version:
TypeScript lib to work with ArcBlock Verifiable Credentials
295 lines (293 loc) • 14.1 kB
JavaScript
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;