@blamejs/pki 0.7.37 → 0.7.39
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 +22 -0
- package/README.md +1 -1
- package/lib/constants.js +5 -0
- package/lib/guard-parsed.js +2 -0
- package/lib/lint.js +68 -0
- package/lib/schema-ocsp.js +11 -1
- package/lib/schema-x509.js +7 -4
- package/lib/tsp-sign.js +3 -2
- package/lib/x509-sign.js +19 -6
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.7.39 — 2026-09-12
|
|
8
|
+
|
|
9
|
+
An OCSP CertID's issuer hashes are held to the length of the digest they name.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- pki.schema.ocsp.parseRequest and parseResponse refuse a CertID whose issuerNameHash or issuerKeyHash is not the length of the digest its hashAlgorithm names (ocsp/bad-cert-id, RFC 6960 sec. 4.1.1), for SHA-1, SHA-256, SHA-384, SHA-512, SHA3-256 and SHA3-512; a CertID under a digest this build does not know is read as given. pki.ocsp.verify and pki.ocsp.verifyRequest, which parse first, report such a request or response as malformed instead of comparing a hash that could never match.
|
|
14
|
+
- The digest output lengths live in pki.constants.NAMES.DIGEST_OCTETS, read by the OCSP parser and by pki.tsp.sign and pki.tsp.verify for the messageImprint length check they already made.
|
|
15
|
+
|
|
16
|
+
## v0.7.38 — 2026-09-12
|
|
17
|
+
|
|
18
|
+
pki.lint.certificate grades six RFC 5280 rules its parser read past, and the parser surfaces the unique identifiers.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- pki.lint.certificate rules: lint/rfc5280/policy-mapping-any-policy (error, sec. 4.2.1.5: policies MUST NOT be mapped to or from anyPolicy), lint/rfc5280/unique-identifier-present (error, sec. 4.1.2.8: a conforming CA MUST NOT generate a certificate with a unique identifier), lint/rfc5280/san-critical-with-subject (warn, sec. 4.2.1.6: a subjectAltName beside a non-empty subject SHOULD be non-critical), lint/rfc5280/eku-critical-with-any-purpose (warn, sec. 4.2.1.12: an extKeyUsage naming anyExtendedKeyUsage SHOULD NOT be critical), lint/rfc5280/notice-ref-used (warn, sec. 4.2.1.4: noticeRef SHOULD NOT be used), and lint/rfc5280/recommended-criticality (warn, sec. 4.2.1.7 and 4.2.1.13: issuerAltName and cRLDistributionPoints SHOULD be non-critical), each naming its clause in the finding.
|
|
23
|
+
- pki.schema.x509.parse returns issuerUniqueID and subjectUniqueID, each { unusedBits, bytes } or null, so a relying party sees the fields RFC 5280 sec. 4.1.2.8 tells a CA not to generate.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- pki.x509.sign refuses a pre-encoded policyMappings extension that maps to or from anyPolicy, the rule its object form and pki.path.validate already applied (RFC 5280 sec. 4.2.1.5).
|
|
28
|
+
|
|
7
29
|
## v0.7.37 — 2026-09-12
|
|
8
30
|
|
|
9
31
|
Every object a signer reads refuses a field it does not read, a signed OCSP request names its signer, and the requester nonce meets the RFC 9654 floor.
|
package/README.md
CHANGED
|
@@ -252,7 +252,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
252
252
|
| `pki.sigstore` | Offline verifier for a Sigstore bundle, the artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots and Rekor log keys, never trusted from the bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key, the ephemeral Fulcio certificate chain validated as of the Rekor log time, the RFC 9162 inclusion proof folded to a Rekor-signed tree root, the log entry bound to this exact signature, and the in-toto SLSA subject digest the caller confirms against the published artifact. It reuses the X.509 parser, the RFC 5280 path validator, and the Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `verified: true` says the artifact was signed and logged, not that a party you trust signed it — Fulcio issues to anyone who completes an OIDC flow, so who signed is decided only by `opts.identity`, and `identityChecked` reports which of its fields were compared. An identity policy naming no field, or a field name that is not one of the three, is refused rather than satisfied — either would accept every signer while reading as a policy in force. A sixth leg is opt-in: pinning the certificate-transparency logs in `opts.ctLogs` checks the receipt Fulcio embeds in the certificate it issues (RFC 6962 §3.2), verified over the certificate as it stood before the receipt was added and under the key of the log that issued it, so the verdict says the signing certificate was public when it was issued rather than handed out quietly. At least one receipt must verify against a pinned log, the verdict reports `sctChecked` and `validScts`, and an empty log set is refused rather than read as a policy that checks nothing. Both content arms are read: a DSSE-wrapped in-toto attestation, and a message signature over an artifact's own bytes, which `cosign sign-blob` produces. For that arm `opts.artifact` supplies the bytes, and they are held to the hash the Rekor `hashedrekord` entry records before the signature is checked over them; the `messageDigest` the bundle carries is covered by no signature, so it is compared against the computed digest and never used in its place. `contentType` names the arm and the verdict has one shape for both — `pae`, `parseBundle`, `verifyBundle` |
|
|
253
253
|
| `pki.inspect` | Human-readable inspection, the pure-JS equivalent of `openssl x509/crl/req/cms -text`. `certificate(pem \| der \| parsed)` renders an OpenSSL-style report: version, serial, signature algorithm, issuer and subject distinguished names, validity, public-key details (curve or modulus size plus the raw point or modulus), every decoded extension with its critical flag, and the signature. `crl`, `csr`, and `cms` render the other formats the same way — a CRL like `openssl crl -text`, a CSR like `openssl req -text`, and a CMS message like `openssl cms -cmsout -print`, with a stable summary for a non-SignedData ContentInfo — and `any(input)` detects the format and routes to the right report. Built over the strict parsers and the two-way OID registry with one set of field renderers, it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency, and the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version. A certificate policy's user notice renders as text, both its explicit text and a notice reference with the notice numbers that identify it, rather than hex, and a malformed part falls back to a hex dump rather than throwing — `certificate`, `crl`, `csr`, `cms`, `any` |
|
|
254
254
|
| `pki.webauthn` | WebAuthn and passkey verification, both halves: offline trust evaluation of a W3C WebAuthn (Level 3) registration, and signature verification of the assertion every login returns. `parseAttestationObject(bytes)` decodes the CBOR attestation object, authenticatorData, and COSE credential key over the strict `pki.cbor` codec; `parseAuthenticatorData(bytes)` reads the bare form an assertion carries through the same parser; `parseClientData(bytes, opts)` decodes the `clientDataJSON` no signature check looks inside, through the shared JSON guard since these are attacker-chosen bytes, returning the challenge decoded so a caller compares bytes rather than spellings, and checking the ceremony type, challenge, and origin when the relying party supplies what it issued. `verifyAssertion(input)` verifies an assertion signature over `authenticatorData \|\| SHA-256(clientDataJSON)` — raw bytes, no COSE_Sign1, an ES256 signature in ASN.1 DER — and applies the §7.2 step 21 counter rule when a stored `previousSignCount` is given, so a counter that fails to advance is refused as a cloned authenticator. `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for packed, tpm, android-key, apple, fido-u2f, and none: the x5c leaf key, the apple nonce, the tpm `certInfo` Name and `extraData` over the `pubArea`, the android `KeyDescription`, and the fido-u2f `verificationData`. It binds the credential public key to each attestation, through the signed authenticatorData for packed and fido-u2f or a cert or `pubArea`-key equality check for android-key, apple, and tpm, and enforces each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256/384/512, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers ESP256/384/512, Ed25519, and Ed448 — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally encoded DER ECDSA signature. The verdict field is `attestationVerified`, and `signatureVerified` for an assertion, rather than a bare `verified`, because a sound statement is a different claim from an acceptable ceremony: an attestation naming another origin's RP ID with user presence clear is perfectly sound and must not be registered. Pass `expectedRpId`, `requireUserPresence`, `requireUserVerification`, or `allowedAlgorithms` and those are checked, with `bindingChecked` reporting which ran, so a check that passed can be told from one that never happened. Pass `opts.clientDataJSON` instead of the digest and the client data is read here too: the ceremony type is checked unconditionally, since a login response replayed into a registration is what that rule stops, and the challenge, origin, and top-level origin are checked against what the relying party issued, with `clientData.checked` reporting which comparisons ran. A registration verdict also carries the `credentialId`, `credentialPublicKey`, and initial `signCount` a later login needs. A credential key declaring COSE algorithm `-65535` (RSASSA-PKCS1-v1_5 with SHA-1) is refused unless `allowedAlgorithms` names it, since every signature that credential ever makes would use SHA-1. Anchoring the trust path has two routes: `opts.metadata` resolves the roots the authenticator's own model registered, and `opts.rootCertificates` pins roots directly, which is what anchors the formats FIDO MDS does not cover, Apple's authenticators and the Google hardware-attestation roots among them. `metadata` governs when both are given, and `anchoredTo` names every route that anchored the path, joined with `+` when more than one did: `"metadata"`, `"rootCertificates"`, and `"safetyNetRoots"` for the android-safetynet chain, which anchors through the roots that format requires whether or not either other route was asked for. It is `null` only when nothing anchored the path. `verifyMetadataBlob(blob, opts)` reads a FIDO Metadata Service (MDS v3) BLOB, the signed catalogue of registered authenticator models, verifying its JWS and chaining its signer to an operator-supplied FIDO root before the payload is parsed, with sequence-number rollback and `nextUpdate` freshness checks. Passing the result as `opts.metadata` to `verify` resolves the authenticator's registered attestation roots from its identifier and requires the trust path to fully validate to one of them, refusing an unlisted or revoked model. `metadataAnchors(entry, opts)` applies the same status gate for a caller anchoring the path themselves: a model the catalogue has disqualified registers no anchors. Both of the catalogue's key spaces are covered: an aaguid, and the attestation-certificate key identifiers a U2F authenticator is listed under instead. No FIDO root is bundled, there is no trust-on-first-use, and retrieving the BLOB is out of scope. Fail-closed with typed `webauthn/*` errors — `parseAttestationObject`, `verify`, `verifyMetadataBlob`, `metadataFor`, `metadataAnchors` |
|
|
255
|
-
| `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized), SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `rules`, `profiles` |
|
|
255
|
+
| `pki.lint` | Certificate linting, the zlint or pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded advisory findings, each with a stable id, a severity (`fatal`, `error`, `warn`, `notice`), a source, a spec-clause citation, and a message, against the RFC 5280 profile, the post-quantum certificate profiles, and a representative CA/Browser Forum TLS BR subset: serial sign and size, validity ordering and the SC081v3 reducing validity schedule, keyCertSign coherence, extension criticality (basicConstraints, nameConstraints, policyConstraints and inhibitAnyPolicy must be critical, and keyUsage should be), nameConstraints CA-scope, unknown critical extensions, empty-subject SAN, SKI and AKI presence including the end-entity subjectKeyIdentifier, the six extensions a conforming CA must mark non-critical (reported by clause rather than as unrecognized) and the two it should (issuerAltName, cRLDistributionPoints), a policy mapping to or from anyPolicy, a unique identifier a conforming CA must not generate, a critical subjectAltName beside a non-empty subject, a critical extKeyUsage naming anyExtendedKeyUsage, a userNotice noticeRef, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, the post-quantum key-usage rules binding a certificate's keyUsage bits to its subject key type (ML-KEM under RFC 9935 §5, ML-DSA under RFC 9881 §5, and SLH-DSA under RFC 9909 §6 across both its pure and prehash identifiers) together with the ML-KEM encapsulation-key encoding and size, and the §4.2.1.4 certificate-policy user-notice rules (a VisibleString or BMPString explicitText, a notice past 200 characters, an empty notice, control characters, and a non-NFC UTF8String notice, each at the strength the clause states). Alone among these entries the data path never throws: hostile bytes return a `fatal` `lint/unparseable` finding carrying the strict parser's code, so a whole directory lints without a try/catch, and only config-time misuse throws a typed `LintError` — `certificate`, `crl` (the RFC 5280 §5 revocation-list profile: nextUpdate presence, update ordering, cRLNumber presence and its 20-octet ceiling, authorityKeyIdentifier presence, the criticality each profiled CRL and entry extension must carry, freshestCRL in a delta CRL, and unrecognized critical extensions), `ocsp` (the RFC 6960 response profile: an empty signature, each SingleResponse's update ordering and its relation to producedAt, an empty `certs` field, the extended revoked definition and the fixed shape of a revoked answer for a non-issued certificate, the list each extension belongs in, the criticality and value syntax of the CRL entry extensions a SingleResponse may carry, unrecognized critical extensions, the reasons unspecified and removeFromCRL, and two answers for one CertID; the RFC 5019 lightweight rows run on request), `rules`, `profiles` |
|
|
256
256
|
| `pki.C` / `pki.constants` | Version-stable constants: the functional scale helpers `C.TIME.*` and `C.BYTES.*`, the codec `LIMITS`, and `version` |
|
|
257
257
|
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError`, `Asn1Error`, `OidError`, `PemError`, `CertificateError`, `CrlError`, `CsrError`, `Pkcs8Error`, `CmsError`, `OcspError`, `TspError`, `AttrCertError`, `CrmfError`, `Pkcs12Error`, `CmpError`, `PathError`, `CtError`, `JoseError`, `AcmeError`, `WebauthnError`, and `LintError`, each carrying a stable `code` in `domain/reason` form |
|
|
258
258
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <cert>`, `pki lint <cert\|crl\|ocsp-response>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>` |
|
package/lib/constants.js
CHANGED
|
@@ -238,6 +238,11 @@ var NAMES = Object.assign(Object.create(null), {
|
|
|
238
238
|
"SLH-DSA-SHA2-128s": "SLH-DSA-SHA2-128s", "SLH-DSA-SHA2-128f": "SLH-DSA-SHA2-128f",
|
|
239
239
|
"SLH-DSA-SHA2-192s": "SLH-DSA-SHA2-192s", "SLH-DSA-SHA2-256s": "SLH-DSA-SHA2-256s",
|
|
240
240
|
},
|
|
241
|
+
/** @internal The output length in octets of each fixed-length digest, by the OID registry's name
|
|
242
|
+
* (FIPS 180-4, FIPS 202). A structure that carries "the hash of X under algorithm A" is held to
|
|
243
|
+
* this length: an OCSP CertID's issuer hashes, a timestamp's messageImprint. The XOFs (SHAKE) have
|
|
244
|
+
* no single length and are absent. */
|
|
245
|
+
DIGEST_OCTETS: { sha1: 20, sha256: 32, sha384: 48, sha512: 64, "sha3-256": 32, "sha3-512": 64 },
|
|
241
246
|
});
|
|
242
247
|
|
|
243
248
|
/** @internal Each inner table is looked up by a name a certificate supplies, so it answers from
|
package/lib/guard-parsed.js
CHANGED
|
@@ -79,6 +79,8 @@ function isCert(o) {
|
|
|
79
79
|
!!o.subjectPublicKeyInfo && _isBuffer(o.subjectPublicKeyInfo.bytes) &&
|
|
80
80
|
_isAlgorithmIdentifier(o.subjectPublicKeyInfo.algorithm) &&
|
|
81
81
|
_isBitString(o.subjectPublicKeyInfo.publicKey) &&
|
|
82
|
+
(o.issuerUniqueID === null || _isBitString(o.issuerUniqueID)) &&
|
|
83
|
+
(o.subjectUniqueID === null || _isBitString(o.subjectUniqueID)) &&
|
|
82
84
|
_isArray(o.extensions) && _every(o.extensions, _isExtensionEntry);
|
|
83
85
|
}
|
|
84
86
|
|
package/lib/lint.js
CHANGED
|
@@ -234,6 +234,13 @@ function _ecCurveName(spki) {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
var OID_UNOTICE = oid.byName("unotice");
|
|
237
|
+
var OID_ANY_POLICY = oid.byName("anyPolicy");
|
|
238
|
+
/** @internal The extensions RFC 5280 says a conforming CA SHOULD mark non-critical, keyed by OID
|
|
239
|
+
* with the clause; the MUSTs live in the shared CERT_FIXED_CRITICALITY table. The subjectAltName
|
|
240
|
+
* and extKeyUsage recommendations are conditional and have their own rules. */
|
|
241
|
+
var CERT_RECOMMENDED_NON_CRITICAL = Object.create(null);
|
|
242
|
+
CERT_RECOMMENDED_NON_CRITICAL[oid.byName("issuerAltName")] = "RFC 5280 sec. 4.2.1.7";
|
|
243
|
+
CERT_RECOMMENDED_NON_CRITICAL[oid.byName("cRLDistributionPoints")] = "RFC 5280 sec. 4.2.1.13";
|
|
237
244
|
var _T_BMP = asn1.TAGS.BMP_STRING, _T_VISIBLE = asn1.TAGS.VISIBLE_STRING, _T_UTF8 = asn1.TAGS.UTF8_STRING;
|
|
238
245
|
function _hasControlChar(str) {
|
|
239
246
|
for (var i = 0; i < str.length; i++) {
|
|
@@ -397,6 +404,67 @@ var RFC5280_RULES = [
|
|
|
397
404
|
"RFC 5280 4.2.1.14", "the inhibitAnyPolicy extension must be marked critical"),
|
|
398
405
|
_criticalityRule("keyUsage", "lint/rfc5280/key-usage-not-critical", "warn",
|
|
399
406
|
"RFC 5280 4.2.1.3", "the keyUsage extension should be marked critical"),
|
|
407
|
+
{
|
|
408
|
+
id: "lint/rfc5280/recommended-criticality", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.7 / 4.2.1.13",
|
|
409
|
+
message: "an extension a conforming CA should mark non-critical is marked critical",
|
|
410
|
+
check: function (cert) {
|
|
411
|
+
var rows = [];
|
|
412
|
+
(cert.extensions || []).forEach(function (e) {
|
|
413
|
+
var row = CERT_RECOMMENDED_NON_CRITICAL[e.oid];
|
|
414
|
+
if (row === undefined || e.critical !== true) return;
|
|
415
|
+
rows.push({ context: { extension: e.name || e.oid, oid: e.oid, recommended: "non-critical", was: "critical", citation: row } });
|
|
416
|
+
});
|
|
417
|
+
return rows.length ? rows : null;
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: "lint/rfc5280/policy-mapping-any-policy", severity: "error", source: "rfc5280", citation: "RFC 5280 4.2.1.5",
|
|
422
|
+
message: "a policy mapping names anyPolicy as its issuer or subject domain policy, which a mapping must not do",
|
|
423
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("policyMappings"); },
|
|
424
|
+
check: function (cert, ctx) {
|
|
425
|
+
var d = ctx.decode("policyMappings");
|
|
426
|
+
if (!d || !Array.isArray(d.value)) return null;
|
|
427
|
+
var bad = d.value.filter(function (m) { return m.issuerDomainPolicy === OID_ANY_POLICY || m.subjectDomainPolicy === OID_ANY_POLICY; });
|
|
428
|
+
return bad.length ? { context: { count: bad.length } } : null;
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: "lint/rfc5280/unique-identifier-present", severity: "error", source: "rfc5280", citation: "RFC 5280 4.1.2.8",
|
|
433
|
+
message: "the certificate carries a unique identifier, which a conforming CA must not generate",
|
|
434
|
+
check: function (cert) {
|
|
435
|
+
var which = [];
|
|
436
|
+
if (cert.issuerUniqueID) which.push("issuerUniqueID");
|
|
437
|
+
if (cert.subjectUniqueID) which.push("subjectUniqueID");
|
|
438
|
+
return which.length ? { context: { fields: which } } : null;
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
id: "lint/rfc5280/san-critical-with-subject", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.6",
|
|
443
|
+
message: "a subjectAltName beside a non-empty subject should be marked non-critical",
|
|
444
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("subjectAltName"); },
|
|
445
|
+
check: function (cert, ctx) {
|
|
446
|
+
var e = ctx.raw("subjectAltName");
|
|
447
|
+
return (e.critical === true && cert.subject.rdns.length > 0) ? true : null;
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
id: "lint/rfc5280/eku-critical-with-any-purpose", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.12",
|
|
452
|
+
message: "an extKeyUsage that names anyExtendedKeyUsage should not be marked critical",
|
|
453
|
+
appliesTo: function (cert, ctx) { return !!ctx.raw("extKeyUsage"); },
|
|
454
|
+
check: function (cert, ctx) {
|
|
455
|
+
var e = ctx.raw("extKeyUsage");
|
|
456
|
+
return (e.critical === true && _hasEku(cert, "anyExtendedKeyUsage")) ? true : null;
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
id: "lint/rfc5280/notice-ref-used", severity: "warn", source: "rfc5280", citation: "RFC 5280 4.2.1.4",
|
|
461
|
+
message: "a userNotice policy qualifier carries a noticeRef, which should not be used",
|
|
462
|
+
appliesTo: _hasPolicyDisplayText,
|
|
463
|
+
check: function (cert, ctx) {
|
|
464
|
+
var refs = _policyDisplayTexts(ctx).filter(function (d) { return d.field === "organization"; });
|
|
465
|
+
return refs.length ? { context: { count: refs.length } } : null;
|
|
466
|
+
},
|
|
467
|
+
},
|
|
400
468
|
{
|
|
401
469
|
id: "lint/rfc5280/ski-missing-ee", severity: "notice", source: "rfc5280", citation: "RFC 5280 4.2.1.2",
|
|
402
470
|
message: "an end-entity certificate should carry a subjectKeyIdentifier extension",
|
package/lib/schema-ocsp.js
CHANGED
|
@@ -183,9 +183,19 @@ var CERT_ID = schema.seq([
|
|
|
183
183
|
schema.field("serialNumber", schema.integerLeaf()),
|
|
184
184
|
], {
|
|
185
185
|
assert: "sequence", arity: { exact: 4 }, code: "ocsp/bad-cert-id", what: "CertID",
|
|
186
|
+
/** @internal RFC 6960 sec. 4.1.1: issuerNameHash and issuerKeyHash are the hashes of the issuer's
|
|
187
|
+
* name and key "using the hash algorithm specified in hashAlgorithm", so each is that digest's
|
|
188
|
+
* length; a hash of any other length names no certificate. A digest this build does not know is
|
|
189
|
+
* read as given, since its length is not known here. */
|
|
186
190
|
build: function (m) {
|
|
191
|
+
var hashAlgorithm = m.fields.hashAlgorithm.value.result;
|
|
192
|
+
var want = constants.NAMES.DIGEST_OCTETS[hashAlgorithm.name];
|
|
193
|
+
if (want !== undefined) {
|
|
194
|
+
if (m.fields.issuerNameHash.value.length !== want) throw NS.E("ocsp/bad-cert-id", "CertID issuerNameHash is " + m.fields.issuerNameHash.value.length + " octets, not the " + want + " a " + hashAlgorithm.name + " digest is (RFC 6960 sec. 4.1.1)");
|
|
195
|
+
if (m.fields.issuerKeyHash.value.length !== want) throw NS.E("ocsp/bad-cert-id", "CertID issuerKeyHash is " + m.fields.issuerKeyHash.value.length + " octets, not the " + want + " a " + hashAlgorithm.name + " digest is (RFC 6960 sec. 4.1.1)");
|
|
196
|
+
}
|
|
187
197
|
return {
|
|
188
|
-
hashAlgorithm:
|
|
198
|
+
hashAlgorithm: hashAlgorithm,
|
|
189
199
|
issuerNameHash: m.fields.issuerNameHash.value,
|
|
190
200
|
issuerKeyHash: m.fields.issuerKeyHash.value,
|
|
191
201
|
serialNumber: m.fields.serialNumber.value,
|
package/lib/schema-x509.js
CHANGED
|
@@ -153,6 +153,8 @@ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
|
|
|
153
153
|
subject: tbs.fields.subject.value.result,
|
|
154
154
|
validity: tbs.fields.validity.value.result,
|
|
155
155
|
subjectPublicKeyInfo: tbs.fields.subjectPublicKeyInfo.value.result,
|
|
156
|
+
issuerUniqueID: tbs.fields.issuerUniqueID.present ? tbs.fields.issuerUniqueID.value : null,
|
|
157
|
+
subjectUniqueID: tbs.fields.subjectUniqueID.present ? tbs.fields.subjectUniqueID.value : null,
|
|
156
158
|
extensions: hasExtensions ? extField.value.result : [],
|
|
157
159
|
tbsBytes: e.tbsBytes,
|
|
158
160
|
signatureValue: e.signatureValue,
|
|
@@ -173,10 +175,11 @@ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
|
|
|
173
175
|
* Parse a DER `Buffer` or a PEM string/Buffer into a structured
|
|
174
176
|
* certificate: `{ version, serialNumber, serialNumberHex,
|
|
175
177
|
* signatureAlgorithm, issuer, subject, validity, subjectPublicKeyInfo,
|
|
176
|
-
* extensions, tbsBytes, signatureValue }`.
|
|
177
|
-
* both as a rendered `dn` string and as
|
|
178
|
-
* window is real `Date`s;
|
|
179
|
-
*
|
|
178
|
+
* issuerUniqueID, subjectUniqueID, extensions, tbsBytes, signatureValue }`.
|
|
179
|
+
* Distinguished names come back both as a rendered `dn` string and as
|
|
180
|
+
* structured `rdns`; the validity window is real `Date`s; the two unique
|
|
181
|
+
* identifiers are `{ unusedBits, bytes }` or null (RFC 5280 sec. 4.1.2.8);
|
|
182
|
+
* `tbsBytes` is the exact signed byte range for a downstream verifier.
|
|
180
183
|
*
|
|
181
184
|
* Throws `CertificateError` when the bytes are not a well-formed
|
|
182
185
|
* certificate and `Asn1Error` when the underlying DER is malformed.
|
package/lib/tsp-sign.js
CHANGED
|
@@ -31,6 +31,7 @@ var frameworkError = require("./framework-error");
|
|
|
31
31
|
|
|
32
32
|
var pkix = require("./schema-pkix");
|
|
33
33
|
var pkiBuild = require("./pki-build");
|
|
34
|
+
var constants = require("./constants");
|
|
34
35
|
var TspError = frameworkError.TspError;
|
|
35
36
|
var _NS = pkix.makeNS("tsp", TspError, oid);
|
|
36
37
|
var b = asn1.build;
|
|
@@ -48,7 +49,7 @@ var _b = pkiBuild.makeBuilder({
|
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
var NODE_DIGEST = intrinsic.assign(intrinsic.create(null), { sha256: "sha256", sha384: "sha384", sha512: "sha512" });
|
|
51
|
-
var HASH_LEN =
|
|
52
|
+
var HASH_LEN = constants.NAMES.DIGEST_OCTETS;
|
|
52
53
|
|
|
53
54
|
var _REVOCATION_RANK = intrinsic.assign(intrinsic.create(null), { "false": 0, "undetermined": 1, "waived": 2, "determined": 3 });
|
|
54
55
|
function _rankRevocation(v) {
|
|
@@ -422,7 +423,7 @@ function parseResponse(input) { return schemaTsp.parse(input); }
|
|
|
422
423
|
|
|
423
424
|
|
|
424
425
|
var _VERIFY_DIGEST = intrinsic.assign(intrinsic.create(null), { sha256: "sha256", sha384: "sha384", sha512: "sha512", sha1: "sha1" });
|
|
425
|
-
var _VERIFY_DIGEST_LEN =
|
|
426
|
+
var _VERIFY_DIGEST_LEN = constants.NAMES.DIGEST_OCTETS;
|
|
426
427
|
var _BIND_DIGEST = intrinsic.assign(intrinsic.create(null), { sha256: "sha256", sha384: "sha384", sha512: "sha512" });
|
|
427
428
|
|
|
428
429
|
function _imprintMatches(mi, data) {
|
package/lib/x509-sign.js
CHANGED
|
@@ -130,7 +130,7 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
130
130
|
if (extSpec == null) extSpec = {};
|
|
131
131
|
if (_isArray(extSpec)) {
|
|
132
132
|
var seenExt = intrinsic.create(null), arrCa = false, arrKeyCertSign = false, arrPathLen = false, arrNameConstraints = false;
|
|
133
|
-
var oidBc = O("basicConstraints"), oidKu = O("keyUsage"), oidNc = O("nameConstraints");
|
|
133
|
+
var oidBc = O("basicConstraints"), oidKu = O("keyUsage"), oidNc = O("nameConstraints"), oidPm = O("policyMappings");
|
|
134
134
|
var arr = extSpec.map(function (e, i) {
|
|
135
135
|
var der = _reqDer(e, "extension");
|
|
136
136
|
_assertValidExtension(der, i, EXT_DECODERS);
|
|
@@ -153,6 +153,7 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
153
153
|
if (arrCa && n.children.length !== 3) throw _err("x509/bad-input", "a CA certificate's basicConstraints MUST be critical (RFC 5280 sec. 4.2.1.9)");
|
|
154
154
|
}
|
|
155
155
|
else if (extnId === oidKu) { arrKeyCertSign = decoded.keyCertSign === true; }
|
|
156
|
+
else if (extnId === oidPm) { _assertNoAnyPolicyMapping(decoded); }
|
|
156
157
|
}
|
|
157
158
|
return b.raw(der);
|
|
158
159
|
});
|
|
@@ -272,6 +273,17 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
272
273
|
* the log answers, reconstructed from the other by removing this extension. A certificate carrying
|
|
273
274
|
* both names no stage of that exchange, and the SCTs in it could never verify. Both the typed spec
|
|
274
275
|
* and the pre-encoded array reach the same certificate, so both are held to it here. */
|
|
276
|
+
/** @internal RFC 5280 sec. 4.2.1.5: "Policies MUST NOT be mapped either to or from the special
|
|
277
|
+
* value anyPolicy." The object form's encoder refuses it as it builds; the pre-encoded form is
|
|
278
|
+
* held to the same clause on the decoded value. */
|
|
279
|
+
function _assertNoAnyPolicyMapping(mappings) {
|
|
280
|
+
var anyPolicy = O("anyPolicy");
|
|
281
|
+
for (var i = 0; i < mappings.length; i++) {
|
|
282
|
+
if (mappings[i].issuerDomainPolicy === anyPolicy || mappings[i].subjectDomainPolicy === anyPolicy) {
|
|
283
|
+
throw _err("x509/bad-input", "pre-encoded policyMappings entry " + i + " maps to or from anyPolicy, which RFC 5280 sec. 4.2.1.5 forbids");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
275
287
|
function _assertCtStagesExclusive(hasPoison, hasSctList) {
|
|
276
288
|
if (hasPoison && hasSctList) {
|
|
277
289
|
throw _err("x509/bad-input", "precertificatePoison names a precertificate and signedCertificateTimestampList names the certificate issued after it, so a certificate carries one or the other (RFC 6962 sec. 3.2)");
|
|
@@ -402,11 +414,12 @@ function _hasCriticalSan(extSpec) {
|
|
|
402
414
|
* skip count (sec. 4.2.1.14); both are emitted critical, which those clauses require.
|
|
403
415
|
* `extensions.policyMappings` takes a list of `{ issuerDomainPolicy, subjectDomainPolicy }` naming
|
|
404
416
|
* registered policies or dotted OIDs, and refuses a mapping to or from `anyPolicy`, which sec.
|
|
405
|
-
* 4.2.1.5 forbids, the same rule `pki.path.validate` applies at sec. 6.1.4(a)
|
|
406
|
-
* SHOULD for marking it critical. The validator
|
|
407
|
-
* certificate, where sec. 6.1.4 applies the mappings to
|
|
408
|
-
* the TARGET certificate as unprocessed, rejecting it;
|
|
409
|
-
* `unknown-critical-extension`. It is emitted non-critical
|
|
417
|
+
* 4.2.1.5 forbids, the same rule `pki.path.validate` applies at sec. 6.1.4(a); the pre-encoded
|
|
418
|
+
* form is held to it too. That clause states a SHOULD for marking it critical. The validator
|
|
419
|
+
* processes the extension on an intermediate certificate, where sec. 6.1.4 applies the mappings to
|
|
420
|
+
* the policy tree, and treats a critical one on the TARGET certificate as unprocessed, rejecting it;
|
|
421
|
+
* `pki.lint.certificate` grades that case `unknown-critical-extension`. It is emitted non-critical
|
|
422
|
+
* unless `policyMappingsCritical` is set.
|
|
410
423
|
* `extensions.issuerAltName` takes the `GeneralName` forms `subjectAltName` takes and is emitted
|
|
411
424
|
* non-critical (sec. 4.2.1.7).
|
|
412
425
|
*
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:5b3f8b70-36de-4855-8e1e-c6d22b99d982",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-12T14:40:25.102Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.7.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.7.39",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.39",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.7.39",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.7.
|
|
57
|
+
"ref": "@blamejs/pki@0.7.39",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|