@blamejs/pki 0.4.9 → 0.4.11

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.
@@ -27,6 +27,7 @@
27
27
  // coordinate by a leading 0x00), the RSA exponent over its full UINT32 width.
28
28
 
29
29
  var ByteReader = require("./byte-reader");
30
+ var guard = require("./guard-all");
30
31
 
31
32
  var TPM_GENERATED_VALUE = 0xff544347;
32
33
  var TPM_ST_ATTEST_CERTIFY = 0x8017;
@@ -52,15 +53,47 @@ function _symDef(r) { if (r.u16() !== TPM_ALG.NULL) { r.u16(); r.u16(); } }
52
53
  // UINT16 hashAlg) follows for the signing/kdf schemes in scope.
53
54
  function _scheme(r) { if (r.u16() !== TPM_ALG.NULL) { r.u16(); } }
54
55
 
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
+ var TPMA_OBJECT_BITS = Object.assign(Object.create(null), {
65
+ fixedTPM: 1, stClear: 2, fixedParent: 4, sensitiveDataOrigin: 5, userWithAuth: 6,
66
+ adminWithPolicy: 7, firmwareLimited: 8, svnLimited: 9, noDA: 10,
67
+ encryptedDuplication: 11, restricted: 16, decrypt: 17, sign: 18, x509sign: 19,
68
+ });
69
+ var TPMA_OBJECT_RESERVED = 0xfff0f009;
70
+
71
+ // _decodeAttributes(oa) -> the named booleans of a TPMA_OBJECT word.
72
+ // @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
+ function _decodeAttributes(oa) {
75
+ var out = {};
76
+ Object.keys(TPMA_OBJECT_BITS).forEach(function (n) { out[n] = ((oa >>> TPMA_OBJECT_BITS[n]) & 1) === 1; });
77
+ return out;
78
+ }
79
+
55
80
  // parsePubArea(buf, E, code) -> the decoded TPMT_PUBLIC (WebAuthn 8.3 item 17-20).
56
81
  // @enforced-by behavioral -- a packed TPM structure decode has no rename-proof code shape;
57
82
  // the RED vectors (trailing bytes, an unsupported type, a truncated TPM2B) are the guard.
58
83
  function parsePubArea(buf, E, code) {
59
84
  var r = new ByteReader(buf, 0, buf.length, E, code);
60
85
  var type = r.u16(), nameAlg = r.u16();
61
- r.u32(); // objectAttributes (full policy validation deferred)
62
- r.vector(2, 0, null); // authPolicy TPM2B_DIGEST
63
- var pub = { type: type, nameAlg: nameAlg, nameAlgBytes: buf.subarray(2, 4) };
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
+ var objectAttributes = r.u32();
93
+ var authPolicy = r.vector(2, 0, null); // a zero-length digest IS the Empty Policy, not "absent"
94
+ var pub = { type: type, nameAlg: nameAlg, nameAlgBytes: buf.subarray(2, 4),
95
+ objectAttributes: objectAttributes >>> 0, attributes: _decodeAttributes(objectAttributes),
96
+ authPolicy: authPolicy };
64
97
  if (type === TPM_ALG.RSA) {
65
98
  _symDef(r); // symmetric TPMT_SYM_DEF_OBJECT
66
99
  _scheme(r); // scheme TPMT_RSA_SCHEME
@@ -127,8 +160,145 @@ function pubKeyEqualsCose(pub, cose, E, mismatchCode, code) {
127
160
  throw new E(code, "unsupported TPM pubArea key type");
128
161
  }
129
162
 
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
+ var TPM_POLICY_PROFILES = Object.assign(Object.create(null), {
168
+ "hardware-bound": { fixedTPM: true, fixedParent: true, sensitiveDataOrigin: true, sign: true, restricted: false, x509sign: false },
169
+ });
170
+ var _TPM_POLICY_KEYS = Object.assign(Object.create(null), { profile: 1, objectAttributes: 1, reservedBitsClear: 1, consistency: 1, authPolicy: 1 });
171
+ var _AUTH_POLICY_KEYS = Object.assign(Object.create(null), { present: 1, allow: 1 });
172
+
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 rather than being ignored.
175
+ // @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
+ function normalizeObjectAttributePolicy(policy, E, code) {
179
+ if (policy === undefined) return null;
180
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) throw new E(code, "opts.tpmPolicy must be an object");
181
+ guard.identifier.assertKnownKeys(policy, _TPM_POLICY_KEYS, function (c, m) { return new E(c, m); }, code, "opts.tpmPolicy has an unknown key ");
182
+ var want = {};
183
+ if (policy.profile !== undefined) {
184
+ var preset = TPM_POLICY_PROFILES[policy.profile];
185
+ if (!preset) throw new E(code, "opts.tpmPolicy.profile " + JSON.stringify(policy.profile) + " is not a defined profile");
186
+ Object.keys(preset).forEach(function (n) { want[n] = preset[n]; });
187
+ }
188
+ // An explicit map layers OVER the preset, so a caller can keep the profile and override one bit.
189
+ var explicit = policy.objectAttributes;
190
+ if (explicit !== undefined) {
191
+ if (!explicit || typeof explicit !== "object" || Array.isArray(explicit)) throw new E(code, "opts.tpmPolicy.objectAttributes must be an object");
192
+ Object.keys(explicit).forEach(function (n) {
193
+ if (TPMA_OBJECT_BITS[n] === undefined) throw new E(code, "opts.tpmPolicy.objectAttributes has an unknown attribute " + JSON.stringify(n));
194
+ if (typeof explicit[n] !== "boolean") throw new E(code, "opts.tpmPolicy.objectAttributes." + n + " must be a boolean");
195
+ want[n] = explicit[n];
196
+ });
197
+ }
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, so say so at config time rather than letting a caller believe it is protected.
201
+ if (want.sensitiveDataOrigin === true && want.fixedTPM !== true) {
202
+ 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
+ }
204
+ ["reservedBitsClear", "consistency"].forEach(function (k) {
205
+ if (policy[k] !== undefined && typeof policy[k] !== "boolean") throw new E(code, "opts.tpmPolicy." + k + " must be a boolean");
206
+ });
207
+ var ap = policy.authPolicy;
208
+ var allow = null;
209
+ if (ap !== undefined) {
210
+ 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
+ guard.identifier.assertKnownKeys(ap, _AUTH_POLICY_KEYS, function (c, m) { return new E(c, m); }, code, "opts.tpmPolicy.authPolicy has an unknown key ");
215
+ if (ap.present !== undefined && typeof ap.present !== "boolean") throw new E(code, "opts.tpmPolicy.authPolicy.present must be a boolean");
216
+ if (ap.allow !== undefined) {
217
+ if (!Array.isArray(ap.allow)) throw new E(code, "opts.tpmPolicy.authPolicy.allow must be an array");
218
+ // Decode every entry HERE, at config time, rather than inside the comparison. Node's hex
219
+ // decoder is permissive: it stops at the first character that is not a hex digit, so
220
+ // "<digest>zz" decodes back to the digest and a non-string decodes to an empty buffer --
221
+ // either of which 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, and it fails here rather than becoming a digest nobody intended.
224
+ allow = ap.allow.map(function (entry, i) {
225
+ if (Buffer.isBuffer(entry)) return entry;
226
+ if (typeof entry !== "string" || entry.length === 0 || entry.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(entry)) {
227
+ throw new E(code, "opts.tpmPolicy.authPolicy.allow[" + i + "] must be a Buffer or an even-length hex string");
228
+ }
229
+ return Buffer.from(entry, "hex");
230
+ });
231
+ }
232
+ }
233
+ return { objectAttributes: want, reservedBitsClear: policy.reservedBitsClear === true,
234
+ consistency: policy.consistency === true,
235
+ authPolicy: ap ? { present: ap.present === true, allow: allow } : null };
236
+ }
237
+
238
+ // Apply a normalized policy to a parsed pubArea. Nothing here is a WebAuthn sec. 8.3 requirement --
239
+ // that section constrains only pubArea's `parameters` and `unique` fields -- so every rule runs
240
+ // only because a caller asked for it by name.
241
+ // @enforced-by behavioral -- an opt-gated policy gate has no rename-proof code shape, and there is
242
+ // no general vector shape to detect: the RED vectors (each required bit in both directions, the
243
+ // reserved-bit mask across the sign bit, the restricted sign+decrypt combination, the over-long
244
+ // digest, the Empty Policy, and the allow-list miss) are the guard.
245
+ function assertObjectAttributePolicy(pub, policy, E, policyCode, structuralCode) {
246
+ if (!policy) return;
247
+ var oa = pub.objectAttributes >>> 0;
248
+ // Bit 31 lies inside the reserved mask, so the AND must be coerced back to unsigned: a bare
249
+ // `oa & 0xfff0f009` is a signed int32 and would read negative rather than "a bit is set".
250
+ if (policy.reservedBitsClear && ((oa & TPMA_OBJECT_RESERVED) >>> 0) !== 0) {
251
+ 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)");
252
+ }
253
+ if (policy.consistency) {
254
+ // sec. 8.3.3.11, verbatim: "This attribute shall not be SET in any object that has fixedTPM
255
+ // SET." A key that cannot leave its TPM cannot be duplicated, so requiring its duplication be
256
+ // encrypted describes an object that cannot exist.
257
+ if (pub.attributes.encryptedDuplication && pub.attributes.fixedTPM) {
258
+ 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)");
259
+ }
260
+ // sec. 8.3.3.16/.17: a restricted key is a TPM-internal key, and the two use bits are a choice
261
+ // -- a restricted object that both signs and decrypts is not a defined combination.
262
+ if (pub.attributes.sign && pub.attributes.decrypt && pub.attributes.restricted) {
263
+ throw new E(structuralCode, "the TPMT_PUBLIC objectAttributes sets restricted with both sign and decrypt (TPM 2.0 Part 2 sec. 8.3.3)");
264
+ }
265
+ // A TPM2B_DIGEST carries at most a SHA-512 digest.
266
+ if (pub.authPolicy.length > 64) {
267
+ throw new E(structuralCode, "the TPMT_PUBLIC authPolicy is " + pub.authPolicy.length + " octets, above the 64-octet TPM2B_DIGEST maximum");
268
+ }
269
+ // The REMAINING sec. 8.3.3 "shall" statements are all parent-relative -- fixedTPM, stClear and
270
+ // encryptedDuplication are each constrained against the value the object's PARENT carries. An
271
+ // attestation presents one public area and no parent, so a verifier cannot evaluate them at
272
+ // all; they are not omitted by oversight, they are unverifiable from what is on the wire.
273
+ }
274
+ Object.keys(policy.objectAttributes).forEach(function (name) {
275
+ var want = policy.objectAttributes[name];
276
+ if (pub.attributes[name] !== want) {
277
+ throw new E(policyCode, "the TPM credential key has " + name + " " + (pub.attributes[name] ? "SET" : "CLEAR") +
278
+ "; opts.tpmPolicy requires it " + (want ? "SET" : "CLEAR"));
279
+ }
280
+ });
281
+ var ap = policy.authPolicy;
282
+ if (!ap) return;
283
+ if (ap.present === true && pub.authPolicy.length === 0) {
284
+ throw new E(policyCode, "the TPM credential key carries the Empty Policy; opts.tpmPolicy.authPolicy.present requires a policy digest");
285
+ }
286
+ if (ap.allow) {
287
+ // Every entry was decoded and validated at config time, so this compares digests only.
288
+ var got = pub.authPolicy;
289
+ var ok = ap.allow.some(function (want) {
290
+ return want.length === got.length && guard.crypto.constantTimeEqual(want, got);
291
+ });
292
+ if (!ok) throw new E(policyCode, "the TPM credential key authPolicy is not in opts.tpmPolicy.authPolicy.allow");
293
+ }
294
+ }
295
+
130
296
  module.exports = {
131
297
  parsePubArea: parsePubArea,
298
+ TPMA_OBJECT_BITS: TPMA_OBJECT_BITS,
299
+ decodeObjectAttributes: _decodeAttributes,
300
+ normalizeObjectAttributePolicy: normalizeObjectAttributePolicy,
301
+ assertObjectAttributePolicy: assertObjectAttributePolicy,
132
302
  parseCertInfo: parseCertInfo,
133
303
  pubKeyEqualsCose: pubKeyEqualsCose,
134
304
  TPM_ALG_HASH: TPM_ALG_HASH,