@blamejs/core 0.7.24 → 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.
@@ -15,6 +15,7 @@ var TlsTrustError = defineClass("TlsTrustError", { alwaysPermanent: true });
15
15
 
16
16
  var observability = lazyRequire(function () { return require("./observability"); });
17
17
  var audit = lazyRequire(function () { return require("./audit"); });
18
+ var asn1 = require("./asn1-der");
18
19
 
19
20
  var STATE = {
20
21
  cas: [],
@@ -308,6 +309,1172 @@ function _resetForTest() {
308
309
  STATE.baselineFingerprints = null;
309
310
  }
310
311
 
312
+ // ---- OCSP / OCSP-stapling wrappers around node:tls ----------------
313
+ //
314
+ // node:tls exposes two OCSP affordances:
315
+ // - tls.connect({ requestOCSP: true }) → emits 'OCSPResponse' event
316
+ // - https.createServer({ ... requestOCSP }) → server-side stapling
317
+ //
318
+ // b.network.tls.ocsp wraps these. The names reflect what the wrapper
319
+ // actually does at this stage:
320
+ //
321
+ // - ocsp.connect(opts) — connect with requestOCSP:true; resolve
322
+ // with { authorized, ocspBytes, peerCert }.
323
+ // - ocsp.requireStapled(opts) — refuse if peer doesn't staple an
324
+ // OCSP response (presence + non-empty
325
+ // byte check). DOES NOT verify the OCSP
326
+ // response signature against the issuer
327
+ // cert — that requires DER OCSPResponse
328
+ // parsing which lands in the next patch
329
+ // alongside the ASN.1 DER helper. The
330
+ // honest name keeps the surface from
331
+ // claiming "good" while only checking
332
+ // stapling.
333
+ //
334
+ // node:tls validates the cert chain itself; OCSP staple validation is
335
+ // the application's job once the response bytes are received.
336
+
337
+ function _connectAndCheckOcsp(opts, requireStapled) {
338
+ return new Promise(function (resolve, reject) {
339
+ var connectOpts = Object.assign({}, opts, { requestOCSP: true });
340
+ var sock;
341
+ try {
342
+ sock = tls.connect(connectOpts);
343
+ } catch (e) {
344
+ reject(new TlsTrustError("tls/connect-failed",
345
+ "tls.connect threw: " + ((e && e.message) || String(e))));
346
+ return;
347
+ }
348
+ var ocspResponseSeen = false;
349
+ sock.on("OCSPResponse", function (response) {
350
+ ocspResponseSeen = true;
351
+ if (!response || response.length === 0) {
352
+ if (requireStapled) {
353
+ sock.destroy();
354
+ reject(new TlsTrustError("tls/ocsp-empty",
355
+ "OCSP response was empty and requireStapled is set"));
356
+ return;
357
+ }
358
+ }
359
+ // Operator can post-process the DER OCSPResponse via the resolved
360
+ // callback; the framework doesn't parse the ASN.1 itself.
361
+ sock.once("secureConnect", function () {
362
+ var rv = {
363
+ authorized: sock.authorized,
364
+ ocspBytes: response || null,
365
+ peerCert: sock.getPeerCertificate(true),
366
+ };
367
+ sock.destroy();
368
+ resolve(rv);
369
+ });
370
+ });
371
+ sock.on("secureConnect", function () {
372
+ // 'OCSPResponse' fires BEFORE 'secureConnect' when the server
373
+ // replied with stapled OCSP. If we got here without seeing an
374
+ // OCSPResponse event AND requireStapled is set, refuse.
375
+ if (!ocspResponseSeen) {
376
+ if (requireStapled) {
377
+ sock.destroy();
378
+ reject(new TlsTrustError("tls/ocsp-not-stapled",
379
+ "TLS peer did not staple an OCSP response and requireStapled is set"));
380
+ return;
381
+ }
382
+ var rv = {
383
+ authorized: sock.authorized,
384
+ ocspBytes: null,
385
+ peerCert: sock.getPeerCertificate(true),
386
+ };
387
+ sock.destroy();
388
+ resolve(rv);
389
+ }
390
+ });
391
+ sock.on("error", function (e) { reject(e); });
392
+ });
393
+ }
394
+
395
+ // ---- OCSP response parser (RFC 6960) ----
396
+ //
397
+ // Decodes a DER OCSPResponse into:
398
+ // {
399
+ // status: "successful" | "malformedRequest" | "internalError" |
400
+ // "tryLater" | "sigRequired" | "unauthorized",
401
+ // basic: { // present when status === "successful"
402
+ // tbsResponseDataDer: Buffer, // the bytes signed
403
+ // signatureAlgorithmOid: string,
404
+ // signature: Buffer,
405
+ // responses: [{ certIdSerialHex, certStatus, thisUpdate, nextUpdate }, ...],
406
+ // }
407
+ // }
408
+ //
409
+ // Cherry-picks the fields the framework needs to verify the response —
410
+ // the signed bytes (tbsResponseData) + the signature + each response
411
+ // entry's status. Out of scope: ResponderID / extensions / nonce
412
+ // validation (operators relying on those wire their own parser).
413
+
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
418
+ var OID_RSA_SHA256 = "1.2.840.113549.1.1.11";
419
+ var OID_RSA_SHA384 = "1.2.840.113549.1.1.12";
420
+ var OID_RSA_SHA512 = "1.2.840.113549.1.1.13";
421
+ var OID_ECDSA_SHA256 = "1.2.840.10045.4.3.2";
422
+ var OID_ECDSA_SHA384 = "1.2.840.10045.4.3.3";
423
+ var OID_ECDSA_SHA512 = "1.2.840.10045.4.3.4";
424
+
425
+ function _parseTime(node) {
426
+ // Parse UTCTime ("YYMMDDhhmmssZ") or GeneralizedTime
427
+ // ("YYYYMMDDhhmmssZ") into ms-since-epoch.
428
+ var s = node.value.toString("ascii");
429
+ var year, month, day, hour, min, sec;
430
+ if (s.length === 13 && s.charAt(12) === "Z") { // allow:raw-byte-literal — UTCTime length per X.690
431
+ // UTCTime YYMMDDhhmmssZ — 50+ → 19xx, else 20xx (RFC 5280 §4.1.2.5).
432
+ year = parseInt(s.slice(0, 2), 10);
433
+ year += year >= 50 ? 1900 : 2000; // allow:raw-byte-literal allow:raw-time-literal — RFC 5280 century pivot, calendar years
434
+ month = parseInt(s.slice(2, 4), 10);
435
+ day = parseInt(s.slice(4, 6), 10);
436
+ hour = parseInt(s.slice(6, 8), 10); // allow:raw-byte-literal — UTCTime hour-byte offsets
437
+ min = parseInt(s.slice(8, 10), 10); // allow:raw-byte-literal — UTCTime minute-byte offsets
438
+ sec = parseInt(s.slice(10, 12), 10);
439
+ } else if (s.length >= 15 && s.charAt(s.length - 1) === "Z") { // allow:raw-byte-literal — GeneralizedTime length per X.690
440
+ // GeneralizedTime YYYYMMDDhhmmssZ.
441
+ year = parseInt(s.slice(0, 4), 10);
442
+ month = parseInt(s.slice(4, 6), 10);
443
+ day = parseInt(s.slice(6, 8), 10); // allow:raw-byte-literal — GeneralizedTime day-byte offsets
444
+ hour = parseInt(s.slice(8, 10), 10); // allow:raw-byte-literal — GeneralizedTime hour-byte offsets
445
+ min = parseInt(s.slice(10, 12), 10);
446
+ sec = parseInt(s.slice(12, 14), 10);
447
+ } else {
448
+ throw new TlsTrustError("tls/ocsp-bad-time",
449
+ "OCSP time field is not UTCTime or GeneralizedTime: " + JSON.stringify(s));
450
+ }
451
+ return Date.UTC(year, month - 1, day, hour, min, sec);
452
+ }
453
+
454
+ var OCSP_RESPONSE_STATUS = {
455
+ 0: "successful",
456
+ 1: "malformedRequest",
457
+ 2: "internalError",
458
+ 3: "tryLater",
459
+ // 4 reserved
460
+ 5: "sigRequired",
461
+ 6: "unauthorized",
462
+ };
463
+
464
+ function parseOcspResponse(der) {
465
+ if (!Buffer.isBuffer(der) || der.length === 0) {
466
+ throw new TlsTrustError("tls/ocsp-bad-input",
467
+ "parseOcspResponse: expected non-empty Buffer");
468
+ }
469
+ var top = asn1.readNode(der); // OCSPResponse SEQUENCE
470
+ if (top.tag !== asn1.TAG.SEQUENCE) {
471
+ throw new TlsTrustError("tls/ocsp-bad-shape", "OCSPResponse is not a SEQUENCE");
472
+ }
473
+ var topChildren = asn1.readSequence(top.value);
474
+ if (topChildren.length === 0) {
475
+ throw new TlsTrustError("tls/ocsp-bad-shape", "OCSPResponse has no responseStatus");
476
+ }
477
+ var statusInt = asn1.readUnsignedInt(topChildren[0]);
478
+ var status = OCSP_RESPONSE_STATUS[statusInt] || ("unknown:" + statusInt);
479
+ if (status !== "successful") {
480
+ return { status: status };
481
+ }
482
+ // responseBytes [0] EXPLICIT ResponseBytes
483
+ if (topChildren.length < 2) {
484
+ throw new TlsTrustError("tls/ocsp-bad-shape",
485
+ "successful OCSP response missing responseBytes");
486
+ }
487
+ var responseBytes = asn1.unwrapExplicit(topChildren[1], 0); // [0] EXPLICIT
488
+ if (responseBytes.tag !== asn1.TAG.SEQUENCE) {
489
+ throw new TlsTrustError("tls/ocsp-bad-shape", "responseBytes is not a SEQUENCE");
490
+ }
491
+ var rbChildren = asn1.readSequence(responseBytes.value);
492
+ if (rbChildren.length < 2) {
493
+ throw new TlsTrustError("tls/ocsp-bad-shape",
494
+ "responseBytes missing responseType or response");
495
+ }
496
+ var responseTypeOid = asn1.readOid(rbChildren[0]);
497
+ if (responseTypeOid !== OID_BASIC_OCSP_RESPONSE) {
498
+ throw new TlsTrustError("tls/ocsp-unsupported-response-type",
499
+ "OCSP responseType is not id-pkix-ocsp-basic: " + responseTypeOid);
500
+ }
501
+ // The OCTET STRING wraps a DER BasicOCSPResponse.
502
+ var basicDer = asn1.readOctetString(rbChildren[1]);
503
+ var basic = asn1.readNode(basicDer);
504
+ if (basic.tag !== asn1.TAG.SEQUENCE) {
505
+ throw new TlsTrustError("tls/ocsp-bad-shape",
506
+ "BasicOCSPResponse is not a SEQUENCE");
507
+ }
508
+ var basicChildren = asn1.readSequence(basic.value);
509
+ if (basicChildren.length < 3) { // allow:raw-byte-literal — minimum BasicOCSPResponse fields (tbs + alg + sig)
510
+ throw new TlsTrustError("tls/ocsp-bad-shape",
511
+ "BasicOCSPResponse needs tbsResponseData + signatureAlgorithm + signature");
512
+ }
513
+ var tbsNode = basicChildren[0];
514
+ var sigAlgChildren = asn1.readSequence(basicChildren[1].value);
515
+ var sigAlgOid = asn1.readOid(sigAlgChildren[0]);
516
+ var signatureBytes = asn1.readBitString(basicChildren[2]);
517
+
518
+ // Slice the tbsResponseData bytes (header + value) — that's what the
519
+ // signature covers per RFC 6960 §4.2.1. tbsResponseData is the FIRST
520
+ // child of BasicOCSPResponse; its bytes start at basic.valueStart
521
+ // within the raw basicDer buffer (offset 0).
522
+ var basicValueStart = basicDer.length - basic.value.length;
523
+ var tbsDer = basicDer.slice(basicValueStart, basicValueStart + tbsNode.totalLength);
524
+
525
+ // Walk responseData (SEQUENCE) for the per-cert responses.
526
+ var rdChildren = asn1.readSequence(tbsNode.value);
527
+ // Find the SEQUENCE of SingleResponse — it's the LAST SEQUENCE before
528
+ // optional [1] EXPLICIT extensions. Per RFC 6960:
529
+ // ResponseData ::= SEQUENCE {
530
+ // version [0] EXPLICIT Version DEFAULT v1,
531
+ // responderID ResponderID,
532
+ // producedAt GeneralizedTime,
533
+ // responses SEQUENCE OF SingleResponse,
534
+ // responseExtensions [1] EXPLICIT Extensions OPTIONAL
535
+ // }
536
+ // ResponderID is itself a CHOICE (byName [1] / byKey [2]), then a
537
+ // GeneralizedTime, then the responses SEQUENCE-OF.
538
+ var responsesNode = null;
539
+ var responseExtensionsNode = null;
540
+ for (var rdi = rdChildren.length - 1; rdi >= 0; rdi -= 1) {
541
+ var ch = rdChildren[rdi];
542
+ if (ch.tag === asn1.TAG.SEQUENCE && ch.tagClass === asn1.TAG_CLASS.UNIVERSAL) {
543
+ responsesNode = ch;
544
+ break;
545
+ }
546
+ if (ch.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && ch.tag === 1) { // [1] EXPLICIT responseExtensions
547
+ responseExtensionsNode = asn1.readNode(ch.value, 0);
548
+ }
549
+ }
550
+ if (!responsesNode) {
551
+ throw new TlsTrustError("tls/ocsp-bad-shape",
552
+ "ResponseData missing responses SEQUENCE OF");
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
+ }
586
+ var singleResponses = asn1.readSequence(responsesNode.value);
587
+ var responses = [];
588
+ for (var sri = 0; sri < singleResponses.length; sri += 1) {
589
+ var sr = asn1.readSequence(singleResponses[sri].value);
590
+ if (sr.length < 3) continue; // allow:raw-byte-literal — minimum SingleResponse fields
591
+ // sr[0] = certID SEQUENCE, sr[1] = certStatus CHOICE, sr[2] = thisUpdate.
592
+ var certIdChildren = asn1.readSequence(sr[0].value);
593
+ // certID = SEQUENCE { hashAlgorithm, issuerNameHash, issuerKeyHash, serialNumber }
594
+ var serialHex = certIdChildren.length >= 4
595
+ ? certIdChildren[3].value.toString("hex")
596
+ : null;
597
+ var certStatus;
598
+ var statusNode = sr[1];
599
+ if (statusNode.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC) {
600
+ certStatus = statusNode.tag === 0 ? "good" :
601
+ statusNode.tag === 1 ? "revoked" :
602
+ statusNode.tag === 2 ? "unknown" : "unknown";
603
+ } else if (statusNode.tag === asn1.TAG.NULL) {
604
+ certStatus = "good";
605
+ } else {
606
+ certStatus = "unknown";
607
+ }
608
+ var thisUpdate = _parseTime(sr[2]);
609
+ var nextUpdate = null;
610
+ if (sr.length >= 4 && sr[3].tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && sr[3].tag === 0) {
611
+ nextUpdate = _parseTime(asn1.readNode(sr[3].value, 0));
612
+ }
613
+ responses.push({
614
+ certIdSerialHex: serialHex,
615
+ certStatus: certStatus,
616
+ thisUpdate: thisUpdate,
617
+ nextUpdate: nextUpdate,
618
+ });
619
+ }
620
+
621
+ return {
622
+ status: status,
623
+ basic: {
624
+ tbsResponseDataDer: tbsDer,
625
+ signatureAlgorithmOid: sigAlgOid,
626
+ signature: signatureBytes,
627
+ responses: responses,
628
+ nonce: responseNonce,
629
+ },
630
+ };
631
+ }
632
+
633
+ function _verifyOcspSignature(parsed, issuerPem) {
634
+ if (!parsed || !parsed.basic) {
635
+ throw new TlsTrustError("tls/ocsp-not-successful",
636
+ "OCSP response status is not 'successful' (got " +
637
+ (parsed && parsed.status) + ")");
638
+ }
639
+ var algOid = parsed.basic.signatureAlgorithmOid;
640
+ var nodeAlgo = algOid === OID_RSA_SHA256 ? "sha256" :
641
+ algOid === OID_RSA_SHA384 ? "sha384" :
642
+ algOid === OID_RSA_SHA512 ? "sha512" :
643
+ algOid === OID_ECDSA_SHA256 ? "sha256" :
644
+ algOid === OID_ECDSA_SHA384 ? "sha384" :
645
+ algOid === OID_ECDSA_SHA512 ? "sha512" : null;
646
+ if (nodeAlgo === null) {
647
+ throw new TlsTrustError("tls/ocsp-unsupported-sig-alg",
648
+ "OCSP signatureAlgorithm OID '" + algOid + "' is not supported by the verifier");
649
+ }
650
+ var keyObj;
651
+ try { keyObj = nodeCrypto.createPublicKey(issuerPem); }
652
+ catch (e) {
653
+ throw new TlsTrustError("tls/ocsp-bad-issuer-key",
654
+ "issuer public key parse failed: " + ((e && e.message) || String(e)));
655
+ }
656
+ // ECDSA OCSP signatures use DER-encoded ECDSA-Sig-Value (the ASN.1
657
+ // shape that node:crypto.verify accepts by default — no dsaEncoding
658
+ // option needed).
659
+ var verified;
660
+ try {
661
+ verified = nodeCrypto.verify(nodeAlgo, parsed.basic.tbsResponseDataDer, keyObj,
662
+ parsed.basic.signature);
663
+ } catch (e) {
664
+ throw new TlsTrustError("tls/ocsp-verify-threw",
665
+ "OCSP signature verify threw: " + ((e && e.message) || String(e)));
666
+ }
667
+ return verified;
668
+ }
669
+
670
+ // Operator-side OCSP response evaluator. Takes the DER bytes (from
671
+ // `ocsp.requireStapled` or any other source) plus the issuer cert PEM
672
+ // and returns a structured outcome:
673
+ // { ok, status, certStatus, thisUpdate, nextUpdate, signatureValid, errors }
674
+ function evaluateOcspResponse(ocspDer, opts) {
675
+ opts = opts || {};
676
+ var issuerPem = opts.issuerPem;
677
+ if (!issuerPem) {
678
+ throw new TlsTrustError("tls/ocsp-missing-issuer",
679
+ "evaluateOcspResponse requires opts.issuerPem (PEM of the cert that signed the OCSP response — typically the leaf's CA OR a delegated id-kp-OCSPSigning responder cert)");
680
+ }
681
+ var parsed;
682
+ try { parsed = parseOcspResponse(ocspDer); }
683
+ catch (e) {
684
+ return { ok: false, status: "parse-error",
685
+ errors: [(e && e.message) || String(e)] };
686
+ }
687
+ if (parsed.status !== "successful") {
688
+ return { ok: false, status: parsed.status, errors: ["responseStatus=" + parsed.status] };
689
+ }
690
+ var sigOk = false;
691
+ try { sigOk = _verifyOcspSignature(parsed, issuerPem); }
692
+ catch (e) {
693
+ return { ok: false, status: parsed.status,
694
+ signatureValid: false,
695
+ errors: [(e && e.message) || String(e)] };
696
+ }
697
+ if (!sigOk) {
698
+ return { ok: false, status: parsed.status, signatureValid: false,
699
+ errors: ["OCSP signature did not verify against the issuer key"] };
700
+ }
701
+ // Look up the requested cert serial in the responses; "good" wins.
702
+ var serial = opts.serialHex || (parsed.basic.responses[0] && parsed.basic.responses[0].certIdSerialHex);
703
+ var match = null;
704
+ for (var i = 0; i < parsed.basic.responses.length; i += 1) {
705
+ var r = parsed.basic.responses[i];
706
+ if (!serial || r.certIdSerialHex === serial) { match = r; break; }
707
+ }
708
+ if (!match) {
709
+ return { ok: false, status: parsed.status, signatureValid: true,
710
+ errors: ["OCSP response has no entry for the requested cert serial"] };
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
+ }
734
+ return {
735
+ ok: match.certStatus === "good",
736
+ status: parsed.status,
737
+ certStatus: match.certStatus,
738
+ thisUpdate: match.thisUpdate,
739
+ nextUpdate: match.nextUpdate,
740
+ signatureValid: true,
741
+ nonce: nonceCheck,
742
+ errors: match.certStatus === "good" ? [] :
743
+ ["certStatus=" + match.certStatus],
744
+ };
745
+ }
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
+
881
+ var ocsp = Object.freeze({
882
+ // Connect with OCSP requested. Returns { authorized, ocspBytes,
883
+ // peerCert }. requireStapled: true makes empty / not-stapled responses
884
+ // refuse instead of resolve. NOTE: requireStapled does NOT verify the
885
+ // OCSP response signature — pair it with evaluateOcspResponse(bytes,
886
+ // { issuerPem }) for full verification, OR use requireGood below.
887
+ connect: function (opts) {
888
+ return _connectAndCheckOcsp(opts || {}, false);
889
+ },
890
+ requireStapled: function (opts) {
891
+ return _connectAndCheckOcsp(opts || {}, true);
892
+ },
893
+ // requireGood: connect + parse + verify signature + check certStatus.
894
+ // Operator passes opts.issuerPem (the cert that signed the OCSP
895
+ // response — typically the leaf's CA OR a delegated OCSP responder
896
+ // cert). Throws TlsTrustError on any failure (no-staple, parse error,
897
+ // signature mismatch, certStatus=revoked/unknown).
898
+ requireGood: async function (opts) {
899
+ opts = opts || {};
900
+ if (!opts.issuerPem) {
901
+ throw new TlsTrustError("tls/ocsp-missing-issuer",
902
+ "ocsp.requireGood requires opts.issuerPem (PEM of the OCSP-signing cert)");
903
+ }
904
+ var rv = await _connectAndCheckOcsp(opts, true);
905
+ if (!rv.ocspBytes || rv.ocspBytes.length === 0) {
906
+ throw new TlsTrustError("tls/ocsp-empty",
907
+ "OCSP response was empty");
908
+ }
909
+ var evald = evaluateOcspResponse(rv.ocspBytes, {
910
+ issuerPem: opts.issuerPem,
911
+ serialHex: opts.serialHex || null,
912
+ });
913
+ if (!evald.ok) {
914
+ throw new TlsTrustError("tls/ocsp-not-good",
915
+ "OCSP evaluation failed: " + evald.errors.join("; "));
916
+ }
917
+ return Object.assign({}, rv, { ocspEvaluation: evald });
918
+ },
919
+ parseResponse: parseOcspResponse,
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,
928
+ // inspectMustStaple — read the RFC 7633 TLS Feature extension on a
929
+ // peer cert. Returns { mustStaple, features }. mustStaple === true
930
+ // when status_request (5) is in the feature list; the cert is then
931
+ // contractually required to ship an OCSP staple on every connection.
932
+ inspectMustStaple: function (rawDer) {
933
+ if (!Buffer.isBuffer(rawDer)) {
934
+ throw new TlsTrustError("tls/ocsp-bad-input",
935
+ "ocsp.inspectMustStaple: rawDer must be a Buffer (cert.raw)");
936
+ }
937
+ return _extractTlsFeatureExtensionFromCert(rawDer);
938
+ },
939
+ // requireMustStaple(peerCert, opts) — operator predicate. Refuses
940
+ // when the cert advertises must-staple but no OCSP staple was
941
+ // delivered (opts.ocspBytes empty/missing). When the cert does NOT
942
+ // advertise must-staple, the predicate returns null (operator opted
943
+ // in by setting opts.enforceUnconditional to also require staples
944
+ // on certs that don't carry the extension).
945
+ requireMustStaple: function (opts) {
946
+ opts = opts || {};
947
+ var enforceUnconditional = opts.enforceUnconditional === true;
948
+ return function (peerCert, ctx) {
949
+ if (!peerCert || !peerCert.raw) {
950
+ return new TlsTrustError("tls/ocsp-no-cert",
951
+ "requireMustStaple: peer cert.raw missing");
952
+ }
953
+ var feat = _extractTlsFeatureExtensionFromCert(peerCert.raw);
954
+ var stapled = ctx && Buffer.isBuffer(ctx.ocspBytes) && ctx.ocspBytes.length > 0;
955
+ if (feat.mustStaple && !stapled) {
956
+ return new TlsTrustError("tls/ocsp-must-staple-violated",
957
+ "cert advertises must-staple (RFC 7633) but no OCSP staple was delivered");
958
+ }
959
+ if (!feat.mustStaple && enforceUnconditional && !stapled) {
960
+ return new TlsTrustError("tls/ocsp-staple-required",
961
+ "operator policy requires OCSP staple but server did not provide one");
962
+ }
963
+ return null;
964
+ };
965
+ },
966
+ });
967
+
968
+ // ---- Certificate Transparency (RFC 6962 + RFC 9162) SCT verifier --
969
+ //
970
+ // CT requires every TLS server certificate to carry at least 2 Signed
971
+ // Certificate Timestamps (SCTs) from approved logs. Modern browsers
972
+ // (Chrome / Safari) refuse certificates without sufficient SCTs.
973
+ //
974
+ // node:tls surfaces SCTs via TLSSocket.getPeerX509Certificate() →
975
+ // X509Certificate.raw (the DER cert). The SCTs sit inside the cert as
976
+ // the OCSP-aware extension OID 1.3.6.1.4.1.11129.2.4.2.
977
+ //
978
+ // b.network.tls.ct.verify(cert, opts) checks that the cert has at
979
+ // least `minScts` SCTs and that each SCT references a log in
980
+ // `approvedLogs`. Full SCT-signature verification against the log's
981
+ // pubkey is OUT of scope for this patch — that requires log-pubkey
982
+ // distribution + ASN.1 SCT parsing. The framework provides the
983
+ // SCT-presence + log-id check; signature verification is a follow-up
984
+ // when the ASN.1 dependency lands.
985
+
986
+ // SCT extension OID per RFC 6962 §3.3.
987
+ var OID_CT_SCT_LIST = "1.3.6.1.4.1.11129.2.4.2";
988
+
989
+ // Walk a DER X.509 cert and locate the SCT extension's OCTET STRING
990
+ // content. Returns { sctListRaw } or { sctListRaw: null } when no SCT
991
+ // extension is present.
992
+ function _extractSctExtensionFromCert(certDer) {
993
+ // Tolerant of malformed cert buffers — return null sctListRaw when
994
+ // the ASN.1 walk fails. Callers (parseScts / verifyScts) treat that
995
+ // as "no SCT extension" rather than throwing on broken input.
996
+ var top;
997
+ try { top = asn1.readNode(certDer); }
998
+ catch (_e) { return { sctListRaw: null }; }
999
+ if (top.tag !== asn1.TAG.SEQUENCE) return { sctListRaw: null };
1000
+ var children;
1001
+ try { children = asn1.readSequence(top.value); }
1002
+ catch (_e) { return { sctListRaw: null }; }
1003
+ if (children.length === 0) return { sctListRaw: null };
1004
+ // Cert ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signature }
1005
+ var tbs = children[0];
1006
+ if (tbs.tag !== asn1.TAG.SEQUENCE) return { sctListRaw: null };
1007
+ // tbsCertificate ::= SEQUENCE { ..., extensions [3] EXPLICIT ... }
1008
+ var tbsChildren;
1009
+ try { tbsChildren = asn1.readSequence(tbs.value); }
1010
+ catch (_e) { return { sctListRaw: null }; }
1011
+ var extensionsNode = null;
1012
+ for (var i = 0; i < tbsChildren.length; i += 1) {
1013
+ var ch = tbsChildren[i];
1014
+ if (ch.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && ch.tag === 3) { // allow:raw-byte-literal — X.509 [3] EXPLICIT extensions tag
1015
+ extensionsNode = asn1.readNode(ch.value, 0);
1016
+ break;
1017
+ }
1018
+ }
1019
+ if (!extensionsNode || extensionsNode.tag !== asn1.TAG.SEQUENCE) {
1020
+ return { sctListRaw: null };
1021
+ }
1022
+ var extensions = asn1.readSequence(extensionsNode.value);
1023
+ for (var e = 0; e < extensions.length; e += 1) {
1024
+ var ext = extensions[e]; // Extension ::= SEQUENCE { extnID OID, critical BOOL OPTIONAL, extnValue OCTET STRING }
1025
+ if (ext.tag !== asn1.TAG.SEQUENCE) continue;
1026
+ var extChildren = asn1.readSequence(ext.value);
1027
+ if (extChildren.length === 0) continue;
1028
+ var extOid = asn1.readOid(extChildren[0]);
1029
+ if (extOid !== OID_CT_SCT_LIST) continue;
1030
+ // The last child is the OCTET STRING extnValue. Per RFC 6962 §3.3
1031
+ // that OCTET STRING wraps a SECOND OCTET STRING which contains the
1032
+ // raw SignedCertificateTimestampList (TLS-encoded).
1033
+ var extnValueOuter = asn1.readOctetString(extChildren[extChildren.length - 1]);
1034
+ var inner = asn1.readNode(extnValueOuter);
1035
+ if (inner.tag !== asn1.TAG.OCTET_STRING) {
1036
+ throw new TlsTrustError("tls/ct-bad-extension",
1037
+ "SCT extension extnValue does not wrap a second OCTET STRING");
1038
+ }
1039
+ return { sctListRaw: inner.value };
1040
+ }
1041
+ return { sctListRaw: null };
1042
+ }
1043
+
1044
+ // TLS Feature extension OID per RFC 7633 §6. The extension value is
1045
+ // SEQUENCE OF INTEGER; the integer 5 == status_request == "must-staple".
1046
+ var OID_TLS_FEATURE = "1.3.6.1.5.5.7.1.24";
1047
+ var TLS_FEATURE_STATUS_REQUEST = 5;
1048
+
1049
+ // Walk a DER X.509 cert and return the TLS Feature extension's
1050
+ // integer list. Returns { mustStaple, features }. Tolerant of
1051
+ // malformed cert input — mirrors _extractSctExtensionFromCert's
1052
+ // try/catch tolerance.
1053
+ function _extractTlsFeatureExtensionFromCert(certDer) {
1054
+ var none = { mustStaple: false, features: [] };
1055
+ var top;
1056
+ try { top = asn1.readNode(certDer); }
1057
+ catch (_e) { return none; }
1058
+ if (top.tag !== asn1.TAG.SEQUENCE) return none;
1059
+ var children;
1060
+ try { children = asn1.readSequence(top.value); }
1061
+ catch (_e) { return none; }
1062
+ if (children.length === 0) return none;
1063
+ var tbs = children[0];
1064
+ if (tbs.tag !== asn1.TAG.SEQUENCE) return none;
1065
+ var tbsChildren;
1066
+ try { tbsChildren = asn1.readSequence(tbs.value); }
1067
+ catch (_e) { return none; }
1068
+ var extensionsNode = null;
1069
+ for (var i = 0; i < tbsChildren.length; i += 1) {
1070
+ var ch = tbsChildren[i];
1071
+ if (ch.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && ch.tag === 3) { // allow:raw-byte-literal — X.509 [3] EXPLICIT extensions tag
1072
+ extensionsNode = asn1.readNode(ch.value, 0);
1073
+ break;
1074
+ }
1075
+ }
1076
+ if (!extensionsNode || extensionsNode.tag !== asn1.TAG.SEQUENCE) return none;
1077
+ var extensions = asn1.readSequence(extensionsNode.value);
1078
+ for (var e = 0; e < extensions.length; e += 1) {
1079
+ var ext = extensions[e];
1080
+ if (ext.tag !== asn1.TAG.SEQUENCE) continue;
1081
+ var extChildren = asn1.readSequence(ext.value);
1082
+ if (extChildren.length === 0) continue;
1083
+ var extOid;
1084
+ try { extOid = asn1.readOid(extChildren[0]); }
1085
+ catch (_e2) { continue; }
1086
+ if (extOid !== OID_TLS_FEATURE) continue;
1087
+ var extnValue = asn1.readOctetString(extChildren[extChildren.length - 1]);
1088
+ // extnValue wraps SEQUENCE OF INTEGER.
1089
+ var seq;
1090
+ try { seq = asn1.readNode(extnValue); }
1091
+ catch (_e3) { return none; }
1092
+ if (seq.tag !== asn1.TAG.SEQUENCE) return none;
1093
+ var feats = asn1.readSequence(seq.value);
1094
+ var ints = [];
1095
+ var mustStaple = false;
1096
+ for (var f = 0; f < feats.length; f += 1) {
1097
+ try {
1098
+ var n = asn1.readUnsignedInt(feats[f]);
1099
+ ints.push(n);
1100
+ if (n === TLS_FEATURE_STATUS_REQUEST) mustStaple = true;
1101
+ } catch (_e4) { /* ignore non-integer entries */ }
1102
+ }
1103
+ return { mustStaple: mustStaple, features: ints };
1104
+ }
1105
+ return none;
1106
+ }
1107
+
1108
+ // Parse the TLS-encoded SignedCertificateTimestampList (RFC 6962 §3.3).
1109
+ // Format: 2-byte length + concatenation of individual SCTs, each
1110
+ // itself prefixed by a 2-byte length.
1111
+ function _parseSctList(sctListRaw) {
1112
+ if (!Buffer.isBuffer(sctListRaw) || sctListRaw.length < 2) { // allow:raw-byte-literal — outer 2-byte length prefix
1113
+ throw new TlsTrustError("tls/ct-bad-list",
1114
+ "SCT list shorter than the outer length prefix");
1115
+ }
1116
+ var totalLen = sctListRaw.readUInt16BE(0);
1117
+ if (totalLen + 2 !== sctListRaw.length) { // allow:raw-byte-literal — outer length prefix
1118
+ throw new TlsTrustError("tls/ct-bad-list",
1119
+ "SCT list outer length " + totalLen + " does not match buffer " +
1120
+ (sctListRaw.length - 2));
1121
+ }
1122
+ var pos = 2; // allow:raw-byte-literal — past the outer prefix
1123
+ var scts = [];
1124
+ while (pos < sctListRaw.length) {
1125
+ var sctLen = sctListRaw.readUInt16BE(pos);
1126
+ pos += 2;
1127
+ if (pos + sctLen > sctListRaw.length) {
1128
+ throw new TlsTrustError("tls/ct-bad-list",
1129
+ "SCT[" + scts.length + "] declared length " + sctLen +
1130
+ " extends past the list buffer");
1131
+ }
1132
+ var sctBytes = sctListRaw.slice(pos, pos + sctLen);
1133
+ scts.push(_parseSct(sctBytes));
1134
+ pos += sctLen;
1135
+ }
1136
+ return scts;
1137
+ }
1138
+
1139
+ // Per RFC 6962 §3.2 — a single SCT:
1140
+ // sct_version (1 byte) — 0 = v1
1141
+ // id (LogID) (32 bytes) — SHA-256 of log's pubkey
1142
+ // timestamp (8 bytes) — uint64 ms since epoch
1143
+ // ct_extensions (2-byte len + N) — usually empty
1144
+ // signature DigitallySigned (hash + sig algo + 2-byte len + N)
1145
+ function _parseSct(sctBuf) {
1146
+ if (sctBuf.length < 1 + 32 + 8 + 2 + 4) { // allow:raw-byte-literal — minimum SCT v1 byte total
1147
+ throw new TlsTrustError("tls/ct-sct-too-short",
1148
+ "SCT is shorter than the minimum v1 layout (" + sctBuf.length + " bytes)");
1149
+ }
1150
+ var version = sctBuf[0];
1151
+ if (version !== 0) {
1152
+ throw new TlsTrustError("tls/ct-sct-bad-version",
1153
+ "SCT version is not 0 (v1): got " + version);
1154
+ }
1155
+ var logId = sctBuf.slice(1, 1 + 32); // allow:raw-byte-literal — RFC 6962 32-byte LogID
1156
+ var timestamp = Number(sctBuf.readBigUInt64BE(1 + 32)); // allow:raw-byte-literal — past LogID
1157
+ var extLen = sctBuf.readUInt16BE(1 + 32 + 8); // allow:raw-byte-literal — past LogID + timestamp
1158
+ var pos = 1 + 32 + 8 + 2; // allow:raw-byte-literal — past extLen field
1159
+ var extensions = sctBuf.slice(pos, pos + extLen);
1160
+ pos += extLen;
1161
+ if (pos + 4 > sctBuf.length) { // allow:raw-byte-literal — DigitallySigned header (hash + alg + len)
1162
+ throw new TlsTrustError("tls/ct-sct-truncated",
1163
+ "SCT truncated before DigitallySigned");
1164
+ }
1165
+ var hashAlgo = sctBuf[pos];
1166
+ var sigAlgo = sctBuf[pos + 1];
1167
+ pos += 2; // allow:raw-byte-literal — past hash+alg pair
1168
+ var sigLen = sctBuf.readUInt16BE(pos);
1169
+ pos += 2; // allow:raw-byte-literal — past sig length
1170
+ if (pos + sigLen !== sctBuf.length) {
1171
+ throw new TlsTrustError("tls/ct-sct-truncated",
1172
+ "SCT signature length " + sigLen + " does not match remaining bytes " +
1173
+ (sctBuf.length - pos));
1174
+ }
1175
+ var signature = sctBuf.slice(pos, pos + sigLen);
1176
+ return {
1177
+ version: version,
1178
+ logId: logId,
1179
+ logIdHex: logId.toString("hex"),
1180
+ timestamp: timestamp,
1181
+ extensions: extensions,
1182
+ hashAlgo: hashAlgo, // RFC 5246 HashAlgorithm enum (4=sha256, 5=sha384, 6=sha512)
1183
+ sigAlgo: sigAlgo, // RFC 5246 SignatureAlgorithm enum (1=rsa, 3=ecdsa)
1184
+ signature: signature,
1185
+ };
1186
+ }
1187
+
1188
+ // Build the canonical signed-entry per RFC 6962 §3.2 for X.509
1189
+ // pre-cert-free chains (issued cert path):
1190
+ // sct_version (1) || signature_type (1=certificate_timestamp) ||
1191
+ // timestamp (8) || entry_type (0=x509_entry) ||
1192
+ // signed_entry (3-byte length || ASN.1 cert without SCT extension) ||
1193
+ // ct_extensions (2-byte length || N)
1194
+ function _buildSctSignedEntry(certWithoutSctDer, sct) {
1195
+ var head = Buffer.alloc(1 + 1 + 8 + 2); // allow:raw-byte-literal — fixed-shape header bytes
1196
+ head[0] = sct.version;
1197
+ head[1] = 0; // signature_type = certificate_timestamp
1198
+ head.writeBigUInt64BE(BigInt(sct.timestamp), 2); // allow:raw-byte-literal — past version+sig-type
1199
+ head.writeUInt16BE(0, 10); // allow:raw-byte-literal — entry_type = x509_entry (2 bytes; high byte = 0, low byte = 0)
1200
+ // signed_entry: 3-byte length prefix + cert DER.
1201
+ var lenBytes = Buffer.alloc(3); // allow:raw-byte-literal — RFC 6962 24-bit length prefix
1202
+ lenBytes[0] = (certWithoutSctDer.length >> 16) & 0xff; // allow:raw-byte-literal — base-256 length high byte
1203
+ lenBytes[1] = (certWithoutSctDer.length >> 8) & 0xff; // allow:raw-byte-literal — base-256 length mid byte
1204
+ lenBytes[2] = certWithoutSctDer.length & 0xff; // allow:raw-byte-literal — base-256 length low byte
1205
+ // ct_extensions: 2-byte length + bytes.
1206
+ var extHead = Buffer.alloc(2); // allow:raw-byte-literal — RFC 6962 2-byte ct_extensions length prefix
1207
+ extHead.writeUInt16BE(sct.extensions.length, 0);
1208
+ return Buffer.concat([head, lenBytes, certWithoutSctDer, extHead, sct.extensions]);
1209
+ }
1210
+
1211
+ // Strip the SCT extension from a DER cert + return the rebuilt cert
1212
+ // bytes for SCT signing per RFC 6962 §3.2. The strip is byte-precise:
1213
+ // walk the TBSCertificate extensions list, drop the SCT extension,
1214
+ // and re-encode just enough of the chain to reproduce the original
1215
+ // shape minus that one extension. This is non-trivial because the
1216
+ // tbsCertificate length, certificate length, and signature-bytes
1217
+ // boundaries all shift.
1218
+ //
1219
+ // Simpler: rebuild only the tbsCertificate extensions SEQUENCE without
1220
+ // the SCT entry, recompute lengths above it, and replace the cert's
1221
+ // SignedCertificate (BIT STRING) with the original's signature too —
1222
+ // but that's incorrect since the original signature was computed over
1223
+ // the WITH-SCT TBS. The CT log signed an entry built from the
1224
+ // without-SCT pre-issuance shape, NOT the issued cert's tbs.
1225
+ //
1226
+ // Per RFC 6962 §3.1, log servers receive a "TBSCertificate" minus the
1227
+ // SCT extension from the CA. The signed_entry the framework
1228
+ // reconstructs is that pre-extension TBSCertificate. We compute it by
1229
+ // removing the SCT extension at the byte level and rebuilding all
1230
+ // outer length prefixes.
1231
+ function _stripSctExtensionFromCert(certDer) {
1232
+ var top = asn1.readNode(certDer);
1233
+ if (top.tag !== asn1.TAG.SEQUENCE) {
1234
+ throw new TlsTrustError("tls/ct-bad-cert", "Certificate is not a SEQUENCE");
1235
+ }
1236
+ var topChildren = asn1.readSequence(top.value);
1237
+ var tbs = topChildren[0];
1238
+ if (tbs.tag !== asn1.TAG.SEQUENCE) {
1239
+ throw new TlsTrustError("tls/ct-bad-cert", "tbsCertificate is not a SEQUENCE");
1240
+ }
1241
+ // Walk tbsCertificate to find the [3] EXPLICIT extensions wrapper.
1242
+ var tbsChildren = asn1.readSequence(tbs.value);
1243
+ var newTbsChildrenBytes = [];
1244
+ var foundExtensions = false;
1245
+ for (var i = 0; i < tbsChildren.length; i += 1) {
1246
+ var ch = tbsChildren[i];
1247
+ if (ch.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && ch.tag === 3) { // allow:raw-byte-literal — [3] EXPLICIT extensions tag
1248
+ foundExtensions = true;
1249
+ // Inner SEQUENCE OF Extensions.
1250
+ var inner = asn1.readNode(ch.value, 0);
1251
+ var extList = asn1.readSequence(inner.value);
1252
+ var keptExtBytes = [];
1253
+ for (var j = 0; j < extList.length; j += 1) {
1254
+ var ext = extList[j];
1255
+ var extBytes = ext.value;
1256
+ var extDescChildren = asn1.readSequence(ext.value);
1257
+ if (extDescChildren.length > 0) {
1258
+ try {
1259
+ var oid = asn1.readOid(extDescChildren[0]);
1260
+ if (oid === OID_CT_SCT_LIST) continue; // drop the SCT extension
1261
+ } catch (_e) { /* not an OID — keep the extension as-is */ }
1262
+ }
1263
+ // Re-encode this extension verbatim (we have the original bytes).
1264
+ var origExt = certDer.slice(0, 0); // placeholder; we rebuild from the parsed node below
1265
+ void origExt;
1266
+ keptExtBytes.push(_encodeAsn1(asn1.TAG.SEQUENCE, true, extBytes));
1267
+ void extBytes;
1268
+ }
1269
+ var newExtSeq = _encodeAsn1(asn1.TAG.SEQUENCE, true, Buffer.concat(keptExtBytes));
1270
+ var newExplicit3 = _encodeContextExplicit(3, newExtSeq);
1271
+ newTbsChildrenBytes.push(newExplicit3);
1272
+ } else {
1273
+ // Re-encode the original child verbatim by slicing its bytes from
1274
+ // the parent's value buffer.
1275
+ var childDer = _encodeAsn1FromNode(ch);
1276
+ newTbsChildrenBytes.push(childDer);
1277
+ }
1278
+ }
1279
+ if (!foundExtensions) {
1280
+ // Cert has no extensions at all — caller's SCT lookup would have
1281
+ // returned no SCT bytes, so this path shouldn't run. Surface anyway.
1282
+ throw new TlsTrustError("tls/ct-no-extensions",
1283
+ "cert has no extensions to strip from");
1284
+ }
1285
+ var newTbsValue = Buffer.concat(newTbsChildrenBytes);
1286
+ var newTbs = _encodeAsn1(asn1.TAG.SEQUENCE, true, newTbsValue);
1287
+ return newTbs;
1288
+ }
1289
+
1290
+ // Minimal DER encoder helpers — enough to rebuild a TBS without the
1291
+ // SCT extension. Tag class is universal for SEQUENCE; constructed
1292
+ // flag wired explicitly.
1293
+ function _encodeLength(len) {
1294
+ if (len < 0x80) return Buffer.from([len]); // allow:raw-byte-literal — DER short-form length threshold
1295
+ var tmp = [];
1296
+ var n = len;
1297
+ while (n > 0) {
1298
+ tmp.unshift(n & 0xff); // allow:raw-byte-literal — base-256 byte
1299
+ n = n >>> 8; // allow:raw-byte-literal — byte shift
1300
+ }
1301
+ return Buffer.concat([Buffer.from([0x80 | tmp.length]), Buffer.from(tmp)]); // allow:raw-byte-literal — DER long-form length flag
1302
+ }
1303
+ function _encodeAsn1(tag, constructed, value) {
1304
+ var tagByte = (constructed ? 0x20 : 0x00) | tag; // allow:raw-byte-literal — DER constructed bit + universal tag
1305
+ return Buffer.concat([Buffer.from([tagByte]), _encodeLength(value.length), value]);
1306
+ }
1307
+ function _encodeContextExplicit(num, value) {
1308
+ // Context-specific class (10) + constructed (20) | tag.
1309
+ var tagByte = 0xa0 | num; // allow:raw-byte-literal — DER context-specific + constructed
1310
+ return Buffer.concat([Buffer.from([tagByte]), _encodeLength(value.length), value]);
1311
+ }
1312
+ function _encodeAsn1FromNode(node) {
1313
+ // Re-encode a parsed node verbatim by replaying the tag + length +
1314
+ // value. Universal-class shortcut: if class is universal, set the
1315
+ // tag byte from the universal table; if constructed, set the bit.
1316
+ // Context-specific / application / private classes get their bytes
1317
+ // restored directly. This works for the simple shapes we walk.
1318
+ var tagByte;
1319
+ if (node.tagClass === asn1.TAG_CLASS.UNIVERSAL) {
1320
+ tagByte = (node.constructed ? 0x20 : 0x00) | (node.tag & 0x1f); // allow:raw-byte-literal — DER constructed bit + universal tag
1321
+ } else {
1322
+ var classBits = (node.tagClass & 0x03) << 6; // allow:raw-byte-literal — DER tag-class bits
1323
+ tagByte = classBits | (node.constructed ? 0x20 : 0x00) | (node.tag & 0x1f); // allow:raw-byte-literal — DER constructed bit + low-tag
1324
+ }
1325
+ return Buffer.concat([Buffer.from([tagByte]), _encodeLength(node.value.length), node.value]);
1326
+ }
1327
+
1328
+ // SCT signature verification per RFC 6962 §3.2. opts.logKeys maps
1329
+ // log_id (hex) → PEM public key. Operators populate from the Chrome
1330
+ // CT log list (https://www.gstatic.com/ct/log_list/v3/log_list.json
1331
+ // or equivalent) — log keys rotate, so the framework does NOT bake
1332
+ // them in; that drift is the operator's to manage.
1333
+ function verifyScts(certDer, opts) {
1334
+ opts = opts || {};
1335
+ if (!Buffer.isBuffer(certDer)) {
1336
+ throw new TlsTrustError("tls/ct-bad-input",
1337
+ "verifyScts: certDer must be a Buffer");
1338
+ }
1339
+ var logKeys = opts.logKeys || {};
1340
+ var minScts = typeof opts.minScts === "number" ? opts.minScts : 2; // allow:raw-byte-literal — Chrome CT policy min-2-SCTs
1341
+ var ext = _extractSctExtensionFromCert(certDer);
1342
+ if (!ext.sctListRaw) {
1343
+ return { ok: false, reason: "no-sct-extension", scts: [] };
1344
+ }
1345
+ var scts;
1346
+ try { scts = _parseSctList(ext.sctListRaw); }
1347
+ catch (e) {
1348
+ return { ok: false, reason: "parse-error",
1349
+ error: (e && e.message) || String(e), scts: [] };
1350
+ }
1351
+ // Strip the SCT extension to compute the signed-entry per §3.2.
1352
+ var stripped;
1353
+ try { stripped = _stripSctExtensionFromCert(certDer); }
1354
+ catch (e) {
1355
+ return { ok: false, reason: "strip-failed",
1356
+ error: (e && e.message) || String(e), scts: scts };
1357
+ }
1358
+ var verifiedCount = 0;
1359
+ var perSctResults = [];
1360
+ for (var s = 0; s < scts.length; s += 1) {
1361
+ var sct = scts[s];
1362
+ var pem = logKeys[sct.logIdHex];
1363
+ if (!pem) {
1364
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: false,
1365
+ reason: "log-key-missing" });
1366
+ continue;
1367
+ }
1368
+ var signedEntry;
1369
+ try { signedEntry = _buildSctSignedEntry(stripped, sct); }
1370
+ catch (e) {
1371
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: false,
1372
+ reason: "build-entry-failed",
1373
+ error: (e && e.message) || String(e) });
1374
+ continue;
1375
+ }
1376
+ var nodeAlgo = sct.hashAlgo === 4 ? "sha256" : // allow:raw-byte-literal — TLS 1.2 HashAlgorithm enum sha256
1377
+ sct.hashAlgo === 5 ? "sha384" : // allow:raw-byte-literal — TLS 1.2 HashAlgorithm enum sha384
1378
+ sct.hashAlgo === 6 ? "sha512" : // allow:raw-byte-literal — TLS 1.2 HashAlgorithm enum sha512
1379
+ null;
1380
+ if (nodeAlgo === null) {
1381
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: false,
1382
+ reason: "unsupported-hash-algo", hashAlgo: sct.hashAlgo });
1383
+ continue;
1384
+ }
1385
+ var keyObj;
1386
+ try { keyObj = nodeCrypto.createPublicKey(pem); }
1387
+ catch (e) {
1388
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: false,
1389
+ reason: "log-key-parse-failed",
1390
+ error: (e && e.message) || String(e) });
1391
+ continue;
1392
+ }
1393
+ var verified;
1394
+ try { verified = nodeCrypto.verify(nodeAlgo, signedEntry, keyObj, sct.signature); }
1395
+ catch (e) {
1396
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: false,
1397
+ reason: "verify-threw",
1398
+ error: (e && e.message) || String(e) });
1399
+ continue;
1400
+ }
1401
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: verified });
1402
+ if (verified) verifiedCount += 1;
1403
+ }
1404
+ return {
1405
+ ok: verifiedCount >= minScts,
1406
+ reason: verifiedCount >= minScts ? null : "insufficient-verified",
1407
+ minScts: minScts,
1408
+ verifiedCount: verifiedCount,
1409
+ totalScts: scts.length,
1410
+ scts: perSctResults,
1411
+ };
1412
+ }
1413
+
1414
+ function _findSctOid(rawDer) {
1415
+ // Cheap presence check — used by inspect() before ASN.1 walking.
1416
+ // OID 1.3.6.1.4.1.11129.2.4.2 = 06 0A 2B 06 01 04 01 D6 79 02 04 02.
1417
+ var oidBytes = Buffer.from([
1418
+ 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xd6, 0x79, 0x02, 0x04, 0x02,
1419
+ ]);
1420
+ return rawDer.indexOf(oidBytes) !== -1;
1421
+ }
1422
+
1423
+ var ct = Object.freeze({
1424
+ // inspect — quick presence check for the SCT extension.
1425
+ inspect: function (rawDer) {
1426
+ if (!Buffer.isBuffer(rawDer)) {
1427
+ throw new TlsTrustError("tls/ct-bad-input",
1428
+ "ct.inspect: rawDer must be a Buffer (cert.raw)");
1429
+ }
1430
+ return {
1431
+ hasSctExtension: _findSctOid(rawDer),
1432
+ rawLength: rawDer.length,
1433
+ };
1434
+ },
1435
+ // parseScts — full ASN.1 walk + SCT-list parse. Returns
1436
+ // [{ version, logIdHex, timestamp, signature, ... }, ...] or [] when
1437
+ // no SCT extension is present.
1438
+ parseScts: function (rawDer) {
1439
+ if (!Buffer.isBuffer(rawDer)) {
1440
+ throw new TlsTrustError("tls/ct-bad-input",
1441
+ "ct.parseScts: rawDer must be a Buffer");
1442
+ }
1443
+ var ext = _extractSctExtensionFromCert(rawDer);
1444
+ if (!ext.sctListRaw) return [];
1445
+ return _parseSctList(ext.sctListRaw);
1446
+ },
1447
+ // verifyScts — full RFC 6962 verification. opts.logKeys maps
1448
+ // log_id (hex SHA-256 of the log's pubkey) → PEM public key.
1449
+ // Operators populate from the Chrome CT log list. Returns
1450
+ // { ok, verifiedCount, totalScts, scts: [{ logIdHex, verified, ... }] }.
1451
+ verifyScts: verifyScts,
1452
+ // Operator middleware predicate: refuse a peer cert lacking SCT
1453
+ // verification. Composes verifyScts under the hood.
1454
+ requireScts: function (opts) {
1455
+ opts = opts || {};
1456
+ return function (peerCert) {
1457
+ if (!peerCert || !peerCert.raw) {
1458
+ return new TlsTrustError("tls/ct-no-cert",
1459
+ "requireScts: peer cert.raw missing");
1460
+ }
1461
+ var rv = verifyScts(peerCert.raw, opts);
1462
+ if (!rv.ok) {
1463
+ // Map verifier reason → operator-facing error code so call
1464
+ // sites can distinguish "no SCT extension at all" from
1465
+ // "extension present but verification short of minScts".
1466
+ var code = "tls/ct-not-verified";
1467
+ if (rv.reason === "no-sct-extension") code = "tls/ct-no-sct-extension";
1468
+ else if (rv.reason === "insufficient-verified") code = "tls/ct-insufficient-verified";
1469
+ return new TlsTrustError(code,
1470
+ "SCT verification failed: " + (rv.reason || "unknown") +
1471
+ " (" + rv.verifiedCount + "/" + rv.totalScts + " verified)");
1472
+ }
1473
+ return null;
1474
+ };
1475
+ },
1476
+ });
1477
+
311
1478
  module.exports = {
312
1479
  addCa: addCa,
313
1480
  addCaBundle: addCaBundle,
@@ -323,6 +1490,8 @@ module.exports = {
323
1490
  detectBaselineDrift: detectBaselineDrift,
324
1491
  applyToContext: applyToContext,
325
1492
  getCaPems: getCaPems,
1493
+ ocsp: ocsp,
1494
+ ct: ct,
326
1495
  TlsTrustError: TlsTrustError,
327
1496
  _resetForTest: _resetForTest,
328
1497
  };