@blamejs/blamejs-shop 0.4.85 → 0.4.86

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/lib/asset-manifest.json +1 -1
  3. package/lib/order.js +4 -2
  4. package/lib/security-middleware.js +1 -0
  5. package/lib/vendor/MANIFEST.json +71 -39
  6. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  7. package/lib/vendor/blamejs/api-snapshot.json +11 -3
  8. package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
  9. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
  10. package/lib/vendor/blamejs/lib/auth/password.js +1 -0
  11. package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
  12. package/lib/vendor/blamejs/lib/daemon.js +4 -1
  13. package/lib/vendor/blamejs/lib/external-db.js +131 -0
  14. package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
  15. package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
  16. package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
  17. package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
  18. package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
  19. package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
  20. package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
  21. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
  22. package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
  23. package/lib/vendor/blamejs/lib/mail.js +39 -0
  24. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
  25. package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
  26. package/lib/vendor/blamejs/lib/network-dns.js +47 -2
  27. package/lib/vendor/blamejs/lib/network-nts.js +16 -0
  28. package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
  29. package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
  30. package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
  31. package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
  32. package/lib/vendor/blamejs/lib/outbox.js +29 -0
  33. package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
  34. package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
  35. package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
  36. package/lib/vendor/blamejs/lib/session.js +85 -28
  37. package/lib/vendor/blamejs/package.json +1 -1
  38. package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
  39. package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
  40. package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
  41. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
  42. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
  43. package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
  44. package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
  45. package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
  46. package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
  47. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
  48. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
  49. package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
  50. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
  53. package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
  55. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
  57. package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
  58. package/lib/webhooks.js +38 -9
  59. package/package.json +1 -1
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /**
3
+ * b.auth.fidoMds3 — x5c cert validity-window must fail closed when the
4
+ * notBefore / notAfter dates are present but unparseable.
5
+ *
6
+ * _validateChain reads chain[v].validFrom / .validTo and feeds them to
7
+ * Date.parse. A cert whose encoded validity dates parse to NaN (e.g. a
8
+ * malformed / hand-forged DER) makes isFinite(notBefore) /
9
+ * isFinite(notAfter) false, which — pre-fix — SKIPS both the
10
+ * not-yet-valid and the expired checks, so the cert sails through the
11
+ * validity window unchecked. The verifier must instead refuse with
12
+ * fido-mds3/cert-bad-validity.
13
+ *
14
+ * The test drives the real fetch() consumer path with a mocked
15
+ * httpClient serving a genuine RS256-signed BLOB whose self-signed leaf
16
+ * is also the operator-supplied caCertificate, so absent the validity
17
+ * bug the chain anchors and the JWS verifies — the validity-window
18
+ * check is the only gate left. node:crypto's X509Certificate is
19
+ * overridden so the parsed leaf reports an unparseable validFrom while
20
+ * every other cert behaviour (verify / checkIssued / publicKey /
21
+ * fingerprint256) stays real.
22
+ */
23
+
24
+ var nodeCrypto = require("node:crypto");
25
+ var helpers = require("../helpers");
26
+ var check = helpers.check;
27
+
28
+ // Mint an RSA self-signed cert + matching private-key PEM via the
29
+ // vendored pki bundle, mirroring the sibling fido-mds3 test fixture.
30
+ async function _makeSelfSignedRsaCert() {
31
+ var pki = require("../../lib/vendor/pki.cjs");
32
+ var x509 = pki.x509;
33
+ var keys = await nodeCrypto.webcrypto.subtle.generateKey(
34
+ { name: "RSASSA-PKCS1-v1_5",
35
+ modulusLength: 2048,
36
+ publicExponent: new Uint8Array([1, 0, 1]),
37
+ hash: "SHA-256" },
38
+ true, ["sign", "verify"]);
39
+ var now = new Date();
40
+ var notAfter = new Date(now.getTime() + 7 * 86400000);
41
+ var cert = await x509.X509CertificateGenerator.createSelfSigned({
42
+ serialNumber: "01",
43
+ name: "CN=blamejs-mds3-test-root",
44
+ notBefore: now,
45
+ notAfter: notAfter,
46
+ signingAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
47
+ keys: keys,
48
+ extensions: [
49
+ new x509.BasicConstraintsExtension(true, 0, true),
50
+ new x509.KeyUsagesExtension(
51
+ x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.digitalSignature,
52
+ true),
53
+ ],
54
+ });
55
+ var pkcs8 = await nodeCrypto.webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
56
+ var pkB64 = Buffer.from(pkcs8).toString("base64").match(/.{1,64}/g).join("\n");
57
+ var keyPem = "-----BEGIN PRIVATE KEY-----\n" + pkB64 + "\n-----END PRIVATE KEY-----\n";
58
+ return { keyPem: keyPem, certPem: cert.toString("pem") };
59
+ }
60
+
61
+ function _b64url(buf) {
62
+ return Buffer.from(buf).toString("base64url");
63
+ }
64
+
65
+ function _makeBlob(payload, keyPem, certPem) {
66
+ var derB64 = certPem
67
+ .replace(/-----BEGIN CERTIFICATE-----/g, "")
68
+ .replace(/-----END CERTIFICATE-----/g, "")
69
+ .replace(/\s+/g, "");
70
+ var header = { alg: "RS256", typ: "JWT", x5c: [derB64] };
71
+ var headerB64 = _b64url(JSON.stringify(header));
72
+ var payloadB64 = _b64url(JSON.stringify(payload));
73
+ var signingInput = headerB64 + "." + payloadB64;
74
+ var sig = nodeCrypto.sign("sha256", Buffer.from(signingInput, "ascii"), {
75
+ key: keyPem,
76
+ padding: nodeCrypto.constants.RSA_PKCS1_PADDING,
77
+ });
78
+ return signingInput + "." + _b64url(sig);
79
+ }
80
+
81
+ function _futureDateString(daysFromNow) {
82
+ var d = new Date(Date.now() + daysFromNow * 86400000);
83
+ return d.toISOString().slice(0, 10);
84
+ }
85
+
86
+ // Make every freshly-parsed X509Certificate report an unparseable
87
+ // validFrom (Date.parse -> NaN) while keeping all real cert behaviour.
88
+ async function _withUnparseableValidFrom(fn) {
89
+ var realX509 = nodeCrypto.X509Certificate;
90
+ function Wrapped(arg) {
91
+ var c = new realX509(arg);
92
+ Object.defineProperty(c, "validFrom", {
93
+ configurable: true,
94
+ get: function () { return "GARBAGE-NOT-A-DATE"; },
95
+ });
96
+ return c;
97
+ }
98
+ nodeCrypto.X509Certificate = Wrapped;
99
+ try { return await fn(); }
100
+ finally { nodeCrypto.X509Certificate = realX509; }
101
+ }
102
+
103
+ async function run() {
104
+ var pair = await _makeSelfSignedRsaCert();
105
+ var payload = {
106
+ legalHeader: "Test BLOB",
107
+ no: 1,
108
+ nextUpdate: _futureDateString(7),
109
+ entries: [{
110
+ aaguid: "01234567-89ab-cdef-0123-456789abcdef",
111
+ metadataStatement: { description: "Test entry" },
112
+ statusReports: [{ status: "FIDO_CERTIFIED_L2" }],
113
+ }],
114
+ };
115
+ var token = _makeBlob(payload, pair.keyPem, pair.certPem);
116
+
117
+ // Mock httpClient.request via require-cache override (sibling pattern).
118
+ var hcPath = require.resolve("../../lib/http-client");
119
+ var origHc = require.cache[hcPath].exports;
120
+ require.cache[hcPath].exports = Object.assign({}, origHc, {
121
+ request: async function () {
122
+ return { statusCode: 200, headers: {}, body: Buffer.from(token, "ascii") };
123
+ },
124
+ });
125
+
126
+ // Re-require the verifier so it captures both the mocked httpClient and
127
+ // the wrapped X509Certificate through its module-level references.
128
+ var fmPath = require.resolve("../../lib/auth/fido-mds3");
129
+ delete require.cache[fmPath];
130
+ var fm = require(fmPath);
131
+
132
+ var threw = null;
133
+ try {
134
+ await _withUnparseableValidFrom(async function () {
135
+ await fm.fetch({
136
+ url: "https://test.invalid/mds3",
137
+ caCertificate: pair.certPem,
138
+ force: true,
139
+ });
140
+ });
141
+ } catch (e) {
142
+ threw = e;
143
+ } finally {
144
+ require.cache[hcPath].exports = origHc;
145
+ delete require.cache[fmPath];
146
+ }
147
+
148
+ check("fetch rejects a cert with present-but-unparseable validity dates",
149
+ threw !== null);
150
+ check("rejection is fido-mds3/cert-bad-validity (fail closed, not silently accepted)",
151
+ threw && /cert-bad-validity/.test(threw.code || ""));
152
+ }
153
+
154
+ module.exports = { run: run };
155
+
156
+ if (require.main === module) {
157
+ run().then(function () { console.log("OK"); })
158
+ .catch(function (e) { console.error(e.stack || e); process.exit(1); });
159
+ }
@@ -38,14 +38,61 @@ async function run() {
38
38
 
39
39
  // nonceHeader override — the replay nonce can be read from a custom
40
40
  // request header (not just the Apollo-vendor default), for operators
41
- // fronting the gateway with a non-Apollo router.
41
+ // fronting the gateway with a non-Apollo router. The replay store speaks
42
+ // the framework's ATOMIC checkAndInsert(nonce, expireAt) contract.
42
43
  var withNonce = b.graphqlFederation.guardSdl({
43
44
  routerToken: "a".repeat(64),
44
- nonceStore: { has: function () { return false; }, remember: function () {} },
45
+ nonceStore: { checkAndInsert: function () { return true; } },
45
46
  nonceHeader: "x-my-nonce",
46
47
  });
47
48
  check("guardSdl: accepts custom nonceHeader", typeof withNonce === "function");
48
49
 
50
+ // #328 sibling — the replay nonceStore must speak the framework's ATOMIC
51
+ // checkAndInsert(nonce, expireAt) contract (was a racy non-atomic
52
+ // has()-then-remember() check-then-set). A store lacking checkAndInsert —
53
+ // including the legacy { has, remember } shape — is refused at config time.
54
+ var racyStoreThrew = null;
55
+ try {
56
+ b.graphqlFederation.guardSdl({
57
+ routerToken: "a".repeat(64),
58
+ nonceStore: { has: function () { return false; }, remember: function () {} },
59
+ });
60
+ } catch (e) { racyStoreThrew = e; }
61
+ check("guardSdl: refuses a nonceStore lacking checkAndInsert (legacy racy has/remember)",
62
+ racyStoreThrew && racyStoreThrew.code === "graphql-federation/bad-nonce-store");
63
+
64
+ // The framework's own b.nonceStore.create drops straight in, and a replay of
65
+ // the SAME nonce on a valid bearer _service probe is refused (401) — driving
66
+ // the REAL consumer path through the atomic store.
67
+ var nonceStore = b.nonceStore.create({ backend: "memory" });
68
+ var nguard = b.graphqlFederation.guardSdl({
69
+ routerToken: "a".repeat(64), nonceStore: nonceStore, audit: false,
70
+ });
71
+ function _driveNonce(nonce) {
72
+ return new Promise(function (resolve) {
73
+ var out = { status: null, nextCalled: false };
74
+ var res = {
75
+ statusCode: 200,
76
+ setHeader: function () {}, getHeader: function () { return undefined; },
77
+ writeHead: function (s) { out.status = s; },
78
+ end: function () { if (out.status === null) out.status = res.statusCode; resolve(out); },
79
+ };
80
+ nguard({
81
+ method: "POST",
82
+ headers: { authorization: "Bearer " + "a".repeat(64),
83
+ "x-apollographql-router-nonce": nonce },
84
+ body: { query: "{ _service { sdl } }" },
85
+ on: function () {},
86
+ }, res, function () { out.nextCalled = true; resolve(out); });
87
+ });
88
+ }
89
+ var freshNonce = await _driveNonce("nonce-aaaaaaaaaaaaaaaa");
90
+ check("guardSdl: fresh nonce on a valid bearer _service probe is forwarded",
91
+ freshNonce.nextCalled === true && freshNonce.status === null);
92
+ var replayNonce = await _driveNonce("nonce-aaaaaaaaaaaaaaaa");
93
+ check("guardSdl: a replayed nonce is refused (401), not forwarded",
94
+ replayNonce.status === 401 && replayNonce.nextCalled === false);
95
+
49
96
  // ---- guardSdl drives: a batched ARRAY body whose SECOND operation probes
50
97
  // _service must be refused (401) without a router token, not forwarded. ----
51
98
  function _drive(mw, body) {
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ /**
3
+ * b.mail.dmarc.evaluate — a DMARC record that lacks a valid required p=
4
+ * tag (RFC 7489 §6.3) must NOT recommend "deliver" for a failing/unaligned
5
+ * message.
6
+ *
7
+ * Regression: _parseDmarcRecord took p= / sp= verbatim and never required
8
+ * p=, so the disposition ternary mapped any policy.p that was not exactly
9
+ * "reject"/"quarantine" — including null (no p=) and typos ("rejct") —
10
+ * to recommendedAction "deliver". A spoofed sender with a
11
+ * failing SPF and no DKIM therefore got result:"fail" +
12
+ * recommendedAction:"deliver" (fail-open). The fix validates p=/sp= against
13
+ * {none,quarantine,reject}, requires a valid p= for the record to count,
14
+ * and fails closed (permerror, no "deliver") on an absent/invalid policy.
15
+ *
16
+ * Network-free: DNS is supplied via an operator dnsLookup mock.
17
+ */
18
+
19
+ var helpers = require("../helpers");
20
+ var b = helpers.b;
21
+ var check = helpers.check;
22
+
23
+ // Failing/unaligned auth for a spoofed sender: SPF fail, no DKIM, From a
24
+ // domain whose published DMARC record is the one under test.
25
+ var SPOOF = {
26
+ from: "ceo@victim.example",
27
+ spf: { result: "fail", domain: "attacker.invalid" },
28
+ dkim: [],
29
+ };
30
+
31
+ function dnsReturning(record) {
32
+ return async function (host) {
33
+ if (host === "_dmarc.victim.example") {
34
+ return [[record]];
35
+ }
36
+ var err = new Error("ENOTFOUND"); err.code = "ENOTFOUND"; throw err;
37
+ };
38
+ }
39
+
40
+ async function testNoPolicyTagFailsClosed() {
41
+ // RFC 7489 §6.3 — p= is REQUIRED. A record without it carries no usable
42
+ // policy; the receiver must treat it as none/permerror, NOT deliver.
43
+ var rv = await b.mail.dmarc.evaluate(Object.assign({}, SPOOF, {
44
+ dnsLookup: dnsReturning("v=DMARC1; adkim=s; aspf=s"),
45
+ }));
46
+ check("dmarc.evaluate: missing p= → NOT deliver",
47
+ rv.recommendedAction !== "deliver");
48
+ check("dmarc.evaluate: missing p= → permerror",
49
+ rv.result === "permerror");
50
+ }
51
+
52
+ async function testTypoPolicyTagFailsClosed() {
53
+ // "rejct" is not a recognized policy; must not silently fall through to
54
+ // deliver.
55
+ var rv = await b.mail.dmarc.evaluate(Object.assign({}, SPOOF, {
56
+ dnsLookup: dnsReturning("v=DMARC1; p=rejct"),
57
+ }));
58
+ check("dmarc.evaluate: typo p=rejct → NOT deliver",
59
+ rv.recommendedAction !== "deliver");
60
+ check("dmarc.evaluate: typo p=rejct → permerror",
61
+ rv.result === "permerror");
62
+ }
63
+
64
+ async function testWhitespacePaddedPolicyTag() {
65
+ // RFC 7489 §6.4 tag-list grammar permits whitespace around the value;
66
+ // "p= reject " is a legitimate reject policy once the grammar's trim
67
+ // applies — it must NOT deliver the spoofed/failing message.
68
+ var rv = await b.mail.dmarc.evaluate(Object.assign({}, SPOOF, {
69
+ dnsLookup: dnsReturning("v=DMARC1; p= reject "),
70
+ }));
71
+ check("dmarc.evaluate: whitespace-padded p= reject → NOT deliver",
72
+ rv.recommendedAction !== "deliver");
73
+ check("dmarc.evaluate: whitespace-padded p= reject → fail + reject",
74
+ rv.result === "fail" && rv.recommendedAction === "reject");
75
+ }
76
+
77
+ async function testBadSpTagFailsClosed() {
78
+ // sp= feeds policy.p on the organizational-domain path; an invalid sp=
79
+ // value must fail closed there too (validated like p=).
80
+ var policy;
81
+ var threw = false;
82
+ try {
83
+ policy = b.mail.dmarc.parseRecord("v=DMARC1; p=reject; sp=quarntine");
84
+ } catch (e) {
85
+ threw = true;
86
+ check("dmarc.parseRecord: bad sp= throws dmarcbis-bad-tag",
87
+ e && e.code === "mail-auth/dmarcbis-bad-tag");
88
+ }
89
+ check("dmarc.parseRecord: bad sp= rejected", threw === true && !policy);
90
+ }
91
+
92
+ async function testMissingPolicyParseThrows() {
93
+ // parseRecord must reject a record missing the required p= outright.
94
+ var threw = false;
95
+ try {
96
+ b.mail.dmarc.parseRecord("v=DMARC1; adkim=s");
97
+ } catch (e) {
98
+ threw = true;
99
+ check("dmarc.parseRecord: missing p= throws dmarc-missing-policy",
100
+ e && e.code === "mail-auth/dmarc-missing-policy");
101
+ }
102
+ check("dmarc.parseRecord: missing p= rejected", threw === true);
103
+ }
104
+
105
+ async function testValidRejectStillFailsClosed() {
106
+ // Control: a well-formed p=reject for the same spoofed/failing message
107
+ // still reaches the reject disposition (the fix doesn't over-broaden).
108
+ var rv = await b.mail.dmarc.evaluate(Object.assign({}, SPOOF, {
109
+ dnsLookup: dnsReturning("v=DMARC1; p=reject"),
110
+ }));
111
+ check("dmarc.evaluate: valid p=reject + spoof → fail + reject",
112
+ rv.result === "fail" && rv.recommendedAction === "reject");
113
+ }
114
+
115
+ async function testValidNoneDelivers() {
116
+ // Control: p=none is monitor-only → deliver is the correct disposition.
117
+ var rv = await b.mail.dmarc.evaluate(Object.assign({}, SPOOF, {
118
+ dnsLookup: dnsReturning("v=DMARC1; p=none"),
119
+ }));
120
+ check("dmarc.evaluate: valid p=none + spoof → fail + deliver (monitor)",
121
+ rv.result === "fail" && rv.recommendedAction === "deliver");
122
+ }
123
+
124
+ async function run() {
125
+ await testNoPolicyTagFailsClosed();
126
+ await testTypoPolicyTagFailsClosed();
127
+ await testWhitespacePaddedPolicyTag();
128
+ await testBadSpTagFailsClosed();
129
+ await testMissingPolicyParseThrows();
130
+ await testValidRejectStillFailsClosed();
131
+ await testValidNoneDelivers();
132
+ }
133
+
134
+ module.exports = { run: run };
135
+
136
+ if (require.main === module) {
137
+ run().then(function () { console.log("OK"); })
138
+ .catch(function (e) { console.error(e.stack || e); process.exit(1); });
139
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ /**
3
+ * b.mail.bimi — VMC / CMC certificate validity-window enforcement when the
4
+ * cert's notBefore / notAfter are PRESENT but UNPARSEABLE.
5
+ *
6
+ * _verifyCertChain reads `current.validFrom` / `current.validTo` and runs
7
+ * `Date.parse` to derive the validity window. A cert whose date strings
8
+ * Date.parse cannot interpret yields NaN. The window checks were guarded by
9
+ * `isFinite(...)`, so a NaN date SKIPPED both the not-yet-valid and the
10
+ * expired check and the cert validated — a present-but-unparseable validity
11
+ * window must FAIL CLOSED, not be waved through.
12
+ *
13
+ * This drives the real consumer path: b.mail.bimi.fetchAndVerifyMark with a
14
+ * self-signed test chain + stubbed httpClient. Date.parse is stubbed to
15
+ * return NaN for exactly the leaf cert's two date strings (simulating a cert
16
+ * whose ASN.1 time fields node surfaces as a string Date.parse rejects);
17
+ * every other Date.parse call is unaffected, so chain signature / issuer /
18
+ * SAN / EKU validation proceed normally and the ONLY failing signal is the
19
+ * unparseable window.
20
+ *
21
+ * Live network is NOT used.
22
+ */
23
+
24
+ var helpers = require("../helpers");
25
+ var b = helpers.b;
26
+ var check = helpers.check;
27
+
28
+ var pki = require("../../lib/vendor/pki.cjs");
29
+ var x509 = pki.x509;
30
+ var nodeCrypto = require("crypto");
31
+
32
+ async function _generateTestChain() {
33
+ var caKeys = await pki.crypto.subtle.generateKey(
34
+ { name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
35
+ var leafKeys = await pki.crypto.subtle.generateKey(
36
+ { name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
37
+
38
+ var now = new Date();
39
+ var notAfter = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000);
40
+
41
+ var ca = await x509.X509CertificateGenerator.createSelfSigned({
42
+ serialNumber: "01",
43
+ name: "CN=BIMI Test Root",
44
+ notBefore: now,
45
+ notAfter: notAfter,
46
+ signingAlgorithm: { name: "ECDSA", hash: "SHA-256" },
47
+ keys: caKeys,
48
+ extensions: [
49
+ new x509.BasicConstraintsExtension(true, 1, true),
50
+ new x509.KeyUsagesExtension(
51
+ x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
52
+ ],
53
+ });
54
+
55
+ var leaf = await x509.X509CertificateGenerator.create({
56
+ serialNumber: "02",
57
+ issuer: ca.subject,
58
+ subject: "CN=example.com",
59
+ notBefore: now,
60
+ notAfter: notAfter,
61
+ signingAlgorithm: { name: "ECDSA", hash: "SHA-256" },
62
+ publicKey: leafKeys.publicKey,
63
+ signingKey: caKeys.privateKey,
64
+ extensions: [
65
+ new x509.BasicConstraintsExtension(false, undefined, true),
66
+ new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true),
67
+ new x509.SubjectAlternativeNameExtension([
68
+ { type: "dns", value: "example.com" },
69
+ ]),
70
+ new x509.ExtendedKeyUsageExtension(["1.3.6.1.5.5.7.3.31"], false),
71
+ ],
72
+ });
73
+
74
+ return { rootPem: ca.toString("pem"), leafPem: leaf.toString("pem") };
75
+ }
76
+
77
+ function _stubHttpClient(body) {
78
+ return {
79
+ request: function () {
80
+ return Promise.resolve({
81
+ statusCode: 200,
82
+ headers: {},
83
+ body: Buffer.from(String(body), "utf8"),
84
+ });
85
+ },
86
+ };
87
+ }
88
+
89
+ async function testUnparseableValidityFailsClosed() {
90
+ var chain = await _generateTestChain();
91
+
92
+ // Surface the leaf's exact date strings the lib will feed to Date.parse,
93
+ // then stub Date.parse to return NaN for ONLY those two strings — every
94
+ // other Date.parse call (and the rest of chain validation) is untouched.
95
+ var leafCert = new nodeCrypto.X509Certificate(chain.leafPem);
96
+ var certDateStrings = [leafCert.validFrom, leafCert.validTo];
97
+ var origParse = Date.parse;
98
+ Date.parse = function (value) {
99
+ if (certDateStrings.indexOf(value) !== -1) return NaN;
100
+ return origParse.call(Date, value);
101
+ };
102
+
103
+ var threw = null;
104
+ var rv = null;
105
+ try {
106
+ rv = await b.mail.bimi.fetchAndVerifyMark({
107
+ domain: "example.com",
108
+ vmcUrl: "https://example.com/cert.pem",
109
+ trustAnchorsPem: chain.rootPem,
110
+ httpClient: _stubHttpClient(chain.leafPem),
111
+ });
112
+ } catch (e) {
113
+ threw = e;
114
+ } finally {
115
+ Date.parse = origParse;
116
+ }
117
+
118
+ // RED on the buggy tree: rv.ok === true (cert accepted despite an
119
+ // unparseable validity window). GREEN after the fix: rejected with
120
+ // bimi/vmc-chain-invalid and a "validity dates unparseable" reason.
121
+ check("fetchAndVerifyMark: present-but-unparseable cert validity is rejected",
122
+ rv === null && threw !== null &&
123
+ threw.code === "bimi/vmc-chain-invalid");
124
+ check("fetchAndVerifyMark: unparseable-validity rejection names the cause",
125
+ threw !== null && /unparseable/i.test(String(threw.message)));
126
+ }
127
+
128
+ async function run() {
129
+ await testUnparseableValidityFailsClosed();
130
+ }
131
+
132
+ module.exports = { run: run };
133
+
134
+ if (require.main === module) {
135
+ run().then(function () { console.log("OK"); })
136
+ .catch(function (e) { console.error(e.stack || e); process.exit(1); });
137
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /**
3
+ * b.mail.crypto.smime.checkCert — fail closed on unparseable validity.
4
+ *
5
+ * RFC 5280 §4.1.2.5 makes notBefore / notAfter mandatory; checkCert's
6
+ * validity-window guard refuses certs outside that window. The guard
7
+ * gated each comparison behind isFinite(Date.parse(...)), so a cert
8
+ * carrying a PRESENT-but-unparseable validFrom / validTo (Date.parse
9
+ * → NaN) silently skipped BOTH checks and passed validity. A signing
10
+ * cert whose dates a peer cannot parse must be refused at preflight,
11
+ * not accepted and left to fail interop later.
12
+ *
13
+ * This drives the real operator consumer path
14
+ * b.mail.crypto.smime.checkCert({ certPem }). node:crypto's
15
+ * X509Certificate always exposes parseable RFC date strings, so we
16
+ * stub the parser on the shared crypto module (require("node:crypto")
17
+ * === require("crypto"); lib reads X509Certificate off it) to present
18
+ * unparseable validity to the real checkCert logic.
19
+ *
20
+ * Run standalone: `node test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js`
21
+ * Or via smoke: `node test/smoke.js`
22
+ */
23
+ var helpers = require("../helpers");
24
+ var check = helpers.check;
25
+ var nodeCrypto = require("crypto");
26
+
27
+ var b = helpers.b;
28
+ var smime = b.mail.crypto.smime;
29
+
30
+ // ---- Stub: a cert that parses fine but carries unparseable dates ----
31
+ //
32
+ // Passes the sig-algorithm (sha256WithRSAEncryption) and RSA-bit
33
+ // (publicKey null → bit-floor check skipped) gates so execution
34
+ // reaches the validity window — the only thing under test here.
35
+ function _withUnparseableValidityCert(validFrom, validTo, fn) {
36
+ var Real = nodeCrypto.X509Certificate;
37
+ function StubCert(_input) {
38
+ return {
39
+ subject: "CN=signer.example",
40
+ issuer: "CN=signer.example",
41
+ signatureAlgorithm: "sha256WithRSAEncryption",
42
+ signatureAlgorithmOid: "1.2.840.113549.1.1.11",
43
+ publicKey: null,
44
+ fingerprint256: "AA:BB:CC",
45
+ validFrom: validFrom,
46
+ validTo: validTo,
47
+ };
48
+ }
49
+ nodeCrypto.X509Certificate = StubCert;
50
+ try {
51
+ return fn();
52
+ } finally {
53
+ nodeCrypto.X509Certificate = Real;
54
+ }
55
+ }
56
+
57
+ var DUMMY_PEM =
58
+ "-----BEGIN CERTIFICATE-----\nMIIBdummy\n-----END CERTIFICATE-----\n";
59
+
60
+ function testRejectsUnparseableValidity() {
61
+ // Both dates unparseable.
62
+ var threw = null, res = null;
63
+ _withUnparseableValidityCert("GARBAGE-NOT-A-DATE", "ALSO-GARBAGE", function () {
64
+ try { res = smime.checkCert({ certPem: DUMMY_PEM }); }
65
+ catch (e) { threw = e; }
66
+ });
67
+ check("checkCert refuses cert with unparseable validity (does not return)",
68
+ res === null);
69
+ check("checkCert throws on unparseable validity",
70
+ threw instanceof Error);
71
+ check("checkCert unparseable-validity uses the bad-validity code",
72
+ threw && threw.code === "mail-crypto/smime/bad-validity");
73
+ check("checkCert unparseable-validity message names the dates",
74
+ threw && /unparseable validity/.test(threw.message) &&
75
+ threw.message.indexOf("GARBAGE-NOT-A-DATE") !== -1);
76
+ }
77
+
78
+ function testRejectsUnparseableNotBeforeOnly() {
79
+ var threw = null, res = null;
80
+ // validTo parseable + in the future, validFrom unparseable — the
81
+ // old isFinite-gated notAfter check passes, so only fail-closed on
82
+ // the NaN notBefore catches this.
83
+ var future = new Date(Date.now() + 86400000).toUTCString();
84
+ _withUnparseableValidityCert("NOT-A-REAL-DATE", future, function () {
85
+ try { res = smime.checkCert({ certPem: DUMMY_PEM }); }
86
+ catch (e) { threw = e; }
87
+ });
88
+ check("checkCert refuses cert with unparseable notBefore only",
89
+ res === null && threw && threw.code === "mail-crypto/smime/bad-validity");
90
+ }
91
+
92
+ function testRejectsUnparseableNotAfterOnly() {
93
+ var threw = null, res = null;
94
+ var past = new Date(Date.now() - 86400000).toUTCString();
95
+ _withUnparseableValidityCert(past, "NOT-A-REAL-DATE", function () {
96
+ try { res = smime.checkCert({ certPem: DUMMY_PEM }); }
97
+ catch (e) { threw = e; }
98
+ });
99
+ check("checkCert refuses cert with unparseable notAfter only",
100
+ res === null && threw && threw.code === "mail-crypto/smime/bad-validity");
101
+ }
102
+
103
+ function testParseableValidityStillPasses() {
104
+ // Regression: a cert with parseable, in-window dates must NOT be
105
+ // tripped by the new fail-closed branch.
106
+ var threw = null, res = null;
107
+ var past = new Date(Date.now() - 86400000).toUTCString();
108
+ var future = new Date(Date.now() + 86400000).toUTCString();
109
+ _withUnparseableValidityCert(past, future, function () {
110
+ try { res = smime.checkCert({ certPem: DUMMY_PEM }); }
111
+ catch (e) { threw = e; }
112
+ });
113
+ check("checkCert accepts cert with parseable in-window validity",
114
+ threw === null && res && res.subject === "CN=signer.example");
115
+ }
116
+
117
+ function run() {
118
+ testRejectsUnparseableValidity();
119
+ testRejectsUnparseableNotBeforeOnly();
120
+ testRejectsUnparseableNotAfterOnly();
121
+ testParseableValidityStillPasses();
122
+ }
123
+
124
+ module.exports = { run: run };
125
+
126
+ if (require.main === module) {
127
+ try {
128
+ run();
129
+ console.log("OK — " + helpers.getChecks() + " checks passed");
130
+ } catch (e) {
131
+ console.error("FAIL:", e && e.stack || e);
132
+ process.exit(1);
133
+ }
134
+ }