tunnelfetch 1.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/LICENSE +28 -0
- package/README.md +617 -0
- package/README.zh-CN.md +470 -0
- package/package.json +74 -0
- package/src/client/cookies.js +429 -0
- package/src/client/decode.js +346 -0
- package/src/client/redirect.js +249 -0
- package/src/client.js +704 -0
- package/src/errors.js +181 -0
- package/src/http1/chunked.js +289 -0
- package/src/http1/index.js +10 -0
- package/src/http1/request.js +143 -0
- package/src/http1/response.js +493 -0
- package/src/http2/connection.js +1170 -0
- package/src/http2/constants.js +129 -0
- package/src/http2/frames.js +291 -0
- package/src/http2/hpack.js +420 -0
- package/src/http2/huffman.js +203 -0
- package/src/http2/index.js +21 -0
- package/src/index.js +46 -0
- package/src/pool.js +256 -0
- package/src/proxy/direct.js +62 -0
- package/src/proxy/http-connect.js +206 -0
- package/src/proxy/index.js +197 -0
- package/src/proxy/socks5.js +344 -0
- package/src/tls/aead.js +263 -0
- package/src/tls/connect.js +407 -0
- package/src/tls/constants.js +334 -0
- package/src/tls/extensions.js +376 -0
- package/src/tls/handshake-messages.js +901 -0
- package/src/tls/handshake.js +568 -0
- package/src/tls/handshake12.js +507 -0
- package/src/tls/index.js +44 -0
- package/src/tls/keyschedule.js +473 -0
- package/src/tls/record.js +872 -0
- package/src/tls/tickets.js +145 -0
- package/src/tls/transcript.js +101 -0
- package/src/tls/wire.js +224 -0
- package/src/transport.js +296 -0
- package/src/trust/der.js +551 -0
- package/src/trust/index.js +375 -0
- package/src/trust/name.js +235 -0
- package/src/trust/ocsp.js +759 -0
- package/src/trust/path.js +595 -0
- package/src/trust/roots.js +454 -0
- package/src/trust/x509.js +902 -0
- package/src/util/bytes.js +470 -0
- package/src/util/deadline.js +266 -0
- package/src/warmup-fixture.js +85 -0
- package/src/warmup.js +243 -0
- package/types/client/cookies.d.ts +159 -0
- package/types/client/decode.d.ts +54 -0
- package/types/client/redirect.d.ts +96 -0
- package/types/client.d.ts +323 -0
- package/types/errors.d.ts +141 -0
- package/types/http1/chunked.d.ts +48 -0
- package/types/http1/index.d.ts +3 -0
- package/types/http1/request.d.ts +44 -0
- package/types/http1/response.d.ts +183 -0
- package/types/http2/connection.d.ts +282 -0
- package/types/http2/constants.d.ts +95 -0
- package/types/http2/frames.d.ts +116 -0
- package/types/http2/hpack.d.ts +99 -0
- package/types/http2/huffman.d.ts +21 -0
- package/types/http2/index.d.ts +5 -0
- package/types/index.d.ts +17 -0
- package/types/pool.d.ts +135 -0
- package/types/proxy/direct.d.ts +26 -0
- package/types/proxy/http-connect.d.ts +37 -0
- package/types/proxy/index.d.ts +62 -0
- package/types/proxy/socks5.d.ts +47 -0
- package/types/tls/aead.d.ts +67 -0
- package/types/tls/connect.d.ts +280 -0
- package/types/tls/constants.d.ts +275 -0
- package/types/tls/extensions.d.ts +195 -0
- package/types/tls/handshake-messages.d.ts +430 -0
- package/types/tls/handshake.d.ts +90 -0
- package/types/tls/handshake12.d.ts +35 -0
- package/types/tls/index.d.ts +9 -0
- package/types/tls/keyschedule.d.ts +272 -0
- package/types/tls/record.d.ts +361 -0
- package/types/tls/tickets.d.ts +66 -0
- package/types/tls/transcript.d.ts +52 -0
- package/types/tls/wire.d.ts +106 -0
- package/types/transport.d.ts +222 -0
- package/types/trust/der.d.ts +239 -0
- package/types/trust/index.d.ts +194 -0
- package/types/trust/name.d.ts +33 -0
- package/types/trust/ocsp.d.ts +138 -0
- package/types/trust/path.d.ts +139 -0
- package/types/trust/roots.d.ts +36 -0
- package/types/trust/x509.d.ts +401 -0
- package/types/util/bytes.d.ts +183 -0
- package/types/util/deadline.d.ts +133 -0
- package/types/warmup-fixture.d.ts +11 -0
- package/types/warmup.d.ts +45 -0
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
// X.509 v3 certificate parsing (RFC 5280 s4), on top of the DER reader.
|
|
2
|
+
//
|
|
3
|
+
// Parsed from the raw bytes, deliberately not via any runtime helper: the runtime's certificate
|
|
4
|
+
// object was measured on the target edge and silently lacks pathLenConstraint, the signature
|
|
5
|
+
// algorithm, and real key-usage bits. A security check that reads `undefined` and shrugs is the
|
|
6
|
+
// failure mode this package exists to close, so every field consumed by path validation comes
|
|
7
|
+
// from our own DER walk of the original bytes.
|
|
8
|
+
//
|
|
9
|
+
// parseCertificate() is a parser, not a judge: it records what the certificate says (including
|
|
10
|
+
// weak algorithms and unknown critical extensions) and throws only on malformed or self-
|
|
11
|
+
// contradictory encodings. Judgement lives in path.js, which consults resolveSignatureScheme()
|
|
12
|
+
// and unknownCriticalExtensions for exactly the certificates whose signatures actually anchor
|
|
13
|
+
// trust — a server may ship a stale SHA-1 self-signed root alongside a modern chain, and that
|
|
14
|
+
// root's own signature is never consumed, so it must not be able to break the connection.
|
|
15
|
+
|
|
16
|
+
import { CertificateError, codes } from '../errors.js';
|
|
17
|
+
import { equal, toHex } from '../util/bytes.js';
|
|
18
|
+
import { SIG_SCHEME } from '../tls/constants.js';
|
|
19
|
+
import {
|
|
20
|
+
TAG, CLS, readTlv, readAll, readSequence, children, content, element, expectTlv,
|
|
21
|
+
readInteger, readOid, readBitString, readBoolean, readTime, readString, parseError, tagName,
|
|
22
|
+
} from './der.js';
|
|
23
|
+
|
|
24
|
+
export const OID = /** @type {const} */ ({
|
|
25
|
+
// public key algorithms
|
|
26
|
+
rsaEncryption: '1.2.840.113549.1.1.1',
|
|
27
|
+
rsassaPss: '1.2.840.113549.1.1.10',
|
|
28
|
+
ecPublicKey: '1.2.840.10045.2.1',
|
|
29
|
+
ed25519: '1.3.101.112',
|
|
30
|
+
ed448: '1.3.101.113',
|
|
31
|
+
// named curves
|
|
32
|
+
secp256r1: '1.2.840.10045.3.1.7',
|
|
33
|
+
secp384r1: '1.3.132.0.34',
|
|
34
|
+
secp521r1: '1.3.132.0.35',
|
|
35
|
+
// signature algorithms
|
|
36
|
+
sha256WithRsa: '1.2.840.113549.1.1.11',
|
|
37
|
+
sha384WithRsa: '1.2.840.113549.1.1.12',
|
|
38
|
+
sha512WithRsa: '1.2.840.113549.1.1.13',
|
|
39
|
+
ecdsaWithSha256: '1.2.840.10045.4.3.2',
|
|
40
|
+
ecdsaWithSha384: '1.2.840.10045.4.3.3',
|
|
41
|
+
ecdsaWithSha512: '1.2.840.10045.4.3.4',
|
|
42
|
+
// digests (referenced by RSA-PSS parameters)
|
|
43
|
+
sha1: '1.3.14.3.2.26',
|
|
44
|
+
sha256: '2.16.840.1.101.3.4.2.1',
|
|
45
|
+
sha384: '2.16.840.1.101.3.4.2.2',
|
|
46
|
+
sha512: '2.16.840.1.101.3.4.2.3',
|
|
47
|
+
mgf1: '1.2.840.113549.1.1.8',
|
|
48
|
+
// extensions
|
|
49
|
+
subjectKeyIdentifier: '2.5.29.14',
|
|
50
|
+
keyUsage: '2.5.29.15',
|
|
51
|
+
subjectAltName: '2.5.29.17',
|
|
52
|
+
issuerAltName: '2.5.29.18',
|
|
53
|
+
basicConstraints: '2.5.29.19',
|
|
54
|
+
nameConstraints: '2.5.29.30',
|
|
55
|
+
crlDistributionPoints: '2.5.29.31',
|
|
56
|
+
certificatePolicies: '2.5.29.32',
|
|
57
|
+
authorityKeyIdentifier: '2.5.29.35',
|
|
58
|
+
extendedKeyUsage: '2.5.29.37',
|
|
59
|
+
freshestCrl: '2.5.29.46',
|
|
60
|
+
authorityInfoAccess: '1.3.6.1.5.5.7.1.1',
|
|
61
|
+
subjectInfoAccess: '1.3.6.1.5.5.7.1.11',
|
|
62
|
+
sctList: '1.3.6.1.4.1.11129.2.4.2',
|
|
63
|
+
// extended key usage members
|
|
64
|
+
serverAuth: '1.3.6.1.5.5.7.3.1',
|
|
65
|
+
clientAuth: '1.3.6.1.5.5.7.3.2',
|
|
66
|
+
ocspSigning: '1.3.6.1.5.5.7.3.9',
|
|
67
|
+
anyExtendedKeyUsage: '2.5.29.37.0',
|
|
68
|
+
// OCSP (RFC 6960)
|
|
69
|
+
ocspBasic: '1.3.6.1.5.5.7.48.1.1',
|
|
70
|
+
ocspNonce: '1.3.6.1.5.5.7.48.1.2',
|
|
71
|
+
ocspNocheck: '1.3.6.1.5.5.7.48.1.5',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Signature algorithms rejected outright: collisions are practical or near-practical. */
|
|
75
|
+
const WEAK_SIG_OIDS = {
|
|
76
|
+
'1.2.840.113549.1.1.2': 'md2WithRSAEncryption',
|
|
77
|
+
'1.2.840.113549.1.1.3': 'md4WithRSAEncryption',
|
|
78
|
+
'1.2.840.113549.1.1.4': 'md5WithRSAEncryption',
|
|
79
|
+
'1.2.840.113549.1.1.5': 'sha1WithRSAEncryption',
|
|
80
|
+
'1.3.14.3.2.29': 'sha1WithRSAEncryption (OIW)',
|
|
81
|
+
'1.2.840.10045.4.1': 'ecdsa-with-SHA1',
|
|
82
|
+
'1.2.840.10040.4.3': 'dsa-with-sha1',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extensions this validator understands, or has deliberately judged safe to leave unprocessed
|
|
87
|
+
* even when marked critical. Everything else that is critical causes rejection in path.js
|
|
88
|
+
* (RFC 5280 s6.1: a relying party MUST reject on unrecognised critical extensions — ignoring
|
|
89
|
+
* them is the classic fail-open).
|
|
90
|
+
*
|
|
91
|
+
* certificatePolicies is listed because with no required policy set, RFC 5280 policy processing
|
|
92
|
+
* cannot fail; policyConstraints / inhibitAnyPolicy are deliberately NOT listed, because they
|
|
93
|
+
* make policy processing mandatory and we do not implement it — they are always critical, so
|
|
94
|
+
* their presence in a path rejects it.
|
|
95
|
+
*/
|
|
96
|
+
export const KNOWN_EXTENSIONS = new Set([
|
|
97
|
+
OID.subjectKeyIdentifier, OID.keyUsage, OID.subjectAltName, OID.issuerAltName,
|
|
98
|
+
OID.basicConstraints, OID.nameConstraints, OID.crlDistributionPoints, OID.certificatePolicies,
|
|
99
|
+
OID.authorityKeyIdentifier, OID.extendedKeyUsage, OID.freshestCrl,
|
|
100
|
+
OID.authorityInfoAccess, OID.subjectInfoAccess, OID.sctList,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
// ------------------------------------------------------------------ names
|
|
104
|
+
|
|
105
|
+
const ATTR_NAME = {
|
|
106
|
+
'2.5.4.3': 'CN', '2.5.4.4': 'SN', '2.5.4.5': 'serialNumber', '2.5.4.6': 'C', '2.5.4.7': 'L',
|
|
107
|
+
'2.5.4.8': 'ST', '2.5.4.9': 'street', '2.5.4.10': 'O', '2.5.4.11': 'OU', '2.5.4.15': 'businessCategory',
|
|
108
|
+
'2.5.4.17': 'postalCode', '2.5.4.42': 'GN', '2.5.4.97': 'organizationIdentifier',
|
|
109
|
+
'1.2.840.113549.1.9.1': 'emailAddress', '0.9.2342.19200300.100.1.1': 'UID',
|
|
110
|
+
'0.9.2342.19200300.100.1.25': 'DC',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A parsed X.500 Name. `bytes` is the exact DER of the whole Name — the canonical identity for
|
|
115
|
+
* every comparison; `rdns` and `text` exist for constraint checks and log lines respectively.
|
|
116
|
+
* @typedef {object} DistinguishedName
|
|
117
|
+
* @property {Uint8Array} bytes
|
|
118
|
+
* @property {Array<Array<{ oid: string, value: string }>>} rdns one array per RDN, in order;
|
|
119
|
+
* non-string attribute values are rendered as '#hex'
|
|
120
|
+
* @property {string} text human-readable 'CN=..., O=...' form
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Name ::= RDNSequence ::= SEQUENCE OF RelativeDistinguishedName (SET OF AttributeTypeAndValue).
|
|
125
|
+
*
|
|
126
|
+
* `bytes` is the exact DER of the whole Name and is the canonical form used for all issuer ==
|
|
127
|
+
* subject comparisons. RFC 5280 s7.1 also allows caseIgnore/whitespace-folded matching, but a CA
|
|
128
|
+
* that spells its own name two different ways between certificates breaks every deployed
|
|
129
|
+
* validator that matters (they compare bytes too), and a lax comparator is one more place to
|
|
130
|
+
* confuse two names. Exact bytes, fail closed.
|
|
131
|
+
*
|
|
132
|
+
* @param {Uint8Array} bytes
|
|
133
|
+
* @param {import('./der.js').Tlv} tlv
|
|
134
|
+
* @returns {DistinguishedName}
|
|
135
|
+
*/
|
|
136
|
+
export function parseName(bytes, tlv) {
|
|
137
|
+
expectTlv(tlv, { tag: TAG.SEQUENCE, constructed: true }, 'Name');
|
|
138
|
+
const rdns = [];
|
|
139
|
+
const parts = [];
|
|
140
|
+
for (const rdn of children(bytes, tlv, 'Name')) {
|
|
141
|
+
expectTlv(rdn, { tag: TAG.SET, constructed: true }, 'RelativeDistinguishedName');
|
|
142
|
+
const avas = [];
|
|
143
|
+
const kids = children(bytes, rdn, 'RelativeDistinguishedName');
|
|
144
|
+
if (kids.length === 0) throw parseError(rdn.start, 'empty RelativeDistinguishedName');
|
|
145
|
+
for (const ava of kids) {
|
|
146
|
+
expectTlv(ava, { tag: TAG.SEQUENCE, constructed: true }, 'AttributeTypeAndValue');
|
|
147
|
+
const [typeTlv, valueTlv, ...extra] = children(bytes, ava, 'AttributeTypeAndValue');
|
|
148
|
+
if (!typeTlv || !valueTlv || extra.length) {
|
|
149
|
+
throw parseError(ava.start, 'AttributeTypeAndValue must be exactly { type, value }');
|
|
150
|
+
}
|
|
151
|
+
const oid = readOid(bytes, typeTlv, 'attribute type');
|
|
152
|
+
let value = null;
|
|
153
|
+
try {
|
|
154
|
+
value = readString(bytes, valueTlv, `attribute ${oid}`);
|
|
155
|
+
} catch {
|
|
156
|
+
// Non-string attribute values are legal (rare); render as hex rather than fail a
|
|
157
|
+
// certificate over an attribute we only ever display.
|
|
158
|
+
value = `#${toHex(content(bytes, valueTlv))}`;
|
|
159
|
+
}
|
|
160
|
+
avas.push({ oid, value });
|
|
161
|
+
parts.push(`${ATTR_NAME[oid] ?? oid}=${value}`);
|
|
162
|
+
}
|
|
163
|
+
rdns.push(avas);
|
|
164
|
+
}
|
|
165
|
+
return { bytes: element(bytes, tlv), rdns, text: parts.join(', ') || '<empty name>' };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ------------------------------------------------------------------ GeneralName
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* GeneralName (RFC 5280 s4.2.1.6), used by subjectAltName and nameConstraints. `ipLens` differs
|
|
172
|
+
* by context: SAN carries bare addresses (4/16), name constraints carry address+mask (8/32).
|
|
173
|
+
*/
|
|
174
|
+
function parseGeneralName(bytes, tlv, ipLens, what) {
|
|
175
|
+
if (tlv.cls !== CLS.CONTEXT) {
|
|
176
|
+
throw parseError(tlv.start,
|
|
177
|
+
`expected context-tagged GeneralName in ${what}, got ${tagName(tlv.cls, tlv.tag)}`);
|
|
178
|
+
}
|
|
179
|
+
const c = content(bytes, tlv);
|
|
180
|
+
switch (tlv.tag) {
|
|
181
|
+
case 1: // rfc822Name, IA5String
|
|
182
|
+
case 2: // dNSName, IA5String
|
|
183
|
+
case 6: { // uniformResourceIdentifier, IA5String
|
|
184
|
+
let s = '';
|
|
185
|
+
for (let i = 0; i < c.byteLength; i++) {
|
|
186
|
+
const b = c[i];
|
|
187
|
+
// An embedded NUL is the null-prefix attack ("example.com\0.evil.test"): parsers that
|
|
188
|
+
// stop at NUL and parsers that do not will disagree about this name. Never tolerated.
|
|
189
|
+
if (b === 0x00) throw parseError(tlv.contentStart + i, `NUL byte in ${what} GeneralName`);
|
|
190
|
+
if (b > 0x7f) {
|
|
191
|
+
throw parseError(tlv.contentStart + i, `non-ASCII byte 0x${b.toString(16)} in ${what} IA5String`);
|
|
192
|
+
}
|
|
193
|
+
s += String.fromCharCode(b);
|
|
194
|
+
}
|
|
195
|
+
return { type: tlv.tag === 1 ? 'email' : tlv.tag === 2 ? 'dns' : 'uri', value: s };
|
|
196
|
+
}
|
|
197
|
+
case 7: { // iPAddress, OCTET STRING
|
|
198
|
+
if (!ipLens.includes(c.byteLength)) {
|
|
199
|
+
throw parseError(
|
|
200
|
+
tlv.start,
|
|
201
|
+
`iPAddress in ${what} must be ${ipLens.join(' or ')} bytes, got ${c.byteLength}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return { type: 'ip', bytes: c };
|
|
205
|
+
}
|
|
206
|
+
case 4: // directoryName (EXPLICIT Name)
|
|
207
|
+
return { type: 'dirName', bytes: c };
|
|
208
|
+
default:
|
|
209
|
+
// otherName [0], x400Address [3], ediPartyName [5], registeredID [8]: recorded so that
|
|
210
|
+
// name-constraint processing can refuse to ignore them when the extension is critical.
|
|
211
|
+
return { type: 'other', tag: tlv.tag };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ------------------------------------------------------------------ extensions
|
|
216
|
+
|
|
217
|
+
function parseBasicConstraints(valueBytes) {
|
|
218
|
+
const seq = readSequence(valueBytes, 0, 'BasicConstraints');
|
|
219
|
+
if (seq.end !== valueBytes.byteLength) throw parseError(seq.end, 'trailing bytes in BasicConstraints');
|
|
220
|
+
const kids = children(valueBytes, seq, 'BasicConstraints');
|
|
221
|
+
let i = 0;
|
|
222
|
+
let ca = false;
|
|
223
|
+
if (i < kids.length && kids[i].tag === TAG.BOOLEAN && kids[i].cls === CLS.UNIVERSAL) {
|
|
224
|
+
ca = readBoolean(valueBytes, kids[i], 'cA');
|
|
225
|
+
// DER forbids encoding a DEFAULT value; cA FALSE spelled out is a second encoding of the
|
|
226
|
+
// same meaning, which is exactly the ambiguity class this parser exists to refuse.
|
|
227
|
+
if (!ca) throw parseError(kids[i].start, 'cA FALSE must be omitted in DER');
|
|
228
|
+
i++;
|
|
229
|
+
}
|
|
230
|
+
let pathLenConstraint = null;
|
|
231
|
+
if (i < kids.length) {
|
|
232
|
+
const { value, negative } = readInteger(valueBytes, kids[i], 'pathLenConstraint');
|
|
233
|
+
if (negative || value === null) {
|
|
234
|
+
throw parseError(kids[i].start, 'pathLenConstraint must be a small non-negative integer');
|
|
235
|
+
}
|
|
236
|
+
if (!ca) {
|
|
237
|
+
// RFC 5280 s4.2.1.9: pathLenConstraint is only meaningful with cA=TRUE. A constraint on a
|
|
238
|
+
// non-CA is self-contradictory; guessing which half to believe would be failing open.
|
|
239
|
+
throw parseError(kids[i].start, 'pathLenConstraint present without cA TRUE');
|
|
240
|
+
}
|
|
241
|
+
pathLenConstraint = value;
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
if (i !== kids.length) throw parseError(kids[i].start, 'unexpected extra field in BasicConstraints');
|
|
245
|
+
return { present: true, ca, pathLenConstraint };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const KEY_USAGE_BITS = [
|
|
249
|
+
'digitalSignature', 'nonRepudiation', 'keyEncipherment', 'dataEncipherment',
|
|
250
|
+
'keyAgreement', 'keyCertSign', 'cRLSign', 'encipherOnly', 'decipherOnly',
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The nine RFC 5280 s4.2.1.3 bits, each explicit so a validator reads `false`, never
|
|
255
|
+
* `undefined` — an absent bit and an unset bit must be indistinguishable.
|
|
256
|
+
* @typedef {object} KeyUsage
|
|
257
|
+
* @property {boolean} digitalSignature
|
|
258
|
+
* @property {boolean} nonRepudiation
|
|
259
|
+
* @property {boolean} keyEncipherment
|
|
260
|
+
* @property {boolean} dataEncipherment
|
|
261
|
+
* @property {boolean} keyAgreement
|
|
262
|
+
* @property {boolean} keyCertSign
|
|
263
|
+
* @property {boolean} cRLSign
|
|
264
|
+
* @property {boolean} encipherOnly
|
|
265
|
+
* @property {boolean} decipherOnly
|
|
266
|
+
*/
|
|
267
|
+
|
|
268
|
+
/** @returns {KeyUsage} */
|
|
269
|
+
function parseKeyUsage(valueBytes) {
|
|
270
|
+
const tlv = readAll(valueBytes, 'KeyUsage');
|
|
271
|
+
const { bytes: bits, unusedBits } = readBitString(valueBytes, tlv, 'KeyUsage');
|
|
272
|
+
const usage = {};
|
|
273
|
+
let any = false;
|
|
274
|
+
for (let i = 0; i < KEY_USAGE_BITS.length; i++) {
|
|
275
|
+
const byte = i >> 3;
|
|
276
|
+
const set =
|
|
277
|
+
byte < bits.byteLength &&
|
|
278
|
+
(byte < bits.byteLength - 1 || (7 - (i & 7)) >= unusedBits) &&
|
|
279
|
+
(bits[byte] & (0x80 >> (i & 7))) !== 0;
|
|
280
|
+
usage[KEY_USAGE_BITS[i]] = set;
|
|
281
|
+
any = any || set;
|
|
282
|
+
}
|
|
283
|
+
// RFC 5280 s4.2.1.3: when the extension appears, at least one bit MUST be set. An all-zero
|
|
284
|
+
// keyUsage asserts "this key may do nothing", which cannot coexist with using the key.
|
|
285
|
+
if (!any) throw parseError(tlv.start, 'KeyUsage extension with no bits set');
|
|
286
|
+
return Object.freeze(usage);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function parseExtendedKeyUsage(valueBytes) {
|
|
290
|
+
const seq = readSequence(valueBytes, 0, 'ExtKeyUsageSyntax');
|
|
291
|
+
if (seq.end !== valueBytes.byteLength) throw parseError(seq.end, 'trailing bytes in ExtendedKeyUsage');
|
|
292
|
+
const kids = children(valueBytes, seq, 'ExtKeyUsageSyntax');
|
|
293
|
+
if (kids.length === 0) throw parseError(seq.start, 'empty ExtendedKeyUsage');
|
|
294
|
+
return Object.freeze(kids.map((k) => readOid(valueBytes, k, 'KeyPurposeId')));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The SAN entries identity matching consults. `present` distinguishes "no SAN extension"
|
|
299
|
+
* (matches nothing, by policy) from "SAN with no entries of this type".
|
|
300
|
+
* @typedef {object} SubjectAltNames
|
|
301
|
+
* @property {boolean} present
|
|
302
|
+
* @property {ReadonlyArray<string>} dns
|
|
303
|
+
* @property {ReadonlyArray<Uint8Array>} ip raw 4- or 16-byte addresses
|
|
304
|
+
* @property {ReadonlyArray<string>} uri
|
|
305
|
+
* @property {ReadonlyArray<string>} email
|
|
306
|
+
*/
|
|
307
|
+
|
|
308
|
+
/** @returns {SubjectAltNames} */
|
|
309
|
+
function parseSubjectAltName(valueBytes) {
|
|
310
|
+
const seq = readSequence(valueBytes, 0, 'GeneralNames');
|
|
311
|
+
if (seq.end !== valueBytes.byteLength) throw parseError(seq.end, 'trailing bytes in SubjectAltName');
|
|
312
|
+
const kids = children(valueBytes, seq, 'GeneralNames');
|
|
313
|
+
// RFC 5280 s4.2.1.6: if present, the SAN sequence MUST contain at least one entry.
|
|
314
|
+
if (kids.length === 0) throw parseError(seq.start, 'empty SubjectAltName');
|
|
315
|
+
const dns = [];
|
|
316
|
+
const ip = [];
|
|
317
|
+
const uri = [];
|
|
318
|
+
const email = [];
|
|
319
|
+
for (const k of kids) {
|
|
320
|
+
const gn = parseGeneralName(valueBytes, k, [4, 16], 'subjectAltName');
|
|
321
|
+
if (gn.type === 'dns') dns.push(gn.value);
|
|
322
|
+
else if (gn.type === 'ip') ip.push(gn.bytes);
|
|
323
|
+
else if (gn.type === 'uri') uri.push(gn.value);
|
|
324
|
+
else if (gn.type === 'email') email.push(gn.value);
|
|
325
|
+
// dirName / other entries are legal; nothing in this package matches on them.
|
|
326
|
+
}
|
|
327
|
+
return Object.freeze({ present: true, dns: Object.freeze(dns), ip: Object.freeze(ip),
|
|
328
|
+
uri: Object.freeze(uri), email: Object.freeze(email) });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseSubjectKeyIdentifier(valueBytes) {
|
|
332
|
+
const tlv = readAll(valueBytes, 'SubjectKeyIdentifier');
|
|
333
|
+
expectTlv(tlv, { tag: TAG.OCTET_STRING, constructed: false }, 'SubjectKeyIdentifier');
|
|
334
|
+
return content(valueBytes, tlv);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function parseAuthorityKeyIdentifier(valueBytes) {
|
|
338
|
+
const seq = readSequence(valueBytes, 0, 'AuthorityKeyIdentifier');
|
|
339
|
+
if (seq.end !== valueBytes.byteLength) {
|
|
340
|
+
throw parseError(seq.end, 'trailing bytes in AuthorityKeyIdentifier');
|
|
341
|
+
}
|
|
342
|
+
let keyId = null;
|
|
343
|
+
for (const k of children(valueBytes, seq, 'AuthorityKeyIdentifier')) {
|
|
344
|
+
if (k.cls !== CLS.CONTEXT) {
|
|
345
|
+
throw parseError(k.start, 'AuthorityKeyIdentifier fields must be context-tagged');
|
|
346
|
+
}
|
|
347
|
+
if (k.tag === 0) keyId = content(valueBytes, k); // [0] IMPLICIT KeyIdentifier
|
|
348
|
+
// [1] authorityCertIssuer / [2] authorityCertSerialNumber: parsed past, not used — chain
|
|
349
|
+
// building matches on DN plus keyIdentifier, which is what CAs actually populate.
|
|
350
|
+
}
|
|
351
|
+
return keyId;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* One GeneralSubtree, reduced to what constraint enforcement can act on. 'other' entries are
|
|
356
|
+
* forms this validator cannot enforce; path.js rejects the path when a critical extension
|
|
357
|
+
* carries one, which is why they are preserved rather than dropped.
|
|
358
|
+
* @typedef {{ type: 'dns', value: string } | { type: 'email', value: string }
|
|
359
|
+
* | { type: 'uri', value: string } | { type: 'ip', addr: Uint8Array, mask: Uint8Array }
|
|
360
|
+
* | { type: 'other', tag: number }} NameConstraintSubtree
|
|
361
|
+
*/
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* @typedef {object} NameConstraints
|
|
365
|
+
* @property {ReadonlyArray<NameConstraintSubtree> | null} permitted
|
|
366
|
+
* @property {ReadonlyArray<NameConstraintSubtree> | null} excluded
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* NameConstraints (RFC 5280 s4.2.1.10). Subtrees we cannot enforce are preserved as
|
|
371
|
+
* `{type:'other'}` entries so path.js can refuse to ignore them when the extension is critical.
|
|
372
|
+
* A GeneralSubtree with minimum != 0 or maximum present is demoted to unsupported for the same
|
|
373
|
+
* reason: RFC 5280 forbids them, and enforcing a constraint we cannot interpret is worse than
|
|
374
|
+
* rejecting.
|
|
375
|
+
*
|
|
376
|
+
* @param {Uint8Array} valueBytes the extnValue content
|
|
377
|
+
* @returns {NameConstraints}
|
|
378
|
+
*/
|
|
379
|
+
export function parseNameConstraints(valueBytes) {
|
|
380
|
+
const seq = readSequence(valueBytes, 0, 'NameConstraints');
|
|
381
|
+
if (seq.end !== valueBytes.byteLength) throw parseError(seq.end, 'trailing bytes in NameConstraints');
|
|
382
|
+
const kids = children(valueBytes, seq, 'NameConstraints');
|
|
383
|
+
if (kids.length === 0) {
|
|
384
|
+
throw parseError(seq.start, 'NameConstraints with neither permitted nor excluded subtrees');
|
|
385
|
+
}
|
|
386
|
+
const out = { permitted: null, excluded: null };
|
|
387
|
+
for (const k of kids) {
|
|
388
|
+
if (k.cls !== CLS.CONTEXT || (k.tag !== 0 && k.tag !== 1) || !k.constructed) {
|
|
389
|
+
throw parseError(k.start, 'NameConstraints fields must be [0] or [1] GeneralSubtrees');
|
|
390
|
+
}
|
|
391
|
+
const which = k.tag === 0 ? 'permitted' : 'excluded';
|
|
392
|
+
if (out[which] !== null) throw parseError(k.start, `duplicate ${which}Subtrees`);
|
|
393
|
+
const subtrees = [];
|
|
394
|
+
const trees = children(valueBytes, k, 'GeneralSubtrees');
|
|
395
|
+
if (trees.length === 0) throw parseError(k.start, `empty ${which}Subtrees`);
|
|
396
|
+
for (const t of trees) {
|
|
397
|
+
expectTlv(t, { tag: TAG.SEQUENCE, constructed: true }, 'GeneralSubtree');
|
|
398
|
+
const fields = children(valueBytes, t, 'GeneralSubtree');
|
|
399
|
+
if (fields.length === 0) throw parseError(t.start, 'empty GeneralSubtree');
|
|
400
|
+
const gn = parseGeneralName(valueBytes, fields[0], [8, 32], 'nameConstraints');
|
|
401
|
+
const boundsOk = fields.length === 1; // minimum defaults to 0, maximum must be absent
|
|
402
|
+
if (!boundsOk || gn.type === 'dirName') {
|
|
403
|
+
subtrees.push({ type: 'other', tag: fields[0].tag });
|
|
404
|
+
} else if (gn.type === 'ip') {
|
|
405
|
+
const half = gn.bytes.byteLength / 2;
|
|
406
|
+
subtrees.push({ type: 'ip', addr: gn.bytes.subarray(0, half), mask: gn.bytes.subarray(half) });
|
|
407
|
+
} else {
|
|
408
|
+
subtrees.push(gn); // dns / email / uri / other
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
out[which] = Object.freeze(subtrees);
|
|
412
|
+
}
|
|
413
|
+
return Object.freeze(out);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ------------------------------------------------------------------ algorithm identifiers
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters ANY OPTIONAL }.
|
|
420
|
+
*
|
|
421
|
+
* Exported for the OCSP checker, which meets the same structure in BasicOCSPResponse and CertID
|
|
422
|
+
* and must read it with the same strictness rather than a second, slightly different walk.
|
|
423
|
+
*
|
|
424
|
+
* @param {Uint8Array} bytes
|
|
425
|
+
* @param {import('./der.js').Tlv} tlv
|
|
426
|
+
* @param {string} what
|
|
427
|
+
* @returns {AlgorithmId}
|
|
428
|
+
*/
|
|
429
|
+
export function parseAlgorithmIdentifier(bytes, tlv, what) {
|
|
430
|
+
expectTlv(tlv, { tag: TAG.SEQUENCE, constructed: true }, what);
|
|
431
|
+
const kids = children(bytes, tlv, what);
|
|
432
|
+
if (kids.length < 1 || kids.length > 2) {
|
|
433
|
+
throw parseError(tlv.start, `${what} must be { algorithm, parameters? }`);
|
|
434
|
+
}
|
|
435
|
+
const oid = readOid(bytes, kids[0], `${what} algorithm`);
|
|
436
|
+
const params = kids.length === 2 ? kids[1] : null;
|
|
437
|
+
return {
|
|
438
|
+
oid,
|
|
439
|
+
paramsTlv: params,
|
|
440
|
+
paramsBytes: params ? element(bytes, params) : null,
|
|
441
|
+
bytes: element(bytes, tlv),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* RSASSA-PSS-params (RFC 4055). All fields are EXPLICIT-tagged with SHA-1 defaults, so an absent
|
|
447
|
+
* field *means* SHA-1 — which is why "assume the modern default" is not an option here and every
|
|
448
|
+
* field is resolved before the algorithm is accepted.
|
|
449
|
+
*/
|
|
450
|
+
function parsePssParams(algo) {
|
|
451
|
+
const empty = { hashOid: OID.sha1, mgfHashOid: OID.sha1, saltLength: 20, trailer: 1 };
|
|
452
|
+
if (!algo.paramsTlv) return empty;
|
|
453
|
+
const bytes = algo.paramsBytes;
|
|
454
|
+
const seq = readAll(bytes, 'RSASSA-PSS-params');
|
|
455
|
+
expectTlv(seq, { tag: TAG.SEQUENCE, constructed: true }, 'RSASSA-PSS-params');
|
|
456
|
+
const out = { ...empty };
|
|
457
|
+
for (const field of children(bytes, seq, 'RSASSA-PSS-params')) {
|
|
458
|
+
if (field.cls !== CLS.CONTEXT || !field.constructed) {
|
|
459
|
+
throw parseError(field.start, 'RSASSA-PSS-params fields must be explicit context tags');
|
|
460
|
+
}
|
|
461
|
+
const inner = readTlv(bytes, field.contentStart);
|
|
462
|
+
if (inner.end !== field.contentEnd) throw parseError(inner.end, 'trailing bytes in PSS parameter');
|
|
463
|
+
switch (field.tag) {
|
|
464
|
+
case 0:
|
|
465
|
+
out.hashOid = parseAlgorithmIdentifier(bytes, inner, 'PSS hashAlgorithm').oid;
|
|
466
|
+
break;
|
|
467
|
+
case 1: {
|
|
468
|
+
const mgf = parseAlgorithmIdentifier(bytes, inner, 'PSS maskGenAlgorithm');
|
|
469
|
+
if (mgf.oid !== OID.mgf1 || !mgf.paramsTlv) {
|
|
470
|
+
throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
|
|
471
|
+
`RSA-PSS mask generation function ${mgf.oid} is not MGF1, refusing`, { oid: mgf.oid });
|
|
472
|
+
}
|
|
473
|
+
out.mgfHashOid = parseAlgorithmIdentifier(bytes, mgf.paramsTlv, 'MGF1 hash').oid;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case 2: {
|
|
477
|
+
const { value, negative } = readInteger(bytes, inner, 'PSS saltLength');
|
|
478
|
+
if (negative || value === null) throw parseError(inner.start, 'PSS saltLength out of range');
|
|
479
|
+
out.saltLength = value;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case 3: {
|
|
483
|
+
const { value } = readInteger(bytes, inner, 'PSS trailerField');
|
|
484
|
+
out.trailer = value;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
default:
|
|
488
|
+
throw parseError(field.start, `unknown RSASSA-PSS-params field [${field.tag}]`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const PSS_BY_HASH = {
|
|
495
|
+
[OID.sha256]: { scheme: SIG_SCHEME.rsa_pss_rsae_sha256, hash: 'SHA-256', saltLength: 32 },
|
|
496
|
+
[OID.sha384]: { scheme: SIG_SCHEME.rsa_pss_rsae_sha384, hash: 'SHA-384', saltLength: 48 },
|
|
497
|
+
[OID.sha512]: { scheme: SIG_SCHEME.rsa_pss_rsae_sha512, hash: 'SHA-512', saltLength: 64 },
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const DIRECT_SCHEMES = {
|
|
501
|
+
[OID.sha256WithRsa]: { kind: 'rsa-pkcs1', scheme: SIG_SCHEME.rsa_pkcs1_sha256, name: 'rsa_pkcs1_sha256' },
|
|
502
|
+
[OID.sha384WithRsa]: { kind: 'rsa-pkcs1', scheme: SIG_SCHEME.rsa_pkcs1_sha384, name: 'rsa_pkcs1_sha384' },
|
|
503
|
+
[OID.sha512WithRsa]: { kind: 'rsa-pkcs1', scheme: SIG_SCHEME.rsa_pkcs1_sha512, name: 'rsa_pkcs1_sha512' },
|
|
504
|
+
[OID.ed25519]: { kind: 'ed25519', scheme: SIG_SCHEME.ed25519, name: 'ed25519' },
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const ECDSA_HASH = {
|
|
508
|
+
[OID.ecdsaWithSha256]: 'SHA-256',
|
|
509
|
+
[OID.ecdsaWithSha384]: 'SHA-384',
|
|
510
|
+
[OID.ecdsaWithSha512]: 'SHA-512',
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* How to verify one certificate signature. `scheme` indexes SIG_SCHEME_PARAMS where the OID
|
|
515
|
+
* fully determines it; for ECDSA only the hash is known here and path.js completes the plan
|
|
516
|
+
* from the issuer's curve.
|
|
517
|
+
* @typedef {object} SignaturePlan
|
|
518
|
+
* @property {'rsa-pkcs1' | 'rsa-pss' | 'ecdsa' | 'ed25519'} kind
|
|
519
|
+
* @property {number} [scheme]
|
|
520
|
+
* @property {'SHA-256' | 'SHA-384' | 'SHA-512'} [hash] weaker hashes died in the OID check
|
|
521
|
+
* @property {string} name for error messages
|
|
522
|
+
*/
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Map a certificate's signature algorithm to a verification plan, or throw.
|
|
526
|
+
*
|
|
527
|
+
* Called by path.js exactly when a certificate's signature is about to anchor trust, and by the
|
|
528
|
+
* OCSP checker for a response's own signature — the parameter is therefore the structural subset
|
|
529
|
+
* both can supply, and a full Certificate qualifies as-is. Weak algorithms (MD2/MD4/MD5, SHA-1)
|
|
530
|
+
* are rejected here by OID, before any cryptography runs — some runtimes' verifiers still accept
|
|
531
|
+
* SHA-1 and this one must provably not be among them. ECDSA returns only the hash: in X.509
|
|
532
|
+
* (unlike TLS) the curve belongs to the issuer's key, so path.js completes the plan from the
|
|
533
|
+
* issuer's SPKI.
|
|
534
|
+
*
|
|
535
|
+
* @param {{ signatureAlgorithm: AlgorithmId, subject: { text: string } }} cert
|
|
536
|
+
* @returns {SignaturePlan}
|
|
537
|
+
*/
|
|
538
|
+
export function resolveSignatureScheme(cert) {
|
|
539
|
+
const { oid } = cert.signatureAlgorithm;
|
|
540
|
+
const weak = WEAK_SIG_OIDS[oid];
|
|
541
|
+
if (weak) {
|
|
542
|
+
throw new CertificateError(codes.CERT_SIGNATURE_WEAK,
|
|
543
|
+
`certificate "${cert.subject.text}" is signed with ${weak} (${oid}); ` +
|
|
544
|
+
'MD-family and SHA-1 signatures are refused',
|
|
545
|
+
{ oid, algorithm: weak, subject: cert.subject.text });
|
|
546
|
+
}
|
|
547
|
+
const direct = DIRECT_SCHEMES[oid];
|
|
548
|
+
if (direct) return direct;
|
|
549
|
+
const ecdsaHash = ECDSA_HASH[oid];
|
|
550
|
+
if (ecdsaHash) return { kind: 'ecdsa', hash: ecdsaHash, name: `ecdsa-with-${ecdsaHash}` };
|
|
551
|
+
if (oid === OID.rsassaPss) {
|
|
552
|
+
const p = parsePssParams(cert.signatureAlgorithm);
|
|
553
|
+
if (p.hashOid === OID.sha1 || p.mgfHashOid === OID.sha1) {
|
|
554
|
+
throw new CertificateError(codes.CERT_SIGNATURE_WEAK,
|
|
555
|
+
`certificate "${cert.subject.text}" uses RSA-PSS with SHA-1 ` +
|
|
556
|
+
`(hash ${p.hashOid}, MGF1 hash ${p.mgfHashOid}); SHA-1 signatures are refused`,
|
|
557
|
+
{ oid, subject: cert.subject.text });
|
|
558
|
+
}
|
|
559
|
+
const entry = PSS_BY_HASH[p.hashOid];
|
|
560
|
+
if (!entry) {
|
|
561
|
+
throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
|
|
562
|
+
`RSA-PSS hash ${p.hashOid} on "${cert.subject.text}" is not supported`, { oid: p.hashOid });
|
|
563
|
+
}
|
|
564
|
+
// RFC 4055 consistency: MGF1 must run the same hash, the salt must be one hash-length, and
|
|
565
|
+
// the trailer must be 0xBC. Every real PSS certificate satisfies this; the exceptions are
|
|
566
|
+
// parameter-confusion experiments we refuse rather than approximate.
|
|
567
|
+
if (p.mgfHashOid !== p.hashOid) {
|
|
568
|
+
throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
|
|
569
|
+
`RSA-PSS with MGF1 hash ${p.mgfHashOid} != message hash ${p.hashOid}, refusing`,
|
|
570
|
+
{ hash: p.hashOid, mgfHash: p.mgfHashOid });
|
|
571
|
+
}
|
|
572
|
+
if (p.saltLength !== entry.saltLength) {
|
|
573
|
+
throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
|
|
574
|
+
`RSA-PSS salt length ${p.saltLength} != hash length ${entry.saltLength}, refusing`,
|
|
575
|
+
{ saltLength: p.saltLength, expected: entry.saltLength });
|
|
576
|
+
}
|
|
577
|
+
if (p.trailer !== 1) {
|
|
578
|
+
throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
|
|
579
|
+
`RSA-PSS trailer field ${p.trailer} != 1, refusing`, { trailer: p.trailer });
|
|
580
|
+
}
|
|
581
|
+
return { kind: 'rsa-pss', scheme: entry.scheme, hash: entry.hash, name: `rsa_pss_${entry.hash}` };
|
|
582
|
+
}
|
|
583
|
+
throw new CertificateError(codes.CERT_SIGNATURE_UNSUPPORTED,
|
|
584
|
+
`signature algorithm ${oid} on "${cert.subject.text}" is not supported`,
|
|
585
|
+
{ oid, subject: cert.subject.text });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ------------------------------------------------------------------ SPKI
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* A parsed SubjectPublicKeyInfo. `spkiDer` is the exact original element — the bytes WebCrypto
|
|
592
|
+
* imports and the bytes SPKI pinning hashes, so it must never be a re-encoding.
|
|
593
|
+
* @typedef {object} Spki
|
|
594
|
+
* @property {string} algorithmOid
|
|
595
|
+
* @property {string | null} curveOid named curve, EC keys only
|
|
596
|
+
* @property {Uint8Array} keyBytes the subjectPublicKey payload
|
|
597
|
+
* @property {Uint8Array} spkiDer
|
|
598
|
+
*/
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* @param {Uint8Array} bytes
|
|
602
|
+
* @param {import('./der.js').Tlv} tlv
|
|
603
|
+
* @returns {Spki}
|
|
604
|
+
*/
|
|
605
|
+
function parseSpki(bytes, tlv) {
|
|
606
|
+
expectTlv(tlv, { tag: TAG.SEQUENCE, constructed: true }, 'SubjectPublicKeyInfo');
|
|
607
|
+
const kids = children(bytes, tlv, 'SubjectPublicKeyInfo');
|
|
608
|
+
if (kids.length !== 2) {
|
|
609
|
+
throw parseError(tlv.start, 'SubjectPublicKeyInfo must be { algorithm, subjectPublicKey }');
|
|
610
|
+
}
|
|
611
|
+
const algo = parseAlgorithmIdentifier(bytes, kids[0], 'SPKI algorithm');
|
|
612
|
+
const { unusedBits, bytes: keyBytes } = readBitString(bytes, kids[1], 'subjectPublicKey');
|
|
613
|
+
// Every key type this package can use (RSA, EC points, Ed25519) is a whole number of octets.
|
|
614
|
+
if (unusedBits !== 0) throw parseError(kids[1].start, `subjectPublicKey has ${unusedBits} unused bits`);
|
|
615
|
+
let curveOid = null;
|
|
616
|
+
if (algo.oid === OID.ecPublicKey) {
|
|
617
|
+
if (!algo.paramsTlv || algo.paramsTlv.tag !== TAG.OID) {
|
|
618
|
+
// RFC 5480 allows explicit curve parameters; no public CA uses them, and accepting an
|
|
619
|
+
// attacker-described curve is a known validation trap. Named curves only.
|
|
620
|
+
throw parseError(kids[0].start, 'EC key without a named curve (explicit parameters are refused)');
|
|
621
|
+
}
|
|
622
|
+
curveOid = readOid(bytes, algo.paramsTlv, 'EC named curve');
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
algorithmOid: algo.oid,
|
|
626
|
+
curveOid,
|
|
627
|
+
keyBytes,
|
|
628
|
+
spkiDer: element(bytes, tlv),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Parse a bare SubjectPublicKeyInfo element (as stored for trust anchors, which persist only the
|
|
634
|
+
* SPKI rather than a whole certificate). Same walk as inside a certificate.
|
|
635
|
+
* @param {Uint8Array} spkiDer
|
|
636
|
+
* @returns {Spki}
|
|
637
|
+
*/
|
|
638
|
+
export function parseSubjectPublicKeyInfo(spkiDer) {
|
|
639
|
+
return parseSpki(spkiDer, readAll(spkiDer, 'SubjectPublicKeyInfo'));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ------------------------------------------------------------------ certificate
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* The certificate's signatureAlgorithm, with parameters kept both raw and as a Tlv because
|
|
646
|
+
* RSA-PSS resolution has to re-walk them.
|
|
647
|
+
* @typedef {object} AlgorithmId
|
|
648
|
+
* @property {string} oid
|
|
649
|
+
* @property {Uint8Array | null} paramsBytes
|
|
650
|
+
* @property {import('./der.js').Tlv | null} paramsTlv
|
|
651
|
+
* @property {Uint8Array} bytes the whole AlgorithmIdentifier element
|
|
652
|
+
*/
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* A fully parsed certificate. Frozen; every byte field is a subarray of the original `der`.
|
|
656
|
+
* This is the complete shape behind the trimmed `ParsedCertificate` documented on the public
|
|
657
|
+
* verifyChain surface.
|
|
658
|
+
* @typedef {object} Certificate
|
|
659
|
+
* @property {Uint8Array} der the original bytes, never re-encoded
|
|
660
|
+
* @property {Uint8Array} tbsBytes exact TBSCertificate slice the signature covers
|
|
661
|
+
* @property {number} version 1, 2 or 3
|
|
662
|
+
* @property {string} serialNumber hex of the INTEGER content bytes
|
|
663
|
+
* @property {boolean} serialNegative negative serials are misissuance but must still parse
|
|
664
|
+
* @property {AlgorithmId} signatureAlgorithm
|
|
665
|
+
* @property {Uint8Array} signature
|
|
666
|
+
* @property {DistinguishedName} issuer
|
|
667
|
+
* @property {DistinguishedName} subject
|
|
668
|
+
* @property {number} notBefore epoch ms
|
|
669
|
+
* @property {number} notAfter epoch ms
|
|
670
|
+
* @property {Spki} spki
|
|
671
|
+
* @property {Map<string, { critical: boolean, valueBytes: Uint8Array }>} extensions by OID
|
|
672
|
+
* @property {{ present: boolean, ca: boolean, pathLenConstraint: number | null }} basicConstraints
|
|
673
|
+
* @property {KeyUsage | null} keyUsage null when the extension is absent
|
|
674
|
+
* @property {ReadonlyArray<string> | null} extendedKeyUsage KeyPurposeId OIDs
|
|
675
|
+
* @property {SubjectAltNames} subjectAltNames
|
|
676
|
+
* @property {Uint8Array | null} subjectKeyIdentifier
|
|
677
|
+
* @property {Uint8Array | null} authorityKeyIdentifier keyIdentifier field only
|
|
678
|
+
* @property {NameConstraints | null} nameConstraints
|
|
679
|
+
* @property {ReadonlyArray<string>} unknownCriticalExtensions OIDs path.js must reject on
|
|
680
|
+
* @property {boolean} isSelfIssued subject DER equals issuer DER
|
|
681
|
+
*/
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Parse one DER certificate into a frozen, fully-walked structure. Throws CertificateError
|
|
685
|
+
* (CERT_PARSE) on malformed or self-contradictory encodings; judgement about what the
|
|
686
|
+
* certificate MAY do lives in path.js.
|
|
687
|
+
*
|
|
688
|
+
* `tbsBytes` is the exact original slice of the TBSCertificate element — signature verification
|
|
689
|
+
* happens over these bytes and never over anything re-encoded.
|
|
690
|
+
*
|
|
691
|
+
* @param {Uint8Array} der
|
|
692
|
+
* @returns {Certificate}
|
|
693
|
+
*/
|
|
694
|
+
export function parseCertificate(der) {
|
|
695
|
+
if (!(der instanceof Uint8Array)) {
|
|
696
|
+
throw new CertificateError(codes.CERT_PARSE, 'certificate must be a Uint8Array of DER');
|
|
697
|
+
}
|
|
698
|
+
const cert = readAll(der, 'Certificate');
|
|
699
|
+
expectTlv(cert, { tag: TAG.SEQUENCE, constructed: true }, 'Certificate');
|
|
700
|
+
const [tbsTlv, sigAlgTlv, sigTlv, ...extraTop] = children(der, cert, 'Certificate');
|
|
701
|
+
if (!tbsTlv || !sigAlgTlv || !sigTlv || extraTop.length) {
|
|
702
|
+
throw parseError(cert.start,
|
|
703
|
+
'Certificate must be { tbsCertificate, signatureAlgorithm, signatureValue }');
|
|
704
|
+
}
|
|
705
|
+
expectTlv(tbsTlv, { tag: TAG.SEQUENCE, constructed: true }, 'TBSCertificate');
|
|
706
|
+
const outerAlg = parseAlgorithmIdentifier(der, sigAlgTlv, 'signatureAlgorithm');
|
|
707
|
+
const { bytes: signature } = readBitString(der, sigTlv, 'signatureValue');
|
|
708
|
+
|
|
709
|
+
const fields = children(der, tbsTlv, 'TBSCertificate');
|
|
710
|
+
let i = 0;
|
|
711
|
+
const next = (what) => {
|
|
712
|
+
if (i >= fields.length) throw parseError(tbsTlv.end, `TBSCertificate ends before ${what}`);
|
|
713
|
+
return fields[i++];
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// version [0] EXPLICIT INTEGER DEFAULT v1(0)
|
|
717
|
+
let version = 1;
|
|
718
|
+
if (fields[0] && fields[0].cls === CLS.CONTEXT && fields[0].tag === 0) {
|
|
719
|
+
const wrapper = next('version');
|
|
720
|
+
if (!wrapper.constructed) throw parseError(wrapper.start, 'version [0] must be constructed (EXPLICIT)');
|
|
721
|
+
const inner = readTlv(der, wrapper.contentStart);
|
|
722
|
+
if (inner.end !== wrapper.contentEnd) throw parseError(inner.end, 'trailing bytes in version');
|
|
723
|
+
const { value, negative } = readInteger(der, inner, 'version');
|
|
724
|
+
if (negative || value === null || value > 2) {
|
|
725
|
+
throw parseError(inner.start, `unsupported certificate version ${negative ? 'negative' : value}`);
|
|
726
|
+
}
|
|
727
|
+
// DER forbids encoding the DEFAULT: version v1 must be expressed by omission.
|
|
728
|
+
if (value === 0) throw parseError(inner.start, 'version v1 must be omitted, not encoded');
|
|
729
|
+
version = value + 1;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const serialTlv = next('serialNumber');
|
|
733
|
+
const serial = readInteger(der, serialTlv, 'serialNumber');
|
|
734
|
+
const innerAlg = parseAlgorithmIdentifier(der, next('signature'), 'TBSCertificate signature');
|
|
735
|
+
|
|
736
|
+
// RFC 5280 s4.1.1.2: outer signatureAlgorithm MUST equal the TBS signature field. A mismatch
|
|
737
|
+
// means someone re-wrapped a signed body under a different algorithm claim — the textbook
|
|
738
|
+
// algorithm-substitution forgery — so the comparison is over full DER bytes, not just OIDs.
|
|
739
|
+
if (!equal(outerAlg.bytes, innerAlg.bytes)) {
|
|
740
|
+
throw new CertificateError(codes.CERT_PARSE,
|
|
741
|
+
`signatureAlgorithm mismatch: outer ${outerAlg.oid} vs tbsCertificate ${innerAlg.oid}` +
|
|
742
|
+
(outerAlg.oid === innerAlg.oid ? ' (same OID, different parameters)' : ''),
|
|
743
|
+
{ outer: outerAlg.oid, inner: innerAlg.oid });
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const issuer = parseName(der, next('issuer'));
|
|
747
|
+
const validityTlv = next('validity');
|
|
748
|
+
expectTlv(validityTlv, { tag: TAG.SEQUENCE, constructed: true }, 'Validity');
|
|
749
|
+
const times = children(der, validityTlv, 'Validity');
|
|
750
|
+
if (times.length !== 2) throw parseError(validityTlv.start, 'Validity must be { notBefore, notAfter }');
|
|
751
|
+
// RFC 5280 wants UTCTime through 2049 and GeneralizedTime after; both encodings name an
|
|
752
|
+
// unambiguous instant, so a cert that picked the wrong one is accepted — rejecting it would
|
|
753
|
+
// add availability risk and zero security.
|
|
754
|
+
const notBefore = readTime(der, times[0], 'notBefore');
|
|
755
|
+
const notAfter = readTime(der, times[1], 'notAfter');
|
|
756
|
+
if (notAfter < notBefore) {
|
|
757
|
+
throw parseError(validityTlv.start,
|
|
758
|
+
`notAfter precedes notBefore (${new Date(notAfter).toISOString()} < ` +
|
|
759
|
+
`${new Date(notBefore).toISOString()})`);
|
|
760
|
+
}
|
|
761
|
+
const subject = parseName(der, next('subject'));
|
|
762
|
+
const spki = parseSpki(der, next('subjectPublicKeyInfo'));
|
|
763
|
+
|
|
764
|
+
// issuerUniqueID [1] / subjectUniqueID [2] IMPLICIT: legal in v2/v3, obsolete, skipped.
|
|
765
|
+
while (i < fields.length && fields[i].cls === CLS.CONTEXT &&
|
|
766
|
+
(fields[i].tag === 1 || fields[i].tag === 2)) {
|
|
767
|
+
if (version === 1) {
|
|
768
|
+
throw parseError(fields[i].start, 'unique identifiers are not allowed in v1 certificates');
|
|
769
|
+
}
|
|
770
|
+
i++;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const extensions = new Map();
|
|
774
|
+
let basicConstraints = Object.freeze({ present: false, ca: false, pathLenConstraint: null });
|
|
775
|
+
let keyUsage = null;
|
|
776
|
+
let extendedKeyUsage = null;
|
|
777
|
+
let subjectAltNames = Object.freeze({ present: false, dns: Object.freeze([]), ip: Object.freeze([]),
|
|
778
|
+
uri: Object.freeze([]), email: Object.freeze([]) });
|
|
779
|
+
let subjectKeyIdentifier = null;
|
|
780
|
+
let authorityKeyIdentifier = null;
|
|
781
|
+
let nameConstraints = null;
|
|
782
|
+
const unknownCriticalExtensions = [];
|
|
783
|
+
|
|
784
|
+
if (i < fields.length) {
|
|
785
|
+
const wrapper = fields[i++];
|
|
786
|
+
if (wrapper.cls !== CLS.CONTEXT || wrapper.tag !== 3 || !wrapper.constructed) {
|
|
787
|
+
throw parseError(wrapper.start, `unexpected TBSCertificate field ${tagName(wrapper.cls, wrapper.tag)}`);
|
|
788
|
+
}
|
|
789
|
+
if (version !== 3) {
|
|
790
|
+
throw parseError(wrapper.start, `extensions are not allowed in v${version} certificates`);
|
|
791
|
+
}
|
|
792
|
+
const listTlv = readTlv(der, wrapper.contentStart);
|
|
793
|
+
if (listTlv.end !== wrapper.contentEnd) throw parseError(listTlv.end, 'trailing bytes in extensions');
|
|
794
|
+
expectTlv(listTlv, { tag: TAG.SEQUENCE, constructed: true }, 'Extensions');
|
|
795
|
+
const extTlvs = children(der, listTlv, 'Extensions');
|
|
796
|
+
if (extTlvs.length === 0) throw parseError(listTlv.start, 'empty Extensions sequence');
|
|
797
|
+
for (const extTlv of extTlvs) {
|
|
798
|
+
expectTlv(extTlv, { tag: TAG.SEQUENCE, constructed: true }, 'Extension');
|
|
799
|
+
const parts = children(der, extTlv, 'Extension');
|
|
800
|
+
if (parts.length < 2 || parts.length > 3) {
|
|
801
|
+
throw parseError(extTlv.start, 'Extension must be { extnID, critical?, extnValue }');
|
|
802
|
+
}
|
|
803
|
+
const extnId = readOid(der, parts[0], 'extnID');
|
|
804
|
+
let critical = false;
|
|
805
|
+
let valueIdx = 1;
|
|
806
|
+
if (parts.length === 3) {
|
|
807
|
+
critical = readBoolean(der, parts[1], 'critical');
|
|
808
|
+
// DER: critical FALSE is the DEFAULT and must be omitted.
|
|
809
|
+
if (!critical) throw parseError(parts[1].start, 'critical FALSE must be omitted in DER');
|
|
810
|
+
valueIdx = 2;
|
|
811
|
+
}
|
|
812
|
+
expectTlv(parts[valueIdx], { tag: TAG.OCTET_STRING, constructed: false }, 'extnValue');
|
|
813
|
+
const valueBytes = content(der, parts[valueIdx]);
|
|
814
|
+
// RFC 5280 s4.2: at most one instance of each extension. Two copies of keyUsage is two
|
|
815
|
+
// policies for one key; whichever a validator reads, the other was the lie.
|
|
816
|
+
if (extensions.has(extnId)) {
|
|
817
|
+
throw parseError(extTlv.start, `duplicate extension ${extnId}`);
|
|
818
|
+
}
|
|
819
|
+
extensions.set(extnId, Object.freeze({ critical, valueBytes }));
|
|
820
|
+
|
|
821
|
+
switch (extnId) {
|
|
822
|
+
case OID.basicConstraints: basicConstraints = Object.freeze(parseBasicConstraints(valueBytes)); break;
|
|
823
|
+
case OID.keyUsage: keyUsage = parseKeyUsage(valueBytes); break;
|
|
824
|
+
case OID.extendedKeyUsage: extendedKeyUsage = parseExtendedKeyUsage(valueBytes); break;
|
|
825
|
+
case OID.subjectAltName: subjectAltNames = parseSubjectAltName(valueBytes); break;
|
|
826
|
+
case OID.subjectKeyIdentifier:
|
|
827
|
+
subjectKeyIdentifier = parseSubjectKeyIdentifier(valueBytes);
|
|
828
|
+
break;
|
|
829
|
+
case OID.authorityKeyIdentifier:
|
|
830
|
+
authorityKeyIdentifier = parseAuthorityKeyIdentifier(valueBytes);
|
|
831
|
+
break;
|
|
832
|
+
case OID.nameConstraints: nameConstraints = parseNameConstraints(valueBytes); break;
|
|
833
|
+
default:
|
|
834
|
+
if (critical && !KNOWN_EXTENSIONS.has(extnId)) unknownCriticalExtensions.push(extnId);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (i !== fields.length) {
|
|
839
|
+
throw parseError(fields[i].start, 'unexpected trailing TBSCertificate field');
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return Object.freeze({
|
|
843
|
+
der,
|
|
844
|
+
tbsBytes: element(der, tbsTlv),
|
|
845
|
+
version,
|
|
846
|
+
serialNumber: toHex(serial.bytes),
|
|
847
|
+
serialNegative: serial.negative,
|
|
848
|
+
signatureAlgorithm: Object.freeze({
|
|
849
|
+
oid: outerAlg.oid,
|
|
850
|
+
paramsBytes: outerAlg.paramsBytes,
|
|
851
|
+
paramsTlv: outerAlg.paramsTlv,
|
|
852
|
+
bytes: outerAlg.bytes,
|
|
853
|
+
}),
|
|
854
|
+
signature,
|
|
855
|
+
issuer: Object.freeze(issuer),
|
|
856
|
+
subject: Object.freeze(subject),
|
|
857
|
+
notBefore,
|
|
858
|
+
notAfter,
|
|
859
|
+
spki: Object.freeze(spki),
|
|
860
|
+
extensions,
|
|
861
|
+
basicConstraints,
|
|
862
|
+
keyUsage,
|
|
863
|
+
extendedKeyUsage,
|
|
864
|
+
subjectAltNames,
|
|
865
|
+
subjectKeyIdentifier,
|
|
866
|
+
authorityKeyIdentifier,
|
|
867
|
+
nameConstraints,
|
|
868
|
+
unknownCriticalExtensions: Object.freeze(unknownCriticalExtensions),
|
|
869
|
+
isSelfIssued: equal(subject.bytes, issuer.bytes),
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// ------------------------------------------------------------------ PEM
|
|
874
|
+
|
|
875
|
+
const PEM_RE = /-----BEGIN CERTIFICATE-----([A-Za-z0-9+/=\s]+?)-----END CERTIFICATE-----/g;
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Extract every CERTIFICATE block from PEM text as DER. Used for user-supplied trust anchors;
|
|
879
|
+
* TLS itself always delivers DER. Throws CERT_PARSE on bad base64 or when no block is found.
|
|
880
|
+
* @param {string} text
|
|
881
|
+
* @returns {Uint8Array[]}
|
|
882
|
+
*/
|
|
883
|
+
export function decodePem(text) {
|
|
884
|
+
const out = [];
|
|
885
|
+
for (const match of String(text).matchAll(PEM_RE)) {
|
|
886
|
+
const b64 = match[1].replace(/\s+/g, '');
|
|
887
|
+
let bin;
|
|
888
|
+
try {
|
|
889
|
+
bin = atob(b64);
|
|
890
|
+
} catch {
|
|
891
|
+
throw new CertificateError(codes.CERT_PARSE,
|
|
892
|
+
`PEM certificate block ${out.length + 1} is not valid base64`);
|
|
893
|
+
}
|
|
894
|
+
const der = new Uint8Array(bin.length);
|
|
895
|
+
for (let j = 0; j < bin.length; j++) der[j] = bin.charCodeAt(j);
|
|
896
|
+
out.push(der);
|
|
897
|
+
}
|
|
898
|
+
if (out.length === 0) {
|
|
899
|
+
throw new CertificateError(codes.CERT_PARSE, 'no CERTIFICATE blocks found in PEM input');
|
|
900
|
+
}
|
|
901
|
+
return out;
|
|
902
|
+
}
|