@blamejs/pki 0.5.10 → 0.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,55 @@ 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.11 — 2026-08-19
8
+
9
+ An option a verb never reads is now refused, so a misspelled `password` on key export can no longer leave a private key unprotected.
10
+
11
+ ### Changed
12
+
13
+ - `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.
14
+ - 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.
15
+ - 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.
16
+ - `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.
17
+
18
+ ### Fixed
19
+
20
+ - `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.
21
+ - `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.
22
+ - 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.
23
+ - 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.
24
+ - 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.
25
+ - 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.
26
+ - 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.
27
+ - 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.
28
+ - 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.
29
+ - 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.
30
+ - `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.
31
+ - 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.
32
+ - 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)`.
33
+ - 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.
34
+ - `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.
35
+ - 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.
36
+ - 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.
37
+ - 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.
38
+ - 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.
39
+ - 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.
40
+ - 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.
41
+ - 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.
42
+ - 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.
43
+ - 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.
44
+ - 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.
45
+ - 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.
46
+ - 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.
47
+ - 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.
48
+ - 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.
49
+ - 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.
50
+ - 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.
51
+ - 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.
52
+ - 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.
53
+ - 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.
54
+ - 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.
55
+
7
56
  ## v0.5.10 — 2026-08-18
8
57
 
9
58
  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/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.length : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
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.getTime();
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 > notAfter.getTime()) throw E("acme/certificate-expired", "the certificate is already past its notAfter; there is nothing to renew (RFC 9773 sec. 4.3)");
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 = notAfter.getTime();
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(d.getTime())) throw new Asn1Error("asn1/bad-time", "unparseable time " + JSON.stringify(s));
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.
@@ -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 (notBefore.getTime() > notAfter.getTime()) throw _err("attrcert/bad-input", "notBeforeTime must not be after notAfterTime (RFC 5755 sec. 4.2.6)");
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(d.getTime())) throw new CborError("cbor/bad-time", "epoch time out of range");
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 instanceof Date) {
734
- ms = v.getTime();
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 instanceof Date ? BigInt(Math.floor(ms / 1000)) : (typeof v === "bigint" ? v : BigInt(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 (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, CmcError, "cmc/bad-input", "a byte field of the request");
420
- if (ArrayBuffer.isView(v)) return Buffer.from(new Uint8Array(v.buffer, v.byteOffset, v.byteLength));
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 instanceof Date) ? when : new Date();
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
- if (v instanceof Date) return new Date(v.getTime());
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 instanceof Date ? new Date(opts.time.getTime()) : 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.getTime() < spec.thisUpdate.getTime()) throw _err("crl/bad-input", "nextUpdate must not be before thisUpdate (RFC 5280 sec. 5.1.2.5)");
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 });
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 && nb.getTime() > na.getTime()) throw _err("crmf/bad-validity", "notBefore must not be after notAfter");
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 (start.getTime() >= end.getTime()) throw _ctErr("ct/bad-log-list", "a CT log temporal_interval start_inclusive must be strictly before end_exclusive");
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 a.startInclusive.getTime() === b.startInclusive.getTime() && a.endExclusive.getTime() === b.endExclusive.getTime();
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 && a.state.since.getTime() === b.state.since.getTime() && _sameTemporal(a.temporalInterval, b.temporalInterval);
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 instanceof Date) return 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.getTime())) throw _ctErr("ct/log-untrusted", "the CT log is retired and the SCT is not timestamped before its retirement (" + log.state.since.toISOString() + ")");
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
- if (!Buffer.isBuffer(channelBinding) || channelBinding.length === 0) throw E("est/bad-input", "challengePasswordFromTlsUnique requires the channel-binding bytes");
703
- var b64 = channelBinding.toString("base64");
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.length : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
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.length : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
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 (Buffer.isBuffer(v) || v instanceof Uint8Array) return guard.bytes.snapshot(v, EstError, "est/bad-input", "a byte field of the request");
1454
- if (ArrayBuffer.isView(v)) return Buffer.from(new Uint8Array(v.buffer, v.byteOffset, v.byteLength));
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.length : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
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.length : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
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.length : Buffer.byteLength(String(res.body == null ? "" : res.body), "utf8");
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));