@blamejs/pki 0.5.7 → 0.5.8
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/CHANGELOG.md +405 -386
- package/MIGRATING.md +43 -0
- package/README.md +12 -12
- package/lib/acme.js +31 -31
- package/lib/asn1-der.js +10 -10
- package/lib/attrcert-sign.js +19 -19
- package/lib/byte-reader.js +6 -6
- package/lib/byte-writer.js +5 -5
- package/lib/cbor-det.js +27 -24
- package/lib/cmc-build.js +97 -92
- package/lib/cmc-verify.js +106 -30
- package/lib/cmp-build.js +25 -25
- package/lib/cmp-session.js +70 -70
- package/lib/cmp-verify.js +71 -57
- package/lib/cms-compress.js +7 -7
- package/lib/cms-decrypt.js +90 -72
- package/lib/cms-encrypt.js +32 -32
- package/lib/cms-sign.js +74 -55
- package/lib/cms-verify.js +97 -75
- package/lib/composite-sig.js +13 -13
- package/lib/constants.js +4 -4
- package/lib/crl-sign.js +22 -22
- package/lib/crl-verify.js +7 -6
- package/lib/crmf-sign.js +14 -14
- package/lib/csr-sign.js +8 -8
- package/lib/ct.js +37 -37
- package/lib/edwards-point.js +7 -7
- package/lib/est.js +98 -55
- package/lib/framework-error.js +5 -5
- package/lib/guard-all.js +3 -3
- package/lib/guard-async.js +4 -4
- package/lib/guard-bytes.js +79 -79
- package/lib/guard-compress.js +17 -17
- package/lib/guard-crypto.js +1 -1
- package/lib/guard-encoding.js +15 -15
- package/lib/guard-header.js +3 -3
- package/lib/guard-identifier.js +16 -16
- package/lib/guard-json.js +15 -15
- package/lib/guard-limits.js +7 -7
- package/lib/guard-name.js +81 -16
- package/lib/guard-parsed.js +80 -80
- package/lib/guard-range.js +19 -19
- package/lib/guard-secret.js +11 -10
- package/lib/guard-text.js +6 -6
- package/lib/guard-time.js +10 -10
- package/lib/hpke.js +18 -17
- package/lib/http-digest.js +35 -35
- package/lib/http-retry-after.js +13 -13
- package/lib/http-transport.js +20 -19
- package/lib/inspect.js +53 -53
- package/lib/ip-utils.js +2 -2
- package/lib/jose.js +13 -13
- package/lib/key.js +16 -16
- package/lib/lint.js +51 -51
- package/lib/merkle.js +51 -36
- package/lib/mime.js +18 -18
- package/lib/ocsp-verify.js +10 -10
- package/lib/ocsp.js +13 -13
- package/lib/oid.js +29 -29
- package/lib/path-validate.js +114 -113
- package/lib/pbes2.js +16 -16
- package/lib/pkcs12-build.js +53 -53
- package/lib/pki-build.js +21 -19
- package/lib/rc2.js +1 -1
- package/lib/rfc3339.js +5 -5
- package/lib/schema-all.js +31 -31
- package/lib/schema-attrcert.js +12 -12
- package/lib/schema-c509.js +144 -142
- package/lib/schema-cmc.js +58 -58
- package/lib/schema-cmp.js +43 -43
- package/lib/schema-cms.js +45 -45
- package/lib/schema-crl.js +7 -7
- package/lib/schema-crmf.js +28 -28
- package/lib/schema-csr.js +12 -12
- package/lib/schema-csrattrs.js +16 -16
- package/lib/schema-engine.js +18 -18
- package/lib/schema-ocsp.js +15 -15
- package/lib/schema-pkcs12.js +20 -20
- package/lib/schema-pkcs8.js +2 -2
- package/lib/schema-pkix.js +131 -126
- package/lib/schema-smime.js +19 -19
- package/lib/schema-tsp.js +12 -12
- package/lib/schema-x509.js +3 -3
- package/lib/shbs.js +18 -18
- package/lib/sign-scheme.js +13 -13
- package/lib/sigstore.js +10 -11
- package/lib/sleep.js +1 -1
- package/lib/smime.js +308 -96
- package/lib/tls-cert-compress.js +18 -18
- package/lib/trust.js +27 -27
- package/lib/tsp-sign.js +17 -17
- package/lib/validator-all.js +1 -1
- package/lib/validator-attcert.js +1 -1
- package/lib/validator-cose.js +43 -44
- package/lib/validator-keydesc.js +3 -3
- package/lib/validator-sig.js +13 -13
- package/lib/validator-tls.js +11 -11
- package/lib/validator-tpm.js +20 -19
- package/lib/webauthn-mds.js +66 -66
- package/lib/webauthn.js +33 -33
- package/lib/webcrypto.js +15 -15
- package/lib/x509-sign.js +13 -13
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/tls-cert-compress.js
CHANGED
|
@@ -15,18 +15,18 @@
|
|
|
15
15
|
* largest thing a handshake sends and matters most for post-quantum chains,
|
|
16
16
|
* where certificates grow by kilobytes. `parseCertificateMessage` decodes the
|
|
17
17
|
* RFC 8446 sec. 4.4.2 `Certificate` message itself, so a compressed chain
|
|
18
|
-
* arrives as certificate DER ready for `pki.schema.x509.parse`
|
|
18
|
+
* arrives as certificate DER ready for `pki.schema.x509.parse` instead of as
|
|
19
19
|
* an opaque blob.
|
|
20
20
|
*
|
|
21
|
-
* These are encoded in the TLS presentation language
|
|
22
|
-
* fixed-width big-endian integers and length-prefixed opaque vectors
|
|
23
|
-
* ASN.1/DER, so this module composes the toolkit's bounded big-endian cursor
|
|
24
|
-
*
|
|
21
|
+
* These are encoded in the TLS presentation language (positional, tag-less,
|
|
22
|
+
* fixed-width big-endian integers and length-prefixed opaque vectors) and not
|
|
23
|
+
* in ASN.1/DER, so this module composes the toolkit's bounded big-endian cursor
|
|
24
|
+
* in place of the DER schema engine.
|
|
25
25
|
*
|
|
26
26
|
* Decompression is the attack surface and is fail-closed on both sides of the
|
|
27
27
|
* bound RFC 8879 sec. 5 requires: the decompressor is capped at the message's
|
|
28
|
-
*
|
|
29
|
-
* allocated), and the output must then equal that length
|
|
28
|
+
* own declared uncompressed length (so a bomb is refused mid-stream, never
|
|
29
|
+
* allocated), and the output must then equal that length exactly (which catches
|
|
30
30
|
* the under-length direction a cap cannot see). The caller's policy cap applies
|
|
31
31
|
* independently, so a peer declaring 16 MiB does not get 16 MiB. This module
|
|
32
32
|
* decodes structure only -- it never verifies a certificate, builds a path, or
|
|
@@ -83,14 +83,14 @@ var LEVEL_OPT = {
|
|
|
83
83
|
zstd: function (n) { var p = {}; p[zlib.constants.ZSTD_c_compressionLevel] = n; return { params: p }; },
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
// The names that are
|
|
86
|
+
// The names that are both registered by RFC 8879 and reachable through the decompression
|
|
87
87
|
// guard on this runtime. An algorithm the guard cannot decompress must never be offered
|
|
88
|
-
// or accepted
|
|
88
|
+
// or accepted: advertising one would mean accepting a message we then cannot open.
|
|
89
89
|
var SUPPORTED = guard.compress.algorithms().filter(function (n) {
|
|
90
90
|
return Object.prototype.hasOwnProperty.call(ALG_BY_NAME, n) && typeof COMPRESS[n] === "function";
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
// The minimum Certificate message size and the widest opaque<..2^24-1> vector
|
|
93
|
+
// The minimum Certificate message size and the widest opaque<..2^24-1> vector, both owned
|
|
94
94
|
// by validator-tls, alongside the rest of the framing rule set. A declared
|
|
95
95
|
// uncompressed_length below the minimum cannot be a Certificate message, and refusing it
|
|
96
96
|
// also keeps a zero-length declaration away from the decompression cap, which is a positive
|
|
@@ -147,12 +147,12 @@ function _resolveAllowed(opts) {
|
|
|
147
147
|
*
|
|
148
148
|
* Decode an RFC 8879 sec. 4 `CompressedCertificate`: the `algorithm` code point, the
|
|
149
149
|
* declared `uncompressedLength`, the recovered `certificateMessage` (the raw RFC 8446
|
|
150
|
-
* sec. 4.4.2 `Certificate` message bytes, surfaced verbatim), and `certificate
|
|
150
|
+
* sec. 4.4.2 `Certificate` message bytes, surfaced verbatim), and `certificate`, that
|
|
151
151
|
* message already decoded into its request context and per-entry certificate DER.
|
|
152
152
|
*
|
|
153
153
|
* The decompression bound is two-sided, as RFC 8879 sec. 5 requires. The decompressor is
|
|
154
154
|
* capped at `min(uncompressedLength, policy cap)`, so a bomb is refused the moment its
|
|
155
|
-
* output would exceed what the message itself declared
|
|
155
|
+
* output would exceed what the message itself declared, so it is never allocated. The
|
|
156
156
|
* recovered length must then equal `uncompressedLength` exactly, which catches the
|
|
157
157
|
* under-length direction no cap can see. A message that fails either way is refused;
|
|
158
158
|
* RFC 8879 sec. 5 maps both to the `bad_certificate` alert, but they keep distinct codes
|
|
@@ -160,7 +160,7 @@ function _resolveAllowed(opts) {
|
|
|
160
160
|
* diagnoses.
|
|
161
161
|
*
|
|
162
162
|
* An algorithm outside the RFC 8879 registry, one this runtime cannot decompress, or one
|
|
163
|
-
* absent from `opts.allowedAlgorithms` is refused before any decompressor runs
|
|
163
|
+
* absent from `opts.allowedAlgorithms` is refused before any decompressor runs, since the
|
|
164
164
|
* algorithm MUST be one the receiver advertised (RFC 8879 sec. 4). An empty compressed
|
|
165
165
|
* body is a framing violation (`opaque<1..2^24-1>`), not an empty certificate list.
|
|
166
166
|
*
|
|
@@ -263,18 +263,18 @@ function decompressCertificate(bytes, opts) {
|
|
|
263
263
|
* Decode an RFC 8446 sec. 4.4.2 `Certificate` message into its
|
|
264
264
|
* `certificateRequestContext` (raw, empty in a server's handshake certificate) and its
|
|
265
265
|
* `entries`. Each entry surfaces `certData` -- the certificate DER exactly as it appeared
|
|
266
|
-
* on the wire, ready for `pki.schema.x509.parse` and never re-serialized
|
|
266
|
+
* on the wire, ready for `pki.schema.x509.parse` and never re-serialized, plus the raw
|
|
267
267
|
* `extensions` bytes that follow it and `extensionList`, those bytes decoded to their
|
|
268
268
|
* RFC 8446 sec. 4.2 records (`type` and raw `data` per extension). The vector's framing is
|
|
269
|
-
* validated
|
|
269
|
+
* validated and not accepted opaquely, so a vector that cannot be a whole number of
|
|
270
270
|
* Extensions is refused instead of being reported as a well-formed message.
|
|
271
271
|
*
|
|
272
|
-
* `certificate_type` is negotiated by a separate extension (RFC 7250) and is
|
|
272
|
+
* `certificate_type` is negotiated by a separate extension (RFC 7250) and is not present
|
|
273
273
|
* in this message, so it cannot be inferred from the bytes. It is declared through
|
|
274
274
|
* `opts.certificateType` and defaults to X509; under `"RawPublicKey"` the same slot is a
|
|
275
275
|
* `SubjectPublicKeyInfo` and is surfaced as `spki` instead of `certData`.
|
|
276
276
|
*
|
|
277
|
-
* Throws `TlsError` with a stable `tls/*` code on any framing violation
|
|
277
|
+
* Throws `TlsError` with a stable `tls/*` code on any framing violation: a lying vector
|
|
278
278
|
* length, a field past its bound, or bytes trailing the entry list.
|
|
279
279
|
*
|
|
280
280
|
* @opts
|
|
@@ -330,7 +330,7 @@ function parseCertificateMessage(bytes, opts) {
|
|
|
330
330
|
* through `decompressCertificate` and the recovered bytes compared to the input, so a
|
|
331
331
|
* message this toolkit produces can never be one this toolkit's own decoder refuses.
|
|
332
332
|
*
|
|
333
|
-
* Throws `TlsError` with a stable `tls/*` code
|
|
333
|
+
* Throws `TlsError` with a stable `tls/*` code for an unknown algorithm, or a message
|
|
334
334
|
* that cannot be framed (empty, or past the 2^24-1 ceiling either compressed or not).
|
|
335
335
|
*
|
|
336
336
|
* @opts
|
package/lib/trust.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @intro
|
|
12
12
|
* Mozilla / CCADB root-program ingestion into constraint-carrying trust
|
|
13
13
|
* anchors. The bare root list (`tls.rootCertificates`) throws away exactly
|
|
14
|
-
* the metadata that decides
|
|
14
|
+
* the metadata that decides which roots may vouch for what: the per-purpose
|
|
15
15
|
* trust bits (a root trusted for TLS is not thereby trusted for S/MIME) and
|
|
16
16
|
* the per-purpose distrust-after dates (a sunsetting root keeps validating
|
|
17
17
|
* already-issued certificates while certificates issued after the cutoff are
|
|
@@ -57,7 +57,7 @@ var PURPOSE_ATTRS = [
|
|
|
57
57
|
["CKA_TRUST_CODE_SIGNING", "codeSigning"],
|
|
58
58
|
];
|
|
59
59
|
|
|
60
|
-
// The recognized CK_TRUST vocabulary.
|
|
60
|
+
// The recognized CK_TRUST vocabulary. Only CKT_NSS_TRUSTED_DELEGATOR grants a
|
|
61
61
|
// purpose; the rest are recognized-but-untrusted (fail-closed: an absent or
|
|
62
62
|
// non-delegator bit never defaults to trusted). A token outside this set is a
|
|
63
63
|
// typed trust/bad-trust-value, never a silent false.
|
|
@@ -138,7 +138,7 @@ function _lexObjects(text) {
|
|
|
138
138
|
var rawVal = m[3] !== undefined ? m[3].trim() : undefined;
|
|
139
139
|
if (!type) throw E("trust/bad-block", "attribute line is missing its type token: " + name);
|
|
140
140
|
if (!cur) {
|
|
141
|
-
// Refuse the block bomb
|
|
141
|
+
// Refuse the block bomb before lexing another block's payloads.
|
|
142
142
|
if (objects.length >= LIMITS.TRUST_MAX_OBJECTS) throw E("trust/bad-block", "certdata object count exceeds LIMITS.TRUST_MAX_OBJECTS");
|
|
143
143
|
cur = { attrs: Object.create(null) };
|
|
144
144
|
}
|
|
@@ -167,8 +167,8 @@ function _lexObjects(text) {
|
|
|
167
167
|
if (rawVal === undefined || /\s/.test(rawVal)) throw E("trust/bad-block", name + " " + type + " must carry a single token value");
|
|
168
168
|
entry = { type: type, value: rawVal };
|
|
169
169
|
} else {
|
|
170
|
-
// An unknown value
|
|
171
|
-
// unknown)
|
|
170
|
+
// An unknown value type cannot be lexed safely (its value grammar is
|
|
171
|
+
// unknown), so fail closed instead of guessing past it.
|
|
172
172
|
throw E("trust/bad-block", "unrecognized attribute type " + JSON.stringify(type) + " on " + name);
|
|
173
173
|
}
|
|
174
174
|
cur.attrs[name] = entry;
|
|
@@ -190,8 +190,8 @@ function _requireOctal(obj, name, what) {
|
|
|
190
190
|
return a.bytes;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
// The NSS distrust-after payload is the
|
|
194
|
-
// (13 bytes) or a YYYYMMDDHHMMSSZ GeneralizedTime (15 bytes)
|
|
193
|
+
// The NSS distrust-after payload is the bare ASCII of a YYMMDDHHMMSSZ UTCTime
|
|
194
|
+
// (13 bytes) or a YYYYMMDDHHMMSSZ GeneralizedTime (15 bytes), with no DER tag or
|
|
195
195
|
// length. Synthesize the minimal TLV and route it through the strict asn1
|
|
196
196
|
// time reader so the Z-terminator / mandatory-seconds / component-rollover /
|
|
197
197
|
// RFC 5280 sec. 4.1.2.5.1 2050-pivot rules are enforced once, never re-derived.
|
|
@@ -300,14 +300,14 @@ function _mkAnchor(cert, meta) {
|
|
|
300
300
|
label: meta.label,
|
|
301
301
|
mozillaCaPolicy: meta.mozillaCaPolicy,
|
|
302
302
|
};
|
|
303
|
-
// A trust anchor
|
|
303
|
+
// A trust anchor is the pair (name, key) per RFC 5280 sec. 6.1.1, so those two fields are one
|
|
304
304
|
// fact about one certificate, and they were derived here from one. `anchor()` re-derives them
|
|
305
|
-
//
|
|
305
|
+
// instead of reading them back, so an entry rebuilt with a substituted key cannot carry the
|
|
306
306
|
// store's NAME and its per-purpose trust metadata over to a key the store never vouched for.
|
|
307
307
|
// Recorded off the object, so rebuilding the entry loses the record along with the binding.
|
|
308
|
-
//
|
|
309
|
-
// name object, so overwriting the entry's publicKey in place
|
|
310
|
-
// for an equal-length key
|
|
308
|
+
// Copied, not aliased. The record and the entry would otherwise hold the same Buffer and the same
|
|
309
|
+
// name object, so overwriting the entry's publicKey in place (otherSpki.copy(entry.publicKey)
|
|
310
|
+
// for an equal-length key) would overwrite the record with it, and re-deriving would hand back
|
|
311
311
|
// exactly the substituted key. A record that changes with the thing it is meant to pin is not one.
|
|
312
312
|
//
|
|
313
313
|
// The store's METADATA is recorded with them, and for the stronger reason: `purposes` IS the
|
|
@@ -334,7 +334,7 @@ var _DERIVED_FROM = new WeakMap();
|
|
|
334
334
|
// The per-purpose distrust dates, with fresh Date objects. A Date is mutable, so handing back the
|
|
335
335
|
// entry's own would let a consumer move the date this anchor is judged against.
|
|
336
336
|
// The three trust bits, normalized to booleans. Read by the purpose gate and handed back on every
|
|
337
|
-
// anchor, so it is built fresh from whichever source is authoritative
|
|
337
|
+
// anchor, so it is built fresh from whichever source is authoritative and never shared.
|
|
338
338
|
function _copyPurposes(src) {
|
|
339
339
|
return {
|
|
340
340
|
serverAuth: !!src && src.serverAuth === true,
|
|
@@ -352,13 +352,13 @@ function _copyDistrustAfter(src) {
|
|
|
352
352
|
return out;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
// A parsed Name, copied
|
|
356
|
-
// mutable shared. Used on both sides of the record
|
|
355
|
+
// A parsed Name, copied faithfully: every field the parser assigns, at every level, with nothing
|
|
356
|
+
// mutable shared. Used on both sides of the record, writing it in and handing it out, so
|
|
357
357
|
// neither direction shares an object with the caller.
|
|
358
358
|
//
|
|
359
|
-
// Faithful
|
|
359
|
+
// Faithful, not "the fields the path validator reads", because this Name is handed back on
|
|
360
360
|
// pki.trust.anchor and is the same structure pki.schema.x509.parse produces. A copy that keeps the
|
|
361
|
-
// subset one consumer needs silently degrades it for every other reader
|
|
361
|
+
// subset one consumer needs silently degrades it for every other reader: an attribute would lose
|
|
362
362
|
// its registry `name`, so a caller walking anchor.name.rdns would see a different shape depending
|
|
363
363
|
// on whether the anchor came from a store entry or straight from the parser. Each attribute value
|
|
364
364
|
// is a string and so needs no copy of its own; the DER does.
|
|
@@ -396,11 +396,11 @@ function _anchorsAgree(x, y) {
|
|
|
396
396
|
_datesEqual(x.distrustAfter, y.distrustAfter);
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
-
// Dedup by (subjectDer, publicKey):
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
399
|
+
// Dedup by (subjectDer, publicKey): one root appearing twice (a duplicated
|
|
400
|
+
// block, or certdata + CCADB merged by the operator) collapses to one anchor,
|
|
401
|
+
// while a distinct root that merely shares a subject DN has a different key and
|
|
402
|
+
// survives. Two entries for one root that disagree on trust metadata are
|
|
403
|
+
// ambiguous, so they fail closed instead of silently picking one.
|
|
404
404
|
function _dedupAnchors(anchors) {
|
|
405
405
|
var seen = Object.create(null);
|
|
406
406
|
var out = [];
|
|
@@ -443,7 +443,7 @@ function _dedupAnchors(anchors) {
|
|
|
443
443
|
* dropped); a trust object with no certificate grants nothing. Malformed
|
|
444
444
|
* octal, an oversized block, an unrecognized trust value, a mispaired or
|
|
445
445
|
* ambiguous-duplicate block, or an undecodable distrust-after time throws a
|
|
446
|
-
* typed `trust/*` error
|
|
446
|
+
* typed `trust/*` error, never a silently truncated or misattributed root.
|
|
447
447
|
*
|
|
448
448
|
* @example
|
|
449
449
|
* // Real input is the NSS certdata.txt read from disk; a one-root stream is
|
|
@@ -653,9 +653,9 @@ function _pemCell(cell) {
|
|
|
653
653
|
* @defends trust-metadata-misattribution (CWE-345), trust-store-parser-DoS (CWE-770)
|
|
654
654
|
* @related pki.trust.parseCertdata, pki.trust.anchor, pki.path.validate
|
|
655
655
|
*
|
|
656
|
-
* Parse a CCADB certificate-records CSV export into the
|
|
656
|
+
* Parse a CCADB certificate-records CSV export into the same `Anchor` shape
|
|
657
657
|
* `parseCertdata` produces, so downstream enforcement is source-agnostic.
|
|
658
|
-
* Columns are located by header
|
|
658
|
+
* Columns are located by header name, never by position, and unknown,
|
|
659
659
|
* reordered, or extra columns are tolerated; a MISSING required column
|
|
660
660
|
* (`Common Name or Certificate Name`, `Trust Bits`, `Distrust for TLS After
|
|
661
661
|
* Date`, `Distrust for S/MIME After Date`, `PEM Info`) fails closed with
|
|
@@ -737,7 +737,7 @@ function parseCcadbCsv(text) {
|
|
|
737
737
|
*
|
|
738
738
|
* Turn a parsed trust-store entry into the `trustAnchor` object
|
|
739
739
|
* `pki.path.validate` consumes: `{ name, publicKey, algorithm, parameters,
|
|
740
|
-
* distrustAfter, purposes }
|
|
740
|
+
* distrustAfter, purposes }`, a straight hand-off (validate reads
|
|
741
741
|
* `distrustAfter` as a per-purpose map and `purposes` as the delegator set,
|
|
742
742
|
* selected by its own `opts.checkPurpose`). With `opts.purpose` it
|
|
743
743
|
* fail-fasts: an entry that is not a trusted delegator for that purpose
|
|
@@ -746,7 +746,7 @@ function parseCcadbCsv(text) {
|
|
|
746
746
|
* authoritative gate stays inside `validate`).
|
|
747
747
|
*
|
|
748
748
|
* @opts
|
|
749
|
-
* purpose: string // "serverAuth" | "emailProtection" | "codeSigning"
|
|
749
|
+
* purpose: string // "serverAuth" | "emailProtection" | "codeSigning"; fail-fast purpose check
|
|
750
750
|
*
|
|
751
751
|
* @example
|
|
752
752
|
* var ca = await pki.key.generate("Ed25519");
|
package/lib/tsp-sign.js
CHANGED
|
@@ -51,7 +51,7 @@ var HASH_LEN = { sha256: 32, sha384: 48, sha512: 64 };
|
|
|
51
51
|
// checker ran at all"; "undetermined" is "one ran and could not answer"; "waived" is an
|
|
52
52
|
// undetermined one a caller chose to pass; "determined" is an explicit good-or-revoked answer.
|
|
53
53
|
// Ranking them lets a caller trying several candidate paths keep the most established outcome any
|
|
54
|
-
// of them reached
|
|
54
|
+
// of them reached. Order of evaluation then does not decide the answer.
|
|
55
55
|
var _REVOCATION_RANK = { "false": 0, "undetermined": 1, "waived": 2, "determined": 3 };
|
|
56
56
|
function _rankRevocation(v) {
|
|
57
57
|
var r = _REVOCATION_RANK[String(v)];
|
|
@@ -61,7 +61,7 @@ function _moreEstablished(candidate, current) {
|
|
|
61
61
|
return _rankRevocation(candidate) > _rankRevocation(current);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// A hash AlgorithmIdentifier { OID, NULL }
|
|
64
|
+
// A hash AlgorithmIdentifier { OID, NULL }: messageImprint and ESSCertIDv2 hash algorithms
|
|
65
65
|
// carry an explicit NULL parameter (the form RFC 3161 / RFC 5035 producers emit).
|
|
66
66
|
function _hashAlgId(name) {
|
|
67
67
|
if (!NODE_DIGEST[name]) throw _err("tsp/unsupported-algorithm", "unsupported hash algorithm " + JSON.stringify(name));
|
|
@@ -114,7 +114,7 @@ function _signingCertV2(certDer, hashName) {
|
|
|
114
114
|
* @opts serialNumber REQUIRED -- a unique token serial number (a number or BigInt).
|
|
115
115
|
* @opts genTime The trusted time (a `Date`). Default: now.
|
|
116
116
|
* @opts nonce The request nonce to echo (a number or BigInt), for replay protection.
|
|
117
|
-
* @opts accuracy `{ seconds?, millis?, micros? }
|
|
117
|
+
* @opts accuracy The genTime +/- accuracy, as `{ seconds?, millis?, micros? }`.
|
|
118
118
|
* @opts ordering Whether tokens from this TSA are strictly ordered in time (boolean).
|
|
119
119
|
* @opts certHashAlgorithm The ESSCertIDv2 hash algorithm name. Default `sha256`.
|
|
120
120
|
* @opts sid / pem Passed through to `pki.cms.sign` (signer identifier, PEM output).
|
|
@@ -250,7 +250,7 @@ function _assertImprint(mi) {
|
|
|
250
250
|
* @related pki.tsp.parseRequest, pki.tsp.sign
|
|
251
251
|
*
|
|
252
252
|
* Build an RFC 3161 `TimeStampReq` (sec. 2.4.1) over `messageImprint`
|
|
253
|
-
* (`{ hashAlgorithm, hashedMessage }
|
|
253
|
+
* (`{ hashAlgorithm, hashedMessage }`, the same shape `pki.tsp.sign` takes). `version` is 1;
|
|
254
254
|
* `certReq` is BOOLEAN DEFAULT FALSE, so it is emitted only when explicitly `true`. Returns DER
|
|
255
255
|
* (or PEM when `opts.pem`).
|
|
256
256
|
*
|
|
@@ -611,38 +611,38 @@ function _buildTsaChains(leaf, pool) {
|
|
|
611
611
|
* @related pki.tsp.sign, pki.cms.verify, pki.path.validate
|
|
612
612
|
*
|
|
613
613
|
* Verify an RFC 3161 TimeStampToken against the data it should cover. `token` is the token DER /
|
|
614
|
-
* PEM
|
|
615
|
-
* mutated parsed structure cannot desynchronize the checks from the signed bytes
|
|
614
|
+
* PEM, never a parsed object: every checked field is read from the CMS-verified eContent, so a
|
|
615
|
+
* mutated parsed structure cannot desynchronize the checks from the signed bytes. `data` is the
|
|
616
616
|
* original bytes (hashed under the token's messageImprint algorithm) or a precomputed
|
|
617
617
|
* `{ hashAlgorithm, hashedMessage }`. Returns `{ valid, trusted, revocationChecked,
|
|
618
618
|
* anchorConstraints, genTime, accuracy, serialNumber,
|
|
619
619
|
* serialNumberHex, policy, nonce, tsa, tstInfo, signer, code?, reason? }`. `valid` is true only
|
|
620
620
|
* when the CMS signature, the imprint match, the eContentType, the ESSCertID(V2) binding, the
|
|
621
621
|
* RFC 3161 sec. 2.3 critical single-`timeStamping` extendedKeyUsage rule, the requested nonce (when
|
|
622
|
-
* supplied), and
|
|
622
|
+
* supplied), and, when a `trustAnchor` is supplied, the full out-of-path TSA-cert path
|
|
623
623
|
* validation all pass. A conformance / trust failure of a well-formed token is a
|
|
624
624
|
* `{ valid:false, code }` verdict; malformed or config input throws a typed `TspError`.
|
|
625
625
|
*
|
|
626
626
|
* `trusted` is the second claim and is kept apart from the first. `valid` says the token's
|
|
627
627
|
* signature and structural bindings hold; `trusted` says the timestamp authority chained to an
|
|
628
628
|
* anchor this caller named. Without `trustAnchor` there is nothing to chain to and `trusted` is
|
|
629
|
-
* `false
|
|
630
|
-
*
|
|
629
|
+
* `false`: every branch, refusing and accepting alike, carries the field with a definite
|
|
630
|
+
* value. A timestamp is archived precisely to be re-read years later, and one boolean
|
|
631
631
|
* cannot answer both questions then.
|
|
632
632
|
*
|
|
633
633
|
* `revocationChecked` is the third claim, for the same reason. Revocation runs only when a
|
|
634
|
-
* `revocationChecker` is supplied
|
|
635
|
-
* or an OCSP responder reads identically to one established
|
|
636
|
-
*
|
|
634
|
+
* `revocationChecker` is supplied. Without a field naming which happened, a `trusted` token whose
|
|
635
|
+
* TSA was never checked against a CRL or an OCSP responder reads identically to one established
|
|
636
|
+
* un-revoked. It is `false` whenever no path ran at all. `anchorConstraints` carries whatever the anchor
|
|
637
637
|
* itself constrained, from `pki.path.validate`.
|
|
638
638
|
*
|
|
639
|
-
* @opts trustAnchor Anchor `{ name, publicKey, algorithm }
|
|
639
|
+
* @opts trustAnchor Anchor `{ name, publicKey, algorithm }`. Runs `pki.path.validate` on the
|
|
640
640
|
* TSA certificate chain ordered from the token's embedded certificates
|
|
641
641
|
* (validity at genTime, requiredEku timeStamping, and revocation when a
|
|
642
|
-
* `revocationChecker` is supplied
|
|
643
|
-
* TSA under an intermediate CA validates, not only one directly under
|
|
644
|
-
* anchor. Omit to verify signature + imprint + binding + EKU only and
|
|
645
|
-
* the cert yourself.
|
|
642
|
+
* `revocationChecker` is supplied, with `revocationChecked` reporting which),
|
|
643
|
+
* so a TSA under an intermediate CA validates, not only one directly under
|
|
644
|
+
* the anchor. Omit to verify signature + imprint + binding + EKU only and
|
|
645
|
+
* anchor the cert yourself.
|
|
646
646
|
* @opts nonce Require the token's TSTInfo.nonce to equal this (a number/BigInt).
|
|
647
647
|
* @opts reqPolicy Require the token's policy to equal this (an OID name or dotted string).
|
|
648
648
|
* @opts certs Out-of-band TSA certificates (an array of DER `Buffer`s) added to the signer
|
package/lib/validator-all.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// validator-all -- the validator-family orchestrator (schema-all's role for the
|
|
9
9
|
// validator family, guard-all's role for the guards). It assembles the per-type
|
|
10
10
|
// conformance validators into one namespaced surface every format module composes,
|
|
11
|
-
// so a format depends on the family
|
|
11
|
+
// so a format depends on the family and no module re-derives a decoded type's rule set
|
|
12
12
|
// inline (the drift that leaks a spec MUST out one review round at a time):
|
|
13
13
|
//
|
|
14
14
|
// validator.cose.credentialKey / .toSpki
|
package/lib/validator-attcert.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// attestation-certificate profile" (WebAuthn sec. 8.2.1 packed, sec. 8.3.1 TPM AIK).
|
|
10
10
|
// Sibling to the guard family: a validator owns a decoded TYPE's COMPLETE conformance rule
|
|
11
11
|
// set once, so the packed and AIK profiles cannot drift apart or lose a gate (e.g. the v3
|
|
12
|
-
// check that previously lived split out in the tpm verifier
|
|
12
|
+
// check that previously lived split out in the tpm verifier, away from the profile).
|
|
13
13
|
//
|
|
14
14
|
// The caller supplies its parsed certificate + an extension-accessor object `exts`
|
|
15
15
|
// { find(cert, oidName) -> rawExt|null, decode(cert, oidName) -> {critical, value}|null }
|
package/lib/validator-cose.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// CTAP2 canonical-CBOR profile WebAuthn sec. 6.5.1 imposes). Sibling to the guard
|
|
11
11
|
// family: where a guard owns a CVE-class fail-closed defence once, a validator owns a
|
|
12
12
|
// decoded TYPE's COMPLETE conformance rule set once, so a format module composes the
|
|
13
|
-
// family
|
|
13
|
+
// family instead of re-deriving a partial subset inline (the drift that leaks MUSTs
|
|
14
14
|
// out one review round at a time). Enforced by the validator-shape-reinlined
|
|
15
15
|
// codebase-patterns detector: a lib function that re-inlines COSE-key validation fires.
|
|
16
16
|
//
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
//
|
|
21
21
|
// Rule set (gap-checked verbatim against RFC 9052 sec. 7 + RFC 9053 sec. 2/6 +
|
|
22
22
|
// WebAuthn sec. 6.5.1 + the IANA COSE Key Type / Key Type Parameters registries):
|
|
23
|
-
// - kty (label 1) REQUIRED; value an integer (CTAP2 canonical
|
|
23
|
+
// - kty (label 1) REQUIRED; value an integer (CTAP2 canonical, so a tstr kty/alg is
|
|
24
24
|
// rejected as non-canonical for the WebAuthn profile).
|
|
25
25
|
// - alg (label 3) REQUIRED (the RP needs it to verify the later assertion).
|
|
26
26
|
// - EC2 (kty 2): crv (-1), x (-2), y (-3) all present; x/y length == the curve field
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
// - CANONICAL: EXACTLY the type's parameters, nothing more (EC2 = 5, OKP/RSA = 4) --
|
|
30
30
|
// rejects a padded key / a private "d" component / kid / key_ops (WebAuthn 6.5.1
|
|
31
31
|
// CTAP2 canonical, stricter than open COSE `* label => values`).
|
|
32
|
-
// - PROFILE: alg <-> kty (and, for EC2, alg <-> crv) consistent; -8 (EdDSA) is
|
|
33
|
-
//
|
|
34
|
-
// - COMPRESSED: an EC2 credential key MUST use the uncompressed point form
|
|
35
|
-
// coordinate byte string, never a CBOR bool sign bit
|
|
32
|
+
// - PROFILE: alg <-> kty (and, for EC2, alg <-> crv) consistent; -8 (EdDSA) is only
|
|
33
|
+
// Ed25519; the RFC 9864 fully-specified ids (-9/-51/-52/-19/-53) are accepted.
|
|
34
|
+
// - COMPRESSED: an EC2 credential key MUST use the uncompressed point form, per WebAuthn
|
|
35
|
+
// sec. alg identifier: y is a full coordinate byte string, never a CBOR bool sign bit.
|
|
36
36
|
// - ON-CURVE: the public key point MUST be valid for its curve. For EC2 the SPKI is
|
|
37
37
|
// imported via node:crypto so OpenSSL validates the point (an off-curve or identity
|
|
38
|
-
// point fails to parse). For OKP, OpenSSL does
|
|
38
|
+
// point fails to parse). For OKP, OpenSSL does not validate the Edwards point on
|
|
39
39
|
// import (an all-zeroes key parses, and even verifies a trivial signature), so the
|
|
40
40
|
// point is checked explicitly via edwards-point (RFC 8032 decode + cofactor low-order
|
|
41
|
-
// rejection)
|
|
41
|
+
// rejection). An off-curve, non-canonical, or low-order OKP key fails closed.
|
|
42
42
|
|
|
43
43
|
var cbor = require("./cbor-det");
|
|
44
44
|
var asn1 = require("./asn1-der");
|
|
@@ -54,7 +54,7 @@ var EC2_CRV_OID = { 1: "prime256v1", 2: "secp384r1", 3: "secp521r1" };
|
|
|
54
54
|
var OKP_CRV = { 6: { oid: "Ed25519", len: 32 }, 7: { oid: "Ed448", len: 57 } };
|
|
55
55
|
// alg (label 3) -> the key type (and, for EC2, curve) it pins. WebAuthn (sec. alg
|
|
56
56
|
// identifier) adds guarantees over the open COSE registry: an ECDSA alg fixes its curve,
|
|
57
|
-
// -8 (EdDSA) is Ed25519
|
|
57
|
+
// -8 (EdDSA) is only Ed25519, and the RFC 9864 fully-specified ids (-9 ESP256, -51 ESP384,
|
|
58
58
|
// -52 ESP512, -19 Ed25519, -53 Ed448) each pin key type + curve. A verifier accepts the
|
|
59
59
|
// fully-specified ids even though WebAuthn recommends against them for credential creation.
|
|
60
60
|
// The RSA credential-key bounds. 2048 bits is the floor every current FIDO authenticator and
|
|
@@ -73,8 +73,8 @@ var ALG_PROFILE = {
|
|
|
73
73
|
"-9": { kty: 2, crv: 1 }, "-51": { kty: 2, crv: 2 }, "-52": { kty: 2, crv: 3 },
|
|
74
74
|
"-8": { kty: 1, crv: 6 }, "-19": { kty: 1, crv: 6 }, "-53": { kty: 1, crv: 7 },
|
|
75
75
|
// RSASSA-PSS at all three strengths. PS256 alone left PS384/PS512 refused at PARSE time on a key
|
|
76
|
-
// that is perfectly well-formed
|
|
77
|
-
// key
|
|
76
|
+
// that is perfectly well-formed (the same bytes accepted under -37), so the refusal blamed the
|
|
77
|
+
// key instead of the algorithm, and a relying party migrating credential rows written by another
|
|
78
78
|
// implementation could not tell which of its stored keys this verifier would decline, or why.
|
|
79
79
|
"-257": { kty: 3 }, "-258": { kty: 3 }, "-259": { kty: 3 },
|
|
80
80
|
"-37": { kty: 3 }, "-38": { kty: 3 }, "-39": { kty: 3 }, "-65535": { kty: 3 },
|
|
@@ -89,12 +89,12 @@ var ALG_PROFILE = {
|
|
|
89
89
|
// @validator-shape kty\s*===\s*2n
|
|
90
90
|
// @validator-shape EC2_CRV_LEN|ALG_PROFILE
|
|
91
91
|
// `unsupportedCode` is OPTIONAL and names the code raised when the key is well-formed but its
|
|
92
|
-
// algorithm is not one this verifier implements
|
|
92
|
+
// algorithm is not one this verifier implements, a different fact from a malformed key. Omit it
|
|
93
93
|
// and that case keeps raising `code`, so an existing caller sees no change.
|
|
94
94
|
function credentialKey(node, E, code, unsupportedCode) {
|
|
95
95
|
function bad(msg, cause) { return new E(code, msg, cause); }
|
|
96
96
|
if (!node || node.majorType !== 5) throw bad("a COSE_Key must be a CBOR map (RFC 9052 sec. 7)");
|
|
97
|
-
// Every parameter read maps a wrong-type cbor/* fault to the caller's domain
|
|
97
|
+
// Every parameter read maps a wrong-type cbor/* fault to the caller's domain: a
|
|
98
98
|
// wrong-typed COSE label (x as an integer, kty as a string) is bad input, not a leak.
|
|
99
99
|
function ib(label) { var n = cbor.read.mapGet(node, label); if (!n) return null; try { return cbor.read.byteString(n); } catch (e) { throw bad("COSE_Key parameter " + label + " must be a byte string", e); } }
|
|
100
100
|
function ii(label) { var n = cbor.read.mapGet(node, label); if (!n) return null; try { return cbor.read.int(n); } catch (e) { throw bad("COSE_Key parameter " + label + " must be an integer", e); } }
|
|
@@ -122,17 +122,17 @@ function credentialKey(node, E, code, unsupportedCode) {
|
|
|
122
122
|
} else if (kty === 3n) {
|
|
123
123
|
key.n = ib(-1); key.e = ib(-2);
|
|
124
124
|
if (!key.n || !key.n.length || !key.e || !key.e.length) throw bad("an RSA COSE_Key must carry n (-1) and e (-2)");
|
|
125
|
-
// The
|
|
126
|
-
// to, where EC2 pins x/y to the curve's field size and has the point validated on the curve,
|
|
125
|
+
// The material itself, not merely its presence: the same standard the other two key types are
|
|
126
|
+
// held to, where EC2 pins x/y to the curve's field size and has the point validated on the curve,
|
|
127
127
|
// and OKP pins x to an exact length. Checking only presence let a 1-byte modulus and an
|
|
128
128
|
// exponent of 1 through as conformant credential public keys, and both reach the WebCrypto
|
|
129
129
|
// import, so they reach real signature verification. e = 1 makes RSA the identity function:
|
|
130
130
|
// the "signature" is the message, and it verifies under any modulus.
|
|
131
|
-
//
|
|
132
|
-
// big-endian integers with no leading zero, and every check below reads a byte
|
|
131
|
+
// Both values first, before either is judged. RFC 8230 sec. 4 encodes n and e as unsigned
|
|
132
|
+
// big-endian integers with no leading zero, and every check below reads a byte length as though
|
|
133
133
|
// it were a magnitude: the modulus floor, the exponent bound, and the exponent's value. A
|
|
134
134
|
// padded encoding decouples the two, so `00 01` would be read as a two-byte exponent and skip
|
|
135
|
-
// the value check that refuses 1
|
|
135
|
+
// the value check that refuses 1, the degenerate key the whole check exists to catch. (An EC2
|
|
136
136
|
// coordinate is the opposite case, fixed-width and zero-padded by definition, which is why this
|
|
137
137
|
// rule is stated for the RSA parameters and not for x/y.)
|
|
138
138
|
if (key.n[0] === 0) throw bad("an RSA COSE_Key modulus (-1) must be minimally encoded, with no leading zero byte (RFC 8230 sec. 4)");
|
|
@@ -147,9 +147,9 @@ function credentialKey(node, E, code, unsupportedCode) {
|
|
|
147
147
|
// so a caller cannot hand over a megabyte of exponent for the modular exponentiation to chew.
|
|
148
148
|
if (key.e.length > RSA_MAX_EXPONENT_BYTES) throw bad("an RSA COSE_Key exponent (-2) is longer than " + RSA_MAX_EXPONENT_BYTES + " bytes");
|
|
149
149
|
if ((key.e[key.e.length - 1] & 1) === 0) throw bad("an RSA COSE_Key exponent (-2) must be odd");
|
|
150
|
-
// Minimal encoding above makes a one-byte e the
|
|
150
|
+
// Minimal encoding above makes a one-byte e the only way to express a value this small, so the
|
|
151
151
|
// comparison is on the value and not on where it happens to sit.
|
|
152
|
-
if (key.e.length === 1 && key.e[0] <= 1) throw bad("an RSA COSE_Key exponent (-2) must be greater than 1
|
|
152
|
+
if (key.e.length === 1 && key.e[0] <= 1) throw bad("an RSA COSE_Key exponent (-2) must be greater than 1; e = 1 makes RSA the identity function");
|
|
153
153
|
} else {
|
|
154
154
|
throw bad("unsupported COSE_Key kty " + Number(kty));
|
|
155
155
|
}
|
|
@@ -170,40 +170,39 @@ function credentialKey(node, E, code, unsupportedCode) {
|
|
|
170
170
|
// form their datastore round-trips, which is not a choice about how carefully their
|
|
171
171
|
// credential is checked.
|
|
172
172
|
//
|
|
173
|
-
// Everything above this line is about the
|
|
174
|
-
// strings, the canonical parameter count
|
|
173
|
+
// Everything above this line is about the encoding (a CBOR map, integer labels, byte
|
|
174
|
+
// strings, the canonical parameter count) and can only be asked of bytes. Everything
|
|
175
175
|
// here is about the key, and is asked of both.
|
|
176
176
|
//
|
|
177
177
|
// @enforced-by behavioral -- key-material rules have no rename-proof code shape distinct
|
|
178
|
-
// from ordinary length and byte comparisons; the RED vectors that drive
|
|
178
|
+
// from ordinary length and byte comparisons; the RED vectors that drive both accepted
|
|
179
179
|
// forms of a stored credential key (the COSE bytes and the object) through
|
|
180
180
|
// pki.webauthn.verifyAssertion with an undersized modulus, e = 1, a curve/length
|
|
181
181
|
// mismatch and a short OKP x are the guard.
|
|
182
182
|
function assertKeyMaterial(key, E, code, unsupportedCode) {
|
|
183
183
|
function bad(msg, cause) { return new E(code, msg, cause); }
|
|
184
184
|
if (!key || typeof key !== "object") throw bad("a credential key must be a decoded COSE_Key object");
|
|
185
|
-
//
|
|
185
|
+
// One read of each field, into a plain object, before anything is checked or used.
|
|
186
186
|
//
|
|
187
|
-
// The object form comes from the caller, so any of these can be an accessor. One that
|
|
188
|
-
// turns a validation into a raw fault
|
|
189
|
-
// that returns
|
|
190
|
-
// field that is used two different values, which
|
|
191
|
-
//
|
|
192
|
-
// rather than for the ones a particular branch happens to reach.
|
|
187
|
+
// The object form comes from the caller, so any of these can be an accessor. One that throws
|
|
188
|
+
// turns a validation into a raw fault, the thing this function exists to prevent, and one
|
|
189
|
+
// that returns different values on successive reads makes the field that was checked and the
|
|
190
|
+
// field that is used two different values, which defeats the check outright. Reading each
|
|
191
|
+
// exactly once settles both, for every field, not just the ones a particular branch reaches.
|
|
193
192
|
try {
|
|
194
193
|
key = { kty: key.kty, alg: key.alg, crv: key.crv, x: key.x, y: key.y, n: key.n, e: key.e };
|
|
195
194
|
} catch (e) { throw bad("a credential key field could not be read", e); }
|
|
196
|
-
// Then the
|
|
197
|
-
// a raw RangeError on a fractional or non-finite number, so "it is a number"
|
|
198
|
-
// "it is an integer"
|
|
199
|
-
// happens to need first:
|
|
195
|
+
// Then the type and the value. BigInt() throws a raw TypeError on a Symbol and on undefined, and
|
|
196
|
+
// a raw RangeError on a fractional or non-finite number, so the check is not "it is a number";
|
|
197
|
+
// it is "it is an integer". Every integer label the branches below read, not the two the
|
|
198
|
+
// dispatch happens to need first:
|
|
200
199
|
// crv indexes a lookup table, and a Symbol thrown at a property read is the same raw fault as a
|
|
201
200
|
// Symbol thrown at BigInt(). The decoded form gets these from the CBOR reader, which has already
|
|
202
201
|
// established them; the object form gets them from the caller, so this is where they are settled.
|
|
203
202
|
// A BigInt is bounded too. COSE labels are small registry integers, and an unbounded one converts
|
|
204
|
-
// to Infinity, which then throws a raw RangeError at the next conversion
|
|
205
|
-
// fractional number, reached by a value that
|
|
206
|
-
// check either; "it is an integer this code can carry" is.
|
|
203
|
+
// to Infinity, which then throws a raw RangeError at the next conversion: the same defeat as a
|
|
204
|
+
// fractional number, reached by a value that is an integer. So "it is an integer" is not the
|
|
205
|
+
// whole check either; "it is an integer this code can carry" is.
|
|
207
206
|
var MAX = BigInt(Number.MAX_SAFE_INTEGER);
|
|
208
207
|
function _isInt(v) {
|
|
209
208
|
if (typeof v === "bigint") return v <= MAX && v >= -MAX;
|
|
@@ -212,8 +211,8 @@ function assertKeyMaterial(key, E, code, unsupportedCode) {
|
|
|
212
211
|
if (!_isInt(key.kty)) throw bad("a COSE_Key kty (label 1) must be an integer");
|
|
213
212
|
if (!_isInt(key.alg)) throw bad("a COSE_Key alg (label 3) must be an integer");
|
|
214
213
|
if (key.crv !== undefined && key.crv !== null && !_isInt(key.crv)) throw bad("a COSE_Key crv (label -1) must be an integer");
|
|
215
|
-
//
|
|
216
|
-
// hands out BigInt
|
|
214
|
+
// One representation from here down. A label may arrive as a Number or a BigInt (a CBOR reader
|
|
215
|
+
// hands out BigInt; an object built in JavaScript is likelier to hold Number), and everything
|
|
217
216
|
// below compares with === against the Number-keyed profile table and the curve tables. Accepting
|
|
218
217
|
// both forms at the gate and then comparing only one is a check that answers by how the caller
|
|
219
218
|
// happened to spell the value; the decoded arm normalizes here too, for the same reason.
|
|
@@ -236,14 +235,14 @@ function assertKeyMaterial(key, E, code, unsupportedCode) {
|
|
|
236
235
|
if (bits < RSA_MIN_MODULUS_BITS) throw bad("an RSA COSE_Key modulus (-1) is " + bits + " bits, below the " + RSA_MIN_MODULUS_BITS + "-bit minimum");
|
|
237
236
|
if (key.e.length > RSA_MAX_EXPONENT_BYTES) throw bad("an RSA COSE_Key exponent (-2) is longer than " + RSA_MAX_EXPONENT_BYTES + " bytes");
|
|
238
237
|
if ((key.e[key.e.length - 1] & 1) === 0) throw bad("an RSA COSE_Key exponent (-2) must be odd");
|
|
239
|
-
if (key.e.length === 1 && key.e[0] <= 1) throw bad("an RSA COSE_Key exponent (-2) must be greater than 1
|
|
238
|
+
if (key.e.length === 1 && key.e[0] <= 1) throw bad("an RSA COSE_Key exponent (-2) must be greater than 1; e = 1 makes RSA the identity function");
|
|
240
239
|
} else {
|
|
241
240
|
throw bad("unsupported COSE_Key kty " + Number(key.kty));
|
|
242
241
|
}
|
|
243
242
|
// PROFILE: the declared alg must match the key type (and, for EC2, the curve).
|
|
244
243
|
var prof = ALG_PROFILE[String(key.alg)];
|
|
245
|
-
// An algorithm this verifier does not implement is
|
|
246
|
-
// well-formed
|
|
244
|
+
// An algorithm this verifier does not implement is not a malformed key. The key can be perfectly
|
|
245
|
+
// well-formed (the same bytes may parse under a neighbouring algorithm id), and a relying
|
|
247
246
|
// party migrating credential rows written elsewhere needs to tell "I cannot check this
|
|
248
247
|
// algorithm" from "these bytes are wrong", since only one of those is fixable by re-registering.
|
|
249
248
|
// Callers that do not distinguish the two pass one code and keep the previous behaviour.
|
|
@@ -254,8 +253,8 @@ function assertKeyMaterial(key, E, code, unsupportedCode) {
|
|
|
254
253
|
// x/y or the identity point fails to parse here.
|
|
255
254
|
try { nodeCrypto.createPublicKey({ key: toSpki(key, E, code), format: "der", type: "spki" }); }
|
|
256
255
|
catch (e) { throw bad("the credential public key point is not valid for its curve", e); }
|
|
257
|
-
// OpenSSL does
|
|
258
|
-
// parses, and even verifies a trivial signature
|
|
256
|
+
// OpenSSL does not validate an OKP (Ed25519/Ed448) point on import. An all-zeroes key
|
|
257
|
+
// parses, and even verifies a trivial signature, so an OKP point needs an explicit
|
|
259
258
|
// on-curve + full-order (non-low-order) check (RFC 8032 decode + the cofactor check).
|
|
260
259
|
if (Number(key.kty) === 1 && !edwardsPoint.validate(key.x, key.crv)) throw bad("the OKP credential public key is not a valid, full-order Edwards point");
|
|
261
260
|
return key;
|
package/lib/validator-keydesc.js
CHANGED
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
// schema):
|
|
20
20
|
// - KeyDescription is a positional SEQUENCE; attestationChallenge (position 4) MUST equal
|
|
21
21
|
// clientDataHash.
|
|
22
|
-
// - allApplications (tag 600) MUST be absent from
|
|
22
|
+
// - allApplications (tag 600) MUST be absent from both the softwareEnforced (position 6)
|
|
23
23
|
// and teeEnforced (position 7) AuthorizationLists (the key MUST be scoped to the RP).
|
|
24
24
|
// - origin (tag 702) MUST equal KM_ORIGIN_GENERATED (0): at least one list declares it,
|
|
25
25
|
// and every list that declares one says GENERATED (a mixed IMPORTED/GENERATED key is
|
|
26
26
|
// contradictory and rejected).
|
|
27
|
-
// - purpose (tag 1) MUST be
|
|
27
|
+
// - purpose (tag 1) MUST be exactly { KM_PURPOSE_SIGN (2) } over the union of the lists.
|
|
28
28
|
|
|
29
29
|
var asn1 = require("./asn1-der");
|
|
30
30
|
|
|
@@ -53,7 +53,7 @@ function _purposeUnion(a, b, E, code) {
|
|
|
53
53
|
return out;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// androidKeyDescription(cert, clientDataHash, exts, E, code, failCode)
|
|
56
|
+
// androidKeyDescription(cert, clientDataHash, exts, E, code, failCode) is the complete
|
|
57
57
|
// WebAuthn 8.4.1 KeyDescription gate. A verifier MUST route the android-key description
|
|
58
58
|
// through here, never re-derive a partial subset of the four checks inline.
|
|
59
59
|
// @enforced-by behavioral -- the challenge / allApplications / origin / purpose checks are
|