@blamejs/pki 0.6.14 → 0.6.16
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 +27 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +140 -286
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/validator-tls.js
CHANGED
|
@@ -1,85 +1,21 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- no operator-facing namespace. The documented surface is the codec whose
|
|
6
|
-
// TLS handshake handling composes this validator (pki.tls).
|
|
7
|
-
//
|
|
8
|
-
// validator-tls -- the SINGLE home for the framing conformance of the TLS handshake
|
|
9
|
-
// structures that carry certificates: the RFC 8879 sec. 4 CompressedCertificate and the
|
|
10
|
-
// RFC 8446 sec. 4.4.2 Certificate message it decompresses to. Sibling to the guard family:
|
|
11
|
-
// a validator owns a decoded TYPE's COMPLETE framing rule set once, so the vector bounds,
|
|
12
|
-
// the minimum sizes and the trailing-byte rejection cannot drift as consumers are added.
|
|
13
|
-
//
|
|
14
|
-
// Every field is unsigned big-endian, packed with no padding, in the TLS presentation
|
|
15
|
-
// language: `uintN` is a fixed-width integer and `opaque<min..max>` is a length prefix wide
|
|
16
|
-
// enough for `max` followed by exactly that many bytes. The caller supplies its typed error
|
|
17
|
-
// constructor E (a validator takes a class, `new E(code, msg)`, where the guard family
|
|
18
|
-
// takes a factory) plus the `codes` its domain uses for each fault.
|
|
19
|
-
//
|
|
20
|
-
// Rule set (verbatim against RFC 8879 sec. 4 and RFC 8446 sec. 4.4.2):
|
|
21
|
-
// - compressedCertificate: uint16 algorithm, uint24 uncompressed_length, then
|
|
22
|
-
// opaque compressed_certificate_message<1..2^24-1>, where the minimum of 1 makes an empty
|
|
23
|
-
// compressed body a framing violation, which matters because the three decompressors do
|
|
24
|
-
// not agree on what empty input means. Bytes after the vector are rejected: one chain
|
|
25
|
-
// must have exactly one encoding.
|
|
26
|
-
// - certificateMessage: opaque certificate_request_context<0..2^8-1>, then
|
|
27
|
-
// CertificateEntry certificate_list<0..2^24-1>, each entry an opaque<1..2^24-1> payload
|
|
28
|
-
// (cert_data, or a SubjectPublicKeyInfo under RFC 7250 RawPublicKey) plus
|
|
29
|
-
// Extension extensions<0..2^16-1>. Bytes after the list are rejected. A zero-length
|
|
30
|
-
// entry payload is a framing violation, not a blank certificate.
|
|
31
|
-
//
|
|
32
|
-
// This validator decodes and bounds STRUCTURE only. Which algorithms are acceptable, what
|
|
33
|
-
// the output cap is, and whether the recovered length matches its declaration are the
|
|
34
|
-
// codec's policy and stay with the codec.
|
|
35
|
-
//
|
|
36
|
-
// The RFC 8446 sec. 4.4.2 / sec. 4.2 rules that bind a Certificate-message DECODER, and where
|
|
37
|
-
// each one lands, enumerated so the ones deliberately left unenforced are a recorded decision
|
|
38
|
-
// and not an oversight:
|
|
39
|
-
// ENFORCED here:
|
|
40
|
-
// - RawPublicKey carries at most one CertificateEntry (sec. 4.4.2).
|
|
41
|
-
// - No extension type appears twice in one extension block (sec. 4.2).
|
|
42
|
-
// - Every vector's framing, its declared minimum, and no trailing bytes at either level.
|
|
43
|
-
// Not enforced, deliberately:
|
|
44
|
-
// - "The sender's certificate MUST come in the first CertificateEntry" is about which
|
|
45
|
-
// certificate a consumer treats as the leaf, not about framing. Entries are surfaced in
|
|
46
|
-
// wire order and never reordered, so the caller sees exactly what arrived, and the same
|
|
47
|
-
// section tells implementations to tolerate extraneous certificates and arbitrary
|
|
48
|
-
// orderings beyond the first, so refusing any order here would be wrong.
|
|
49
|
-
// - "The server's certificate_list MUST always be non-empty" is conditioned on the sender's
|
|
50
|
-
// role: a client legitimately sends an empty list when it has no certificate to offer.
|
|
51
|
-
// This decoder has no role, so an empty list decodes to zero entries and the caller, which
|
|
52
|
-
// does know the role, decides.
|
|
53
|
-
// - Extension correspondence ("extensions MUST correspond to ones from the client") and any
|
|
54
|
-
// extension's own contents (a status_request body being a CertificateStatus) need the
|
|
55
|
-
// negotiation state and the extension's semantics. Neither exists here: extension values
|
|
56
|
-
// are surfaced raw.
|
|
57
|
-
// - The OpenPGP certificate type is unreachable, not checked: the only accepted
|
|
58
|
-
// types are X509 and RawPublicKey, and anything else is refused at the entry point.
|
|
4
|
+
// @internal
|
|
59
5
|
|
|
60
6
|
var C = require("./constants");
|
|
61
7
|
var ByteReader = require("./byte-reader");
|
|
62
8
|
|
|
63
|
-
// The widest an opaque<..2^24-1> vector can be, and the width of the length prefix that
|
|
64
|
-
// frames it. Both come straight from the TLS presentation language.
|
|
65
9
|
var MAX_VECTOR_24 = 0xffffff;
|
|
66
10
|
var MAX_VECTOR_16 = 0xffff;
|
|
67
11
|
|
|
68
|
-
// RFC 8446 sec. 4.4.2 frames a Certificate message as a 1-byte-prefixed request context plus
|
|
69
|
-
// a 3-byte-prefixed entry list, so the smallest well-formed message is 4 bytes.
|
|
70
12
|
var MIN_CERT_MSG_BYTES = 4;
|
|
71
13
|
|
|
72
|
-
// compressedCertificate(view, E, codes) -> { algorithm, uncompressedLength, body }.
|
|
73
|
-
// Decode the RFC 8879 sec. 4 framing. `codes` names { truncated, framing, trailing }.
|
|
74
14
|
// @enforced-by behavioral -- a packed TLS presentation-language decode has no rename-proof
|
|
75
|
-
// code shape; the framing rules are pinned by the RED conformance vectors that drive the
|
|
76
|
-
// shipped consumer (pki.tls.decompressCertificate) on each malformed shape.
|
|
77
15
|
function compressedCertificate(view, E, codes) {
|
|
78
16
|
var r = new ByteReader(view, 0, view.length, E, codes.truncated);
|
|
79
17
|
var algorithm = r.u16(codes.truncated);
|
|
80
18
|
var uncompressedLength = r.u24(codes.truncated);
|
|
81
|
-
// opaque compressed_certificate_message<1..2^24-1>, minimum 1, so an empty body is a
|
|
82
|
-
// framing violation and not something a decompressor gets to interpret.
|
|
83
19
|
var body = r.vector(3, 1, MAX_VECTOR_24, codes.framing);
|
|
84
20
|
if (!r.atEnd()) {
|
|
85
21
|
throw new E(codes.trailing, "the CompressedCertificate carries " + r.remaining() +
|
|
@@ -88,25 +24,13 @@ function compressedCertificate(view, E, codes) {
|
|
|
88
24
|
return { algorithm: algorithm, uncompressedLength: uncompressedLength, body: Buffer.from(body) };
|
|
89
25
|
}
|
|
90
26
|
|
|
91
|
-
// The extensions vector of a CertificateEntry, decoded to its records. RFC 8446 sec. 4.2 makes an
|
|
92
|
-
// Extension a uint16 type plus a uint16-prefixed value, so the vector is a whole number of them and
|
|
93
|
-
// the smallest is 4 bytes. Walking it is what keeps a malformed Certificate message from passing as
|
|
94
|
-
// structurally valid: a one-byte vector cannot be an Extension, and accepting the bytes opaquely
|
|
95
|
-
// would report it as well-formed. Each record's value is surfaced raw: this decodes the framing,
|
|
96
|
-
// never the extension's own contents.
|
|
97
27
|
function _extensionRecords(view, E, codes) {
|
|
98
28
|
var r = new ByteReader(view, 0, view.length, E, codes.truncated);
|
|
99
29
|
var out = [];
|
|
100
30
|
var seen = Object.create(null);
|
|
101
31
|
while (!r.atEnd()) {
|
|
102
|
-
// Each iteration consumes at least 4 bytes and every read is bounds-checked, so the walk
|
|
103
|
-
// always terminates or faults.
|
|
104
32
|
var type = r.u16(codes.framing);
|
|
105
33
|
var data = r.vector(2, 0, MAX_VECTOR_16, codes.framing);
|
|
106
|
-
// RFC 8446 sec. 4.2: "There MUST NOT be more than one extension of the same type in a given
|
|
107
|
-
// extension block." Left unenforced, one type could appear twice and a consumer reading only
|
|
108
|
-
// the first (or only the last) would act on a different value than another implementation --
|
|
109
|
-
// the same one-structure-two-meanings ambiguity a duplicate DER SET member creates.
|
|
110
34
|
if (seen[type]) {
|
|
111
35
|
throw new E(codes.framing, "the extension block carries more than one extension of type " +
|
|
112
36
|
type + " (RFC 8446 sec. 4.2)");
|
|
@@ -117,14 +41,7 @@ function _extensionRecords(view, E, codes) {
|
|
|
117
41
|
return out;
|
|
118
42
|
}
|
|
119
43
|
|
|
120
|
-
// certificateMessage(view, E, codes, certificateType) -> { certificateRequestContext, entries }.
|
|
121
|
-
// Decode the RFC 8446 sec. 4.4.2 Certificate message. `codes` names { truncated, framing,
|
|
122
|
-
// trailing }. `certificateType` is "X509" or "RawPublicKey" and is DECLARED by the caller --
|
|
123
|
-
// it is negotiated by a separate extension (RFC 7250) and is not present in these bytes, so
|
|
124
|
-
// it can never be inferred from them.
|
|
125
44
|
// @enforced-by behavioral -- a packed TLS presentation-language decode has no rename-proof
|
|
126
|
-
// code shape; the framing rules are pinned by the RED conformance vectors that drive the
|
|
127
|
-
// shipped consumer (pki.tls.parseCertificateMessage) on each malformed shape.
|
|
128
45
|
function certificateMessage(view, E, codes, certificateType) {
|
|
129
46
|
var r = new ByteReader(view, 0, view.length, E, codes.truncated);
|
|
130
47
|
var ctxLen = r.u8(codes.truncated);
|
|
@@ -135,10 +52,6 @@ function certificateMessage(view, E, codes, certificateType) {
|
|
|
135
52
|
throw new E(codes.trailing, "the Certificate message carries " + r.remaining() +
|
|
136
53
|
" byte(s) after the certificate list");
|
|
137
54
|
}
|
|
138
|
-
// RFC 8446 sec. 4.4.2: under a negotiated RawPublicKey certificate type the list "MUST contain
|
|
139
|
-
// no more than one CertificateEntry". Otherwise the count is bounded because the byte ceiling
|
|
140
|
-
// does not bound it -- the smallest legal entry is 6 bytes, so a message well inside the
|
|
141
|
-
// framing limit can declare millions, each costing far more heap than wire.
|
|
142
55
|
var maxEntries = certificateType === "RawPublicKey" ? 1 : C.LIMITS.TLS_CERT_MAX_ENTRIES;
|
|
143
56
|
var entries = [];
|
|
144
57
|
while (!list.atEnd()) {
|
package/lib/validator-tpm.js
CHANGED
|
@@ -1,30 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- no operator-facing namespace. The documented surface is the verifier whose
|
|
6
|
-
// tpm handling composes this validator (pki.webauthn).
|
|
7
|
-
//
|
|
8
|
-
// validator-tpm -- the SINGLE home for the TPM 2.0 structure conformance a WebAuthn tpm
|
|
9
|
-
// attestation carries: the TPMT_PUBLIC (pubArea) and TPMS_ATTEST (certInfo) decode, plus the
|
|
10
|
-
// pubArea-key == credential-key binding (WebAuthn sec. 8.3 + TCG TPM 2.0 Part 2). Sibling to
|
|
11
|
-
// the guard family: a validator owns a decoded TYPE's COMPLETE conformance rule set once, so
|
|
12
|
-
// the strict TPM2B sizing / algorithm-union walking / trailing-byte rejection cannot drift.
|
|
13
|
-
//
|
|
14
|
-
// Every field is unsigned big-endian, packed with no padding; every TPM2B_* is a UINT16 size
|
|
15
|
-
// followed by exactly `size` bytes. The bare certInfo and pubArea carry NO outer TPM2B size
|
|
16
|
-
// prefix. The caller supplies its typed error CONSTRUCTOR E + a structural `code` (a
|
|
17
|
-
// malformed structure is bad input) and, for the key binding, a `mismatchCode`.
|
|
18
|
-
//
|
|
19
|
-
// Rule set (gap-checked verbatim against WebAuthn sec. 8.3 + TCG TPM 2.0 Part 2 sec. 10.12 /
|
|
20
|
-
// 12.2):
|
|
21
|
-
// - parsePubArea: decode type/nameAlg, walk parameters per the algorithm union, read the
|
|
22
|
-
// public key in `unique`; reject trailing bytes (they perturb the TPM Name hash).
|
|
23
|
-
// - parseCertInfo: magic == TPM_GENERATED_VALUE, type == TPM_ST_ATTEST_CERTIFY; read
|
|
24
|
-
// extraData + attested.name; reject trailing bytes past the attested structure.
|
|
25
|
-
// - pubKeyEqualsCose: the pubArea key (EC curve+x+y, or RSA modulus+exponent) equals the
|
|
26
|
-
// credential COSE key, compared as unsigned magnitudes (a TPM2B may differ from a COSE
|
|
27
|
-
// coordinate by a leading 0x00), the RSA exponent over its full UINT32 width.
|
|
4
|
+
// @internal
|
|
28
5
|
|
|
29
6
|
var ByteReader = require("./byte-reader");
|
|
30
7
|
var guard = require("./guard-all");
|
|
@@ -33,11 +10,8 @@ var TPM_GENERATED_VALUE = 0xff544347;
|
|
|
33
10
|
var TPM_ST_ATTEST_CERTIFY = 0x8017;
|
|
34
11
|
var TPM_ALG = { RSA: 0x0001, SHA1: 0x0004, SHA256: 0x000b, SHA384: 0x000c, SHA512: 0x000d, NULL: 0x0010, ECC: 0x0023 };
|
|
35
12
|
var TPM_ALG_HASH = {}; TPM_ALG_HASH[TPM_ALG.SHA1] = "sha1"; TPM_ALG_HASH[TPM_ALG.SHA256] = "sha256"; TPM_ALG_HASH[TPM_ALG.SHA384] = "sha384"; TPM_ALG_HASH[TPM_ALG.SHA512] = "sha512";
|
|
36
|
-
// TPM_ECC_CURVE -> COSE crv (the codepoints differ; this is a mapping, not equality).
|
|
37
13
|
var TPM_CURVE_TO_COSE = {}; TPM_CURVE_TO_COSE[0x0003] = 1; TPM_CURVE_TO_COSE[0x0004] = 2; TPM_CURVE_TO_COSE[0x0005] = 3;
|
|
38
14
|
|
|
39
|
-
// Unsigned big-endian magnitude compare: strip leading zero octets, then byte-equal. TPM2B
|
|
40
|
-
// buffers and COSE fixed-length coordinates can differ by a leading 0x00.
|
|
41
15
|
function _ucmp(a, b) {
|
|
42
16
|
function strip(x) { var i = 0; while (i < x.length - 1 && x[i] === 0) i++; return x.subarray(i); }
|
|
43
17
|
return Buffer.compare(strip(a), strip(b)) === 0;
|
|
@@ -46,21 +20,9 @@ function _uintBytes(n) {
|
|
|
46
20
|
var hex = n.toString(16); if (hex.length % 2) hex = "0" + hex;
|
|
47
21
|
var b = Buffer.from(hex, "hex"); var i = 0; while (i < b.length - 1 && b[i] === 0) i++; return b.subarray(i);
|
|
48
22
|
}
|
|
49
|
-
// TPMT_SYM_DEF_OBJECT: algorithm UINT16; when not NULL, keyBits+mode follow. An attestation
|
|
50
|
-
// (restricted signing) key is NULL, so the non-NULL arm is defensive.
|
|
51
23
|
function _symDef(r) { if (r.u16() !== TPM_ALG.NULL) { r.u16(); r.u16(); } }
|
|
52
|
-
// TPMT_*_SCHEME / TPMT_KDF_SCHEME: scheme UINT16; when not NULL, a TPMS_SCHEME_HASH (a single
|
|
53
|
-
// UINT16 hashAlg) follows for the signing/kdf schemes in scope.
|
|
54
24
|
function _scheme(r) { if (r.u16() !== TPM_ALG.NULL) { r.u16(); } }
|
|
55
25
|
|
|
56
|
-
// TPMA_OBJECT bit positions (TPM 2.0 Part 2 sec. 8.3.2, Table 33). A data row set, not a switch:
|
|
57
|
-
// a bit index absent here is reserved by definition, and the reserved mask below is its complement
|
|
58
|
-
// over the positions Table 33 marks "shall be zero" (bits 0, 3, 15:12 and 31:20).
|
|
59
|
-
// Null-prototype: every one of these tables is indexed by a CALLER-SUPPLIED name, and an inherited
|
|
60
|
-
// member ("constructor", "toString", "__proto__") would resolve to a truthy non-value. In a lookup
|
|
61
|
-
// that decides whether a name is known, that reads as "known" and the caller's policy silently
|
|
62
|
-
// applies nothing at all -- a fail-open. Applied to all three tables here, not only the one where
|
|
63
|
-
// it was noticed.
|
|
64
26
|
var TPMA_OBJECT_BITS = Object.assign(Object.create(null), {
|
|
65
27
|
fixedTPM: 1, stClear: 2, fixedParent: 4, sensitiveDataOrigin: 5, userWithAuth: 6,
|
|
66
28
|
adminWithPolicy: 7, firmwareLimited: 8, svnLimited: 9, noDA: 10,
|
|
@@ -68,78 +30,60 @@ var TPMA_OBJECT_BITS = Object.assign(Object.create(null), {
|
|
|
68
30
|
});
|
|
69
31
|
var TPMA_OBJECT_RESERVED = 0xfff0f009;
|
|
70
32
|
|
|
71
|
-
// _decodeAttributes(oa) -> the named booleans of a TPMA_OBJECT word.
|
|
72
33
|
// @enforced-by behavioral -- a bit-position table lookup has no rename-proof code shape; the RED
|
|
73
|
-
// vectors (each named bit read off a real attestation, and the sign-bit case) are the guard.
|
|
74
34
|
function _decodeAttributes(oa) {
|
|
75
35
|
var out = {};
|
|
76
36
|
Object.keys(TPMA_OBJECT_BITS).forEach(function (n) { out[n] = ((oa >>> TPMA_OBJECT_BITS[n]) & 1) === 1; });
|
|
77
37
|
return out;
|
|
78
38
|
}
|
|
79
39
|
|
|
80
|
-
// parsePubArea(buf, E, code) -> the decoded TPMT_PUBLIC (WebAuthn 8.3 item 17-20).
|
|
81
40
|
// @enforced-by behavioral -- a packed TPM structure decode has no rename-proof code shape;
|
|
82
|
-
// the RED vectors (trailing bytes, an unsupported type, a truncated TPM2B) are the guard.
|
|
83
41
|
function parsePubArea(buf, E, code) {
|
|
84
42
|
var r = new ByteReader(buf, 0, buf.length, E, code);
|
|
85
43
|
var type = r.u16(), nameAlg = r.u16();
|
|
86
|
-
// objectAttributes (TPM 2.0 Part 2 sec. 8.3.2 Table 33) and authPolicy (Table 87) are SURFACED,
|
|
87
|
-
// not gated: WebAuthn sec. 8.3 constrains only the `parameters` and `unique` fields of pubArea,
|
|
88
|
-
// so enforcing these would invent a conformance rule. A relying party that wants them applies its
|
|
89
|
-
// own policy, which it cannot do to a field it never sees. The bytes are trustworthy by the time
|
|
90
|
-
// a caller reads them -- pubArea is hashed into certInfo.attested.name, which the AIK signature
|
|
91
|
-
// covers, and both are verified before the result is built.
|
|
92
44
|
var objectAttributes = r.u32();
|
|
93
|
-
var authPolicy = r.vector(2, 0, null);
|
|
45
|
+
var authPolicy = r.vector(2, 0, null);
|
|
94
46
|
var pub = { type: type, nameAlg: nameAlg, nameAlgBytes: buf.subarray(2, 4),
|
|
95
47
|
objectAttributes: objectAttributes >>> 0, attributes: _decodeAttributes(objectAttributes),
|
|
96
48
|
authPolicy: authPolicy };
|
|
97
49
|
if (type === TPM_ALG.RSA) {
|
|
98
|
-
_symDef(r);
|
|
99
|
-
_scheme(r);
|
|
100
|
-
r.u16();
|
|
101
|
-
var exp = r.u32();
|
|
50
|
+
_symDef(r);
|
|
51
|
+
_scheme(r);
|
|
52
|
+
r.u16();
|
|
53
|
+
var exp = r.u32();
|
|
102
54
|
pub.exponent = exp === 0 ? 65537 : exp;
|
|
103
|
-
pub.rsa = r.vector(2, 0, null);
|
|
55
|
+
pub.rsa = r.vector(2, 0, null);
|
|
104
56
|
} else if (type === TPM_ALG.ECC) {
|
|
105
|
-
_symDef(r);
|
|
106
|
-
_scheme(r);
|
|
107
|
-
pub.curveId = r.u16();
|
|
108
|
-
_scheme(r);
|
|
109
|
-
pub.x = r.vector(2, 0, null);
|
|
110
|
-
pub.y = r.vector(2, 0, null);
|
|
57
|
+
_symDef(r);
|
|
58
|
+
_scheme(r);
|
|
59
|
+
pub.curveId = r.u16();
|
|
60
|
+
_scheme(r);
|
|
61
|
+
pub.x = r.vector(2, 0, null);
|
|
62
|
+
pub.y = r.vector(2, 0, null);
|
|
111
63
|
} else {
|
|
112
64
|
throw new E(code, "unsupported TPMT_PUBLIC type 0x" + type.toString(16));
|
|
113
65
|
}
|
|
114
|
-
// TPMT_PUBLIC ends with `unique`; trailing bytes mean a malformed pubArea (and would
|
|
115
|
-
// perturb the TPM Name hash), so the decode fails closed on them.
|
|
116
66
|
if (!r.atEnd()) throw new E(code, "pubArea has trailing bytes after the unique field (WebAuthn 8.3)");
|
|
117
67
|
return pub;
|
|
118
68
|
}
|
|
119
69
|
|
|
120
|
-
// parseCertInfo(buf, E, code) -> the decoded + magic/type-validated TPMS_ATTEST
|
|
121
|
-
// { extraData, attestedName } (WebAuthn 8.3 item 13-15).
|
|
122
70
|
// @enforced-by behavioral -- a packed TPM structure decode has no rename-proof code shape;
|
|
123
|
-
// the RED vectors (wrong magic, wrong type, trailing bytes) are the guard.
|
|
124
71
|
function parseCertInfo(buf, E, code) {
|
|
125
72
|
var r = new ByteReader(buf, 0, buf.length, E, code);
|
|
126
73
|
var magic = r.u32(), type = r.u16();
|
|
127
74
|
if (magic !== TPM_GENERATED_VALUE) throw new E(code, "certInfo magic is not TPM_GENERATED_VALUE (WebAuthn 8.3)");
|
|
128
75
|
if (type !== TPM_ST_ATTEST_CERTIFY) throw new E(code, "certInfo type is not TPM_ST_ATTEST_CERTIFY (WebAuthn 8.3)");
|
|
129
|
-
r.vector(2, 0, null);
|
|
130
|
-
var extraData = r.vector(2, 0, null);
|
|
131
|
-
r.u64(); r.u32(); r.u32(); r.u8();
|
|
132
|
-
r.u64();
|
|
133
|
-
var name = r.vector(2, 0, null);
|
|
134
|
-
r.vector(2, 0, null);
|
|
76
|
+
r.vector(2, 0, null);
|
|
77
|
+
var extraData = r.vector(2, 0, null);
|
|
78
|
+
r.u64(); r.u32(); r.u32(); r.u8();
|
|
79
|
+
r.u64();
|
|
80
|
+
var name = r.vector(2, 0, null);
|
|
81
|
+
r.vector(2, 0, null);
|
|
135
82
|
if (!r.atEnd()) throw new E(code, "certInfo has trailing bytes after the attested structure (WebAuthn 8.3)");
|
|
136
83
|
return { extraData: extraData, attestedName: name };
|
|
137
84
|
}
|
|
138
85
|
|
|
139
|
-
// pubKeyEqualsCose(pub, cose, E, mismatchCode, code) -- the pubArea public key MUST equal the
|
|
140
|
-
// credential COSE key (WebAuthn 8.3 item 22).
|
|
141
86
|
// @enforced-by behavioral -- a decoded-key equality check has no rename-proof code shape; the
|
|
142
|
-
// RED vectors (a mismatched EC curve/coordinate, a mismatched RSA modulus/exponent) are the guard.
|
|
143
87
|
function pubKeyEqualsCose(pub, cose, E, mismatchCode, code) {
|
|
144
88
|
if (pub.type === TPM_ALG.ECC) {
|
|
145
89
|
if (cose.kty !== 2 || TPM_CURVE_TO_COSE[pub.curveId] !== cose.crv || !_ucmp(pub.x, cose.x || Buffer.alloc(0)) || !_ucmp(pub.y, cose.y || Buffer.alloc(0))) {
|
|
@@ -148,9 +92,6 @@ function pubKeyEqualsCose(pub, cose, E, mismatchCode, code) {
|
|
|
148
92
|
return;
|
|
149
93
|
}
|
|
150
94
|
if (pub.type === TPM_ALG.RSA) {
|
|
151
|
-
// Compare the exponent as an unsigned integer over its full width (a UINT32 up to
|
|
152
|
-
// 0xFFFFFFFF); a fixed 3-byte re-encode would silently truncate an exponent > 0xFFFFFF
|
|
153
|
-
// and let a mismatched key pass (WebAuthn 8.3 item 22).
|
|
154
95
|
var e = _uintBytes(pub.exponent >>> 0);
|
|
155
96
|
if (cose.kty !== 3 || !_ucmp(pub.rsa, cose.n || Buffer.alloc(0)) || !_ucmp(e, cose.e || Buffer.alloc(0))) {
|
|
156
97
|
throw new E(mismatchCode, "the TPM pubArea RSA key does not equal the credential public key");
|
|
@@ -160,21 +101,13 @@ function pubKeyEqualsCose(pub, cose, E, mismatchCode, code) {
|
|
|
160
101
|
throw new E(code, "unsupported TPM pubArea key type");
|
|
161
102
|
}
|
|
162
103
|
|
|
163
|
-
// The one defined preset: the six TPMA_OBJECT bits every genuine attestation observed sets the same
|
|
164
|
-
// way. `decrypt`, `noDA` and `userWithAuth` are deliberately absent: they differ across real
|
|
165
|
-
// Windows Hello statements, so requiring any of them rejects working hardware. A second preset
|
|
166
|
-
// would be policy invention; a caller who wants different bits spells them out.
|
|
167
104
|
var TPM_POLICY_PROFILES = Object.assign(Object.create(null), {
|
|
168
105
|
"hardware-bound": { fixedTPM: true, fixedParent: true, sensitiveDataOrigin: true, sign: true, restricted: false, x509sign: false },
|
|
169
106
|
});
|
|
170
107
|
var _TPM_POLICY_KEYS = Object.assign(Object.create(null), { profile: 1, objectAttributes: 1, reservedBitsClear: 1, consistency: 1, authPolicy: 1 });
|
|
171
108
|
var _AUTH_POLICY_KEYS = Object.assign(Object.create(null), { present: 1, allow: 1 });
|
|
172
109
|
|
|
173
|
-
// Config-time validation of opts.tpmPolicy: a typo must never silently disable a check, so an
|
|
174
|
-
// unknown key or attribute name throws at the boundary.
|
|
175
110
|
// @enforced-by behavioral -- an opts-shape validator has no rename-proof code shape; the RED
|
|
176
|
-
// vectors (unknown key, unknown profile, unknown attribute, non-boolean value, and the
|
|
177
|
-
// sensitiveDataOrigin-without-fixedTPM rule) are the guard.
|
|
178
111
|
function normalizeObjectAttributePolicy(policy, E, code) {
|
|
179
112
|
if (policy === undefined) return null;
|
|
180
113
|
if (!policy || typeof policy !== "object" || Array.isArray(policy)) throw new E(code, "opts.tpmPolicy must be an object");
|
|
@@ -185,7 +118,6 @@ function normalizeObjectAttributePolicy(policy, E, code) {
|
|
|
185
118
|
if (!preset) throw new E(code, "opts.tpmPolicy.profile " + JSON.stringify(policy.profile) + " is not a defined profile");
|
|
186
119
|
Object.keys(preset).forEach(function (n) { want[n] = preset[n]; });
|
|
187
120
|
}
|
|
188
|
-
// An explicit map layers OVER the preset, so a caller can keep the profile and override one bit.
|
|
189
121
|
var explicit = policy.objectAttributes;
|
|
190
122
|
if (explicit !== undefined) {
|
|
191
123
|
if (!explicit || typeof explicit !== "object" || Array.isArray(explicit)) throw new E(code, "opts.tpmPolicy.objectAttributes must be an object");
|
|
@@ -195,9 +127,6 @@ function normalizeObjectAttributePolicy(policy, E, code) {
|
|
|
195
127
|
want[n] = explicit[n];
|
|
196
128
|
});
|
|
197
129
|
}
|
|
198
|
-
// sec. 8.3.3.5 NOTE 1: sensitiveDataOrigin only asserts the TPM generated the key when fixedTPM
|
|
199
|
-
// is also SET; otherwise the object could have been imported. Demanding it alone asserts
|
|
200
|
-
// nothing, and a caller who thinks otherwise should hear it at config time.
|
|
201
130
|
if (want.sensitiveDataOrigin === true && want.fixedTPM !== true) {
|
|
202
131
|
throw new E(code, "opts.tpmPolicy requires sensitiveDataOrigin without fixedTPM; on its own it does not establish that the TPM generated the key (TPM 2.0 Part 2 sec. 8.3.3.5)");
|
|
203
132
|
}
|
|
@@ -208,23 +137,10 @@ function normalizeObjectAttributePolicy(policy, E, code) {
|
|
|
208
137
|
var allow = null;
|
|
209
138
|
if (ap !== undefined) {
|
|
210
139
|
if (!ap || typeof ap !== "object" || Array.isArray(ap)) throw new E(code, "opts.tpmPolicy.authPolicy must be an object");
|
|
211
|
-
// The nested keys are enumerated for the same reason the top-level ones are: a misspelled
|
|
212
|
-
// `alow` would leave the allow-list unset, and the assertion would then impose no digest
|
|
213
|
-
// restriction at all -- the caller's policy silently doing nothing.
|
|
214
140
|
guard.identifier.assertKnownKeys(ap, _AUTH_POLICY_KEYS, function (c, m) { return new E(c, m); }, code, "opts.tpmPolicy.authPolicy has an unknown key ");
|
|
215
141
|
if (ap.present !== undefined && typeof ap.present !== "boolean") throw new E(code, "opts.tpmPolicy.authPolicy.present must be a boolean");
|
|
216
142
|
if (ap.allow !== undefined) {
|
|
217
143
|
if (!Array.isArray(ap.allow)) throw new E(code, "opts.tpmPolicy.authPolicy.allow must be an array");
|
|
218
|
-
// Every entry is decoded here, at config time, so the comparison only ever sees bytes.
|
|
219
|
-
// Node's hex decoder is permissive: it stops at the first character that is not a hex
|
|
220
|
-
// digit, so "<digest>zz" decodes back to the digest and a non-string decodes to an empty
|
|
221
|
-
// buffer. Either could match a key this policy was written to exclude, including the
|
|
222
|
-
// Empty Policy. An entry that is not a Buffer or a canonical even-length hex string is a
|
|
223
|
-
// caller error; it fails here, before it can become a digest nobody intended.
|
|
224
|
-
// The decoding runs through guard.encoding.hex, which owns the alphabet, the even-length
|
|
225
|
-
// rule and the canonical round-trip. Those three checks were once written out here by hand
|
|
226
|
-
// and now live in one place. It also decodes, so the hex path cannot validate one string
|
|
227
|
-
// and decode another.
|
|
228
144
|
allow = ap.allow.map(function (entry, i) {
|
|
229
145
|
if (guard.bytes.isByteSource(entry)) return guard.bytes.snapshotSource(entry, E, code, "opts.tpmPolicy.authPolicy.allow[" + i + "]");
|
|
230
146
|
var label = "opts.tpmPolicy.authPolicy.allow[" + i + "]";
|
|
@@ -241,41 +157,23 @@ function normalizeObjectAttributePolicy(policy, E, code) {
|
|
|
241
157
|
authPolicy: ap ? { present: ap.present === true, allow: allow } : null };
|
|
242
158
|
}
|
|
243
159
|
|
|
244
|
-
// Apply a normalized policy to a parsed pubArea. Nothing here is a WebAuthn sec. 8.3 requirement,
|
|
245
|
-
// since that section constrains only pubArea's `parameters` and `unique` fields. Every rule runs
|
|
246
|
-
// only because a caller asked for it by name.
|
|
247
160
|
// @enforced-by behavioral -- an opt-gated policy gate has no rename-proof code shape, and there is
|
|
248
|
-
// no general vector shape to detect: the RED vectors (each required bit in both directions, the
|
|
249
|
-
// reserved-bit mask across the sign bit, the restricted sign+decrypt combination, the over-long
|
|
250
|
-
// digest, the Empty Policy, and the allow-list miss) are the guard.
|
|
251
161
|
function assertObjectAttributePolicy(pub, policy, E, policyCode, structuralCode) {
|
|
252
162
|
if (!policy) return;
|
|
253
163
|
var oa = pub.objectAttributes >>> 0;
|
|
254
|
-
// Bit 31 lies inside the reserved mask, so the AND must be coerced back to unsigned: a bare
|
|
255
|
-
// `oa & 0xfff0f009` is a signed int32, and a negative result would not read as "a bit is set".
|
|
256
164
|
if (policy.reservedBitsClear && ((oa & TPMA_OBJECT_RESERVED) >>> 0) !== 0) {
|
|
257
165
|
throw new E(structuralCode, "the TPMT_PUBLIC objectAttributes sets a reserved bit (TPM 2.0 Part 2 sec. 8.3.2 Table 33: shall be zero)");
|
|
258
166
|
}
|
|
259
167
|
if (policy.consistency) {
|
|
260
|
-
// sec. 8.3.3.11, verbatim: "This attribute shall not be SET in any object that has fixedTPM
|
|
261
|
-
// SET." A key that cannot leave its TPM cannot be duplicated, so requiring its duplication be
|
|
262
|
-
// encrypted describes an object that cannot exist.
|
|
263
168
|
if (pub.attributes.encryptedDuplication && pub.attributes.fixedTPM) {
|
|
264
169
|
throw new E(structuralCode, "the TPMT_PUBLIC objectAttributes sets encryptedDuplication on an object with fixedTPM SET (TPM 2.0 Part 2 sec. 8.3.3.11)");
|
|
265
170
|
}
|
|
266
|
-
// sec. 8.3.3.16/.17: a restricted key is a TPM-internal key, and the two use bits are a choice
|
|
267
|
-
// -- a restricted object that both signs and decrypts is not a defined combination.
|
|
268
171
|
if (pub.attributes.sign && pub.attributes.decrypt && pub.attributes.restricted) {
|
|
269
172
|
throw new E(structuralCode, "the TPMT_PUBLIC objectAttributes sets restricted with both sign and decrypt (TPM 2.0 Part 2 sec. 8.3.3)");
|
|
270
173
|
}
|
|
271
|
-
// A TPM2B_DIGEST carries at most a SHA-512 digest.
|
|
272
174
|
if (pub.authPolicy.length > 64) {
|
|
273
175
|
throw new E(structuralCode, "the TPMT_PUBLIC authPolicy is " + pub.authPolicy.length + " octets, above the 64-octet TPM2B_DIGEST maximum");
|
|
274
176
|
}
|
|
275
|
-
// The remaining sec. 8.3.3 "shall" statements are all parent-relative. fixedTPM, stClear and
|
|
276
|
-
// encryptedDuplication are each constrained against the value the object's parent carries. An
|
|
277
|
-
// attestation presents one public area and no parent, so a verifier cannot evaluate them at
|
|
278
|
-
// all; they are not omitted by oversight, they are unverifiable from what is on the wire.
|
|
279
177
|
}
|
|
280
178
|
Object.keys(policy.objectAttributes).forEach(function (name) {
|
|
281
179
|
var want = policy.objectAttributes[name];
|
|
@@ -290,7 +188,6 @@ function assertObjectAttributePolicy(pub, policy, E, policyCode, structuralCode)
|
|
|
290
188
|
throw new E(policyCode, "the TPM credential key carries the Empty Policy; opts.tpmPolicy.authPolicy.present requires a policy digest");
|
|
291
189
|
}
|
|
292
190
|
if (ap.allow) {
|
|
293
|
-
// Every entry was decoded and validated at config time, so this compares digests only.
|
|
294
191
|
var got = pub.authPolicy;
|
|
295
192
|
var ok = ap.allow.some(function (want) {
|
|
296
193
|
return want.length === got.length && guard.crypto.constantTimeEqual(want, got);
|
package/lib/verify-seam.js
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- the injected-signature-engine seam, once.
|
|
6
|
-
//
|
|
7
|
-
// A CRL, a PKCS#10 certification request and an RFC 4211 CertReqMsg all verify an untrusted
|
|
8
|
-
// signature through the same path-validation engine, which carries the algorithm-confusion, EdDSA
|
|
9
|
-
// low-order and composite gates. None of them may reach that engine through pki.path's public
|
|
10
|
-
// surface, so each holds a slot that path-validate fills at its module load. Each was writing the
|
|
11
|
-
// same slot, the same wiring assertion and the same octet-alignment refusal. That shape lives here
|
|
12
|
-
// now. A format module supplies what is its own: an error class, a code, and the preimage bytes.
|
|
13
|
-
//
|
|
14
|
-
// The producing side of each format asks a different question, through pki-build's
|
|
15
|
-
// assertSignatureVerifies: whether the signature it has just made verifies under a key the caller
|
|
16
|
-
// already controls. That check waives the EdDSA low-order-point gate for that reason, and is a
|
|
17
|
-
// separate mechanism from this one.
|
|
4
|
+
// @internal
|
|
18
5
|
|
|
19
6
|
var guard = require("./guard-all");
|
|
20
7
|
|
|
21
|
-
// makeSeam(name, ErrorClass, code) -> { setEngine, verify }.
|
|
22
|
-
//
|
|
23
|
-
// `verify(sigAlg, signature, spkiBytes, preimage)` takes the signature as the parsed BIT STRING
|
|
24
|
-
// ({ unusedBits, bytes }), because the octet-alignment refusal is part of the contract: a signature
|
|
25
|
-
// carrying unused bits is malformed for every algorithm these formats admit, and a caller that had
|
|
26
|
-
// already discarded `unusedBits` could not be held to it.
|
|
27
8
|
function makeSeam(name, ErrorClass, code) {
|
|
28
9
|
var engine = null;
|
|
29
10
|
|
|
@@ -31,10 +12,6 @@ function makeSeam(name, ErrorClass, code) {
|
|
|
31
12
|
setEngine: function (verifyWithSpki) { engine = verifyWithSpki; },
|
|
32
13
|
|
|
33
14
|
verify: function (sigAlg, signature, spkiBytes, preimage) {
|
|
34
|
-
// Each format's producing verb requires path-validate for the injection side-effect, so the
|
|
35
|
-
// engine is set on every route that reaches here through a public verb. A future internal
|
|
36
|
-
// consumer that required a seam without it would otherwise call null and surface a raw
|
|
37
|
-
// TypeError out of a verify, which names neither the defect nor a verdict.
|
|
38
15
|
if (engine == null) {
|
|
39
16
|
throw new ErrorClass(code, "the " + name + " signature engine is not initialized (require pki before use)");
|
|
40
17
|
}
|