@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/LICENSE +192 -0
- package/dist/index.cjs +886 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +328 -0
- package/dist/index.d.ts +328 -0
- package/dist/index.js +843 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let asn1js = require("asn1js");
|
|
25
|
+
asn1js = __toESM(asn1js, 1);
|
|
26
|
+
let pkijs = require("pkijs");
|
|
27
|
+
pkijs = __toESM(pkijs, 1);
|
|
28
|
+
//#region src/cms/engine.ts
|
|
29
|
+
let ensured = false;
|
|
30
|
+
/**
|
|
31
|
+
* PKI.js needs a WebCrypto engine. Browsers and Node ≥ 19 expose
|
|
32
|
+
* `globalThis.crypto`; register it once, lazily, so importing this package
|
|
33
|
+
* has no side effect.
|
|
34
|
+
*/
|
|
35
|
+
function ensureEngine() {
|
|
36
|
+
if (!ensured) {
|
|
37
|
+
ensured = true;
|
|
38
|
+
if (!pkijs.getCrypto()) {
|
|
39
|
+
const webcrypto = globalThis.crypto;
|
|
40
|
+
if (!webcrypto?.subtle) throw new Error("@embedpdf/core-signature needs WebCrypto (globalThis.crypto.subtle)");
|
|
41
|
+
pkijs.setEngine("embedpdf", new pkijs.CryptoEngine({
|
|
42
|
+
name: "embedpdf",
|
|
43
|
+
crypto: webcrypto
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return pkijs.getCrypto(true);
|
|
48
|
+
}
|
|
49
|
+
function toArrayBuffer(view) {
|
|
50
|
+
const copy = new Uint8Array(view.byteLength);
|
|
51
|
+
copy.set(view);
|
|
52
|
+
return copy.buffer;
|
|
53
|
+
}
|
|
54
|
+
function bytesEqual(a, b) {
|
|
55
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
56
|
+
for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/cms/oids.ts
|
|
61
|
+
const OID = {
|
|
62
|
+
data: "1.2.840.113549.1.7.1",
|
|
63
|
+
signedData: "1.2.840.113549.1.7.2",
|
|
64
|
+
contentType: "1.2.840.113549.1.9.3",
|
|
65
|
+
messageDigest: "1.2.840.113549.1.9.4",
|
|
66
|
+
signingTime: "1.2.840.113549.1.9.5",
|
|
67
|
+
signingCertificateV2: "1.2.840.113549.1.9.16.2.47",
|
|
68
|
+
timestampToken: "1.2.840.113549.1.9.16.2.14",
|
|
69
|
+
sha1: "1.3.14.3.2.26",
|
|
70
|
+
sha256: "2.16.840.1.101.3.4.2.1",
|
|
71
|
+
sha384: "2.16.840.1.101.3.4.2.2",
|
|
72
|
+
sha512: "2.16.840.1.101.3.4.2.3",
|
|
73
|
+
rsaEncryption: "1.2.840.113549.1.1.1",
|
|
74
|
+
sha256WithRsa: "1.2.840.113549.1.1.11",
|
|
75
|
+
sha384WithRsa: "1.2.840.113549.1.1.12",
|
|
76
|
+
sha512WithRsa: "1.2.840.113549.1.1.13",
|
|
77
|
+
rsaPss: "1.2.840.113549.1.1.10",
|
|
78
|
+
mgf1: "1.2.840.113549.1.1.8",
|
|
79
|
+
ecdsaSha256: "1.2.840.10045.4.3.2",
|
|
80
|
+
ecdsaSha384: "1.2.840.10045.4.3.3",
|
|
81
|
+
ecdsaSha512: "1.2.840.10045.4.3.4",
|
|
82
|
+
subjectKeyIdentifier: "2.5.29.14",
|
|
83
|
+
keyUsage: "2.5.29.15",
|
|
84
|
+
basicConstraints: "2.5.29.19",
|
|
85
|
+
commonName: "2.5.4.3"
|
|
86
|
+
};
|
|
87
|
+
const DIGEST_BY_OID = {
|
|
88
|
+
[OID.sha1]: "sha1",
|
|
89
|
+
[OID.sha256]: "sha256",
|
|
90
|
+
[OID.sha384]: "sha384",
|
|
91
|
+
[OID.sha512]: "sha512"
|
|
92
|
+
};
|
|
93
|
+
const OID_BY_DIGEST = {
|
|
94
|
+
sha1: OID.sha1,
|
|
95
|
+
sha256: OID.sha256,
|
|
96
|
+
sha384: OID.sha384,
|
|
97
|
+
sha512: OID.sha512
|
|
98
|
+
};
|
|
99
|
+
/** WebCrypto hash names. */
|
|
100
|
+
const WEBCRYPTO_HASH = {
|
|
101
|
+
sha1: "SHA-1",
|
|
102
|
+
sha256: "SHA-256",
|
|
103
|
+
sha384: "SHA-384",
|
|
104
|
+
sha512: "SHA-512"
|
|
105
|
+
};
|
|
106
|
+
const DIGEST_LENGTH = {
|
|
107
|
+
sha1: 20,
|
|
108
|
+
sha256: 32,
|
|
109
|
+
sha384: 48,
|
|
110
|
+
sha512: 64
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/cms/parse.ts
|
|
114
|
+
var CmsError = class extends Error {
|
|
115
|
+
constructor(reason, message) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.reason = reason;
|
|
118
|
+
this.name = "CmsError";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/** Parse a detached CMS SignedData with exactly one signer. Throws `CmsError`. */
|
|
122
|
+
function parseDetachedCms(cms) {
|
|
123
|
+
return parseCmsInternal(cms).parsed;
|
|
124
|
+
}
|
|
125
|
+
function parseCmsInternal(cms) {
|
|
126
|
+
if (cms.byteLength === 0) throw new CmsError("malformed", "empty CMS");
|
|
127
|
+
const asn1 = asn1js.fromBER(toArrayBuffer(cms));
|
|
128
|
+
if (asn1.offset === -1) throw new CmsError("malformed", "CMS is not valid BER");
|
|
129
|
+
let contentInfo;
|
|
130
|
+
let signedData;
|
|
131
|
+
try {
|
|
132
|
+
contentInfo = new pkijs.ContentInfo({ schema: asn1.result });
|
|
133
|
+
if (contentInfo.contentType !== OID.signedData) throw new CmsError("unsupported", `CMS content type ${contentInfo.contentType} is not SignedData`);
|
|
134
|
+
signedData = new pkijs.SignedData({ schema: contentInfo.content });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err instanceof CmsError) throw err;
|
|
137
|
+
throw new CmsError("malformed", `CMS does not parse as SignedData: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
if (signedData.encapContentInfo.eContent) throw new CmsError("unsupported", "CMS carries encapsulated content; a PDF signature is detached");
|
|
140
|
+
if (signedData.signerInfos.length !== 1) throw new CmsError("unsupported", `CMS has ${signedData.signerInfos.length} signers; exactly one is expected`);
|
|
141
|
+
const signerInfo = signedData.signerInfos[0];
|
|
142
|
+
const digestAlgorithm = DIGEST_BY_OID[signerInfo.digestAlgorithm.algorithmId];
|
|
143
|
+
if (!digestAlgorithm) throw new CmsError("unsupported", `digest algorithm ${signerInfo.digestAlgorithm.algorithmId}`);
|
|
144
|
+
const certificates = (signedData.certificates ?? []).filter((c) => c instanceof pkijs.Certificate);
|
|
145
|
+
const signerCertificate = findSignerCertificate(signerInfo, certificates);
|
|
146
|
+
if (!signerCertificate) throw new CmsError("malformed", "the signer certificate is not carried by the CMS");
|
|
147
|
+
const signedAttrs = signerInfo.signedAttrs?.attributes ?? [];
|
|
148
|
+
if (signedAttrs.length === 0) throw new CmsError("unsupported", "CMS has no signed attributes; a PDF signature needs message-digest");
|
|
149
|
+
const attr = (type) => signedAttrs.find((a) => a.type === type);
|
|
150
|
+
const contentType = attr(OID.contentType);
|
|
151
|
+
if (contentType && contentType.values[0]?.valueBlock?.toString?.() !== OID.data) {
|
|
152
|
+
const oid = contentType.values[0];
|
|
153
|
+
if (!oid || oid.valueBlock.toString() !== OID.data) throw new CmsError("unsupported", "CMS content-type attribute is not id-data");
|
|
154
|
+
}
|
|
155
|
+
const messageDigestValue = attr(OID.messageDigest)?.values[0];
|
|
156
|
+
if (!messageDigestValue || !(messageDigestValue instanceof asn1js.OctetString)) throw new CmsError("malformed", "CMS has no message-digest attribute");
|
|
157
|
+
const messageDigest = new Uint8Array(messageDigestValue.valueBlock.valueHexView);
|
|
158
|
+
let signingTime = null;
|
|
159
|
+
const timeValue = attr(OID.signingTime)?.values[0];
|
|
160
|
+
if (timeValue instanceof asn1js.UTCTime || timeValue instanceof asn1js.GeneralizedTime) signingTime = timeValue.toDate();
|
|
161
|
+
const essAttr = attr(OID.signingCertificateV2);
|
|
162
|
+
let essCertHash = null;
|
|
163
|
+
if (essAttr) {
|
|
164
|
+
const members = ((essAttr.values[0]?.valueBlock?.value?.[0])?.valueBlock?.value?.[0])?.valueBlock?.value ?? [];
|
|
165
|
+
let cursor = 0;
|
|
166
|
+
const head = members[0];
|
|
167
|
+
if (head instanceof asn1js.Sequence && head.valueBlock.value[0] instanceof asn1js.ObjectIdentifier) {
|
|
168
|
+
if (head.valueBlock.value[0].valueBlock.toString() !== OID.sha256) throw new CmsError("unsupported", "signing-certificate-v2 uses a hash other than SHA-256");
|
|
169
|
+
cursor = 1;
|
|
170
|
+
}
|
|
171
|
+
const hash = members[cursor];
|
|
172
|
+
if (!(hash instanceof asn1js.OctetString)) throw new CmsError("malformed", "signing-certificate-v2 carries no certHash");
|
|
173
|
+
essCertHash = new Uint8Array(hash.valueBlock.valueHexView);
|
|
174
|
+
}
|
|
175
|
+
let timestampToken = null;
|
|
176
|
+
const tokenAttr = signerInfo.unsignedAttrs?.attributes.find((a) => a.type === OID.timestampToken);
|
|
177
|
+
if (tokenAttr?.values[0]) timestampToken = new Uint8Array(tokenAttr.values[0].toBER(false));
|
|
178
|
+
const ordered = [signerCertificate, ...certificates.filter((c) => c !== signerCertificate)];
|
|
179
|
+
return {
|
|
180
|
+
parsed: {
|
|
181
|
+
digestAlgorithm,
|
|
182
|
+
messageDigest,
|
|
183
|
+
signingTime,
|
|
184
|
+
signerCertificate: certDer(signerCertificate),
|
|
185
|
+
certificates: ordered.map(certDer),
|
|
186
|
+
signatureAlgorithm: signerInfo.signatureAlgorithm.algorithmId,
|
|
187
|
+
signingCertificateV2: essAttr !== void 0,
|
|
188
|
+
timestampToken
|
|
189
|
+
},
|
|
190
|
+
signedData,
|
|
191
|
+
signerInfo,
|
|
192
|
+
signerCertificate,
|
|
193
|
+
certificates: ordered,
|
|
194
|
+
essCertHash
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function certDer(cert) {
|
|
198
|
+
return new Uint8Array(cert.toSchema(true).toBER(false));
|
|
199
|
+
}
|
|
200
|
+
function findSignerCertificate(signerInfo, certificates) {
|
|
201
|
+
const sid = signerInfo.sid;
|
|
202
|
+
if (sid instanceof pkijs.IssuerAndSerialNumber) {
|
|
203
|
+
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;
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const ski = sid?.valueBlock?.valueHexView;
|
|
207
|
+
if (!ski) return null;
|
|
208
|
+
for (const cert of certificates) {
|
|
209
|
+
const ext = cert.extensions?.find((e) => e.extnID === OID.subjectKeyIdentifier);
|
|
210
|
+
const value = ext ? asn1js.fromBER(ext.extnValue.valueBlock.valueHexView) : null;
|
|
211
|
+
const inner = value && value.offset !== -1 ? value.result : null;
|
|
212
|
+
if (inner && bytesEqual(new Uint8Array(inner.valueBlock.valueHexView), new Uint8Array(ski))) return cert;
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/cms/verify.ts
|
|
218
|
+
/** The DER of the signed attributes as they are signed: re-tagged as a SET. */
|
|
219
|
+
function signedAttributesDer(signerInfo) {
|
|
220
|
+
const set = new asn1js.Set({ value: (signerInfo.signedAttrs?.attributes ?? []).map((a) => a.toSchema()) });
|
|
221
|
+
return new Uint8Array(set.toBER(false));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Does the CMS's signature verify with the certificate it names, over the
|
|
225
|
+
* attributes it signed? Says nothing about the content: the message digest
|
|
226
|
+
* is compared by the caller (`validateSignatures`, `verifyForCompletion`).
|
|
227
|
+
* `unsupported` means the algorithm is unknown to this engine, which is
|
|
228
|
+
* never `invalid`.
|
|
229
|
+
*/
|
|
230
|
+
async function verifyCmsSignature(cms) {
|
|
231
|
+
let internal;
|
|
232
|
+
try {
|
|
233
|
+
internal = cms instanceof Uint8Array ? parseCmsInternal(cms) : cms;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return err instanceof CmsError && err.reason === "unsupported" ? "unsupported" : "invalid";
|
|
236
|
+
}
|
|
237
|
+
const engine = ensureEngine();
|
|
238
|
+
const { signerInfo, signerCertificate, parsed, essCertHash } = internal;
|
|
239
|
+
if (essCertHash) {
|
|
240
|
+
if (!bytesEqual(new Uint8Array(await engine.digest("SHA-256", toArrayBuffer(parsed.signerCertificate))), essCertHash)) return "invalid";
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
return await engine.verifyWithPublicKey(toArrayBuffer(signedAttributesDer(signerInfo)), signerInfo.signature, signerCertificate.subjectPublicKeyInfo, signerInfo.signatureAlgorithm, WEBCRYPTO_HASH[parsed.digestAlgorithm]) ? "valid" : "invalid";
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const message = err.message ?? "";
|
|
246
|
+
return /unsupported|unknown|not supported|not implemented/i.test(message) ? "unsupported" : "invalid";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/cms/build.ts
|
|
251
|
+
/**
|
|
252
|
+
* Build a detached CMS SignedData around a raw signature: content-type,
|
|
253
|
+
* message-digest, and for CAdES-B the ESS signing-certificate-v2 attribute;
|
|
254
|
+
* the signer's certificate chain; one SignerInfo identified by issuer and
|
|
255
|
+
* serial number.
|
|
256
|
+
*/
|
|
257
|
+
async function buildDetachedCms(input) {
|
|
258
|
+
const engine = ensureEngine();
|
|
259
|
+
if (input.digest.byteLength !== DIGEST_LENGTH[input.hash]) throw new Error(`digest is ${input.digest.byteLength} bytes; ${input.hash} needs ${DIGEST_LENGTH[input.hash]}`);
|
|
260
|
+
if (input.signer.hash !== input.hash) throw new Error(`the signer hashes with ${input.signer.hash}, the document was prepared with ${input.hash}`);
|
|
261
|
+
if (input.signer.certificateChain.length === 0) throw new Error("the signer carries no certificate");
|
|
262
|
+
const chain = input.signer.certificateChain.map((der) => pkijs.Certificate.fromBER(toArrayBuffer(der)));
|
|
263
|
+
const signerCert = chain[0];
|
|
264
|
+
const attributes = [new pkijs.Attribute({
|
|
265
|
+
type: OID.contentType,
|
|
266
|
+
values: [new asn1js.ObjectIdentifier({ value: OID.data })]
|
|
267
|
+
}), new pkijs.Attribute({
|
|
268
|
+
type: OID.messageDigest,
|
|
269
|
+
values: [new asn1js.OctetString({ valueHex: toArrayBuffer(input.digest) })]
|
|
270
|
+
})];
|
|
271
|
+
if (input.profile === "pkcs7" && input.signingTime) attributes.push(new pkijs.Attribute({
|
|
272
|
+
type: OID.signingTime,
|
|
273
|
+
values: [new asn1js.UTCTime({ valueDate: input.signingTime })]
|
|
274
|
+
}));
|
|
275
|
+
if (input.profile === "cades-b") {
|
|
276
|
+
const certHash = await engine.digest("SHA-256", toArrayBuffer(input.signer.certificateChain[0]));
|
|
277
|
+
const issuerSerial = new pkijs.IssuerSerial({
|
|
278
|
+
issuer: new pkijs.GeneralNames({ names: [new pkijs.GeneralName({
|
|
279
|
+
type: 4,
|
|
280
|
+
value: signerCert.issuer
|
|
281
|
+
})] }),
|
|
282
|
+
serialNumber: signerCert.serialNumber
|
|
283
|
+
});
|
|
284
|
+
const essCertId = new asn1js.Sequence({ value: [new asn1js.OctetString({ valueHex: certHash }), issuerSerial.toSchema()] });
|
|
285
|
+
attributes.push(new pkijs.Attribute({
|
|
286
|
+
type: OID.signingCertificateV2,
|
|
287
|
+
values: [new asn1js.Sequence({ value: [new asn1js.Sequence({ value: [essCertId] })] })]
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
const digestAlgorithm = new pkijs.AlgorithmIdentifier({ algorithmId: OID_BY_DIGEST[input.hash] });
|
|
291
|
+
const signerInfo = new pkijs.SignerInfo({
|
|
292
|
+
version: 1,
|
|
293
|
+
sid: new pkijs.IssuerAndSerialNumber({
|
|
294
|
+
issuer: signerCert.issuer,
|
|
295
|
+
serialNumber: signerCert.serialNumber
|
|
296
|
+
}),
|
|
297
|
+
digestAlgorithm,
|
|
298
|
+
signedAttrs: new pkijs.SignedAndUnsignedAttributes({
|
|
299
|
+
type: 0,
|
|
300
|
+
attributes
|
|
301
|
+
}),
|
|
302
|
+
signatureAlgorithm: signatureAlgorithmFor(input.signer.algorithm, input.hash)
|
|
303
|
+
});
|
|
304
|
+
const toSign = new asn1js.Set({ value: attributes.map((a) => a.toSchema()) }).toBER(false);
|
|
305
|
+
const raw = await input.signer.sign(new Uint8Array(toSign));
|
|
306
|
+
const signature = input.signer.algorithm === "ECDSA" ? pkijs.createCMSECDSASignature(toArrayBuffer(raw)) : toArrayBuffer(raw);
|
|
307
|
+
signerInfo.signature = new asn1js.OctetString({ valueHex: signature });
|
|
308
|
+
const signedData = new pkijs.SignedData({
|
|
309
|
+
version: 1,
|
|
310
|
+
digestAlgorithms: [digestAlgorithm],
|
|
311
|
+
encapContentInfo: new pkijs.EncapsulatedContentInfo({ eContentType: OID.data }),
|
|
312
|
+
certificates: chain,
|
|
313
|
+
signerInfos: [signerInfo]
|
|
314
|
+
});
|
|
315
|
+
const contentInfo = new pkijs.ContentInfo({
|
|
316
|
+
contentType: OID.signedData,
|
|
317
|
+
content: signedData.toSchema(true)
|
|
318
|
+
});
|
|
319
|
+
return new Uint8Array(contentInfo.toSchema().toBER(false));
|
|
320
|
+
}
|
|
321
|
+
function signatureAlgorithmFor(algorithm, hash) {
|
|
322
|
+
switch (algorithm) {
|
|
323
|
+
case "RSA-PKCS1-v1_5": return new pkijs.AlgorithmIdentifier({
|
|
324
|
+
algorithmId: {
|
|
325
|
+
sha256: OID.sha256WithRsa,
|
|
326
|
+
sha384: OID.sha384WithRsa,
|
|
327
|
+
sha512: OID.sha512WithRsa
|
|
328
|
+
}[hash],
|
|
329
|
+
algorithmParams: new asn1js.Null()
|
|
330
|
+
});
|
|
331
|
+
case "ECDSA": return new pkijs.AlgorithmIdentifier({ algorithmId: {
|
|
332
|
+
sha256: OID.ecdsaSha256,
|
|
333
|
+
sha384: OID.ecdsaSha384,
|
|
334
|
+
sha512: OID.ecdsaSha512
|
|
335
|
+
}[hash] });
|
|
336
|
+
case "RSA-PSS": {
|
|
337
|
+
const hashAlgorithm = new pkijs.AlgorithmIdentifier({
|
|
338
|
+
algorithmId: OID_BY_DIGEST[hash],
|
|
339
|
+
algorithmParams: new asn1js.Null()
|
|
340
|
+
});
|
|
341
|
+
const params = new pkijs.RSASSAPSSParams({
|
|
342
|
+
hashAlgorithm,
|
|
343
|
+
maskGenAlgorithm: new pkijs.AlgorithmIdentifier({
|
|
344
|
+
algorithmId: OID.mgf1,
|
|
345
|
+
algorithmParams: hashAlgorithm.toSchema()
|
|
346
|
+
}),
|
|
347
|
+
saltLength: DIGEST_LENGTH[hash]
|
|
348
|
+
});
|
|
349
|
+
return new pkijs.AlgorithmIdentifier({
|
|
350
|
+
algorithmId: OID.rsaPss,
|
|
351
|
+
algorithmParams: params.toSchema()
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/gate.ts
|
|
358
|
+
/**
|
|
359
|
+
* The gate between a signer's answer and `signatures.complete`: the CMS
|
|
360
|
+
* must fit the reserved room, hash with the prepared algorithm, carry the
|
|
361
|
+
* prepared digest as its message digest, verify with its own certificate,
|
|
362
|
+
* and match the profile the `/SubFilter` promises (CAdES: ESS
|
|
363
|
+
* signing-certificate-v2 present, no CMS signing-time). A bad CMS never
|
|
364
|
+
* reaches the document.
|
|
365
|
+
*/
|
|
366
|
+
async function verifyForCompletion(input) {
|
|
367
|
+
const { cms, prepared, profile } = input;
|
|
368
|
+
if (cms.byteLength > prepared.contentsSize) return {
|
|
369
|
+
ok: false,
|
|
370
|
+
reason: "too-large",
|
|
371
|
+
detail: `the CMS is ${cms.byteLength} bytes; ${prepared.contentsSize} were reserved`
|
|
372
|
+
};
|
|
373
|
+
let internal;
|
|
374
|
+
try {
|
|
375
|
+
internal = parseCmsInternal(cms);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
reason: "malformed",
|
|
380
|
+
detail: err instanceof CmsError ? err.message : String(err)
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const parsed = internal.parsed;
|
|
384
|
+
if (parsed.digestAlgorithm !== prepared.algorithm) return {
|
|
385
|
+
ok: false,
|
|
386
|
+
reason: "algorithm-mismatch",
|
|
387
|
+
detail: `the CMS digests with ${parsed.digestAlgorithm}, the document was prepared with ${prepared.algorithm}`
|
|
388
|
+
};
|
|
389
|
+
if (!bytesEqual(parsed.messageDigest, prepared.digest)) return {
|
|
390
|
+
ok: false,
|
|
391
|
+
reason: "digest-mismatch",
|
|
392
|
+
detail: "the CMS message digest is not the prepared digest"
|
|
393
|
+
};
|
|
394
|
+
if (profile === "cades-b") {
|
|
395
|
+
if (!parsed.signingCertificateV2) return {
|
|
396
|
+
ok: false,
|
|
397
|
+
reason: "profile-violation",
|
|
398
|
+
detail: "CAdES requires the ESS signing-certificate-v2 attribute"
|
|
399
|
+
};
|
|
400
|
+
if (parsed.signingTime) return {
|
|
401
|
+
ok: false,
|
|
402
|
+
reason: "profile-violation",
|
|
403
|
+
detail: "PAdES forbids the CMS signing-time attribute"
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const cryptography = await verifyCmsSignature(internal);
|
|
407
|
+
if (cryptography !== "valid") return {
|
|
408
|
+
ok: false,
|
|
409
|
+
reason: "signature-invalid",
|
|
410
|
+
detail: cryptography === "unsupported" ? "the signature algorithm is not supported" : "the signature does not verify"
|
|
411
|
+
};
|
|
412
|
+
return { ok: true };
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/trust.ts
|
|
416
|
+
/**
|
|
417
|
+
* Build and validate the certificate path from the CMS's signer to one
|
|
418
|
+
* of the port's anchors at `at`. No port, or a port with no anchors, is
|
|
419
|
+
* `unknown`: nothing was checked, and nothing was refused.
|
|
420
|
+
*/
|
|
421
|
+
async function validateChain(cms, trust, at = { kind: "now" }) {
|
|
422
|
+
if (!trust) return {
|
|
423
|
+
status: "unknown",
|
|
424
|
+
reason: "no trust anchors configured"
|
|
425
|
+
};
|
|
426
|
+
const anchors = await trust.anchors();
|
|
427
|
+
if (anchors.length === 0) return {
|
|
428
|
+
status: "unknown",
|
|
429
|
+
reason: "no trust anchors configured"
|
|
430
|
+
};
|
|
431
|
+
ensureEngine();
|
|
432
|
+
let internal;
|
|
433
|
+
try {
|
|
434
|
+
internal = cms instanceof Uint8Array ? parseCmsInternal(cms) : cms;
|
|
435
|
+
} catch (err) {
|
|
436
|
+
return {
|
|
437
|
+
status: "untrusted",
|
|
438
|
+
reason: err.message
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const trustedCerts = anchors.map((der) => pkijs.Certificate.fromBER(toArrayBuffer(der)));
|
|
442
|
+
const chainEngine = new pkijs.CertificateChainValidationEngine({
|
|
443
|
+
certs: internal.certificates,
|
|
444
|
+
trustedCerts,
|
|
445
|
+
checkDate: at.kind === "now" ? /* @__PURE__ */ new Date() : at.genTime
|
|
446
|
+
});
|
|
447
|
+
try {
|
|
448
|
+
const result = await chainEngine.verify();
|
|
449
|
+
if (!result.result) return {
|
|
450
|
+
status: "untrusted",
|
|
451
|
+
reason: result.resultMessage || `chain validation code ${result.resultCode}`
|
|
452
|
+
};
|
|
453
|
+
return {
|
|
454
|
+
status: "trusted",
|
|
455
|
+
path: (result.certificatePath ?? []).map((c) => new Uint8Array(c.toSchema(true).toBER(false)))
|
|
456
|
+
};
|
|
457
|
+
} catch (err) {
|
|
458
|
+
return {
|
|
459
|
+
status: "untrusted",
|
|
460
|
+
reason: err.message
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/sign.ts
|
|
466
|
+
var SigningError = class extends Error {
|
|
467
|
+
constructor(reason, detail) {
|
|
468
|
+
super(`signing refused (${reason}): ${detail}`);
|
|
469
|
+
this.reason = reason;
|
|
470
|
+
this.name = "SigningError";
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
function profileFor(subFilter) {
|
|
474
|
+
return subFilter === "adbe.pkcs7.detached" ? "pkcs7" : "cades-b";
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Sign a field in one call: `prepare` on the engine, the signer turns the
|
|
478
|
+
* digest into a CMS (or a raw signature this package wraps), the gate
|
|
479
|
+
* checks the CMS matches what was prepared, `complete` installs it. Any
|
|
480
|
+
* failure aborts the candidate and rethrows; the document is never left
|
|
481
|
+
* with a pending signing.
|
|
482
|
+
*/
|
|
483
|
+
async function sign(doc, input) {
|
|
484
|
+
if (!doc.signatures) throw new Error("this engine does not implement signatures");
|
|
485
|
+
const { signer, ...rest } = input;
|
|
486
|
+
const subFilter = rest.subFilter ?? "ETSI.CAdES.detached";
|
|
487
|
+
const profile = profileFor(subFilter);
|
|
488
|
+
const digest = rest.digest ?? (signer.kind === "raw" ? signer.hash : "sha256");
|
|
489
|
+
const prepared = await doc.signatures.prepare({
|
|
490
|
+
...rest,
|
|
491
|
+
subFilter,
|
|
492
|
+
digest
|
|
493
|
+
});
|
|
494
|
+
try {
|
|
495
|
+
const cms = signer.kind === "cms" ? await signer.sign({
|
|
496
|
+
digest: prepared.digest,
|
|
497
|
+
algorithm: prepared.algorithm,
|
|
498
|
+
subFilter
|
|
499
|
+
}) : await buildDetachedCms({
|
|
500
|
+
digest: prepared.digest,
|
|
501
|
+
hash: prepared.algorithm,
|
|
502
|
+
profile,
|
|
503
|
+
signer,
|
|
504
|
+
signingTime: profile === "pkcs7" ? /* @__PURE__ */ new Date() : void 0
|
|
505
|
+
});
|
|
506
|
+
const gate = await verifyForCompletion({
|
|
507
|
+
cms,
|
|
508
|
+
prepared,
|
|
509
|
+
profile
|
|
510
|
+
});
|
|
511
|
+
if (!gate.ok) throw new SigningError(gate.reason, gate.detail);
|
|
512
|
+
return await doc.signatures.complete({
|
|
513
|
+
signingId: prepared.signingId,
|
|
514
|
+
cms,
|
|
515
|
+
expectedVersion: prepared.expectedVersion
|
|
516
|
+
});
|
|
517
|
+
} catch (err) {
|
|
518
|
+
await doc.signatures.abort(prepared.signingId).catch(() => void 0);
|
|
519
|
+
throw err;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/verdict.ts
|
|
524
|
+
/**
|
|
525
|
+
* Validate every signature of a document: integrity from the engine's
|
|
526
|
+
* bytes, cryptography from the CMS, trust from the caller's anchors.
|
|
527
|
+
*/
|
|
528
|
+
async function validateSignatures(doc, opts = {}) {
|
|
529
|
+
if (!doc.signatures) throw new Error("this engine does not implement signatures");
|
|
530
|
+
const snapshot = await doc.signatures.list();
|
|
531
|
+
const verdicts = [];
|
|
532
|
+
for (const signature of snapshot.signatures) {
|
|
533
|
+
if (!signature.signed) continue;
|
|
534
|
+
verdicts.push(await validateOne(doc, snapshot, signature, opts));
|
|
535
|
+
}
|
|
536
|
+
return verdicts;
|
|
537
|
+
}
|
|
538
|
+
async function validateOne(doc, snapshot, signature, opts) {
|
|
539
|
+
const at = opts.at ?? { kind: "now" };
|
|
540
|
+
let integrity = "indeterminate";
|
|
541
|
+
let cryptography = "unsupported";
|
|
542
|
+
let trust = "unknown";
|
|
543
|
+
let trustReason;
|
|
544
|
+
let internal = null;
|
|
545
|
+
let cms = null;
|
|
546
|
+
if (!snapshot.chainValid || signature.coverage === "malformed") integrity = snapshot.chainValid ? "invalid" : "indeterminate";
|
|
547
|
+
else try {
|
|
548
|
+
internal = parseCmsInternal(await doc.signatures.contents(signature.field));
|
|
549
|
+
cms = internal.parsed;
|
|
550
|
+
const digestMatches = bytesEqual(await doc.signatures.digest(signature.field, cms.digestAlgorithm), cms.messageDigest);
|
|
551
|
+
integrity = signature.coverage === "whole-revision" && digestMatches ? "valid" : "invalid";
|
|
552
|
+
} catch (err) {
|
|
553
|
+
integrity = err instanceof CmsError && err.reason === "unsupported" ? "indeterminate" : "invalid";
|
|
554
|
+
if (!(err instanceof CmsError)) integrity = "indeterminate";
|
|
555
|
+
}
|
|
556
|
+
if (internal) {
|
|
557
|
+
cryptography = await verifyCmsSignature(internal);
|
|
558
|
+
const chain = await validateChain(internal, opts.trust, at);
|
|
559
|
+
trust = chain.status;
|
|
560
|
+
trustReason = chain.reason;
|
|
561
|
+
}
|
|
562
|
+
const modifications = await modificationsOf(doc, snapshot, signature, opts.until ?? "persisted");
|
|
563
|
+
let summary;
|
|
564
|
+
if (integrity === "invalid" || cryptography === "invalid" || modifications.verdict === "forbidden") summary = "invalid";
|
|
565
|
+
else if (integrity === "indeterminate" || cryptography === "unsupported" || modifications.verdict === "indeterminate") summary = "indeterminate";
|
|
566
|
+
else summary = trust === "trusted" ? "valid" : "valid-untrusted";
|
|
567
|
+
return {
|
|
568
|
+
signature,
|
|
569
|
+
integrity,
|
|
570
|
+
cryptography,
|
|
571
|
+
trust,
|
|
572
|
+
trustReason,
|
|
573
|
+
validatedAt: at,
|
|
574
|
+
modifications,
|
|
575
|
+
summary,
|
|
576
|
+
cms
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* What changed after the signature, from the engine's revision analysis:
|
|
581
|
+
* the current document judged against the revision the signature sealed
|
|
582
|
+
* (net state; a certification also replays every intervening revision).
|
|
583
|
+
* A signature that seals the last revision is `unchanged` without analysis.
|
|
584
|
+
*/
|
|
585
|
+
async function modificationsOf(doc, snapshot, signature, until) {
|
|
586
|
+
if (signature.revisionIndex === null) return {
|
|
587
|
+
verdict: "indeterminate",
|
|
588
|
+
detail: "the signature seals no whole revision",
|
|
589
|
+
basis: "persisted"
|
|
590
|
+
};
|
|
591
|
+
if (until === "persisted" && signature.revisionIndex === snapshot.revisions.length - 1) return {
|
|
592
|
+
verdict: "unchanged",
|
|
593
|
+
basis: "persisted"
|
|
594
|
+
};
|
|
595
|
+
try {
|
|
596
|
+
const analysis = await doc.signatures.analyze({
|
|
597
|
+
since: { signatureIndex: signature.index },
|
|
598
|
+
until
|
|
599
|
+
});
|
|
600
|
+
const primary = analysis.current.primary;
|
|
601
|
+
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;
|
|
602
|
+
return {
|
|
603
|
+
verdict: analysis.current.verdict,
|
|
604
|
+
detail,
|
|
605
|
+
basis: analysis.basis.source,
|
|
606
|
+
...analysis.later.revisionCount > 0 ? { laterRevisions: analysis.later.revisionCount } : {},
|
|
607
|
+
...analysis.later.undoneObjectNumbers.length > 0 ? { undone: true } : {}
|
|
608
|
+
};
|
|
609
|
+
} catch (err) {
|
|
610
|
+
return {
|
|
611
|
+
verdict: "indeterminate",
|
|
612
|
+
detail: `analysis failed: ${err.message}`,
|
|
613
|
+
basis: "persisted"
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/signers/self-signed.ts
|
|
619
|
+
/**
|
|
620
|
+
* A self-signed certificate over a WebCrypto key pair: the shape a personal
|
|
621
|
+
* signer, a test signer, and a demo all need. It is its own trust anchor and
|
|
622
|
+
* nothing else's — Acrobat shows a signature made with it as "validity
|
|
623
|
+
* unknown" until the reader trusts the certificate explicitly.
|
|
624
|
+
*/
|
|
625
|
+
async function selfSignedCertificate(input) {
|
|
626
|
+
ensureEngine();
|
|
627
|
+
const cert = new pkijs.Certificate();
|
|
628
|
+
cert.version = 2;
|
|
629
|
+
const serial = /* @__PURE__ */ new Uint8Array(8);
|
|
630
|
+
globalThis.crypto.getRandomValues(serial);
|
|
631
|
+
serial[0] &= 127;
|
|
632
|
+
cert.serialNumber = new asn1js.Integer({ valueHex: serial.buffer });
|
|
633
|
+
const name = new pkijs.AttributeTypeAndValue({
|
|
634
|
+
type: OID.commonName,
|
|
635
|
+
value: new asn1js.Utf8String({ value: input.commonName })
|
|
636
|
+
});
|
|
637
|
+
cert.issuer.typesAndValues.push(name);
|
|
638
|
+
cert.subject.typesAndValues.push(name);
|
|
639
|
+
const now = Date.now();
|
|
640
|
+
cert.notBefore.value = /* @__PURE__ */ new Date(now - 24 * 3600 * 1e3);
|
|
641
|
+
cert.notAfter.value = new Date(now + (input.validityDays ?? 365) * 24 * 3600 * 1e3);
|
|
642
|
+
const basicConstraints = new pkijs.BasicConstraints({ cA: true });
|
|
643
|
+
const keyUsage = new asn1js.BitString({
|
|
644
|
+
valueHex: new Uint8Array([132]).buffer,
|
|
645
|
+
unusedBits: 2
|
|
646
|
+
});
|
|
647
|
+
cert.extensions = [new pkijs.Extension({
|
|
648
|
+
extnID: OID.basicConstraints,
|
|
649
|
+
critical: true,
|
|
650
|
+
extnValue: basicConstraints.toSchema().toBER(false),
|
|
651
|
+
parsedValue: basicConstraints
|
|
652
|
+
}), new pkijs.Extension({
|
|
653
|
+
extnID: OID.keyUsage,
|
|
654
|
+
critical: true,
|
|
655
|
+
extnValue: keyUsage.toBER(false)
|
|
656
|
+
})];
|
|
657
|
+
await cert.subjectPublicKeyInfo.importKey(input.publicKey);
|
|
658
|
+
await cert.sign(input.privateKey, WEBCRYPTO_HASH[input.hash]);
|
|
659
|
+
return new Uint8Array(cert.toSchema(true).toBER(false));
|
|
660
|
+
}
|
|
661
|
+
/** The subject common name of a DER certificate, or null when it has none. */
|
|
662
|
+
function certificateCommonName(der) {
|
|
663
|
+
ensureEngine();
|
|
664
|
+
const copy = new Uint8Array(der.byteLength);
|
|
665
|
+
copy.set(der);
|
|
666
|
+
const text = (pkijs.Certificate.fromBER(copy.buffer).subject.typesAndValues.find((entry) => entry.type === OID.commonName)?.value)?.valueBlock?.value;
|
|
667
|
+
return typeof text === "string" && text.length > 0 ? text : null;
|
|
668
|
+
}
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/signers/webcrypto.ts
|
|
671
|
+
/**
|
|
672
|
+
* A `RawSigner` over a WebCrypto private key: the key never leaves the
|
|
673
|
+
* runtime. The algorithm and hash come from the key itself.
|
|
674
|
+
*/
|
|
675
|
+
function webCryptoSigner(input) {
|
|
676
|
+
const keyAlgorithm = input.privateKey.algorithm;
|
|
677
|
+
let algorithm;
|
|
678
|
+
let hash;
|
|
679
|
+
switch (keyAlgorithm.name) {
|
|
680
|
+
case "RSASSA-PKCS1-v1_5":
|
|
681
|
+
algorithm = "RSA-PKCS1-v1_5";
|
|
682
|
+
hash = hashFromWebCrypto(keyAlgorithm.hash.name);
|
|
683
|
+
break;
|
|
684
|
+
case "RSA-PSS":
|
|
685
|
+
algorithm = "RSA-PSS";
|
|
686
|
+
hash = hashFromWebCrypto(keyAlgorithm.hash.name);
|
|
687
|
+
break;
|
|
688
|
+
case "ECDSA":
|
|
689
|
+
algorithm = "ECDSA";
|
|
690
|
+
hash = input.hash ?? "sha256";
|
|
691
|
+
break;
|
|
692
|
+
default: throw new Error(`unsupported WebCrypto key algorithm ${keyAlgorithm.name}`);
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
kind: "raw",
|
|
696
|
+
certificateChain: input.certificateChain,
|
|
697
|
+
algorithm,
|
|
698
|
+
hash,
|
|
699
|
+
async sign(data) {
|
|
700
|
+
const params = algorithm === "RSA-PSS" ? {
|
|
701
|
+
name: "RSA-PSS",
|
|
702
|
+
saltLength: {
|
|
703
|
+
sha256: 32,
|
|
704
|
+
sha384: 48,
|
|
705
|
+
sha512: 64
|
|
706
|
+
}[hash]
|
|
707
|
+
} : algorithm === "ECDSA" ? {
|
|
708
|
+
name: "ECDSA",
|
|
709
|
+
hash: WEBCRYPTO_HASH[hash]
|
|
710
|
+
} : { name: "RSASSA-PKCS1-v1_5" };
|
|
711
|
+
const copy = new Uint8Array(data.byteLength);
|
|
712
|
+
copy.set(data);
|
|
713
|
+
const signature = await globalThis.crypto.subtle.sign(params, input.privateKey, copy);
|
|
714
|
+
return new Uint8Array(signature);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function hashFromWebCrypto(name) {
|
|
719
|
+
switch (name) {
|
|
720
|
+
case "SHA-256": return "sha256";
|
|
721
|
+
case "SHA-384": return "sha384";
|
|
722
|
+
case "SHA-512": return "sha512";
|
|
723
|
+
default: throw new Error(`unsupported key hash ${name}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* A throwaway signer for tests and demos: a fresh key pair and a
|
|
728
|
+
* self-signed certificate, never persisted. Trust it with
|
|
729
|
+
* `{ anchors: async () => [signer.certificate] }`.
|
|
730
|
+
*/
|
|
731
|
+
async function createTestSigner(opts = {}) {
|
|
732
|
+
ensureEngine();
|
|
733
|
+
const hash = opts.hash ?? "sha256";
|
|
734
|
+
const keys = await generateSigningKeyPair(opts.algorithm ?? "RSA-PKCS1-v1_5", hash, true);
|
|
735
|
+
const certificate = await selfSignedCertificate({
|
|
736
|
+
publicKey: keys.publicKey,
|
|
737
|
+
privateKey: keys.privateKey,
|
|
738
|
+
commonName: opts.commonName ?? "EmbedPDF test signer",
|
|
739
|
+
hash
|
|
740
|
+
});
|
|
741
|
+
return {
|
|
742
|
+
...webCryptoSigner({
|
|
743
|
+
privateKey: keys.privateKey,
|
|
744
|
+
certificateChain: [certificate],
|
|
745
|
+
hash
|
|
746
|
+
}),
|
|
747
|
+
certificate,
|
|
748
|
+
privateKey: keys.privateKey
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/** A WebCrypto signing key pair; `extractable: false` keeps the private key inside the runtime for good. */
|
|
752
|
+
async function generateSigningKeyPair(algorithm, hash, extractable) {
|
|
753
|
+
return await globalThis.crypto.subtle.generateKey(algorithm === "ECDSA" ? {
|
|
754
|
+
name: "ECDSA",
|
|
755
|
+
namedCurve: "P-256"
|
|
756
|
+
} : {
|
|
757
|
+
name: algorithm === "RSA-PSS" ? "RSA-PSS" : "RSASSA-PKCS1-v1_5",
|
|
758
|
+
modulusLength: 2048,
|
|
759
|
+
publicExponent: new Uint8Array([
|
|
760
|
+
1,
|
|
761
|
+
0,
|
|
762
|
+
1
|
|
763
|
+
]),
|
|
764
|
+
hash: WEBCRYPTO_HASH[hash]
|
|
765
|
+
}, extractable, ["sign", "verify"]);
|
|
766
|
+
}
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region src/signers/remote.ts
|
|
769
|
+
/**
|
|
770
|
+
* A `CmsSigner` over the customer's own signing service or HSM: the engine's
|
|
771
|
+
* digest goes out, a finished detached CMS comes back. Nothing but the
|
|
772
|
+
* digest (and which hash and profile it is for) ever leaves the runtime.
|
|
773
|
+
*/
|
|
774
|
+
function remoteSigner(input) {
|
|
775
|
+
return {
|
|
776
|
+
kind: "cms",
|
|
777
|
+
sign: (request) => input.sign(request)
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
//#endregion
|
|
781
|
+
//#region src/signers/personal.ts
|
|
782
|
+
/**
|
|
783
|
+
* One self-signed identity per subject (a person's name), persisted in the
|
|
784
|
+
* caller's store and created on first use. The private key is generated
|
|
785
|
+
* non-extractable: it can sign, it can be stored by the browser, it can
|
|
786
|
+
* never be read out. This is Preview's "sign with your own signature"
|
|
787
|
+
* experience with real cryptography behind it — a reader who trusts the
|
|
788
|
+
* certificate validates the signature; every other reader sees "validity
|
|
789
|
+
* unknown", which is the honest answer for a self-issued identity.
|
|
790
|
+
*/
|
|
791
|
+
async function personalSigner(input) {
|
|
792
|
+
ensureEngine();
|
|
793
|
+
const hash = input.hash ?? "sha256";
|
|
794
|
+
let record = await input.store.load(input.subject);
|
|
795
|
+
if (!record) {
|
|
796
|
+
const keys = await generateSigningKeyPair(input.algorithm ?? "RSA-PKCS1-v1_5", hash, false);
|
|
797
|
+
const certificate = await selfSignedCertificate({
|
|
798
|
+
publicKey: keys.publicKey,
|
|
799
|
+
privateKey: keys.privateKey,
|
|
800
|
+
commonName: input.subject,
|
|
801
|
+
hash
|
|
802
|
+
});
|
|
803
|
+
record = {
|
|
804
|
+
privateKey: keys.privateKey,
|
|
805
|
+
certificate
|
|
806
|
+
};
|
|
807
|
+
await input.store.save(input.subject, record);
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
...webCryptoSigner({
|
|
811
|
+
privateKey: record.privateKey,
|
|
812
|
+
certificateChain: [record.certificate],
|
|
813
|
+
hash
|
|
814
|
+
}),
|
|
815
|
+
certificate: record.certificate,
|
|
816
|
+
subject: input.subject
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
/** An in-memory store: tests, and runtimes without IndexedDB. */
|
|
820
|
+
function memoryKeyStore() {
|
|
821
|
+
const records = /* @__PURE__ */ new Map();
|
|
822
|
+
return {
|
|
823
|
+
load: async (subject) => records.get(subject) ?? null,
|
|
824
|
+
save: async (subject, record) => {
|
|
825
|
+
records.set(subject, record);
|
|
826
|
+
},
|
|
827
|
+
remove: async (subject) => {
|
|
828
|
+
records.delete(subject);
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* An IndexedDB store: the browser keeps the non-extractable `CryptoKey`
|
|
834
|
+
* itself (structured clone), so the key survives reloads without ever
|
|
835
|
+
* existing as bytes.
|
|
836
|
+
*/
|
|
837
|
+
function indexedDbKeyStore(dbName, opts = {}) {
|
|
838
|
+
const storeName = opts.storeName ?? "keys";
|
|
839
|
+
let opening = null;
|
|
840
|
+
const open = () => {
|
|
841
|
+
opening ?? (opening = new Promise((resolve, reject) => {
|
|
842
|
+
const request = indexedDB.open(dbName, 1);
|
|
843
|
+
request.onupgradeneeded = () => {
|
|
844
|
+
if (!request.result.objectStoreNames.contains(storeName)) request.result.createObjectStore(storeName);
|
|
845
|
+
};
|
|
846
|
+
request.onsuccess = () => resolve(request.result);
|
|
847
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexedDB open failed"));
|
|
848
|
+
}));
|
|
849
|
+
return opening;
|
|
850
|
+
};
|
|
851
|
+
const run = async (mode, op) => {
|
|
852
|
+
const db = await open();
|
|
853
|
+
return new Promise((resolve, reject) => {
|
|
854
|
+
const request = op(db.transaction(storeName, mode).objectStore(storeName));
|
|
855
|
+
request.onsuccess = () => resolve(request.result);
|
|
856
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("indexedDB request failed"));
|
|
857
|
+
});
|
|
858
|
+
};
|
|
859
|
+
return {
|
|
860
|
+
load: async (subject) => await run("readonly", (s) => s.get(subject)) ?? null,
|
|
861
|
+
save: (subject, record) => run("readwrite", (s) => s.put(record, subject)).then(() => void 0),
|
|
862
|
+
remove: (subject) => run("readwrite", (s) => s.delete(subject)).then(() => void 0)
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
//#endregion
|
|
866
|
+
exports.CmsError = CmsError;
|
|
867
|
+
exports.SigningError = SigningError;
|
|
868
|
+
exports.buildDetachedCms = buildDetachedCms;
|
|
869
|
+
exports.certificateCommonName = certificateCommonName;
|
|
870
|
+
exports.createTestSigner = createTestSigner;
|
|
871
|
+
exports.generateSigningKeyPair = generateSigningKeyPair;
|
|
872
|
+
exports.indexedDbKeyStore = indexedDbKeyStore;
|
|
873
|
+
exports.memoryKeyStore = memoryKeyStore;
|
|
874
|
+
exports.parseDetachedCms = parseDetachedCms;
|
|
875
|
+
exports.personalSigner = personalSigner;
|
|
876
|
+
exports.profileFor = profileFor;
|
|
877
|
+
exports.remoteSigner = remoteSigner;
|
|
878
|
+
exports.selfSignedCertificate = selfSignedCertificate;
|
|
879
|
+
exports.sign = sign;
|
|
880
|
+
exports.validateChain = validateChain;
|
|
881
|
+
exports.validateSignatures = validateSignatures;
|
|
882
|
+
exports.verifyCmsSignature = verifyCmsSignature;
|
|
883
|
+
exports.verifyForCompletion = verifyForCompletion;
|
|
884
|
+
exports.webCryptoSigner = webCryptoSigner;
|
|
885
|
+
|
|
886
|
+
//# sourceMappingURL=index.cjs.map
|