@dwk/vc 0.1.0-beta.0
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/LICENSE +15 -0
- package/README.md +143 -0
- package/dist/config.d.ts +97 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +62 -0
- package/dist/config.js.map +1 -0
- package/dist/credential.d.ts +70 -0
- package/dist/credential.d.ts.map +1 -0
- package/dist/credential.js +139 -0
- package/dist/credential.js.map +1 -0
- package/dist/data-integrity.d.ts +102 -0
- package/dist/data-integrity.d.ts.map +1 -0
- package/dist/data-integrity.js +253 -0
- package/dist/data-integrity.js.map +1 -0
- package/dist/datetime.d.ts +26 -0
- package/dist/datetime.d.ts.map +1 -0
- package/dist/datetime.js +54 -0
- package/dist/datetime.js.map +1 -0
- package/dist/did-web.d.ts +93 -0
- package/dist/did-web.d.ts.map +1 -0
- package/dist/did-web.js +206 -0
- package/dist/did-web.js.map +1 -0
- package/dist/handler.d.ts +37 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +362 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/jcs.d.ts +31 -0
- package/dist/jcs.d.ts.map +1 -0
- package/dist/jcs.js +67 -0
- package/dist/jcs.js.map +1 -0
- package/dist/log.d.ts +34 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +32 -0
- package/dist/log.js.map +1 -0
- package/dist/multibase.d.ts +57 -0
- package/dist/multibase.d.ts.map +1 -0
- package/dist/multibase.js +165 -0
- package/dist/multibase.js.map +1 -0
- package/dist/status-list.d.ts +116 -0
- package/dist/status-list.d.ts.map +1 -0
- package/dist/status-list.js +241 -0
- package/dist/status-list.js.map +1 -0
- package/package.json +48 -0
- package/src/config.ts +158 -0
- package/src/credential.ts +188 -0
- package/src/data-integrity.ts +425 -0
- package/src/datetime.ts +57 -0
- package/src/did-web.ts +273 -0
- package/src/handler.ts +477 -0
- package/src/index.ts +133 -0
- package/src/jcs.ts +83 -0
- package/src/log.ts +35 -0
- package/src/multibase.ts +189 -0
- package/src/status-list.ts +356 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Integrity proofs over JSON credentials, using the JCS cryptosuites.
|
|
3
|
+
*
|
|
4
|
+
* Implements `eddsa-jcs-2022` (Ed25519) and `ecdsa-jcs-2019` (ECDSA P-256 /
|
|
5
|
+
* P-384) from the W3C Data Integrity specs. The proof pipeline is:
|
|
6
|
+
*
|
|
7
|
+
* 1. Build a **proof configuration** (the proof object minus `proofValue`),
|
|
8
|
+
* copying the document's `@context`.
|
|
9
|
+
* 2. JCS-canonicalize the proof config and the document-without-proof, hash each
|
|
10
|
+
* with the cryptosuite's digest, and concatenate `proofConfigHash ‖ docHash`.
|
|
11
|
+
* 3. Sign (or verify) that concatenation with the verification key.
|
|
12
|
+
*
|
|
13
|
+
* The proof value is multibase base58-btc. ECDSA signatures use the IEEE P1363
|
|
14
|
+
* (`r ‖ s`) form Web Crypto produces, which is exactly what the cryptosuite
|
|
15
|
+
* expects. Signing/verification mirror the `@dwk/dpop` and `@dwk/http-signatures`
|
|
16
|
+
* posture — asymmetric only, an explicit cryptosuite allow-list, and the key is
|
|
17
|
+
* validated against the claimed cryptosuite.
|
|
18
|
+
*
|
|
19
|
+
* Pure aside from Web Crypto: plain-data in, plain-data out, no runtime bindings.
|
|
20
|
+
*
|
|
21
|
+
* @see https://www.w3.org/TR/vc-di-eddsa/
|
|
22
|
+
* @see https://www.w3.org/TR/vc-di-ecdsa/
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { toXsdDateTime } from "./datetime";
|
|
26
|
+
import { canonicalize, canonicalizeToBytes, type JcsValue } from "./jcs";
|
|
27
|
+
import {
|
|
28
|
+
base58btcDecode,
|
|
29
|
+
decodeMultikey,
|
|
30
|
+
encodeMultibaseBase58btc,
|
|
31
|
+
MULTIBASE_BASE58BTC,
|
|
32
|
+
} from "./multibase";
|
|
33
|
+
|
|
34
|
+
/** A JSON object with string keys — a credential, proof, or proof config. */
|
|
35
|
+
export type JsonObject = { [key: string]: JcsValue | undefined };
|
|
36
|
+
|
|
37
|
+
/** The Data Integrity cryptosuites this package implements. */
|
|
38
|
+
export type Cryptosuite = "eddsa-jcs-2022" | "ecdsa-jcs-2019";
|
|
39
|
+
|
|
40
|
+
/** Whether `value` names a supported cryptosuite. */
|
|
41
|
+
export function isSupportedCryptosuite(value: unknown): value is Cryptosuite {
|
|
42
|
+
return value === "eddsa-jcs-2022" || value === "ecdsa-jcs-2019";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type ComponentHash = "SHA-256" | "SHA-384";
|
|
46
|
+
|
|
47
|
+
// Structural Web Crypto algorithm/param shapes. We avoid the DOM lib names
|
|
48
|
+
// (EcKeyImportParams, EcdsaParams, …) because @cloudflare/workers-types does not
|
|
49
|
+
// declare them; these objects are accepted structurally by crypto.subtle.
|
|
50
|
+
interface AlgParams {
|
|
51
|
+
name: string;
|
|
52
|
+
namedCurve?: string;
|
|
53
|
+
hash?: string;
|
|
54
|
+
}
|
|
55
|
+
type SignVerifyParams = AlgParams;
|
|
56
|
+
|
|
57
|
+
/** A resolved signing key plus the cryptosuite parameters it implies. */
|
|
58
|
+
export interface Signer {
|
|
59
|
+
readonly key: CryptoKey;
|
|
60
|
+
readonly cryptosuite: Cryptosuite;
|
|
61
|
+
readonly componentHash: ComponentHash;
|
|
62
|
+
readonly params: SignVerifyParams;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ecParamsForCurve(curve: string): {
|
|
66
|
+
cryptosuite: Cryptosuite;
|
|
67
|
+
componentHash: ComponentHash;
|
|
68
|
+
params: SignVerifyParams;
|
|
69
|
+
} {
|
|
70
|
+
if (curve === "P-256") {
|
|
71
|
+
return {
|
|
72
|
+
cryptosuite: "ecdsa-jcs-2019",
|
|
73
|
+
componentHash: "SHA-256",
|
|
74
|
+
params: { name: "ECDSA", hash: "SHA-256" },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (curve === "P-384") {
|
|
78
|
+
return {
|
|
79
|
+
cryptosuite: "ecdsa-jcs-2019",
|
|
80
|
+
componentHash: "SHA-384",
|
|
81
|
+
params: { name: "ECDSA", hash: "SHA-384" },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
throw new Error(
|
|
85
|
+
`@dwk/vc: unsupported EC curve "${curve}" for ecdsa-jcs-2019`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Import a private signing key from a JWK and resolve the cryptosuite it
|
|
91
|
+
* implies: `OKP`/`Ed25519` → `eddsa-jcs-2022`; `EC`/`P-256`|`P-384` →
|
|
92
|
+
* `ecdsa-jcs-2019`. Throws for unsupported or non-private keys.
|
|
93
|
+
*/
|
|
94
|
+
export async function importSigner(jwk: JsonWebKey): Promise<Signer> {
|
|
95
|
+
if (jwk.d === undefined) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"@dwk/vc: signing key JWK is missing the private `d` member",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
|
|
101
|
+
const key = await crypto.subtle.importKey(
|
|
102
|
+
"jwk",
|
|
103
|
+
jwk,
|
|
104
|
+
{ name: "Ed25519" },
|
|
105
|
+
false,
|
|
106
|
+
["sign"],
|
|
107
|
+
);
|
|
108
|
+
return {
|
|
109
|
+
key,
|
|
110
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
111
|
+
componentHash: "SHA-256",
|
|
112
|
+
params: { name: "Ed25519" },
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (jwk.kty === "EC" && typeof jwk.crv === "string") {
|
|
116
|
+
const ec = ecParamsForCurve(jwk.crv);
|
|
117
|
+
const key = await crypto.subtle.importKey(
|
|
118
|
+
"jwk",
|
|
119
|
+
jwk,
|
|
120
|
+
{ name: "ECDSA", namedCurve: jwk.crv },
|
|
121
|
+
false,
|
|
122
|
+
["sign"],
|
|
123
|
+
);
|
|
124
|
+
return { key, ...ec };
|
|
125
|
+
}
|
|
126
|
+
throw new Error(
|
|
127
|
+
`@dwk/vc: unsupported signing key (kty=${String(jwk.kty)}, crv=${String(jwk.crv)})`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function digest(
|
|
132
|
+
hash: ComponentHash,
|
|
133
|
+
bytes: Uint8Array,
|
|
134
|
+
): Promise<Uint8Array> {
|
|
135
|
+
const out = await crypto.subtle.digest(hash, bytes);
|
|
136
|
+
return new Uint8Array(out);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `proofConfigHash ‖ documentHash`, the bytes a JCS cryptosuite signs. */
|
|
140
|
+
async function hashData(
|
|
141
|
+
proofConfig: JsonObject,
|
|
142
|
+
document: JsonObject,
|
|
143
|
+
componentHash: ComponentHash,
|
|
144
|
+
): Promise<Uint8Array> {
|
|
145
|
+
const proofConfigHash = await digest(
|
|
146
|
+
componentHash,
|
|
147
|
+
canonicalizeToBytes(proofConfig),
|
|
148
|
+
);
|
|
149
|
+
const documentHash = await digest(
|
|
150
|
+
componentHash,
|
|
151
|
+
canonicalizeToBytes(document),
|
|
152
|
+
);
|
|
153
|
+
const out = new Uint8Array(proofConfigHash.length + documentHash.length);
|
|
154
|
+
out.set(proofConfigHash, 0);
|
|
155
|
+
out.set(documentHash, proofConfigHash.length);
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Options controlling how a Data Integrity proof is attached. */
|
|
160
|
+
export interface AddProofOptions {
|
|
161
|
+
/** The verification method id (`did:web:…#key`) clients resolve to verify. */
|
|
162
|
+
readonly verificationMethod: string;
|
|
163
|
+
/** Proof purpose. Defaults to `"assertionMethod"`. */
|
|
164
|
+
readonly proofPurpose?: string;
|
|
165
|
+
/** Proof creation time. A `Date` or an XSD dateTime string. Defaults to now. */
|
|
166
|
+
readonly created?: Date | string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A document carrying its attached Data Integrity proof. */
|
|
170
|
+
export type SecuredDocument = JsonObject & { proof: JsonObject };
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Attach a Data Integrity proof to `document`, returning a new secured document.
|
|
174
|
+
* Any existing `proof` on the input is excluded from the signed payload (a proof
|
|
175
|
+
* never signs over itself).
|
|
176
|
+
*/
|
|
177
|
+
export async function addProof(
|
|
178
|
+
document: JsonObject,
|
|
179
|
+
signer: Signer,
|
|
180
|
+
options: AddProofOptions,
|
|
181
|
+
): Promise<SecuredDocument> {
|
|
182
|
+
const proofConfig: JsonObject = {
|
|
183
|
+
type: "DataIntegrityProof",
|
|
184
|
+
cryptosuite: signer.cryptosuite,
|
|
185
|
+
created: toXsdDateTime(options.created ?? new Date()),
|
|
186
|
+
verificationMethod: options.verificationMethod,
|
|
187
|
+
proofPurpose: options.proofPurpose ?? "assertionMethod",
|
|
188
|
+
};
|
|
189
|
+
// The proof config's `@context` MUST match the document's, when present.
|
|
190
|
+
if (document["@context"] !== undefined) {
|
|
191
|
+
proofConfig["@context"] = document["@context"];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const { proof: _existing, ...unsigned } = document;
|
|
195
|
+
void _existing;
|
|
196
|
+
|
|
197
|
+
const data = await hashData(proofConfig, unsigned, signer.componentHash);
|
|
198
|
+
const signature = await crypto.subtle.sign(signer.params, signer.key, data);
|
|
199
|
+
|
|
200
|
+
const proof: JsonObject = {
|
|
201
|
+
...proofConfig,
|
|
202
|
+
proofValue: encodeMultibaseBase58btc(new Uint8Array(signature)),
|
|
203
|
+
};
|
|
204
|
+
return { ...unsigned, proof };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A verification method as published in a DID document. */
|
|
208
|
+
export interface VerificationMethod {
|
|
209
|
+
readonly id: string;
|
|
210
|
+
readonly type?: string;
|
|
211
|
+
readonly controller?: string;
|
|
212
|
+
readonly publicKeyMultibase?: string;
|
|
213
|
+
readonly publicKeyJwk?: JsonWebKey;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Resolve a verification method id to its document, or `undefined` if unknown. */
|
|
217
|
+
export type VerificationMethodResolver = (
|
|
218
|
+
id: string,
|
|
219
|
+
) => VerificationMethod | undefined | Promise<VerificationMethod | undefined>;
|
|
220
|
+
|
|
221
|
+
interface ImportedVerifier {
|
|
222
|
+
readonly key: CryptoKey;
|
|
223
|
+
readonly componentHash: ComponentHash;
|
|
224
|
+
readonly params: SignVerifyParams;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Import a public verification key, requiring it to match `cryptosuite`. */
|
|
228
|
+
async function importVerifier(
|
|
229
|
+
vm: VerificationMethod,
|
|
230
|
+
cryptosuite: Cryptosuite,
|
|
231
|
+
): Promise<ImportedVerifier> {
|
|
232
|
+
if (vm.publicKeyMultibase !== undefined) {
|
|
233
|
+
if (cryptosuite !== "eddsa-jcs-2022") {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"@dwk/vc: publicKeyMultibase (Multikey) requires the eddsa-jcs-2022 cryptosuite",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const { rawPublicKey } = decodeMultikey(vm.publicKeyMultibase);
|
|
239
|
+
const key = await crypto.subtle.importKey(
|
|
240
|
+
"raw",
|
|
241
|
+
rawPublicKey,
|
|
242
|
+
{ name: "Ed25519" },
|
|
243
|
+
false,
|
|
244
|
+
["verify"],
|
|
245
|
+
);
|
|
246
|
+
return { key, componentHash: "SHA-256", params: { name: "Ed25519" } };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (vm.publicKeyJwk !== undefined) {
|
|
250
|
+
const jwk = vm.publicKeyJwk;
|
|
251
|
+
if (cryptosuite === "eddsa-jcs-2022") {
|
|
252
|
+
if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519") {
|
|
253
|
+
throw new Error("@dwk/vc: eddsa-jcs-2022 requires an Ed25519 key");
|
|
254
|
+
}
|
|
255
|
+
const key = await crypto.subtle.importKey(
|
|
256
|
+
"jwk",
|
|
257
|
+
jwk,
|
|
258
|
+
{ name: "Ed25519" },
|
|
259
|
+
false,
|
|
260
|
+
["verify"],
|
|
261
|
+
);
|
|
262
|
+
return { key, componentHash: "SHA-256", params: { name: "Ed25519" } };
|
|
263
|
+
}
|
|
264
|
+
// ecdsa-jcs-2019
|
|
265
|
+
if (jwk.kty !== "EC" || typeof jwk.crv !== "string") {
|
|
266
|
+
throw new Error("@dwk/vc: ecdsa-jcs-2019 requires an EC key");
|
|
267
|
+
}
|
|
268
|
+
const ec = ecParamsForCurve(jwk.crv);
|
|
269
|
+
const key = await crypto.subtle.importKey(
|
|
270
|
+
"jwk",
|
|
271
|
+
jwk,
|
|
272
|
+
{ name: "ECDSA", namedCurve: jwk.crv },
|
|
273
|
+
false,
|
|
274
|
+
["verify"],
|
|
275
|
+
);
|
|
276
|
+
return { key, componentHash: ec.componentHash, params: ec.params };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
throw new Error(
|
|
280
|
+
"@dwk/vc: verification method has neither publicKeyMultibase nor publicKeyJwk",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Options for {@link verifyProof}. */
|
|
285
|
+
export interface VerifyProofOptions {
|
|
286
|
+
/** Resolves the proof's `verificationMethod` to its public key. */
|
|
287
|
+
readonly resolveVerificationMethod: VerificationMethodResolver;
|
|
288
|
+
/** Required proof purpose. Defaults to `"assertionMethod"`. */
|
|
289
|
+
readonly expectedProofPurpose?: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The outcome of verifying a document's Data Integrity proof(s). */
|
|
293
|
+
export interface VerifyProofResult {
|
|
294
|
+
readonly verified: boolean;
|
|
295
|
+
/** Stable, human-readable failure descriptions; empty when verified. */
|
|
296
|
+
readonly errors: readonly string[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isJsonObject(value: JcsValue | undefined): value is JsonObject {
|
|
300
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function asProofArray(proof: JcsValue | undefined): JsonObject[] {
|
|
304
|
+
if (proof === undefined || proof === null) return [];
|
|
305
|
+
if (Array.isArray(proof)) return proof.filter(isJsonObject);
|
|
306
|
+
return isJsonObject(proof) ? [proof] : [];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function asContextArray(context: JcsValue | undefined): JcsValue[] {
|
|
310
|
+
if (context === undefined) return [];
|
|
311
|
+
return Array.isArray(context) ? context : [context];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Whether the document's `@context` begins with every value in the proof's
|
|
316
|
+
* `@context`, in order (vc-di-eddsa §3.3.2 step 4.1). A proof without an
|
|
317
|
+
* `@context` imposes no constraint. Values are compared by JCS canonical form,
|
|
318
|
+
* so embedded context objects match structurally regardless of key order.
|
|
319
|
+
*/
|
|
320
|
+
function contextStartsWith(
|
|
321
|
+
documentContext: JcsValue | undefined,
|
|
322
|
+
proofContext: JcsValue | undefined,
|
|
323
|
+
): boolean {
|
|
324
|
+
const proofArr = asContextArray(proofContext);
|
|
325
|
+
if (proofArr.length === 0) return true;
|
|
326
|
+
const docArr = asContextArray(documentContext);
|
|
327
|
+
if (docArr.length < proofArr.length) return false;
|
|
328
|
+
for (let i = 0; i < proofArr.length; i++) {
|
|
329
|
+
if (canonicalize(proofArr[i]!) !== canonicalize(docArr[i]!)) return false;
|
|
330
|
+
}
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function verifySingleProof(
|
|
335
|
+
document: JsonObject,
|
|
336
|
+
proof: JsonObject,
|
|
337
|
+
options: VerifyProofOptions,
|
|
338
|
+
): Promise<string | null> {
|
|
339
|
+
if (proof.type !== "DataIntegrityProof") {
|
|
340
|
+
return `unsupported proof type "${String(proof.type)}"`;
|
|
341
|
+
}
|
|
342
|
+
if (!isSupportedCryptosuite(proof.cryptosuite)) {
|
|
343
|
+
return `unsupported cryptosuite "${String(proof.cryptosuite)}"`;
|
|
344
|
+
}
|
|
345
|
+
const expectedPurpose = options.expectedProofPurpose ?? "assertionMethod";
|
|
346
|
+
if (proof.proofPurpose !== expectedPurpose) {
|
|
347
|
+
return `proof purpose "${String(proof.proofPurpose)}" does not match expected "${expectedPurpose}"`;
|
|
348
|
+
}
|
|
349
|
+
if (typeof proof.verificationMethod !== "string") {
|
|
350
|
+
return "proof is missing a string verificationMethod";
|
|
351
|
+
}
|
|
352
|
+
if (typeof proof.proofValue !== "string") {
|
|
353
|
+
return "proof is missing a string proofValue";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// vc-di-eddsa §3.3.2 step 4.1: the secured document's `@context` MUST begin
|
|
357
|
+
// with the proof's `@context` values, in order, or verification fails.
|
|
358
|
+
if (!contextStartsWith(document["@context"], proof["@context"])) {
|
|
359
|
+
return "document @context does not start with the proof's @context values, in order";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Both JCS cryptosuites mandate a base58-btc (`z`) proofValue; reject any
|
|
363
|
+
// other multibase encoding (e.g. base64url) rather than silently decoding it.
|
|
364
|
+
if (proof.proofValue[0] !== MULTIBASE_BASE58BTC) {
|
|
365
|
+
return `proofValue must be base58-btc multibase (a "${MULTIBASE_BASE58BTC}" prefix), got "${proof.proofValue[0] ?? ""}"`;
|
|
366
|
+
}
|
|
367
|
+
let signature: Uint8Array;
|
|
368
|
+
try {
|
|
369
|
+
signature = base58btcDecode(proof.proofValue.slice(1));
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return `proofValue is not valid base58-btc: ${(error as Error).message}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const vm = await options.resolveVerificationMethod(proof.verificationMethod);
|
|
375
|
+
if (vm === undefined) {
|
|
376
|
+
return `could not resolve verification method "${proof.verificationMethod}"`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let verifier: ImportedVerifier;
|
|
380
|
+
try {
|
|
381
|
+
verifier = await importVerifier(vm, proof.cryptosuite);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
return (error as Error).message;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const { proofValue: _pv, ...proofConfig } = proof;
|
|
387
|
+
void _pv;
|
|
388
|
+
const { proof: _existing, ...unsigned } = document;
|
|
389
|
+
void _existing;
|
|
390
|
+
|
|
391
|
+
const data = await hashData(proofConfig, unsigned, verifier.componentHash);
|
|
392
|
+
let ok: boolean;
|
|
393
|
+
try {
|
|
394
|
+
ok = await crypto.subtle.verify(
|
|
395
|
+
verifier.params,
|
|
396
|
+
verifier.key,
|
|
397
|
+
signature,
|
|
398
|
+
data,
|
|
399
|
+
);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
return `signature verification threw: ${(error as Error).message}`;
|
|
402
|
+
}
|
|
403
|
+
return ok ? null : "signature verification failed";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Verify the Data Integrity proof(s) on `document`. A document with no proof, or
|
|
408
|
+
* any proof that fails, yields `verified: false` with the reasons collected in
|
|
409
|
+
* `errors`. Never throws — verification failures are returned, not raised.
|
|
410
|
+
*/
|
|
411
|
+
export async function verifyProof(
|
|
412
|
+
document: JsonObject,
|
|
413
|
+
options: VerifyProofOptions,
|
|
414
|
+
): Promise<VerifyProofResult> {
|
|
415
|
+
const proofs = asProofArray(document.proof);
|
|
416
|
+
if (proofs.length === 0) {
|
|
417
|
+
return { verified: false, errors: ["document has no proof"] };
|
|
418
|
+
}
|
|
419
|
+
const errors: string[] = [];
|
|
420
|
+
for (const proof of proofs) {
|
|
421
|
+
const error = await verifySingleProof(document, proof, options);
|
|
422
|
+
if (error !== null) errors.push(error);
|
|
423
|
+
}
|
|
424
|
+
return { verified: errors.length === 0, errors };
|
|
425
|
+
}
|
package/src/datetime.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XSD `dateTimeStamp` validation and canonicalization for VC date fields.
|
|
3
|
+
*
|
|
4
|
+
* VCDM 2.0 and the Data Integrity suites express temporal values — a
|
|
5
|
+
* credential's `validFrom`/`validUntil` and a proof's `created` — as XSD
|
|
6
|
+
* `dateTimeStamp` strings: an `xsd:dateTime` with a **mandatory** timezone
|
|
7
|
+
* offset. vc-di-eddsa §3.3.5 step 3 requires erroring on an invalid `created`,
|
|
8
|
+
* and a verifier must not silently treat a malformed `validUntil` as "no
|
|
9
|
+
* expiry". These pure helpers gate both, so unparseable datetimes fail loudly
|
|
10
|
+
* (at signing) or fail closed (at verification) rather than slipping through.
|
|
11
|
+
*
|
|
12
|
+
* @see https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// xsd:dateTimeStamp — an xsd:dateTime that REQUIRES an explicit timezone (`Z`
|
|
16
|
+
// or `±HH:MM`). Mirrors the W3C XML Schema 1.1 lexical space. The day-of-month
|
|
17
|
+
// upper bound (28–31) is checked separately against the actual month/year.
|
|
18
|
+
const XSD_DATE_TIME_STAMP =
|
|
19
|
+
/^(-?(?:[1-9][0-9]{3,}|0[0-9]{3}))-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T(?:([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\.[0-9]+)?|24:00:00(?:\.0+)?)(?:Z|[+-](?:(?:0[0-9]|1[0-3]):[0-5][0-9]|14:00))$/;
|
|
20
|
+
|
|
21
|
+
function daysInMonth(year: number, month: number): number {
|
|
22
|
+
if (month === 2) {
|
|
23
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
24
|
+
return leap ? 29 : 28;
|
|
25
|
+
}
|
|
26
|
+
return month === 4 || month === 6 || month === 9 || month === 11 ? 30 : 31;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether `value` is a valid XSD `dateTimeStamp`: a well-formed `xsd:dateTime`
|
|
31
|
+
* carrying a mandatory timezone that also denotes a real calendar date (so
|
|
32
|
+
* lexically-shaped-but-impossible dates like `2026-02-30` are rejected).
|
|
33
|
+
*/
|
|
34
|
+
export function isValidXsdDateTimeStamp(value: string): boolean {
|
|
35
|
+
const match = XSD_DATE_TIME_STAMP.exec(value);
|
|
36
|
+
if (match === null) return false;
|
|
37
|
+
const year = Number(match[1]);
|
|
38
|
+
const month = Number(match[2]);
|
|
39
|
+
const day = Number(match[3]);
|
|
40
|
+
return day <= daysInMonth(year, month);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Canonicalize a date value to a second-precision XSD `dateTimeStamp`. A `Date`
|
|
45
|
+
* is rendered in UTC (milliseconds dropped); a string is validated and passed
|
|
46
|
+
* through unchanged. Throws when a string is not a valid XSD `dateTimeStamp`.
|
|
47
|
+
*/
|
|
48
|
+
export function toXsdDateTime(value: Date | string): string {
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
if (!isValidXsdDateTimeStamp(value)) {
|
|
51
|
+
throw new Error(`@dwk/vc: "${value}" is not a valid XSD dateTimeStamp`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
// Drop milliseconds for the canonical, second-precision VC form.
|
|
56
|
+
return value.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
57
|
+
}
|