@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.
- package/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/order.js +4 -2
- package/lib/security-middleware.js +1 -0
- 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,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: {
|
|
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
|
+
}
|
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
|
+
}
|