@blamejs/core 0.7.38 → 0.7.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/asn1-der.js +93 -11
- package/lib/network-tls.js +204 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.7.x
|
|
10
10
|
|
|
11
|
+
- **0.7.39** (2026-05-05) — RFC 8954 OCSP nonce extension default-on, plus persistent-log reliability fix in the smoke runner. **`b.network.tls.ocsp.buildRequest(opts)`** (NEW) — constructs a DER-encoded OCSPRequest for a single (`leafCertDer`, `issuerCertDer`) pair. The RFC 8954 nonce extension is **on by default** (security-defaults-on rule); operators talking to responders that ignore nonces opt out via `nonce: false`. Default nonce length is 16 bytes (RFC 8954 §2.1 floor 1, ceiling 32 — `nonceLen` overrides). CertID hashes are SHA-1 per RFC 6960 §4.1.1 (the universally-supported algorithm; SHA-256 in OCSP requests is §4.3 optional and many responders reject). Returns `{ requestDer, nonce }`. Operators send `requestDer` to the OCSP responder URL via `b.httpClient` (Content-Type: `application/ocsp-request`) and pass the returned `nonce` Buffer to `evaluate(responseDer, { issuerPem, expectedNonce })` — defends against replay attacks where an attacker captures a "good" OCSP response and replays it after the cert is revoked. **DER writer surface added to `lib/asn1-der.js`** — `writeNode` / `writeSequence` / `writeOctetString` / `writeInteger` / `writeNull` / `writeOid` / `writeContextExplicit` (minimal, ~80 lines, X.690 short + long-form length encoding). **`evaluate(der, opts)` accepts `expectedNonce`** — when supplied, the response's nonce extension MUST match (else `tls/ocsp-nonce-mismatch`). Result shape gains a `nonce` field: `"matched"` / `"present-not-checked"` / `"n/a"`. **Smoke-runner persistent log reliability fix** — the `.test-output/smoke.log` tee was using `fs.createWriteStream` (async); when the runner threw on a test failure the buffer didn't flush before `process.exit`, so the failure detail never reached the log. Replaced with synchronous `fs.writeSync` against an open fd — every byte hits disk before exit. Surface change for diagnosing future failures: the log now ALWAYS captures the failure stack instead of truncating mid-run. Smoke 8635 → 8642 / wiki e2e 178 / Linux container smoke 8642 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
|
12
|
+
|
|
11
13
|
- **0.7.38** (2026-05-05) — RFC 6698 + RFC 7672 DANE certificate-chain verification, plus smoke-runner LPT scheduling and a wiki failure-detail bugfix. **`b.network.smtp.dane.verifyChain(certChain, tlsaRecords, opts?)`** (NEW) — walks the peer cert chain (leaf-first DER buffers — typically `sock.getPeerCertificate(true).raw`) and confirms at least one TLSA record matches per the record's usage / selector / mtype. SMTP outbound (RFC 7672) only honors `DANE-TA` (2) and `DANE-EE` (3); `PKIX-TA` (0) and `PKIX-EE` (1) require a full PKIX path validator + CA-bundle and are refused unless the operator opts in via `allowPkixModes`. Selector `Cert` (0) compares the full DER; selector `SPKI` (1) compares the SubjectPublicKeyInfo bytes (extracted via the framework ASN.1 walker). Matching types: `Full`, sha-two-family at the short-digest length, sha-two-family at the long-digest length. Returns `{ ok, matches: [{ tlsaIndex, certIndex, usage, mtype }], errors }`. **ASN.1 walker** — `lib/asn1-der.js` `readNode` now surfaces a `.raw` field on each node (header + value bytes from the source buffer) so SPKI extraction can return the exact wire bytes for byte-identical TLSA matching. **Smoke-runner LPT scheduling** — `test/smoke.js` parallel mode now uses Longest-Processing-Time-first scheduling with a continuous worker queue. Per-test durations persist under `.test-output/smoke-timings.json` keyed by `process.platform` (so host win32/darwin and Linux container don't pollute each other's medians) with a 5-run history per test. Replaces the prior batched `Promise.all(slice(i, i + PARALLEL))` scheduling that left workers idle when one batch contained the long-tail test. **Wiki failure-detail bugfix** — `examples/wiki/test/e2e.js` was printing the per-example failure list AFTER the assert that throws on failure → operators only saw "1 of 178 failed" without WHICH example. Reordered so failure detail prints first. Smoke 8627 → 8635 / wiki e2e 178 / Linux container smoke 8635 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
|
12
14
|
|
|
13
15
|
- **0.7.37** (2026-05-05) — RFC 8460 TLS-RPT submission transport: `b.network.smtp.tlsRpt.fetchPolicy` + `submit`. The previous `recordShape` only generated the JSON; operators had to wire submission themselves. **`b.network.smtp.tlsRpt.fetchPolicy(domain, opts)`** (NEW) — reads the RFC 8460 §3 `_smtp._tls.<domain>` TXT record and returns `{ version, rua: [string,...] }` where `rua` is the comma-separated list of report URIs (`https://` and `mailto:`) the recipient publishes. Returns `null` when no record is published. `opts.dnsLookup` is operator-supplied so the call composes with `b.network.dns` (DoH / DoT) without a hard dep; falls back to `node:dns/promises.resolveTxt`. **`b.network.smtp.tlsRpt.submit(report, opts)`** (NEW) — submits a TLS-RPT report to the published rua endpoints. `https://` URIs receive an HTTPS `POST` with `Content-Type: application/tlsrpt+gzip` and a gzip-compressed JSON body per RFC 8460 §6.2. `mailto:` URIs return a prepared `{ to, subject, contentType, encoding, body }` object so operators hand it to `b.mail` with their configured relay (the framework doesn't bake an SMTP relay in — operators wire transport per their environment). Per-endpoint result records carry `{ uri, kind, ok, status, error }` so a failure on one rua doesn't cascade. Routes through `b.httpClient` so SSRF + DNS-pin + retry policy come for free. Smoke 8619 → 8627 / wiki e2e 178 / Linux container smoke 8627 / Linux container wiki e2e 178 / eslint clean / api-snapshot baseline refreshed.
|
package/lib/asn1-der.js
CHANGED
|
@@ -250,6 +250,81 @@ function unwrapExplicit(node, expectedTag) {
|
|
|
250
250
|
return readNode(node.value, 0);
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
// ---- DER writers (minimal — just the shapes the framework needs) ----
|
|
254
|
+
//
|
|
255
|
+
// Encoders for the small ASN.1 set the framework currently constructs:
|
|
256
|
+
// SEQUENCE / OCTET STRING / OID / INTEGER / NULL / context-specific
|
|
257
|
+
// [N] EXPLICIT. Fewer than 100 lines because we only emit the DER that
|
|
258
|
+
// crosses the wire (OCSP requests today; future TLSA encoding tomorrow).
|
|
259
|
+
// All length fields use the standard X.690 short / long-form encoding.
|
|
260
|
+
|
|
261
|
+
function _encodeLength(n) {
|
|
262
|
+
if (n < 128) return Buffer.from([n]); // allow:raw-byte-literal — X.690 short-form length boundary
|
|
263
|
+
// Long-form: first byte is 0x80 | numLengthOctets, then the length
|
|
264
|
+
// big-endian.
|
|
265
|
+
var bytes = [];
|
|
266
|
+
while (n > 0) {
|
|
267
|
+
bytes.unshift(n & 0xff); // allow:raw-byte-literal — base-256 length encoding mask
|
|
268
|
+
n = n >>> 8; // allow:raw-byte-literal — base-256 length encoding shift
|
|
269
|
+
}
|
|
270
|
+
return Buffer.concat([Buffer.from([0x80 | bytes.length]), Buffer.from(bytes)]);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function writeNode(tagByte, value) {
|
|
274
|
+
return Buffer.concat([Buffer.from([tagByte]), _encodeLength(value.length), value]);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function writeSequence(children) {
|
|
278
|
+
// children: Array<Buffer> of already-encoded child nodes.
|
|
279
|
+
return writeNode(TAG.SEQUENCE | 0x20, Buffer.concat(children)); // allow:raw-byte-literal — DER constructed bit
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function writeOctetString(value) {
|
|
283
|
+
return writeNode(TAG.OCTET_STRING, value);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function writeNull() {
|
|
287
|
+
return writeNode(TAG.NULL, Buffer.alloc(0));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function writeInteger(buf) {
|
|
291
|
+
// INTEGER values are big-endian. If high bit of leading byte is set
|
|
292
|
+
// and the value is positive (cert serials always are here), prepend
|
|
293
|
+
// 0x00 to disambiguate from a negative two's complement.
|
|
294
|
+
if (buf.length === 0) return writeNode(TAG.INTEGER, Buffer.from([0]));
|
|
295
|
+
if (buf[0] & 0x80) { // allow:raw-byte-literal — sign-bit disambiguation
|
|
296
|
+
return writeNode(TAG.INTEGER, Buffer.concat([Buffer.from([0]), buf]));
|
|
297
|
+
}
|
|
298
|
+
return writeNode(TAG.INTEGER, buf);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function writeOid(dotted) {
|
|
302
|
+
// Encode dotted-decimal OID per X.690 §8.19.
|
|
303
|
+
var parts = String(dotted).split(".").map(function (s) { return parseInt(s, 10); });
|
|
304
|
+
if (parts.length < 2) {
|
|
305
|
+
throw new Asn1Error("asn1/oid-too-short", "OID needs at least 2 arcs");
|
|
306
|
+
}
|
|
307
|
+
var bytes = [parts[0] * 40 + parts[1]]; // allow:raw-byte-literal — OID first-arc encoding
|
|
308
|
+
for (var i = 2; i < parts.length; i += 1) {
|
|
309
|
+
var arc = parts[i];
|
|
310
|
+
if (arc === 0) { bytes.push(0); continue; }
|
|
311
|
+
var stack = [];
|
|
312
|
+
while (arc > 0) {
|
|
313
|
+
stack.unshift(arc & 0x7f); // allow:raw-byte-literal — base-128 mask
|
|
314
|
+
arc = arc >>> 7; // allow:raw-byte-literal — base-128 shift
|
|
315
|
+
}
|
|
316
|
+
for (var j = 0; j < stack.length - 1; j += 1) stack[j] |= 0x80; // allow:raw-byte-literal — continuation bit
|
|
317
|
+
for (var k = 0; k < stack.length; k += 1) bytes.push(stack[k]);
|
|
318
|
+
}
|
|
319
|
+
return writeNode(TAG.OID, Buffer.from(bytes));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function writeContextExplicit(tagNumber, child) {
|
|
323
|
+
// [N] EXPLICIT — context-specific class (0xA0 | tag) + constructed.
|
|
324
|
+
var tagByte = 0xa0 | (tagNumber & 0x1f); // allow:raw-byte-literal — context-specific constructed mask
|
|
325
|
+
return writeNode(tagByte, child);
|
|
326
|
+
}
|
|
327
|
+
|
|
253
328
|
// Find a child node of a SEQUENCE / SET by predicate. Returns null if
|
|
254
329
|
// no child matches.
|
|
255
330
|
function findChild(children, predicate) {
|
|
@@ -260,15 +335,22 @@ function findChild(children, predicate) {
|
|
|
260
335
|
}
|
|
261
336
|
|
|
262
337
|
module.exports = {
|
|
263
|
-
TAG_CLASS:
|
|
264
|
-
TAG:
|
|
265
|
-
readNode:
|
|
266
|
-
readSequence:
|
|
267
|
-
readOid:
|
|
268
|
-
readOctetString:
|
|
269
|
-
readUnsignedInt:
|
|
270
|
-
readBitString:
|
|
271
|
-
unwrapExplicit:
|
|
272
|
-
findChild:
|
|
273
|
-
|
|
338
|
+
TAG_CLASS: TAG_CLASS,
|
|
339
|
+
TAG: TAG,
|
|
340
|
+
readNode: readNode,
|
|
341
|
+
readSequence: readSequence,
|
|
342
|
+
readOid: readOid,
|
|
343
|
+
readOctetString: readOctetString,
|
|
344
|
+
readUnsignedInt: readUnsignedInt,
|
|
345
|
+
readBitString: readBitString,
|
|
346
|
+
unwrapExplicit: unwrapExplicit,
|
|
347
|
+
findChild: findChild,
|
|
348
|
+
writeNode: writeNode,
|
|
349
|
+
writeSequence: writeSequence,
|
|
350
|
+
writeOctetString: writeOctetString,
|
|
351
|
+
writeInteger: writeInteger,
|
|
352
|
+
writeNull: writeNull,
|
|
353
|
+
writeOid: writeOid,
|
|
354
|
+
writeContextExplicit: writeContextExplicit,
|
|
355
|
+
Asn1Error: Asn1Error,
|
|
274
356
|
};
|
package/lib/network-tls.js
CHANGED
|
@@ -412,6 +412,9 @@ function _connectAndCheckOcsp(opts, requireStapled) {
|
|
|
412
412
|
// validation (operators relying on those wire their own parser).
|
|
413
413
|
|
|
414
414
|
var OID_BASIC_OCSP_RESPONSE = "1.3.6.1.5.5.7.48.1.1";
|
|
415
|
+
// OCSP nonce extension — id-pkix-ocsp-nonce.
|
|
416
|
+
var OID_OCSP_NONCE = "1.3.6.1.5.5.7.48.1.2";
|
|
417
|
+
var OID_SHA1 = "1.3.14.3.2.26"; // allow:raw-byte-literal — SHA-1 algorithm OID arc
|
|
415
418
|
var OID_RSA_SHA256 = "1.2.840.113549.1.1.11";
|
|
416
419
|
var OID_RSA_SHA384 = "1.2.840.113549.1.1.12";
|
|
417
420
|
var OID_RSA_SHA512 = "1.2.840.113549.1.1.13";
|
|
@@ -533,17 +536,53 @@ function parseOcspResponse(der) {
|
|
|
533
536
|
// ResponderID is itself a CHOICE (byName [1] / byKey [2]), then a
|
|
534
537
|
// GeneralizedTime, then the responses SEQUENCE-OF.
|
|
535
538
|
var responsesNode = null;
|
|
539
|
+
var responseExtensionsNode = null;
|
|
536
540
|
for (var rdi = rdChildren.length - 1; rdi >= 0; rdi -= 1) {
|
|
537
541
|
var ch = rdChildren[rdi];
|
|
538
542
|
if (ch.tag === asn1.TAG.SEQUENCE && ch.tagClass === asn1.TAG_CLASS.UNIVERSAL) {
|
|
539
543
|
responsesNode = ch;
|
|
540
544
|
break;
|
|
541
545
|
}
|
|
546
|
+
if (ch.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && ch.tag === 1) { // [1] EXPLICIT responseExtensions
|
|
547
|
+
responseExtensionsNode = asn1.readNode(ch.value, 0);
|
|
548
|
+
}
|
|
542
549
|
}
|
|
543
550
|
if (!responsesNode) {
|
|
544
551
|
throw new TlsTrustError("tls/ocsp-bad-shape",
|
|
545
552
|
"ResponseData missing responses SEQUENCE OF");
|
|
546
553
|
}
|
|
554
|
+
// Walk responseExtensions for the OCSP nonce (RFC 8954 / RFC 6960
|
|
555
|
+
// §4.4.1). Returns the raw nonce bytes when present, or null.
|
|
556
|
+
var responseNonce = null;
|
|
557
|
+
if (responseExtensionsNode && responseExtensionsNode.tag === asn1.TAG.SEQUENCE) {
|
|
558
|
+
var extKids = asn1.readSequence(responseExtensionsNode.value);
|
|
559
|
+
for (var ei = 0; ei < extKids.length; ei += 1) {
|
|
560
|
+
var ext = extKids[ei];
|
|
561
|
+
if (ext.tag !== asn1.TAG.SEQUENCE) continue;
|
|
562
|
+
var extChildren = asn1.readSequence(ext.value);
|
|
563
|
+
if (extChildren.length === 0) continue;
|
|
564
|
+
var extOid;
|
|
565
|
+
try { extOid = asn1.readOid(extChildren[0]); }
|
|
566
|
+
catch (_e3) { continue; }
|
|
567
|
+
if (extOid !== OID_OCSP_NONCE) continue;
|
|
568
|
+
var extnValue = asn1.readOctetString(extChildren[extChildren.length - 1]);
|
|
569
|
+
// RFC 8954 §2.1 — the nonce extension value is the raw bytes
|
|
570
|
+
// wrapped in an OCTET STRING (the value here). RFC 6960 §4.4.1
|
|
571
|
+
// historically wrapped the nonce in another OCTET STRING; tolerate
|
|
572
|
+
// both shapes.
|
|
573
|
+
try {
|
|
574
|
+
var inner = asn1.readNode(extnValue);
|
|
575
|
+
if (inner.tag === asn1.TAG.OCTET_STRING) {
|
|
576
|
+
responseNonce = inner.value;
|
|
577
|
+
} else {
|
|
578
|
+
responseNonce = extnValue;
|
|
579
|
+
}
|
|
580
|
+
} catch (_e4) {
|
|
581
|
+
responseNonce = extnValue;
|
|
582
|
+
}
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
547
586
|
var singleResponses = asn1.readSequence(responsesNode.value);
|
|
548
587
|
var responses = [];
|
|
549
588
|
for (var sri = 0; sri < singleResponses.length; sri += 1) {
|
|
@@ -586,6 +625,7 @@ function parseOcspResponse(der) {
|
|
|
586
625
|
signatureAlgorithmOid: sigAlgOid,
|
|
587
626
|
signature: signatureBytes,
|
|
588
627
|
responses: responses,
|
|
628
|
+
nonce: responseNonce,
|
|
589
629
|
},
|
|
590
630
|
};
|
|
591
631
|
}
|
|
@@ -669,6 +709,28 @@ function evaluateOcspResponse(ocspDer, opts) {
|
|
|
669
709
|
return { ok: false, status: parsed.status, signatureValid: true,
|
|
670
710
|
errors: ["OCSP response has no entry for the requested cert serial"] };
|
|
671
711
|
}
|
|
712
|
+
// Optional nonce echo verification (RFC 8954 / RFC 6960 §4.4.1).
|
|
713
|
+
// When opts.expectedNonce is supplied, the response MUST carry an
|
|
714
|
+
// OCSP nonce extension equal to the expected bytes — defends against
|
|
715
|
+
// replay of a stale "good" response captured before revocation.
|
|
716
|
+
var nonceCheck = "n/a";
|
|
717
|
+
if (opts.expectedNonce !== undefined && opts.expectedNonce !== null) {
|
|
718
|
+
if (!Buffer.isBuffer(opts.expectedNonce)) {
|
|
719
|
+
return { ok: false, status: parsed.status, signatureValid: true,
|
|
720
|
+
errors: ["evaluateOcspResponse: opts.expectedNonce must be a Buffer when supplied"] };
|
|
721
|
+
}
|
|
722
|
+
if (!parsed.basic.nonce) {
|
|
723
|
+
return { ok: false, status: parsed.status, signatureValid: true,
|
|
724
|
+
errors: ["OCSP response missing nonce extension (expected for replay defense)"] };
|
|
725
|
+
}
|
|
726
|
+
if (!parsed.basic.nonce.equals(opts.expectedNonce)) {
|
|
727
|
+
return { ok: false, status: parsed.status, signatureValid: true,
|
|
728
|
+
errors: ["OCSP nonce mismatch — possible replay or wrong responder"] };
|
|
729
|
+
}
|
|
730
|
+
nonceCheck = "matched";
|
|
731
|
+
} else if (parsed.basic.nonce) {
|
|
732
|
+
nonceCheck = "present-not-checked";
|
|
733
|
+
}
|
|
672
734
|
return {
|
|
673
735
|
ok: match.certStatus === "good",
|
|
674
736
|
status: parsed.status,
|
|
@@ -676,11 +738,146 @@ function evaluateOcspResponse(ocspDer, opts) {
|
|
|
676
738
|
thisUpdate: match.thisUpdate,
|
|
677
739
|
nextUpdate: match.nextUpdate,
|
|
678
740
|
signatureValid: true,
|
|
741
|
+
nonce: nonceCheck,
|
|
679
742
|
errors: match.certStatus === "good" ? [] :
|
|
680
743
|
["certStatus=" + match.certStatus],
|
|
681
744
|
};
|
|
682
745
|
}
|
|
683
746
|
|
|
747
|
+
// ---- OCSPRequest builder (RFC 6960 §4.1 + RFC 8954 nonce ext) ----
|
|
748
|
+
//
|
|
749
|
+
// Constructs a DER-encoded OCSPRequest for a single (leafCertDer,
|
|
750
|
+
// issuerCertDer) pair, optionally with an RFC 8954 nonce extension.
|
|
751
|
+
// Operators send the returned `requestDer` to the OCSP responder URL
|
|
752
|
+
// (e.g. via b.httpClient with `Content-Type: application/ocsp-request`)
|
|
753
|
+
// and pass `nonce` to `ocsp.evaluate(responseDer, { expectedNonce })`
|
|
754
|
+
// to defend against replay attacks.
|
|
755
|
+
//
|
|
756
|
+
// Nonce DEFAULT ON — defense in depth. RFC 6960 §4.4.1 marks nonce
|
|
757
|
+
// optional and some public responders (notably Let's Encrypt's) ignore
|
|
758
|
+
// it; operators explicitly targeting those responders opt out via
|
|
759
|
+
// `opts.nonce: false`. The framework default is RFC 8954 with 16 random
|
|
760
|
+
// bytes (RFC 8954 §2.1 floor; ceiling 32).
|
|
761
|
+
|
|
762
|
+
function _extractIssuerNameDerAndKeyBitString(certDer) {
|
|
763
|
+
// From the leaf cert's tbsCertificate, pull the issuer Name (DER) +
|
|
764
|
+
// the issuer's SubjectPublicKey BIT STRING content. For OCSP CertID,
|
|
765
|
+
// RFC 6960 §4.1.1 specifies hash(issuerName) and hash(issuerKey).
|
|
766
|
+
// This helper operates on the ISSUER cert (not the leaf): the issuer's
|
|
767
|
+
// Name and SubjectPublicKey are what get hashed.
|
|
768
|
+
var top = asn1.readNode(certDer);
|
|
769
|
+
if (top.tag !== asn1.TAG.SEQUENCE) {
|
|
770
|
+
throw new TlsTrustError("tls/ocsp-bad-issuer-cert", "issuer cert is not a SEQUENCE");
|
|
771
|
+
}
|
|
772
|
+
var children = asn1.readSequence(top.value);
|
|
773
|
+
if (children.length === 0) {
|
|
774
|
+
throw new TlsTrustError("tls/ocsp-bad-issuer-cert", "issuer cert has no children");
|
|
775
|
+
}
|
|
776
|
+
var tbs = children[0];
|
|
777
|
+
if (tbs.tag !== asn1.TAG.SEQUENCE) {
|
|
778
|
+
throw new TlsTrustError("tls/ocsp-bad-issuer-cert", "tbsCertificate is not a SEQUENCE");
|
|
779
|
+
}
|
|
780
|
+
var tbsKids = asn1.readSequence(tbs.value);
|
|
781
|
+
// Skip optional [0] EXPLICIT version, then serialNumber, signature,
|
|
782
|
+
// then issuer (the cert's own subject in a self-signed CA, or its
|
|
783
|
+
// issuer field for a sub-CA — we just want THIS cert's subject).
|
|
784
|
+
var idx = 0;
|
|
785
|
+
if (tbsKids.length > 0 &&
|
|
786
|
+
tbsKids[0].tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC &&
|
|
787
|
+
tbsKids[0].tag === 0) { // allow:raw-byte-literal — X.509 [0] EXPLICIT version tag
|
|
788
|
+
idx = 1;
|
|
789
|
+
}
|
|
790
|
+
// After version: serialNumber, signature, issuer, validity, subject, SPKI.
|
|
791
|
+
var subjectIdx = idx + 4; // allow:raw-byte-literal — X.509 TBSCertificate field count
|
|
792
|
+
var spkiIdx = idx + 5; // allow:raw-byte-literal — X.509 TBSCertificate field count
|
|
793
|
+
if (spkiIdx >= tbsKids.length) {
|
|
794
|
+
throw new TlsTrustError("tls/ocsp-bad-issuer-cert", "issuer cert lacks SPKI field");
|
|
795
|
+
}
|
|
796
|
+
var subject = tbsKids[subjectIdx];
|
|
797
|
+
var spki = tbsKids[spkiIdx];
|
|
798
|
+
// Within SPKI: SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }
|
|
799
|
+
var spkiKids = asn1.readSequence(spki.value);
|
|
800
|
+
if (spkiKids.length < 2) { // allow:raw-byte-literal — minimum SPKI fields
|
|
801
|
+
throw new TlsTrustError("tls/ocsp-bad-issuer-cert", "SPKI missing subjectPublicKey BIT STRING");
|
|
802
|
+
}
|
|
803
|
+
var keyBytes = asn1.readBitString(spkiKids[1]);
|
|
804
|
+
return {
|
|
805
|
+
issuerNameDer: subject.raw, // the DER of the Name SEQUENCE (header + value)
|
|
806
|
+
issuerKey: keyBytes,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function _extractLeafSerial(leafCertDer) {
|
|
811
|
+
var top = asn1.readNode(leafCertDer);
|
|
812
|
+
if (top.tag !== asn1.TAG.SEQUENCE) {
|
|
813
|
+
throw new TlsTrustError("tls/ocsp-bad-leaf-cert", "leaf cert is not a SEQUENCE");
|
|
814
|
+
}
|
|
815
|
+
var children = asn1.readSequence(top.value);
|
|
816
|
+
var tbs = children[0];
|
|
817
|
+
var tbsKids = asn1.readSequence(tbs.value);
|
|
818
|
+
var idx = 0;
|
|
819
|
+
if (tbsKids.length > 0 &&
|
|
820
|
+
tbsKids[0].tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC &&
|
|
821
|
+
tbsKids[0].tag === 0) { // allow:raw-byte-literal — X.509 [0] EXPLICIT version tag
|
|
822
|
+
idx = 1;
|
|
823
|
+
}
|
|
824
|
+
// serialNumber is the next field after the optional version.
|
|
825
|
+
return tbsKids[idx].value;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function buildOcspRequest(opts) {
|
|
829
|
+
opts = opts || {};
|
|
830
|
+
if (!Buffer.isBuffer(opts.leafCertDer)) {
|
|
831
|
+
throw new TlsTrustError("tls/ocsp-bad-input",
|
|
832
|
+
"buildRequest: opts.leafCertDer must be a Buffer (peer cert raw DER)");
|
|
833
|
+
}
|
|
834
|
+
if (!Buffer.isBuffer(opts.issuerCertDer)) {
|
|
835
|
+
throw new TlsTrustError("tls/ocsp-bad-input",
|
|
836
|
+
"buildRequest: opts.issuerCertDer must be a Buffer (issuer cert raw DER)");
|
|
837
|
+
}
|
|
838
|
+
var iss = _extractIssuerNameDerAndKeyBitString(opts.issuerCertDer);
|
|
839
|
+
var serial = _extractLeafSerial(opts.leafCertDer);
|
|
840
|
+
// CertID hashes — SHA-1 per RFC 6960 §4.1.1 (the only universally
|
|
841
|
+
// supported algorithm; SHA-256 in OCSP requests is RFC 6960 §4.3
|
|
842
|
+
// optional and many responders reject).
|
|
843
|
+
var nameHash = nodeCrypto.createHash("sha1").update(iss.issuerNameDer).digest();
|
|
844
|
+
var keyHash = nodeCrypto.createHash("sha1").update(iss.issuerKey).digest();
|
|
845
|
+
// hashAlgorithm AlgorithmIdentifier ::= SEQUENCE { algorithm OID, NULL }
|
|
846
|
+
var algId = asn1.writeSequence([asn1.writeOid(OID_SHA1), asn1.writeNull()]);
|
|
847
|
+
var certId = asn1.writeSequence([
|
|
848
|
+
algId,
|
|
849
|
+
asn1.writeOctetString(nameHash),
|
|
850
|
+
asn1.writeOctetString(keyHash),
|
|
851
|
+
asn1.writeInteger(serial),
|
|
852
|
+
]);
|
|
853
|
+
var requestNode = asn1.writeSequence([certId]);
|
|
854
|
+
var requestList = asn1.writeSequence([requestNode]);
|
|
855
|
+
var nonceBytes = null;
|
|
856
|
+
var tbsChildren = [requestList];
|
|
857
|
+
// Default ON per the framework security-defaults-on rule. Operators
|
|
858
|
+
// talking to a responder that ignores nonces opt out via nonce: false.
|
|
859
|
+
var includeNonce = opts.nonce !== false;
|
|
860
|
+
if (includeNonce) {
|
|
861
|
+
var nonceLen = typeof opts.nonceLen === "number" ? opts.nonceLen : 16; // allow:raw-byte-literal — RFC 8954 §2.1 nonce length floor
|
|
862
|
+
if (nonceLen < 1 || nonceLen > 32) { // allow:raw-byte-literal — RFC 8954 §2.1 nonce length ceiling
|
|
863
|
+
throw new TlsTrustError("tls/ocsp-bad-nonce-len",
|
|
864
|
+
"nonce length out of RFC 8954 range (1..32)");
|
|
865
|
+
}
|
|
866
|
+
nonceBytes = nodeCrypto.randomBytes(nonceLen);
|
|
867
|
+
// Extension ::= SEQUENCE { extnID OID, critical BOOL DEFAULT FALSE, extnValue OCTET STRING }
|
|
868
|
+
// For nonce, extnValue is OCTET STRING wrapping the raw nonce bytes (RFC 8954 §2.1 — outer OCTET STRING only).
|
|
869
|
+
var nonceExt = asn1.writeSequence([
|
|
870
|
+
asn1.writeOid(OID_OCSP_NONCE),
|
|
871
|
+
asn1.writeOctetString(nonceBytes),
|
|
872
|
+
]);
|
|
873
|
+
var extensions = asn1.writeSequence([nonceExt]);
|
|
874
|
+
tbsChildren.push(asn1.writeContextExplicit(2, extensions)); // [2] EXPLICIT requestExtensions
|
|
875
|
+
}
|
|
876
|
+
var tbs = asn1.writeSequence(tbsChildren);
|
|
877
|
+
var requestDer = asn1.writeSequence([tbs]);
|
|
878
|
+
return { requestDer: requestDer, nonce: nonceBytes };
|
|
879
|
+
}
|
|
880
|
+
|
|
684
881
|
var ocsp = Object.freeze({
|
|
685
882
|
// Connect with OCSP requested. Returns { authorized, ocspBytes,
|
|
686
883
|
// peerCert }. requireStapled: true makes empty / not-stapled responses
|
|
@@ -721,6 +918,13 @@ var ocsp = Object.freeze({
|
|
|
721
918
|
},
|
|
722
919
|
parseResponse: parseOcspResponse,
|
|
723
920
|
evaluate: evaluateOcspResponse,
|
|
921
|
+
// buildRequest — construct a DER-encoded OCSPRequest for a single
|
|
922
|
+
// (leafCertDer, issuerCertDer) pair. RFC 8954 nonce extension is ON
|
|
923
|
+
// by default (16 random bytes; opts.nonceLen overrides within RFC
|
|
924
|
+
// 8954's 1..32 range; opts.nonce: false opts out for responders that
|
|
925
|
+
// ignore nonces). Returns { requestDer, nonce }; pass `nonce` to
|
|
926
|
+
// evaluate({ expectedNonce }).
|
|
927
|
+
buildRequest: buildOcspRequest,
|
|
724
928
|
// inspectMustStaple — read the RFC 7633 TLS Feature extension on a
|
|
725
929
|
// peer cert. Returns { mustStaple, features }. mustStaple === true
|
|
726
930
|
// when status_request (5) is in the feature list; the cert is then
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:3c8db39e-745e-4810-ae2a-7b527c808492",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-05T19:
|
|
8
|
+
"timestamp": "2026-05-05T19:17:04.824Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.7.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.39",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.39",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.39",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.7.
|
|
57
|
+
"ref": "@blamejs/core@0.7.39",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|