@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/engine-BPmX1KyF.cjs +108 -0
- package/dist/engine-BPmX1KyF.cjs.map +1 -0
- package/dist/engine-C7lxkbZP.js +53 -0
- package/dist/engine-C7lxkbZP.js.map +1 -0
- package/dist/index.cjs +380 -288
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -19
- package/dist/index.d.ts +50 -19
- package/dist/index.js +337 -223
- package/dist/index.js.map +1 -1
- package/dist/oids-BUfXTCCm.cjs +87 -0
- package/dist/oids-BUfXTCCm.cjs.map +1 -0
- package/dist/oids-BhZXI2ZX.js +58 -0
- package/dist/oids-BhZXI2ZX.js.map +1 -0
- package/package.json +3 -3
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
|
-
|
|
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 (
|
|
111
|
-
if (
|
|
112
|
-
throw new CmsError("malformed", `CMS does not parse as SignedData: ${
|
|
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((
|
|
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((
|
|
50
|
+
const attr = (type) => signedAttrs.find((attribute) => attribute.type === type);
|
|
125
51
|
const contentType = attr(OID.contentType);
|
|
126
|
-
if (contentType
|
|
52
|
+
if (contentType) {
|
|
127
53
|
const oid = contentType.values[0];
|
|
128
|
-
if (!oid || oid.valueBlock.toString() !==
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
|
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((
|
|
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 (
|
|
210
|
-
return
|
|
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 (
|
|
220
|
-
const 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((
|
|
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
|
-
|
|
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 (
|
|
386
|
+
} catch (error) {
|
|
352
387
|
return {
|
|
353
388
|
ok: false,
|
|
354
389
|
reason: "malformed",
|
|
355
|
-
detail:
|
|
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 (
|
|
486
|
+
} catch (error) {
|
|
411
487
|
return {
|
|
412
488
|
status: "untrusted",
|
|
413
|
-
reason:
|
|
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((
|
|
506
|
+
path: (result.certificatePath ?? []).map((certificate) => new Uint8Array(certificate.toSchema(true).toBER(false)))
|
|
431
507
|
};
|
|
432
|
-
} catch (
|
|
508
|
+
} catch (error) {
|
|
433
509
|
return {
|
|
434
510
|
status: "untrusted",
|
|
435
|
-
reason:
|
|
511
|
+
reason: error.message
|
|
436
512
|
};
|
|
437
513
|
}
|
|
438
514
|
}
|
|
439
515
|
//#endregion
|
|
440
516
|
//#region src/sign.ts
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
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 ?? (
|
|
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 =
|
|
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
|
|
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 (
|
|
493
|
-
await doc.signatures.
|
|
494
|
-
throw
|
|
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,
|
|
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,
|
|
597
|
+
verdicts.push(await validateOne(doc, snapshot, signature, options));
|
|
510
598
|
}
|
|
511
599
|
return verdicts;
|
|
512
600
|
}
|
|
513
|
-
async function validateOne(doc, snapshot, signature,
|
|
514
|
-
const at =
|
|
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.
|
|
611
|
+
internal = parseCmsInternal(await doc.signatures.getContents(signature.field));
|
|
524
612
|
cms = internal.parsed;
|
|
525
|
-
const digestMatches = bytesEqual(await doc.signatures.
|
|
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 (
|
|
528
|
-
integrity =
|
|
529
|
-
if (!(
|
|
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,
|
|
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,
|
|
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 (
|
|
672
|
+
} catch (error) {
|
|
585
673
|
return {
|
|
586
674
|
verdict: "indeterminate",
|
|
587
|
-
detail: `analysis failed: ${
|
|
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(
|
|
707
|
-
|
|
708
|
-
const hash =
|
|
709
|
-
const keys = await generateSigningKeyPair(
|
|
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:
|
|
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
|
|
746
|
-
* digest goes out, a finished detached CMS
|
|
747
|
-
*
|
|
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,
|
|
813
|
-
const storeName =
|
|
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", (
|
|
836
|
-
save: (subject, record) => run("readwrite", (
|
|
837
|
-
remove: (subject) => run("readwrite", (
|
|
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
|
-
|
|
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
|