@arcblock/vc 1.29.18 → 1.29.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/esm/index.d.mts CHANGED
@@ -7,7 +7,11 @@ interface Proof {
7
7
  created: string;
8
8
  proofPurpose: string;
9
9
  jws: string;
10
+ id?: string;
11
+ signer?: string;
10
12
  pk?: string;
13
+ previousProof?: string | string[];
14
+ verificationMethod?: string;
11
15
  }
12
16
  interface Issuer {
13
17
  id: string;
@@ -24,14 +28,14 @@ interface CredentialStatus {
24
28
  scope: string;
25
29
  }
26
30
  interface VerifiableCredential {
27
- '@context': string;
31
+ '@context': string | string[];
28
32
  id: string;
29
- type: string;
33
+ type: string | string[];
30
34
  issuer: Issuer;
31
35
  issuanceDate: string;
32
36
  expirationDate?: string;
33
37
  credentialSubject: CredentialSubject;
34
- proof: Proof;
38
+ proof: Proof | Proof[];
35
39
  tag?: string;
36
40
  credentialStatus?: CredentialStatus;
37
41
  signature?: unknown;
@@ -50,7 +54,7 @@ interface Credential {
50
54
  id: string;
51
55
  issued: string;
52
56
  issuer: Issuer;
53
- proof: Proof;
57
+ proof: Proof | Proof[];
54
58
  claim: unknown;
55
59
  }
56
60
  declare const proofTypes: Record<number, string>;
@@ -77,7 +81,7 @@ declare function create({
77
81
  endpoint,
78
82
  endpointScope
79
83
  }: {
80
- type: string;
84
+ type: string | string[];
81
85
  subject: CredentialSubject;
82
86
  issuer: IssuerInfo;
83
87
  issuanceDate?: string;
@@ -110,6 +114,25 @@ declare function verify({
110
114
  trustedIssuers: string | string[];
111
115
  ignoreExpired?: boolean;
112
116
  }): Promise<boolean>;
117
+ /**
118
+ * Counter-sign an existing verifiable credential
119
+ * Adds a new proof from a different signer to the VC
120
+ *
121
+ * @param params
122
+ * @param params.vc - The already-signed verifiable credential
123
+ * @param params.wallet - The counter-signer's wallet
124
+ * @param params.mode - 'set' (independent proof) or 'chain' (references previous proofs)
125
+ * @returns Promise<VerifiableCredential>
126
+ */
127
+ declare function counterSign({
128
+ vc,
129
+ wallet,
130
+ mode
131
+ }: {
132
+ vc: VerifiableCredential;
133
+ wallet: WalletObject;
134
+ mode?: 'set' | 'chain';
135
+ }): Promise<VerifiableCredential>;
113
136
  /**
114
137
  * Verify that the Presentation is valid
115
138
  * - It is signed by VC's owner
@@ -152,4 +175,4 @@ declare function verifyCredentialList({
152
175
  }): Promise<unknown[]>;
153
176
  declare const stableStringify: typeof stringify;
154
177
  //#endregion
155
- export { create, createCredentialList, proofTypes, stableStringify, verify, verifyCredentialList, verifyPresentation };
178
+ export { counterSign, create, createCredentialList, proofTypes, stableStringify, verify, verifyCredentialList, verifyPresentation };
package/esm/index.mjs CHANGED
@@ -36,7 +36,9 @@ const proofTypes = {
36
36
  * @returns Promise<object>
37
37
  */
38
38
  async function create({ type, subject, issuer, issuanceDate, expirationDate, tag = "", endpoint = "", endpointScope = "public" }) {
39
- if (!type) throw new Error("Can not create verifiable credential without empty type");
39
+ const typeArray = Array.isArray(type) ? [...type] : type ? [type] : [];
40
+ if (typeArray.length === 0 || typeArray.some((t) => !t)) throw new Error("Can not create verifiable credential without empty type");
41
+ if (!typeArray.includes("VerifiableCredential")) typeArray.unshift("VerifiableCredential");
40
42
  if (!subject) throw new Error("Can not create verifiable credential from empty subject");
41
43
  if (!subject.id) throw new Error("Can not create verifiable credential without holder");
42
44
  if (!isValid(subject.id)) throw new Error("Can not create verifiable credential invalid holder did");
@@ -50,14 +52,17 @@ async function create({ type, subject, issuer, issuanceDate, expirationDate, tag
50
52
  role: types.RoleType.ROLE_VC
51
53
  };
52
54
  const vcDid = fromPublicKeyHash(wallet.hash(stringify(subject)), vcType);
55
+ const pkType = typeInfo.pk;
56
+ if (!proofTypes[pkType]) throw new Error("Unsupported signer type when create verifiable credential");
53
57
  const issuanceDateValue = issuanceDate || (/* @__PURE__ */ new Date()).toISOString();
58
+ const issuerPk = toBase58(wallet.publicKey);
54
59
  const vcObj = {
55
- "@context": "https://schema.arcblock.io/v0.1/context.jsonld",
60
+ "@context": ["https://www.w3.org/2018/credentials/v1", "https://schema.arcblock.io/v0.1/context.jsonld"],
56
61
  id: vcDid,
57
- type,
62
+ type: typeArray,
58
63
  issuer: {
59
64
  id: issuerDid,
60
- pk: toBase58(wallet.publicKey),
65
+ pk: issuerPk,
61
66
  name: issuerName || issuerDid
62
67
  },
63
68
  issuanceDate: issuanceDateValue,
@@ -70,14 +75,15 @@ async function create({ type, subject, issuer, issuanceDate, expirationDate, tag
70
75
  type: "NFTStatusList2021",
71
76
  scope: endpointScope || "public"
72
77
  };
73
- const pkType = typeInfo.pk;
74
- if (!proofTypes[pkType]) throw new Error("Unsupported signer type when create verifiable credential");
75
78
  const signature = await wallet.sign(stringify(vcObj));
76
79
  const result = {
77
80
  proof: {
78
81
  type: proofTypes[pkType],
79
82
  created: issuanceDateValue,
80
83
  proofPurpose: "assertionMethod",
84
+ id: crypto.randomUUID(),
85
+ signer: issuerDid,
86
+ pk: issuerPk,
81
87
  jws: toBase64(signature)
82
88
  },
83
89
  ...vcObj
@@ -107,21 +113,77 @@ async function verify({ vc, ownerDid, trustedIssuers, ignoreExpired = false }) {
107
113
  if (!vc) throw new Error("Empty verifiable credential object");
108
114
  if (!vc.issuer || !vc.issuer.id || !vc.issuer.pk || !isValid(vc.issuer.id)) throw new Error("Invalid verifiable credential issuer");
109
115
  if (!vc.credentialSubject || !vc.credentialSubject.id || !isValid(vc.credentialSubject.id)) throw new Error("Invalid verifiable credential subject");
110
- if (!vc.proof || !vc.proof.jws) throw new Error("Invalid verifiable credential proof");
116
+ const proofList = Array.isArray(vc.proof) ? vc.proof : vc.proof ? [vc.proof] : [];
117
+ if (proofList.length === 0 || proofList.some((p) => !p || !p.jws)) throw new Error("Invalid verifiable credential proof");
111
118
  if (vc.issuanceDate === void 0) throw Error("Invalid verifiable credential issue date");
112
119
  if (new Date(vc.issuanceDate).getTime() > Date.now()) throw Error("Verifiable credential has not take effect");
113
120
  if (!ignoreExpired && vc.expirationDate !== void 0 && new Date(vc.expirationDate).getTime() < Date.now()) throw Error("Verifiable credential has expired");
114
- const issuerDid = (Array.isArray(trustedIssuers) ? trustedIssuers : [trustedIssuers]).find((x) => x === vc.issuer.id);
115
- if (!issuerDid) throw new Error("Verifiable credential not issued by trusted issuers");
116
- if (!isFromPublicKey(issuerDid, vc.issuer.pk)) throw new Error("Verifiable credential not issuer pk not match with issuer did");
121
+ const issuers = Array.isArray(trustedIssuers) ? trustedIssuers : [trustedIssuers];
122
+ if (!isFromPublicKey(vc.issuer.id, vc.issuer.pk)) throw new Error("Verifiable credential not issuer pk not match with issuer did");
117
123
  if (ownerDid !== vc.credentialSubject.id) throw new Error("Verifiable credential not owned by specified owner did");
118
- const issuerWallet = fromPublicKey(vc.issuer.pk, toTypeInfo(issuerDid));
124
+ for (const proof of proofList) {
125
+ const proofPk = proof.pk || vc.issuer.pk;
126
+ const proofSigner = proof.signer || vc.issuer.id;
127
+ if (!issuers.includes(proofSigner)) throw new Error("Proof signer not in trusted issuers");
128
+ if (proof.signer && proof.pk && !isFromPublicKey(proof.signer, proof.pk)) throw new Error("Proof pk does not match signer");
129
+ const clone = cloneDeep(vc);
130
+ delete clone.signature;
131
+ const prevProofRefs = proof.previousProof;
132
+ if (prevProofRefs && (typeof prevProofRefs === "string" || Array.isArray(prevProofRefs) && prevProofRefs.length > 0)) {
133
+ const prevIds = Array.isArray(prevProofRefs) ? prevProofRefs : [prevProofRefs];
134
+ const matchingProofs = proofList.filter((p) => p.id && prevIds.includes(p.id));
135
+ if (matchingProofs.length !== prevIds.length) throw new Error("Referenced previous proof not found");
136
+ clone.proof = matchingProofs;
137
+ } else delete clone.proof;
138
+ const signedContent = stringify(clone);
139
+ if (await fromPublicKey(proofPk, toTypeInfo(proofSigner)).verify(signedContent, fromBase64(proof.jws)) !== true) throw Error("Verifiable credential signature not valid");
140
+ }
141
+ return true;
142
+ }
143
+ /**
144
+ * Counter-sign an existing verifiable credential
145
+ * Adds a new proof from a different signer to the VC
146
+ *
147
+ * @param params
148
+ * @param params.vc - The already-signed verifiable credential
149
+ * @param params.wallet - The counter-signer's wallet
150
+ * @param params.mode - 'set' (independent proof) or 'chain' (references previous proofs)
151
+ * @returns Promise<VerifiableCredential>
152
+ */
153
+ async function counterSign({ vc, wallet, mode = "set" }) {
154
+ if (!vc) throw new Error("Cannot counter-sign empty verifiable credential");
155
+ const existingProofs = Array.isArray(vc.proof) ? vc.proof : vc.proof ? [vc.proof] : [];
156
+ if (existingProofs.length === 0) throw new Error("Cannot counter-sign verifiable credential without existing proof");
157
+ const signerDid = wallet.address;
158
+ const pkType = toTypeInfo(signerDid).pk;
159
+ if (!proofTypes[pkType]) throw new Error("Unsupported signer type when counter-signing verifiable credential");
160
+ const signerPk = toBase58(wallet.publicKey);
161
+ const proofsWithIds = existingProofs.map((p) => p.id ? p : {
162
+ ...p,
163
+ id: crypto.randomUUID()
164
+ });
119
165
  const clone = cloneDeep(vc);
120
- const signatureStr = clone.proof.jws;
121
- delete clone.proof;
122
166
  delete clone.signature;
123
- if (await issuerWallet.verify(stringify(clone), fromBase64(signatureStr)) !== true) throw Error("Verifiable credential signature not valid");
124
- return true;
167
+ const newProof = {
168
+ type: proofTypes[pkType],
169
+ created: (/* @__PURE__ */ new Date()).toISOString(),
170
+ proofPurpose: "assertionMethod",
171
+ id: crypto.randomUUID(),
172
+ signer: signerDid,
173
+ pk: signerPk,
174
+ jws: ""
175
+ };
176
+ if (mode === "chain") {
177
+ clone.proof = proofsWithIds;
178
+ const prevIds = proofsWithIds.map((p) => p.id);
179
+ newProof.previousProof = prevIds.length === 1 ? prevIds[0] : prevIds;
180
+ } else delete clone.proof;
181
+ const signedContent = stringify(clone);
182
+ newProof.jws = toBase64(await wallet.sign(signedContent));
183
+ return {
184
+ ...vc,
185
+ proof: [...proofsWithIds, newProof]
186
+ };
125
187
  }
126
188
  /**
127
189
  * Verify that the Presentation is valid
@@ -170,13 +232,14 @@ async function createCredentialList({ claims, issuer, issuanceDate }) {
170
232
  };
171
233
  const issued = issuanceDate || (/* @__PURE__ */ new Date()).toISOString();
172
234
  const pkType = typeInfo.pk;
235
+ const issuerPk = toBase58(wallet.publicKey);
173
236
  return await Promise.all(claims.map(async (x) => {
174
237
  const vc = { claim: x };
175
238
  vc.id = fromPublicKeyHash(wallet.hash(stringify(vc.claim)), vcType);
176
239
  vc.issued = issued;
177
240
  vc.issuer = {
178
241
  id: issuerDid,
179
- pk: toBase58(wallet.publicKey),
242
+ pk: issuerPk,
180
243
  name: name$1 || issuerDid
181
244
  };
182
245
  const signature = await wallet.sign(stringify(vc));
@@ -184,6 +247,9 @@ async function createCredentialList({ claims, issuer, issuanceDate }) {
184
247
  type: proofTypes[pkType],
185
248
  created: issued,
186
249
  proofPurpose: "assertionMethod",
250
+ id: crypto.randomUUID(),
251
+ signer: issuerDid,
252
+ pk: issuerPk,
187
253
  jws: toBase64(signature)
188
254
  };
189
255
  return vc;
@@ -192,18 +258,26 @@ async function createCredentialList({ claims, issuer, issuanceDate }) {
192
258
  async function verifyCredentialList({ credentials, trustedIssuers }) {
193
259
  if (!credentials || !Array.isArray(credentials)) throw new Error("Can not verify with empty credentials list");
194
260
  return Promise.all(credentials.map(async (x) => {
195
- const issuerDid = (Array.isArray(trustedIssuers) ? trustedIssuers : [trustedIssuers]).find((d) => d === x.issuer.id);
261
+ const issuers = Array.isArray(trustedIssuers) ? trustedIssuers : [trustedIssuers];
262
+ const issuerDid = issuers.find((d) => d === x.issuer.id);
196
263
  if (!issuerDid) throw new Error("Credential not issued by trusted issuers");
197
264
  if (!isFromPublicKey(issuerDid, x.issuer.pk)) throw new Error("Credential not issuer pk not match with issuer did");
198
- const issuerWallet = fromPublicKey(x.issuer.pk, toTypeInfo(issuerDid));
265
+ const proofList = Array.isArray(x.proof) ? x.proof : x.proof ? [x.proof] : [];
266
+ if (proofList.length === 0 || proofList.some((p) => !p || !p.jws)) throw new Error("Invalid credential proof");
199
267
  const clone = cloneDeep(x);
200
- const signatureStr = clone.proof.jws;
201
268
  delete clone.proof;
202
- if (await issuerWallet.verify(stringify(clone), fromBase64(signatureStr)) !== true) throw Error("Status credential signature not valid");
269
+ const signedContent = stringify(clone);
270
+ for (const proof of proofList) {
271
+ const proofPk = proof.pk || x.issuer.pk;
272
+ const proofSigner = proof.signer || x.issuer.id;
273
+ if (!issuers.includes(proofSigner)) throw new Error("Proof signer not in trusted issuers");
274
+ if (proof.signer && proof.pk && !isFromPublicKey(proof.signer, proof.pk)) throw new Error("Proof pk does not match signer");
275
+ if (await fromPublicKey(proofPk, toTypeInfo(proofSigner)).verify(signedContent, fromBase64(proof.jws)) !== true) throw Error("Status credential signature not valid");
276
+ }
203
277
  return x.claim;
204
278
  }));
205
279
  }
206
280
  const stableStringify = stringify;
207
281
 
208
282
  //#endregion
209
- export { create, createCredentialList, proofTypes, stableStringify, verify, verifyCredentialList, verifyPresentation };
283
+ export { counterSign, create, createCredentialList, proofTypes, stableStringify, verify, verifyCredentialList, verifyPresentation };
package/lib/index.cjs CHANGED
@@ -41,7 +41,9 @@ const proofTypes = {
41
41
  * @returns Promise<object>
42
42
  */
43
43
  async function create({ type, subject, issuer, issuanceDate, expirationDate, tag = "", endpoint = "", endpointScope = "public" }) {
44
- if (!type) throw new Error("Can not create verifiable credential without empty type");
44
+ const typeArray = Array.isArray(type) ? [...type] : type ? [type] : [];
45
+ if (typeArray.length === 0 || typeArray.some((t) => !t)) throw new Error("Can not create verifiable credential without empty type");
46
+ if (!typeArray.includes("VerifiableCredential")) typeArray.unshift("VerifiableCredential");
45
47
  if (!subject) throw new Error("Can not create verifiable credential from empty subject");
46
48
  if (!subject.id) throw new Error("Can not create verifiable credential without holder");
47
49
  if (!(0, _arcblock_did.isValid)(subject.id)) throw new Error("Can not create verifiable credential invalid holder did");
@@ -55,14 +57,17 @@ async function create({ type, subject, issuer, issuanceDate, expirationDate, tag
55
57
  role: _ocap_mcrypto.types.RoleType.ROLE_VC
56
58
  };
57
59
  const vcDid = (0, _arcblock_did.fromPublicKeyHash)(wallet.hash((0, json_stable_stringify.default)(subject)), vcType);
60
+ const pkType = typeInfo.pk;
61
+ if (!proofTypes[pkType]) throw new Error("Unsupported signer type when create verifiable credential");
58
62
  const issuanceDateValue = issuanceDate || (/* @__PURE__ */ new Date()).toISOString();
63
+ const issuerPk = (0, _ocap_util.toBase58)(wallet.publicKey);
59
64
  const vcObj = {
60
- "@context": "https://schema.arcblock.io/v0.1/context.jsonld",
65
+ "@context": ["https://www.w3.org/2018/credentials/v1", "https://schema.arcblock.io/v0.1/context.jsonld"],
61
66
  id: vcDid,
62
- type,
67
+ type: typeArray,
63
68
  issuer: {
64
69
  id: issuerDid,
65
- pk: (0, _ocap_util.toBase58)(wallet.publicKey),
70
+ pk: issuerPk,
66
71
  name: issuerName || issuerDid
67
72
  },
68
73
  issuanceDate: issuanceDateValue,
@@ -75,14 +80,15 @@ async function create({ type, subject, issuer, issuanceDate, expirationDate, tag
75
80
  type: "NFTStatusList2021",
76
81
  scope: endpointScope || "public"
77
82
  };
78
- const pkType = typeInfo.pk;
79
- if (!proofTypes[pkType]) throw new Error("Unsupported signer type when create verifiable credential");
80
83
  const signature = await wallet.sign((0, json_stable_stringify.default)(vcObj));
81
84
  const result = {
82
85
  proof: {
83
86
  type: proofTypes[pkType],
84
87
  created: issuanceDateValue,
85
88
  proofPurpose: "assertionMethod",
89
+ id: crypto.randomUUID(),
90
+ signer: issuerDid,
91
+ pk: issuerPk,
86
92
  jws: (0, _ocap_util.toBase64)(signature)
87
93
  },
88
94
  ...vcObj
@@ -112,21 +118,77 @@ async function verify({ vc, ownerDid, trustedIssuers, ignoreExpired = false }) {
112
118
  if (!vc) throw new Error("Empty verifiable credential object");
113
119
  if (!vc.issuer || !vc.issuer.id || !vc.issuer.pk || !(0, _arcblock_did.isValid)(vc.issuer.id)) throw new Error("Invalid verifiable credential issuer");
114
120
  if (!vc.credentialSubject || !vc.credentialSubject.id || !(0, _arcblock_did.isValid)(vc.credentialSubject.id)) throw new Error("Invalid verifiable credential subject");
115
- if (!vc.proof || !vc.proof.jws) throw new Error("Invalid verifiable credential proof");
121
+ const proofList = Array.isArray(vc.proof) ? vc.proof : vc.proof ? [vc.proof] : [];
122
+ if (proofList.length === 0 || proofList.some((p) => !p || !p.jws)) throw new Error("Invalid verifiable credential proof");
116
123
  if (vc.issuanceDate === void 0) throw Error("Invalid verifiable credential issue date");
117
124
  if (new Date(vc.issuanceDate).getTime() > Date.now()) throw Error("Verifiable credential has not take effect");
118
125
  if (!ignoreExpired && vc.expirationDate !== void 0 && new Date(vc.expirationDate).getTime() < Date.now()) throw Error("Verifiable credential has expired");
119
- const issuerDid = (Array.isArray(trustedIssuers) ? trustedIssuers : [trustedIssuers]).find((x) => x === vc.issuer.id);
120
- if (!issuerDid) throw new Error("Verifiable credential not issued by trusted issuers");
121
- if (!(0, _arcblock_did.isFromPublicKey)(issuerDid, vc.issuer.pk)) throw new Error("Verifiable credential not issuer pk not match with issuer did");
126
+ const issuers = Array.isArray(trustedIssuers) ? trustedIssuers : [trustedIssuers];
127
+ if (!(0, _arcblock_did.isFromPublicKey)(vc.issuer.id, vc.issuer.pk)) throw new Error("Verifiable credential not issuer pk not match with issuer did");
122
128
  if (ownerDid !== vc.credentialSubject.id) throw new Error("Verifiable credential not owned by specified owner did");
123
- const issuerWallet = (0, _ocap_wallet.fromPublicKey)(vc.issuer.pk, (0, _arcblock_did.toTypeInfo)(issuerDid));
129
+ for (const proof of proofList) {
130
+ const proofPk = proof.pk || vc.issuer.pk;
131
+ const proofSigner = proof.signer || vc.issuer.id;
132
+ if (!issuers.includes(proofSigner)) throw new Error("Proof signer not in trusted issuers");
133
+ if (proof.signer && proof.pk && !(0, _arcblock_did.isFromPublicKey)(proof.signer, proof.pk)) throw new Error("Proof pk does not match signer");
134
+ const clone = (0, lodash_cloneDeep.default)(vc);
135
+ delete clone.signature;
136
+ const prevProofRefs = proof.previousProof;
137
+ if (prevProofRefs && (typeof prevProofRefs === "string" || Array.isArray(prevProofRefs) && prevProofRefs.length > 0)) {
138
+ const prevIds = Array.isArray(prevProofRefs) ? prevProofRefs : [prevProofRefs];
139
+ const matchingProofs = proofList.filter((p) => p.id && prevIds.includes(p.id));
140
+ if (matchingProofs.length !== prevIds.length) throw new Error("Referenced previous proof not found");
141
+ clone.proof = matchingProofs;
142
+ } else delete clone.proof;
143
+ const signedContent = (0, json_stable_stringify.default)(clone);
144
+ 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");
145
+ }
146
+ return true;
147
+ }
148
+ /**
149
+ * Counter-sign an existing verifiable credential
150
+ * Adds a new proof from a different signer to the VC
151
+ *
152
+ * @param params
153
+ * @param params.vc - The already-signed verifiable credential
154
+ * @param params.wallet - The counter-signer's wallet
155
+ * @param params.mode - 'set' (independent proof) or 'chain' (references previous proofs)
156
+ * @returns Promise<VerifiableCredential>
157
+ */
158
+ async function counterSign({ vc, wallet, mode = "set" }) {
159
+ if (!vc) throw new Error("Cannot counter-sign empty verifiable credential");
160
+ const existingProofs = Array.isArray(vc.proof) ? vc.proof : vc.proof ? [vc.proof] : [];
161
+ if (existingProofs.length === 0) throw new Error("Cannot counter-sign verifiable credential without existing proof");
162
+ const signerDid = wallet.address;
163
+ const pkType = (0, _arcblock_did.toTypeInfo)(signerDid).pk;
164
+ if (!proofTypes[pkType]) throw new Error("Unsupported signer type when counter-signing verifiable credential");
165
+ const signerPk = (0, _ocap_util.toBase58)(wallet.publicKey);
166
+ const proofsWithIds = existingProofs.map((p) => p.id ? p : {
167
+ ...p,
168
+ id: crypto.randomUUID()
169
+ });
124
170
  const clone = (0, lodash_cloneDeep.default)(vc);
125
- const signatureStr = clone.proof.jws;
126
- delete clone.proof;
127
171
  delete clone.signature;
128
- if (await issuerWallet.verify((0, json_stable_stringify.default)(clone), (0, _ocap_util.fromBase64)(signatureStr)) !== true) throw Error("Verifiable credential signature not valid");
129
- return true;
172
+ const newProof = {
173
+ type: proofTypes[pkType],
174
+ created: (/* @__PURE__ */ new Date()).toISOString(),
175
+ proofPurpose: "assertionMethod",
176
+ id: crypto.randomUUID(),
177
+ signer: signerDid,
178
+ pk: signerPk,
179
+ jws: ""
180
+ };
181
+ if (mode === "chain") {
182
+ clone.proof = proofsWithIds;
183
+ const prevIds = proofsWithIds.map((p) => p.id);
184
+ newProof.previousProof = prevIds.length === 1 ? prevIds[0] : prevIds;
185
+ } else delete clone.proof;
186
+ const signedContent = (0, json_stable_stringify.default)(clone);
187
+ newProof.jws = (0, _ocap_util.toBase64)(await wallet.sign(signedContent));
188
+ return {
189
+ ...vc,
190
+ proof: [...proofsWithIds, newProof]
191
+ };
130
192
  }
131
193
  /**
132
194
  * Verify that the Presentation is valid
@@ -175,13 +237,14 @@ async function createCredentialList({ claims, issuer, issuanceDate }) {
175
237
  };
176
238
  const issued = issuanceDate || (/* @__PURE__ */ new Date()).toISOString();
177
239
  const pkType = typeInfo.pk;
240
+ const issuerPk = (0, _ocap_util.toBase58)(wallet.publicKey);
178
241
  return await Promise.all(claims.map(async (x) => {
179
242
  const vc = { claim: x };
180
243
  vc.id = (0, _arcblock_did.fromPublicKeyHash)(wallet.hash((0, json_stable_stringify.default)(vc.claim)), vcType);
181
244
  vc.issued = issued;
182
245
  vc.issuer = {
183
246
  id: issuerDid,
184
- pk: (0, _ocap_util.toBase58)(wallet.publicKey),
247
+ pk: issuerPk,
185
248
  name: name$1 || issuerDid
186
249
  };
187
250
  const signature = await wallet.sign((0, json_stable_stringify.default)(vc));
@@ -189,6 +252,9 @@ async function createCredentialList({ claims, issuer, issuanceDate }) {
189
252
  type: proofTypes[pkType],
190
253
  created: issued,
191
254
  proofPurpose: "assertionMethod",
255
+ id: crypto.randomUUID(),
256
+ signer: issuerDid,
257
+ pk: issuerPk,
192
258
  jws: (0, _ocap_util.toBase64)(signature)
193
259
  };
194
260
  return vc;
@@ -197,20 +263,29 @@ async function createCredentialList({ claims, issuer, issuanceDate }) {
197
263
  async function verifyCredentialList({ credentials, trustedIssuers }) {
198
264
  if (!credentials || !Array.isArray(credentials)) throw new Error("Can not verify with empty credentials list");
199
265
  return Promise.all(credentials.map(async (x) => {
200
- const issuerDid = (Array.isArray(trustedIssuers) ? trustedIssuers : [trustedIssuers]).find((d) => d === x.issuer.id);
266
+ const issuers = Array.isArray(trustedIssuers) ? trustedIssuers : [trustedIssuers];
267
+ const issuerDid = issuers.find((d) => d === x.issuer.id);
201
268
  if (!issuerDid) throw new Error("Credential not issued by trusted issuers");
202
269
  if (!(0, _arcblock_did.isFromPublicKey)(issuerDid, x.issuer.pk)) throw new Error("Credential not issuer pk not match with issuer did");
203
- const issuerWallet = (0, _ocap_wallet.fromPublicKey)(x.issuer.pk, (0, _arcblock_did.toTypeInfo)(issuerDid));
270
+ const proofList = Array.isArray(x.proof) ? x.proof : x.proof ? [x.proof] : [];
271
+ if (proofList.length === 0 || proofList.some((p) => !p || !p.jws)) throw new Error("Invalid credential proof");
204
272
  const clone = (0, lodash_cloneDeep.default)(x);
205
- const signatureStr = clone.proof.jws;
206
273
  delete clone.proof;
207
- if (await issuerWallet.verify((0, json_stable_stringify.default)(clone), (0, _ocap_util.fromBase64)(signatureStr)) !== true) throw Error("Status credential signature not valid");
274
+ const signedContent = (0, json_stable_stringify.default)(clone);
275
+ for (const proof of proofList) {
276
+ const proofPk = proof.pk || x.issuer.pk;
277
+ const proofSigner = proof.signer || x.issuer.id;
278
+ if (!issuers.includes(proofSigner)) throw new Error("Proof signer not in trusted issuers");
279
+ if (proof.signer && proof.pk && !(0, _arcblock_did.isFromPublicKey)(proof.signer, proof.pk)) throw new Error("Proof pk does not match signer");
280
+ 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");
281
+ }
208
282
  return x.claim;
209
283
  }));
210
284
  }
211
285
  const stableStringify = json_stable_stringify.default;
212
286
 
213
287
  //#endregion
288
+ exports.counterSign = counterSign;
214
289
  exports.create = create;
215
290
  exports.createCredentialList = createCredentialList;
216
291
  exports.proofTypes = proofTypes;
package/lib/index.d.cts CHANGED
@@ -7,7 +7,11 @@ interface Proof {
7
7
  created: string;
8
8
  proofPurpose: string;
9
9
  jws: string;
10
+ id?: string;
11
+ signer?: string;
10
12
  pk?: string;
13
+ previousProof?: string | string[];
14
+ verificationMethod?: string;
11
15
  }
12
16
  interface Issuer {
13
17
  id: string;
@@ -24,14 +28,14 @@ interface CredentialStatus {
24
28
  scope: string;
25
29
  }
26
30
  interface VerifiableCredential {
27
- '@context': string;
31
+ '@context': string | string[];
28
32
  id: string;
29
- type: string;
33
+ type: string | string[];
30
34
  issuer: Issuer;
31
35
  issuanceDate: string;
32
36
  expirationDate?: string;
33
37
  credentialSubject: CredentialSubject;
34
- proof: Proof;
38
+ proof: Proof | Proof[];
35
39
  tag?: string;
36
40
  credentialStatus?: CredentialStatus;
37
41
  signature?: unknown;
@@ -50,7 +54,7 @@ interface Credential {
50
54
  id: string;
51
55
  issued: string;
52
56
  issuer: Issuer;
53
- proof: Proof;
57
+ proof: Proof | Proof[];
54
58
  claim: unknown;
55
59
  }
56
60
  declare const proofTypes: Record<number, string>;
@@ -77,7 +81,7 @@ declare function create({
77
81
  endpoint,
78
82
  endpointScope
79
83
  }: {
80
- type: string;
84
+ type: string | string[];
81
85
  subject: CredentialSubject;
82
86
  issuer: IssuerInfo;
83
87
  issuanceDate?: string;
@@ -110,6 +114,25 @@ declare function verify({
110
114
  trustedIssuers: string | string[];
111
115
  ignoreExpired?: boolean;
112
116
  }): Promise<boolean>;
117
+ /**
118
+ * Counter-sign an existing verifiable credential
119
+ * Adds a new proof from a different signer to the VC
120
+ *
121
+ * @param params
122
+ * @param params.vc - The already-signed verifiable credential
123
+ * @param params.wallet - The counter-signer's wallet
124
+ * @param params.mode - 'set' (independent proof) or 'chain' (references previous proofs)
125
+ * @returns Promise<VerifiableCredential>
126
+ */
127
+ declare function counterSign({
128
+ vc,
129
+ wallet,
130
+ mode
131
+ }: {
132
+ vc: VerifiableCredential;
133
+ wallet: WalletObject;
134
+ mode?: 'set' | 'chain';
135
+ }): Promise<VerifiableCredential>;
113
136
  /**
114
137
  * Verify that the Presentation is valid
115
138
  * - It is signed by VC's owner
@@ -152,4 +175,4 @@ declare function verifyCredentialList({
152
175
  }): Promise<unknown[]>;
153
176
  declare const stableStringify: typeof stringify;
154
177
  //#endregion
155
- export { create, createCredentialList, proofTypes, stableStringify, verify, verifyCredentialList, verifyPresentation };
178
+ export { counterSign, create, createCredentialList, proofTypes, stableStringify, verify, verifyCredentialList, verifyPresentation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/vc",
3
- "version": "1.29.18",
3
+ "version": "1.29.20",
4
4
  "description": "TypeScript lib to work with ArcBlock Verifiable Credentials",
5
5
  "keywords": [
6
6
  "arcblock",
@@ -72,10 +72,10 @@
72
72
  "url": "https://github.com/ArcBlock/blockchain/issues"
73
73
  },
74
74
  "dependencies": {
75
- "@arcblock/did": "1.29.18",
76
- "@ocap/mcrypto": "1.29.18",
77
- "@ocap/util": "1.29.18",
78
- "@ocap/wallet": "1.29.18",
75
+ "@arcblock/did": "1.29.20",
76
+ "@ocap/mcrypto": "1.29.20",
77
+ "@ocap/util": "1.29.20",
78
+ "@ocap/wallet": "1.29.20",
79
79
  "debug": "^4.4.3",
80
80
  "is-absolute-url": "^3.0.3",
81
81
  "json-stable-stringify": "^1.0.1",