@embedpdf/core-signature 3.0.0-next.13 → 3.0.0-next.15

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/dist/index.js CHANGED
@@ -1,90 +1,8 @@
1
+ import { a as __exportAll, i as toArrayBuffer, r as ensureEngine, t as bytesEqual } from "./engine-C7lxkbZP.js";
2
+ import { a as WEBCRYPTO_HASH, i as OID_BY_DIGEST, n as DIGEST_LENGTH, r as OID, t as DIGEST_BY_OID } from "./oids-BhZXI2ZX.js";
1
3
  import * as asn1js from "asn1js";
2
4
  import * as pkijs from "pkijs";
3
- //#region src/cms/engine.ts
4
- let ensured = false;
5
- /**
6
- * PKI.js needs a WebCrypto engine. Browsers and Node ≥ 19 expose
7
- * `globalThis.crypto`; register it once, lazily, so importing this package
8
- * has no side effect.
9
- */
10
- function ensureEngine() {
11
- if (!ensured) {
12
- ensured = true;
13
- if (!pkijs.getCrypto()) {
14
- const webcrypto = globalThis.crypto;
15
- if (!webcrypto?.subtle) throw new Error("@embedpdf/core-signature needs WebCrypto (globalThis.crypto.subtle)");
16
- pkijs.setEngine("embedpdf", new pkijs.CryptoEngine({
17
- name: "embedpdf",
18
- crypto: webcrypto
19
- }));
20
- }
21
- }
22
- return pkijs.getCrypto(true);
23
- }
24
- function toArrayBuffer(view) {
25
- const copy = new Uint8Array(view.byteLength);
26
- copy.set(view);
27
- return copy.buffer;
28
- }
29
- function bytesEqual(a, b) {
30
- if (a.byteLength !== b.byteLength) return false;
31
- for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
32
- return true;
33
- }
34
- //#endregion
35
- //#region src/cms/oids.ts
36
- const OID = {
37
- data: "1.2.840.113549.1.7.1",
38
- signedData: "1.2.840.113549.1.7.2",
39
- contentType: "1.2.840.113549.1.9.3",
40
- messageDigest: "1.2.840.113549.1.9.4",
41
- signingTime: "1.2.840.113549.1.9.5",
42
- signingCertificateV2: "1.2.840.113549.1.9.16.2.47",
43
- timestampToken: "1.2.840.113549.1.9.16.2.14",
44
- sha1: "1.3.14.3.2.26",
45
- sha256: "2.16.840.1.101.3.4.2.1",
46
- sha384: "2.16.840.1.101.3.4.2.2",
47
- sha512: "2.16.840.1.101.3.4.2.3",
48
- rsaEncryption: "1.2.840.113549.1.1.1",
49
- sha256WithRsa: "1.2.840.113549.1.1.11",
50
- sha384WithRsa: "1.2.840.113549.1.1.12",
51
- sha512WithRsa: "1.2.840.113549.1.1.13",
52
- rsaPss: "1.2.840.113549.1.1.10",
53
- mgf1: "1.2.840.113549.1.1.8",
54
- ecdsaSha256: "1.2.840.10045.4.3.2",
55
- ecdsaSha384: "1.2.840.10045.4.3.3",
56
- ecdsaSha512: "1.2.840.10045.4.3.4",
57
- subjectKeyIdentifier: "2.5.29.14",
58
- keyUsage: "2.5.29.15",
59
- basicConstraints: "2.5.29.19",
60
- commonName: "2.5.4.3"
61
- };
62
- const DIGEST_BY_OID = {
63
- [OID.sha1]: "sha1",
64
- [OID.sha256]: "sha256",
65
- [OID.sha384]: "sha384",
66
- [OID.sha512]: "sha512"
67
- };
68
- const OID_BY_DIGEST = {
69
- sha1: OID.sha1,
70
- sha256: OID.sha256,
71
- sha384: OID.sha384,
72
- sha512: OID.sha512
73
- };
74
- /** WebCrypto hash names. */
75
- const WEBCRYPTO_HASH = {
76
- sha1: "SHA-1",
77
- sha256: "SHA-256",
78
- sha384: "SHA-384",
79
- sha512: "SHA-512"
80
- };
81
- const DIGEST_LENGTH = {
82
- sha1: 20,
83
- sha256: 32,
84
- sha384: 48,
85
- sha512: 64
86
- };
87
- //#endregion
5
+ import { EngineError, EngineErrorCode } from "@embedpdf/engine-core/runtime";
88
6
  //#region src/cms/parse.ts
89
7
  var CmsError = class extends Error {
90
8
  constructor(reason, message) {
@@ -97,7 +15,7 @@ var CmsError = class extends Error {
97
15
  function parseDetachedCms(cms) {
98
16
  return parseCmsInternal(cms).parsed;
99
17
  }
100
- function parseCmsInternal(cms) {
18
+ function parseCmsInternal(cms, content = "detached") {
101
19
  if (cms.byteLength === 0) throw new CmsError("malformed", "empty CMS");
102
20
  const asn1 = asn1js.fromBER(toArrayBuffer(cms));
103
21
  if (asn1.offset === -1) throw new CmsError("malformed", "CMS is not valid BER");
@@ -107,25 +25,33 @@ function parseCmsInternal(cms) {
107
25
  contentInfo = new pkijs.ContentInfo({ schema: asn1.result });
108
26
  if (contentInfo.contentType !== OID.signedData) throw new CmsError("unsupported", `CMS content type ${contentInfo.contentType} is not SignedData`);
109
27
  signedData = new pkijs.SignedData({ schema: contentInfo.content });
110
- } catch (err) {
111
- if (err instanceof CmsError) throw err;
112
- throw new CmsError("malformed", `CMS does not parse as SignedData: ${err.message}`);
28
+ } catch (error) {
29
+ if (error instanceof CmsError) throw error;
30
+ throw new CmsError("malformed", `CMS does not parse as SignedData: ${error.message}`);
31
+ }
32
+ const encapsulated = signedData.encapContentInfo;
33
+ const expectedContentType = content === "tst-info" ? OID.tstInfo : OID.data;
34
+ let eContent = null;
35
+ if (content === "detached") {
36
+ if (encapsulated.eContent) throw new CmsError("unsupported", "CMS carries encapsulated content; a PDF signature is detached");
37
+ } else {
38
+ if (encapsulated.eContentType !== OID.tstInfo || !encapsulated.eContent) throw new CmsError("malformed", "a timestamp token must carry a TSTInfo");
39
+ eContent = new Uint8Array(encapsulated.eContent.getValue());
113
40
  }
114
- if (signedData.encapContentInfo.eContent) throw new CmsError("unsupported", "CMS carries encapsulated content; a PDF signature is detached");
115
41
  if (signedData.signerInfos.length !== 1) throw new CmsError("unsupported", `CMS has ${signedData.signerInfos.length} signers; exactly one is expected`);
116
42
  const signerInfo = signedData.signerInfos[0];
117
43
  const digestAlgorithm = DIGEST_BY_OID[signerInfo.digestAlgorithm.algorithmId];
118
44
  if (!digestAlgorithm) throw new CmsError("unsupported", `digest algorithm ${signerInfo.digestAlgorithm.algorithmId}`);
119
- const certificates = (signedData.certificates ?? []).filter((c) => c instanceof pkijs.Certificate);
45
+ const certificates = (signedData.certificates ?? []).filter((certificate) => certificate instanceof pkijs.Certificate);
120
46
  const signerCertificate = findSignerCertificate(signerInfo, certificates);
121
47
  if (!signerCertificate) throw new CmsError("malformed", "the signer certificate is not carried by the CMS");
122
48
  const signedAttrs = signerInfo.signedAttrs?.attributes ?? [];
123
49
  if (signedAttrs.length === 0) throw new CmsError("unsupported", "CMS has no signed attributes; a PDF signature needs message-digest");
124
- const attr = (type) => signedAttrs.find((a) => a.type === type);
50
+ const attr = (type) => signedAttrs.find((attribute) => attribute.type === type);
125
51
  const contentType = attr(OID.contentType);
126
- if (contentType && contentType.values[0]?.valueBlock?.toString?.() !== OID.data) {
52
+ if (contentType) {
127
53
  const oid = contentType.values[0];
128
- if (!oid || oid.valueBlock.toString() !== OID.data) throw new CmsError("unsupported", "CMS content-type attribute is not id-data");
54
+ if (!oid || oid.valueBlock.toString() !== expectedContentType) throw new CmsError("unsupported", `CMS content-type attribute is not ${content === "tst-info" ? "id-ct-TSTInfo" : "id-data"}`);
129
55
  }
130
56
  const messageDigestValue = attr(OID.messageDigest)?.values[0];
131
57
  if (!messageDigestValue || !(messageDigestValue instanceof asn1js.OctetString)) throw new CmsError("malformed", "CMS has no message-digest attribute");
@@ -148,9 +74,9 @@ function parseCmsInternal(cms) {
148
74
  essCertHash = new Uint8Array(hash.valueBlock.valueHexView);
149
75
  }
150
76
  let timestampToken = null;
151
- const tokenAttr = signerInfo.unsignedAttrs?.attributes.find((a) => a.type === OID.timestampToken);
77
+ const tokenAttr = signerInfo.unsignedAttrs?.attributes.find((attribute) => attribute.type === OID.timestampToken);
152
78
  if (tokenAttr?.values[0]) timestampToken = new Uint8Array(tokenAttr.values[0].toBER(false));
153
- const ordered = [signerCertificate, ...certificates.filter((c) => c !== signerCertificate)];
79
+ const ordered = [signerCertificate, ...certificates.filter((certificate) => certificate !== signerCertificate)];
154
80
  return {
155
81
  parsed: {
156
82
  digestAlgorithm,
@@ -166,7 +92,8 @@ function parseCmsInternal(cms) {
166
92
  signerInfo,
167
93
  signerCertificate,
168
94
  certificates: ordered,
169
- essCertHash
95
+ essCertHash,
96
+ eContent
170
97
  };
171
98
  }
172
99
  function certDer(cert) {
@@ -181,7 +108,7 @@ function findSignerCertificate(signerInfo, certificates) {
181
108
  const ski = sid?.valueBlock?.valueHexView;
182
109
  if (!ski) return null;
183
110
  for (const cert of certificates) {
184
- const ext = cert.extensions?.find((e) => e.extnID === OID.subjectKeyIdentifier);
111
+ const ext = cert.extensions?.find((extension) => extension.extnID === OID.subjectKeyIdentifier);
185
112
  const value = ext ? asn1js.fromBER(ext.extnValue.valueBlock.valueHexView) : null;
186
113
  const inner = value && value.offset !== -1 ? value.result : null;
187
114
  if (inner && bytesEqual(new Uint8Array(inner.valueBlock.valueHexView), new Uint8Array(ski))) return cert;
@@ -190,9 +117,9 @@ function findSignerCertificate(signerInfo, certificates) {
190
117
  }
191
118
  //#endregion
192
119
  //#region src/cms/verify.ts
193
- /** The DER of the signed attributes as they are signed: re-tagged as a SET. */
120
+ /** The DER of the signed attributes as they are signed: re-tagged as a set. */
194
121
  function signedAttributesDer(signerInfo) {
195
- const set = new asn1js.Set({ value: (signerInfo.signedAttrs?.attributes ?? []).map((a) => a.toSchema()) });
122
+ const set = new asn1js.Set({ value: (signerInfo.signedAttrs?.attributes ?? []).map((attribute) => attribute.toSchema()) });
196
123
  return new Uint8Array(set.toBER(false));
197
124
  }
198
125
  /**
@@ -206,8 +133,8 @@ async function verifyCmsSignature(cms) {
206
133
  let internal;
207
134
  try {
208
135
  internal = cms instanceof Uint8Array ? parseCmsInternal(cms) : cms;
209
- } catch (err) {
210
- return err instanceof CmsError && err.reason === "unsupported" ? "unsupported" : "invalid";
136
+ } catch (error) {
137
+ return error instanceof CmsError && error.reason === "unsupported" ? "unsupported" : "invalid";
211
138
  }
212
139
  const engine = ensureEngine();
213
140
  const { signerInfo, signerCertificate, parsed, essCertHash } = internal;
@@ -216,8 +143,8 @@ async function verifyCmsSignature(cms) {
216
143
  }
217
144
  try {
218
145
  return await engine.verifyWithPublicKey(toArrayBuffer(signedAttributesDer(signerInfo)), signerInfo.signature, signerCertificate.subjectPublicKeyInfo, signerInfo.signatureAlgorithm, WEBCRYPTO_HASH[parsed.digestAlgorithm]) ? "valid" : "invalid";
219
- } catch (err) {
220
- const message = err.message ?? "";
146
+ } catch (error) {
147
+ const message = error.message ?? "";
221
148
  return /unsupported|unknown|not supported|not implemented/i.test(message) ? "unsupported" : "invalid";
222
149
  }
223
150
  }
@@ -276,10 +203,9 @@ async function buildDetachedCms(input) {
276
203
  }),
277
204
  signatureAlgorithm: signatureAlgorithmFor(input.signer.algorithm, input.hash)
278
205
  });
279
- const toSign = new asn1js.Set({ value: attributes.map((a) => a.toSchema()) }).toBER(false);
206
+ const toSign = new asn1js.Set({ value: attributes.map((attribute) => attribute.toSchema()) }).toBER(false);
280
207
  const raw = await input.signer.sign(new Uint8Array(toSign));
281
- const signature = input.signer.algorithm === "ECDSA" ? pkijs.createCMSECDSASignature(toArrayBuffer(raw)) : toArrayBuffer(raw);
282
- signerInfo.signature = new asn1js.OctetString({ valueHex: signature });
208
+ signerInfo.signature = new asn1js.OctetString({ valueHex: cmsSignatureValue(input.signer.algorithm, raw) });
283
209
  const signedData = new pkijs.SignedData({
284
210
  version: 1,
285
211
  digestAlgorithms: [digestAlgorithm],
@@ -293,6 +219,11 @@ async function buildDetachedCms(input) {
293
219
  });
294
220
  return new Uint8Array(contentInfo.toSchema().toBER(false));
295
221
  }
222
+ /** A raw signature as a SignerInfo carries it (ECDSA `r || s` becomes DER). */
223
+ function cmsSignatureValue(algorithm, raw) {
224
+ return algorithm === "ECDSA" ? pkijs.createCMSECDSASignature(toArrayBuffer(raw)) : toArrayBuffer(raw);
225
+ }
226
+ /** The SignerInfo signature algorithm of a raw key and hash. */
296
227
  function signatureAlgorithmFor(algorithm, hash) {
297
228
  switch (algorithm) {
298
229
  case "RSA-PKCS1-v1_5": return new pkijs.AlgorithmIdentifier({
@@ -329,6 +260,109 @@ function signatureAlgorithmFor(algorithm, hash) {
329
260
  }
330
261
  }
331
262
  //#endregion
263
+ //#region src/cms/timestamp.ts
264
+ var timestamp_exports = /* @__PURE__ */ __exportAll({
265
+ buildTimestampToken: () => buildTimestampToken,
266
+ readTimestampInfo: () => readTimestampInfo
267
+ });
268
+ /** Read a token's TSTInfo (the encapsulated content of an RFC 3161 token). Throws `CmsError`. */
269
+ function readTimestampInfo(eContent) {
270
+ const asn1 = asn1js.fromBER(toArrayBuffer(eContent));
271
+ if (asn1.offset === -1) throw new CmsError("malformed", "TSTInfo is not valid BER");
272
+ let info;
273
+ try {
274
+ info = new pkijs.TSTInfo({ schema: asn1.result });
275
+ } catch (error) {
276
+ throw new CmsError("malformed", `TSTInfo does not parse: ${error.message}`);
277
+ }
278
+ const algorithm = DIGEST_BY_OID[info.messageImprint.hashAlgorithm.algorithmId];
279
+ if (!algorithm) throw new CmsError("unsupported", `message imprint algorithm ${info.messageImprint.hashAlgorithm.algorithmId}`);
280
+ return {
281
+ imprintAlgorithm: algorithm,
282
+ imprint: new Uint8Array(info.messageImprint.hashedMessage.valueBlock.valueHexView),
283
+ genTime: info.genTime
284
+ };
285
+ }
286
+ /**
287
+ * Build an RFC 3161 timestamp token: a SignedData whose content is a
288
+ * TSTInfo stamping `digest`, signed by the authority over content-type
289
+ * (id-ct-TSTInfo), message-digest (of the TSTInfo) and ESS
290
+ * signing-certificate-v2. What a timestamp authority returns; used by the
291
+ * test authority, never to stamp a real document.
292
+ */
293
+ async function buildTimestampToken(input) {
294
+ const engine = ensureEngine();
295
+ if (input.digest.byteLength !== DIGEST_LENGTH[input.hash]) throw new Error(`digest is ${input.digest.byteLength} bytes; ${input.hash} needs ${DIGEST_LENGTH[input.hash]}`);
296
+ const authorityCert = pkijs.Certificate.fromBER(toArrayBuffer(input.authority.certificateChain[0]));
297
+ const serial = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(8));
298
+ serial[0] &= 127;
299
+ const eContent = new pkijs.TSTInfo({
300
+ version: 1,
301
+ policy: "1.3.6.1.4.1.57264.1.1",
302
+ messageImprint: new pkijs.MessageImprint({
303
+ hashAlgorithm: new pkijs.AlgorithmIdentifier({ algorithmId: OID_BY_DIGEST[input.hash] }),
304
+ hashedMessage: new asn1js.OctetString({ valueHex: toArrayBuffer(input.digest) })
305
+ }),
306
+ serialNumber: new asn1js.Integer({ valueHex: toArrayBuffer(serial) }),
307
+ genTime: input.genTime ?? /* @__PURE__ */ new Date()
308
+ }).toSchema().toBER(false);
309
+ const contentDigest = await engine.digest(WEBCRYPTO_HASH[input.authority.hash], eContent);
310
+ const certHash = await engine.digest("SHA-256", toArrayBuffer(input.authority.certificateChain[0]));
311
+ const issuerSerial = new pkijs.IssuerSerial({
312
+ issuer: new pkijs.GeneralNames({ names: [new pkijs.GeneralName({
313
+ type: 4,
314
+ value: authorityCert.issuer
315
+ })] }),
316
+ serialNumber: authorityCert.serialNumber
317
+ });
318
+ const attributes = [
319
+ new pkijs.Attribute({
320
+ type: OID.contentType,
321
+ values: [new asn1js.ObjectIdentifier({ value: OID.tstInfo })]
322
+ }),
323
+ new pkijs.Attribute({
324
+ type: OID.messageDigest,
325
+ values: [new asn1js.OctetString({ valueHex: contentDigest })]
326
+ }),
327
+ new pkijs.Attribute({
328
+ type: OID.signingCertificateV2,
329
+ values: [new asn1js.Sequence({ value: [new asn1js.Sequence({ value: [new asn1js.Sequence({ value: [new asn1js.OctetString({ valueHex: certHash }), issuerSerial.toSchema()] })] })] })]
330
+ })
331
+ ];
332
+ const digestAlgorithm = new pkijs.AlgorithmIdentifier({ algorithmId: OID_BY_DIGEST[input.authority.hash] });
333
+ const signerInfo = new pkijs.SignerInfo({
334
+ version: 1,
335
+ sid: new pkijs.IssuerAndSerialNumber({
336
+ issuer: authorityCert.issuer,
337
+ serialNumber: authorityCert.serialNumber
338
+ }),
339
+ digestAlgorithm,
340
+ signedAttrs: new pkijs.SignedAndUnsignedAttributes({
341
+ type: 0,
342
+ attributes
343
+ }),
344
+ signatureAlgorithm: signatureAlgorithmFor(input.authority.algorithm, input.authority.hash)
345
+ });
346
+ const toSign = new asn1js.Set({ value: attributes.map((attribute) => attribute.toSchema()) }).toBER(false);
347
+ const raw = await input.authority.sign(new Uint8Array(toSign));
348
+ signerInfo.signature = new asn1js.OctetString({ valueHex: cmsSignatureValue(input.authority.algorithm, raw) });
349
+ const signedData = new pkijs.SignedData({
350
+ version: 3,
351
+ digestAlgorithms: [digestAlgorithm],
352
+ encapContentInfo: new pkijs.EncapsulatedContentInfo({
353
+ eContentType: OID.tstInfo,
354
+ eContent: new asn1js.OctetString({ valueHex: eContent })
355
+ }),
356
+ certificates: [authorityCert],
357
+ signerInfos: [signerInfo]
358
+ });
359
+ const contentInfo = new pkijs.ContentInfo({
360
+ contentType: OID.signedData,
361
+ content: signedData.toSchema(true)
362
+ });
363
+ return new Uint8Array(contentInfo.toSchema().toBER(false));
364
+ }
365
+ //#endregion
332
366
  //#region src/gate.ts
333
367
  /**
334
368
  * The gate between a signer's answer and `signatures.complete`: the CMS
@@ -345,14 +379,15 @@ async function verifyForCompletion(input) {
345
379
  reason: "too-large",
346
380
  detail: `the CMS is ${cms.byteLength} bytes; ${prepared.contentsSize} were reserved`
347
381
  };
382
+ if (profile === "timestamp") return verifyTimestampToken(cms, prepared);
348
383
  let internal;
349
384
  try {
350
385
  internal = parseCmsInternal(cms);
351
- } catch (err) {
386
+ } catch (error) {
352
387
  return {
353
388
  ok: false,
354
389
  reason: "malformed",
355
- detail: err instanceof CmsError ? err.message : String(err)
390
+ detail: error instanceof CmsError ? error.message : String(error)
356
391
  };
357
392
  }
358
393
  const parsed = internal.parsed;
@@ -386,6 +421,47 @@ async function verifyForCompletion(input) {
386
421
  };
387
422
  return { ok: true };
388
423
  }
424
+ /**
425
+ * A document timestamp's token: it must stamp the prepared digest with the
426
+ * prepared algorithm, its own message digest must cover its TSTInfo, and
427
+ * its signature must verify with the authority's certificate.
428
+ */
429
+ async function verifyTimestampToken(cms, prepared) {
430
+ let internal;
431
+ let info;
432
+ try {
433
+ internal = parseCmsInternal(cms, "tst-info");
434
+ info = readTimestampInfo(internal.eContent);
435
+ } catch (error) {
436
+ return {
437
+ ok: false,
438
+ reason: "malformed",
439
+ detail: error instanceof CmsError ? error.message : String(error)
440
+ };
441
+ }
442
+ if (info.imprintAlgorithm !== prepared.algorithm) return {
443
+ ok: false,
444
+ reason: "algorithm-mismatch",
445
+ detail: `the token stamps a ${info.imprintAlgorithm} digest, the document was prepared with ${prepared.algorithm}`
446
+ };
447
+ if (!bytesEqual(info.imprint, prepared.digest)) return {
448
+ ok: false,
449
+ reason: "digest-mismatch",
450
+ detail: "the token's message imprint is not the prepared digest"
451
+ };
452
+ if (!bytesEqual(new Uint8Array(await ensureEngine().digest(WEBCRYPTO_HASH[internal.parsed.digestAlgorithm], toArrayBuffer(internal.eContent))), internal.parsed.messageDigest)) return {
453
+ ok: false,
454
+ reason: "signature-invalid",
455
+ detail: "the token's message digest does not cover its TSTInfo"
456
+ };
457
+ const cryptography = await verifyCmsSignature(internal);
458
+ if (cryptography !== "valid") return {
459
+ ok: false,
460
+ reason: "signature-invalid",
461
+ detail: cryptography === "unsupported" ? "the signature algorithm is not supported" : "the token signature does not verify"
462
+ };
463
+ return { ok: true };
464
+ }
389
465
  //#endregion
390
466
  //#region src/trust.ts
391
467
  /**
@@ -407,10 +483,10 @@ async function validateChain(cms, trust, at = { kind: "now" }) {
407
483
  let internal;
408
484
  try {
409
485
  internal = cms instanceof Uint8Array ? parseCmsInternal(cms) : cms;
410
- } catch (err) {
486
+ } catch (error) {
411
487
  return {
412
488
  status: "untrusted",
413
- reason: err.message
489
+ reason: error.message
414
490
  };
415
491
  }
416
492
  const trustedCerts = anchors.map((der) => pkijs.Certificate.fromBER(toArrayBuffer(der)));
@@ -427,91 +503,103 @@ async function validateChain(cms, trust, at = { kind: "now" }) {
427
503
  };
428
504
  return {
429
505
  status: "trusted",
430
- path: (result.certificatePath ?? []).map((c) => new Uint8Array(c.toSchema(true).toBER(false)))
506
+ path: (result.certificatePath ?? []).map((certificate) => new Uint8Array(certificate.toSchema(true).toBER(false)))
431
507
  };
432
- } catch (err) {
508
+ } catch (error) {
433
509
  return {
434
510
  status: "untrusted",
435
- reason: err.message
511
+ reason: error.message
436
512
  };
437
513
  }
438
514
  }
439
515
  //#endregion
440
516
  //#region src/sign.ts
441
- var SigningError = class extends Error {
442
- constructor(reason, detail) {
443
- super(`signing refused (${reason}): ${detail}`);
444
- this.reason = reason;
445
- this.name = "SigningError";
446
- }
447
- };
517
+ /**
518
+ * The gate refused the key's answer: `SignatureRefused`, with the gate's
519
+ * reason in `details.reason`.
520
+ */
521
+ function signingRefused(reason, detail) {
522
+ return new EngineError(EngineErrorCode.SignatureRefused, `signing refused (${reason}): ${detail}`, { details: { reason } });
523
+ }
448
524
  function profileFor(subFilter) {
449
- return subFilter === "adbe.pkcs7.detached" ? "pkcs7" : "cades-b";
525
+ switch (subFilter) {
526
+ case "adbe.pkcs7.detached": return "pkcs7";
527
+ case "ETSI.CAdES.detached": return "cades-b";
528
+ case "ETSI.RFC3161": return "timestamp";
529
+ default: throw new EngineError(EngineErrorCode.InvalidArg, `unknown subFilter '${String(subFilter)}'`, { details: { field: "subFilter" } });
530
+ }
450
531
  }
451
532
  /**
452
- * Sign a field in one call: `prepare` on the engine, the signer turns the
533
+ * Sign a field in one call: `prepare` on the engine, the key turns the
453
534
  * digest into a CMS (or a raw signature this package wraps), the gate
454
535
  * checks the CMS matches what was prepared, `complete` installs it. Any
455
- * failure aborts the candidate and rethrows; the document is never left
536
+ * failure cancels the candidate and rethrows; the document is never left
456
537
  * with a pending signing.
457
538
  */
458
539
  async function sign(doc, input) {
459
- if (!doc.signatures) throw new Error("this engine does not implement signatures");
460
- const { signer, ...rest } = input;
461
- const subFilter = rest.subFilter ?? "ETSI.CAdES.detached";
540
+ const { key, ...rest } = input;
541
+ const timestamp = rest.kind === "timestamp";
542
+ if (timestamp && key.kind !== "cms") throw new EngineError(EngineErrorCode.InvalidArg, "a document timestamp needs a timestamp authority: a CMS key that returns an RFC 3161 token", { details: { field: "key" } });
543
+ const subFilter = timestamp ? "ETSI.RFC3161" : rest.subFilter ?? "ETSI.CAdES.detached";
462
544
  const profile = profileFor(subFilter);
463
- const digest = rest.digest ?? (signer.kind === "raw" ? signer.hash : "sha256");
545
+ const digest = rest.digest ?? (key.kind === "raw" ? key.hash : "sha256");
464
546
  const prepared = await doc.signatures.prepare({
465
547
  ...rest,
466
- subFilter,
548
+ ...timestamp ? {} : { subFilter },
467
549
  digest
468
550
  });
469
551
  try {
470
- const cms = signer.kind === "cms" ? await signer.sign({
471
- digest: prepared.digest,
472
- algorithm: prepared.algorithm,
473
- subFilter
474
- }) : await buildDetachedCms({
475
- digest: prepared.digest,
476
- hash: prepared.algorithm,
477
- profile,
478
- signer,
479
- signingTime: profile === "pkcs7" ? /* @__PURE__ */ new Date() : void 0
480
- });
552
+ const cms = await cmsFor(key, prepared, subFilter, profile);
481
553
  const gate = await verifyForCompletion({
482
554
  cms,
483
555
  prepared,
484
556
  profile
485
557
  });
486
- if (!gate.ok) throw new SigningError(gate.reason, gate.detail);
558
+ if (!gate.ok) throw signingRefused(gate.reason, gate.detail);
487
559
  return await doc.signatures.complete({
488
560
  signingId: prepared.signingId,
489
561
  cms,
490
562
  expectedVersion: prepared.expectedVersion
491
563
  });
492
- } catch (err) {
493
- await doc.signatures.abort(prepared.signingId).catch(() => void 0);
494
- throw err;
564
+ } catch (error) {
565
+ await doc.signatures.cancel(prepared.signingId).catch(() => void 0);
566
+ throw error;
495
567
  }
496
568
  }
569
+ /** The key's answer for the prepared digest: a service's CMS, or one built here around a raw signature. */
570
+ async function cmsFor(key, prepared, subFilter, profile) {
571
+ if (key.kind === "cms") return key.sign({
572
+ digest: prepared.digest,
573
+ algorithm: prepared.algorithm,
574
+ subFilter
575
+ });
576
+ if (profile === "timestamp") throw new EngineError(EngineErrorCode.InvalidArg, "a raw key cannot make a timestamp token");
577
+ return buildDetachedCms({
578
+ digest: prepared.digest,
579
+ hash: prepared.algorithm,
580
+ profile,
581
+ signer: key,
582
+ signingTime: profile === "pkcs7" ? /* @__PURE__ */ new Date() : void 0
583
+ });
584
+ }
497
585
  //#endregion
498
586
  //#region src/verdict.ts
499
587
  /**
500
588
  * Validate every signature of a document: integrity from the engine's
501
589
  * bytes, cryptography from the CMS, trust from the caller's anchors.
502
590
  */
503
- async function validateSignatures(doc, opts = {}) {
591
+ async function validateSignatures(doc, options = {}) {
504
592
  if (!doc.signatures) throw new Error("this engine does not implement signatures");
505
593
  const snapshot = await doc.signatures.list();
506
594
  const verdicts = [];
507
595
  for (const signature of snapshot.signatures) {
508
596
  if (!signature.signed) continue;
509
- verdicts.push(await validateOne(doc, snapshot, signature, opts));
597
+ verdicts.push(await validateOne(doc, snapshot, signature, options));
510
598
  }
511
599
  return verdicts;
512
600
  }
513
- async function validateOne(doc, snapshot, signature, opts) {
514
- const at = opts.at ?? { kind: "now" };
601
+ async function validateOne(doc, snapshot, signature, options) {
602
+ const at = options.at ?? { kind: "now" };
515
603
  let integrity = "indeterminate";
516
604
  let cryptography = "unsupported";
517
605
  let trust = "unknown";
@@ -520,21 +608,21 @@ async function validateOne(doc, snapshot, signature, opts) {
520
608
  let cms = null;
521
609
  if (!snapshot.chainValid || signature.coverage === "malformed") integrity = snapshot.chainValid ? "invalid" : "indeterminate";
522
610
  else try {
523
- internal = parseCmsInternal(await doc.signatures.contents(signature.field));
611
+ internal = parseCmsInternal(await doc.signatures.getContents(signature.field));
524
612
  cms = internal.parsed;
525
- const digestMatches = bytesEqual(await doc.signatures.digest(signature.field, cms.digestAlgorithm), cms.messageDigest);
613
+ const digestMatches = bytesEqual(await doc.signatures.getDigest(signature.field, cms.digestAlgorithm), cms.messageDigest);
526
614
  integrity = signature.coverage === "whole-revision" && digestMatches ? "valid" : "invalid";
527
- } catch (err) {
528
- integrity = err instanceof CmsError && err.reason === "unsupported" ? "indeterminate" : "invalid";
529
- if (!(err instanceof CmsError)) integrity = "indeterminate";
615
+ } catch (error) {
616
+ integrity = error instanceof CmsError && error.reason === "unsupported" ? "indeterminate" : "invalid";
617
+ if (!(error instanceof CmsError)) integrity = "indeterminate";
530
618
  }
531
619
  if (internal) {
532
620
  cryptography = await verifyCmsSignature(internal);
533
- const chain = await validateChain(internal, opts.trust, at);
621
+ const chain = await validateChain(internal, options.trust, at);
534
622
  trust = chain.status;
535
623
  trustReason = chain.reason;
536
624
  }
537
- const modifications = await modificationsOf(doc, snapshot, signature, opts.until ?? "persisted");
625
+ const modifications = await modificationsOf(doc, snapshot, signature, options.until ?? "persisted");
538
626
  let summary;
539
627
  if (integrity === "invalid" || cryptography === "invalid" || modifications.verdict === "forbidden") summary = "invalid";
540
628
  else if (integrity === "indeterminate" || cryptography === "unsupported" || modifications.verdict === "indeterminate") summary = "indeterminate";
@@ -581,67 +669,15 @@ async function modificationsOf(doc, snapshot, signature, until) {
581
669
  ...analysis.later.revisionCount > 0 ? { laterRevisions: analysis.later.revisionCount } : {},
582
670
  ...analysis.later.undoneObjectNumbers.length > 0 ? { undone: true } : {}
583
671
  };
584
- } catch (err) {
672
+ } catch (error) {
585
673
  return {
586
674
  verdict: "indeterminate",
587
- detail: `analysis failed: ${err.message}`,
675
+ detail: `analysis failed: ${error.message}`,
588
676
  basis: "persisted"
589
677
  };
590
678
  }
591
679
  }
592
680
  //#endregion
593
- //#region src/signers/self-signed.ts
594
- /**
595
- * A self-signed certificate over a WebCrypto key pair: the shape a personal
596
- * signer, a test signer, and a demo all need. It is its own trust anchor and
597
- * nothing else's — Acrobat shows a signature made with it as "validity
598
- * unknown" until the reader trusts the certificate explicitly.
599
- */
600
- async function selfSignedCertificate(input) {
601
- ensureEngine();
602
- const cert = new pkijs.Certificate();
603
- cert.version = 2;
604
- const serial = /* @__PURE__ */ new Uint8Array(8);
605
- globalThis.crypto.getRandomValues(serial);
606
- serial[0] &= 127;
607
- cert.serialNumber = new asn1js.Integer({ valueHex: serial.buffer });
608
- const name = new pkijs.AttributeTypeAndValue({
609
- type: OID.commonName,
610
- value: new asn1js.Utf8String({ value: input.commonName })
611
- });
612
- cert.issuer.typesAndValues.push(name);
613
- cert.subject.typesAndValues.push(name);
614
- const now = Date.now();
615
- cert.notBefore.value = /* @__PURE__ */ new Date(now - 24 * 3600 * 1e3);
616
- cert.notAfter.value = new Date(now + (input.validityDays ?? 365) * 24 * 3600 * 1e3);
617
- const basicConstraints = new pkijs.BasicConstraints({ cA: true });
618
- const keyUsage = new asn1js.BitString({
619
- valueHex: new Uint8Array([132]).buffer,
620
- unusedBits: 2
621
- });
622
- cert.extensions = [new pkijs.Extension({
623
- extnID: OID.basicConstraints,
624
- critical: true,
625
- extnValue: basicConstraints.toSchema().toBER(false),
626
- parsedValue: basicConstraints
627
- }), new pkijs.Extension({
628
- extnID: OID.keyUsage,
629
- critical: true,
630
- extnValue: keyUsage.toBER(false)
631
- })];
632
- await cert.subjectPublicKeyInfo.importKey(input.publicKey);
633
- await cert.sign(input.privateKey, WEBCRYPTO_HASH[input.hash]);
634
- return new Uint8Array(cert.toSchema(true).toBER(false));
635
- }
636
- /** The subject common name of a DER certificate, or null when it has none. */
637
- function certificateCommonName(der) {
638
- ensureEngine();
639
- const copy = new Uint8Array(der.byteLength);
640
- copy.set(der);
641
- const text = (pkijs.Certificate.fromBER(copy.buffer).subject.typesAndValues.find((entry) => entry.type === OID.commonName)?.value)?.valueBlock?.value;
642
- return typeof text === "string" && text.length > 0 ? text : null;
643
- }
644
- //#endregion
645
681
  //#region src/signers/webcrypto.ts
646
682
  /**
647
683
  * A `RawSigner` over a WebCrypto private key: the key never leaves the
@@ -703,14 +739,14 @@ function hashFromWebCrypto(name) {
703
739
  * self-signed certificate, never persisted. Trust it with
704
740
  * `{ anchors: async () => [signer.certificate] }`.
705
741
  */
706
- async function createTestSigner(opts = {}) {
707
- ensureEngine();
708
- const hash = opts.hash ?? "sha256";
709
- const keys = await generateSigningKeyPair(opts.algorithm ?? "RSA-PKCS1-v1_5", hash, true);
742
+ async function createTestSigner(options = {}) {
743
+ const { selfSignedCertificate } = await Promise.resolve().then(() => self_signed_exports);
744
+ const hash = options.hash ?? "sha256";
745
+ const keys = await generateSigningKeyPair(options.algorithm ?? "RSA-PKCS1-v1_5", hash, true);
710
746
  const certificate = await selfSignedCertificate({
711
747
  publicKey: keys.publicKey,
712
748
  privateKey: keys.privateKey,
713
- commonName: opts.commonName ?? "EmbedPDF test signer",
749
+ commonName: options.commonName ?? "EmbedPDF test signer",
714
750
  hash
715
751
  });
716
752
  return {
@@ -723,6 +759,25 @@ async function createTestSigner(opts = {}) {
723
759
  privateKey: keys.privateKey
724
760
  };
725
761
  }
762
+ /**
763
+ * A throwaway RFC 3161 timestamp authority for tests and demos: it stamps
764
+ * whatever digest it is given with the current time, signed by a fresh
765
+ * self-signed key. The key for `sign(doc, { kind: 'timestamp', key })`;
766
+ * never use it to stamp a real document.
767
+ */
768
+ async function createTestTimestampAuthority(options = {}) {
769
+ const authority = await createTestSigner({ commonName: options.commonName ?? "EmbedPDF test timestamp authority" });
770
+ const { buildTimestampToken } = await Promise.resolve().then(() => timestamp_exports);
771
+ return {
772
+ kind: "cms",
773
+ certificate: authority.certificate,
774
+ sign: ({ digest, algorithm }) => buildTimestampToken({
775
+ digest,
776
+ hash: algorithm,
777
+ authority
778
+ })
779
+ };
780
+ }
726
781
  /** A WebCrypto signing key pair; `extractable: false` keeps the private key inside the runtime for good. */
727
782
  async function generateSigningKeyPair(algorithm, hash, extractable) {
728
783
  return await globalThis.crypto.subtle.generateKey(algorithm === "ECDSA" ? {
@@ -742,9 +797,10 @@ async function generateSigningKeyPair(algorithm, hash, extractable) {
742
797
  //#endregion
743
798
  //#region src/signers/remote.ts
744
799
  /**
745
- * A `CmsSigner` over the customer's own signing service or HSM: the engine's
746
- * digest goes out, a finished detached CMS comes back. Nothing but the
747
- * digest (and which hash and profile it is for) ever leaves the runtime.
800
+ * A `CmsSigner` over the customer's own signing service, HSM or timestamp
801
+ * authority: the engine's digest goes out, a finished detached CMS (or, for
802
+ * `'ETSI.RFC3161'`, an RFC 3161 token) comes back. Nothing but the digest
803
+ * (and which hash and profile it is for) ever leaves the runtime.
748
804
  */
749
805
  function remoteSigner(input) {
750
806
  return {
@@ -764,10 +820,12 @@ function remoteSigner(input) {
764
820
  * unknown", which is the honest answer for a self-issued identity.
765
821
  */
766
822
  async function personalSigner(input) {
823
+ const { ensureEngine } = await import("./engine-C7lxkbZP.js").then((n) => n.n);
767
824
  ensureEngine();
768
825
  const hash = input.hash ?? "sha256";
769
826
  let record = await input.store.load(input.subject);
770
827
  if (!record) {
828
+ const { selfSignedCertificate } = await Promise.resolve().then(() => self_signed_exports);
771
829
  const keys = await generateSigningKeyPair(input.algorithm ?? "RSA-PKCS1-v1_5", hash, false);
772
830
  const certificate = await selfSignedCertificate({
773
831
  publicKey: keys.publicKey,
@@ -809,8 +867,8 @@ function memoryKeyStore() {
809
867
  * itself (structured clone), so the key survives reloads without ever
810
868
  * existing as bytes.
811
869
  */
812
- function indexedDbKeyStore(dbName, opts = {}) {
813
- const storeName = opts.storeName ?? "keys";
870
+ function indexedDbKeyStore(dbName, options = {}) {
871
+ const storeName = options.storeName ?? "keys";
814
872
  let opening = null;
815
873
  const open = () => {
816
874
  opening ?? (opening = new Promise((resolve, reject) => {
@@ -832,12 +890,68 @@ function indexedDbKeyStore(dbName, opts = {}) {
832
890
  });
833
891
  };
834
892
  return {
835
- load: async (subject) => await run("readonly", (s) => s.get(subject)) ?? null,
836
- save: (subject, record) => run("readwrite", (s) => s.put(record, subject)).then(() => void 0),
837
- remove: (subject) => run("readwrite", (s) => s.delete(subject)).then(() => void 0)
893
+ load: async (subject) => await run("readonly", (store) => store.get(subject)) ?? null,
894
+ save: (subject, record) => run("readwrite", (store) => store.put(record, subject)).then(() => void 0),
895
+ remove: (subject) => run("readwrite", (store) => store.delete(subject)).then(() => void 0)
838
896
  };
839
897
  }
840
898
  //#endregion
841
- export { CmsError, SigningError, buildDetachedCms, certificateCommonName, createTestSigner, generateSigningKeyPair, indexedDbKeyStore, memoryKeyStore, parseDetachedCms, personalSigner, profileFor, remoteSigner, selfSignedCertificate, sign, validateChain, validateSignatures, verifyCmsSignature, verifyForCompletion, webCryptoSigner };
899
+ //#region src/signers/self-signed.ts
900
+ var self_signed_exports = /* @__PURE__ */ __exportAll({
901
+ certificateCommonName: () => certificateCommonName,
902
+ selfSignedCertificate: () => selfSignedCertificate
903
+ });
904
+ /**
905
+ * A self-signed certificate over a WebCrypto key pair: the shape a personal
906
+ * signer, a test signer, and a demo all need. It is its own trust anchor and
907
+ * nothing else's — Acrobat shows a signature made with it as "validity
908
+ * unknown" until the reader trusts the certificate explicitly.
909
+ */
910
+ async function selfSignedCertificate(input) {
911
+ ensureEngine();
912
+ const cert = new pkijs.Certificate();
913
+ cert.version = 2;
914
+ const serial = /* @__PURE__ */ new Uint8Array(8);
915
+ globalThis.crypto.getRandomValues(serial);
916
+ serial[0] &= 127;
917
+ cert.serialNumber = new asn1js.Integer({ valueHex: serial.buffer });
918
+ const name = new pkijs.AttributeTypeAndValue({
919
+ type: OID.commonName,
920
+ value: new asn1js.Utf8String({ value: input.commonName })
921
+ });
922
+ cert.issuer.typesAndValues.push(name);
923
+ cert.subject.typesAndValues.push(name);
924
+ const now = Date.now();
925
+ cert.notBefore.value = /* @__PURE__ */ new Date(now - 24 * 3600 * 1e3);
926
+ cert.notAfter.value = new Date(now + (input.validityDays ?? 365) * 24 * 3600 * 1e3);
927
+ const basicConstraints = new pkijs.BasicConstraints({ cA: true });
928
+ const keyUsage = new asn1js.BitString({
929
+ valueHex: new Uint8Array([132]).buffer,
930
+ unusedBits: 2
931
+ });
932
+ cert.extensions = [new pkijs.Extension({
933
+ extnID: OID.basicConstraints,
934
+ critical: true,
935
+ extnValue: basicConstraints.toSchema().toBER(false),
936
+ parsedValue: basicConstraints
937
+ }), new pkijs.Extension({
938
+ extnID: OID.keyUsage,
939
+ critical: true,
940
+ extnValue: keyUsage.toBER(false)
941
+ })];
942
+ await cert.subjectPublicKeyInfo.importKey(input.publicKey);
943
+ await cert.sign(input.privateKey, WEBCRYPTO_HASH[input.hash]);
944
+ return new Uint8Array(cert.toSchema(true).toBER(false));
945
+ }
946
+ /** The subject common name of a DER certificate, or null when it has none. */
947
+ function certificateCommonName(der) {
948
+ ensureEngine();
949
+ const copy = new Uint8Array(der.byteLength);
950
+ copy.set(der);
951
+ const text = (pkijs.Certificate.fromBER(copy.buffer).subject.typesAndValues.find((entry) => entry.type === OID.commonName)?.value)?.valueBlock?.value;
952
+ return typeof text === "string" && text.length > 0 ? text : null;
953
+ }
954
+ //#endregion
955
+ export { CmsError, buildDetachedCms, certificateCommonName, createTestSigner, createTestTimestampAuthority, generateSigningKeyPair, indexedDbKeyStore, memoryKeyStore, parseDetachedCms, personalSigner, profileFor, remoteSigner, selfSignedCertificate, sign, validateChain, validateSignatures, verifyCmsSignature, verifyForCompletion, webCryptoSigner };
842
956
 
843
957
  //# sourceMappingURL=index.js.map