@blamejs/pki 0.8.30 → 0.8.48
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 +411 -0
- package/README.md +67 -16
- package/bin/pki.js +716 -30
- package/examples/issue-a-certificate.js +90 -0
- package/examples/post-quantum.js +66 -0
- package/examples/read-a-certificate.js +56 -0
- package/examples/revoke-and-check.js +86 -0
- package/examples/sign-and-verify-cms.js +63 -0
- package/index.js +18 -5
- package/lib/acme.js +223 -74
- package/lib/alt-sig.js +441 -0
- package/lib/asn1-der.js +48 -25
- package/lib/attrcert-sign.js +27 -19
- package/lib/cbor-det.js +24 -14
- package/lib/cmc-build.js +9 -2
- package/lib/cmc-verify.js +14 -3
- package/lib/cmp-build.js +17 -5
- package/lib/cmp-session.js +24 -11
- package/lib/cmp-verify.js +28 -19
- package/lib/cms-compress.js +3 -1
- package/lib/cms-decrypt.js +102 -7
- package/lib/cms-encrypt.js +59 -7
- package/lib/cms-sign.js +31 -9
- package/lib/cms-verify.js +22 -18
- package/lib/composite-kem.js +30 -18
- package/lib/composite-sig.js +29 -14
- package/lib/constants.js +28 -0
- package/lib/crl-sign.js +111 -19
- package/lib/crmf-sign.js +167 -15
- package/lib/csr-sign.js +117 -6
- package/lib/ct.js +939 -48
- package/lib/est.js +29 -16
- package/lib/framework-error.js +12 -0
- package/lib/guard-async.js +1 -1
- package/lib/guard-bytes.js +103 -20
- package/lib/guard-crypto.js +205 -3
- package/lib/guard-encoding.js +6 -1
- package/lib/guard-identifier.js +55 -8
- package/lib/guard-intrinsic.js +43 -0
- package/lib/guard-json.js +122 -9
- package/lib/guard-limits.js +12 -1
- package/lib/guard-list.js +63 -8
- package/lib/guard-name.js +167 -20
- package/lib/guard-range.js +43 -5
- package/lib/guard-text.js +41 -0
- package/lib/guard-verdict.js +39 -8
- package/lib/hpke.js +688 -42
- package/lib/http-transport.js +156 -4
- package/lib/identity-match.js +4 -4
- package/lib/inspect.js +448 -2
- package/lib/jose.js +93 -25
- package/lib/key.js +18 -13
- package/lib/lint.js +165 -1
- package/lib/merkle.js +233 -27
- package/lib/ocsp.js +27 -14
- package/lib/oid.js +14 -4
- package/lib/path-validate.js +143 -61
- package/lib/pkcs12-build.js +7 -2
- package/lib/pki-build.js +31 -7
- package/lib/possession.js +575 -0
- package/lib/related-cert.js +708 -0
- package/lib/revocation-fetch.js +6 -6
- package/lib/scep.js +15 -5
- package/lib/schema-c509.js +5 -2
- package/lib/schema-cmc.js +1 -1
- package/lib/schema-cmp.js +8 -8
- package/lib/schema-cms.js +2 -15
- package/lib/schema-csr.js +69 -0
- package/lib/schema-engine.js +10 -3
- package/lib/schema-pkix.js +267 -4
- package/lib/schema-tsp.js +7 -6
- package/lib/sign-scheme.js +22 -9
- package/lib/sigstore.js +684 -168
- package/lib/sleep.js +8 -1
- package/lib/smime.js +22 -1
- package/lib/tlog.js +1235 -0
- package/lib/tls-cert-compress.js +25 -8
- package/lib/tsp-sign.js +120 -35
- package/lib/tuf.js +1395 -0
- package/lib/validator-tpm.js +4 -1
- package/lib/verify-seam.js +7 -1
- package/lib/webauthn-mds.js +41 -4
- package/lib/webauthn.js +43 -25
- package/lib/webcrypto.js +48 -46
- package/lib/x509-sign.js +225 -19
- package/package.json +2 -1
- package/sbom.cdx.json +6 -6
package/lib/jose.js
CHANGED
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
var constants = require("./constants");
|
|
40
40
|
var wcEngine = require("./webcrypto");
|
|
41
41
|
var webcrypto = wcEngine.webcrypto;
|
|
42
|
+
/** @internal The verify operation, captured at load. The object was held and its METHOD fetched per
|
|
43
|
+
* call, which leaves it replaceable: one answering true accepts a JWS whose signer never signed it,
|
|
44
|
+
* including on the embedded-key route this module pins against its thumbprint. */
|
|
45
|
+
var _subtleVerify = require("./guard-intrinsic").uncurry(webcrypto.subtle.verify);
|
|
42
46
|
var frameworkError = require("./framework-error");
|
|
43
47
|
var guard = require("./guard-all");
|
|
44
48
|
var edwardsPoint = require("./edwards-point");
|
|
@@ -121,11 +125,11 @@ function b64uDecode(text) {
|
|
|
121
125
|
* pki.jose.parseJson('{"a":1}'); // -> { a: 1 }
|
|
122
126
|
*/
|
|
123
127
|
function parseJson(input) {
|
|
124
|
-
return guard.json.parse(input, E, {
|
|
128
|
+
return guard.verdict.shield(guard.json.parse(input, E, {
|
|
125
129
|
maxBytes: LIMITS.JSON_MAX_BYTES, maxDepth: LIMITS.JSON_MAX_DEPTH,
|
|
126
130
|
badJson: "jose/bad-json", tooDeep: "jose/too-deep", duplicateMember: "jose/duplicate-member",
|
|
127
131
|
tooLarge: "jose/too-large", badInput: "jose/bad-input", label: "the JSON document",
|
|
128
|
-
});
|
|
132
|
+
}));
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
|
|
@@ -329,9 +333,18 @@ async function verify(jws, opts) {
|
|
|
329
333
|
if (!jws || typeof jws !== "object" || _isArray(jws)) throw E("jose/bad-jws", "a flattened JWS object is required");
|
|
330
334
|
if (_hasOwn(jws, "signatures")) throw E("jose/bad-jws", "the multi-signature signatures member is forbidden");
|
|
331
335
|
if (_hasOwn(jws, "header")) throw E("jose/bad-jws", "the unprotected header member is forbidden");
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
336
|
+
/** @internal The three members are taken ONCE, for the reason `opts` is above: the JWS is the caller's
|
|
337
|
+
* object, and a member reached through an accessor answers every read separately. `protected` decides
|
|
338
|
+
* both the header the profile rules are applied to and the signing input the signature is checked over,
|
|
339
|
+
* and the thumbprint comparison awaits between those two uses, so reading it twice would let the rules
|
|
340
|
+
* be applied to one message and the signature checked over another. Everything below reads these
|
|
341
|
+
* locals. */
|
|
342
|
+
var protectedB64 = jws.protected;
|
|
343
|
+
var signatureB64 = jws.signature;
|
|
344
|
+
var payloadB64 = jws.payload;
|
|
345
|
+
if (typeof protectedB64 !== "string" || typeof signatureB64 !== "string") throw E("jose/bad-jws", "protected and signature must be base64url strings");
|
|
346
|
+
if (typeof payloadB64 !== "string") throw E("jose/bad-jws", "payload must be a base64url string (never detached)");
|
|
347
|
+
var header = parseJson(b64uDecode(protectedB64));
|
|
335
348
|
var profileName = opts.profile || "acme-outer";
|
|
336
349
|
var checked = _checkHeader(header, profileName);
|
|
337
350
|
/** @internal The embedded jwk is the signer's PUBLIC key (RFC 7515 sec. 4.1.3). One carrying
|
|
@@ -342,14 +355,32 @@ async function verify(jws, opts) {
|
|
|
342
355
|
if (opts.key !== undefined && (typeof opts.key !== "object" || opts.key === null || _isArray(opts.key))) {
|
|
343
356
|
throw E("jose/bad-key", "opts.key was supplied but is not a JWK object, so the key this message must be signed under cannot be established");
|
|
344
357
|
}
|
|
345
|
-
|
|
358
|
+
/** @internal The caller's key is COPIED here, before ANY member of it is read, and everything below
|
|
359
|
+
* reads the copy: the thumbprint comparison against the embedded jwk, the key-type check, the Edwards
|
|
360
|
+
* low-order point gate, the signature-width calculation, the import parameters and the algorithm
|
|
361
|
+
* selector. Its members were read many times over, `crv` eight times and `kty` nine on one successful
|
|
362
|
+
* verify, and an accessor answers each read separately.
|
|
363
|
+
*
|
|
364
|
+
* The FIRST read is the one that decides which curve the point gate validates against
|
|
365
|
+
* (`jwk.crv === "Ed25519" ? 6 : 7`), so a `crv` answering `Ed448` once and `Ed25519` afterwards had the
|
|
366
|
+
* point gated under the wrong curve's parameters while an Ed25519 key was imported and verified. That
|
|
367
|
+
* gate is the whole defense against a low-order key verifying a FORGED signature. Copying before the
|
|
368
|
+
* thumbprint comparison rather than after it matters for the same reason: otherwise a key could
|
|
369
|
+
* thumbprint as the embedded one and be copied as a different one.
|
|
370
|
+
*
|
|
371
|
+
* `snapshotDeep` refuses an accessor-backed member rather than reading it once, which is the stronger
|
|
372
|
+
* answer for a value that selects an algorithm. The embedded jwk comes from the parsed header and is
|
|
373
|
+
* this module's own object, so it needs no copy. */
|
|
374
|
+
var suppliedKey = opts.key === undefined ? undefined
|
|
375
|
+
: guard.bytes.snapshotDeep(opts.key, JoseError, "jose/bad-key", "opts.key");
|
|
376
|
+
var jwk = header.jwk || suppliedKey;
|
|
346
377
|
if (!jwk) throw E("jose/bad-key", "a verification key is required (opts.key) when the profile does not embed a jwk");
|
|
347
378
|
var keySource = "embedded-jwk";
|
|
348
|
-
if (
|
|
379
|
+
if (suppliedKey) {
|
|
349
380
|
keySource = "opts.key";
|
|
350
381
|
if (header.jwk) {
|
|
351
382
|
var embeddedTp, suppliedTp;
|
|
352
|
-
try { embeddedTp = await thumbprint(header.jwk); suppliedTp = await thumbprint(
|
|
383
|
+
try { embeddedTp = await thumbprint(header.jwk); suppliedTp = await thumbprint(suppliedKey); }
|
|
353
384
|
catch (e) { throw E("jose/bad-key", "the embedded jwk and opts.key could not be compared as RFC 7638 thumbprints", e); }
|
|
354
385
|
if (embeddedTp !== suppliedTp) {
|
|
355
386
|
throw E("jose/key-mismatch",
|
|
@@ -358,19 +389,19 @@ async function verify(jws, opts) {
|
|
|
358
389
|
", supplied " + suppliedTp + ")");
|
|
359
390
|
}
|
|
360
391
|
}
|
|
361
|
-
jwk =
|
|
392
|
+
jwk = suppliedKey;
|
|
362
393
|
}
|
|
363
394
|
_assertKeyType(checked.algRow, jwk);
|
|
364
|
-
var sig = b64uDecode(
|
|
395
|
+
var sig = b64uDecode(signatureB64);
|
|
365
396
|
var want = _expectedSigBytes(checked.algRow, jwk);
|
|
366
397
|
if (want != null && sig.length !== want) throw E("jose/bad-signature", "the signature length " + sig.length + " is not the " + want + " bytes " + checked.alg + " requires");
|
|
367
|
-
var signingInput = _bufferFrom(
|
|
398
|
+
var signingInput = _bufferFrom(protectedB64 + "." + payloadB64, "ascii");
|
|
368
399
|
var key;
|
|
369
400
|
try { key = await webcrypto.subtle.importKey("jwk", jwk, _importParams(checked.algRow, jwk), false, ["verify"]); }
|
|
370
401
|
catch (e) { throw E("jose/bad-key", "the JWK could not be imported for verification", e); }
|
|
371
|
-
var ok = await webcrypto.subtle
|
|
402
|
+
var ok = await _subtleVerify(webcrypto.subtle, _cryptoAlg(checked.algRow, jwk), key, sig, signingInput);
|
|
372
403
|
if (!ok) throw E("jose/verify-failed", "the JWS signature did not verify");
|
|
373
|
-
return { header: header, payload: b64uDecode(
|
|
404
|
+
return { header: header, payload: b64uDecode(payloadB64), keySource: keySource };
|
|
374
405
|
}
|
|
375
406
|
|
|
376
407
|
|
|
@@ -409,9 +440,23 @@ async function verify(jws, opts) {
|
|
|
409
440
|
*/
|
|
410
441
|
var _SIGN_KEYS = intrinsic.assign(intrinsic.create(null), { protected: 1, payload: 1, key: 1, jwk: 1, profile: 1 });
|
|
411
442
|
var _SIGN_KEYS_EMBEDDED_JWK = intrinsic.assign(intrinsic.create(null), { protected: 1, payload: 1, key: 1, profile: 1 });
|
|
443
|
+
|
|
444
|
+
/** @internal The payload, copied, taken before anything else on the caller's object is read. Every later
|
|
445
|
+
* read of that object is caller code when the field is an accessor, and the protected header is read
|
|
446
|
+
* FIRST and SERIALIZED, which reaches the header object, each of its fields and any `toJSON` on it. Over
|
|
447
|
+
* a view of the payload any of those would replace what gets signed, after the verb had accepted the
|
|
448
|
+
* payload it was handed, so the copy comes before all of them rather than before whichever one is in
|
|
449
|
+
* view. A payload that is not a Buffer is carried through untouched and refused where it always was, so
|
|
450
|
+
* no diagnostic moves ahead of the header's own. */
|
|
451
|
+
function _fixedPayload(supplied) {
|
|
452
|
+
if (!_isBuffer(supplied)) return supplied;
|
|
453
|
+
return guard.bytes.snapshot(supplied, JoseError, "jose/bad-input", "payload");
|
|
454
|
+
}
|
|
455
|
+
|
|
412
456
|
async function sign(opts) {
|
|
413
457
|
opts = opts || {};
|
|
414
458
|
guard.identifier.assertKnownKeys(opts, _SIGN_KEYS, E, "jose/bad-input", "unknown pki.jose.sign option ");
|
|
459
|
+
var fixedPayload = _fixedPayload(opts.payload);
|
|
415
460
|
var headerJson;
|
|
416
461
|
try { headerJson = _stringify(opts.protected); }
|
|
417
462
|
catch (e) { throw E("jose/bad-header", "the protected header must be JSON-serializable", e); }
|
|
@@ -424,13 +469,25 @@ async function sign(opts) {
|
|
|
424
469
|
(k === "jwk" ? "; the protected header already embeds the public JWK" : "");
|
|
425
470
|
});
|
|
426
471
|
}
|
|
427
|
-
var payload =
|
|
472
|
+
var payload = fixedPayload;
|
|
428
473
|
if (!_isBuffer(payload)) throw E("jose/bad-input", "payload must be a Buffer (empty Buffer for POST-as-GET)");
|
|
429
|
-
|
|
430
|
-
|
|
474
|
+
/** @internal ONE read of the caller's signing key, taken before anything inspects it, and every use
|
|
475
|
+
* below reads this binding. It was read four times: the key-like test, twice by the hash-to-algorithm
|
|
476
|
+
* check, and once more to sign with. An accessor answers each read separately, so the check could
|
|
477
|
+
* approve a SHA-256 key while the signature was produced by a SHA-512 one, and the JWS emitted then
|
|
478
|
+
* DECLARES an algorithm its signature was not made under, which is the algorithm confusion the header
|
|
479
|
+
* registry exists to close before any crypto call.
|
|
480
|
+
*
|
|
481
|
+
* The RSA arms are where it showed: the registry's fixed signature widths catch the HMAC and EC arms,
|
|
482
|
+
* and two 2048-bit RSA keys produce the same 256-byte signature, so the width pin below cannot see a
|
|
483
|
+
* substitution between them. */
|
|
484
|
+
var suppliedKey = opts.key;
|
|
485
|
+
if (!wcEngine.isCryptoKeyLike(suppliedKey)) throw E("jose/bad-input", "a private or secret CryptoKey (opts.key) is required");
|
|
431
486
|
var checked = _checkHeader(header, opts.profile || "acme-outer");
|
|
487
|
+
var suppliedAlg = suppliedKey.algorithm;
|
|
488
|
+
var suppliedHash = suppliedAlg && suppliedAlg.hash;
|
|
432
489
|
if ((checked.algRow.kty === "RSA" || checked.algRow.kty === "oct") &&
|
|
433
|
-
(!
|
|
490
|
+
(!suppliedHash || suppliedHash.name !== checked.algRow.hash)) {
|
|
434
491
|
throw E("jose/bad-key", "the signing key's hash does not match the algorithm " + checked.alg);
|
|
435
492
|
}
|
|
436
493
|
if (header.jwk) { _assertKeyType(checked.algRow, header.jwk); assertPublicJwk(header.jwk); }
|
|
@@ -438,15 +495,18 @@ async function sign(opts) {
|
|
|
438
495
|
var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
|
|
439
496
|
var signingInput = _bufferFrom(protectedB64 + "." + payloadB64, "ascii");
|
|
440
497
|
var jwk = header.jwk || opts.jwk || {};
|
|
441
|
-
|
|
498
|
+
/** @internal `suppliedKey` is the reading taken at the top, so the key adopted for signing, the key the
|
|
499
|
+
* hash-to-algorithm check approved and the key the expected signature width is computed from are all
|
|
500
|
+
* one key. */
|
|
501
|
+
var signKey = await wcEngine.adoptKey(suppliedKey, null, ["sign"], E, "jose/bad-input");
|
|
442
502
|
var sigBuf = _bufferFrom(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, signKey), signKey, signingInput));
|
|
443
|
-
var want = _expectedSigBytes(checked.algRow, jwk,
|
|
503
|
+
var want = _expectedSigBytes(checked.algRow, jwk, suppliedKey);
|
|
444
504
|
if (want != null && sigBuf.length !== want) throw E("jose/bad-key", "the signing key produced a " + sigBuf.length + "-byte signature but " + checked.alg + " requires " + want + " (the key does not match the algorithm)");
|
|
445
505
|
if (header.jwk) {
|
|
446
506
|
var pubKey;
|
|
447
507
|
try { pubKey = await webcrypto.subtle.importKey("jwk", header.jwk, _importParams(checked.algRow, header.jwk), false, ["verify"]); }
|
|
448
508
|
catch (e) { throw E("jose/bad-key", "the embedded jwk could not be imported to confirm it matches the signing key", e); }
|
|
449
|
-
if (!(await webcrypto.subtle
|
|
509
|
+
if (!(await _subtleVerify(webcrypto.subtle, _cryptoAlg(checked.algRow, header.jwk), pubKey, sigBuf, signingInput))) {
|
|
450
510
|
throw E("jose/bad-key", "the embedded jwk does not match the signing key (it cannot verify the produced signature)");
|
|
451
511
|
}
|
|
452
512
|
}
|
|
@@ -485,14 +545,22 @@ var THUMBPRINT_MEMBERS = intrinsic.assign(intrinsic.create(null), {
|
|
|
485
545
|
* await pki.jose.thumbprint(accountJwk); // -> "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
|
|
486
546
|
*/
|
|
487
547
|
async function thumbprint(jwk) {
|
|
488
|
-
if (!jwk || typeof jwk !== "object"
|
|
489
|
-
|
|
490
|
-
|
|
548
|
+
if (!jwk || typeof jwk !== "object") throw E("jose/bad-key", "a JWK object with a kty is required");
|
|
549
|
+
/** @internal COPIED before any member is read, because what this returns is an IDENTITY. Each member
|
|
550
|
+
* was read twice, the type check and then the value put into the hash, and `kty` four times; an
|
|
551
|
+
* accessor answers each read separately, so a member could be checked as one value and HASHED as
|
|
552
|
+
* another. The thumbprint is what RFC 7638 identifies a key by: `pki.jose.verify` compares an embedded
|
|
553
|
+
* jwk against `opts.key` with it, and ACME uses it as the account identity in a key authorization. A
|
|
554
|
+
* thumbprint over a value the checks never approved names a key that was never the one validated. */
|
|
555
|
+
var copied = guard.bytes.snapshotDeep(jwk, JoseError, "jose/bad-key", "the JWK");
|
|
556
|
+
if (typeof copied.kty !== "string") throw E("jose/bad-key", "a JWK object with a kty is required");
|
|
557
|
+
var members = THUMBPRINT_MEMBERS[copied.kty];
|
|
558
|
+
if (!members) throw E("jose/bad-key", "unsupported JWK key type " + _stringify(copied.kty));
|
|
491
559
|
var parts = [];
|
|
492
560
|
for (var i = 0; i < members.length; i++) {
|
|
493
561
|
var m = members[i];
|
|
494
|
-
if (typeof
|
|
495
|
-
_push(parts, _stringify(m) + ":" + _stringify(
|
|
562
|
+
if (typeof copied[m] !== "string") throw E("jose/bad-key", "the JWK is missing the required member " + _stringify(m));
|
|
563
|
+
_push(parts, _stringify(m) + ":" + _stringify(copied[m]));
|
|
496
564
|
}
|
|
497
565
|
var canonical = "{" + _join(parts, ",") + "}";
|
|
498
566
|
var digest = _bufferFrom(await webcrypto.subtle.digest("SHA-256", _bufferFrom(canonical, "utf8")));
|
package/lib/key.js
CHANGED
|
@@ -30,6 +30,11 @@ var schemaSec1 = require("./schema-sec1");
|
|
|
30
30
|
var pkix = require("./schema-pkix");
|
|
31
31
|
var schema = require("./schema-engine");
|
|
32
32
|
var webcrypto = require("./webcrypto");
|
|
33
|
+
/** @internal The key-material exporter, captured at load. The signature probe that decides whether two
|
|
34
|
+
* halves are a pair imports and signs with what this returns, so a replacement exporting a different
|
|
35
|
+
* private key makes a mismatched pair report as corresponding: the probe then signs and verifies with
|
|
36
|
+
* a pair that does correspond, and neither half is the one the caller supplied. */
|
|
37
|
+
var _exportAnyKey = webcrypto.exportAnyKey;
|
|
33
38
|
var compositeKem = require("./composite-kem");
|
|
34
39
|
var frameworkError = require("./framework-error");
|
|
35
40
|
var guard = require("./guard-all");
|
|
@@ -123,7 +128,7 @@ function _sec1ToPkcs8(der, curveOid) {
|
|
|
123
128
|
async function _toPrivateKeyDer(input) {
|
|
124
129
|
if (_isCryptoKey(input)) {
|
|
125
130
|
if (input.type !== "private") throw _err("key/bad-input", "a private CryptoKey is required (got a " + input.type + " key)");
|
|
126
|
-
return
|
|
131
|
+
return _exportAnyKey(input, _err, "key/bad-input");
|
|
127
132
|
}
|
|
128
133
|
return pkix.coerceToDer(input, { pemLabel: "PRIVATE KEY", PemError: PemError, ErrorClass: KeyError, prefix: "key" });
|
|
129
134
|
}
|
|
@@ -295,13 +300,13 @@ async function export_(key, opts) {
|
|
|
295
300
|
var fmt = opts.format || "der";
|
|
296
301
|
if (fmt === "jwk") {
|
|
297
302
|
try {
|
|
298
|
-
return await
|
|
303
|
+
return await _exportAnyKey(key, _err, "key/bad-input", "jwk");
|
|
299
304
|
} catch (e) {
|
|
300
305
|
if (e && e.isPkiError) throw e;
|
|
301
306
|
throw _err("key/bad-input", "exporting the key as a JWK failed", e);
|
|
302
307
|
}
|
|
303
308
|
}
|
|
304
|
-
var der = await
|
|
309
|
+
var der = await _exportAnyKey(key, _err, "key/bad-input");
|
|
305
310
|
if (fmt === "der") return der;
|
|
306
311
|
if (fmt === "pem") return pkix.pemEncode(der, opts.label || defaultLabel, PemError);
|
|
307
312
|
if (fmt === "pkcs1") return _toPkcs1(der, key);
|
|
@@ -870,8 +875,8 @@ async function publicFromPrivate(privateKey, opts) {
|
|
|
870
875
|
return opts.pem ? pkix.pemEncode(spki, "PUBLIC KEY", PemError) : spki;
|
|
871
876
|
}
|
|
872
877
|
try {
|
|
873
|
-
var priv =
|
|
874
|
-
spki =
|
|
878
|
+
var priv = guard.crypto.privateKey({ key: der, format: "der", type: "pkcs8" });
|
|
879
|
+
spki = guard.crypto.exportKey(guard.crypto.publicKey(priv), { format: "der", type: "spki" });
|
|
875
880
|
} catch (e) { throw _err("key/bad-input", "could not derive the public key from the private key", e); }
|
|
876
881
|
return opts.pem ? pkix.pemEncode(spki, "PUBLIC KEY", PemError) : spki;
|
|
877
882
|
}
|
|
@@ -946,7 +951,7 @@ function _pairSignOptions(priv, pub) {
|
|
|
946
951
|
/** @internal Null-prototype, because the engine READS this object by name. A plain object would
|
|
947
952
|
* let an inherited `saltLength` reach the signature as a restriction neither half stated. */
|
|
948
953
|
var options = _create(null);
|
|
949
|
-
options.padding =
|
|
954
|
+
options.padding = guard.crypto.padding.RSA_PKCS1_PSS;
|
|
950
955
|
if (salt != null) options.saltLength = salt;
|
|
951
956
|
return { digest: digest, options: options };
|
|
952
957
|
}
|
|
@@ -1006,7 +1011,7 @@ async function correspondsTo(privateKey, spkiDer) {
|
|
|
1006
1011
|
var privAsPkcs3 = _dhAsPkcs3(privDer, 1, "the private key's");
|
|
1007
1012
|
try {
|
|
1008
1013
|
_assertDhWithin(privAsPkcs3, 1, "the private key's");
|
|
1009
|
-
try { priv =
|
|
1014
|
+
try { priv = guard.crypto.privateKey({ key: privAsPkcs3, format: "der", type: "pkcs8" }); }
|
|
1010
1015
|
catch (e) { throw _err("key/bad-input", "the private key could not be read", e); }
|
|
1011
1016
|
} finally { if (privAsPkcs3 !== privDer) guard.secret.zeroize(privAsPkcs3, KeyError, "key/bad-input", "the rewritten private key"); }
|
|
1012
1017
|
var publicOid = _algorithmOidAt(spki, 0);
|
|
@@ -1014,7 +1019,7 @@ async function correspondsTo(privateKey, spkiDer) {
|
|
|
1014
1019
|
if (_isDhOid(_algorithmOidAt(privDer, 1)) !== _isDhOid(publicOid)) return false;
|
|
1015
1020
|
var pubAsPkcs3 = _dhAsPkcs3(spki, 0, "the public key's");
|
|
1016
1021
|
_assertDhWithin(pubAsPkcs3, 0, "the public key's");
|
|
1017
|
-
try { pub =
|
|
1022
|
+
try { pub = guard.crypto.publicKey({ key: pubAsPkcs3, format: "der", type: "spki" }); }
|
|
1018
1023
|
catch (e) { throw _err("key/bad-input", "the public key could not be read", e); }
|
|
1019
1024
|
if (!_sameKeyFamily(priv, pub)) return false;
|
|
1020
1025
|
var kind = guard.text.keyOf(priv.asymmetricKeyType);
|
|
@@ -1029,7 +1034,7 @@ async function correspondsTo(privateKey, spkiDer) {
|
|
|
1029
1034
|
/** @internal Compared as KEYS rather than as encodings: a PKCS #3 DHParameter may carry the
|
|
1030
1035
|
* optional privateValueLength beside the same p and g, and two encodings of one public value
|
|
1031
1036
|
* are one key. The runtime's equality compares the group and the value. */
|
|
1032
|
-
return _keyEquals(
|
|
1037
|
+
return _keyEquals(guard.crypto.publicKey(priv), pub) === true;
|
|
1033
1038
|
}
|
|
1034
1039
|
if (_PAIR_KEM[kind]) {
|
|
1035
1040
|
var sealed = nodeCrypto.encapsulate(pub);
|
|
@@ -1053,8 +1058,8 @@ async function correspondsTo(privateKey, spkiDer) {
|
|
|
1053
1058
|
* structure for an attacker to plant, so what the scalar generates IS the canonical answer and
|
|
1054
1059
|
* comparing against it adds a bound the agreement alone does not give. */
|
|
1055
1060
|
return guard.crypto.constantTimeEqual(
|
|
1056
|
-
|
|
1057
|
-
|
|
1061
|
+
guard.crypto.exportKey(guard.crypto.publicKey(priv), { format: "der", type: "spki" }),
|
|
1062
|
+
guard.crypto.exportKey(pub, { format: "der", type: "spki" }));
|
|
1058
1063
|
}
|
|
1059
1064
|
var how = _pairSignOptions(priv, pub);
|
|
1060
1065
|
if (how === null) {
|
|
@@ -1062,8 +1067,8 @@ async function correspondsTo(privateKey, spkiDer) {
|
|
|
1062
1067
|
}
|
|
1063
1068
|
var signKey = how.options ? _assign(_create(null), { key: priv }, how.options) : priv;
|
|
1064
1069
|
var verifyKey = how.options ? _assign(_create(null), { key: pub }, how.options) : pub;
|
|
1065
|
-
return
|
|
1066
|
-
|
|
1070
|
+
return guard.crypto.verify(how.digest, _PAIR_PROBE, verifyKey,
|
|
1071
|
+
guard.crypto.sign(how.digest, _PAIR_PROBE, signKey)) === true;
|
|
1067
1072
|
} catch (e) {
|
|
1068
1073
|
/** @internal The incompatible-parameters verdict is decided rather than caught, so it is passed
|
|
1069
1074
|
* through with the reason it was raised for. Rewrapping it would tell an operator the pair could
|
package/lib/lint.js
CHANGED
|
@@ -3972,6 +3972,89 @@ var RFC9881_RULES = [
|
|
|
3972
3972
|
var RFC9909_RULES = [
|
|
3973
3973
|
_pqcSigKeyUsageRule("lint/rfc9909/slhdsa-key-usage", "rfc9909", "RFC 9909 6", "SLH-DSA", _isSlhDsa),
|
|
3974
3974
|
];
|
|
3975
|
+
/** @internal Section 4.1 states the criticality at SHOULD NOT ("This extension SHOULD NOT be marked
|
|
3976
|
+
* critical", the section adding that marking it "would severely impact interoperability"), so it is
|
|
3977
|
+
* graded at warn here rather than held to a value by the shared criticality table, which carries only
|
|
3978
|
+
* the MUSTs. The same reason keeps issuerAltName out of the CRL table. Placement is a MUST in the same
|
|
3979
|
+
* section: "For certificate chains, this extension MUST only be included in the end-entity
|
|
3980
|
+
* certificate", so the two clauses are two rows at two strengths. */
|
|
3981
|
+
var OID_SAPKI = oid.byName("subjectAltPublicKeyInfo");
|
|
3982
|
+
var OID_ALT_ALG = oid.byName("altSignatureAlgorithm");
|
|
3983
|
+
var OID_ALT_VAL = oid.byName("altSignatureValue");
|
|
3984
|
+
/** @internal ITU-T X.509 (2019) clause 9.8. Two clauses at two strengths, and the difference matters.
|
|
3985
|
+
* Clause 9.8.3 states the pairing as a shall: "When the altSignatureAlgorithm extension is included in
|
|
3986
|
+
* a particular value that is an instance of a data type that supports extensions, the altSignatureValue
|
|
3987
|
+
* extension shall also be included." Clause 7.2.2 requires the full set in a certificate: one "shall
|
|
3988
|
+
* contain alternative public key information in the subjectAltPublicKeyInfo extension ..., an
|
|
3989
|
+
* alternative signature algorithm value in the altSignatureAlgorithm extension ... and an alternative
|
|
3990
|
+
* signature value in the altSignatureValue extension". Criticality is stated only in NOTEs recommending
|
|
3991
|
+
* non-critical, with the clauses saying the extensions "may be flagged as critical or as non-critical",
|
|
3992
|
+
* so that row is a warning and never an error: grading a NOTE as an error would invent a requirement
|
|
3993
|
+
* level the text does not state. */
|
|
3994
|
+
var X509_ALTSIG_RULES = [
|
|
3995
|
+
{
|
|
3996
|
+
id: "lint/x509-altsig/algorithm-without-value", severity: "error", source: "x509-altsig", citation: "ITU-T X.509 (2019) 9.8.3",
|
|
3997
|
+
message: "a value carrying altSignatureAlgorithm must also carry altSignatureValue",
|
|
3998
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("altSignatureAlgorithm"); },
|
|
3999
|
+
check: function (cert, ctx) {
|
|
4000
|
+
return ctx.raw("altSignatureValue") ? null : { context: { present: "altSignatureAlgorithm", missing: "altSignatureValue" } };
|
|
4001
|
+
},
|
|
4002
|
+
},
|
|
4003
|
+
{
|
|
4004
|
+
id: "lint/x509-altsig/value-without-algorithm", severity: "error", source: "x509-altsig", citation: "ITU-T X.509 (2019) 9.8.3",
|
|
4005
|
+
message: "a value carrying altSignatureValue must also carry altSignatureAlgorithm, which states how to verify it",
|
|
4006
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("altSignatureValue"); },
|
|
4007
|
+
check: function (cert, ctx) {
|
|
4008
|
+
return ctx.raw("altSignatureAlgorithm") ? null : { context: { present: "altSignatureValue", missing: "altSignatureAlgorithm" } };
|
|
4009
|
+
},
|
|
4010
|
+
},
|
|
4011
|
+
{
|
|
4012
|
+
id: "lint/x509-altsig/incomplete-set", severity: "error", source: "x509-altsig", citation: "ITU-T X.509 (2019) 7.2.2",
|
|
4013
|
+
message: "a certificate carrying the alternative cryptographic algorithm extensions must carry all three",
|
|
4014
|
+
appliesTo: function (cert, ctx) {
|
|
4015
|
+
return !!(ctx.raw("subjectAltPublicKeyInfo") || ctx.raw("altSignatureAlgorithm") || ctx.raw("altSignatureValue"));
|
|
4016
|
+
},
|
|
4017
|
+
check: function (cert, ctx) {
|
|
4018
|
+
var missing = ["subjectAltPublicKeyInfo", "altSignatureAlgorithm", "altSignatureValue"]
|
|
4019
|
+
.filter(function (n) { return !ctx.raw(n); });
|
|
4020
|
+
return missing.length ? { context: { missing: missing.join(", ") } } : null;
|
|
4021
|
+
},
|
|
4022
|
+
},
|
|
4023
|
+
{
|
|
4024
|
+
id: "lint/x509-altsig/critical", severity: "warn", source: "x509-altsig", citation: "ITU-T X.509 (2019) 9.8.2 / 9.8.3",
|
|
4025
|
+
message: "the alternative cryptographic algorithm extensions are recommended non-critical",
|
|
4026
|
+
check: function (cert) {
|
|
4027
|
+
var rows = [];
|
|
4028
|
+
(cert.extensions || []).forEach(function (e) {
|
|
4029
|
+
if (e.critical !== true) return;
|
|
4030
|
+
if (e.oid !== OID_SAPKI && e.oid !== OID_ALT_ALG && e.oid !== OID_ALT_VAL) return;
|
|
4031
|
+
rows.push({ context: { extension: e.name || e.oid, recommended: "non-critical", was: "critical" } });
|
|
4032
|
+
});
|
|
4033
|
+
return rows.length ? rows : null;
|
|
4034
|
+
},
|
|
4035
|
+
},
|
|
4036
|
+
];
|
|
4037
|
+
|
|
4038
|
+
var RFC9763_RULES = [
|
|
4039
|
+
{
|
|
4040
|
+
id: "lint/rfc9763/related-certificate-critical", severity: "warn", source: "rfc9763", citation: "RFC 9763 4.1",
|
|
4041
|
+
message: "the relatedCertificate extension should not be marked critical",
|
|
4042
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("relatedCertificate"); },
|
|
4043
|
+
check: function (cert, ctx) {
|
|
4044
|
+
var e = ctx.raw("relatedCertificate");
|
|
4045
|
+
return e.critical === true ? { context: { extension: "relatedCertificate", recommended: "non-critical", was: "critical" } } : null;
|
|
4046
|
+
},
|
|
4047
|
+
},
|
|
4048
|
+
{
|
|
4049
|
+
id: "lint/rfc9763/related-certificate-on-ca", severity: "error", source: "rfc9763", citation: "RFC 9763 4.1",
|
|
4050
|
+
message: "the relatedCertificate extension must only be included in the end-entity certificate",
|
|
4051
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("relatedCertificate"); },
|
|
4052
|
+
check: function (cert, ctx) {
|
|
4053
|
+
var bc = ctx.decode("basicConstraints");
|
|
4054
|
+
return (bc && bc.value && bc.value.cA === true) ? { context: { extension: "relatedCertificate", cA: true } } : null;
|
|
4055
|
+
},
|
|
4056
|
+
},
|
|
4057
|
+
];
|
|
3975
4058
|
|
|
3976
4059
|
/** @internal RFC 5280 sec. 5 states the criticality of each CRL extension it profiles, and sec. 5.3
|
|
3977
4060
|
* does the same for the entry extensions. certificateIssuer is critical (sec. 5.3.3) because a
|
|
@@ -6329,6 +6412,8 @@ var PROFILES = Object.assign(Object.create(null), {
|
|
|
6329
6412
|
"rfc9881": RFC9881_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6330
6413
|
"rfc9909": RFC9909_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6331
6414
|
"rfc9935": RFC9935_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6415
|
+
"rfc9763": RFC9763_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6416
|
+
"x509-altsig": X509_ALTSIG_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6332
6417
|
"cabf-tls": CABF_TLS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6333
6418
|
"cabf-smime": CABF_SMIME_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6334
6419
|
"cabf-cs": CABF_CS_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
@@ -6351,6 +6436,7 @@ var PROFILES = Object.assign(Object.create(null), {
|
|
|
6351
6436
|
.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6352
6437
|
});
|
|
6353
6438
|
var ALL_RULES = RFC5280_RULES.concat(RFC9881_RULES).concat(RFC9909_RULES).concat(RFC9935_RULES)
|
|
6439
|
+
.concat(RFC9763_RULES).concat(X509_ALTSIG_RULES)
|
|
6354
6440
|
.concat(CABF_TLS_RULES).concat(CABF_SMIME_RULES).concat(CABF_CS_RULES).concat(ETSI_QC_RULES)
|
|
6355
6441
|
/** @internal The root-program rows are in the registry so `rules()` enumerates them, and stay
|
|
6356
6442
|
* inert under the default set because each one asks whether the caller named the profile. */
|
|
@@ -6363,11 +6449,89 @@ var CABF_TLS_CSR_RULES = CABF_TLS_RULES.filter(function (r) {
|
|
|
6363
6449
|
/** @internal A requested extension whose value does not decode is the same fault in a request as
|
|
6364
6450
|
* in a certificate, so the request profiles take the same row the certificate profiles do. */
|
|
6365
6451
|
var CSR_STRUCTURAL_RULES = RFC2986_CSR_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES);
|
|
6452
|
+
|
|
6453
|
+
var OID_SOP_ATTR = oid.byName("statementOfPossession");
|
|
6454
|
+
var SOP_SIGNATURE_USAGES = ["digitalSignature", "nonRepudiation", "keyCertSign", "cRLSign"];
|
|
6455
|
+
/** @internal RFC 9883 sec. 6 states one prohibition outright: "The privateKeyPossessionStatement
|
|
6456
|
+
* attribute MUST NOT be used to obtain a signature certificate." Sec. 4 states the same requirement as a
|
|
6457
|
+
* property of the request: "the subjectPKInfo MUST contain the public key for the key establishment
|
|
6458
|
+
* algorithm". The attribute exists so a key-establishment key, which cannot sign, can still be certified;
|
|
6459
|
+
* a request carrying it while asking for a signature key usage, or naming a subject key that can only
|
|
6460
|
+
* sign, is asking for the certificate those sentences forbid. The two name comparisons sec. 3 states are
|
|
6461
|
+
* deliberately absent here: each ends in "the certificate policy MUST describe how the CA can determine
|
|
6462
|
+
* that the two subject names identify the same entity", which is a question no linter can answer, and
|
|
6463
|
+
* `pki.possession.verifyRequest` reports the comparison for a caller's policy to resolve. */
|
|
6464
|
+
function _csrHasPossessionStatement(csr) {
|
|
6465
|
+
return (csr.attributes || []).some(function (a) { return a.type === OID_SOP_ATTR; });
|
|
6466
|
+
}
|
|
6467
|
+
/** @internal The same table `pki.possession.verifyRequest` reads, so the linter and the verifier grade one
|
|
6468
|
+
* request alike. Read off the requested key usages alone, this row passed a request that named no usage
|
|
6469
|
+
* while asking to certify a key that can only ever sign, which is the sec. 4 MUST on the subjectPKInfo and
|
|
6470
|
+
* the sec. 6 prohibition both, and which the verifier refuses; `pki.csr.sign` under this profile then
|
|
6471
|
+
* emitted it. */
|
|
6472
|
+
var _isSignatureOnlyKey = pkix.signatureOnlyKeyOid(NS);
|
|
6473
|
+
function _csrSubjectKeyOid(csr) {
|
|
6474
|
+
return csr.subjectPublicKeyInfo && csr.subjectPublicKeyInfo.algorithm
|
|
6475
|
+
? csr.subjectPublicKeyInfo.algorithm.oid : null;
|
|
6476
|
+
}
|
|
6477
|
+
var RFC9883_CSR_RULES = [
|
|
6478
|
+
{
|
|
6479
|
+
id: "lint/rfc9883/signature-certificate-requested", severity: "error", source: "rfc9883", citation: "RFC 9883 4 and 6",
|
|
6480
|
+
message: "a request carrying a possession statement must not ask for a signature certificate",
|
|
6481
|
+
appliesTo: function (csr) { return _csrHasPossessionStatement(csr); },
|
|
6482
|
+
check: function (csr, ctx) {
|
|
6483
|
+
/** @internal The subject key decides on its own when it can only sign, so this is read whether or
|
|
6484
|
+
* not the request names a usage. A key that can do either says nothing here and is judged by the
|
|
6485
|
+
* usages the request asks for. */
|
|
6486
|
+
var keyOid = _csrSubjectKeyOid(csr);
|
|
6487
|
+
if (_isSignatureOnlyKey(keyOid)) {
|
|
6488
|
+
return { context: { subjectKeyAlgorithm: oid.name(keyOid) || keyOid } };
|
|
6489
|
+
}
|
|
6490
|
+
var ku = ctx.decode("keyUsage");
|
|
6491
|
+
var value = ku && ku.value;
|
|
6492
|
+
if (!value) return null;
|
|
6493
|
+
var asked = SOP_SIGNATURE_USAGES.filter(function (u) { return value[u] === true; });
|
|
6494
|
+
return asked.length ? { context: { keyUsage: asked.join(", ") } } : null;
|
|
6495
|
+
},
|
|
6496
|
+
},
|
|
6497
|
+
{
|
|
6498
|
+
id: "lint/rfc9883/statement-value-undecodable", severity: "error", source: "rfc9883", citation: "RFC 9883 3",
|
|
6499
|
+
message: "a statementOfPossession attribute value must be a well-formed PrivateKeyPossessionStatement",
|
|
6500
|
+
appliesTo: function (csr) { return _csrHasPossessionStatement(csr); },
|
|
6501
|
+
check: function (csr) {
|
|
6502
|
+
var rows = [];
|
|
6503
|
+
(csr.attributes || []).forEach(function (a) {
|
|
6504
|
+
if (a.type !== OID_SOP_ATTR) return;
|
|
6505
|
+
if (a.privateKeyPossessionStatement === undefined) {
|
|
6506
|
+
rows.push({ context: { attribute: a.name || a.type } });
|
|
6507
|
+
}
|
|
6508
|
+
});
|
|
6509
|
+
return rows.length ? rows : null;
|
|
6510
|
+
},
|
|
6511
|
+
},
|
|
6512
|
+
/** @internal Sec. 3 states the attribute's syntax in the singular and nothing anywhere in RFC 9883 says
|
|
6513
|
+
* which of several a CA should honor, so a request carrying more than one leaves every check sec. 3 asks
|
|
6514
|
+
* for without a subject: the checks would be made against one statement while the CA certifies a request
|
|
6515
|
+
* that carries another naming a different certificate. `pki.possession.verifyRequest` refuses such a
|
|
6516
|
+
* request with `possession/bad-request`, and without this row the linter graded it conforming and
|
|
6517
|
+
* `pki.csr.sign` under this profile would emit it. */
|
|
6518
|
+
{
|
|
6519
|
+
id: "lint/rfc9883/statement-attribute-repeated", severity: "error", source: "rfc9883", citation: "RFC 9883 3",
|
|
6520
|
+
message: "a request must carry one statementOfPossession attribute, not several",
|
|
6521
|
+
appliesTo: function (csr) { return _csrHasPossessionStatement(csr); },
|
|
6522
|
+
check: function (csr) {
|
|
6523
|
+
var count = 0;
|
|
6524
|
+
(csr.attributes || []).forEach(function (a) { if (a.type === OID_SOP_ATTR) count += 1; });
|
|
6525
|
+
return count > 1 ? { context: { attributes: String(count) } } : null;
|
|
6526
|
+
},
|
|
6527
|
+
},
|
|
6528
|
+
];
|
|
6366
6529
|
var CSR_PROFILES = Object.assign(Object.create(null), {
|
|
6367
6530
|
"rfc2986": CSR_STRUCTURAL_RULES,
|
|
6368
6531
|
"cabf-tls": CSR_STRUCTURAL_RULES.concat(CABF_TLS_CSR_RULES),
|
|
6532
|
+
"rfc9883": RFC9883_CSR_RULES.concat(RFC5280_EXTENSION_SYNTAX_RULES),
|
|
6369
6533
|
});
|
|
6370
|
-
var ALL_CSR_RULES = CSR_STRUCTURAL_RULES;
|
|
6534
|
+
var ALL_CSR_RULES = CSR_STRUCTURAL_RULES.concat(RFC9883_CSR_RULES);
|
|
6371
6535
|
|
|
6372
6536
|
function _subjectIsNonEmpty(csr) {
|
|
6373
6537
|
var rdns = csr.subject && csr.subject.rdns;
|