@blamejs/blamejs-shop 0.4.85 → 0.4.87
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 +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/cart.js +75 -0
- package/lib/order.js +4 -2
- package/lib/security-middleware.js +1 -0
- package/lib/storefront.js +8 -1
- package/lib/vendor/MANIFEST.json +71 -39
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +11 -3
- package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
- package/lib/vendor/blamejs/lib/auth/password.js +1 -0
- package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
- package/lib/vendor/blamejs/lib/daemon.js +4 -1
- package/lib/vendor/blamejs/lib/external-db.js +131 -0
- package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
- package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
- package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
- package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
- package/lib/vendor/blamejs/lib/mail.js +39 -0
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
- package/lib/vendor/blamejs/lib/network-dns.js +47 -2
- package/lib/vendor/blamejs/lib/network-nts.js +16 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
- package/lib/vendor/blamejs/lib/outbox.js +29 -0
- package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
- package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
- package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
- package/lib/vendor/blamejs/lib/session.js +85 -28
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
- package/lib/webhooks.js +38 -9
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.mail.dkim.verify — x= / t= present-but-unparseable must FAIL CLOSED.
|
|
4
|
+
*
|
|
5
|
+
* RFC 6376 §3.5 — x= (expiration) and t= (timestamp) are NumericDate
|
|
6
|
+
* (digits-only seconds-since-epoch). The prior code parsed each with
|
|
7
|
+
* parseInt() guarded by isFinite(): a present-but-unparseable value
|
|
8
|
+
* (x=abc, x=, t=oops) yielded NaN, isFinite() was false, and the WHOLE
|
|
9
|
+
* expiry / future-date / ordering check SILENTLY SKIPPED — verification
|
|
10
|
+
* then proceeded to result:"pass" with no expiry enforced. An attacker
|
|
11
|
+
* replaying a message could neuter x= expiry by corrupting the tag.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors the SAML Conditions present-but-unparseable fail-closed fix:
|
|
14
|
+
* when x= / t= is PRESENT it MUST parse as a finite NumericDate, else
|
|
15
|
+
* verify() returns result:"permerror".
|
|
16
|
+
*
|
|
17
|
+
* Drives the real consumer path b.mail.dkim.verify (not an internal
|
|
18
|
+
* helper). The x=/t= checks run BEFORE the DNS key fetch + signature
|
|
19
|
+
* verify, so a spliced tag over an otherwise-valid signature reaches
|
|
20
|
+
* them regardless of the (mocked) key.
|
|
21
|
+
*
|
|
22
|
+
* Run standalone: `node test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js`
|
|
23
|
+
* Or via smoke: `node test/smoke.js`
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
var helpers = require("../helpers");
|
|
27
|
+
var b = helpers.b;
|
|
28
|
+
var check = helpers.check;
|
|
29
|
+
var nodeCrypto = require("crypto");
|
|
30
|
+
|
|
31
|
+
function _rsaKeypair() {
|
|
32
|
+
return nodeCrypto.generateKeyPairSync("rsa", {
|
|
33
|
+
modulusLength: 2048,
|
|
34
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
35
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _spkiPemToB64(pem) {
|
|
40
|
+
return pem.replace(/-----[A-Z ]+-----/g, "").replace(/\s+/g, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _signedMessage(keypair) {
|
|
44
|
+
var signer = b.mail.dkim.create({
|
|
45
|
+
domain: "example.com",
|
|
46
|
+
selector: "s1",
|
|
47
|
+
privateKey: keypair.privateKey,
|
|
48
|
+
});
|
|
49
|
+
var rfc822 =
|
|
50
|
+
"From: alice@example.com\r\n" +
|
|
51
|
+
"To: bob@example.org\r\n" +
|
|
52
|
+
"Subject: Test\r\n" +
|
|
53
|
+
"Date: Mon, 5 May 2026 12:00:00 +0000\r\n" +
|
|
54
|
+
"Message-ID: <abc@example.com>\r\n" +
|
|
55
|
+
"\r\n" +
|
|
56
|
+
"Hello world.\r\n";
|
|
57
|
+
return signer.sign(rfc822);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Splice a tag (e.g. "x=abc") into the DKIM-Signature header value, right
|
|
61
|
+
// after the "v=1" prefix, leaving the rest of the (valid) signature intact.
|
|
62
|
+
function _spliceTag(signed, tag) {
|
|
63
|
+
return signed.replace("DKIM-Signature: v=1", "DKIM-Signature: v=1; " + tag);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function _verify(signed, kp) {
|
|
67
|
+
b.mail.dkim._resetDkimKeyCacheForTest();
|
|
68
|
+
var b64 = _spkiPemToB64(kp.publicKey);
|
|
69
|
+
var dnsLookup = async function () { return [["v=DKIM1; k=rsa; p=" + b64]]; };
|
|
70
|
+
return b.mail.dkim.verify(signed, { dnsLookup: dnsLookup });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function testDkimXTagUnparseableFailsClosed() {
|
|
74
|
+
var kp = _rsaKeypair();
|
|
75
|
+
var signed = await _signedMessage(kp);
|
|
76
|
+
|
|
77
|
+
var rvAbc = await _verify(_spliceTag(signed, "x=abc"), kp);
|
|
78
|
+
check("verify: x=abc → permerror (not pass)",
|
|
79
|
+
rvAbc[0] && rvAbc[0].result === "permerror");
|
|
80
|
+
check("verify: x=abc error names present-but-unparseable",
|
|
81
|
+
rvAbc[0] && /x=.*unparseable/i.test((rvAbc[0].errors || []).join(" ")));
|
|
82
|
+
|
|
83
|
+
var rvEmpty = await _verify(_spliceTag(signed, "x="), kp);
|
|
84
|
+
check("verify: x= (empty) → permerror (not pass)",
|
|
85
|
+
rvEmpty[0] && rvEmpty[0].result === "permerror");
|
|
86
|
+
|
|
87
|
+
var rvLenient = await _verify(_spliceTag(signed, "x=12abc"), kp);
|
|
88
|
+
check("verify: x=12abc (parseInt-lenient) → permerror (not pass)",
|
|
89
|
+
rvLenient[0] && rvLenient[0].result === "permerror");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function testDkimLTagUnparseableFailsClosed() {
|
|
93
|
+
// Sibling of the same root: l= (body-length cap, §3.5 unsigned integer).
|
|
94
|
+
// A present-but-unparseable l= previously fell through to lcap=undefined,
|
|
95
|
+
// silently hashing the WHOLE body instead of refusing the malformed tag.
|
|
96
|
+
var kp = _rsaKeypair();
|
|
97
|
+
var signed = await _signedMessage(kp);
|
|
98
|
+
|
|
99
|
+
var rv = await _verify(_spliceTag(signed, "l=abc"), kp);
|
|
100
|
+
check("verify: l=abc → permerror (not fall-through)",
|
|
101
|
+
rv[0] && rv[0].result === "permerror");
|
|
102
|
+
check("verify: l=abc error names present-but-unparseable",
|
|
103
|
+
rv[0] && /l=.*unparseable/i.test((rv[0].errors || []).join(" ")));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function testDkimTTagUnparseableFailsClosed() {
|
|
107
|
+
var kp = _rsaKeypair();
|
|
108
|
+
var signed = await _signedMessage(kp);
|
|
109
|
+
|
|
110
|
+
var rv = await _verify(_spliceTag(signed, "t=oops"), kp);
|
|
111
|
+
check("verify: t=oops → permerror (not pass)",
|
|
112
|
+
rv[0] && rv[0].result === "permerror");
|
|
113
|
+
check("verify: t=oops error names present-but-unparseable",
|
|
114
|
+
rv[0] && /t=.*unparseable/i.test((rv[0].errors || []).join(" ")));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Guard against over-correction: a well-formed x= far in the future + a
|
|
118
|
+
// well-formed t= still verify to "pass" (the fix must not reject valid
|
|
119
|
+
// NumericDate values).
|
|
120
|
+
async function testDkimWellFormedNumericDateStillPasses() {
|
|
121
|
+
var kp = _rsaKeypair();
|
|
122
|
+
var signed = await _signedMessage(kp);
|
|
123
|
+
|
|
124
|
+
// x= 1 year out, t= now — both well-formed, ordering valid.
|
|
125
|
+
var nowSec = Math.floor(Date.now() / 1000);
|
|
126
|
+
var future = nowSec + (365 * 24 * 60 * 60);
|
|
127
|
+
// Note: splicing x=/t= changes the signed header value, so the body-hash
|
|
128
|
+
// / signature won't match — but the verdict must be a CRYPTO failure
|
|
129
|
+
// ("fail"/"permerror" from sig mismatch), NEVER skip the well-formed
|
|
130
|
+
// time checks. The point of THIS case: well-formed values are PARSED
|
|
131
|
+
// (don't trip the present-but-unparseable refusal). Assert the error,
|
|
132
|
+
// if any, is not the unparseable refusal.
|
|
133
|
+
var rv = await _verify(_spliceTag(signed, "t=" + nowSec + "; x=" + future), kp);
|
|
134
|
+
var joined = (rv[0].errors || []).join(" ");
|
|
135
|
+
check("verify: well-formed x=/t= not refused as unparseable",
|
|
136
|
+
!/unparseable/i.test(joined));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function run() {
|
|
140
|
+
await testDkimXTagUnparseableFailsClosed();
|
|
141
|
+
await testDkimLTagUnparseableFailsClosed();
|
|
142
|
+
await testDkimTTagUnparseableFailsClosed();
|
|
143
|
+
await testDkimWellFormedNumericDateStillPasses();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { run: run };
|
|
147
|
+
|
|
148
|
+
if (require.main === module) {
|
|
149
|
+
run().then(function () {
|
|
150
|
+
console.log("mail-dkim-numericdate-failclosed: OK");
|
|
151
|
+
}).catch(function (e) {
|
|
152
|
+
console.error(e && e.stack ? e.stack : e);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
});
|
|
155
|
+
}
|