@embedpdf/core-signature 3.0.0-next.13

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 ADDED
@@ -0,0 +1,843 @@
1
+ import * as asn1js from "asn1js";
2
+ 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
88
+ //#region src/cms/parse.ts
89
+ var CmsError = class extends Error {
90
+ constructor(reason, message) {
91
+ super(message);
92
+ this.reason = reason;
93
+ this.name = "CmsError";
94
+ }
95
+ };
96
+ /** Parse a detached CMS SignedData with exactly one signer. Throws `CmsError`. */
97
+ function parseDetachedCms(cms) {
98
+ return parseCmsInternal(cms).parsed;
99
+ }
100
+ function parseCmsInternal(cms) {
101
+ if (cms.byteLength === 0) throw new CmsError("malformed", "empty CMS");
102
+ const asn1 = asn1js.fromBER(toArrayBuffer(cms));
103
+ if (asn1.offset === -1) throw new CmsError("malformed", "CMS is not valid BER");
104
+ let contentInfo;
105
+ let signedData;
106
+ try {
107
+ contentInfo = new pkijs.ContentInfo({ schema: asn1.result });
108
+ if (contentInfo.contentType !== OID.signedData) throw new CmsError("unsupported", `CMS content type ${contentInfo.contentType} is not SignedData`);
109
+ 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}`);
113
+ }
114
+ if (signedData.encapContentInfo.eContent) throw new CmsError("unsupported", "CMS carries encapsulated content; a PDF signature is detached");
115
+ if (signedData.signerInfos.length !== 1) throw new CmsError("unsupported", `CMS has ${signedData.signerInfos.length} signers; exactly one is expected`);
116
+ const signerInfo = signedData.signerInfos[0];
117
+ const digestAlgorithm = DIGEST_BY_OID[signerInfo.digestAlgorithm.algorithmId];
118
+ if (!digestAlgorithm) throw new CmsError("unsupported", `digest algorithm ${signerInfo.digestAlgorithm.algorithmId}`);
119
+ const certificates = (signedData.certificates ?? []).filter((c) => c instanceof pkijs.Certificate);
120
+ const signerCertificate = findSignerCertificate(signerInfo, certificates);
121
+ if (!signerCertificate) throw new CmsError("malformed", "the signer certificate is not carried by the CMS");
122
+ const signedAttrs = signerInfo.signedAttrs?.attributes ?? [];
123
+ 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);
125
+ const contentType = attr(OID.contentType);
126
+ if (contentType && contentType.values[0]?.valueBlock?.toString?.() !== OID.data) {
127
+ 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");
129
+ }
130
+ const messageDigestValue = attr(OID.messageDigest)?.values[0];
131
+ if (!messageDigestValue || !(messageDigestValue instanceof asn1js.OctetString)) throw new CmsError("malformed", "CMS has no message-digest attribute");
132
+ const messageDigest = new Uint8Array(messageDigestValue.valueBlock.valueHexView);
133
+ let signingTime = null;
134
+ const timeValue = attr(OID.signingTime)?.values[0];
135
+ if (timeValue instanceof asn1js.UTCTime || timeValue instanceof asn1js.GeneralizedTime) signingTime = timeValue.toDate();
136
+ const essAttr = attr(OID.signingCertificateV2);
137
+ let essCertHash = null;
138
+ if (essAttr) {
139
+ const members = ((essAttr.values[0]?.valueBlock?.value?.[0])?.valueBlock?.value?.[0])?.valueBlock?.value ?? [];
140
+ let cursor = 0;
141
+ const head = members[0];
142
+ if (head instanceof asn1js.Sequence && head.valueBlock.value[0] instanceof asn1js.ObjectIdentifier) {
143
+ if (head.valueBlock.value[0].valueBlock.toString() !== OID.sha256) throw new CmsError("unsupported", "signing-certificate-v2 uses a hash other than SHA-256");
144
+ cursor = 1;
145
+ }
146
+ const hash = members[cursor];
147
+ if (!(hash instanceof asn1js.OctetString)) throw new CmsError("malformed", "signing-certificate-v2 carries no certHash");
148
+ essCertHash = new Uint8Array(hash.valueBlock.valueHexView);
149
+ }
150
+ let timestampToken = null;
151
+ const tokenAttr = signerInfo.unsignedAttrs?.attributes.find((a) => a.type === OID.timestampToken);
152
+ if (tokenAttr?.values[0]) timestampToken = new Uint8Array(tokenAttr.values[0].toBER(false));
153
+ const ordered = [signerCertificate, ...certificates.filter((c) => c !== signerCertificate)];
154
+ return {
155
+ parsed: {
156
+ digestAlgorithm,
157
+ messageDigest,
158
+ signingTime,
159
+ signerCertificate: certDer(signerCertificate),
160
+ certificates: ordered.map(certDer),
161
+ signatureAlgorithm: signerInfo.signatureAlgorithm.algorithmId,
162
+ signingCertificateV2: essAttr !== void 0,
163
+ timestampToken
164
+ },
165
+ signedData,
166
+ signerInfo,
167
+ signerCertificate,
168
+ certificates: ordered,
169
+ essCertHash
170
+ };
171
+ }
172
+ function certDer(cert) {
173
+ return new Uint8Array(cert.toSchema(true).toBER(false));
174
+ }
175
+ function findSignerCertificate(signerInfo, certificates) {
176
+ const sid = signerInfo.sid;
177
+ if (sid instanceof pkijs.IssuerAndSerialNumber) {
178
+ for (const cert of certificates) if (cert.issuer.isEqual(sid.issuer) && bytesEqual(new Uint8Array(cert.serialNumber.valueBlock.valueHexView), new Uint8Array(sid.serialNumber.valueBlock.valueHexView))) return cert;
179
+ return null;
180
+ }
181
+ const ski = sid?.valueBlock?.valueHexView;
182
+ if (!ski) return null;
183
+ for (const cert of certificates) {
184
+ const ext = cert.extensions?.find((e) => e.extnID === OID.subjectKeyIdentifier);
185
+ const value = ext ? asn1js.fromBER(ext.extnValue.valueBlock.valueHexView) : null;
186
+ const inner = value && value.offset !== -1 ? value.result : null;
187
+ if (inner && bytesEqual(new Uint8Array(inner.valueBlock.valueHexView), new Uint8Array(ski))) return cert;
188
+ }
189
+ return null;
190
+ }
191
+ //#endregion
192
+ //#region src/cms/verify.ts
193
+ /** The DER of the signed attributes as they are signed: re-tagged as a SET. */
194
+ function signedAttributesDer(signerInfo) {
195
+ const set = new asn1js.Set({ value: (signerInfo.signedAttrs?.attributes ?? []).map((a) => a.toSchema()) });
196
+ return new Uint8Array(set.toBER(false));
197
+ }
198
+ /**
199
+ * Does the CMS's signature verify with the certificate it names, over the
200
+ * attributes it signed? Says nothing about the content: the message digest
201
+ * is compared by the caller (`validateSignatures`, `verifyForCompletion`).
202
+ * `unsupported` means the algorithm is unknown to this engine, which is
203
+ * never `invalid`.
204
+ */
205
+ async function verifyCmsSignature(cms) {
206
+ let internal;
207
+ try {
208
+ internal = cms instanceof Uint8Array ? parseCmsInternal(cms) : cms;
209
+ } catch (err) {
210
+ return err instanceof CmsError && err.reason === "unsupported" ? "unsupported" : "invalid";
211
+ }
212
+ const engine = ensureEngine();
213
+ const { signerInfo, signerCertificate, parsed, essCertHash } = internal;
214
+ if (essCertHash) {
215
+ if (!bytesEqual(new Uint8Array(await engine.digest("SHA-256", toArrayBuffer(parsed.signerCertificate))), essCertHash)) return "invalid";
216
+ }
217
+ try {
218
+ 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 ?? "";
221
+ return /unsupported|unknown|not supported|not implemented/i.test(message) ? "unsupported" : "invalid";
222
+ }
223
+ }
224
+ //#endregion
225
+ //#region src/cms/build.ts
226
+ /**
227
+ * Build a detached CMS SignedData around a raw signature: content-type,
228
+ * message-digest, and for CAdES-B the ESS signing-certificate-v2 attribute;
229
+ * the signer's certificate chain; one SignerInfo identified by issuer and
230
+ * serial number.
231
+ */
232
+ async function buildDetachedCms(input) {
233
+ const engine = ensureEngine();
234
+ if (input.digest.byteLength !== DIGEST_LENGTH[input.hash]) throw new Error(`digest is ${input.digest.byteLength} bytes; ${input.hash} needs ${DIGEST_LENGTH[input.hash]}`);
235
+ if (input.signer.hash !== input.hash) throw new Error(`the signer hashes with ${input.signer.hash}, the document was prepared with ${input.hash}`);
236
+ if (input.signer.certificateChain.length === 0) throw new Error("the signer carries no certificate");
237
+ const chain = input.signer.certificateChain.map((der) => pkijs.Certificate.fromBER(toArrayBuffer(der)));
238
+ const signerCert = chain[0];
239
+ const attributes = [new pkijs.Attribute({
240
+ type: OID.contentType,
241
+ values: [new asn1js.ObjectIdentifier({ value: OID.data })]
242
+ }), new pkijs.Attribute({
243
+ type: OID.messageDigest,
244
+ values: [new asn1js.OctetString({ valueHex: toArrayBuffer(input.digest) })]
245
+ })];
246
+ if (input.profile === "pkcs7" && input.signingTime) attributes.push(new pkijs.Attribute({
247
+ type: OID.signingTime,
248
+ values: [new asn1js.UTCTime({ valueDate: input.signingTime })]
249
+ }));
250
+ if (input.profile === "cades-b") {
251
+ const certHash = await engine.digest("SHA-256", toArrayBuffer(input.signer.certificateChain[0]));
252
+ const issuerSerial = new pkijs.IssuerSerial({
253
+ issuer: new pkijs.GeneralNames({ names: [new pkijs.GeneralName({
254
+ type: 4,
255
+ value: signerCert.issuer
256
+ })] }),
257
+ serialNumber: signerCert.serialNumber
258
+ });
259
+ const essCertId = new asn1js.Sequence({ value: [new asn1js.OctetString({ valueHex: certHash }), issuerSerial.toSchema()] });
260
+ attributes.push(new pkijs.Attribute({
261
+ type: OID.signingCertificateV2,
262
+ values: [new asn1js.Sequence({ value: [new asn1js.Sequence({ value: [essCertId] })] })]
263
+ }));
264
+ }
265
+ const digestAlgorithm = new pkijs.AlgorithmIdentifier({ algorithmId: OID_BY_DIGEST[input.hash] });
266
+ const signerInfo = new pkijs.SignerInfo({
267
+ version: 1,
268
+ sid: new pkijs.IssuerAndSerialNumber({
269
+ issuer: signerCert.issuer,
270
+ serialNumber: signerCert.serialNumber
271
+ }),
272
+ digestAlgorithm,
273
+ signedAttrs: new pkijs.SignedAndUnsignedAttributes({
274
+ type: 0,
275
+ attributes
276
+ }),
277
+ signatureAlgorithm: signatureAlgorithmFor(input.signer.algorithm, input.hash)
278
+ });
279
+ const toSign = new asn1js.Set({ value: attributes.map((a) => a.toSchema()) }).toBER(false);
280
+ 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 });
283
+ const signedData = new pkijs.SignedData({
284
+ version: 1,
285
+ digestAlgorithms: [digestAlgorithm],
286
+ encapContentInfo: new pkijs.EncapsulatedContentInfo({ eContentType: OID.data }),
287
+ certificates: chain,
288
+ signerInfos: [signerInfo]
289
+ });
290
+ const contentInfo = new pkijs.ContentInfo({
291
+ contentType: OID.signedData,
292
+ content: signedData.toSchema(true)
293
+ });
294
+ return new Uint8Array(contentInfo.toSchema().toBER(false));
295
+ }
296
+ function signatureAlgorithmFor(algorithm, hash) {
297
+ switch (algorithm) {
298
+ case "RSA-PKCS1-v1_5": return new pkijs.AlgorithmIdentifier({
299
+ algorithmId: {
300
+ sha256: OID.sha256WithRsa,
301
+ sha384: OID.sha384WithRsa,
302
+ sha512: OID.sha512WithRsa
303
+ }[hash],
304
+ algorithmParams: new asn1js.Null()
305
+ });
306
+ case "ECDSA": return new pkijs.AlgorithmIdentifier({ algorithmId: {
307
+ sha256: OID.ecdsaSha256,
308
+ sha384: OID.ecdsaSha384,
309
+ sha512: OID.ecdsaSha512
310
+ }[hash] });
311
+ case "RSA-PSS": {
312
+ const hashAlgorithm = new pkijs.AlgorithmIdentifier({
313
+ algorithmId: OID_BY_DIGEST[hash],
314
+ algorithmParams: new asn1js.Null()
315
+ });
316
+ const params = new pkijs.RSASSAPSSParams({
317
+ hashAlgorithm,
318
+ maskGenAlgorithm: new pkijs.AlgorithmIdentifier({
319
+ algorithmId: OID.mgf1,
320
+ algorithmParams: hashAlgorithm.toSchema()
321
+ }),
322
+ saltLength: DIGEST_LENGTH[hash]
323
+ });
324
+ return new pkijs.AlgorithmIdentifier({
325
+ algorithmId: OID.rsaPss,
326
+ algorithmParams: params.toSchema()
327
+ });
328
+ }
329
+ }
330
+ }
331
+ //#endregion
332
+ //#region src/gate.ts
333
+ /**
334
+ * The gate between a signer's answer and `signatures.complete`: the CMS
335
+ * must fit the reserved room, hash with the prepared algorithm, carry the
336
+ * prepared digest as its message digest, verify with its own certificate,
337
+ * and match the profile the `/SubFilter` promises (CAdES: ESS
338
+ * signing-certificate-v2 present, no CMS signing-time). A bad CMS never
339
+ * reaches the document.
340
+ */
341
+ async function verifyForCompletion(input) {
342
+ const { cms, prepared, profile } = input;
343
+ if (cms.byteLength > prepared.contentsSize) return {
344
+ ok: false,
345
+ reason: "too-large",
346
+ detail: `the CMS is ${cms.byteLength} bytes; ${prepared.contentsSize} were reserved`
347
+ };
348
+ let internal;
349
+ try {
350
+ internal = parseCmsInternal(cms);
351
+ } catch (err) {
352
+ return {
353
+ ok: false,
354
+ reason: "malformed",
355
+ detail: err instanceof CmsError ? err.message : String(err)
356
+ };
357
+ }
358
+ const parsed = internal.parsed;
359
+ if (parsed.digestAlgorithm !== prepared.algorithm) return {
360
+ ok: false,
361
+ reason: "algorithm-mismatch",
362
+ detail: `the CMS digests with ${parsed.digestAlgorithm}, the document was prepared with ${prepared.algorithm}`
363
+ };
364
+ if (!bytesEqual(parsed.messageDigest, prepared.digest)) return {
365
+ ok: false,
366
+ reason: "digest-mismatch",
367
+ detail: "the CMS message digest is not the prepared digest"
368
+ };
369
+ if (profile === "cades-b") {
370
+ if (!parsed.signingCertificateV2) return {
371
+ ok: false,
372
+ reason: "profile-violation",
373
+ detail: "CAdES requires the ESS signing-certificate-v2 attribute"
374
+ };
375
+ if (parsed.signingTime) return {
376
+ ok: false,
377
+ reason: "profile-violation",
378
+ detail: "PAdES forbids the CMS signing-time attribute"
379
+ };
380
+ }
381
+ const cryptography = await verifyCmsSignature(internal);
382
+ if (cryptography !== "valid") return {
383
+ ok: false,
384
+ reason: "signature-invalid",
385
+ detail: cryptography === "unsupported" ? "the signature algorithm is not supported" : "the signature does not verify"
386
+ };
387
+ return { ok: true };
388
+ }
389
+ //#endregion
390
+ //#region src/trust.ts
391
+ /**
392
+ * Build and validate the certificate path from the CMS's signer to one
393
+ * of the port's anchors at `at`. No port, or a port with no anchors, is
394
+ * `unknown`: nothing was checked, and nothing was refused.
395
+ */
396
+ async function validateChain(cms, trust, at = { kind: "now" }) {
397
+ if (!trust) return {
398
+ status: "unknown",
399
+ reason: "no trust anchors configured"
400
+ };
401
+ const anchors = await trust.anchors();
402
+ if (anchors.length === 0) return {
403
+ status: "unknown",
404
+ reason: "no trust anchors configured"
405
+ };
406
+ ensureEngine();
407
+ let internal;
408
+ try {
409
+ internal = cms instanceof Uint8Array ? parseCmsInternal(cms) : cms;
410
+ } catch (err) {
411
+ return {
412
+ status: "untrusted",
413
+ reason: err.message
414
+ };
415
+ }
416
+ const trustedCerts = anchors.map((der) => pkijs.Certificate.fromBER(toArrayBuffer(der)));
417
+ const chainEngine = new pkijs.CertificateChainValidationEngine({
418
+ certs: internal.certificates,
419
+ trustedCerts,
420
+ checkDate: at.kind === "now" ? /* @__PURE__ */ new Date() : at.genTime
421
+ });
422
+ try {
423
+ const result = await chainEngine.verify();
424
+ if (!result.result) return {
425
+ status: "untrusted",
426
+ reason: result.resultMessage || `chain validation code ${result.resultCode}`
427
+ };
428
+ return {
429
+ status: "trusted",
430
+ path: (result.certificatePath ?? []).map((c) => new Uint8Array(c.toSchema(true).toBER(false)))
431
+ };
432
+ } catch (err) {
433
+ return {
434
+ status: "untrusted",
435
+ reason: err.message
436
+ };
437
+ }
438
+ }
439
+ //#endregion
440
+ //#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
+ };
448
+ function profileFor(subFilter) {
449
+ return subFilter === "adbe.pkcs7.detached" ? "pkcs7" : "cades-b";
450
+ }
451
+ /**
452
+ * Sign a field in one call: `prepare` on the engine, the signer turns the
453
+ * digest into a CMS (or a raw signature this package wraps), the gate
454
+ * checks the CMS matches what was prepared, `complete` installs it. Any
455
+ * failure aborts the candidate and rethrows; the document is never left
456
+ * with a pending signing.
457
+ */
458
+ 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";
462
+ const profile = profileFor(subFilter);
463
+ const digest = rest.digest ?? (signer.kind === "raw" ? signer.hash : "sha256");
464
+ const prepared = await doc.signatures.prepare({
465
+ ...rest,
466
+ subFilter,
467
+ digest
468
+ });
469
+ 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
+ });
481
+ const gate = await verifyForCompletion({
482
+ cms,
483
+ prepared,
484
+ profile
485
+ });
486
+ if (!gate.ok) throw new SigningError(gate.reason, gate.detail);
487
+ return await doc.signatures.complete({
488
+ signingId: prepared.signingId,
489
+ cms,
490
+ expectedVersion: prepared.expectedVersion
491
+ });
492
+ } catch (err) {
493
+ await doc.signatures.abort(prepared.signingId).catch(() => void 0);
494
+ throw err;
495
+ }
496
+ }
497
+ //#endregion
498
+ //#region src/verdict.ts
499
+ /**
500
+ * Validate every signature of a document: integrity from the engine's
501
+ * bytes, cryptography from the CMS, trust from the caller's anchors.
502
+ */
503
+ async function validateSignatures(doc, opts = {}) {
504
+ if (!doc.signatures) throw new Error("this engine does not implement signatures");
505
+ const snapshot = await doc.signatures.list();
506
+ const verdicts = [];
507
+ for (const signature of snapshot.signatures) {
508
+ if (!signature.signed) continue;
509
+ verdicts.push(await validateOne(doc, snapshot, signature, opts));
510
+ }
511
+ return verdicts;
512
+ }
513
+ async function validateOne(doc, snapshot, signature, opts) {
514
+ const at = opts.at ?? { kind: "now" };
515
+ let integrity = "indeterminate";
516
+ let cryptography = "unsupported";
517
+ let trust = "unknown";
518
+ let trustReason;
519
+ let internal = null;
520
+ let cms = null;
521
+ if (!snapshot.chainValid || signature.coverage === "malformed") integrity = snapshot.chainValid ? "invalid" : "indeterminate";
522
+ else try {
523
+ internal = parseCmsInternal(await doc.signatures.contents(signature.field));
524
+ cms = internal.parsed;
525
+ const digestMatches = bytesEqual(await doc.signatures.digest(signature.field, cms.digestAlgorithm), cms.messageDigest);
526
+ 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";
530
+ }
531
+ if (internal) {
532
+ cryptography = await verifyCmsSignature(internal);
533
+ const chain = await validateChain(internal, opts.trust, at);
534
+ trust = chain.status;
535
+ trustReason = chain.reason;
536
+ }
537
+ const modifications = await modificationsOf(doc, snapshot, signature, opts.until ?? "persisted");
538
+ let summary;
539
+ if (integrity === "invalid" || cryptography === "invalid" || modifications.verdict === "forbidden") summary = "invalid";
540
+ else if (integrity === "indeterminate" || cryptography === "unsupported" || modifications.verdict === "indeterminate") summary = "indeterminate";
541
+ else summary = trust === "trusted" ? "valid" : "valid-untrusted";
542
+ return {
543
+ signature,
544
+ integrity,
545
+ cryptography,
546
+ trust,
547
+ trustReason,
548
+ validatedAt: at,
549
+ modifications,
550
+ summary,
551
+ cms
552
+ };
553
+ }
554
+ /**
555
+ * What changed after the signature, from the engine's revision analysis:
556
+ * the current document judged against the revision the signature sealed
557
+ * (net state; a certification also replays every intervening revision).
558
+ * A signature that seals the last revision is `unchanged` without analysis.
559
+ */
560
+ async function modificationsOf(doc, snapshot, signature, until) {
561
+ if (signature.revisionIndex === null) return {
562
+ verdict: "indeterminate",
563
+ detail: "the signature seals no whole revision",
564
+ basis: "persisted"
565
+ };
566
+ if (until === "persisted" && signature.revisionIndex === snapshot.revisions.length - 1) return {
567
+ verdict: "unchanged",
568
+ basis: "persisted"
569
+ };
570
+ try {
571
+ const analysis = await doc.signatures.analyze({
572
+ since: { signatureIndex: signature.index },
573
+ until
574
+ });
575
+ const primary = analysis.current.primary;
576
+ const detail = primary && primary.verdict === "forbidden" ? `object ${primary.objectNumber}, ${primary.rule}${primary.detail ? `: ${primary.detail}` : ""}` : primary && primary.verdict === "incomplete" ? `object ${primary.objectNumber}: ${primary.detail ?? "incomplete evidence"}` : void 0;
577
+ return {
578
+ verdict: analysis.current.verdict,
579
+ detail,
580
+ basis: analysis.basis.source,
581
+ ...analysis.later.revisionCount > 0 ? { laterRevisions: analysis.later.revisionCount } : {},
582
+ ...analysis.later.undoneObjectNumbers.length > 0 ? { undone: true } : {}
583
+ };
584
+ } catch (err) {
585
+ return {
586
+ verdict: "indeterminate",
587
+ detail: `analysis failed: ${err.message}`,
588
+ basis: "persisted"
589
+ };
590
+ }
591
+ }
592
+ //#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
+ //#region src/signers/webcrypto.ts
646
+ /**
647
+ * A `RawSigner` over a WebCrypto private key: the key never leaves the
648
+ * runtime. The algorithm and hash come from the key itself.
649
+ */
650
+ function webCryptoSigner(input) {
651
+ const keyAlgorithm = input.privateKey.algorithm;
652
+ let algorithm;
653
+ let hash;
654
+ switch (keyAlgorithm.name) {
655
+ case "RSASSA-PKCS1-v1_5":
656
+ algorithm = "RSA-PKCS1-v1_5";
657
+ hash = hashFromWebCrypto(keyAlgorithm.hash.name);
658
+ break;
659
+ case "RSA-PSS":
660
+ algorithm = "RSA-PSS";
661
+ hash = hashFromWebCrypto(keyAlgorithm.hash.name);
662
+ break;
663
+ case "ECDSA":
664
+ algorithm = "ECDSA";
665
+ hash = input.hash ?? "sha256";
666
+ break;
667
+ default: throw new Error(`unsupported WebCrypto key algorithm ${keyAlgorithm.name}`);
668
+ }
669
+ return {
670
+ kind: "raw",
671
+ certificateChain: input.certificateChain,
672
+ algorithm,
673
+ hash,
674
+ async sign(data) {
675
+ const params = algorithm === "RSA-PSS" ? {
676
+ name: "RSA-PSS",
677
+ saltLength: {
678
+ sha256: 32,
679
+ sha384: 48,
680
+ sha512: 64
681
+ }[hash]
682
+ } : algorithm === "ECDSA" ? {
683
+ name: "ECDSA",
684
+ hash: WEBCRYPTO_HASH[hash]
685
+ } : { name: "RSASSA-PKCS1-v1_5" };
686
+ const copy = new Uint8Array(data.byteLength);
687
+ copy.set(data);
688
+ const signature = await globalThis.crypto.subtle.sign(params, input.privateKey, copy);
689
+ return new Uint8Array(signature);
690
+ }
691
+ };
692
+ }
693
+ function hashFromWebCrypto(name) {
694
+ switch (name) {
695
+ case "SHA-256": return "sha256";
696
+ case "SHA-384": return "sha384";
697
+ case "SHA-512": return "sha512";
698
+ default: throw new Error(`unsupported key hash ${name}`);
699
+ }
700
+ }
701
+ /**
702
+ * A throwaway signer for tests and demos: a fresh key pair and a
703
+ * self-signed certificate, never persisted. Trust it with
704
+ * `{ anchors: async () => [signer.certificate] }`.
705
+ */
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);
710
+ const certificate = await selfSignedCertificate({
711
+ publicKey: keys.publicKey,
712
+ privateKey: keys.privateKey,
713
+ commonName: opts.commonName ?? "EmbedPDF test signer",
714
+ hash
715
+ });
716
+ return {
717
+ ...webCryptoSigner({
718
+ privateKey: keys.privateKey,
719
+ certificateChain: [certificate],
720
+ hash
721
+ }),
722
+ certificate,
723
+ privateKey: keys.privateKey
724
+ };
725
+ }
726
+ /** A WebCrypto signing key pair; `extractable: false` keeps the private key inside the runtime for good. */
727
+ async function generateSigningKeyPair(algorithm, hash, extractable) {
728
+ return await globalThis.crypto.subtle.generateKey(algorithm === "ECDSA" ? {
729
+ name: "ECDSA",
730
+ namedCurve: "P-256"
731
+ } : {
732
+ name: algorithm === "RSA-PSS" ? "RSA-PSS" : "RSASSA-PKCS1-v1_5",
733
+ modulusLength: 2048,
734
+ publicExponent: new Uint8Array([
735
+ 1,
736
+ 0,
737
+ 1
738
+ ]),
739
+ hash: WEBCRYPTO_HASH[hash]
740
+ }, extractable, ["sign", "verify"]);
741
+ }
742
+ //#endregion
743
+ //#region src/signers/remote.ts
744
+ /**
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.
748
+ */
749
+ function remoteSigner(input) {
750
+ return {
751
+ kind: "cms",
752
+ sign: (request) => input.sign(request)
753
+ };
754
+ }
755
+ //#endregion
756
+ //#region src/signers/personal.ts
757
+ /**
758
+ * One self-signed identity per subject (a person's name), persisted in the
759
+ * caller's store and created on first use. The private key is generated
760
+ * non-extractable: it can sign, it can be stored by the browser, it can
761
+ * never be read out. This is Preview's "sign with your own signature"
762
+ * experience with real cryptography behind it — a reader who trusts the
763
+ * certificate validates the signature; every other reader sees "validity
764
+ * unknown", which is the honest answer for a self-issued identity.
765
+ */
766
+ async function personalSigner(input) {
767
+ ensureEngine();
768
+ const hash = input.hash ?? "sha256";
769
+ let record = await input.store.load(input.subject);
770
+ if (!record) {
771
+ const keys = await generateSigningKeyPair(input.algorithm ?? "RSA-PKCS1-v1_5", hash, false);
772
+ const certificate = await selfSignedCertificate({
773
+ publicKey: keys.publicKey,
774
+ privateKey: keys.privateKey,
775
+ commonName: input.subject,
776
+ hash
777
+ });
778
+ record = {
779
+ privateKey: keys.privateKey,
780
+ certificate
781
+ };
782
+ await input.store.save(input.subject, record);
783
+ }
784
+ return {
785
+ ...webCryptoSigner({
786
+ privateKey: record.privateKey,
787
+ certificateChain: [record.certificate],
788
+ hash
789
+ }),
790
+ certificate: record.certificate,
791
+ subject: input.subject
792
+ };
793
+ }
794
+ /** An in-memory store: tests, and runtimes without IndexedDB. */
795
+ function memoryKeyStore() {
796
+ const records = /* @__PURE__ */ new Map();
797
+ return {
798
+ load: async (subject) => records.get(subject) ?? null,
799
+ save: async (subject, record) => {
800
+ records.set(subject, record);
801
+ },
802
+ remove: async (subject) => {
803
+ records.delete(subject);
804
+ }
805
+ };
806
+ }
807
+ /**
808
+ * An IndexedDB store: the browser keeps the non-extractable `CryptoKey`
809
+ * itself (structured clone), so the key survives reloads without ever
810
+ * existing as bytes.
811
+ */
812
+ function indexedDbKeyStore(dbName, opts = {}) {
813
+ const storeName = opts.storeName ?? "keys";
814
+ let opening = null;
815
+ const open = () => {
816
+ opening ?? (opening = new Promise((resolve, reject) => {
817
+ const request = indexedDB.open(dbName, 1);
818
+ request.onupgradeneeded = () => {
819
+ if (!request.result.objectStoreNames.contains(storeName)) request.result.createObjectStore(storeName);
820
+ };
821
+ request.onsuccess = () => resolve(request.result);
822
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexedDB open failed"));
823
+ }));
824
+ return opening;
825
+ };
826
+ const run = async (mode, op) => {
827
+ const db = await open();
828
+ return new Promise((resolve, reject) => {
829
+ const request = op(db.transaction(storeName, mode).objectStore(storeName));
830
+ request.onsuccess = () => resolve(request.result);
831
+ request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexedDB request failed"));
832
+ });
833
+ };
834
+ 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)
838
+ };
839
+ }
840
+ //#endregion
841
+ export { CmsError, SigningError, buildDetachedCms, certificateCommonName, createTestSigner, generateSigningKeyPair, indexedDbKeyStore, memoryKeyStore, parseDetachedCms, personalSigner, profileFor, remoteSigner, selfSignedCertificate, sign, validateChain, validateSignatures, verifyCmsSignature, verifyForCompletion, webCryptoSigner };
842
+
843
+ //# sourceMappingURL=index.js.map