@owf/x509 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -2
- package/dist/index.cjs +1169 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +600 -0
- package/dist/index.d.mts +600 -0
- package/dist/index.mjs +1115 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1169 @@
|
|
|
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 || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let _peculiar_asn1_schema = require("@peculiar/asn1-schema");
|
|
25
|
+
let _peculiar_asn1_x509 = require("@peculiar/asn1-x509");
|
|
26
|
+
let _peculiar_asn1_x509_qualified = require("@peculiar/asn1-x509-qualified");
|
|
27
|
+
let _peculiar_asn1_x509_qualified_etsi = require("@peculiar/asn1-x509-qualified-etsi");
|
|
28
|
+
let _owf_identity_common = require("@owf/identity-common");
|
|
29
|
+
let asn1js = require("asn1js");
|
|
30
|
+
asn1js = __toESM(asn1js);
|
|
31
|
+
//#region src/constants.ts
|
|
32
|
+
/** X.520 attribute types and PKCS#9 emailAddress, as used in distinguished names */
|
|
33
|
+
const ATTRIBUTE_TYPES = {
|
|
34
|
+
commonName: "2.5.4.3",
|
|
35
|
+
surname: "2.5.4.4",
|
|
36
|
+
serialNumber: "2.5.4.5",
|
|
37
|
+
countryName: "2.5.4.6",
|
|
38
|
+
localityName: "2.5.4.7",
|
|
39
|
+
stateOrProvinceName: "2.5.4.8",
|
|
40
|
+
streetAddress: "2.5.4.9",
|
|
41
|
+
organizationName: "2.5.4.10",
|
|
42
|
+
organizationalUnitName: "2.5.4.11",
|
|
43
|
+
title: "2.5.4.12",
|
|
44
|
+
postalCode: "2.5.4.17",
|
|
45
|
+
telephoneNumber: "2.5.4.20",
|
|
46
|
+
givenName: "2.5.4.42",
|
|
47
|
+
initials: "2.5.4.43",
|
|
48
|
+
generationQualifier: "2.5.4.44",
|
|
49
|
+
pseudonym: "2.5.4.65",
|
|
50
|
+
organizationIdentifier: "2.5.4.97",
|
|
51
|
+
emailAddress: "1.2.840.113549.1.9.1"
|
|
52
|
+
};
|
|
53
|
+
/** Policy qualifier identifiers (RFC 5280 clause 4.2.1.4) */
|
|
54
|
+
const POLICY_QUALIFIERS = {
|
|
55
|
+
cps: "1.3.6.1.5.5.7.2.1",
|
|
56
|
+
unotice: "1.3.6.1.5.5.7.2.2"
|
|
57
|
+
};
|
|
58
|
+
/** Access methods for the Authority Information Access extension (RFC 5280 clause 4.2.2.1) */
|
|
59
|
+
const ACCESS_METHODS = {
|
|
60
|
+
ocsp: "1.3.6.1.5.5.7.48.1",
|
|
61
|
+
caIssuers: "1.3.6.1.5.5.7.48.2"
|
|
62
|
+
};
|
|
63
|
+
/** QCStatements extension (RFC 3739) and statements from ETSI EN 319 412-5 */
|
|
64
|
+
const QC_STATEMENTS = {
|
|
65
|
+
extension: _peculiar_asn1_x509_qualified.id_pe_qcStatements,
|
|
66
|
+
qcCompliance: _peculiar_asn1_x509_qualified_etsi.id_etsi_qcs_qcCompliance,
|
|
67
|
+
qcSSCD: _peculiar_asn1_x509_qualified_etsi.id_etsi_qcs_qcSSCD,
|
|
68
|
+
qcPDS: _peculiar_asn1_x509_qualified_etsi.id_etsi_qcs_qcPDS,
|
|
69
|
+
qcType: _peculiar_asn1_x509_qualified_etsi.id_etsi_qcs_qcType,
|
|
70
|
+
qcCClegislation: _peculiar_asn1_x509_qualified_etsi.id_etsi_qcs_qcCClegislation
|
|
71
|
+
};
|
|
72
|
+
/** QcType values (ETSI EN 319 412-5 clause 4.2.3) */
|
|
73
|
+
const QC_TYPES = {
|
|
74
|
+
esign: _peculiar_asn1_x509_qualified_etsi.id_etsi_qct_esign,
|
|
75
|
+
eseal: _peculiar_asn1_x509_qualified_etsi.id_etsi_qct_eseal,
|
|
76
|
+
web: _peculiar_asn1_x509_qualified_etsi.id_etsi_qct_web
|
|
77
|
+
};
|
|
78
|
+
/** Signature algorithms supported for creating and describing certificates and CRLs */
|
|
79
|
+
const SIGNATURE_ALGORITHMS = {
|
|
80
|
+
ES256: {
|
|
81
|
+
oid: "1.2.840.10045.4.3.2",
|
|
82
|
+
hash: "SHA-256",
|
|
83
|
+
family: "ecdsa"
|
|
84
|
+
},
|
|
85
|
+
ES384: {
|
|
86
|
+
oid: "1.2.840.10045.4.3.3",
|
|
87
|
+
hash: "SHA-384",
|
|
88
|
+
family: "ecdsa"
|
|
89
|
+
},
|
|
90
|
+
ES512: {
|
|
91
|
+
oid: "1.2.840.10045.4.3.4",
|
|
92
|
+
hash: "SHA-512",
|
|
93
|
+
family: "ecdsa"
|
|
94
|
+
},
|
|
95
|
+
RS256: {
|
|
96
|
+
oid: "1.2.840.113549.1.1.11",
|
|
97
|
+
hash: "SHA-256",
|
|
98
|
+
family: "rsa"
|
|
99
|
+
},
|
|
100
|
+
RS384: {
|
|
101
|
+
oid: "1.2.840.113549.1.1.12",
|
|
102
|
+
hash: "SHA-384",
|
|
103
|
+
family: "rsa"
|
|
104
|
+
},
|
|
105
|
+
RS512: {
|
|
106
|
+
oid: "1.2.840.113549.1.1.13",
|
|
107
|
+
hash: "SHA-512",
|
|
108
|
+
family: "rsa"
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/** Public key algorithm and curve identifiers */
|
|
112
|
+
const KEY_ALGORITHMS = {
|
|
113
|
+
ecPublicKey: "1.2.840.10045.2.1",
|
|
114
|
+
rsaEncryption: "1.2.840.113549.1.1.1",
|
|
115
|
+
"P-256": "1.2.840.10045.3.1.7",
|
|
116
|
+
"P-384": "1.3.132.0.34",
|
|
117
|
+
"P-521": "1.3.132.0.35"
|
|
118
|
+
};
|
|
119
|
+
/** CRL entry revocation reasons (RFC 5280 clause 5.3.1) */
|
|
120
|
+
const CRL_REASONS = {
|
|
121
|
+
unspecified: 0,
|
|
122
|
+
keyCompromise: 1,
|
|
123
|
+
cACompromise: 2,
|
|
124
|
+
affiliationChanged: 3,
|
|
125
|
+
superseded: 4,
|
|
126
|
+
cessationOfOperation: 5,
|
|
127
|
+
certificateHold: 6,
|
|
128
|
+
removeFromCRL: 8,
|
|
129
|
+
privilegeWithdrawn: 9,
|
|
130
|
+
aACompromise: 10
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/x509-exception.ts
|
|
134
|
+
/**
|
|
135
|
+
* X509Exception is thrown when a certificate or CRL cannot be parsed or created.
|
|
136
|
+
*/
|
|
137
|
+
var X509Exception = class X509Exception extends _owf_identity_common.IdentityException {
|
|
138
|
+
constructor(message, details) {
|
|
139
|
+
super(message, details);
|
|
140
|
+
Object.setPrototypeOf(this, X509Exception.prototype);
|
|
141
|
+
this.name = "X509Exception";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/encoding.ts
|
|
146
|
+
const PEM_LABELS = {
|
|
147
|
+
certificate: "CERTIFICATE",
|
|
148
|
+
crl: "X509 CRL"
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Normalize a {@link DerInput} to DER bytes. PEM strings must use the given label; the
|
|
152
|
+
* first matching block is used.
|
|
153
|
+
*/
|
|
154
|
+
function toDer(input, label = PEM_LABELS.certificate) {
|
|
155
|
+
if (input instanceof Uint8Array) return input;
|
|
156
|
+
if (input instanceof ArrayBuffer) return new Uint8Array(input);
|
|
157
|
+
if (typeof input !== "string") throw new X509Exception("Input must be DER bytes, a base64 DER string, or PEM");
|
|
158
|
+
let body = input;
|
|
159
|
+
if (input.includes("-----BEGIN")) {
|
|
160
|
+
const pem = input.match(new RegExp(`-----BEGIN ${label}-----([\\s\\S]*?)-----END ${label}-----`));
|
|
161
|
+
if (!pem) throw new X509Exception(`No PEM block with label "${label}" found`);
|
|
162
|
+
body = pem[1];
|
|
163
|
+
}
|
|
164
|
+
body = body.replace(/\s/g, "");
|
|
165
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(body)) throw new X509Exception("Input is neither PEM nor base64 encoded DER");
|
|
166
|
+
return _owf_identity_common.base64.decode(body);
|
|
167
|
+
}
|
|
168
|
+
/** Encode DER bytes as PEM. */
|
|
169
|
+
function toPem(der, label = PEM_LABELS.certificate) {
|
|
170
|
+
return `-----BEGIN ${label}-----\n${(_owf_identity_common.base64.encode(der).match(/.{1,64}/g) ?? []).join("\n")}\n-----END ${label}-----\n`;
|
|
171
|
+
}
|
|
172
|
+
/** Copy bytes into a standalone ArrayBuffer (asn1-schema works on ArrayBuffers). */
|
|
173
|
+
function toArrayBuffer(bytes) {
|
|
174
|
+
return bytes.slice().buffer;
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/der.ts
|
|
178
|
+
const CURVES = {
|
|
179
|
+
"P-256": {
|
|
180
|
+
oid: KEY_ALGORITHMS["P-256"],
|
|
181
|
+
size: 32
|
|
182
|
+
},
|
|
183
|
+
"P-384": {
|
|
184
|
+
oid: KEY_ALGORITHMS["P-384"],
|
|
185
|
+
size: 48
|
|
186
|
+
},
|
|
187
|
+
"P-521": {
|
|
188
|
+
oid: KEY_ALGORITHMS["P-521"],
|
|
189
|
+
size: 66
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
/** Byte length of a coordinate (and of r and s in an ECDSA signature) for a named curve OID */
|
|
193
|
+
function curveCoordinateLength(namedCurve) {
|
|
194
|
+
return Object.values(CURVES).find((c) => c.oid === namedCurve)?.size;
|
|
195
|
+
}
|
|
196
|
+
/** Parse a single DER value, rejecting trailing or malformed data. */
|
|
197
|
+
function parseDer(bytes) {
|
|
198
|
+
const buffer = bytes instanceof Uint8Array ? toArrayBuffer(bytes) : bytes;
|
|
199
|
+
const result = asn1js.fromBER(buffer);
|
|
200
|
+
if (result.offset === -1 || result.offset !== buffer.byteLength) throw new X509Exception("Invalid DER encoding");
|
|
201
|
+
return result.result;
|
|
202
|
+
}
|
|
203
|
+
/** Decode an ASN.1 string type (or unwrap an explicit tag around one) to a JS string. */
|
|
204
|
+
function asn1ToString(node) {
|
|
205
|
+
if (node instanceof asn1js.BaseStringBlock) return node.getValue();
|
|
206
|
+
if (node instanceof asn1js.Constructed && node.valueBlock.value.length === 1) return asn1ToString(node.valueBlock.value[0]);
|
|
207
|
+
}
|
|
208
|
+
/** Strip leading zeros from an unsigned big endian integer. */
|
|
209
|
+
function stripLeadingZeros(bytes) {
|
|
210
|
+
let i = 0;
|
|
211
|
+
while (i < bytes.length - 1 && bytes[i] === 0) i++;
|
|
212
|
+
return bytes.subarray(i);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Encode unsigned big endian bytes as the content of a DER INTEGER, adding a leading
|
|
216
|
+
* zero when the high bit is set so the value stays positive.
|
|
217
|
+
*/
|
|
218
|
+
function toPositiveIntegerBytes(bytes) {
|
|
219
|
+
const stripped = stripLeadingZeros(bytes);
|
|
220
|
+
return stripped[0] & 128 ? (0, _owf_identity_common.concatBytes)(new Uint8Array([0]), stripped) : stripped;
|
|
221
|
+
}
|
|
222
|
+
function integer(bytes) {
|
|
223
|
+
return new asn1js.Integer({ valueHex: toArrayBuffer(toPositiveIntegerBytes(bytes)) });
|
|
224
|
+
}
|
|
225
|
+
function leftPad(bytes, length) {
|
|
226
|
+
const stripped = stripLeadingZeros(bytes);
|
|
227
|
+
if (stripped.length > length) throw new X509Exception("Integer does not fit the expected length");
|
|
228
|
+
const out = new Uint8Array(length);
|
|
229
|
+
out.set(stripped, length - stripped.length);
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
/** Convert an IEEE P1363 (r || s) ECDSA signature to DER. */
|
|
233
|
+
function ecdsaP1363ToDer(signature, coordinateLength) {
|
|
234
|
+
if (signature.length !== coordinateLength * 2) throw new X509Exception(`Expected a ${coordinateLength * 2} byte IEEE P1363 ECDSA signature, got ${signature.length} bytes`);
|
|
235
|
+
const seq = new asn1js.Sequence({ value: [integer(signature.subarray(0, coordinateLength)), integer(signature.subarray(coordinateLength))] });
|
|
236
|
+
return new Uint8Array(seq.toBER());
|
|
237
|
+
}
|
|
238
|
+
/** Convert a DER ECDSA signature to IEEE P1363 (r || s). */
|
|
239
|
+
function ecdsaDerToP1363(signature, coordinateLength) {
|
|
240
|
+
const seq = parseDer(signature);
|
|
241
|
+
if (!(seq instanceof asn1js.Sequence) || seq.valueBlock.value.length !== 2) throw new X509Exception("Invalid DER ECDSA signature");
|
|
242
|
+
const [r, s] = seq.valueBlock.value;
|
|
243
|
+
if (!(r instanceof asn1js.Integer) || !(s instanceof asn1js.Integer)) throw new X509Exception("Invalid DER ECDSA signature");
|
|
244
|
+
return (0, _owf_identity_common.concatBytes)(leftPad(new Uint8Array(r.valueBlock.valueHexView), coordinateLength), leftPad(new Uint8Array(s.valueBlock.valueHexView), coordinateLength));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Convert a public JWK to a DER SubjectPublicKeyInfo. Refuses private keys: only the
|
|
248
|
+
* public part of the wallet-relying party key is needed to issue its certificate.
|
|
249
|
+
*/
|
|
250
|
+
function publicJwkToSpki(jwk) {
|
|
251
|
+
if (jwk.d !== void 0 || jwk.p !== void 0 || jwk.q !== void 0) throw new X509Exception("The subject public key must not contain private key material");
|
|
252
|
+
if (jwk.kty === "EC") {
|
|
253
|
+
const curve = CURVES[jwk.crv];
|
|
254
|
+
if (!curve || !jwk.x || !jwk.y) throw new X509Exception(`Unsupported EC public key (crv: ${jwk.crv})`);
|
|
255
|
+
const point = (0, _owf_identity_common.concatBytes)(new Uint8Array([4]), leftPad(_owf_identity_common.base64url.decode(jwk.x), curve.size), leftPad(_owf_identity_common.base64url.decode(jwk.y), curve.size));
|
|
256
|
+
const spki = new _peculiar_asn1_x509.SubjectPublicKeyInfo({
|
|
257
|
+
algorithm: new _peculiar_asn1_x509.AlgorithmIdentifier({
|
|
258
|
+
algorithm: KEY_ALGORITHMS.ecPublicKey,
|
|
259
|
+
parameters: new asn1js.ObjectIdentifier({ value: curve.oid }).toBER()
|
|
260
|
+
}),
|
|
261
|
+
subjectPublicKey: toArrayBuffer(point)
|
|
262
|
+
});
|
|
263
|
+
return new Uint8Array(_peculiar_asn1_schema.AsnConvert.serialize(spki));
|
|
264
|
+
}
|
|
265
|
+
if (jwk.kty === "RSA") {
|
|
266
|
+
if (!jwk.n || !jwk.e) throw new X509Exception("RSA public key requires n and e");
|
|
267
|
+
const rsaPublicKey = new asn1js.Sequence({ value: [integer(_owf_identity_common.base64url.decode(jwk.n)), integer(_owf_identity_common.base64url.decode(jwk.e))] });
|
|
268
|
+
const spki = new _peculiar_asn1_x509.SubjectPublicKeyInfo({
|
|
269
|
+
algorithm: new _peculiar_asn1_x509.AlgorithmIdentifier({
|
|
270
|
+
algorithm: KEY_ALGORITHMS.rsaEncryption,
|
|
271
|
+
parameters: null
|
|
272
|
+
}),
|
|
273
|
+
subjectPublicKey: rsaPublicKey.toBER()
|
|
274
|
+
});
|
|
275
|
+
return new Uint8Array(_peculiar_asn1_schema.AsnConvert.serialize(spki));
|
|
276
|
+
}
|
|
277
|
+
throw new X509Exception(`Unsupported key type: ${jwk.kty}`);
|
|
278
|
+
}
|
|
279
|
+
/** Describe a SubjectPublicKeyInfo, including a JWK for EC and RSA keys. */
|
|
280
|
+
function describePublicKey(spki) {
|
|
281
|
+
const der = new Uint8Array(_peculiar_asn1_schema.AsnConvert.serialize(spki));
|
|
282
|
+
const algorithm = spki.algorithm.algorithm;
|
|
283
|
+
const key = new Uint8Array(spki.subjectPublicKey);
|
|
284
|
+
if (algorithm === KEY_ALGORITHMS.ecPublicKey && spki.algorithm.parameters) {
|
|
285
|
+
const namedCurveNode = parseDer(spki.algorithm.parameters);
|
|
286
|
+
const namedCurve = namedCurveNode instanceof asn1js.ObjectIdentifier ? namedCurveNode.valueBlock.toString() : void 0;
|
|
287
|
+
const entry = Object.entries(CURVES).find(([, c]) => c.oid === namedCurve);
|
|
288
|
+
let jwk;
|
|
289
|
+
if (entry && key[0] === 4 && key.length === 1 + entry[1].size * 2) {
|
|
290
|
+
const size = entry[1].size;
|
|
291
|
+
jwk = {
|
|
292
|
+
kty: "EC",
|
|
293
|
+
crv: entry[0],
|
|
294
|
+
x: _owf_identity_common.base64url.encode(key.subarray(1, 1 + size)),
|
|
295
|
+
y: _owf_identity_common.base64url.encode(key.subarray(1 + size))
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
algorithm,
|
|
300
|
+
namedCurve,
|
|
301
|
+
spki: der,
|
|
302
|
+
jwk
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (algorithm === KEY_ALGORITHMS.rsaEncryption) {
|
|
306
|
+
let jwk;
|
|
307
|
+
try {
|
|
308
|
+
const seq = parseDer(key);
|
|
309
|
+
if (seq instanceof asn1js.Sequence) {
|
|
310
|
+
const [n, e] = seq.valueBlock.value;
|
|
311
|
+
if (n instanceof asn1js.Integer && e instanceof asn1js.Integer) jwk = {
|
|
312
|
+
kty: "RSA",
|
|
313
|
+
n: _owf_identity_common.base64url.encode(stripLeadingZeros(new Uint8Array(n.valueBlock.valueHexView))),
|
|
314
|
+
e: _owf_identity_common.base64url.encode(stripLeadingZeros(new Uint8Array(e.valueBlock.valueHexView)))
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
} catch {}
|
|
318
|
+
return {
|
|
319
|
+
algorithm,
|
|
320
|
+
spki: der,
|
|
321
|
+
jwk
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
algorithm,
|
|
326
|
+
spki: der
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function toHex(bytes) {
|
|
330
|
+
return (0, _owf_identity_common.hexEncode)(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
|
|
331
|
+
}
|
|
332
|
+
function derLength(length) {
|
|
333
|
+
if (length < 128) return new Uint8Array([length]);
|
|
334
|
+
const bytes = [];
|
|
335
|
+
for (let l = length; l > 0; l >>= 8) bytes.unshift(l & 255);
|
|
336
|
+
return new Uint8Array([128 | bytes.length, ...bytes]);
|
|
337
|
+
}
|
|
338
|
+
/** Encode a single DER TLV. */
|
|
339
|
+
function derTlv(tag, content) {
|
|
340
|
+
return (0, _owf_identity_common.concatBytes)(new Uint8Array([tag]), derLength(content.length), content);
|
|
341
|
+
}
|
|
342
|
+
/** Interpret big endian two's complement INTEGER content as a bigint (non-negative values only). */
|
|
343
|
+
function integerBytesToBigInt(bytes) {
|
|
344
|
+
return bytes.length === 0 ? BigInt(0) : BigInt(`0x${(0, _owf_identity_common.hexEncode)(bytes)}`);
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/name.ts
|
|
348
|
+
const NAME_BY_OID = new Map(Object.entries(ATTRIBUTE_TYPES).map(([name, oid]) => [oid, name]));
|
|
349
|
+
/** Short names for the RFC 4514 representation (RFC 4514 clause 3 plus common extensions) */
|
|
350
|
+
const RFC4514_SHORT_NAMES = {
|
|
351
|
+
commonName: "CN",
|
|
352
|
+
surname: "SN",
|
|
353
|
+
serialNumber: "SERIALNUMBER",
|
|
354
|
+
countryName: "C",
|
|
355
|
+
localityName: "L",
|
|
356
|
+
stateOrProvinceName: "ST",
|
|
357
|
+
streetAddress: "STREET",
|
|
358
|
+
organizationName: "O",
|
|
359
|
+
organizationalUnitName: "OU",
|
|
360
|
+
title: "T",
|
|
361
|
+
givenName: "GN",
|
|
362
|
+
organizationIdentifier: "organizationIdentifier",
|
|
363
|
+
emailAddress: "E"
|
|
364
|
+
};
|
|
365
|
+
function attributeValueToString(value) {
|
|
366
|
+
if (value.anyValue) return `#${toHex(value.anyValue)}`;
|
|
367
|
+
return value.toString();
|
|
368
|
+
}
|
|
369
|
+
function escapeRfc4514(value) {
|
|
370
|
+
return value.replace(/[\\"+,;<>=]/g, (c) => `\\${c}`).replace(/^[ #]/, (c) => `\\${c}`).replace(/ $/, "\\ ");
|
|
371
|
+
}
|
|
372
|
+
/** Convert an ASN.1 Name into a {@link DistinguishedName}. */
|
|
373
|
+
function parseName(name) {
|
|
374
|
+
const attributes = [];
|
|
375
|
+
const rdnStrings = [];
|
|
376
|
+
for (const rdn of name) {
|
|
377
|
+
const parts = [];
|
|
378
|
+
for (const atv of rdn) {
|
|
379
|
+
const attrName = NAME_BY_OID.get(atv.type);
|
|
380
|
+
const value = attributeValueToString(atv.value);
|
|
381
|
+
attributes.push({
|
|
382
|
+
type: atv.type,
|
|
383
|
+
name: attrName,
|
|
384
|
+
value
|
|
385
|
+
});
|
|
386
|
+
const key = (attrName && RFC4514_SHORT_NAMES[attrName]) ?? atv.type;
|
|
387
|
+
parts.push(`${key}=${atv.value.anyValue ? value : escapeRfc4514(value)}`);
|
|
388
|
+
}
|
|
389
|
+
rdnStrings.push(parts.join("+"));
|
|
390
|
+
}
|
|
391
|
+
const first = (n) => attributes.find((a) => a.name === n)?.value;
|
|
392
|
+
return {
|
|
393
|
+
attributes,
|
|
394
|
+
rfc4514: rdnStrings.reverse().join(","),
|
|
395
|
+
commonName: first("commonName"),
|
|
396
|
+
organizationName: first("organizationName"),
|
|
397
|
+
organizationIdentifier: first("organizationIdentifier"),
|
|
398
|
+
organizationalUnitNames: attributes.filter((a) => a.name === "organizationalUnitName").map((a) => a.value),
|
|
399
|
+
countryName: first("countryName"),
|
|
400
|
+
localityName: first("localityName"),
|
|
401
|
+
stateOrProvinceName: first("stateOrProvinceName"),
|
|
402
|
+
serialNumber: first("serialNumber"),
|
|
403
|
+
givenName: first("givenName"),
|
|
404
|
+
surname: first("surname"),
|
|
405
|
+
pseudonym: first("pseudonym")
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/** Attributes that X.520 restricts to PrintableString */
|
|
409
|
+
const PRINTABLE_ATTRIBUTES = /* @__PURE__ */ new Set(["countryName", "serialNumber"]);
|
|
410
|
+
/**
|
|
411
|
+
* Build an ASN.1 Name from attributes in encoding order (most general first), one attribute
|
|
412
|
+
* per RDN. Empty values are skipped. `countryName` and `serialNumber` are encoded as
|
|
413
|
+
* PrintableString, `emailAddress` as IA5String and everything else as UTF8String.
|
|
414
|
+
*/
|
|
415
|
+
function buildName(attributes) {
|
|
416
|
+
const rdns = [];
|
|
417
|
+
for (const [nameOrOid, value] of attributes) {
|
|
418
|
+
if (value === void 0 || value === "") continue;
|
|
419
|
+
const name = nameOrOid in ATTRIBUTE_TYPES ? nameOrOid : NAME_BY_OID.get(nameOrOid);
|
|
420
|
+
if (!name && !/^\d+(\.\d+)+$/.test(nameOrOid)) throw new X509Exception(`Unknown name attribute: ${nameOrOid}`);
|
|
421
|
+
const type = name ? ATTRIBUTE_TYPES[name] : nameOrOid;
|
|
422
|
+
const attributeValue = name && PRINTABLE_ATTRIBUTES.has(name) ? new _peculiar_asn1_x509.AttributeValue({ printableString: value }) : name === "emailAddress" ? new _peculiar_asn1_x509.AttributeValue({ ia5String: value }) : new _peculiar_asn1_x509.AttributeValue({ utf8String: value });
|
|
423
|
+
rdns.push(new _peculiar_asn1_x509.RelativeDistinguishedName([new _peculiar_asn1_x509.AttributeTypeAndValue({
|
|
424
|
+
type,
|
|
425
|
+
value: attributeValue
|
|
426
|
+
})]));
|
|
427
|
+
}
|
|
428
|
+
return new _peculiar_asn1_x509.Name(rdns);
|
|
429
|
+
}
|
|
430
|
+
function normalize(value) {
|
|
431
|
+
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Compare two distinguished names attribute by attribute, ignoring case and insignificant
|
|
435
|
+
* whitespace in the values (a simplification of the RFC 5280 clause 7.1 matching rules).
|
|
436
|
+
*/
|
|
437
|
+
function namesEqual(a, b) {
|
|
438
|
+
return a.attributes.length === b.attributes.length && a.attributes.every((attr, i) => attr.type === b.attributes[i].type && normalize(attr.value) === normalize(b.attributes[i].value));
|
|
439
|
+
}
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/extensions.ts
|
|
442
|
+
/** Convert ASN.1 extensions, rejecting duplicates (RFC 5280 clause 4.2). */
|
|
443
|
+
function toExtensions(extensions) {
|
|
444
|
+
const result = (extensions ?? []).map((e) => ({
|
|
445
|
+
oid: e.extnID,
|
|
446
|
+
critical: e.critical,
|
|
447
|
+
value: new Uint8Array(e.extnValue.buffer)
|
|
448
|
+
}));
|
|
449
|
+
const seen = /* @__PURE__ */ new Set();
|
|
450
|
+
for (const { oid } of result) {
|
|
451
|
+
if (seen.has(oid)) throw new X509Exception(`Duplicate extension ${oid}`);
|
|
452
|
+
seen.add(oid);
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
/** Decode an extension value with an asn1-schema type, if the extension is present. */
|
|
457
|
+
function decodeExtension(extensions, oid, type, name) {
|
|
458
|
+
const extension = extensions.find((e) => e.oid === oid);
|
|
459
|
+
if (!extension) return void 0;
|
|
460
|
+
try {
|
|
461
|
+
return _peculiar_asn1_schema.AsnConvert.parse(toArrayBuffer(extension.value), type);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
throw new X509Exception(`Invalid ${name} extension`, error);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function uriOf(name) {
|
|
467
|
+
return name?.uniformResourceIdentifier;
|
|
468
|
+
}
|
|
469
|
+
function urisOf(names) {
|
|
470
|
+
return (names ?? []).map(uriOf).filter((u) => u !== void 0);
|
|
471
|
+
}
|
|
472
|
+
function emptySubjectAlternativeNames() {
|
|
473
|
+
return {
|
|
474
|
+
dnsNames: [],
|
|
475
|
+
uris: [],
|
|
476
|
+
emails: [],
|
|
477
|
+
ipAddresses: [],
|
|
478
|
+
directoryNames: [],
|
|
479
|
+
otherNames: []
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function toSubjectAlternativeNames(names) {
|
|
483
|
+
const result = emptySubjectAlternativeNames();
|
|
484
|
+
for (const name of names) if (name.uniformResourceIdentifier !== void 0) result.uris.push(name.uniformResourceIdentifier);
|
|
485
|
+
else if (name.rfc822Name !== void 0) result.emails.push(name.rfc822Name);
|
|
486
|
+
else if (name.dNSName !== void 0) result.dnsNames.push(name.dNSName);
|
|
487
|
+
else if (name.iPAddress !== void 0) result.ipAddresses.push(name.iPAddress);
|
|
488
|
+
else if (name.directoryName) result.directoryNames.push(parseName(name.directoryName));
|
|
489
|
+
else if (name.otherName) {
|
|
490
|
+
const value = new Uint8Array(name.otherName.value);
|
|
491
|
+
let text;
|
|
492
|
+
try {
|
|
493
|
+
text = asn1ToString(parseDer(value));
|
|
494
|
+
} catch {}
|
|
495
|
+
result.otherNames.push({
|
|
496
|
+
typeId: name.otherName.typeId,
|
|
497
|
+
value,
|
|
498
|
+
text
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return result;
|
|
502
|
+
}
|
|
503
|
+
/** Telephone numbers from otherName entries with type-id id-at-telephoneNumber */
|
|
504
|
+
function telephoneNumbers(names) {
|
|
505
|
+
return names.otherNames.filter((o) => o.typeId === ATTRIBUTE_TYPES.telephoneNumber && o.text !== void 0).map((o) => o.text);
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/qc-statements.ts
|
|
509
|
+
const QC_TYPE_BY_OID = new Map(Object.entries(QC_TYPES).map(([name, oid]) => [oid, name]));
|
|
510
|
+
/** Decode the statementInfo of a statement with its ETSI EN 319 412-5 schema. */
|
|
511
|
+
function statementInfo(statement, type) {
|
|
512
|
+
if (!statement.statementInfo) throw new X509Exception(`QCStatement ${statement.statementId} is missing its statementInfo`);
|
|
513
|
+
try {
|
|
514
|
+
return _peculiar_asn1_schema.AsnConvert.parse(statement.statementInfo, type);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
throw new X509Exception(`Invalid statementInfo for QCStatement ${statement.statementId}`, error);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/** Convert a decoded QCStatements extension (RFC 3739, ETSI EN 319 412-5). */
|
|
520
|
+
function parseQcStatements(statements) {
|
|
521
|
+
const result = {
|
|
522
|
+
statementIds: [],
|
|
523
|
+
qcCompliance: false,
|
|
524
|
+
qcSSCD: false,
|
|
525
|
+
qcTypes: [],
|
|
526
|
+
pds: [],
|
|
527
|
+
legislationCountries: []
|
|
528
|
+
};
|
|
529
|
+
for (const statement of statements) {
|
|
530
|
+
result.statementIds.push(statement.statementId);
|
|
531
|
+
switch (statement.statementId) {
|
|
532
|
+
case QC_STATEMENTS.qcCompliance:
|
|
533
|
+
result.qcCompliance = true;
|
|
534
|
+
break;
|
|
535
|
+
case QC_STATEMENTS.qcSSCD:
|
|
536
|
+
result.qcSSCD = true;
|
|
537
|
+
break;
|
|
538
|
+
case QC_STATEMENTS.qcType:
|
|
539
|
+
for (const oid of statementInfo(statement, _peculiar_asn1_x509_qualified_etsi.QcType)) result.qcTypes.push(QC_TYPE_BY_OID.get(oid) ?? oid);
|
|
540
|
+
break;
|
|
541
|
+
case QC_STATEMENTS.qcPDS:
|
|
542
|
+
for (const { url, language } of statementInfo(statement, _peculiar_asn1_x509_qualified_etsi.QcEuPDS)) result.pds.push({
|
|
543
|
+
url,
|
|
544
|
+
language
|
|
545
|
+
});
|
|
546
|
+
break;
|
|
547
|
+
case QC_STATEMENTS.qcCClegislation: result.legislationCountries.push(...statementInfo(statement, _peculiar_asn1_x509_qualified_etsi.QcCClegislation));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
function statement(statementId, info) {
|
|
553
|
+
return Object.assign(new _peculiar_asn1_x509_qualified.QCStatement(), {
|
|
554
|
+
statementId,
|
|
555
|
+
statementInfo: info === void 0 ? void 0 : _peculiar_asn1_schema.AsnConvert.serialize(info)
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
/** Build a QCStatements extension value with QcCompliance and the given statements. */
|
|
559
|
+
function toQcStatements(input) {
|
|
560
|
+
const statements = new _peculiar_asn1_x509_qualified.QCStatements([statement(QC_STATEMENTS.qcCompliance)]);
|
|
561
|
+
if (input.qcSSCD) statements.push(statement(QC_STATEMENTS.qcSSCD));
|
|
562
|
+
if (input.qcTypes.length > 0) statements.push(statement(QC_STATEMENTS.qcType, new _peculiar_asn1_x509_qualified_etsi.QcType(input.qcTypes.map((t) => QC_TYPES[t]))));
|
|
563
|
+
if (input.pds?.length) statements.push(statement(QC_STATEMENTS.qcPDS, new _peculiar_asn1_x509_qualified_etsi.QcEuPDS(input.pds.map((p) => new _peculiar_asn1_x509_qualified_etsi.PdsLocation(p)))));
|
|
564
|
+
if (input.legislationCountries?.length) statements.push(statement(QC_STATEMENTS.qcCClegislation, new _peculiar_asn1_x509_qualified_etsi.QcCClegislation(input.legislationCountries)));
|
|
565
|
+
return statements;
|
|
566
|
+
}
|
|
567
|
+
//#endregion
|
|
568
|
+
//#region src/certificate.ts
|
|
569
|
+
const SIGNATURE_ALGORITHM_BY_OID = new Map(Object.entries(SIGNATURE_ALGORITHMS).map(([name, { oid }]) => [oid, name]));
|
|
570
|
+
function signatureAlgorithmName(oid) {
|
|
571
|
+
return SIGNATURE_ALGORITHM_BY_OID.get(oid);
|
|
572
|
+
}
|
|
573
|
+
function toPolicies(policies) {
|
|
574
|
+
return (policies ?? []).map((policy) => {
|
|
575
|
+
const cpsUris = [];
|
|
576
|
+
const userNotices = [];
|
|
577
|
+
for (const qualifier of policy.policyQualifiers ?? []) if (qualifier.policyQualifierId === POLICY_QUALIFIERS.cps) {
|
|
578
|
+
const uri = asn1ToString(parseDer(qualifier.qualifier));
|
|
579
|
+
if (uri) cpsUris.push(uri);
|
|
580
|
+
} else if (qualifier.policyQualifierId === POLICY_QUALIFIERS.unotice) {
|
|
581
|
+
const notice = _peculiar_asn1_schema.AsnConvert.parse(qualifier.qualifier, _peculiar_asn1_x509.UserNotice);
|
|
582
|
+
if (notice.explicitText) userNotices.push(notice.explicitText.toString());
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
oid: policy.policyIdentifier,
|
|
586
|
+
cpsUris,
|
|
587
|
+
userNotices
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Parse an X.509 certificate.
|
|
593
|
+
*
|
|
594
|
+
* Decodes the standard extensions of RFC 5280 plus QCStatements. Parsing does not verify the
|
|
595
|
+
* signature; use {@link verifyCertificateSignature} for that.
|
|
596
|
+
*
|
|
597
|
+
* @param input DER bytes, base64 encoded DER, or PEM
|
|
598
|
+
* @param options Resource limits forwarded to asn1js (`berOptions`)
|
|
599
|
+
* @throws X509Exception when the input is not a well-formed X.509 certificate
|
|
600
|
+
*/
|
|
601
|
+
function parseCertificate(input, options) {
|
|
602
|
+
const der = toDer(input, PEM_LABELS.certificate);
|
|
603
|
+
let certificate;
|
|
604
|
+
try {
|
|
605
|
+
certificate = _peculiar_asn1_schema.AsnConvert.parse(toArrayBuffer(der), _peculiar_asn1_x509.Certificate, options);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
throw new X509Exception("Invalid X.509 certificate", error);
|
|
608
|
+
}
|
|
609
|
+
if (!certificate.tbsCertificateRaw) throw new X509Exception("Unable to extract the TBSCertificate");
|
|
610
|
+
const tbs = certificate.tbsCertificate;
|
|
611
|
+
const extensions = toExtensions(tbs.extensions);
|
|
612
|
+
const decode = (oid, type, name) => decodeExtension(extensions, oid, type, name);
|
|
613
|
+
const san = decode(_peculiar_asn1_x509.id_ce_subjectAltName, _peculiar_asn1_x509.SubjectAlternativeName, "subjectAltName");
|
|
614
|
+
const keyUsage = decode(_peculiar_asn1_x509.id_ce_keyUsage, _peculiar_asn1_x509.KeyUsage, "keyUsage");
|
|
615
|
+
const eku = decode(_peculiar_asn1_x509.id_ce_extKeyUsage, _peculiar_asn1_x509.ExtendedKeyUsage, "extKeyUsage");
|
|
616
|
+
const basicConstraints = decode(_peculiar_asn1_x509.id_ce_basicConstraints, _peculiar_asn1_x509.BasicConstraints, "basicConstraints");
|
|
617
|
+
const ski = decode(_peculiar_asn1_x509.id_ce_subjectKeyIdentifier, _peculiar_asn1_x509.SubjectKeyIdentifier, "subjectKeyIdentifier");
|
|
618
|
+
const aki = decode(_peculiar_asn1_x509.id_ce_authorityKeyIdentifier, _peculiar_asn1_x509.AuthorityKeyIdentifier, "authorityKeyIdentifier");
|
|
619
|
+
const crlDps = decode(_peculiar_asn1_x509.id_ce_cRLDistributionPoints, _peculiar_asn1_x509.CRLDistributionPoints, "cRLDistributionPoints");
|
|
620
|
+
const aia = decode(_peculiar_asn1_x509.id_pe_authorityInfoAccess, _peculiar_asn1_x509.AuthorityInfoAccessSyntax, "authorityInfoAccess") ?? [];
|
|
621
|
+
const qcStatements = decode(QC_STATEMENTS.extension, _peculiar_asn1_x509_qualified.QCStatements, "qcStatements");
|
|
622
|
+
const accessUrls = (method) => aia.filter((a) => a.accessMethod === method).map((a) => uriOf(a.accessLocation)).filter((u) => u !== void 0);
|
|
623
|
+
return {
|
|
624
|
+
der,
|
|
625
|
+
pem: toPem(der, PEM_LABELS.certificate),
|
|
626
|
+
version: tbs.version + 1,
|
|
627
|
+
serialNumber: toHex(tbs.serialNumber),
|
|
628
|
+
issuer: parseName(tbs.issuer),
|
|
629
|
+
subject: parseName(tbs.subject),
|
|
630
|
+
notBefore: tbs.validity.notBefore.getTime(),
|
|
631
|
+
notAfter: tbs.validity.notAfter.getTime(),
|
|
632
|
+
publicKey: describePublicKey(tbs.subjectPublicKeyInfo),
|
|
633
|
+
signatureAlgorithm: certificate.signatureAlgorithm.algorithm,
|
|
634
|
+
signatureAlgorithmName: signatureAlgorithmName(certificate.signatureAlgorithm.algorithm),
|
|
635
|
+
tbsCertificate: new Uint8Array(certificate.tbsCertificateRaw),
|
|
636
|
+
signature: new Uint8Array(certificate.signatureValue),
|
|
637
|
+
policies: toPolicies(decode(_peculiar_asn1_x509.id_ce_certificatePolicies, _peculiar_asn1_x509.CertificatePolicies, "certificatePolicies")),
|
|
638
|
+
subjectAlternativeNames: san ? toSubjectAlternativeNames(san) : emptySubjectAlternativeNames(),
|
|
639
|
+
keyUsage: keyUsage?.toJSON(),
|
|
640
|
+
extendedKeyUsage: eku ? [...eku] : void 0,
|
|
641
|
+
basicConstraints: basicConstraints ? {
|
|
642
|
+
cA: basicConstraints.cA,
|
|
643
|
+
pathLenConstraint: basicConstraints.pathLenConstraint
|
|
644
|
+
} : void 0,
|
|
645
|
+
subjectKeyIdentifier: ski ? toHex(ski.buffer) : void 0,
|
|
646
|
+
authorityKeyIdentifier: aki?.keyIdentifier ? toHex(aki.keyIdentifier.buffer) : void 0,
|
|
647
|
+
crlDistributionPoints: (crlDps ?? []).flatMap((dp) => urisOf(dp.distributionPoint?.fullName)),
|
|
648
|
+
ocspUrls: accessUrls(ACCESS_METHODS.ocsp),
|
|
649
|
+
caIssuersUrls: accessUrls(ACCESS_METHODS.caIssuers),
|
|
650
|
+
qcStatements: qcStatements ? parseQcStatements(qcStatements) : void 0,
|
|
651
|
+
extensions
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/signature.ts
|
|
656
|
+
/** AlgorithmIdentifier for a supported signature algorithm (RFC 3279, RFC 5758). */
|
|
657
|
+
function signatureAlgorithmIdentifier(name) {
|
|
658
|
+
const algorithm = SIGNATURE_ALGORITHMS[name];
|
|
659
|
+
if (!algorithm) throw new X509Exception(`Unsupported signature algorithm: ${name}`);
|
|
660
|
+
return new _peculiar_asn1_x509.AlgorithmIdentifier({
|
|
661
|
+
algorithm: algorithm.oid,
|
|
662
|
+
parameters: algorithm.family === "rsa" ? new asn1js.Null().toBER() : void 0
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
/** Normalize a serial number to positive DER INTEGER content (RFC 5280 clause 4.1.2.2). */
|
|
666
|
+
function toSerialNumber(serialNumber) {
|
|
667
|
+
const positive = toPositiveIntegerBytes(typeof serialNumber === "string" ? (0, _owf_identity_common.hexDecode)(serialNumber.replace(/^0x/i, "")) : serialNumber);
|
|
668
|
+
if (positive.length === 0 || positive.every((b) => b === 0)) throw new X509Exception("Serial number must be a positive integer");
|
|
669
|
+
if (positive.length > 20) throw new X509Exception("Serial number must not be longer than 20 octets (RFC 5280 clause 4.1.2.2)");
|
|
670
|
+
return toArrayBuffer(positive);
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Combine prepared to-be-signed bytes with their signature into the DER encoding of the
|
|
674
|
+
* signed structure: `SEQUENCE { tbs, signatureAlgorithm, BIT STRING signature }`.
|
|
675
|
+
*/
|
|
676
|
+
function assembleSignedStructure(prepared, signature, signatureEncoding = "ieee-p1363") {
|
|
677
|
+
const algorithm = SIGNATURE_ALGORITHMS[prepared.signatureAlgorithm];
|
|
678
|
+
let signatureDer = signature;
|
|
679
|
+
if (algorithm.family === "ecdsa" && signatureEncoding === "ieee-p1363") {
|
|
680
|
+
const coordinateLength = curveCoordinateLength(prepared.signerPublicKey.namedCurve);
|
|
681
|
+
if (!coordinateLength) throw new X509Exception("Unable to determine the curve of the signer key for ECDSA encoding");
|
|
682
|
+
signatureDer = ecdsaP1363ToDer(signature, coordinateLength);
|
|
683
|
+
}
|
|
684
|
+
const algorithmIdentifier = new Uint8Array(_peculiar_asn1_schema.AsnConvert.serialize(signatureAlgorithmIdentifier(prepared.signatureAlgorithm)));
|
|
685
|
+
return derTlv(48, (0, _owf_identity_common.concatBytes)(prepared.tbs, algorithmIdentifier, derTlv(3, (0, _owf_identity_common.concatBytes)(new Uint8Array([0]), signatureDer))));
|
|
686
|
+
}
|
|
687
|
+
async function verifySignature(signed, data, publicKey, verifier) {
|
|
688
|
+
let signatureP1363 = signed.signature;
|
|
689
|
+
if (signed.signatureAlgorithmName && SIGNATURE_ALGORITHMS[signed.signatureAlgorithmName].family === "ecdsa") {
|
|
690
|
+
const coordinateLength = curveCoordinateLength(publicKey.namedCurve);
|
|
691
|
+
if (!coordinateLength) return false;
|
|
692
|
+
try {
|
|
693
|
+
signatureP1363 = ecdsaDerToP1363(signed.signature, coordinateLength);
|
|
694
|
+
} catch {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return verifier({
|
|
699
|
+
data,
|
|
700
|
+
signature: signed.signature,
|
|
701
|
+
signatureP1363,
|
|
702
|
+
signatureAlgorithm: signed.signatureAlgorithm,
|
|
703
|
+
signatureAlgorithmName: signed.signatureAlgorithmName,
|
|
704
|
+
publicKey
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
function toCertificate(input) {
|
|
708
|
+
return typeof input === "object" && "tbsCertificate" in input ? input : parseCertificate(input);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Verify that `certificate` was signed by the key of `issuerCertificate`.
|
|
712
|
+
*
|
|
713
|
+
* The issuer name and, when both are present, the authority / subject key identifiers must
|
|
714
|
+
* match. The cryptographic check is delegated to `verifier`, so no global crypto is needed:
|
|
715
|
+
* with WebCrypto, import `publicKey.jwk` and verify `signatureP1363` over `data`.
|
|
716
|
+
*
|
|
717
|
+
* This checks a single link, not a path to a trust anchor.
|
|
718
|
+
*/
|
|
719
|
+
async function verifyCertificateSignature(certificate, issuerCertificate, verifier) {
|
|
720
|
+
const cert = toCertificate(certificate);
|
|
721
|
+
const issuer = toCertificate(issuerCertificate);
|
|
722
|
+
if (!namesEqual(cert.issuer, issuer.subject)) return false;
|
|
723
|
+
if (cert.authorityKeyIdentifier && issuer.subjectKeyIdentifier && cert.authorityKeyIdentifier !== issuer.subjectKeyIdentifier) return false;
|
|
724
|
+
return verifySignature(cert, cert.tbsCertificate, issuer.publicKey, verifier);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Verify that `crl` was signed by `issuerCertificate`.
|
|
728
|
+
*
|
|
729
|
+
* The CRL issuer name and, when both are present, the key identifiers must match, and the
|
|
730
|
+
* issuer key usage (if present) must allow cRLSign (RFC 5280 clause 6.3.3).
|
|
731
|
+
*/
|
|
732
|
+
async function verifyCrlSignature(crl, issuerCertificate, verifier) {
|
|
733
|
+
const issuer = toCertificate(issuerCertificate);
|
|
734
|
+
if (!namesEqual(crl.issuer, issuer.subject)) return false;
|
|
735
|
+
if (crl.authorityKeyIdentifier && issuer.subjectKeyIdentifier && crl.authorityKeyIdentifier !== issuer.subjectKeyIdentifier) return false;
|
|
736
|
+
if (issuer.keyUsage && !issuer.keyUsage.includes("crlSign")) return false;
|
|
737
|
+
return verifySignature(crl, crl.tbsCertList, issuer.publicKey, verifier);
|
|
738
|
+
}
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/create.ts
|
|
741
|
+
function extension(extnID, value, critical = false) {
|
|
742
|
+
return new _peculiar_asn1_x509.Extension({
|
|
743
|
+
extnID,
|
|
744
|
+
critical,
|
|
745
|
+
extnValue: new _peculiar_asn1_schema.OctetString(_peculiar_asn1_schema.AsnConvert.serialize(value))
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function keyUsageFlags(usages) {
|
|
749
|
+
return usages.reduce((flags, usage) => flags | _peculiar_asn1_x509.KeyUsageFlags[usage === "crlSign" ? "cRLSign" : usage], 0);
|
|
750
|
+
}
|
|
751
|
+
/** Parse an issuer certificate into its ASN.1 form. */
|
|
752
|
+
function parseIssuerCertificate(input) {
|
|
753
|
+
try {
|
|
754
|
+
return _peculiar_asn1_schema.AsnConvert.parse(toArrayBuffer(toDer(input, PEM_LABELS.certificate)), _peculiar_asn1_x509.Certificate);
|
|
755
|
+
} catch (error) {
|
|
756
|
+
throw new X509Exception("Invalid issuer certificate", error);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/** The subject key identifier of an ASN.1 certificate, if present. */
|
|
760
|
+
function subjectKeyIdentifierOf(certificate) {
|
|
761
|
+
const ski = certificate.tbsCertificate.extensions?.find((e) => e.extnID === _peculiar_asn1_x509.id_ce_subjectKeyIdentifier);
|
|
762
|
+
return ski ? _peculiar_asn1_schema.AsnConvert.parse(ski.extnValue.buffer, _peculiar_asn1_x509.SubjectKeyIdentifier).buffer : void 0;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Encode an otherName whose value is a PrintableString (or UTF8String for non-printable
|
|
766
|
+
* values), e.g. an `id-at-telephoneNumber` subject alternative name.
|
|
767
|
+
*/
|
|
768
|
+
function stringOtherName(typeId, value) {
|
|
769
|
+
const encoded = /^[A-Za-z0-9 '()+,\-./:=?]*$/.test(value) ? new asn1js.PrintableString({ value }) : new asn1js.Utf8String({ value });
|
|
770
|
+
return {
|
|
771
|
+
typeId,
|
|
772
|
+
value: new Uint8Array(encoded.toBER())
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Build the TBSCertificate for a certificate template.
|
|
777
|
+
*
|
|
778
|
+
* Sign the returned `tbs` bytes with the issuer key wherever that key lives and pass the
|
|
779
|
+
* signature to {@link assembleCertificate}. {@link createCertificate} wraps both steps
|
|
780
|
+
* around a signer callback.
|
|
781
|
+
*/
|
|
782
|
+
async function prepareCertificate(template) {
|
|
783
|
+
if (!(template.notAfter > template.notBefore)) throw new X509Exception("notAfter must be later than notBefore");
|
|
784
|
+
const spkiDer = template.subjectPublicKey instanceof Uint8Array ? template.subjectPublicKey : publicJwkToSpki(template.subjectPublicKey);
|
|
785
|
+
let subjectPublicKeyInfo;
|
|
786
|
+
try {
|
|
787
|
+
subjectPublicKeyInfo = _peculiar_asn1_schema.AsnConvert.parse(toArrayBuffer(spkiDer), _peculiar_asn1_x509.SubjectPublicKeyInfo);
|
|
788
|
+
} catch (error) {
|
|
789
|
+
throw new X509Exception("Invalid subject public key", error);
|
|
790
|
+
}
|
|
791
|
+
const issuer = template.issuerCertificate ? parseIssuerCertificate(template.issuerCertificate) : void 0;
|
|
792
|
+
const subject = buildName(template.subject);
|
|
793
|
+
if (subject.length === 0) throw new X509Exception("The subject must not be empty");
|
|
794
|
+
const extensions = [];
|
|
795
|
+
const basicConstraints = template.basicConstraints ?? { cA: false };
|
|
796
|
+
extensions.push(extension(_peculiar_asn1_x509.id_ce_basicConstraints, new _peculiar_asn1_x509.BasicConstraints(basicConstraints), true));
|
|
797
|
+
const keyUsage = template.keyUsage ?? (basicConstraints.cA ? ["keyCertSign", "crlSign"] : ["digitalSignature"]);
|
|
798
|
+
if (keyUsage.length > 0) extensions.push(extension(_peculiar_asn1_x509.id_ce_keyUsage, new _peculiar_asn1_x509.KeyUsage(keyUsageFlags(keyUsage)), true));
|
|
799
|
+
if (template.extendedKeyUsage?.length) extensions.push(extension(_peculiar_asn1_x509.id_ce_extKeyUsage, new _peculiar_asn1_x509.ExtendedKeyUsage(template.extendedKeyUsage)));
|
|
800
|
+
if (template.certificatePolicies?.length) extensions.push(extension(_peculiar_asn1_x509.id_ce_certificatePolicies, new _peculiar_asn1_x509.CertificatePolicies(template.certificatePolicies.map((policy) => new _peculiar_asn1_x509.PolicyInformation({
|
|
801
|
+
policyIdentifier: policy.oid,
|
|
802
|
+
policyQualifiers: policy.cpsUris?.length ? policy.cpsUris.map((uri) => new _peculiar_asn1_x509.PolicyQualifierInfo({
|
|
803
|
+
policyQualifierId: POLICY_QUALIFIERS.cps,
|
|
804
|
+
qualifier: new asn1js.IA5String({ value: uri }).toBER()
|
|
805
|
+
})) : void 0
|
|
806
|
+
})))));
|
|
807
|
+
const san = template.subjectAlternativeNames;
|
|
808
|
+
const generalNames = [
|
|
809
|
+
...(san?.dnsNames ?? []).map((dNSName) => new _peculiar_asn1_x509.GeneralName({ dNSName })),
|
|
810
|
+
...(san?.uris ?? []).map((uniformResourceIdentifier) => new _peculiar_asn1_x509.GeneralName({ uniformResourceIdentifier })),
|
|
811
|
+
...(san?.emails ?? []).map((rfc822Name) => new _peculiar_asn1_x509.GeneralName({ rfc822Name })),
|
|
812
|
+
...(san?.otherNames ?? []).map((o) => new _peculiar_asn1_x509.GeneralName({ otherName: new _peculiar_asn1_x509.OtherName({
|
|
813
|
+
typeId: o.typeId,
|
|
814
|
+
value: toArrayBuffer(o.value)
|
|
815
|
+
}) }))
|
|
816
|
+
];
|
|
817
|
+
if (generalNames.length > 0) extensions.push(extension(_peculiar_asn1_x509.id_ce_subjectAltName, new _peculiar_asn1_x509.SubjectAlternativeName(generalNames)));
|
|
818
|
+
let subjectKeyIdentifier;
|
|
819
|
+
if (template.hasher) {
|
|
820
|
+
subjectKeyIdentifier = toArrayBuffer((await template.hasher(subjectPublicKeyInfo.subjectPublicKey, "sha-256")).slice(0, 20));
|
|
821
|
+
extensions.push(extension(_peculiar_asn1_x509.id_ce_subjectKeyIdentifier, new _peculiar_asn1_x509.SubjectKeyIdentifier(subjectKeyIdentifier)));
|
|
822
|
+
}
|
|
823
|
+
const authorityKeyIdentifier = issuer ? subjectKeyIdentifierOf(issuer) : subjectKeyIdentifier;
|
|
824
|
+
if (authorityKeyIdentifier) extensions.push(extension(_peculiar_asn1_x509.id_ce_authorityKeyIdentifier, new _peculiar_asn1_x509.AuthorityKeyIdentifier({ keyIdentifier: new _peculiar_asn1_x509.KeyIdentifier(authorityKeyIdentifier) })));
|
|
825
|
+
if (template.crlDistributionPoints?.length) extensions.push(extension(_peculiar_asn1_x509.id_ce_cRLDistributionPoints, new _peculiar_asn1_x509.CRLDistributionPoints(template.crlDistributionPoints.map((uri) => new _peculiar_asn1_x509.DistributionPoint({ distributionPoint: new _peculiar_asn1_x509.DistributionPointName({ fullName: [new _peculiar_asn1_x509.GeneralName({ uniformResourceIdentifier: uri })] }) })))));
|
|
826
|
+
const accessDescriptions = [...(template.ocspUrls ?? []).map((uri) => [ACCESS_METHODS.ocsp, uri]), ...(template.caIssuersUrls ?? []).map((uri) => [ACCESS_METHODS.caIssuers, uri])].map(([accessMethod, uri]) => new _peculiar_asn1_x509.AccessDescription({
|
|
827
|
+
accessMethod,
|
|
828
|
+
accessLocation: new _peculiar_asn1_x509.GeneralName({ uniformResourceIdentifier: uri })
|
|
829
|
+
}));
|
|
830
|
+
if (accessDescriptions.length > 0) extensions.push(extension(_peculiar_asn1_x509.id_pe_authorityInfoAccess, new _peculiar_asn1_x509.AuthorityInfoAccessSyntax(accessDescriptions)));
|
|
831
|
+
if (template.qcStatements) {
|
|
832
|
+
const qcStatements = toQcStatements({
|
|
833
|
+
...template.qcStatements,
|
|
834
|
+
qcTypes: template.qcStatements.qcTypes ?? []
|
|
835
|
+
});
|
|
836
|
+
extensions.push(extension(QC_STATEMENTS.extension, qcStatements));
|
|
837
|
+
}
|
|
838
|
+
for (const custom of template.extensions ?? []) extensions.push(toAsnExtension(custom));
|
|
839
|
+
const seen = /* @__PURE__ */ new Set();
|
|
840
|
+
for (const { extnID } of extensions) {
|
|
841
|
+
if (seen.has(extnID)) throw new X509Exception(`Duplicate extension ${extnID}`);
|
|
842
|
+
seen.add(extnID);
|
|
843
|
+
}
|
|
844
|
+
const tbs = new _peculiar_asn1_x509.TBSCertificate({
|
|
845
|
+
version: _peculiar_asn1_x509.Version.v3,
|
|
846
|
+
serialNumber: toSerialNumber(template.serialNumber),
|
|
847
|
+
signature: signatureAlgorithmIdentifier(template.signatureAlgorithm),
|
|
848
|
+
issuer: issuer ? issuer.tbsCertificate.subject : subject,
|
|
849
|
+
validity: new _peculiar_asn1_x509.Validity({
|
|
850
|
+
notBefore: template.notBefore,
|
|
851
|
+
notAfter: template.notAfter
|
|
852
|
+
}),
|
|
853
|
+
subject,
|
|
854
|
+
subjectPublicKeyInfo,
|
|
855
|
+
extensions: new _peculiar_asn1_x509.Extensions(extensions)
|
|
856
|
+
});
|
|
857
|
+
return {
|
|
858
|
+
tbs: new Uint8Array(_peculiar_asn1_schema.AsnConvert.serialize(tbs)),
|
|
859
|
+
signatureAlgorithm: template.signatureAlgorithm,
|
|
860
|
+
signerPublicKey: describePublicKey(issuer ? issuer.tbsCertificate.subjectPublicKeyInfo : subjectPublicKeyInfo)
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function toAsnExtension(extension) {
|
|
864
|
+
return new _peculiar_asn1_x509.Extension({
|
|
865
|
+
extnID: extension.oid,
|
|
866
|
+
critical: extension.critical,
|
|
867
|
+
extnValue: new _peculiar_asn1_schema.OctetString(toArrayBuffer(extension.value))
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Combine a prepared TBSCertificate with the issuer signature.
|
|
872
|
+
*
|
|
873
|
+
* @param signatureEncoding Encoding of ECDSA signatures; WebCrypto produces `ieee-p1363` (default)
|
|
874
|
+
*/
|
|
875
|
+
function assembleCertificate(prepared, signature, signatureEncoding) {
|
|
876
|
+
return parseCertificate(assembleSignedStructure(prepared, signature, signatureEncoding));
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Create an X.509 v3 certificate.
|
|
880
|
+
*
|
|
881
|
+
* The issuer key is never passed in: the `signer` callback receives the DER encoded
|
|
882
|
+
* TBSCertificate and returns the signature, so the key can stay in WebCrypto, a KMS or an
|
|
883
|
+
* HSM. Only the public key of the subject is needed; a JWK with private key material is
|
|
884
|
+
* rejected.
|
|
885
|
+
*/
|
|
886
|
+
async function createCertificate(template) {
|
|
887
|
+
const prepared = await prepareCertificate(template);
|
|
888
|
+
return assembleCertificate(prepared, await template.signer(prepared.tbs), template.signatureEncoding);
|
|
889
|
+
}
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/crl.ts
|
|
892
|
+
const REASON_BY_CODE = new Map(Object.entries(CRL_REASONS).map(([name, code]) => [code, name]));
|
|
893
|
+
/** CRL extensions understood by {@link getRevocationStatus} */
|
|
894
|
+
const KNOWN_CRL_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
895
|
+
_peculiar_asn1_x509.id_ce_authorityKeyIdentifier,
|
|
896
|
+
_peculiar_asn1_x509.id_ce_issuerAltName,
|
|
897
|
+
_peculiar_asn1_x509.id_ce_cRLNumber,
|
|
898
|
+
_peculiar_asn1_x509.id_ce_deltaCRLIndicator,
|
|
899
|
+
_peculiar_asn1_x509.id_ce_issuingDistributionPoint,
|
|
900
|
+
_peculiar_asn1_x509.id_ce_freshestCRL,
|
|
901
|
+
_peculiar_asn1_x509.id_pe_authorityInfoAccess
|
|
902
|
+
]);
|
|
903
|
+
/** CRL entry extensions understood by {@link getRevocationStatus} */
|
|
904
|
+
const KNOWN_ENTRY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
905
|
+
_peculiar_asn1_x509.id_ce_cRLReasons,
|
|
906
|
+
_peculiar_asn1_x509.id_ce_invalidityDate,
|
|
907
|
+
_peculiar_asn1_x509.id_ce_certificateIssuer,
|
|
908
|
+
"2.5.29.23"
|
|
909
|
+
]);
|
|
910
|
+
function decodeInteger(extensions, oid, name) {
|
|
911
|
+
const extension = extensions.find((e) => e.oid === oid);
|
|
912
|
+
if (!extension) return void 0;
|
|
913
|
+
const node = parseDer(extension.value);
|
|
914
|
+
const bytes = node instanceof asn1js.Integer ? new Uint8Array(node.valueBlock.valueHexView) : void 0;
|
|
915
|
+
if (!bytes || bytes.length === 0 || bytes[0] & 128) throw new X509Exception(`Invalid ${name} extension`);
|
|
916
|
+
return integerBytesToBigInt(bytes);
|
|
917
|
+
}
|
|
918
|
+
function directoryNameOf(names) {
|
|
919
|
+
const name = names.find((n) => n.directoryName)?.directoryName;
|
|
920
|
+
return name ? parseName(name) : void 0;
|
|
921
|
+
}
|
|
922
|
+
function toRevokedCertificates(entries) {
|
|
923
|
+
let certificateIssuer;
|
|
924
|
+
return (entries ?? []).map((entry) => {
|
|
925
|
+
const extensions = toExtensions(entry.crlEntryExtensions);
|
|
926
|
+
const decode = (oid, type, name) => decodeExtension(extensions, oid, type, name);
|
|
927
|
+
const reason = decode(_peculiar_asn1_x509.id_ce_cRLReasons, _peculiar_asn1_x509.CRLReason, "reasonCode");
|
|
928
|
+
const invalidity = decode(_peculiar_asn1_x509.id_ce_invalidityDate, _peculiar_asn1_x509.InvalidityDate, "invalidityDate");
|
|
929
|
+
const issuer = decode(_peculiar_asn1_x509.id_ce_certificateIssuer, _peculiar_asn1_x509.CertificateIssuer, "certificateIssuer");
|
|
930
|
+
if (issuer) certificateIssuer = directoryNameOf(issuer);
|
|
931
|
+
return {
|
|
932
|
+
serialNumber: toHex(entry.userCertificate),
|
|
933
|
+
revocationDate: entry.revocationDate.getTime(),
|
|
934
|
+
reason: reason ? REASON_BY_CODE.get(reason.reason) : void 0,
|
|
935
|
+
invalidityDate: invalidity?.value,
|
|
936
|
+
certificateIssuer,
|
|
937
|
+
extensions
|
|
938
|
+
};
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
/** ASN.1 nodes per CRL entry: SEQUENCE, INTEGER, Time and up to three extensions of four nodes */
|
|
942
|
+
const NODES_PER_ENTRY = 16;
|
|
943
|
+
/**
|
|
944
|
+
* Parse an X.509 certificate revocation list (RFC 5280 clause 5).
|
|
945
|
+
*
|
|
946
|
+
* Parsing does not verify the signature; use {@link verifyCrlSignature} or {@link checkRevocation}.
|
|
947
|
+
*
|
|
948
|
+
* @param input DER bytes, base64 encoded DER, or PEM (`X509 CRL`)
|
|
949
|
+
* @throws X509Exception when the input is not a well-formed CRL or exceeds the size limits
|
|
950
|
+
*/
|
|
951
|
+
function parseCrl(input, options = {}) {
|
|
952
|
+
const der = toDer(input, PEM_LABELS.crl);
|
|
953
|
+
const { maxEntries = 1e5, berOptions } = options;
|
|
954
|
+
let crl;
|
|
955
|
+
try {
|
|
956
|
+
crl = _peculiar_asn1_schema.AsnConvert.parse(toArrayBuffer(der), _peculiar_asn1_x509.CertificateList, { berOptions: {
|
|
957
|
+
maxNodes: 1e3 + maxEntries * NODES_PER_ENTRY,
|
|
958
|
+
...berOptions
|
|
959
|
+
} });
|
|
960
|
+
} catch (error) {
|
|
961
|
+
throw new X509Exception("Invalid CRL", error);
|
|
962
|
+
}
|
|
963
|
+
if (!crl.tbsCertListRaw) throw new X509Exception("Unable to extract the TBSCertList");
|
|
964
|
+
const tbs = crl.tbsCertList;
|
|
965
|
+
const extensions = toExtensions(tbs.crlExtensions);
|
|
966
|
+
const decode = (oid, type, name) => decodeExtension(extensions, oid, type, name);
|
|
967
|
+
const aki = decode(_peculiar_asn1_x509.id_ce_authorityKeyIdentifier, _peculiar_asn1_x509.AuthorityKeyIdentifier, "authorityKeyIdentifier");
|
|
968
|
+
const idp = decode(_peculiar_asn1_x509.id_ce_issuingDistributionPoint, _peculiar_asn1_x509.IssuingDistributionPoint, "issuingDistributionPoint");
|
|
969
|
+
const freshest = decode(_peculiar_asn1_x509.id_ce_freshestCRL, _peculiar_asn1_x509.FreshestCRL, "freshestCRL");
|
|
970
|
+
return {
|
|
971
|
+
der,
|
|
972
|
+
pem: toPem(der, PEM_LABELS.crl),
|
|
973
|
+
version: tbs.version === void 0 ? 1 : tbs.version + 1,
|
|
974
|
+
issuer: parseName(tbs.issuer),
|
|
975
|
+
thisUpdate: tbs.thisUpdate.getTime(),
|
|
976
|
+
nextUpdate: tbs.nextUpdate?.getTime(),
|
|
977
|
+
signatureAlgorithm: crl.signatureAlgorithm.algorithm,
|
|
978
|
+
signatureAlgorithmName: signatureAlgorithmName(crl.signatureAlgorithm.algorithm),
|
|
979
|
+
signature: new Uint8Array(crl.signature),
|
|
980
|
+
tbsCertList: new Uint8Array(crl.tbsCertListRaw),
|
|
981
|
+
crlNumber: decodeInteger(extensions, _peculiar_asn1_x509.id_ce_cRLNumber, "cRLNumber"),
|
|
982
|
+
deltaCrlIndicator: decodeInteger(extensions, _peculiar_asn1_x509.id_ce_deltaCRLIndicator, "deltaCRLIndicator"),
|
|
983
|
+
authorityKeyIdentifier: aki?.keyIdentifier ? toHex(aki.keyIdentifier.buffer) : void 0,
|
|
984
|
+
issuingDistributionPoint: idp ? {
|
|
985
|
+
distributionPointUris: urisOf(idp.distributionPoint?.fullName),
|
|
986
|
+
onlyContainsUserCerts: idp.onlyContainsUserCerts,
|
|
987
|
+
onlyContainsCACerts: idp.onlyContainsCACerts,
|
|
988
|
+
onlyContainsAttributeCerts: idp.onlyContainsAttributeCerts,
|
|
989
|
+
indirectCRL: idp.indirectCRL,
|
|
990
|
+
onlySomeReasons: idp.onlySomeReasons?.toJSON()
|
|
991
|
+
} : void 0,
|
|
992
|
+
freshestCrl: (freshest ?? []).flatMap((dp) => urisOf(dp.distributionPoint?.fullName)),
|
|
993
|
+
revokedCertificates: toRevokedCertificates(tbs.revokedCertificates),
|
|
994
|
+
extensions
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function normalizeSerial(hex) {
|
|
998
|
+
return hex.toLowerCase().replace(/^(00)+(?=..)/, "");
|
|
999
|
+
}
|
|
1000
|
+
function unknown(reason, message) {
|
|
1001
|
+
return {
|
|
1002
|
+
status: "unknown",
|
|
1003
|
+
reason,
|
|
1004
|
+
message
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Look up the revocation status of `certificate` in `crl`.
|
|
1009
|
+
*
|
|
1010
|
+
* This does **not** verify the CRL signature; use {@link checkRevocation} unless the CRL
|
|
1011
|
+
* has already been verified. The CRL must be issued by the certificate issuer (or be an
|
|
1012
|
+
* indirect CRL naming it), must be current, and must cover the certificate. Otherwise the
|
|
1013
|
+
* status is `unknown` with the reason, and the caller should find a suitable CRL or fall
|
|
1014
|
+
* back to OCSP.
|
|
1015
|
+
*/
|
|
1016
|
+
function getRevocationStatus(certificate, crl, options = {}) {
|
|
1017
|
+
if (crl.deltaCrlIndicator !== void 0) return unknown("delta_crl", "Delta CRLs can only be used together with their base CRL");
|
|
1018
|
+
const unsupported = crl.extensions.find((e) => e.critical && !KNOWN_CRL_EXTENSIONS.has(e.oid));
|
|
1019
|
+
if (unsupported) return unknown("unsupported_critical_extension", `Unsupported critical CRL extension ${unsupported.oid}`);
|
|
1020
|
+
const idp = crl.issuingDistributionPoint;
|
|
1021
|
+
const indirect = idp?.indirectCRL ?? false;
|
|
1022
|
+
if (!indirect) {
|
|
1023
|
+
if (!namesEqual(crl.issuer, certificate.issuer)) return unknown("issuer_mismatch", "The CRL is not issued by the certificate issuer");
|
|
1024
|
+
if (crl.authorityKeyIdentifier && certificate.authorityKeyIdentifier && crl.authorityKeyIdentifier !== certificate.authorityKeyIdentifier) return unknown("issuer_mismatch", "The CRL is signed with a different key than the certificate");
|
|
1025
|
+
}
|
|
1026
|
+
if (options.now !== false) {
|
|
1027
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1028
|
+
if (now < crl.thisUpdate) return unknown("crl_not_yet_valid", `The CRL is not valid before ${crl.thisUpdate.toISOString()}`);
|
|
1029
|
+
if (crl.nextUpdate && now > crl.nextUpdate) return unknown("crl_expired", `The CRL expired at ${crl.nextUpdate.toISOString()}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (idp) {
|
|
1032
|
+
const isCA = certificate.basicConstraints?.cA === true;
|
|
1033
|
+
if (idp.onlyContainsAttributeCerts) return unknown("out_of_scope", "The CRL only covers attribute certificates");
|
|
1034
|
+
if (idp.onlyContainsCACerts && !isCA) return unknown("out_of_scope", "The CRL only covers CA certificates");
|
|
1035
|
+
if (idp.onlyContainsUserCerts && isCA) return unknown("out_of_scope", "The CRL only covers end-entity certificates");
|
|
1036
|
+
if (idp.distributionPointUris.length > 0 && !idp.distributionPointUris.some((uri) => certificate.crlDistributionPoints.includes(uri))) return unknown("out_of_scope", "The CRL distribution point does not match the certificate");
|
|
1037
|
+
}
|
|
1038
|
+
const serial = normalizeSerial(certificate.serialNumber);
|
|
1039
|
+
const entry = crl.revokedCertificates.find((e) => normalizeSerial(e.serialNumber) === serial && (!indirect || namesEqual(e.certificateIssuer ?? crl.issuer, certificate.issuer)));
|
|
1040
|
+
if (entry) {
|
|
1041
|
+
const unsupportedEntry = entry.extensions.find((e) => e.critical && !KNOWN_ENTRY_EXTENSIONS.has(e.oid));
|
|
1042
|
+
if (unsupportedEntry) return unknown("unsupported_critical_extension", `Unsupported critical CRL entry extension ${unsupportedEntry.oid}`);
|
|
1043
|
+
if (entry.reason === "removeFromCRL") return { status: "good" };
|
|
1044
|
+
return {
|
|
1045
|
+
status: "revoked",
|
|
1046
|
+
revocationDate: entry.revocationDate,
|
|
1047
|
+
reason: entry.reason,
|
|
1048
|
+
invalidityDate: entry.invalidityDate
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
if (idp?.onlySomeReasons) return unknown("out_of_scope", `The CRL only covers the reasons ${idp.onlySomeReasons.join(", ")}`);
|
|
1052
|
+
return { status: "good" };
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Verify the CRL signature against `crlIssuerCertificate`, then look up the revocation
|
|
1056
|
+
* status of `certificate` (see {@link getRevocationStatus}).
|
|
1057
|
+
*
|
|
1058
|
+
* The signature check is delegated to `verifier`; an invalid signature yields `unknown`
|
|
1059
|
+
* with reason `invalid_signature`. Fetching the CRL (see `crlDistributionPoints` on the
|
|
1060
|
+
* certificate) is up to the caller.
|
|
1061
|
+
*/
|
|
1062
|
+
async function checkRevocation(options) {
|
|
1063
|
+
if (!await verifyCrlSignature(options.crl, options.crlIssuerCertificate, options.verifier)) return unknown("invalid_signature", "The CRL signature is invalid or not issued by the given certificate");
|
|
1064
|
+
return getRevocationStatus(options.certificate, options.crl, { now: options.now });
|
|
1065
|
+
}
|
|
1066
|
+
function bigintToIntegerBytes(value) {
|
|
1067
|
+
const n = BigInt(value);
|
|
1068
|
+
if (n < BigInt(0)) throw new X509Exception("CRL numbers must not be negative");
|
|
1069
|
+
const hex = n.toString(16);
|
|
1070
|
+
const bytes = toPositiveIntegerBytes((0, _owf_identity_common.hexDecode)(hex.length % 2 ? `0${hex}` : hex));
|
|
1071
|
+
if (bytes.length > 20) throw new X509Exception("CRL numbers must not be longer than 20 octets");
|
|
1072
|
+
return bytes;
|
|
1073
|
+
}
|
|
1074
|
+
function derExtension(oid, value, critical = false) {
|
|
1075
|
+
return new _peculiar_asn1_x509.Extension({
|
|
1076
|
+
extnID: oid,
|
|
1077
|
+
critical,
|
|
1078
|
+
extnValue: new _peculiar_asn1_schema.OctetString(value instanceof Uint8Array ? toArrayBuffer(value) : value)
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Build the TBSCertList of a v2 CRL. Sign the returned `tbs` bytes with the CRL issuer key
|
|
1083
|
+
* and pass the signature to {@link assembleCrl}, or use {@link createCrl} with a signer.
|
|
1084
|
+
*/
|
|
1085
|
+
function prepareCrl(template) {
|
|
1086
|
+
if (!(template.nextUpdate > template.thisUpdate)) throw new X509Exception("nextUpdate must be later than thisUpdate");
|
|
1087
|
+
const issuer = parseIssuerCertificate(template.issuerCertificate);
|
|
1088
|
+
const extensions = [];
|
|
1089
|
+
const ski = subjectKeyIdentifierOf(issuer);
|
|
1090
|
+
if (ski) extensions.push(derExtension(_peculiar_asn1_x509.id_ce_authorityKeyIdentifier, _peculiar_asn1_schema.AsnConvert.serialize(new _peculiar_asn1_x509.AuthorityKeyIdentifier({ keyIdentifier: new _peculiar_asn1_x509.KeyIdentifier(ski) }))));
|
|
1091
|
+
extensions.push(derExtension(_peculiar_asn1_x509.id_ce_cRLNumber, new asn1js.Integer({ valueHex: toArrayBuffer(bigintToIntegerBytes(template.crlNumber)) }).toBER()));
|
|
1092
|
+
const idp = template.issuingDistributionPoint;
|
|
1093
|
+
if (idp) extensions.push(derExtension(_peculiar_asn1_x509.id_ce_issuingDistributionPoint, _peculiar_asn1_schema.AsnConvert.serialize(new _peculiar_asn1_x509.IssuingDistributionPoint({
|
|
1094
|
+
distributionPoint: idp.distributionPointUris?.length ? new _peculiar_asn1_x509.DistributionPointName({ fullName: idp.distributionPointUris.map((uri) => new _peculiar_asn1_x509.GeneralName({ uniformResourceIdentifier: uri })) }) : void 0,
|
|
1095
|
+
onlyContainsUserCerts: idp.onlyContainsUserCerts ?? false,
|
|
1096
|
+
onlyContainsCACerts: idp.onlyContainsCACerts ?? false
|
|
1097
|
+
})), true));
|
|
1098
|
+
for (const custom of template.extensions ?? []) extensions.push(toAsnExtension(custom));
|
|
1099
|
+
const revoked = (template.revokedCertificates ?? []).map((entry) => {
|
|
1100
|
+
const entryExtensions = [];
|
|
1101
|
+
if (entry.reason && entry.reason !== "unspecified") entryExtensions.push(derExtension(_peculiar_asn1_x509.id_ce_cRLReasons, new asn1js.Enumerated({ value: CRL_REASONS[entry.reason] }).toBER()));
|
|
1102
|
+
if (entry.invalidityDate) entryExtensions.push(derExtension(_peculiar_asn1_x509.id_ce_invalidityDate, _peculiar_asn1_schema.AsnConvert.serialize(new _peculiar_asn1_x509.InvalidityDate(entry.invalidityDate))));
|
|
1103
|
+
return new _peculiar_asn1_x509.RevokedCertificate({
|
|
1104
|
+
userCertificate: toSerialNumber(entry.serialNumber),
|
|
1105
|
+
revocationDate: new _peculiar_asn1_x509.Time(entry.revocationDate),
|
|
1106
|
+
crlEntryExtensions: entryExtensions.length > 0 ? entryExtensions : void 0
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
const tbs = new _peculiar_asn1_x509.TBSCertList({
|
|
1110
|
+
version: _peculiar_asn1_x509.Version.v2,
|
|
1111
|
+
signature: signatureAlgorithmIdentifier(template.signatureAlgorithm),
|
|
1112
|
+
issuer: issuer.tbsCertificate.subject,
|
|
1113
|
+
thisUpdate: new _peculiar_asn1_x509.Time(template.thisUpdate),
|
|
1114
|
+
nextUpdate: new _peculiar_asn1_x509.Time(template.nextUpdate),
|
|
1115
|
+
revokedCertificates: revoked.length > 0 ? revoked : void 0,
|
|
1116
|
+
crlExtensions: extensions
|
|
1117
|
+
});
|
|
1118
|
+
return {
|
|
1119
|
+
tbs: new Uint8Array(_peculiar_asn1_schema.AsnConvert.serialize(tbs)),
|
|
1120
|
+
signatureAlgorithm: template.signatureAlgorithm,
|
|
1121
|
+
signerPublicKey: describePublicKey(issuer.tbsCertificate.subjectPublicKeyInfo)
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
/** Combine a prepared TBSCertList with the issuer signature. */
|
|
1125
|
+
function assembleCrl(prepared, signature, signatureEncoding) {
|
|
1126
|
+
return parseCrl(assembleSignedStructure(prepared, signature, signatureEncoding));
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Create a v2 CRL. The CRL issuer key is never passed in: the `signer` callback receives the
|
|
1130
|
+
* DER encoded TBSCertList and returns the signature.
|
|
1131
|
+
*/
|
|
1132
|
+
async function createCrl(template) {
|
|
1133
|
+
const prepared = prepareCrl(template);
|
|
1134
|
+
return assembleCrl(prepared, await template.signer(prepared.tbs), template.signatureEncoding);
|
|
1135
|
+
}
|
|
1136
|
+
//#endregion
|
|
1137
|
+
exports.ACCESS_METHODS = ACCESS_METHODS;
|
|
1138
|
+
exports.ATTRIBUTE_TYPES = ATTRIBUTE_TYPES;
|
|
1139
|
+
exports.CRL_REASONS = CRL_REASONS;
|
|
1140
|
+
exports.KEY_ALGORITHMS = KEY_ALGORITHMS;
|
|
1141
|
+
exports.PEM_LABELS = PEM_LABELS;
|
|
1142
|
+
exports.POLICY_QUALIFIERS = POLICY_QUALIFIERS;
|
|
1143
|
+
exports.QC_STATEMENTS = QC_STATEMENTS;
|
|
1144
|
+
exports.QC_TYPES = QC_TYPES;
|
|
1145
|
+
exports.SIGNATURE_ALGORITHMS = SIGNATURE_ALGORITHMS;
|
|
1146
|
+
exports.X509Exception = X509Exception;
|
|
1147
|
+
exports.assembleCertificate = assembleCertificate;
|
|
1148
|
+
exports.assembleCrl = assembleCrl;
|
|
1149
|
+
exports.assembleSignedStructure = assembleSignedStructure;
|
|
1150
|
+
exports.checkRevocation = checkRevocation;
|
|
1151
|
+
exports.createCertificate = createCertificate;
|
|
1152
|
+
exports.createCrl = createCrl;
|
|
1153
|
+
exports.ecdsaDerToP1363 = ecdsaDerToP1363;
|
|
1154
|
+
exports.ecdsaP1363ToDer = ecdsaP1363ToDer;
|
|
1155
|
+
exports.getRevocationStatus = getRevocationStatus;
|
|
1156
|
+
exports.namesEqual = namesEqual;
|
|
1157
|
+
exports.parseCertificate = parseCertificate;
|
|
1158
|
+
exports.parseCrl = parseCrl;
|
|
1159
|
+
exports.prepareCertificate = prepareCertificate;
|
|
1160
|
+
exports.prepareCrl = prepareCrl;
|
|
1161
|
+
exports.publicJwkToSpki = publicJwkToSpki;
|
|
1162
|
+
exports.stringOtherName = stringOtherName;
|
|
1163
|
+
exports.telephoneNumbers = telephoneNumbers;
|
|
1164
|
+
exports.toDer = toDer;
|
|
1165
|
+
exports.toPem = toPem;
|
|
1166
|
+
exports.verifyCertificateSignature = verifyCertificateSignature;
|
|
1167
|
+
exports.verifyCrlSignature = verifyCrlSignature;
|
|
1168
|
+
|
|
1169
|
+
//# sourceMappingURL=index.cjs.map
|