@blamejs/core 0.7.24 → 0.7.38

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,968 @@ 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
+ var OID_RSA_SHA256 = "1.2.840.113549.1.1.11";
416
+ var OID_RSA_SHA384 = "1.2.840.113549.1.1.12";
417
+ var OID_RSA_SHA512 = "1.2.840.113549.1.1.13";
418
+ var OID_ECDSA_SHA256 = "1.2.840.10045.4.3.2";
419
+ var OID_ECDSA_SHA384 = "1.2.840.10045.4.3.3";
420
+ var OID_ECDSA_SHA512 = "1.2.840.10045.4.3.4";
421
+
422
+ function _parseTime(node) {
423
+ // Parse UTCTime ("YYMMDDhhmmssZ") or GeneralizedTime
424
+ // ("YYYYMMDDhhmmssZ") into ms-since-epoch.
425
+ var s = node.value.toString("ascii");
426
+ var year, month, day, hour, min, sec;
427
+ if (s.length === 13 && s.charAt(12) === "Z") { // allow:raw-byte-literal — UTCTime length per X.690
428
+ // UTCTime YYMMDDhhmmssZ — 50+ → 19xx, else 20xx (RFC 5280 §4.1.2.5).
429
+ year = parseInt(s.slice(0, 2), 10);
430
+ year += year >= 50 ? 1900 : 2000; // allow:raw-byte-literal allow:raw-time-literal — RFC 5280 century pivot, calendar years
431
+ month = parseInt(s.slice(2, 4), 10);
432
+ day = parseInt(s.slice(4, 6), 10);
433
+ hour = parseInt(s.slice(6, 8), 10); // allow:raw-byte-literal — UTCTime hour-byte offsets
434
+ min = parseInt(s.slice(8, 10), 10); // allow:raw-byte-literal — UTCTime minute-byte offsets
435
+ sec = parseInt(s.slice(10, 12), 10);
436
+ } else if (s.length >= 15 && s.charAt(s.length - 1) === "Z") { // allow:raw-byte-literal — GeneralizedTime length per X.690
437
+ // GeneralizedTime YYYYMMDDhhmmssZ.
438
+ year = parseInt(s.slice(0, 4), 10);
439
+ month = parseInt(s.slice(4, 6), 10);
440
+ day = parseInt(s.slice(6, 8), 10); // allow:raw-byte-literal — GeneralizedTime day-byte offsets
441
+ hour = parseInt(s.slice(8, 10), 10); // allow:raw-byte-literal — GeneralizedTime hour-byte offsets
442
+ min = parseInt(s.slice(10, 12), 10);
443
+ sec = parseInt(s.slice(12, 14), 10);
444
+ } else {
445
+ throw new TlsTrustError("tls/ocsp-bad-time",
446
+ "OCSP time field is not UTCTime or GeneralizedTime: " + JSON.stringify(s));
447
+ }
448
+ return Date.UTC(year, month - 1, day, hour, min, sec);
449
+ }
450
+
451
+ var OCSP_RESPONSE_STATUS = {
452
+ 0: "successful",
453
+ 1: "malformedRequest",
454
+ 2: "internalError",
455
+ 3: "tryLater",
456
+ // 4 reserved
457
+ 5: "sigRequired",
458
+ 6: "unauthorized",
459
+ };
460
+
461
+ function parseOcspResponse(der) {
462
+ if (!Buffer.isBuffer(der) || der.length === 0) {
463
+ throw new TlsTrustError("tls/ocsp-bad-input",
464
+ "parseOcspResponse: expected non-empty Buffer");
465
+ }
466
+ var top = asn1.readNode(der); // OCSPResponse SEQUENCE
467
+ if (top.tag !== asn1.TAG.SEQUENCE) {
468
+ throw new TlsTrustError("tls/ocsp-bad-shape", "OCSPResponse is not a SEQUENCE");
469
+ }
470
+ var topChildren = asn1.readSequence(top.value);
471
+ if (topChildren.length === 0) {
472
+ throw new TlsTrustError("tls/ocsp-bad-shape", "OCSPResponse has no responseStatus");
473
+ }
474
+ var statusInt = asn1.readUnsignedInt(topChildren[0]);
475
+ var status = OCSP_RESPONSE_STATUS[statusInt] || ("unknown:" + statusInt);
476
+ if (status !== "successful") {
477
+ return { status: status };
478
+ }
479
+ // responseBytes [0] EXPLICIT ResponseBytes
480
+ if (topChildren.length < 2) {
481
+ throw new TlsTrustError("tls/ocsp-bad-shape",
482
+ "successful OCSP response missing responseBytes");
483
+ }
484
+ var responseBytes = asn1.unwrapExplicit(topChildren[1], 0); // [0] EXPLICIT
485
+ if (responseBytes.tag !== asn1.TAG.SEQUENCE) {
486
+ throw new TlsTrustError("tls/ocsp-bad-shape", "responseBytes is not a SEQUENCE");
487
+ }
488
+ var rbChildren = asn1.readSequence(responseBytes.value);
489
+ if (rbChildren.length < 2) {
490
+ throw new TlsTrustError("tls/ocsp-bad-shape",
491
+ "responseBytes missing responseType or response");
492
+ }
493
+ var responseTypeOid = asn1.readOid(rbChildren[0]);
494
+ if (responseTypeOid !== OID_BASIC_OCSP_RESPONSE) {
495
+ throw new TlsTrustError("tls/ocsp-unsupported-response-type",
496
+ "OCSP responseType is not id-pkix-ocsp-basic: " + responseTypeOid);
497
+ }
498
+ // The OCTET STRING wraps a DER BasicOCSPResponse.
499
+ var basicDer = asn1.readOctetString(rbChildren[1]);
500
+ var basic = asn1.readNode(basicDer);
501
+ if (basic.tag !== asn1.TAG.SEQUENCE) {
502
+ throw new TlsTrustError("tls/ocsp-bad-shape",
503
+ "BasicOCSPResponse is not a SEQUENCE");
504
+ }
505
+ var basicChildren = asn1.readSequence(basic.value);
506
+ if (basicChildren.length < 3) { // allow:raw-byte-literal — minimum BasicOCSPResponse fields (tbs + alg + sig)
507
+ throw new TlsTrustError("tls/ocsp-bad-shape",
508
+ "BasicOCSPResponse needs tbsResponseData + signatureAlgorithm + signature");
509
+ }
510
+ var tbsNode = basicChildren[0];
511
+ var sigAlgChildren = asn1.readSequence(basicChildren[1].value);
512
+ var sigAlgOid = asn1.readOid(sigAlgChildren[0]);
513
+ var signatureBytes = asn1.readBitString(basicChildren[2]);
514
+
515
+ // Slice the tbsResponseData bytes (header + value) — that's what the
516
+ // signature covers per RFC 6960 §4.2.1. tbsResponseData is the FIRST
517
+ // child of BasicOCSPResponse; its bytes start at basic.valueStart
518
+ // within the raw basicDer buffer (offset 0).
519
+ var basicValueStart = basicDer.length - basic.value.length;
520
+ var tbsDer = basicDer.slice(basicValueStart, basicValueStart + tbsNode.totalLength);
521
+
522
+ // Walk responseData (SEQUENCE) for the per-cert responses.
523
+ var rdChildren = asn1.readSequence(tbsNode.value);
524
+ // Find the SEQUENCE of SingleResponse — it's the LAST SEQUENCE before
525
+ // optional [1] EXPLICIT extensions. Per RFC 6960:
526
+ // ResponseData ::= SEQUENCE {
527
+ // version [0] EXPLICIT Version DEFAULT v1,
528
+ // responderID ResponderID,
529
+ // producedAt GeneralizedTime,
530
+ // responses SEQUENCE OF SingleResponse,
531
+ // responseExtensions [1] EXPLICIT Extensions OPTIONAL
532
+ // }
533
+ // ResponderID is itself a CHOICE (byName [1] / byKey [2]), then a
534
+ // GeneralizedTime, then the responses SEQUENCE-OF.
535
+ var responsesNode = null;
536
+ for (var rdi = rdChildren.length - 1; rdi >= 0; rdi -= 1) {
537
+ var ch = rdChildren[rdi];
538
+ if (ch.tag === asn1.TAG.SEQUENCE && ch.tagClass === asn1.TAG_CLASS.UNIVERSAL) {
539
+ responsesNode = ch;
540
+ break;
541
+ }
542
+ }
543
+ if (!responsesNode) {
544
+ throw new TlsTrustError("tls/ocsp-bad-shape",
545
+ "ResponseData missing responses SEQUENCE OF");
546
+ }
547
+ var singleResponses = asn1.readSequence(responsesNode.value);
548
+ var responses = [];
549
+ for (var sri = 0; sri < singleResponses.length; sri += 1) {
550
+ var sr = asn1.readSequence(singleResponses[sri].value);
551
+ if (sr.length < 3) continue; // allow:raw-byte-literal — minimum SingleResponse fields
552
+ // sr[0] = certID SEQUENCE, sr[1] = certStatus CHOICE, sr[2] = thisUpdate.
553
+ var certIdChildren = asn1.readSequence(sr[0].value);
554
+ // certID = SEQUENCE { hashAlgorithm, issuerNameHash, issuerKeyHash, serialNumber }
555
+ var serialHex = certIdChildren.length >= 4
556
+ ? certIdChildren[3].value.toString("hex")
557
+ : null;
558
+ var certStatus;
559
+ var statusNode = sr[1];
560
+ if (statusNode.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC) {
561
+ certStatus = statusNode.tag === 0 ? "good" :
562
+ statusNode.tag === 1 ? "revoked" :
563
+ statusNode.tag === 2 ? "unknown" : "unknown";
564
+ } else if (statusNode.tag === asn1.TAG.NULL) {
565
+ certStatus = "good";
566
+ } else {
567
+ certStatus = "unknown";
568
+ }
569
+ var thisUpdate = _parseTime(sr[2]);
570
+ var nextUpdate = null;
571
+ if (sr.length >= 4 && sr[3].tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && sr[3].tag === 0) {
572
+ nextUpdate = _parseTime(asn1.readNode(sr[3].value, 0));
573
+ }
574
+ responses.push({
575
+ certIdSerialHex: serialHex,
576
+ certStatus: certStatus,
577
+ thisUpdate: thisUpdate,
578
+ nextUpdate: nextUpdate,
579
+ });
580
+ }
581
+
582
+ return {
583
+ status: status,
584
+ basic: {
585
+ tbsResponseDataDer: tbsDer,
586
+ signatureAlgorithmOid: sigAlgOid,
587
+ signature: signatureBytes,
588
+ responses: responses,
589
+ },
590
+ };
591
+ }
592
+
593
+ function _verifyOcspSignature(parsed, issuerPem) {
594
+ if (!parsed || !parsed.basic) {
595
+ throw new TlsTrustError("tls/ocsp-not-successful",
596
+ "OCSP response status is not 'successful' (got " +
597
+ (parsed && parsed.status) + ")");
598
+ }
599
+ var algOid = parsed.basic.signatureAlgorithmOid;
600
+ var nodeAlgo = algOid === OID_RSA_SHA256 ? "sha256" :
601
+ algOid === OID_RSA_SHA384 ? "sha384" :
602
+ algOid === OID_RSA_SHA512 ? "sha512" :
603
+ algOid === OID_ECDSA_SHA256 ? "sha256" :
604
+ algOid === OID_ECDSA_SHA384 ? "sha384" :
605
+ algOid === OID_ECDSA_SHA512 ? "sha512" : null;
606
+ if (nodeAlgo === null) {
607
+ throw new TlsTrustError("tls/ocsp-unsupported-sig-alg",
608
+ "OCSP signatureAlgorithm OID '" + algOid + "' is not supported by the verifier");
609
+ }
610
+ var keyObj;
611
+ try { keyObj = nodeCrypto.createPublicKey(issuerPem); }
612
+ catch (e) {
613
+ throw new TlsTrustError("tls/ocsp-bad-issuer-key",
614
+ "issuer public key parse failed: " + ((e && e.message) || String(e)));
615
+ }
616
+ // ECDSA OCSP signatures use DER-encoded ECDSA-Sig-Value (the ASN.1
617
+ // shape that node:crypto.verify accepts by default — no dsaEncoding
618
+ // option needed).
619
+ var verified;
620
+ try {
621
+ verified = nodeCrypto.verify(nodeAlgo, parsed.basic.tbsResponseDataDer, keyObj,
622
+ parsed.basic.signature);
623
+ } catch (e) {
624
+ throw new TlsTrustError("tls/ocsp-verify-threw",
625
+ "OCSP signature verify threw: " + ((e && e.message) || String(e)));
626
+ }
627
+ return verified;
628
+ }
629
+
630
+ // Operator-side OCSP response evaluator. Takes the DER bytes (from
631
+ // `ocsp.requireStapled` or any other source) plus the issuer cert PEM
632
+ // and returns a structured outcome:
633
+ // { ok, status, certStatus, thisUpdate, nextUpdate, signatureValid, errors }
634
+ function evaluateOcspResponse(ocspDer, opts) {
635
+ opts = opts || {};
636
+ var issuerPem = opts.issuerPem;
637
+ if (!issuerPem) {
638
+ throw new TlsTrustError("tls/ocsp-missing-issuer",
639
+ "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)");
640
+ }
641
+ var parsed;
642
+ try { parsed = parseOcspResponse(ocspDer); }
643
+ catch (e) {
644
+ return { ok: false, status: "parse-error",
645
+ errors: [(e && e.message) || String(e)] };
646
+ }
647
+ if (parsed.status !== "successful") {
648
+ return { ok: false, status: parsed.status, errors: ["responseStatus=" + parsed.status] };
649
+ }
650
+ var sigOk = false;
651
+ try { sigOk = _verifyOcspSignature(parsed, issuerPem); }
652
+ catch (e) {
653
+ return { ok: false, status: parsed.status,
654
+ signatureValid: false,
655
+ errors: [(e && e.message) || String(e)] };
656
+ }
657
+ if (!sigOk) {
658
+ return { ok: false, status: parsed.status, signatureValid: false,
659
+ errors: ["OCSP signature did not verify against the issuer key"] };
660
+ }
661
+ // Look up the requested cert serial in the responses; "good" wins.
662
+ var serial = opts.serialHex || (parsed.basic.responses[0] && parsed.basic.responses[0].certIdSerialHex);
663
+ var match = null;
664
+ for (var i = 0; i < parsed.basic.responses.length; i += 1) {
665
+ var r = parsed.basic.responses[i];
666
+ if (!serial || r.certIdSerialHex === serial) { match = r; break; }
667
+ }
668
+ if (!match) {
669
+ return { ok: false, status: parsed.status, signatureValid: true,
670
+ errors: ["OCSP response has no entry for the requested cert serial"] };
671
+ }
672
+ return {
673
+ ok: match.certStatus === "good",
674
+ status: parsed.status,
675
+ certStatus: match.certStatus,
676
+ thisUpdate: match.thisUpdate,
677
+ nextUpdate: match.nextUpdate,
678
+ signatureValid: true,
679
+ errors: match.certStatus === "good" ? [] :
680
+ ["certStatus=" + match.certStatus],
681
+ };
682
+ }
683
+
684
+ var ocsp = Object.freeze({
685
+ // Connect with OCSP requested. Returns { authorized, ocspBytes,
686
+ // peerCert }. requireStapled: true makes empty / not-stapled responses
687
+ // refuse instead of resolve. NOTE: requireStapled does NOT verify the
688
+ // OCSP response signature — pair it with evaluateOcspResponse(bytes,
689
+ // { issuerPem }) for full verification, OR use requireGood below.
690
+ connect: function (opts) {
691
+ return _connectAndCheckOcsp(opts || {}, false);
692
+ },
693
+ requireStapled: function (opts) {
694
+ return _connectAndCheckOcsp(opts || {}, true);
695
+ },
696
+ // requireGood: connect + parse + verify signature + check certStatus.
697
+ // Operator passes opts.issuerPem (the cert that signed the OCSP
698
+ // response — typically the leaf's CA OR a delegated OCSP responder
699
+ // cert). Throws TlsTrustError on any failure (no-staple, parse error,
700
+ // signature mismatch, certStatus=revoked/unknown).
701
+ requireGood: async function (opts) {
702
+ opts = opts || {};
703
+ if (!opts.issuerPem) {
704
+ throw new TlsTrustError("tls/ocsp-missing-issuer",
705
+ "ocsp.requireGood requires opts.issuerPem (PEM of the OCSP-signing cert)");
706
+ }
707
+ var rv = await _connectAndCheckOcsp(opts, true);
708
+ if (!rv.ocspBytes || rv.ocspBytes.length === 0) {
709
+ throw new TlsTrustError("tls/ocsp-empty",
710
+ "OCSP response was empty");
711
+ }
712
+ var evald = evaluateOcspResponse(rv.ocspBytes, {
713
+ issuerPem: opts.issuerPem,
714
+ serialHex: opts.serialHex || null,
715
+ });
716
+ if (!evald.ok) {
717
+ throw new TlsTrustError("tls/ocsp-not-good",
718
+ "OCSP evaluation failed: " + evald.errors.join("; "));
719
+ }
720
+ return Object.assign({}, rv, { ocspEvaluation: evald });
721
+ },
722
+ parseResponse: parseOcspResponse,
723
+ evaluate: evaluateOcspResponse,
724
+ // inspectMustStaple — read the RFC 7633 TLS Feature extension on a
725
+ // peer cert. Returns { mustStaple, features }. mustStaple === true
726
+ // when status_request (5) is in the feature list; the cert is then
727
+ // contractually required to ship an OCSP staple on every connection.
728
+ inspectMustStaple: function (rawDer) {
729
+ if (!Buffer.isBuffer(rawDer)) {
730
+ throw new TlsTrustError("tls/ocsp-bad-input",
731
+ "ocsp.inspectMustStaple: rawDer must be a Buffer (cert.raw)");
732
+ }
733
+ return _extractTlsFeatureExtensionFromCert(rawDer);
734
+ },
735
+ // requireMustStaple(peerCert, opts) — operator predicate. Refuses
736
+ // when the cert advertises must-staple but no OCSP staple was
737
+ // delivered (opts.ocspBytes empty/missing). When the cert does NOT
738
+ // advertise must-staple, the predicate returns null (operator opted
739
+ // in by setting opts.enforceUnconditional to also require staples
740
+ // on certs that don't carry the extension).
741
+ requireMustStaple: function (opts) {
742
+ opts = opts || {};
743
+ var enforceUnconditional = opts.enforceUnconditional === true;
744
+ return function (peerCert, ctx) {
745
+ if (!peerCert || !peerCert.raw) {
746
+ return new TlsTrustError("tls/ocsp-no-cert",
747
+ "requireMustStaple: peer cert.raw missing");
748
+ }
749
+ var feat = _extractTlsFeatureExtensionFromCert(peerCert.raw);
750
+ var stapled = ctx && Buffer.isBuffer(ctx.ocspBytes) && ctx.ocspBytes.length > 0;
751
+ if (feat.mustStaple && !stapled) {
752
+ return new TlsTrustError("tls/ocsp-must-staple-violated",
753
+ "cert advertises must-staple (RFC 7633) but no OCSP staple was delivered");
754
+ }
755
+ if (!feat.mustStaple && enforceUnconditional && !stapled) {
756
+ return new TlsTrustError("tls/ocsp-staple-required",
757
+ "operator policy requires OCSP staple but server did not provide one");
758
+ }
759
+ return null;
760
+ };
761
+ },
762
+ });
763
+
764
+ // ---- Certificate Transparency (RFC 6962 + RFC 9162) SCT verifier --
765
+ //
766
+ // CT requires every TLS server certificate to carry at least 2 Signed
767
+ // Certificate Timestamps (SCTs) from approved logs. Modern browsers
768
+ // (Chrome / Safari) refuse certificates without sufficient SCTs.
769
+ //
770
+ // node:tls surfaces SCTs via TLSSocket.getPeerX509Certificate() →
771
+ // X509Certificate.raw (the DER cert). The SCTs sit inside the cert as
772
+ // the OCSP-aware extension OID 1.3.6.1.4.1.11129.2.4.2.
773
+ //
774
+ // b.network.tls.ct.verify(cert, opts) checks that the cert has at
775
+ // least `minScts` SCTs and that each SCT references a log in
776
+ // `approvedLogs`. Full SCT-signature verification against the log's
777
+ // pubkey is OUT of scope for this patch — that requires log-pubkey
778
+ // distribution + ASN.1 SCT parsing. The framework provides the
779
+ // SCT-presence + log-id check; signature verification is a follow-up
780
+ // when the ASN.1 dependency lands.
781
+
782
+ // SCT extension OID per RFC 6962 §3.3.
783
+ var OID_CT_SCT_LIST = "1.3.6.1.4.1.11129.2.4.2";
784
+
785
+ // Walk a DER X.509 cert and locate the SCT extension's OCTET STRING
786
+ // content. Returns { sctListRaw } or { sctListRaw: null } when no SCT
787
+ // extension is present.
788
+ function _extractSctExtensionFromCert(certDer) {
789
+ // Tolerant of malformed cert buffers — return null sctListRaw when
790
+ // the ASN.1 walk fails. Callers (parseScts / verifyScts) treat that
791
+ // as "no SCT extension" rather than throwing on broken input.
792
+ var top;
793
+ try { top = asn1.readNode(certDer); }
794
+ catch (_e) { return { sctListRaw: null }; }
795
+ if (top.tag !== asn1.TAG.SEQUENCE) return { sctListRaw: null };
796
+ var children;
797
+ try { children = asn1.readSequence(top.value); }
798
+ catch (_e) { return { sctListRaw: null }; }
799
+ if (children.length === 0) return { sctListRaw: null };
800
+ // Cert ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signature }
801
+ var tbs = children[0];
802
+ if (tbs.tag !== asn1.TAG.SEQUENCE) return { sctListRaw: null };
803
+ // tbsCertificate ::= SEQUENCE { ..., extensions [3] EXPLICIT ... }
804
+ var tbsChildren;
805
+ try { tbsChildren = asn1.readSequence(tbs.value); }
806
+ catch (_e) { return { sctListRaw: null }; }
807
+ var extensionsNode = null;
808
+ for (var i = 0; i < tbsChildren.length; i += 1) {
809
+ var ch = tbsChildren[i];
810
+ if (ch.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && ch.tag === 3) { // allow:raw-byte-literal — X.509 [3] EXPLICIT extensions tag
811
+ extensionsNode = asn1.readNode(ch.value, 0);
812
+ break;
813
+ }
814
+ }
815
+ if (!extensionsNode || extensionsNode.tag !== asn1.TAG.SEQUENCE) {
816
+ return { sctListRaw: null };
817
+ }
818
+ var extensions = asn1.readSequence(extensionsNode.value);
819
+ for (var e = 0; e < extensions.length; e += 1) {
820
+ var ext = extensions[e]; // Extension ::= SEQUENCE { extnID OID, critical BOOL OPTIONAL, extnValue OCTET STRING }
821
+ if (ext.tag !== asn1.TAG.SEQUENCE) continue;
822
+ var extChildren = asn1.readSequence(ext.value);
823
+ if (extChildren.length === 0) continue;
824
+ var extOid = asn1.readOid(extChildren[0]);
825
+ if (extOid !== OID_CT_SCT_LIST) continue;
826
+ // The last child is the OCTET STRING extnValue. Per RFC 6962 §3.3
827
+ // that OCTET STRING wraps a SECOND OCTET STRING which contains the
828
+ // raw SignedCertificateTimestampList (TLS-encoded).
829
+ var extnValueOuter = asn1.readOctetString(extChildren[extChildren.length - 1]);
830
+ var inner = asn1.readNode(extnValueOuter);
831
+ if (inner.tag !== asn1.TAG.OCTET_STRING) {
832
+ throw new TlsTrustError("tls/ct-bad-extension",
833
+ "SCT extension extnValue does not wrap a second OCTET STRING");
834
+ }
835
+ return { sctListRaw: inner.value };
836
+ }
837
+ return { sctListRaw: null };
838
+ }
839
+
840
+ // TLS Feature extension OID per RFC 7633 §6. The extension value is
841
+ // SEQUENCE OF INTEGER; the integer 5 == status_request == "must-staple".
842
+ var OID_TLS_FEATURE = "1.3.6.1.5.5.7.1.24";
843
+ var TLS_FEATURE_STATUS_REQUEST = 5;
844
+
845
+ // Walk a DER X.509 cert and return the TLS Feature extension's
846
+ // integer list. Returns { mustStaple, features }. Tolerant of
847
+ // malformed cert input — mirrors _extractSctExtensionFromCert's
848
+ // try/catch tolerance.
849
+ function _extractTlsFeatureExtensionFromCert(certDer) {
850
+ var none = { mustStaple: false, features: [] };
851
+ var top;
852
+ try { top = asn1.readNode(certDer); }
853
+ catch (_e) { return none; }
854
+ if (top.tag !== asn1.TAG.SEQUENCE) return none;
855
+ var children;
856
+ try { children = asn1.readSequence(top.value); }
857
+ catch (_e) { return none; }
858
+ if (children.length === 0) return none;
859
+ var tbs = children[0];
860
+ if (tbs.tag !== asn1.TAG.SEQUENCE) return none;
861
+ var tbsChildren;
862
+ try { tbsChildren = asn1.readSequence(tbs.value); }
863
+ catch (_e) { return none; }
864
+ var extensionsNode = null;
865
+ for (var i = 0; i < tbsChildren.length; i += 1) {
866
+ var ch = tbsChildren[i];
867
+ if (ch.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && ch.tag === 3) { // allow:raw-byte-literal — X.509 [3] EXPLICIT extensions tag
868
+ extensionsNode = asn1.readNode(ch.value, 0);
869
+ break;
870
+ }
871
+ }
872
+ if (!extensionsNode || extensionsNode.tag !== asn1.TAG.SEQUENCE) return none;
873
+ var extensions = asn1.readSequence(extensionsNode.value);
874
+ for (var e = 0; e < extensions.length; e += 1) {
875
+ var ext = extensions[e];
876
+ if (ext.tag !== asn1.TAG.SEQUENCE) continue;
877
+ var extChildren = asn1.readSequence(ext.value);
878
+ if (extChildren.length === 0) continue;
879
+ var extOid;
880
+ try { extOid = asn1.readOid(extChildren[0]); }
881
+ catch (_e2) { continue; }
882
+ if (extOid !== OID_TLS_FEATURE) continue;
883
+ var extnValue = asn1.readOctetString(extChildren[extChildren.length - 1]);
884
+ // extnValue wraps SEQUENCE OF INTEGER.
885
+ var seq;
886
+ try { seq = asn1.readNode(extnValue); }
887
+ catch (_e3) { return none; }
888
+ if (seq.tag !== asn1.TAG.SEQUENCE) return none;
889
+ var feats = asn1.readSequence(seq.value);
890
+ var ints = [];
891
+ var mustStaple = false;
892
+ for (var f = 0; f < feats.length; f += 1) {
893
+ try {
894
+ var n = asn1.readUnsignedInt(feats[f]);
895
+ ints.push(n);
896
+ if (n === TLS_FEATURE_STATUS_REQUEST) mustStaple = true;
897
+ } catch (_e4) { /* ignore non-integer entries */ }
898
+ }
899
+ return { mustStaple: mustStaple, features: ints };
900
+ }
901
+ return none;
902
+ }
903
+
904
+ // Parse the TLS-encoded SignedCertificateTimestampList (RFC 6962 §3.3).
905
+ // Format: 2-byte length + concatenation of individual SCTs, each
906
+ // itself prefixed by a 2-byte length.
907
+ function _parseSctList(sctListRaw) {
908
+ if (!Buffer.isBuffer(sctListRaw) || sctListRaw.length < 2) { // allow:raw-byte-literal — outer 2-byte length prefix
909
+ throw new TlsTrustError("tls/ct-bad-list",
910
+ "SCT list shorter than the outer length prefix");
911
+ }
912
+ var totalLen = sctListRaw.readUInt16BE(0);
913
+ if (totalLen + 2 !== sctListRaw.length) { // allow:raw-byte-literal — outer length prefix
914
+ throw new TlsTrustError("tls/ct-bad-list",
915
+ "SCT list outer length " + totalLen + " does not match buffer " +
916
+ (sctListRaw.length - 2));
917
+ }
918
+ var pos = 2; // allow:raw-byte-literal — past the outer prefix
919
+ var scts = [];
920
+ while (pos < sctListRaw.length) {
921
+ var sctLen = sctListRaw.readUInt16BE(pos);
922
+ pos += 2;
923
+ if (pos + sctLen > sctListRaw.length) {
924
+ throw new TlsTrustError("tls/ct-bad-list",
925
+ "SCT[" + scts.length + "] declared length " + sctLen +
926
+ " extends past the list buffer");
927
+ }
928
+ var sctBytes = sctListRaw.slice(pos, pos + sctLen);
929
+ scts.push(_parseSct(sctBytes));
930
+ pos += sctLen;
931
+ }
932
+ return scts;
933
+ }
934
+
935
+ // Per RFC 6962 §3.2 — a single SCT:
936
+ // sct_version (1 byte) — 0 = v1
937
+ // id (LogID) (32 bytes) — SHA-256 of log's pubkey
938
+ // timestamp (8 bytes) — uint64 ms since epoch
939
+ // ct_extensions (2-byte len + N) — usually empty
940
+ // signature DigitallySigned (hash + sig algo + 2-byte len + N)
941
+ function _parseSct(sctBuf) {
942
+ if (sctBuf.length < 1 + 32 + 8 + 2 + 4) { // allow:raw-byte-literal — minimum SCT v1 byte total
943
+ throw new TlsTrustError("tls/ct-sct-too-short",
944
+ "SCT is shorter than the minimum v1 layout (" + sctBuf.length + " bytes)");
945
+ }
946
+ var version = sctBuf[0];
947
+ if (version !== 0) {
948
+ throw new TlsTrustError("tls/ct-sct-bad-version",
949
+ "SCT version is not 0 (v1): got " + version);
950
+ }
951
+ var logId = sctBuf.slice(1, 1 + 32); // allow:raw-byte-literal — RFC 6962 32-byte LogID
952
+ var timestamp = Number(sctBuf.readBigUInt64BE(1 + 32)); // allow:raw-byte-literal — past LogID
953
+ var extLen = sctBuf.readUInt16BE(1 + 32 + 8); // allow:raw-byte-literal — past LogID + timestamp
954
+ var pos = 1 + 32 + 8 + 2; // allow:raw-byte-literal — past extLen field
955
+ var extensions = sctBuf.slice(pos, pos + extLen);
956
+ pos += extLen;
957
+ if (pos + 4 > sctBuf.length) { // allow:raw-byte-literal — DigitallySigned header (hash + alg + len)
958
+ throw new TlsTrustError("tls/ct-sct-truncated",
959
+ "SCT truncated before DigitallySigned");
960
+ }
961
+ var hashAlgo = sctBuf[pos];
962
+ var sigAlgo = sctBuf[pos + 1];
963
+ pos += 2; // allow:raw-byte-literal — past hash+alg pair
964
+ var sigLen = sctBuf.readUInt16BE(pos);
965
+ pos += 2; // allow:raw-byte-literal — past sig length
966
+ if (pos + sigLen !== sctBuf.length) {
967
+ throw new TlsTrustError("tls/ct-sct-truncated",
968
+ "SCT signature length " + sigLen + " does not match remaining bytes " +
969
+ (sctBuf.length - pos));
970
+ }
971
+ var signature = sctBuf.slice(pos, pos + sigLen);
972
+ return {
973
+ version: version,
974
+ logId: logId,
975
+ logIdHex: logId.toString("hex"),
976
+ timestamp: timestamp,
977
+ extensions: extensions,
978
+ hashAlgo: hashAlgo, // RFC 5246 HashAlgorithm enum (4=sha256, 5=sha384, 6=sha512)
979
+ sigAlgo: sigAlgo, // RFC 5246 SignatureAlgorithm enum (1=rsa, 3=ecdsa)
980
+ signature: signature,
981
+ };
982
+ }
983
+
984
+ // Build the canonical signed-entry per RFC 6962 §3.2 for X.509
985
+ // pre-cert-free chains (issued cert path):
986
+ // sct_version (1) || signature_type (1=certificate_timestamp) ||
987
+ // timestamp (8) || entry_type (0=x509_entry) ||
988
+ // signed_entry (3-byte length || ASN.1 cert without SCT extension) ||
989
+ // ct_extensions (2-byte length || N)
990
+ function _buildSctSignedEntry(certWithoutSctDer, sct) {
991
+ var head = Buffer.alloc(1 + 1 + 8 + 2); // allow:raw-byte-literal — fixed-shape header bytes
992
+ head[0] = sct.version;
993
+ head[1] = 0; // signature_type = certificate_timestamp
994
+ head.writeBigUInt64BE(BigInt(sct.timestamp), 2); // allow:raw-byte-literal — past version+sig-type
995
+ head.writeUInt16BE(0, 10); // allow:raw-byte-literal — entry_type = x509_entry (2 bytes; high byte = 0, low byte = 0)
996
+ // signed_entry: 3-byte length prefix + cert DER.
997
+ var lenBytes = Buffer.alloc(3); // allow:raw-byte-literal — RFC 6962 24-bit length prefix
998
+ lenBytes[0] = (certWithoutSctDer.length >> 16) & 0xff; // allow:raw-byte-literal — base-256 length high byte
999
+ lenBytes[1] = (certWithoutSctDer.length >> 8) & 0xff; // allow:raw-byte-literal — base-256 length mid byte
1000
+ lenBytes[2] = certWithoutSctDer.length & 0xff; // allow:raw-byte-literal — base-256 length low byte
1001
+ // ct_extensions: 2-byte length + bytes.
1002
+ var extHead = Buffer.alloc(2); // allow:raw-byte-literal — RFC 6962 2-byte ct_extensions length prefix
1003
+ extHead.writeUInt16BE(sct.extensions.length, 0);
1004
+ return Buffer.concat([head, lenBytes, certWithoutSctDer, extHead, sct.extensions]);
1005
+ }
1006
+
1007
+ // Strip the SCT extension from a DER cert + return the rebuilt cert
1008
+ // bytes for SCT signing per RFC 6962 §3.2. The strip is byte-precise:
1009
+ // walk the TBSCertificate extensions list, drop the SCT extension,
1010
+ // and re-encode just enough of the chain to reproduce the original
1011
+ // shape minus that one extension. This is non-trivial because the
1012
+ // tbsCertificate length, certificate length, and signature-bytes
1013
+ // boundaries all shift.
1014
+ //
1015
+ // Simpler: rebuild only the tbsCertificate extensions SEQUENCE without
1016
+ // the SCT entry, recompute lengths above it, and replace the cert's
1017
+ // SignedCertificate (BIT STRING) with the original's signature too —
1018
+ // but that's incorrect since the original signature was computed over
1019
+ // the WITH-SCT TBS. The CT log signed an entry built from the
1020
+ // without-SCT pre-issuance shape, NOT the issued cert's tbs.
1021
+ //
1022
+ // Per RFC 6962 §3.1, log servers receive a "TBSCertificate" minus the
1023
+ // SCT extension from the CA. The signed_entry the framework
1024
+ // reconstructs is that pre-extension TBSCertificate. We compute it by
1025
+ // removing the SCT extension at the byte level and rebuilding all
1026
+ // outer length prefixes.
1027
+ function _stripSctExtensionFromCert(certDer) {
1028
+ var top = asn1.readNode(certDer);
1029
+ if (top.tag !== asn1.TAG.SEQUENCE) {
1030
+ throw new TlsTrustError("tls/ct-bad-cert", "Certificate is not a SEQUENCE");
1031
+ }
1032
+ var topChildren = asn1.readSequence(top.value);
1033
+ var tbs = topChildren[0];
1034
+ if (tbs.tag !== asn1.TAG.SEQUENCE) {
1035
+ throw new TlsTrustError("tls/ct-bad-cert", "tbsCertificate is not a SEQUENCE");
1036
+ }
1037
+ // Walk tbsCertificate to find the [3] EXPLICIT extensions wrapper.
1038
+ var tbsChildren = asn1.readSequence(tbs.value);
1039
+ var newTbsChildrenBytes = [];
1040
+ var foundExtensions = false;
1041
+ for (var i = 0; i < tbsChildren.length; i += 1) {
1042
+ var ch = tbsChildren[i];
1043
+ if (ch.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && ch.tag === 3) { // allow:raw-byte-literal — [3] EXPLICIT extensions tag
1044
+ foundExtensions = true;
1045
+ // Inner SEQUENCE OF Extensions.
1046
+ var inner = asn1.readNode(ch.value, 0);
1047
+ var extList = asn1.readSequence(inner.value);
1048
+ var keptExtBytes = [];
1049
+ for (var j = 0; j < extList.length; j += 1) {
1050
+ var ext = extList[j];
1051
+ var extBytes = ext.value;
1052
+ var extDescChildren = asn1.readSequence(ext.value);
1053
+ if (extDescChildren.length > 0) {
1054
+ try {
1055
+ var oid = asn1.readOid(extDescChildren[0]);
1056
+ if (oid === OID_CT_SCT_LIST) continue; // drop the SCT extension
1057
+ } catch (_e) { /* not an OID — keep the extension as-is */ }
1058
+ }
1059
+ // Re-encode this extension verbatim (we have the original bytes).
1060
+ var origExt = certDer.slice(0, 0); // placeholder; we rebuild from the parsed node below
1061
+ void origExt;
1062
+ keptExtBytes.push(_encodeAsn1(asn1.TAG.SEQUENCE, true, extBytes));
1063
+ void extBytes;
1064
+ }
1065
+ var newExtSeq = _encodeAsn1(asn1.TAG.SEQUENCE, true, Buffer.concat(keptExtBytes));
1066
+ var newExplicit3 = _encodeContextExplicit(3, newExtSeq);
1067
+ newTbsChildrenBytes.push(newExplicit3);
1068
+ } else {
1069
+ // Re-encode the original child verbatim by slicing its bytes from
1070
+ // the parent's value buffer.
1071
+ var childDer = _encodeAsn1FromNode(ch);
1072
+ newTbsChildrenBytes.push(childDer);
1073
+ }
1074
+ }
1075
+ if (!foundExtensions) {
1076
+ // Cert has no extensions at all — caller's SCT lookup would have
1077
+ // returned no SCT bytes, so this path shouldn't run. Surface anyway.
1078
+ throw new TlsTrustError("tls/ct-no-extensions",
1079
+ "cert has no extensions to strip from");
1080
+ }
1081
+ var newTbsValue = Buffer.concat(newTbsChildrenBytes);
1082
+ var newTbs = _encodeAsn1(asn1.TAG.SEQUENCE, true, newTbsValue);
1083
+ return newTbs;
1084
+ }
1085
+
1086
+ // Minimal DER encoder helpers — enough to rebuild a TBS without the
1087
+ // SCT extension. Tag class is universal for SEQUENCE; constructed
1088
+ // flag wired explicitly.
1089
+ function _encodeLength(len) {
1090
+ if (len < 0x80) return Buffer.from([len]); // allow:raw-byte-literal — DER short-form length threshold
1091
+ var tmp = [];
1092
+ var n = len;
1093
+ while (n > 0) {
1094
+ tmp.unshift(n & 0xff); // allow:raw-byte-literal — base-256 byte
1095
+ n = n >>> 8; // allow:raw-byte-literal — byte shift
1096
+ }
1097
+ return Buffer.concat([Buffer.from([0x80 | tmp.length]), Buffer.from(tmp)]); // allow:raw-byte-literal — DER long-form length flag
1098
+ }
1099
+ function _encodeAsn1(tag, constructed, value) {
1100
+ var tagByte = (constructed ? 0x20 : 0x00) | tag; // allow:raw-byte-literal — DER constructed bit + universal tag
1101
+ return Buffer.concat([Buffer.from([tagByte]), _encodeLength(value.length), value]);
1102
+ }
1103
+ function _encodeContextExplicit(num, value) {
1104
+ // Context-specific class (10) + constructed (20) | tag.
1105
+ var tagByte = 0xa0 | num; // allow:raw-byte-literal — DER context-specific + constructed
1106
+ return Buffer.concat([Buffer.from([tagByte]), _encodeLength(value.length), value]);
1107
+ }
1108
+ function _encodeAsn1FromNode(node) {
1109
+ // Re-encode a parsed node verbatim by replaying the tag + length +
1110
+ // value. Universal-class shortcut: if class is universal, set the
1111
+ // tag byte from the universal table; if constructed, set the bit.
1112
+ // Context-specific / application / private classes get their bytes
1113
+ // restored directly. This works for the simple shapes we walk.
1114
+ var tagByte;
1115
+ if (node.tagClass === asn1.TAG_CLASS.UNIVERSAL) {
1116
+ tagByte = (node.constructed ? 0x20 : 0x00) | (node.tag & 0x1f); // allow:raw-byte-literal — DER constructed bit + universal tag
1117
+ } else {
1118
+ var classBits = (node.tagClass & 0x03) << 6; // allow:raw-byte-literal — DER tag-class bits
1119
+ tagByte = classBits | (node.constructed ? 0x20 : 0x00) | (node.tag & 0x1f); // allow:raw-byte-literal — DER constructed bit + low-tag
1120
+ }
1121
+ return Buffer.concat([Buffer.from([tagByte]), _encodeLength(node.value.length), node.value]);
1122
+ }
1123
+
1124
+ // SCT signature verification per RFC 6962 §3.2. opts.logKeys maps
1125
+ // log_id (hex) → PEM public key. Operators populate from the Chrome
1126
+ // CT log list (https://www.gstatic.com/ct/log_list/v3/log_list.json
1127
+ // or equivalent) — log keys rotate, so the framework does NOT bake
1128
+ // them in; that drift is the operator's to manage.
1129
+ function verifyScts(certDer, opts) {
1130
+ opts = opts || {};
1131
+ if (!Buffer.isBuffer(certDer)) {
1132
+ throw new TlsTrustError("tls/ct-bad-input",
1133
+ "verifyScts: certDer must be a Buffer");
1134
+ }
1135
+ var logKeys = opts.logKeys || {};
1136
+ var minScts = typeof opts.minScts === "number" ? opts.minScts : 2; // allow:raw-byte-literal — Chrome CT policy min-2-SCTs
1137
+ var ext = _extractSctExtensionFromCert(certDer);
1138
+ if (!ext.sctListRaw) {
1139
+ return { ok: false, reason: "no-sct-extension", scts: [] };
1140
+ }
1141
+ var scts;
1142
+ try { scts = _parseSctList(ext.sctListRaw); }
1143
+ catch (e) {
1144
+ return { ok: false, reason: "parse-error",
1145
+ error: (e && e.message) || String(e), scts: [] };
1146
+ }
1147
+ // Strip the SCT extension to compute the signed-entry per §3.2.
1148
+ var stripped;
1149
+ try { stripped = _stripSctExtensionFromCert(certDer); }
1150
+ catch (e) {
1151
+ return { ok: false, reason: "strip-failed",
1152
+ error: (e && e.message) || String(e), scts: scts };
1153
+ }
1154
+ var verifiedCount = 0;
1155
+ var perSctResults = [];
1156
+ for (var s = 0; s < scts.length; s += 1) {
1157
+ var sct = scts[s];
1158
+ var pem = logKeys[sct.logIdHex];
1159
+ if (!pem) {
1160
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: false,
1161
+ reason: "log-key-missing" });
1162
+ continue;
1163
+ }
1164
+ var signedEntry;
1165
+ try { signedEntry = _buildSctSignedEntry(stripped, sct); }
1166
+ catch (e) {
1167
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: false,
1168
+ reason: "build-entry-failed",
1169
+ error: (e && e.message) || String(e) });
1170
+ continue;
1171
+ }
1172
+ var nodeAlgo = sct.hashAlgo === 4 ? "sha256" : // allow:raw-byte-literal — TLS 1.2 HashAlgorithm enum sha256
1173
+ sct.hashAlgo === 5 ? "sha384" : // allow:raw-byte-literal — TLS 1.2 HashAlgorithm enum sha384
1174
+ sct.hashAlgo === 6 ? "sha512" : // allow:raw-byte-literal — TLS 1.2 HashAlgorithm enum sha512
1175
+ null;
1176
+ if (nodeAlgo === null) {
1177
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: false,
1178
+ reason: "unsupported-hash-algo", hashAlgo: sct.hashAlgo });
1179
+ continue;
1180
+ }
1181
+ var keyObj;
1182
+ try { keyObj = nodeCrypto.createPublicKey(pem); }
1183
+ catch (e) {
1184
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: false,
1185
+ reason: "log-key-parse-failed",
1186
+ error: (e && e.message) || String(e) });
1187
+ continue;
1188
+ }
1189
+ var verified;
1190
+ try { verified = nodeCrypto.verify(nodeAlgo, signedEntry, keyObj, sct.signature); }
1191
+ catch (e) {
1192
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: false,
1193
+ reason: "verify-threw",
1194
+ error: (e && e.message) || String(e) });
1195
+ continue;
1196
+ }
1197
+ perSctResults.push({ logIdHex: sct.logIdHex, verified: verified });
1198
+ if (verified) verifiedCount += 1;
1199
+ }
1200
+ return {
1201
+ ok: verifiedCount >= minScts,
1202
+ reason: verifiedCount >= minScts ? null : "insufficient-verified",
1203
+ minScts: minScts,
1204
+ verifiedCount: verifiedCount,
1205
+ totalScts: scts.length,
1206
+ scts: perSctResults,
1207
+ };
1208
+ }
1209
+
1210
+ function _findSctOid(rawDer) {
1211
+ // Cheap presence check — used by inspect() before ASN.1 walking.
1212
+ // OID 1.3.6.1.4.1.11129.2.4.2 = 06 0A 2B 06 01 04 01 D6 79 02 04 02.
1213
+ var oidBytes = Buffer.from([
1214
+ 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xd6, 0x79, 0x02, 0x04, 0x02,
1215
+ ]);
1216
+ return rawDer.indexOf(oidBytes) !== -1;
1217
+ }
1218
+
1219
+ var ct = Object.freeze({
1220
+ // inspect — quick presence check for the SCT extension.
1221
+ inspect: function (rawDer) {
1222
+ if (!Buffer.isBuffer(rawDer)) {
1223
+ throw new TlsTrustError("tls/ct-bad-input",
1224
+ "ct.inspect: rawDer must be a Buffer (cert.raw)");
1225
+ }
1226
+ return {
1227
+ hasSctExtension: _findSctOid(rawDer),
1228
+ rawLength: rawDer.length,
1229
+ };
1230
+ },
1231
+ // parseScts — full ASN.1 walk + SCT-list parse. Returns
1232
+ // [{ version, logIdHex, timestamp, signature, ... }, ...] or [] when
1233
+ // no SCT extension is present.
1234
+ parseScts: function (rawDer) {
1235
+ if (!Buffer.isBuffer(rawDer)) {
1236
+ throw new TlsTrustError("tls/ct-bad-input",
1237
+ "ct.parseScts: rawDer must be a Buffer");
1238
+ }
1239
+ var ext = _extractSctExtensionFromCert(rawDer);
1240
+ if (!ext.sctListRaw) return [];
1241
+ return _parseSctList(ext.sctListRaw);
1242
+ },
1243
+ // verifyScts — full RFC 6962 verification. opts.logKeys maps
1244
+ // log_id (hex SHA-256 of the log's pubkey) → PEM public key.
1245
+ // Operators populate from the Chrome CT log list. Returns
1246
+ // { ok, verifiedCount, totalScts, scts: [{ logIdHex, verified, ... }] }.
1247
+ verifyScts: verifyScts,
1248
+ // Operator middleware predicate: refuse a peer cert lacking SCT
1249
+ // verification. Composes verifyScts under the hood.
1250
+ requireScts: function (opts) {
1251
+ opts = opts || {};
1252
+ return function (peerCert) {
1253
+ if (!peerCert || !peerCert.raw) {
1254
+ return new TlsTrustError("tls/ct-no-cert",
1255
+ "requireScts: peer cert.raw missing");
1256
+ }
1257
+ var rv = verifyScts(peerCert.raw, opts);
1258
+ if (!rv.ok) {
1259
+ // Map verifier reason → operator-facing error code so call
1260
+ // sites can distinguish "no SCT extension at all" from
1261
+ // "extension present but verification short of minScts".
1262
+ var code = "tls/ct-not-verified";
1263
+ if (rv.reason === "no-sct-extension") code = "tls/ct-no-sct-extension";
1264
+ else if (rv.reason === "insufficient-verified") code = "tls/ct-insufficient-verified";
1265
+ return new TlsTrustError(code,
1266
+ "SCT verification failed: " + (rv.reason || "unknown") +
1267
+ " (" + rv.verifiedCount + "/" + rv.totalScts + " verified)");
1268
+ }
1269
+ return null;
1270
+ };
1271
+ },
1272
+ });
1273
+
311
1274
  module.exports = {
312
1275
  addCa: addCa,
313
1276
  addCaBundle: addCaBundle,
@@ -323,6 +1286,8 @@ module.exports = {
323
1286
  detectBaselineDrift: detectBaselineDrift,
324
1287
  applyToContext: applyToContext,
325
1288
  getCaPems: getCaPems,
1289
+ ocsp: ocsp,
1290
+ ct: ct,
326
1291
  TlsTrustError: TlsTrustError,
327
1292
  _resetForTest: _resetForTest,
328
1293
  };