@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.
- package/CHANGELOG.md +30 -0
- package/index.js +4 -0
- package/lib/asn1-der.js +356 -0
- package/lib/audit.js +2 -0
- package/lib/compliance.js +114 -0
- package/lib/constants.js +8 -0
- package/lib/crypto.js +92 -0
- package/lib/dora.js +347 -0
- package/lib/framework-error.js +22 -0
- package/lib/gate-contract.js +20 -4
- package/lib/mail-auth.js +661 -0
- package/lib/mail-dkim.js +309 -4
- package/lib/mail.js +8 -0
- package/lib/network-smtp-policy.js +551 -0
- package/lib/network-tls.js +1169 -0
- package/lib/network.js +7 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/lib/network-tls.js
CHANGED
|
@@ -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
|
};
|