@blamejs/pki 0.6.55 → 0.7.1

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 CHANGED
@@ -4,6 +4,47 @@ 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.1 — 2026-09-10
8
+
9
+ A wildcard name in a certificate is held to an excluded name constraint it can reach.
10
+
11
+ ### Security
12
+
13
+ - pki.path.validate refuses a certificate whose wildcard dNSName reaches a name an excluded subtree forbids. An excluded subtree states the names a certificate must not present, and a wildcard subject alternative name stands for every name one label below its parent rather than for the literal text. Comparing the text alone let a leaf carrying only *.example.com pass an exclusion of bar.example.com, which is a name that leaf presents. Any chain whose certificate authority relies on an excluded dNSName subtree to keep a subordinate away from a name was affected, in every version that shipped name-constraint validation. The permitted direction is unchanged and is not widened by this: a permitted subtree still has to cover the whole of a wildcard, so a base of foo.com admits *.foo.com while a base of bar.example.com does not admit *.example.com. A wildcard reaches exactly one label, so an excluded name deeper than that is out of its reach and is not refused: *.example.com is still admitted against an exclusion of deep.bar.example.com.
14
+
15
+ ## v0.7.0 — 2026-09-09
16
+
17
+ A Sigstore bundle signed over an artifact's own bytes is verified, not just one wrapping an attestation.
18
+
19
+ ### Added
20
+
21
+ - pki.sigstore.verifyBundle verifies a bundle carrying a message_signature. The caller passes the artifact as opts.artifact and the verifier hashes it: under the algorithm the Rekor entry names, compared against the hash that entry records, and then under the leaf certificate's key against the signature the bundle carries. The three happy-path bundles the Sigstore conformance suite publishes are covered, spanning all three bundle media types and both shapes of verification material.
22
+ - opts.artifact takes the artifact's bytes and nothing else. The digest a message_signature carries is covered by no signature, and the field's own definition states that a client must not use it to verify the signature, so there is no shape in which a caller hands over a digest instead: a hex string is refused at the door, and the digest bytes are refused where the artifact is compared against the log entry. A message_signature bundle with no opts.artifact is refused rather than verified from the digest it carries.
23
+ - The digest the bundle states is checked against the one computed, when it is there. It is optional, and a bundle omitting it verifies with messageDigestChecked reporting false. Where it disagrees with the artifact the request is refused, and the verdict reports the digest this computed rather than the one the bundle claimed. It may name any of the five HashAlgorithm members that name an algorithm, the two SHA-3 members included: those are marked deprecated, which says a producer should not choose one rather than that a bundle carrying one is malformed, and this digest names its own algorithm independently of the one the log entry records. It may name the member by its name or by its number, since the JSON mapping states that a parser accepts both; the unspecified member is refused under either spelling, as is a number outside the enum. The entry's algorithm is a different field under a different enum and stays the three the Rekor schema lists.
24
+ - The verdict has one shape for both content arms. contentType names the arm. The fields that carry a value report null where the arm has none: artifactDigest and digestAlgorithm on a DSSE bundle, and payload, statement, subjects, predicateType and predicate on a message signature. The fields that report whether a check ran stay boolean, so messageDigestChecked is false on a DSSE bundle exactly as predicateTypeChecked is false on a message signature. A caller reads the same fields either way rather than telling a missing key from a false one.
25
+ - opts.identity and opts.ctLogs run on this arm as they do on the other, so a caller pins who signed and whether the signing certificate was public when it was issued, whichever content the bundle carries.
26
+ - pki.tls offers RFC 8879 zstd certificate compression. All three registered algorithms are implemented, and each is offered only where the running Node decompresses it safely. At load each is handed a frame with its last two bytes cut: a decompressor that returns anything at all rather than faulting is dropped, because a runtime that accepts a cut frame lets a peer strip a frame's tail and have the receiver read a prefix as the whole message. Node 24.19 returned the prefix for a cut zstd frame, so zstd was dropped there. Node 24.21 faults on one, so all three are offered.
27
+
28
+ ### Changed
29
+
30
+ - A hashedrekord entry may name the verifier as a public key rather than as a certificate. The Rekor field is documented as the public key that verifies the signature, which may also be an X509 code signing certificate carrying that key, so an entry writing the key itself is conforming; it binds when that key is the bundle leaf's, and an entry naming a different key does not bind. The armor says which form the entry wrote, so neither is guessed at from the bytes.
31
+ - Every option is read once, when verification starts, and the arm gates and the verdict read that one answer. An option is whatever its name resolves to on the options object, inherited or own, which is how an unrecognized option name is already detected and refused. Reading them as own properties made an inherited artifact look missing on the arm that requires it and let an inherited predicateType past the refusal that belongs to it. Reading one twice let it answer differently across the two reads: a predicateType that was absent where the arm gate ran and present where the verdict was built returned predicateTypeChecked true on a message signature, which carries no statement to check a predicate type against.
32
+ - A transparency-log entry is read by its own kind and version together. The entry registry holds one row per kind and apiVersion, and each row states which content arm it may accompany, so a hashedrekord v0.0.2 body is refused as an unsupported version rather than read for v0.0.1 field names it does not carry, and an entry describing a DSSE envelope beside a message signature is refused rather than bound to it.
33
+ - An option that belongs to the other arm is refused rather than ignored. Supplying opts.artifact on a dsse_envelope bundle, or opts.predicateType on a message_signature, is an error, since an option read and passed over reads as a check that ran. Either name set to null or undefined counts as not supplied.
34
+ - The supported Node floor moves to 24.21.0, the current long-term-support release, which carries OpenSSL 3.5.8. Nothing is transpiled, so the supported version is the version the source runs on. On a lower version npm reports the mismatch as EBADENGINE, and refuses the install where engine-strict is set. Nothing below the floor is tested, and zstd certificate compression is unavailable there.
35
+
36
+ ### Fixed
37
+
38
+ - A bundle given as an object is copied into plain data before any rule runs on it, and every check then runs on that copy. Its fields were the caller's own to answer, and the transparency-log entry's canonicalizedBody was read separately by the three things that examine it: the binding that ties the entry to the signature and certificate, the inclusion proof folded to the signed tree root, and the signed entry timestamp. A field answering differently across those reads let one body be bound while a different one was proven, so a signature the log never recorded was attested by an authentic entry for something else and verifyBundle returned verified. The same shape reached the DSSE payload and signature, the inclusion proof's root and hashes, and the time the certificate is checked at. It affects both content arms and every version that shipped the DSSE one. A bundle given as JSON text was never affected, since parsing it yields fixed values.
39
+ - An object bundle is held to being data, which changes what is accepted. A field reached through an accessor is refused rather than called, since calling one runs code that can change the object while it is being read. So is a value JSON does not carry: a function, a symbol, a bigint or a non-finite number anywhere in the structure, because dropping one silently turned a bundle setting two content arms into a bundle setting one. A field set to undefined reads as absent, and one named `__proto__` stays a field of that name rather than becoming the copy's prototype. pki.sigstore.parseBundle takes the same copy and returns it, so what a caller reads back is what was checked and the two verbs cannot answer differently about one bundle.
40
+ - An object bundle is bounded by the same size and depth limits a text one is, and by the same numbers. The size is charged while the copy is built, so a bundle is refused before the memory is allocated rather than after: sharing one sub-object between two properties doubles the values a copy walks per level, so a few hundred bytes describes hundreds of millions of them, and a modest array of long strings is a handful of values holding tens of megabytes. Each value is charged what JSON writes it as, so the two representations of one document are judged alike: a string costs its escaped bytes, a number written in exponent form costs its digits, and the punctuation between values is counted once. A property whose value is not written costs nothing, since the document omits it. One nesting limit governs both routes.
41
+ - Nothing the copy relies on can be replaced after this package loads. The object path used to serialize the copy and read it back, so a serializer replaced afterwards could drop a field and hand the checks a bundle carrying one content arm where the copy held two; the copy is plain data already and is not converted to text at all. The operations the copy decides with are taken at load for the same reason, since replacing the one that lists what an object owns empties the bundle and replacing the one that counts a number's digits lets an oversized one through.
42
+ - The fields that name a registry row are read for being strings before they are used as keys. A messageDigest algorithm, a log entry's kind and apiVersion, and a log entry's hash algorithm reached a lookup as whatever the bundle carried, so a value whose own conversion throws escaped as an untyped error out of a verb whose contract is that malformed input is refused with a typed one.
43
+
44
+ ### Security
45
+
46
+ - pki.webauthn.verifyMetadataBlob reads opts.previousNo once, so the rollback comparison and the result it reports run on the same baseline. The option was read separately by the comparison and by the result, and a baseline that answered absent at the comparison and present afterwards skipped the comparison entirely: a metadata BLOB whose sequence number did not advance past the held baseline was accepted, and the result stated rollbackChecked true and named the baseline it had not compared against. A caller that holds its baseline in a plain value was never affected. Every option the verb reads is now taken once at entry.
47
+
7
48
  ## v0.6.55 — 2026-09-07
8
49
 
9
50
  A Diffie-Hellman key proves possession by agreeing a secret with the authority, without signing anything.
package/README.md CHANGED
@@ -64,7 +64,7 @@ Crypto API with its limits on streaming, opaque keys, and algorithm reach.
64
64
  npm i @blamejs/pki
65
65
  ```
66
66
 
67
- Requires Node.js 24.19+ and runs on the shipped runtime, with no build step and
67
+ Requires Node.js 24.21+ and runs on the shipped runtime, with no build step and
68
68
  no transpilation.
69
69
 
70
70
  ```js
@@ -249,7 +249,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
249
249
  | `pki.shbs` | Stateful hash-based signature verification: HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and in CMS by RFC 9708, profiled by NIST SP 800-208 for CNSA 2.0 firmware signing. `verify` checks an HSS signature, where every level must pass, and `verifyLms` a single-tree LMS, over the raw public-key and signature blobs the parsers already surface. Pure public-input SHA-256 and SHAKE256 hashing, a data-driven typecode registry, and bounds-before-slice reads; a malformed blob throws a typed `ShbsError` while a well-formed but wrong signature returns `false`. Verification only by design, since stateful signing needs atomic one-time-key state that belongs in an HSM. `pki.path.validate` verifies an HSS-signed certificate on its own, so a chain reaching one needs no separate call: the signature covers the whole tbsCertificate rather than a digest of it (RFC 9802 §7.1), and a CertificateList naming the algorithm takes the same route. It is proven against the self-signed certificate RFC 9802 Appendix A publishes — `verify`, `verifyLms` |
250
250
  | `pki.hpke` | Hybrid Public Key Encryption (RFC 9180), the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS` and `setupR` establish a sender or recipient context (KEM encapsulation plus the HKDF key schedule); the context's `seal` and `open` AEAD-encrypt with a sequence-counter nonce, and `export` derives further secrets; the module-level `seal` and `open` are single-shot wrappers. DHKEM (P-256, P-521, X25519, X448) by HKDF-SHA256/SHA512 by AES-GCM / ChaCha20Poly1305 / export-only, across all four modes, proven against the RFC 9180 Appendix A vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered but Appendix A ships no vector for them, so they fail closed until an authoritative KAT exists. Pure composition over `node:crypto`; ML-KEM and X-Wing are a registry data-row extension pending stable drafts — `suites`, `setupS`, `setupR`, `seal`, `open` |
251
251
  | `pki.kem` | Composite ML-KEM key establishment (draft-ietf-lamps-pq-composite-kem), a post-quantum ML-KEM hybridized with a traditional RSA-OAEP, ECDH, X25519, or X448 so the established secret holds if either component is later broken. `encapsulate` turns a recipient's composite `SubjectPublicKeyInfo` into a 256-bit shared secret and a ciphertext; `decapsulate` recovers the same secret from the ciphertext and the composite PKCS#8 private key. Each component KEM runs independently and the two secrets are mixed through a SHA3-256 combiner that binds the traditional ciphertext, the traditional public key, and a per-algorithm label. The twelve algorithms pair ML-KEM-768 and ML-KEM-1024 with RSA-OAEP 2048/3072/4096, ECDH over P-256/P-384/P-521 and brainpoolP256r1/P384r1, X25519, and X448, each verified against the draft Appendix G known-answer vectors; a malformed key or ciphertext, an unsupported algorithm, or a component decapsulation failure throws a typed `KemError` — `encapsulate`, `decapsulate` |
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 — `pae`, `parseBundle`, `verifyBundle` |
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
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 plus 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, SAN required and CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys, 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`, `rules`, `profiles` |
@@ -87,6 +87,15 @@ function assertKnownKeys(obj, known, E, code, message) {
87
87
  });
88
88
  }
89
89
 
90
+ // @enforced-by behavioral -- reading a property twice has no rename-proof code shape; the RED
91
+ // @enforced-by behavioral -- vectors that drive an accessor-backed option are the guard
92
+ function snapshotOptions(obj, known) {
93
+ var out = _create(null);
94
+ if (obj === null || obj === undefined) return out;
95
+ _forEach(_getOwnPropertyNames(known), function (k) { out[k] = obj[k]; });
96
+ return out;
97
+ }
98
+
90
99
  var _PRISTINE_OBJECT_PROTO = (function () {
91
100
  var s = _create(null);
92
101
  _forEach(["constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString",
@@ -441,6 +450,7 @@ module.exports = intrinsic.freeze({
441
450
  isPlainRecord: isPlainRecord,
442
451
  optionsObject: optionsObject,
443
452
  refuseAccessorFields: refuseAccessorFields,
453
+ snapshotOptions: snapshotOptions,
444
454
  readableNames: readableNames,
445
455
  readableIndices: readableIndices
446
456
  });
@@ -52,6 +52,8 @@ var cmpSession = require("./cmp-session");
52
52
  var guard = require("./guard-all");
53
53
  var intrinsic = require("./guard-intrinsic");
54
54
  var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
55
+ var _strSlice = intrinsic.uncurry(String.prototype.slice);
56
+ var _strIndexOf = intrinsic.uncurry(String.prototype.indexOf);
55
57
  var _resolve = intrinsic.uncurry(intrinsic.promiseResolve);
56
58
  var constants = require("./constants");
57
59
  var validator = require("./validator-all");
@@ -420,11 +422,28 @@ function emailMatch(constraint, mailbox) {
420
422
 
421
423
  var stripTrailingDot = guard.name.stripTrailingDot;
422
424
 
423
- function hostConstraintMatch(constraint, host) {
425
+ /** @internal `reachIntoBase` asks the excluded question rather than the permitted one. A wildcard
426
+ * dNSName stands for every name one label below its parent, so it is a set rather than a name. A
427
+ * permitted subtree asks whether that whole set stays inside the base, which the literal comparison
428
+ * already answers: `*.foo.com` ends with `.foo.com`, so a base of `foo.com` covers all of it. An
429
+ * excluded subtree asks the other question, whether any member of the set falls inside the base, and
430
+ * the literal comparison answers it in the fail-open direction: `*.example.com` is not the string
431
+ * `bar.example.com`, so an exclusion of `bar.example.com` admitted a certificate that presents
432
+ * exactly the name the exclusion forbids. Widening this for the permitted direction would be the
433
+ * fail-open mirror, so it is asked for only where the caller wants reach. A wildcard reaches exactly
434
+ * one label, so a base below the wildcard's parent is reached only when it is one label below it. */
435
+ function hostConstraintMatch(constraint, host, reachIntoBase) {
424
436
  var c = stripTrailingDot(constraint.toLowerCase()), h = stripTrailingDot(host.toLowerCase());
425
437
  if (c === "") return true;
426
438
  if (c.charAt(0) === ".") return h.length > c.length && h.slice(-c.length) === c;
427
- return h === c || (h.length > c.length && h.slice(-(c.length + 1)) === "." + c);
439
+ if (h === c || (h.length > c.length && h.slice(-(c.length + 1)) === "." + c)) return true;
440
+ if (reachIntoBase === true && h.length > 2 && _charCodeAt(h, 0) === 42 && _charCodeAt(h, 1) === 46) {
441
+ var parent = _strSlice(h, 2);
442
+ if (parent.length > 0 && c.length > parent.length && _strSlice(c, -(parent.length + 1)) === "." + parent) {
443
+ return _strIndexOf(_strSlice(c, 0, c.length - parent.length - 1), ".") === -1;
444
+ }
445
+ }
446
+ return false;
428
447
  }
429
448
 
430
449
  function _charTable(chars) { var t = []; for (var i = 0; i < chars.length; i++) t[_charCodeAt(chars, i)] = true; return t; }
@@ -491,14 +510,14 @@ function ipMatch(constraint, addr) {
491
510
  * certificate carries in its own nameConstraints extension met no door, and the same rule decides it
492
511
  * here. Answering a base outside its tag's form with no match rather than no verdict would make an
493
512
  * excluded subtree that covers nothing exclude nothing. */
494
- function nameMatchesConstraint(gnTag, gnValue, base) {
513
+ function nameMatchesConstraint(gnTag, gnValue, base, reachIntoBase) {
495
514
  if (base.tagNumber !== gnTag) return null;
496
515
  if (gnTag === 1 || gnTag === 2 || gnTag === 6) {
497
516
  if (guard.name.constraintBaseUnreadable(gnTag, base.value)) return "unsupported";
498
517
  }
499
518
  switch (gnTag) {
500
519
  case 1: return emailMatch(base.value, gnValue);
501
- case 2: return hostConstraintMatch(base.value, gnValue);
520
+ case 2: return hostConstraintMatch(base.value, gnValue, reachIntoBase);
502
521
  case 6: return uriMatch(base.value, gnValue);
503
522
  case 7: return ipMatch(base.value, gnValue);
504
523
  case 4: return dnStartsWith(gnValue, base.value);
@@ -540,7 +559,7 @@ function checkNameConstraints(state, cert) {
540
559
  for (var e = 0; e < state.excludedSubtrees.length; e++) {
541
560
  var ex = state.excludedSubtrees[e];
542
561
  for (var i = 0; i < forms.length; i++) {
543
- var m = nameMatchesConstraint(forms[i].tag, forms[i].value, { tagNumber: ex.tag, value: ex.base });
562
+ var m = nameMatchesConstraint(forms[i].tag, forms[i].value, { tagNumber: ex.tag, value: ex.base }, true);
544
563
  if (m === true) return { ok: false, code: "path/name-constraint-excluded" };
545
564
  if (m === "unsupported") return { ok: false, code: "path/name-constraint-unsupported" };
546
565
  }