@blamejs/pki 0.5.10 → 0.5.12
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 +62 -0
- package/README.md +1 -1
- package/lib/acme.js +4 -4
- package/lib/asn1-der.js +1 -1
- package/lib/attrcert-sign.js +1 -1
- package/lib/cbor-det.js +4 -4
- package/lib/cmc-verify.js +7 -4
- package/lib/cms-sign.js +3 -5
- package/lib/cms-verify.js +7 -4
- package/lib/crl-sign.js +77 -6
- package/lib/crmf-sign.js +1 -1
- package/lib/ct.js +8 -5
- package/lib/est.js +17 -11
- package/lib/guard-bytes.js +564 -78
- package/lib/guard-identifier.js +691 -3
- package/lib/guard-parsed.js +9 -5
- package/lib/guard-time.js +65 -4
- package/lib/inspect.js +8 -3
- package/lib/key.js +48 -6
- package/lib/lint.js +14 -7
- package/lib/ocsp-verify.js +10 -4
- package/lib/ocsp.js +34 -5
- package/lib/path-validate.js +94 -17
- package/lib/schema-c509.js +3 -3
- package/lib/sigstore.js +6 -2
- package/lib/trust.js +2 -2
- package/lib/tsp-sign.js +2 -2
- package/lib/validator-attcert.js +3 -3
- package/lib/webauthn-mds.js +5 -5
- package/lib/webauthn.js +1 -1
- package/lib/webcrypto.js +15 -6
- package/lib/x509-sign.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,68 @@ 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.5.12 — 2026-08-19
|
|
8
|
+
|
|
9
|
+
`pki.crl.isRevoked` can be asked at an instant, and a CRL that stopped speaking for that instant is refused rather than read as a clean bill of health.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `pki.crl.isRevoked(crl, serialNumber, { time })` asks the question at an instant. A CRL whose `nextUpdate` has passed, whose `thisUpdate` is later, or which carries no `nextUpdate` at all is refused with `crl/not-current` instead of answered from: outside the window a CRL states, an absent serial says nothing about the certificate, and a list stating no window cannot be told from a replayed copy. This joins the refusals already made for a delta, indirect or narrowed-scope CRL, on the same reasoning -- a serial means something only within the set, and the span, a CRL speaks for. `pki.path.crlChecker` is unchanged and remains the verb that decides currency against material it fetched itself.
|
|
14
|
+
- `opts.historicalMode` reads a revocation entry against that same instant, the way `pki.path.crlChecker` reads one. By default a listed serial is revoked whatever its `revocationDate` says, since a date in the future is post-dating or clock skew and must not read good; set `historicalMode` -- validating as of a past instant, a timestamped signature say -- and an entry dated after that instant has not yet applied. Given without `time` it names no instant to read against and is refused.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `pki.crl.isRevoked` takes a third argument. It is optional and every existing call keeps its behavior and its answer: without `time` the verb is the structural lookup it has always been, and its documentation now names the question that then goes unasked, so `null` reads as "not listed on this CRL" rather than "not revoked". An option it does not read is refused rather than ignored.
|
|
19
|
+
|
|
20
|
+
## v0.5.11 — 2026-08-18
|
|
21
|
+
|
|
22
|
+
An option a verb never reads is now refused, so a misspelled `password` on key export can no longer leave a private key unprotected.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `pki.key` (encrypt, decrypt, export, import, generate, publicFromPrivate), `pki.path.validate`, `pki.path.build`, `pki.lint.certificate` and `pki.ocsp` (buildRequest, sign, verify) throw `<domain>/bad-input` on an option they do not read, instead of ignoring it. A call passing an option that did nothing before will now fail; the message names the unknown key, so the fix is to correct or drop it.
|
|
27
|
+
- The same verbs require an options object whose options are values. One supplied through a getter is refused, naming the option, because a getter is asked afresh on every read and the value the check saw is not necessarily the one the verb uses. A caller computing an option can read it into a plain object at the call: `pki.key.export(key, { format: computeFormat() })`. Methods are unaffected, so a class instance carrying them is still an options bag.
|
|
28
|
+
- Where two verbs spell the same idea differently, the refusal says so. `pki.path.validate` takes `trustAnchor` and `pki.path.build` takes `trustAnchors`, and each names the other, because carrying the wrong spelling between them previously bought no anchoring and no error. `pki.key.encrypt` chooses `iterations` while `pki.key.decrypt` caps `maxIterations`; `pki.ocsp` spells the nonce three ways across buildRequest, sign and verify because it means three different things.
|
|
29
|
+
- `pki.path.build` accepts every `pki.path.validate` option, since it forwards them to each internal validation. That union is derived from validate's own list rather than restated, so the two cannot drift apart.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- `pki.sigstore.verifyBundle(bundle, { time })` validates the Fulcio chain at the instant the Date holds. `time` is accepted by its internal slot, so a Date subclass reaches the check; the instant was then read back through `getTime`, which a subclass answers. A caller-supplied Date reporting the log time made the ephemeral signing certificate, whose validity is about ten minutes wide, look current whenever the caller chose to check it. Every read of a Date across the toolkit now goes through the intrinsic, so what a comparison uses is the instant a Date holds rather than the one it reports.
|
|
34
|
+
- `pki.path.validate` and `pki.ocsp.verify` decide the validity window on the instant a Date holds. Both compared the caller's `time` against a certificate's, CRL's or response's Date as objects, and comparing two Dates coerces each one through `Symbol.toPrimitive` or `valueOf`, which a caller's subclass answers. `path.validate` checked `opts.time` for a valid instant at entry and then compared it through a door that check never used, so a Date holding one moment and reporting an earlier one made an expired certificate validate. Both now narrow every operand to its held instant before comparing.
|
|
35
|
+
- A field inherited from an object a caller built over the prototype every typed array shares is reported as the unknown option it is. The kinds were recognized from that shared parent, so any level minted the same way read as one the language installs members on, and a bag inheriting `BYTES_PER_ELEMENT` from it reached `pki.key.generate` unremarked. The concrete kinds are now found by asking the runtime for them and matched by identity, which keeps a kind added later covered the day it lands without admitting anything that merely sits above the same prototype.
|
|
36
|
+
- A key from another WebCrypto implementation reaches the verb even where that implementation keeps its internals under a symbol. Every verb reads its options by name, so nothing under a symbol key can be read as one, and refusing a handle for carrying one protected nothing while turning a documented input away. The options door is unchanged and still reports a symbol on a bag passed as options, which is the case that matters: a caller who wrote one there meant it as an option and no verb will read it. A name a verb could read is still refused on a key handle.
|
|
37
|
+
- A `node:crypto` KeyObject reaches the verb it was passed to. `pki.hpke` documents one as a key input, and its material lives behind an internal slot, so what a copy holds is the shape of a key and none of the key: the object the verb received could not export, sign or derive. It is now recognized from that slot and handed on as itself, which is how a WebCrypto CryptoKey was already treated. A value wearing `KeyObject.prototype` with no key behind it is still copied, and the raw `TypeError` its `type` getter raises is now the calling module's typed refusal with that fault as its cause.
|
|
38
|
+
- The tables naming what each uncopyable kind carries no longer answer for names they inherit. Built as ordinary objects, a table answered `toString` with the function every object in the language inherits, so `toString`, `constructor`, `valueOf`, `hasOwnProperty` and `__proto__` read as published surface on a `RegExp`, an `Error`, a `CryptoKey` and a thenable alike. A caller could write any of the five onto such an object and have it handed to a verb by reference. Membership is now a fact about the table.
|
|
39
|
+
- A field written with `Object.defineProperty` is seen by the check that decides whether a handle may be passed on by reference. A `RegExp`, an `Error`, a key handle and a thenable cannot be copied, so a verb receives the caller's own object and a field on it stays the caller's to change after every check has read it. That check passed over non-enumerable properties on the reasoning that a caller adds fields by assignment; `Object.defineProperty` belongs to a caller as much as to a platform, so a `RegExp` from another realm carrying a hidden `detached` reached `pki.cms.sign`, and flipping it after the call moved the content out of the message the call had already been asked to sign. What is asked now is whether the property can still move: a field that is writable, configurable, or backed by a getter is refused, and so is one whose value can move even where the binding cannot, since a frozen slot holding a `Date` is an instant anybody can still change. A field that is settled through to its value rides along, which is how an implementation writes the internal state a key handle carries.
|
|
40
|
+
- A `Date` copy answers every `Date` method the way a `Date` holding that instant answers. A method written onto the value itself shadows the language's, and the copy carried it: an own `getUTCFullYear` that throws came across onto the copy and threw out of `pki.cms.sign` while the signing time was being encoded. The names the language supplies are read off `Date.prototype`, and what decides is the value rather than the name: a function under one of those names is behavior and is left behind, while a plain value under the same name is a field and comes across, so an option misspelled as `getTime` still reaches the check that refuses an option no verb reads.
|
|
41
|
+
- An element supplied through a getter is refused where a field already was. The deep copy taken at a verb's entry reads each element once and stores the result, so a getter that answers differently on the next read left the check nothing to find. The refusal covered an options bag's named fields and passed over its indices, which is where it pays most: a list element could answer as a trusted signer to the check and as something else afterwards.
|
|
42
|
+
- A signer, anchor or policy list whose elements come from its prototype reaches the verb intact. An array resolves a hole through its prototype chain, so `Object.setPrototypeOf(list, {0: signer})` reads as a one-element list to any consumer while reporting no element of its own. The copy taken at the door enumerated only the array's own keys, so the verb received a hole where the caller passed a value: `pki.cms.sign` refused a list it had been given a signer for. Elements at or past `length` are still left out, since no length-bounded read reaches them.
|
|
43
|
+
- `pki.key.export(key, { password })` no longer writes an unprotected private key. Export serializes; it has never encrypted. The option was ignored, so the file on disk was a plaintext PKCS#8 while the call site named a password. It now throws `key/bad-input` naming `pki.key.encrypt(key, password)`, whose result is what to export.
|
|
44
|
+
- An option carried on a prototype, defined non-enumerably, supplied through a getter, or planted on `Object.prototype` is seen by the check that refuses unknown options. It read own enumerable names only. Each of those four shapes answered `opts.password` while showing the check nothing, and `pki.key.export` returned the private key in the clear, which is the case the refusal exists to prevent. A polluted `Object.prototype` is covered too, where `{}` carries no option of its own; the check reports the planted name and stays silent on the built-ins it was seeded with at load. One thing is still skipped: a method, meaning a data property holding a function that the prototype chain also supplies under that name. So `constructor` and the methods a class defines stay machinery, and an instance of a caller's own class remains a valid options bag whether the verb inspects it directly or after copying it.
|
|
45
|
+
- A `Symbol`-named option is reported rather than passed over. The check enumerated with `Object.getOwnPropertyNames`, which never returns a Symbol key, so one was accepted in silence. Such a name answers no `opts.password` and reaches no verb, but it is still an option supplied and never read, which is what the refusal is for. The message names it as `Symbol(name)`.
|
|
46
|
+
- A name planted on `Object.prototype` before this package loads is reported, whether it holds a value or a function. The built-ins were recognized by reading `Object.prototype` at load, which is the polluted runtime answering the question, so a name already present was taken for one and `pki.key.export` returned a plaintext private key. They are now the twelve members the language specifies, each still required to have the shape a real one has. The level they are looked for at is the last one an object's chain reaches, which is where the root sits in every realm, so a bag built in a `vm` context or a worker thread is read the same way as a local one. A verb called with no options is unaffected, since it is handed a bag with no prototype and inherits nothing.
|
|
47
|
+
- `pki.path.build` forwards the caller's own value for a validation option even on a runtime whose `Object.prototype` carries the same name. It chose what to forward by reading a build-only table with the option name, which answers for any inherited name, so a caller's own `softFail` was taken for a build-only option and dropped; the set it was dropped from then re-read the inherited value. A caller who wrote `softFail: false` could receive a valid path built on an undetermined revocation result they had refused to waive.
|
|
48
|
+
- A number, a string or a boolean passed where an options bag belongs is refused by the verb rather than raising a `TypeError` about a reflection method the caller never used.
|
|
49
|
+
- A value that inherits from `Date`, `Map`, `Set` or `ArrayBuffer` while holding none of one raises the verb's own `<domain>/bad-input` instead of a raw `TypeError`. These kinds were recognized by their prototype. `Object.create(Date.prototype)` carries that whole prototype and holds no instant, so it passed as a Date and the read behind it threw the language's error out of `pki.cms.sign`, `pki.cms.verify`, `pki.x509.sign`, `pki.path.validate` and the argument copy every signing verb makes. The kind is now read off the value itself. That answers the other direction too, since a `Date` or a `Map` built in a `vm` context or a worker thread holds its kind while inheriting from that context's prototypes, and the prototype test called those neither.
|
|
50
|
+
- A `Date`'s instant is read from the Date itself, so a class of the caller's can no longer answer with a different one. `getTime` is an ordinary method on `Date.prototype`, and `class T extends Date { getTime() { ... } }` answers it: one that threw left the time guard through the caller's own exception, and one answering afresh on each call made the instant validated and the instant compared two different reads of the same argument. `pki.cms.verify` copies a caller's validation time and trust-anchor dates the same way, so the policy checked and the policy applied hold one value.
|
|
51
|
+
- A byte view's bytes are read from the view itself, so a class of the caller's can no longer say where they are. `buffer`, `byteOffset` and `byteLength` are accessors on the shared typed-array prototype, and `class Opts extends Uint8Array { get buffer() { return elsewhere; } }` answers all three. One that lied substituted the memory: an array holding `AA AA AA AA` whose `buffer` returned another store had every byte door hand back that other store's bytes, so what a verb went on to hash, sign or parse was not what was in the array it was given. One that threw left those doors through the caller's own exception. `pki.webcrypto.getRandomValues` measured its 64 KiB ceiling the same way, and a subclass reporting `byteLength` of 0 passed the ceiling while the fill wrote the array's real length. It also takes a view over a `SharedArrayBuffer` again, which is a supported W3C argument the platform fills: the refusal of shared memory answers a question about input, where bytes another thread can rewrite after they have been checked cannot be checked at all, and a write target the caller chose is the other direction.
|
|
52
|
+
- A copied argument comes back as a plain one of its kind, so a verb reading it can no longer be sent into the caller's own code. The copy used to be given the original's prototype back, which left it dispatching there: `list.map(...)` inside a signing verb reached an override on an `Array` subclass, `getUTCFullYear()` reached one on a `Date` subclass, and an override that threw surfaced the caller's exception from inside a verb whose contract is a typed error. A prototype can also be rewritten after the call has begun, so a copy taken to stop the argument changing under the verb changed with it. An instance of a caller's own class copies to a plain object the same way, and a dictionary with no prototype keeps having none, since a copy that gained `Object.prototype` would inherit whatever a runtime has planted there. The names the copy reports are unchanged, which is what restoring the prototype had been for.
|
|
53
|
+
- Copying a caller's argument keeps the kind and the instant the value itself holds. The copy chose a byte view's kind through `v.constructor` and its element width through `v.BYTES_PER_ELEMENT`, and took a `Date`'s instant through `getTime`: all three are ordinary properties a subclass answers, so one that threw took the copy out through the caller's own exception, and one that lied produced a copy of a different kind, a different element count, or a different instant than the value being copied. Which kinds those are is taken from the runtime rather than listed, so a typed array of a kind the language adds is copied as itself from the day the runtime names it, and a view of a kind the runtime does not name is refused rather than built through whatever the value says its constructor is. Reading an array element is typed the same way reading a named field already was, so an accessor at an index no longer leaves the copy through its author's own exception while the identical getter under a name raises `<domain>/bad-input`. A `Map` or a `Set` is walked with the language's own `forEach` for the same reason, and the refusal for a value whose state cannot be copied names the kind from what the value is, so a `constructor` that throws no longer replaces the message that says why it was refused.
|
|
54
|
+
- A response-body size cap counts the bytes the body holds. `pki.est` and `pki.acme` take an injectable transport, so the response is a value their caller supplies, and `length` on a Buffer is shadowable by an own property: one reporting 0 carried a body of any size past the cap that exists to bound it (RFC 7030 sec. 6). `pki.est.challengePasswordFromTlsUnique` read its emptiness test the same way, and refused channel-binding bytes that were there.
|
|
55
|
+
- Bytes built in a `vm` context or a worker thread are taken as bytes. The doors deciding whether an argument is a byte container measured it against this realm's `Uint8Array` and `ArrayBuffer`, so a buffer from another context was turned away; at a door that also accepts a parsed structure it was then refused as a rebuilt one, which reads as a complaint about the certificate rather than about the container it arrived in. The four accepted forms are decided in one place now, from what the value holds.
|
|
56
|
+
- An options bag built over a built-in's prototype, or built in another realm, reads the same before and after a verb copies it. Two walks answered the question of which names a caller supplied, and they disagreed. The copy stopped only at this realm's `Object.prototype`, so a foreign bag had its `__proto__` accessor copied down as a supplied name; and away from a collection it read every level of the chain, so `Object.create(Array.prototype)` gained `length` and `Symbol.unscopables`. Either way a bag the check had just accepted was refused once it had been copied. One walk now answers for both.
|
|
57
|
+
- The options a verb reads are the options that were checked. Every one is read once as the verb receives them, and the names are taken again afterwards. Checking the names and reading them were two separate moments before, and a caller's getter sits between: `{ get format() { this.password = "pw"; return "der"; } }` showed only `format` while the check ran and grew `password` when the verb read `format`, so `pki.key.export` accepted it and returned a private key in the clear. An option supplied through a getter is therefore refused, and the message names it. A getter answers afresh every time it is read, so the value the check saw says nothing about the one the verb goes on to use, and a name checked once can be joined by another the next time it runs. That covers the getter on the object, the one a caller's class supplies, and the one that waits several reads before adding anything. A class that defines methods is untouched: a method is machinery, not an option.
|
|
58
|
+
- A name planted on `Array.prototype`, `Map.prototype` or another collection's prototype is reported. Those levels were passed over whole as the kind's own, so on a runtime carrying `Array.prototype.password` an empty array answered `opts.password` while the check saw nothing, and `pki.key.export` returned an unprotected private key for it. `Object.prototype` was already read for this reason. Every level is read the same way now, and what a kind puts on its prototype is passed over by name, and only where the member is still shaped the way the language leaves it, so a planted value borrowing one of those names is reported too. The names passed over are the ones that kind defines and none a neighboring kind defines: a weak collection counts nothing and has no `size`, a `SharedArrayBuffer` is `growable` where an `ArrayBuffer` is `resizable` and `detached`, and a `DataView` reads its bytes through methods, so it has no elements and no `length` and both are ordinary options on one. Naming the members rather than holding this realm's prototypes also settles a bag built in a `vm` context or a worker thread, whose prototypes are that realm's.
|
|
59
|
+
- An option carried on the prototype of an array, a byte view, a Map, a Set or an ArrayBuffer is seen. The check read only such a bag's own names, on the grounds that these kinds carry `buffer`, `byteLength`, `size` and the like on their prototypes and reporting those would refuse a shape the toolkit supports. `Object.setPrototypeOf([], { password: "pw" })` has none of that in its chain and still answers `opts.password`, and `pki.key.export` returned a plaintext private key for it. The chain is now read, with only the levels that describe a kind passed over, so a subclass's own additions are reported too. Copying such a bag keeps its class, so a verb handed the copy reads the same names off it that the check read off the original.
|
|
60
|
+
- A byte view whose class overrides `length` reports it as the option it is. `length` is structure only where the language puts it on the value itself: an array carries an own one that no prototype can shadow, while a byte view carries a prototype accessor, so `class Opts extends Uint8Array { get length() {} }` answers `opts.length` with what its author wrote. The name was passed over for every indexed kind, which hid that one.
|
|
61
|
+
- A property on an array whose name merely looks numeric is treated as the ordinary named property it is. `"-1"`, `"1.5"`, `"NaN"`, `"Infinity"` and `"4294967295"` are not array indices, and taking them for structure meant an unknown option under one of those names was accepted in silence and dropped from the copy the verb was given.
|
|
62
|
+
- An array argument costs what it holds rather than what it measures. Copying walked from zero to `length`, so an array with a high index or an assigned `length` did that many iterations for however few elements it carried.
|
|
63
|
+
- Copying a caller's argument no longer drops a name they added to it. A `Date` came back as its instant alone, and an own `constructor` field was skipped along with the inherited one a class defines, so in both cases the copy held nothing while the original still answered and the checks read the copy.
|
|
64
|
+
- A `Proxy` whose `getPrototypeOf` trap throws is refused with the verb's own `<domain>/bad-input` rather than the trap's raw error. Reading a prototype chain runs that trap, and the chain was read before the value was recognized as a `Proxy` at all, so the caller's own exception escaped a boundary whose contract is a typed error. Every question about the chain now comes after that decision, at all three doors: the copy path, the one that reports an object's option names, and the one a verb normalizes its options argument at, where the chain was walked by the test for a Buffer in the options position. The kind of an options argument is read off the value itself and never off its type tag, so a caller's `Symbol.toStringTag` is not consulted: a getter under it cannot run, and writing `Arguments` there names nothing. Whether the argument is a `Buffer` is settled by walking its prototype chain rather than by `Buffer.isBuffer`, which is an `instanceof` against an ordinary extensible function: a `Symbol.hasInstance` planted on `Buffer` decided that question, and could answer it by throwing.
|
|
65
|
+
- A `Proxy` is refused as an options bag, and as any argument these verbs copy. It answers `ownKeys` and `get` from two independent traps, so one reporting no keys while returning a value for `password` presents an object every enumeration calls empty and every read calls populated. `pki.key.export` accepted such a bag and returned a plaintext private key, which is the case the refusal exists to prevent, and a copy of one silently held nothing the caller had supplied. No enumeration can close that, since it reads only what a trap chooses to say, so the shape is refused by identity rather than probed for a contradiction.
|
|
66
|
+
- An argument whose prototype chain is a cycle no longer hangs the call or raises an untyped `RangeError`. A Proxy may report itself as its own prototype while its target stays extensible. Reading an object's option names, copying it, and any `instanceof` inside the copy all walk that chain, and none of them terminated. Reading the names now stops once it revisits an object, which loses nothing since a second lap finds no new name, and copying refuses the shape with the caller's own `<domain>/bad-input` code.
|
|
67
|
+
- At every verb named here, an options argument of `false`, `0`, `""` or `NaN` is refused rather than read as no options at all. Verbs outside that list still read a falsy options argument as though none were given, and are a later cut. The check that an options argument is an object ran after the argument had already been replaced by an empty object, and that replacement treated any falsy value as absent, so those four reached the body as `{}`. `pki.cms.sign`, `pki.cms.countersign` and `pki.cms.verify` carried this before this release; the verbs gaining option checks here would have inherited it. `null` and `undefined` still mean no options, and a Buffer in the options position is now named as the argument-order mistake it is. The boxed forms go the same way: `new Number(0)` and `new Boolean(false)` carry no option of their own, so they passed as a bag holding nothing, which is the same silence one wrapper away. A boxed primitive, an `Error`, a `Promise` and an `arguments` object are each refused by name, so the message says which argument was passed rather than naming a field of the engine's own.
|
|
68
|
+
|
|
7
69
|
## v0.5.10 — 2026-08-18
|
|
8
70
|
|
|
9
71
|
The documentation and the package's own source comments settle on one spelling of the words they use in both, and a gate keeps them there.
|
package/README.md
CHANGED
|
@@ -234,7 +234,7 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
|
|
|
234
234
|
| `pki.attrcert` | Attribute-certificate issuance (RFC 5755). `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` reference, a `fromCertificate` binding, or an object digest), the validity window as GeneralizedTime, an optional serial (positive, at most 20 octets, randomly generated when omitted), the `attributes` (role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo, or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, or pre-encoded Extension DER) each with its RFC 5755 criticality. An attribute certificate is never self-signed, so the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. Parsing stays at `pki.schema.attrcert.parse` — `sign` |
|
|
235
235
|
| `pki.crmf` | Certificate-request-message issuance (RFC 4211). `build(spec, key, opts)` assembles a `CertReqMessages` from a `spec` of `certReqId` (default 0, with the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested fields (`subject`, `publicKey` as the SPKI DER of the key being certified, `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken, authenticator, utf8Pairs, oldCertID, protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key: the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` and verified before the message is returned, exactly as a PKCS#10 CSR proves possession. A complete template signs the `CertRequest`; an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. `key` is optional for a `raVerified` proof. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. Parsing stays at `pki.schema.crmf.parse` — `build` |
|
|
236
236
|
| `pki.cmp` | CMP message building, transfer, and verification (RFC 9810). `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` and `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata; `message.body` is a single-key object naming the arm — request-side `ir`, `cr`, `kur`, `p10cr`, `certConf`, `pollReq`, `genm`, `rr`, and responder-side `ip`, `cp`, `kup`, `ccp`, `rp`, `genp`, `error`, `pollRep`, `krp`, `pkiconf`. Protection is exactly one of `opts.{ key, cert }`, a signature under the sender key with the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch, or `opts.mac`, a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived). Protection covers the exact DER of the virtual `ProtectedPart` and is self-verified before the message is returned, and `protectionAlg` is derived rather than caller-set, so the message the parser accepts is coherent by construction. `transfer(url, message, opts)` carries a built message to a CMP endpoint over `pki.transport` (RFC 9811): one POST of the DER PKIMessage, with the response classified fail-closed — 200 only for success, a non-200 2xx or an un-followed 3xx refused, a 4xx or 5xx carrying a CMP error PKIMessage forwarded as the integrity-protected verdict — and protection surfaced rather than verified. `wellKnownUrl(base, opts)` builds the §3.4 `/.well-known/cmp` request-URIs. `verify(message, opts)` checks the protection on an incoming message, either a signature through the same certification-path engine `pki.crl.verify` and `pki.ocsp.verify` use, with the EdDSA low-order-point and algorithm-confusion gates, or a PBMAC1 MAC recomputed from `opts.sharedSecret` and the message's own PBKDF2 parameters and constant-time compared, over the exact `ProtectedPart` reconstructed from the parser's raw slices. It is fail-closed on an unprotected message, a legacy or KEM MAC algorithm, an omitted keyLength, or a SHA-1 PRF, and returns a `{ valid, trusted, protectionType, signer, ... }` verdict. With `opts.trustAnchors` the signer certificate is fully path-validated (RFC 5280 §6.1 plus the RFC 9483 §3.2 `keyUsage.digitalSignature` gate) at a trusted current time, or an explicit `opts.time` for historical verification and never the message's self-asserted `messageTime`, before it is reported trusted; without one the verdict is crypto-only and the signer certificate is surfaced to anchor. `session(opts)` returns a stateful enrollment session whose `enroll(request)` drives a full `ir` / `cr` / `kur` / `p10cr` transaction over the shared transport, composing `build`, `transfer`, and `verify`. Every response is protection-verified, signer-trusted, and bound to the exchange (a stable `transactionID`, a fresh-`senderNonce` and echoed-`recipNonce` chain) before its body is read, with a bounded `pollReq` / `pollRep` loop for a `waiting` status and a `certConf` / `pkiConf` (or implicit) confirmation carrying an explicit `hashAlg` for a signature algorithm that does not convey its hash. It returns a terminal `{ outcome, certificate, chain, status, trusted, confirmed, implicitConfirm, transactionID, polls, transcript }`. The signature flavor requires `opts.trustAnchors` to authenticate the CA; a verified rejection or error and an exhausted poll budget are terminal verdicts, while a tampered, untrusted, or desynchronized response is a typed throw. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. Parsing stays at `pki.schema.cmp.parse` — `build`, `transfer`, `wellKnownUrl`, `verify`, `session` |
|
|
237
|
-
| `pki.crl` | CRL issuance and verification (RFC 5280 §5). `sign(spec, issuer, opts)` builds and signs a `CertificateList` from a `spec` of `thisUpdate` and `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` and `revocationDate` with an optional `reason` or `invalidityDate`), and an optional `extensions` object (authority key identifier, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access) or an array of pre-encoded Extension DER, with an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), the outer `signatureAlgorithm` matches `tbsCertList.signature`, an empty revocation list omits the field rather than emitting an empty SEQUENCE, `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime, the RFC fixes per-extension criticality, and the produced signature is verified under the issuer key before return. `verify(crl, issuer)` checks a CRL signature through the one path-validation signature engine, algorithm-confusion and EdDSA low-order gates included; handed a certificate rather than a bare key it also asks what only a certificate can answer — that the certificate is the issuer the CRL names, and that its `keyUsage`, when present, asserts `cRLSign` — so a CRL minted under an end-entity certificate of the same CA does not verify as that CA's. `isRevoked(crl, serialNumber)` looks a serial up, and first checks that the CRL is one a serial can be looked up in at all — it is handed a serial and nothing else, so a CRL that speaks for part of its issuer's certificates is refused rather than answered from. A delta CRL lists changes since a base, so an entry recording a release reads as a revocation when read alone; an indirect CRL carries other issuers' entries, whose serials are unrelated to yours; and any other `issuingDistributionPoint` narrows the CRL to one distribution point, one kind of certificate, or a subset of revocation reasons, none of which a serial can be matched against. `pki.path.crlChecker` is the verb for all of them: it is handed the certificate, merges a delta with its base,
|
|
237
|
+
| `pki.crl` | CRL issuance and verification (RFC 5280 §5). `sign(spec, issuer, opts)` builds and signs a `CertificateList` from a `spec` of `thisUpdate` and `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` and `revocationDate` with an optional `reason` or `invalidityDate`), and an optional `extensions` object (authority key identifier, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access) or an array of pre-encoded Extension DER, with an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), the outer `signatureAlgorithm` matches `tbsCertList.signature`, an empty revocation list omits the field rather than emitting an empty SEQUENCE, `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime, the RFC fixes per-extension criticality, and the produced signature is verified under the issuer key before return. `verify(crl, issuer)` checks a CRL signature through the one path-validation signature engine, algorithm-confusion and EdDSA low-order gates included; handed a certificate rather than a bare key it also asks what only a certificate can answer — that the certificate is the issuer the CRL names, and that its `keyUsage`, when present, asserts `cRLSign` — so a CRL minted under an end-entity certificate of the same CA does not verify as that CA's. `isRevoked(crl, serialNumber, opts)` looks a serial up, and first checks that the CRL is one a serial can be looked up in at all — it is handed a serial and nothing else, so a CRL that speaks for part of its issuer's certificates is refused rather than answered from. A delta CRL lists changes since a base, so an entry recording a release reads as a revocation when read alone; an indirect CRL carries other issuers' entries, whose serials are unrelated to yours; and any other `issuingDistributionPoint` narrows the CRL to one distribution point, one kind of certificate, or a subset of revocation reasons, none of which a serial can be matched against. A CRL speaks for a span as well as a set, so `opts.time` states the instant the question is asked at and a list that has stopped speaking for it — `nextUpdate` passed, `thisUpdate` later, or no `nextUpdate` stated at all — is refused rather than read as a clean bill of health; without `opts.time` the lookup is structural and an absent serial means only that it is not on that list. `pki.path.crlChecker` is the verb for all of them: it is handed the certificate, merges a delta with its base, performs the §6.3.3 correspondence, and decides currency against the material it fetched. Returns DER, or a PEM `X509 CRL` with `opts.pem`; malformed input throws a typed `CrlError`. Parsing stays at `pki.schema.crl.parse` — `sign`, `verify`, `isRevoked` |
|
|
238
238
|
| `pki.key` | Key-material lifecycle (RFC 5958 / RFC 8018). `encrypt(privateKey, password, opts)` wraps a PKCS#8 private key (DER, PEM, or an extractable `CryptoKey`) into an `EncryptedPrivateKeyInfo` under PBES2 (PBKDF2 with AES-CBC-Pad), where `opts` selects the `cipher` (`aes-256-cbc` default, `aes-192-cbc`, `aes-128-cbc`), the `prf` (`hmacWithSHA256` default, SHA-384/512, SHA-1), the `iterations` (default 600000), and the `salt`. The plaintext is validated as PKCS#8 before encryption, a default `prf` and `keyLength` are omitted so the parameters are byte-exact with OpenSSL, and the output is re-parsed before return. `decrypt(encrypted, password, opts)` recovers the inner `PrivateKeyInfo`, re-validated through `pki.schema.pkcs8.parse`: only PBES2 / PBKDF2 / AES-CBC is accepted (PBES1, PBMAC1, and scrypt are refused), the salt and iteration count are bounded before any derivation (`opts.maxIterations` lowers the cap), and a malformed parameter set or wrong-length IV is a distinct typed error. Because a MAC-less PBES2-CBC decrypt must not become a padding oracle (RFC 8018 §8), a wrong password and a valid-pad-but-not-a-key both surface the one uniform `key/decrypt-failed`. `export(key, opts)` and `import(input, opts)` move a private key as PKCS#8 or a public key as SubjectPublicKeyInfo. The key may come from the platform's WebCrypto or from a separately installed copy of this toolkit, and is exported through whichever holds its material; a non-extractable key, or one whose implementation keeps its material out of reach, is refused with that as the reason. Encoding is delegated to WebCrypto, so RSA carries an explicit NULL, EC a named curve, and Ed25519/Ed448/X25519/X448 omit parameters, and an ambiguous RSA or EC import requires `opts.algorithm`. `generate(algorithm, opts)` produces a key pair over RSA, ECDSA/ECDH, the Edwards and Montgomery curves, and the FIPS post-quantum ML-DSA and ML-KEM; `publicFromPrivate(privateKey)` derives the public key. Returns DER or PEM, with a typed `KeyError` on failure. Parsing stays at `pki.schema.pkcs8.parse` — `encrypt`, `decrypt`, `export`, `import`, `generate`, `publicFromPrivate` |
|
|
239
239
|
| `pki.pkcs12` | PKCS#12 (.p12/.pfx) issuance and reading (RFC 7292 / RFC 9579). `build(spec, opts)` assembles a store from the OpenSSL-style `{ key, cert, ca?, friendlyName?, localKeyId? }` or the full `{ safeContents: [...] }`, where each element is a plaintext or PBES2-encrypted `SafeContents` of key, shroudedKey, cert, crl, secret, or nested `safeContents` bags. Keys and certs are validated before wrapping, and `friendlyName` (BMPString) and `localKeyId` are single-value. Integrity is a classic Appendix B HMAC (the default, for maximum interoperability) or an RFC 9579 PBMAC1 (`opts.mac.algorithm`) over SHA-256/384/512, with shrouded keys and cert safes encrypted under RFC 8018 PBES2 (AES-128/192/256-CBC). Every password is encoded the PKCS#12 way — BMPString+NULL for the classic MAC, UTF-8 for the PBES2 bags and PBMAC1 — which is what OpenSSL and NSS consume, so a file it emits opens in both, cross-checked bidirectionally. The MAC covers the exact AuthenticatedSafe byte range, a DEFAULT-1 `MacData.iterations` is rejected up front, and the store is re-parsed before return. `verifyMac(pfx, password, opts)` recomputes a classic or PBMAC1 MAC over `macedBytes` and constant-time-compares it, throwing on a MAC-less or public-key-integrity store. Public-key integrity (`opts.integrity.mode: "public-key"`) wraps the AuthenticatedSafe in a CMS SignedData instead of a MAC, signed by any `pki.cms.sign` signer and carrying no MacData (§4); privacy stays independent, so `password` still PBES2-encrypts the bags. Public-key privacy wraps a SafeContents as a CMS EnvelopedData (AES-CBC, `id-envelopedData`, never GCM) encrypted to recipient public keys through the `pki.cms.encrypt` recipient model, via per-safe `recipients` or the `opts.recipientCerts` convenience, restricted to certificate recipients (RSA-OAEP, ECDH, X25519, X448, ML-KEM) since a password or KEK recipient could not be reopened by `open`. All four integrity-by-privacy combinations are permitted (§3.1). `open(pfx, password, opts)` reads a store back: it verifies the MAC first, so a wrong password is the MAC verdict rather than a decrypt error, then PBES2-decrypts every privacy safe and shrouded key bag and returns `{ integrityMode, macVerified, signers, keys, certs, crls, secrets }` — keys as re-validated PKCS#8 DER, certs, CRLs and secrets as raw DER, all with `friendlyName` and `localKeyId`, nested safes recursively. A MAC-less store is refused unless `opts.allowUnauthenticated`. A public-key-integrity store is verified through its CMS SignedData signature first (`pkcs12/signature-invalid` on failure), with the signer surfaced in `signers` but never trust-chained, which remains the caller's `pki.path.validate` step. A legacy-PBE store's Appendix C 3DES and RC2 bags are decrypted, RC2 through an in-tree RFC 2268 cipher, so an `openssl pkcs12 -legacy` or NSS store opens; the legacy RC4 schemes are refused. An `id-envelopedData` safe is decrypted with `opts.recipientKey` after the integrity gate (`pkcs12/no-recipient-key` when absent), every recipient-side fault and every post-integrity decrypt failure collapsing to the uniform `pkcs12/decrypt-failed`, and `opts.keys: 'crypto'` imports each key to a `CryptoKey`. It reads what OpenSSL and NSS produce. Returns DER or a PEM `PKCS12`, with a typed `Pkcs12Error` on failure. Parsing stays at `pki.schema.pkcs12.parse` — `build`, `verifyMac`, `open` |
|
|
240
240
|
| `pki.cms` | CMS signing, verification, encryption, and compression (RFC 5652). `sign(content, signers, opts)` produces a SignedData (§5), attached or detached, with one or many signers over RSA, RSASSA-PSS, ECDSA, EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA pairing ML-DSA with a traditional RSA, ECDSA, or EdDSA key (accepted only when both components verify, draft-ietf-lamps-cms-composite-sigs). It builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. A signer may also be key-only — `{ key, spki, keyIdentifier }` with no certificate — which RFC 5272 §3.2 requires when a Full PKI Request's signer is the key of a certification request that request carries: the signer identifier takes the subjectKeyIdentifier form carrying the identifier the request declares, the signature scheme resolves from the request's own public key, and no certificate is embedded. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: with signed attributes present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), and otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate. `valid` and `trusted` are separate claims and neither implies the other: a SignedData carries its own certificates, so `valid` says the signature is sound under one of them and nothing about who signed, while `trusted` says every signer chained to a root named in `opts.trustAnchors`, validated through the same RFC 5280 path engine `pki.path.validate` uses. Without anchors there is nothing to chain to and `trusted` is `false`; anchors that cannot be read throw, rather than reading as untrusted. An unrecognized option is refused rather than ignored. `countersign(cms, signers, opts)` adds a countersignature (§11.4) — a `SignerInfo` over the countersigned SignerInfo's signature value, any signer algorithm, nestable, with the primary bytes preserved so it still verifies — attached as the id-countersignature unsigned attribute; `verify` returns each countersignature's verdict under `signers[i].countersignatures` and every unsigned attribute — including an RFC 3161 timestamp token, attachable via `sign`'s `unsignedAttributes` — under `signers[i].unsignedAttrs`, surfaced unauthenticated. `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData, with recipients auto-dispatched off the certificate key to key transport (RSAES-OAEP; v1.5 is never emitted), key agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key wrap, password (PBKDF2 with RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936), wrapping one fresh content key for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher, EFAIL, and password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. Every key-establishment secret the toolkit allocates is wiped once used, on the failing path as well as the succeeding one: the KEM shared secret and its derived key-encryption key, the raw ECDH / X25519 / X448 agreement secret, a password-derived key-encryption key, and the content-encryption key itself, cleared once the message is complete since all recipients share it. Caller-supplied key material is never written to (best-effort; NIST SP 800-227 §4.2, RFC 9629 §7). `authenticate(content, recipients, opts)` produces an `id-ct-authData` (§9): cleartext content plus an HMAC-SHA-256/384/512 MAC, authenticated but not encrypted, with the fresh MAC key wrapped for every recipient through the same RecipientInfo model. The MAC covers the authenticated attributes (content-type and message-digest) re-tagged to the EXPLICIT SET OF (§9.2), or the content octets directly; `decrypt` recovers the MAC key, recomputes the MAC and independently the message-digest (§9.3), and releases the content only after both pass, with every secret-dependent failure collapsing to the uniform `cms/decrypt-failed`. `compress(content, opts)` and `decompress(input, opts)` produce and consume a CompressedData (RFC 3274; ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large`. Compression is a size transform with no integrity or confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `countersign`, `encrypt`, `authenticate`, `decrypt`, `compress`, `decompress` |
|
package/lib/acme.js
CHANGED
|
@@ -1883,7 +1883,7 @@ function client(directoryUrl, opts) {
|
|
|
1883
1883
|
// Measure an injected string body as UTF-8 -- the exact bytes _jsonInput re-encodes it to before the
|
|
1884
1884
|
// decoder sees it -- so a non-ASCII body (e.g. multi-byte emoji, ~half the byte count under latin1)
|
|
1885
1885
|
// cannot slip past the cap and reach the JSON parser.
|
|
1886
|
-
var blen = Buffer.isBuffer(res.body) ? res.body
|
|
1886
|
+
var blen = Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
|
|
1887
1887
|
if (blen > budgets.maxResponseBytes) throw E("acme/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap");
|
|
1888
1888
|
if ((method === "GET" || method === "HEAD") && res.status >= 300 && res.status < 400) {
|
|
1889
1889
|
if (redirects >= budgets.maxRedirects) throw E("acme/too-many-redirects", "the redirect budget of " + budgets.maxRedirects + " was exceeded");
|
|
@@ -2216,7 +2216,7 @@ function client(directoryUrl, opts) {
|
|
|
2216
2216
|
function clk() { var t = _rawClk(); if (typeof t !== "number" || !isFinite(t)) throw E("acme/bad-input", "the renewalInfo clock returned a non-finite value"); return t; }
|
|
2217
2217
|
// RFC 9773 sec. 4.3: a client MUST NOT check a certificate's RenewalInfo after it has expired. Gate BEFORE the
|
|
2218
2218
|
// unauthenticated GET -- the same expiry test renewalWindow applies -- so the raw verb cannot query a dead cert.
|
|
2219
|
-
var notAfterMs = x509.parse(certDer).validity.notAfter
|
|
2219
|
+
var notAfterMs = guard.time.instantOf(x509.parse(certDer).validity.notAfter);
|
|
2220
2220
|
// allow:nan-date-comparison-unguarded -- notAfter is a codec-parsed cert date (asn1 readTime rejects a NaN instant).
|
|
2221
2221
|
if (clk() > notAfterMs) throw E("acme/certificate-expired", "the certificate is already past its notAfter; a client MUST NOT check RenewalInfo after it has expired (RFC 9773 sec. 4.3)");
|
|
2222
2222
|
var certId = ariCertId(certDer);
|
|
@@ -2282,7 +2282,7 @@ function client(directoryUrl, opts) {
|
|
|
2282
2282
|
// path-validate's `t > notAfter` expiry test -- only a cert strictly past notAfter has nothing to renew.
|
|
2283
2283
|
// allow:nan-date-comparison-unguarded -- notAfter is a codec-parsed cert date (asn1 readTime rejects a NaN
|
|
2284
2284
|
// instant) and the window start/end below are rfc3339-validated by validateRenewalInfo; neither is NaN.
|
|
2285
|
-
if (now >
|
|
2285
|
+
if (now > guard.time.instantOf(notAfter)) throw E("acme/certificate-expired", "the certificate is already past its notAfter; there is nothing to renew (RFC 9773 sec. 4.3)");
|
|
2286
2286
|
if (o.replaced === true) throw E("acme/certificate-replaced", "the caller asserts this certificate has already been replaced (RFC 9773 sec. 4.3)");
|
|
2287
2287
|
return _renewalInfo(certDer, clk, RENEWAL_RETRY_MAX_SECONDS).then(function (ri) {
|
|
2288
2288
|
var w = ri.renewalInfo.suggestedWindow;
|
|
@@ -2293,7 +2293,7 @@ function client(directoryUrl, opts) {
|
|
|
2293
2293
|
// must renew BEFORE the cert dies), so a CA suggestedWindow that extends past notAfter is clamped to it
|
|
2294
2294
|
// -- selection stays uniform within the still-valid sub-window. If the whole window is past notAfter,
|
|
2295
2295
|
// effStart collapses to effEnd (renew at the last valid instant).
|
|
2296
|
-
var notAfterMs =
|
|
2296
|
+
var notAfterMs = guard.time.instantOf(notAfter);
|
|
2297
2297
|
var effEnd = Math.min(endMs, notAfterMs), effStart = Math.min(startMs, effEnd);
|
|
2298
2298
|
// RFC 9773 sec. 4.2: if the caller passed a prior result (opts.previous) whose suggestedWindow is UNCHANGED,
|
|
2299
2299
|
// REUSE its selectedTime (clamped to the still-valid, notAfter-bounded window) instead of drawing again, so
|
package/lib/asn1-der.js
CHANGED
|
@@ -758,7 +758,7 @@ function readTime(node, opts) {
|
|
|
758
758
|
d.setUTCFullYear(year, month - 1, day);
|
|
759
759
|
d.setUTCHours(hour, min, sec, ms);
|
|
760
760
|
// Coverage residual -- regex-validated fields are finite integers and Date rolls over (never NaN); out-of-range is caught by the component round-trip check below.
|
|
761
|
-
if (isNaN(
|
|
761
|
+
if (isNaN(guard.time.instantOf(d))) throw new Asn1Error("asn1/bad-time", "unparseable time " + JSON.stringify(s));
|
|
762
762
|
// Date/setUTC* silently roll over out-of-range fields (Feb 30 -> Mar 2,
|
|
763
763
|
// month 13 -> next Jan, hour 25 -> +1 day). Reject unless every component
|
|
764
764
|
// round-trips exactly.
|
package/lib/attrcert-sign.js
CHANGED
|
@@ -175,7 +175,7 @@ function _encodeValidity(notBefore, notAfter) {
|
|
|
175
175
|
guard.time.assertValid(notBefore, _err, "attrcert/bad-input", "notBeforeTime");
|
|
176
176
|
guard.time.assertValid(notAfter, _err, "attrcert/bad-input", "notAfterTime");
|
|
177
177
|
// allow:nan-date-comparison-unguarded -- both operands are guard.time.assertValid'd above.
|
|
178
|
-
if (
|
|
178
|
+
if (guard.time.instantOf(notBefore) > guard.time.instantOf(notAfter)) throw _err("attrcert/bad-input", "notBeforeTime must not be after notAfterTime (RFC 5755 sec. 4.2.6)");
|
|
179
179
|
return b.sequence([b.generalizedTime(notBefore), b.generalizedTime(notAfter)]);
|
|
180
180
|
}
|
|
181
181
|
|
package/lib/cbor-det.js
CHANGED
|
@@ -616,7 +616,7 @@ function readTime(node) {
|
|
|
616
616
|
var d = new Date(ns < 0 ? -constants.TIME.seconds(-ns) : constants.TIME.seconds(ns));
|
|
617
617
|
// Coverage residual -- the epoch-window bound above already rejects any value the
|
|
618
618
|
// Date constructor cannot represent, so this backstop is defense-in-depth.
|
|
619
|
-
if (isNaN(
|
|
619
|
+
if (isNaN(guard.time.instantOf(d))) throw new CborError("cbor/bad-time", "epoch time out of range");
|
|
620
620
|
return d;
|
|
621
621
|
}
|
|
622
622
|
|
|
@@ -730,14 +730,14 @@ var build = {
|
|
|
730
730
|
},
|
|
731
731
|
time: function (v) {
|
|
732
732
|
var ms;
|
|
733
|
-
if (v
|
|
734
|
-
ms =
|
|
733
|
+
if (guard.time.isDate(v)) {
|
|
734
|
+
ms = guard.time.instantOf(v);
|
|
735
735
|
if (Number.isNaN(ms)) throw new CborError("cbor/bad-time", "build.time: an Invalid Date has no epoch value");
|
|
736
736
|
// read.time reads only an integer (second-granularity) tag-1, so a sub-second Date cannot round-trip:
|
|
737
737
|
// reject it rather than silently truncate the milliseconds.
|
|
738
738
|
if (ms % 1000 !== 0) throw new CborError("cbor/bad-time", "build.time: a Date with sub-second precision cannot be a second-granularity CBOR epoch; round to whole seconds");
|
|
739
739
|
}
|
|
740
|
-
var secs = v
|
|
740
|
+
var secs = guard.time.isDate(v) ? BigInt(Math.floor(ms / 1000)) : (typeof v === "bigint" ? v : BigInt(v));
|
|
741
741
|
// Mirror read.time's bound so build.time never emits a tag-1 the strict reader would reject as out of range.
|
|
742
742
|
if (secs < -_MAX_EPOCH_SECONDS || secs > _MAX_EPOCH_SECONDS) throw new CborError("cbor/bad-time", "build.time: epoch seconds " + secs + " are outside the representable Date range");
|
|
743
743
|
return build.tag(1, build.int(secs));
|
package/lib/cmc-verify.js
CHANGED
|
@@ -415,11 +415,14 @@ function _snapshotIfBytes(input) {
|
|
|
415
415
|
// same door as a Buffer, and copying only Buffer and Uint8Array would leave those aliased across
|
|
416
416
|
// the very gap this closes -- reopening the window for exactly the inputs that came in by the
|
|
417
417
|
// wider one. A DataView is copied over its OWN window, not the whole buffer it happens to sit in.
|
|
418
|
+
// One call, because guard.bytes decides what a byte form is and where its bytes are. The three
|
|
419
|
+
// hand-rolled branches this replaces read `v.buffer`, `v.byteOffset` and `v.byteLength` off the
|
|
420
|
+
// value, and all three are accessors a caller's subclass can answer: one that lied copied a
|
|
421
|
+
// different store than the array held, so the bytes checked here and the bytes signed later were
|
|
422
|
+
// two different reads.
|
|
418
423
|
function _copyAnyBytes(v) {
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
if (v instanceof ArrayBuffer) return Buffer.from(new Uint8Array(v));
|
|
422
|
-
return v;
|
|
424
|
+
if (!guard.bytes.isByteSource(v)) return v;
|
|
425
|
+
return guard.bytes.snapshotSource(v, CmcError, "cmc/bad-input", "a byte field of the request");
|
|
423
426
|
}
|
|
424
427
|
|
|
425
428
|
function _snapshotSent(sent) {
|
package/lib/cms-sign.js
CHANGED
|
@@ -264,7 +264,7 @@ function _assertKeyMatchesSpki(keyOnly, soKey, soSpki, scheme, sig, signedBytes,
|
|
|
264
264
|
// A signing-time Time value: UTCTime before 2050, GeneralizedTime from 2050 (RFC 5652 sec. 11.3 /
|
|
265
265
|
// RFC 5280 sec. 4.1.2.5). A caller Date overrides; false omits the attribute (handled above).
|
|
266
266
|
function _timeValue(when) {
|
|
267
|
-
var d = (when
|
|
267
|
+
var d = guard.time.isDate(when) ? when : new Date();
|
|
268
268
|
return d.getUTCFullYear() < 2050 ? b.utcTime(d) : b.generalizedTime(d);
|
|
269
269
|
}
|
|
270
270
|
|
|
@@ -368,8 +368,7 @@ function sign(content, signers, opts) {
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
function _sign(content, signers, opts) {
|
|
371
|
-
opts = opts
|
|
372
|
-
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.sign options must be an object");
|
|
371
|
+
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.sign options");
|
|
373
372
|
guard.identifier.assertKnownKeys(opts, KNOWN_SIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
|
|
374
373
|
// The arguments were copied at entry (see `sign` above), which is what makes the refusal below
|
|
375
374
|
// hold: flipping signedAttributes from true to false after the call returns would otherwise skip
|
|
@@ -590,8 +589,7 @@ function countersign(cmsInput, signers, opts) {
|
|
|
590
589
|
}
|
|
591
590
|
|
|
592
591
|
function _countersign(cmsInput, signers, opts) {
|
|
593
|
-
opts = opts
|
|
594
|
-
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.countersign options must be an object");
|
|
592
|
+
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.countersign options");
|
|
595
593
|
guard.identifier.assertKnownKeys(opts, KNOWN_COUNTERSIGN_OPTS, _err, "cms/bad-input", "unknown opts field ");
|
|
596
594
|
var list = Array.isArray(signers) ? signers : [signers];
|
|
597
595
|
if (!list.length) throw _err("cms/bad-input", "pki.cms.countersign requires at least one countersigner");
|
package/lib/cms-verify.js
CHANGED
|
@@ -693,8 +693,7 @@ function verify(input, opts) {
|
|
|
693
693
|
}
|
|
694
694
|
|
|
695
695
|
function _verify(input, opts) {
|
|
696
|
-
opts = opts
|
|
697
|
-
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.verify options must be an object");
|
|
696
|
+
opts = guard.identifier.optionsObject(opts, _err, "cms/bad-input", "pki.cms.verify options");
|
|
698
697
|
// An unrecognized option is refused, not swallowed. This is what kept the missing trust seam
|
|
699
698
|
// silent: a caller writing `trustAnchors` before it existed -- or `trustAnchor` now -- got a
|
|
700
699
|
// verdict that looked anchored and was not. The guard rejects through a (code, message) FACTORY
|
|
@@ -807,7 +806,11 @@ function _cloneAnchorValue(v, depth) {
|
|
|
807
806
|
// field by field yields `{}` -- an anchor's `distrustAfter: { emailProtection: <Date> }` would
|
|
808
807
|
// survive the snapshot as an empty object and be rejected as an invalid date, disabling the very
|
|
809
808
|
// policy it encodes. Copied by value, like the validation instant.
|
|
810
|
-
|
|
809
|
+
// The instant comes through the guard's intrinsic read, so a caller's `getTime` override can
|
|
810
|
+
// neither throw out of the snapshot nor put a different instant in the copy than the original
|
|
811
|
+
// holds -- which would leave the anchor policy that was checked and the one that is applied two
|
|
812
|
+
// different values.
|
|
813
|
+
if (guard.time.isDate(v)) return new Date(guard.time.instantOf(v));
|
|
811
814
|
// A structure too deep to copy is REFUSED, not shared. Returning it by reference at the cap
|
|
812
815
|
// would leave part of the anchor caller-mutable across the chain walk while the rest was
|
|
813
816
|
// snapshotted -- a guarantee that holds for the shallow fields and quietly lapses for the deep
|
|
@@ -835,7 +838,7 @@ function _snapshotTrust(opts) {
|
|
|
835
838
|
}
|
|
836
839
|
return {
|
|
837
840
|
trustAnchors: anchors,
|
|
838
|
-
time: opts.time
|
|
841
|
+
time: guard.time.isDate(opts.time) ? new Date(guard.time.instantOf(opts.time)) : opts.time,
|
|
839
842
|
requiredEku: Array.isArray(opts.requiredEku) ? opts.requiredEku.slice() : opts.requiredEku,
|
|
840
843
|
checkPurpose: opts.checkPurpose,
|
|
841
844
|
};
|
package/lib/crl-sign.js
CHANGED
|
@@ -426,7 +426,7 @@ function _sign(spec, issuer, opts) {
|
|
|
426
426
|
nextU = _timeDer(spec.nextUpdate, "nextUpdate");
|
|
427
427
|
// allow:nan-date-comparison-unguarded -- both operands are guard.time.assertValid'd via _timeDer on the
|
|
428
428
|
// two lines above (thisUpdate + nextUpdate), so an Invalid Date throws before this comparison.
|
|
429
|
-
if (spec.nextUpdate
|
|
429
|
+
if (guard.time.instantOf(spec.nextUpdate) < guard.time.instantOf(spec.thisUpdate)) throw _err("crl/bad-input", "nextUpdate must not be before thisUpdate (RFC 5280 sec. 5.1.2.5)");
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
var extResult = _buildCrlExtensions(spec, { issuerCert: issuerCert, issuerSpki: issuerSpki });
|
|
@@ -629,7 +629,7 @@ function _serialHexOf(serial) {
|
|
|
629
629
|
|
|
630
630
|
/**
|
|
631
631
|
* @primitive pki.crl.isRevoked
|
|
632
|
-
* @signature pki.crl.isRevoked(crl, serialNumber) -> entry | null
|
|
632
|
+
* @signature pki.crl.isRevoked(crl, serialNumber, opts?) -> entry | null
|
|
633
633
|
* @since 0.3.9
|
|
634
634
|
* @status stable
|
|
635
635
|
* @spec RFC 5280 sec. 5.1.2.6
|
|
@@ -638,8 +638,22 @@ function _serialHexOf(serial) {
|
|
|
638
638
|
* Look a certificate serial number up in a CRL's `revokedCertificates` list. `crl` is a DER `Buffer`, a
|
|
639
639
|
* PEM string, or a parsed CRL; `serialNumber` is a `BigInt`, a safe integer, a decimal / `0x`-hex string,
|
|
640
640
|
* or a magnitude `Buffer`. Returns the matching revoked-certificate entry (`{ serialNumber, serialNumberHex,
|
|
641
|
-
* revocationDate, crlEntryExtensions }`) or `null` when the serial is not listed.
|
|
642
|
-
*
|
|
641
|
+
* revocationDate, crlEntryExtensions }`) or `null` when the serial is not listed. It does not verify the CRL
|
|
642
|
+
* signature; call `pki.crl.verify` or `pki.path.crlChecker` for that.
|
|
643
|
+
*
|
|
644
|
+
* With `opts.time` and `opts.historicalMode`, an entry is read against that instant the way
|
|
645
|
+
* `pki.path.crlChecker` reads it: by default a listed serial is revoked whatever its `revocationDate`
|
|
646
|
+
* says, since a date in the future is post-dating or clock skew and must not read good, and only an
|
|
647
|
+
* explicit historical reading has an entry dated after the instant not yet applying. `historicalMode`
|
|
648
|
+
* without `time` names no instant to read against and is refused.
|
|
649
|
+
*
|
|
650
|
+
* Pass `opts.time` to ask the question at an instant, and a CRL that does not speak for that instant is
|
|
651
|
+
* refused rather than answered from (`crl/not-current`): one whose `thisUpdate` is later, one whose
|
|
652
|
+
* `nextUpdate` has passed, and one carrying no `nextUpdate` at all, which states no window and so cannot
|
|
653
|
+
* be told from a replayed copy. Without `opts.time` currency goes unasked and the verb is the structural
|
|
654
|
+
* lookup it has always been -- `null` then means "not listed on this CRL", which is weaker than "not
|
|
655
|
+
* revoked". `pki.path.crlChecker` decides currency against the material it fetched and is the verb to
|
|
656
|
+
* reach for when the answer has to mean the stronger thing.
|
|
643
657
|
*
|
|
644
658
|
* It does check scope first, because a serial number means something only within the set of
|
|
645
659
|
* certificates a CRL speaks for, and this verb is given a serial and nothing else. So a CRL that
|
|
@@ -668,9 +682,56 @@ function _serialHexOf(serial) {
|
|
|
668
682
|
* { cert: signerCertDer, key: signerKeyPkcs8 });
|
|
669
683
|
* pki.crl.isRevoked(crlDer, 0x1234n) ? "revoked" : "not listed";
|
|
670
684
|
*/
|
|
671
|
-
|
|
685
|
+
var KNOWN_IS_REVOKED_KEYS = { time: 1, historicalMode: 1 };
|
|
686
|
+
function isRevoked(crl, serialNumber, opts) {
|
|
687
|
+
var o = guard.identifier.optionsObject(opts, _err, "crl/bad-input", "pki.crl.isRevoked options");
|
|
688
|
+
guard.identifier.assertKnownKeys(o, KNOWN_IS_REVOKED_KEYS, _err, "crl/bad-input", function (k) {
|
|
689
|
+
return "pki.crl.isRevoked has an unknown option " + JSON.stringify(k) +
|
|
690
|
+
"; it takes `time` and `historicalMode`";
|
|
691
|
+
});
|
|
692
|
+
// Both name the same instant, so one without the other asks nothing: historicalMode says how to
|
|
693
|
+
// read an entry against the time, and there is no time to read it against.
|
|
694
|
+
if (o.historicalMode !== undefined && o.time === undefined) {
|
|
695
|
+
throw _err("crl/bad-input", "pki.crl.isRevoked opts.historicalMode says how to read an entry " +
|
|
696
|
+
"against the instant asked about, so it needs opts.time as well");
|
|
697
|
+
}
|
|
698
|
+
if (o.historicalMode !== undefined && typeof o.historicalMode !== "boolean") {
|
|
699
|
+
throw _err("crl/bad-input", "pki.crl.isRevoked opts.historicalMode must be a boolean");
|
|
700
|
+
}
|
|
672
701
|
var parsed = _coerceCrl(crl);
|
|
673
702
|
var hex = _serialHexOf(serialNumber);
|
|
703
|
+
// CURRENCY, asked on the same footing as scope below and for the same reason: a CRL states the
|
|
704
|
+
// window it speaks for, and outside that window a serial being absent says nothing about the
|
|
705
|
+
// certificate -- the issuer promised a further list by `nextUpdate` and this one predates
|
|
706
|
+
// whatever it says. Read alone, `null` from an expired CRL reads as "not revoked", which is the
|
|
707
|
+
// composition this refusal exists to stop.
|
|
708
|
+
//
|
|
709
|
+
// Only when a time is given. This verb is handed a CRL and a serial, and the instant a question
|
|
710
|
+
// is asked at is the caller's to state throughout this toolkit rather than something read off
|
|
711
|
+
// the system clock here. Without one the verb stays the structural lookup it has always been,
|
|
712
|
+
// and its docstring says which question then went unasked.
|
|
713
|
+
if (o.time !== undefined) {
|
|
714
|
+
guard.time.assertValid(o.time, _err, "crl/bad-input", "pki.crl.isRevoked opts.time");
|
|
715
|
+
var at = guard.time.instantOf(o.time);
|
|
716
|
+
if (at < guard.time.instantOf(parsed.thisUpdate)) {
|
|
717
|
+
throw _err("crl/not-current", "this CRL takes effect at " + parsed.thisUpdate.toISOString() +
|
|
718
|
+
", after the time asked about, so it says nothing about the certificate then");
|
|
719
|
+
}
|
|
720
|
+
if (!parsed.nextUpdate) {
|
|
721
|
+
throw _err("crl/not-current", "this CRL carries no nextUpdate, so it states no window it " +
|
|
722
|
+
"speaks for and a replayed copy cannot be told from a current one (RFC 5280 sec. 5.1.2.5 " +
|
|
723
|
+
"requires one of a conforming CA); ask pki.path.crlChecker, which decides currency against " +
|
|
724
|
+
"the material it fetched");
|
|
725
|
+
}
|
|
726
|
+
// Strictly past, so a time equal to nextUpdate is still covered -- the same boundary
|
|
727
|
+
// pki.path.crlChecker and the OCSP currency check use, and RFC 5280 sec. 5.1.2.5 makes
|
|
728
|
+
// nextUpdate the date BY WHICH the next list arrives rather than the moment this one lapses.
|
|
729
|
+
if (at > guard.time.instantOf(parsed.nextUpdate)) {
|
|
730
|
+
throw _err("crl/not-current", "this CRL was superseded after " +
|
|
731
|
+
parsed.nextUpdate.toISOString() + "; a serial missing from it says nothing about the " +
|
|
732
|
+
"certificate at the time asked about, since the issuer promised a further list by then");
|
|
733
|
+
}
|
|
734
|
+
}
|
|
674
735
|
// SCOPE, before the serial is looked for at all. A serial number is only meaningful within the
|
|
675
736
|
// set of certificates a CRL actually speaks for, and two shapes of CRL speak for a different set
|
|
676
737
|
// than a caller matching on serial alone assumes. pki.path.crlChecker already refuses both; this
|
|
@@ -701,7 +762,17 @@ function isRevoked(crl, serialNumber) {
|
|
|
701
762
|
}
|
|
702
763
|
}
|
|
703
764
|
for (var j = 0; j < parsed.revokedCertificates.length; j++) {
|
|
704
|
-
if (parsed.revokedCertificates[j].serialNumberHex
|
|
765
|
+
if (parsed.revokedCertificates[j].serialNumberHex !== hex) continue;
|
|
766
|
+
// A revocation takes effect on its revocationDate (RFC 5280 sec. 5.3), and how that reads
|
|
767
|
+
// against the instant asked about is the same question pki.path.crlChecker answers -- so the
|
|
768
|
+
// same answer is given here. Asked at the present moment a listed serial is revoked whatever
|
|
769
|
+
// that date says, because a future one is post-dating or clock skew and must not read good.
|
|
770
|
+
// Only an explicit historical reading -- validating as of a past instant, a timestamped
|
|
771
|
+
// signature say -- has an entry dated after that instant not yet applying.
|
|
772
|
+
if (o.historicalMode === true &&
|
|
773
|
+
guard.time.instantOf(parsed.revokedCertificates[j].revocationDate) >
|
|
774
|
+
guard.time.instantOf(o.time)) continue;
|
|
775
|
+
return parsed.revokedCertificates[j];
|
|
705
776
|
}
|
|
706
777
|
return null;
|
|
707
778
|
}
|
package/lib/crmf-sign.js
CHANGED
|
@@ -74,7 +74,7 @@ function _encodeOptionalValidity(validity) {
|
|
|
74
74
|
if (nb != null) parts.push(b.explicit(0, _b.timeDer(nb, "validity notBefore")));
|
|
75
75
|
if (na != null) parts.push(b.explicit(1, _b.timeDer(na, "validity notAfter")));
|
|
76
76
|
// allow:nan-date-comparison-unguarded -- both instants passed timeDer's guard.time.assertValid above.
|
|
77
|
-
if (nb != null && na != null &&
|
|
77
|
+
if (nb != null && na != null && guard.time.instantOf(nb) > guard.time.instantOf(na)) throw _err("crmf/bad-validity", "notBefore must not be after notAfter");
|
|
78
78
|
return b.implicit(4, b.sequence(parts));
|
|
79
79
|
}
|
|
80
80
|
// CertTemplate ::= SEQUENCE { [0..9] all IMPLICIT OPTIONAL }. A request omits serialNumber [1] / signingAlg
|
package/lib/ct.js
CHANGED
|
@@ -666,7 +666,7 @@ function _parseTemporalInterval(ti) {
|
|
|
666
666
|
var start = rfc3339.parse(ti.start_inclusive, _ctErr, "ct/bad-date", "temporal_interval.start_inclusive");
|
|
667
667
|
var end = rfc3339.parse(ti.end_exclusive, _ctErr, "ct/bad-date", "temporal_interval.end_exclusive");
|
|
668
668
|
// allow:nan-date-comparison-unguarded -- start/end are rfc3339.parse results, guaranteed non-NaN (rfc3339.isValid rejects a NaN date).
|
|
669
|
-
if (
|
|
669
|
+
if (guard.time.instantOf(start) >= guard.time.instantOf(end)) throw _ctErr("ct/bad-log-list", "a CT log temporal_interval start_inclusive must be strictly before end_exclusive");
|
|
670
670
|
return { startInclusive: start, endExclusive: end };
|
|
671
671
|
}
|
|
672
672
|
|
|
@@ -700,10 +700,13 @@ function _sameTemporal(a, b) {
|
|
|
700
700
|
// inputs; the remaining c8 sub-arm is a short-circuit path (when `a == null` is true, `b == null` is
|
|
701
701
|
// never evaluated) with no additional reachable input.
|
|
702
702
|
if (a == null || b == null) return false;
|
|
703
|
-
return
|
|
703
|
+
return guard.time.instantOf(a.startInclusive) === guard.time.instantOf(b.startInclusive) &&
|
|
704
|
+
guard.time.instantOf(a.endExclusive) === guard.time.instantOf(b.endExclusive);
|
|
704
705
|
}
|
|
705
706
|
function _logsAgree(a, b) {
|
|
706
|
-
return a.state.name === b.state.name &&
|
|
707
|
+
return a.state.name === b.state.name &&
|
|
708
|
+
guard.time.instantOf(a.state.since) === guard.time.instantOf(b.state.since) &&
|
|
709
|
+
_sameTemporal(a.temporalInterval, b.temporalInterval);
|
|
707
710
|
}
|
|
708
711
|
|
|
709
712
|
/**
|
|
@@ -783,7 +786,7 @@ function parseLogList(json, opts) {
|
|
|
783
786
|
// The covered certificate's notAfter for the temporal gate: an explicit opts.certNotAfter, else derived
|
|
784
787
|
// from a leafCert (entryType 0) via the shipped x509 parser, else null (a precert TBS can't be parsed).
|
|
785
788
|
function _resolveNotAfter(entry, opts) {
|
|
786
|
-
if (opts.certNotAfter
|
|
789
|
+
if (guard.time.isDate(opts.certNotAfter)) return opts.certNotAfter;
|
|
787
790
|
if (entry && entry.entryType === 0 && entry.leafCert != null) {
|
|
788
791
|
// Inline require (circular-load): ct.js is loaded before schema-x509 finishes initializing in the
|
|
789
792
|
// index chain (schema-x509 -> schema-pkix is mid-init when ct.js is first required), so a top-level
|
|
@@ -845,7 +848,7 @@ async function verifySctWithLogList(entry, sct, logList, opts) {
|
|
|
845
848
|
if (!log.state.trusted) {
|
|
846
849
|
if (!log.state.conditional) throw _ctErr("ct/log-untrusted", "the CT log state '" + log.state.name + "' is not trusted");
|
|
847
850
|
var ts = guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp");
|
|
848
|
-
if (ts >= BigInt(log.state.since
|
|
851
|
+
if (ts >= BigInt(guard.time.instantOf(log.state.since))) throw _ctErr("ct/log-untrusted", "the CT log is retired and the SCT is not timestamped before its retirement (" + log.state.since.toISOString() + ")");
|
|
849
852
|
}
|
|
850
853
|
// Temporal gate (fail-closed): the covered cert's notAfter must be in [start_inclusive, end_exclusive).
|
|
851
854
|
if (log.temporalInterval) {
|
package/lib/est.js
CHANGED
|
@@ -699,8 +699,11 @@ function _attr(typeOid, valueNodes) { return asn1.build.sequence([asn1.build.oid
|
|
|
699
699
|
// attribute whose value is their RFC 4648 base64 (SIZE 1..255). The builder
|
|
700
700
|
// takes caller-supplied binding bytes and never fakes one.
|
|
701
701
|
function challengePasswordFromTlsUnique(channelBinding) {
|
|
702
|
-
|
|
703
|
-
|
|
702
|
+
// The count comes from the guard, so an own `length` on the caller's Buffer cannot report a
|
|
703
|
+
// full one as empty. `length` is shadowable on any instance, and `Object.defineProperty(buf,
|
|
704
|
+
// "length", { get: function () { return 0; } })` had this refuse bytes that were there.
|
|
705
|
+
if (!Buffer.isBuffer(channelBinding) || guard.bytes.lengthOf(channelBinding) === 0) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
|
|
706
|
+
var b64 = guard.bytes.view(channelBinding, EstError, "est/bad-input", "the channel-binding bytes").toString("base64");
|
|
704
707
|
if (b64.length > 255) throw E("est/tls-unique-too-long", "the base64 tls-unique value exceeds 255 octets (RFC 7030 sec. 3.5)");
|
|
705
708
|
return _attr(OID_CHALLENGE_PASSWORD, [asn1.build.printable(b64)]);
|
|
706
709
|
}
|
|
@@ -954,7 +957,7 @@ function _drive(method, url, body, headers, opts, transport, budgets) {
|
|
|
954
957
|
// Measure an injected string body as UTF-8 -- the byte width the body is decoded/transfer-decoded at,
|
|
955
958
|
// and what the real socket transport counts -- so a non-ASCII body (multi-byte chars are ~half the
|
|
956
959
|
// count under latin1) cannot undercount past the DoS cap and reach body handling.
|
|
957
|
-
var blen = Buffer.isBuffer(res.body) ? res.body
|
|
960
|
+
var blen = Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
|
|
958
961
|
if (blen > budgets.maxResponseBytes) throw E("est/response-too-large", "the response body (" + blen + " bytes) exceeds the " + budgets.maxResponseBytes + "-byte cap (RFC 7030 sec. 6)");
|
|
959
962
|
// The transport contract returns lowercased headers, but the injectable seam only promises
|
|
960
963
|
// { status, headers, body }; normalize here so a redirect Location / WWW-Authenticate from an
|
|
@@ -1104,7 +1107,7 @@ function _certsResult(op, res, opts, csrSpki) {
|
|
|
1104
1107
|
if (verdict.status !== "ok") throw E("est/http-error", "an EST " + op + " response must be HTTP 200 or 202 (RFC 7030 sec. 4.1.3 / 4.2.3), got " + res.status);
|
|
1105
1108
|
// Measure the body as UTF-8 for parity with the transport cap above (this check only distinguishes empty
|
|
1106
1109
|
// from non-empty, where the encoding is immaterial, but the shape is kept uniform so no site undercounts).
|
|
1107
|
-
var bodyLen = Buffer.isBuffer(res.body) ? res.body
|
|
1110
|
+
var bodyLen = Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
|
|
1108
1111
|
if (bodyLen === 0) throw E("est/empty-body", "a 200 " + op + " response carried an empty body (RFC 7030 sec. 4.1.3 / 4.2.3)");
|
|
1109
1112
|
var parsed = parseCertsOnly(transferDecode(res.body));
|
|
1110
1113
|
if (op === "cacerts") return { certificates: parsed.certificates, crls: parsed.crls };
|
|
@@ -1449,11 +1452,14 @@ function _cmcSent(opts, der) {
|
|
|
1449
1452
|
// Uint8Array would leave those forms aliased across the request's whole flight, which is the same
|
|
1450
1453
|
// window this exists to close, open for exactly the inputs that came in by the wider door.
|
|
1451
1454
|
// A DataView is copied over its OWN window, not the whole backing buffer it happens to sit in.
|
|
1455
|
+
// One call, because guard.bytes decides what a byte form is and where its bytes are. The three
|
|
1456
|
+
// hand-rolled branches this replaces read `v.buffer`, `v.byteOffset` and `v.byteLength` off the
|
|
1457
|
+
// value, and all three are accessors a caller's subclass can answer: one that lied copied a
|
|
1458
|
+
// different store than the array held, so the bytes checked here and the bytes sent later were
|
|
1459
|
+
// two different reads.
|
|
1452
1460
|
function _copyBytes(v) {
|
|
1453
|
-
if (
|
|
1454
|
-
|
|
1455
|
-
if (v instanceof ArrayBuffer) return Buffer.from(new Uint8Array(v));
|
|
1456
|
-
return v;
|
|
1461
|
+
if (!guard.bytes.isByteSource(v)) return v;
|
|
1462
|
+
return guard.bytes.snapshotSource(v, EstError, "est/bad-input", "a byte field of the request");
|
|
1457
1463
|
}
|
|
1458
1464
|
|
|
1459
1465
|
// The exchange-binding controls the request ACTUALLY carries (RFC 5272 sec. 6.6
|
|
@@ -1665,7 +1671,7 @@ function _fullcmcResult(res, opts, wanted, sent) {
|
|
|
1665
1671
|
"an EST /fullcmc response must be HTTP 200 or 202 (RFC 7030 sec. 4.3.2), got " + res.status);
|
|
1666
1672
|
}
|
|
1667
1673
|
|
|
1668
|
-
var bodyLen = Buffer.isBuffer(res.body) ? res.body
|
|
1674
|
+
var bodyLen = Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
|
|
1669
1675
|
if (bodyLen === 0) throw E("est/empty-body", "a 200 /fullcmc response carried an empty body (RFC 7030 sec. 4.3.2)");
|
|
1670
1676
|
var der = transferDecode(res.body);
|
|
1671
1677
|
|
|
@@ -2040,7 +2046,7 @@ async function _serverkeygenResult(res, opts, derived) {
|
|
|
2040
2046
|
// A 202 is an enrollment posture (RFC 7030 sec. 4.4 treats /serverkeygen as an enroll): surfaced, never slept.
|
|
2041
2047
|
if (verdict.status === "retry") return { retry: true, retryAfterSeconds: verdict.retryAfterSeconds, retryAfterDate: verdict.retryAfterDate };
|
|
2042
2048
|
if (verdict.status !== "ok") throw E("est/http-error", "an EST serverkeygen response must be HTTP 200 or 202 (RFC 7030 sec. 4.4.2), got " + res.status);
|
|
2043
|
-
var bodyLen = Buffer.isBuffer(res.body) ? res.body
|
|
2049
|
+
var bodyLen = Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
|
|
2044
2050
|
if (bodyLen === 0) throw E("est/empty-body", "a 200 serverkeygen response carried an empty body (RFC 7030 sec. 4.4.2)");
|
|
2045
2051
|
_assertConfidentialCipher(res);
|
|
2046
2052
|
// parseServerKeygenResponse does the per-part transfer-decode; the recipient coherence is bound to the CSR.
|
|
@@ -2142,7 +2148,7 @@ function _csrattrsResult(res, opts) {
|
|
|
2142
2148
|
// A 202 is nonconforming for a policy GET (sec. 4.5.2 lists only 200 / 204 / 404).
|
|
2143
2149
|
if (verdict.status === "retry") throw E("est/http-error", "a /csrattrs response must be HTTP 200, 204, or 404, not 202 (RFC 7030 sec. 4.5.2)");
|
|
2144
2150
|
if (verdict.status !== "ok") throw E("est/http-error", "an EST csrattrs response must be HTTP 200 / 204 / 404 (RFC 7030 sec. 4.5.2), got " + res.status);
|
|
2145
|
-
var bodyLen = Buffer.isBuffer(res.body) ? res.body
|
|
2151
|
+
var bodyLen = Buffer.isBuffer(res.body) ? guard.bytes.lengthOf(res.body) : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
|
|
2146
2152
|
// An empty HTTP body is distinct from a valid EMPTY CsrAttrs (`30 00` / base64 `MAA=`), which parses as {items:[]}.
|
|
2147
2153
|
if (bodyLen === 0) throw E("est/empty-body", "a 200 csrattrs response carried an empty body (RFC 7030 sec. 4.5.2)");
|
|
2148
2154
|
var attrs = csrattrsFmt.parse(transferDecode(res.body));
|