@blamejs/core 0.15.46 → 0.15.48
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/mail-arc-reuse-token.js +18 -0
- package/lib/mail-arc-sign.js +14 -1
- package/lib/mail-auth.js +10 -1
- package/lib/mail-dkim.js +23 -3
- package/lib/network-tsig.js +11 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.15.x
|
|
10
10
|
|
|
11
|
+
- v0.15.48 (2026-06-28) — **`b.network.dns.tsig` now accepts a message it signed with a non-default Original ID, fixing a sign/verify digest mismatch that made the originalId option non-functional (RFC 8945).** b.network.dns.tsig.verify restores the Original ID carried in the TSIG RDATA into the DNS message header before computing the HMAC, so a signed message survives an on-wire ID rewrite by a forwarder. b.network.dns.tsig.sign digested the message's current header ID instead of the Original ID, so any message signed with the originalId option set to a value other than the message's header ID produced a MAC the framework's own verify rejected (BADSIG) — the advertised originalId option was effectively non-functional for any non-default value. sign() now digests the Original-ID form, matching verify(); when originalId equals the message's header ID (the default) the digest is byte-for-byte identical to before, so existing signatures and the reference vectors are unaffected. **Fixed:** *TSIG: a message signed with a non-default Original ID now verifies (RFC 8945)* — b.network.dns.tsig.verify restores the Original ID carried in the TSIG RDATA into the DNS message header before computing the HMAC, so a signed message survives an on-wire ID rewrite by a forwarder. b.network.dns.tsig.sign digested the message's current header ID instead of the Original ID, so any message signed with the originalId option set to a value other than the message's header ID produced a MAC the framework's own verify rejected (BADSIG). sign() now digests the Original-ID form, matching verify(); when originalId equals the message's header ID (the default) the digest is byte-for-byte identical to before, so existing signatures and the reference vectors are unaffected.
|
|
12
|
+
|
|
13
|
+
- v0.15.47 (2026-06-28) — **`b.mail.arc.verify` now returns chainStatus=pass for a cryptographically valid ARC chain — three defects in the ARC-Message-Signature verification path that made every real chain fail are fixed.** ARC-Message-Signature (AMS) verification reuses the DKIM verifier against a synthetic message, and three independent defects in that seam caused b.mail.arc.verify to reject every cryptographically valid ARC chain — its own and those from upstream relays. First, the AMS i= tag is an RFC 8617 instance number (1..50), not a DKIM Agent/User Identifier, but it was run through the RFC 6376 §3.5 AUID-must-be-a-subdomain-of-d= check, which permerrored every chain. Second, the synthetic renames the AMS header to DKIM-Signature so the DKIM verifier can find it, but the signature header was then canonicalized under that renamed field name instead of ARC-Message-Signature, so the b= signature never matched what the relay signed. Third, when sealing hop i>=2 the signer canonicalized a prior hop's ARC-Authentication-Results into the AMS instead of the current hop's, so multi-hop chains failed verification past the first hop. All three are fixed: arc.verify now confirms valid single- and multi-hop chains as cv=pass. The RFC 6376 §3.5 AUID/d= binding check remains a non-opt-out default on the public b.mail.dkim.verify path — the ARC reuse signal that skips it is framework-internal and cannot be set through the public options. **Fixed:** *ARC chain verification now succeeds for valid chains (it previously failed every one)* — b.mail.arc.verify reused the DKIM verifier to check each ARC-Message-Signature, and three defects in that path made it reject all cryptographically valid ARC chains. (1) The AMS i= instance number (RFC 8617 §4.1.2) was treated as a DKIM AUID and rejected by the RFC 6376 §3.5 AUID/d= binding check (permerror). (2) The synthetic message renames the AMS header to DKIM-Signature so the verifier can locate it, but the signature header was canonicalized under the renamed name rather than ARC-Message-Signature, so the b= value could never match the relay's signature. (3) For hops at instance 2 and beyond, b.mail.arc.sign canonicalized a prior hop's ARC-Authentication-Results into the AMS rather than the current hop's, breaking verification past the first hop. arc.verify now returns chainStatus=pass for valid single- and multi-hop chains; the DKIM AUID check stays enforced on the public DKIM verify path.
|
|
14
|
+
|
|
11
15
|
- v0.15.46 (2026-06-28) — **`b.auth.fidoMds3` now enforces basicConstraints cA:TRUE on every intermediate link of the MDS3 x5c chain, closing a bypass where a non-CA certificate spliced in as an intermediate could sign a forged FIDO metadata BLOB that fido-mds3 accepted.** fido-mds3's x5c chain validation checked each intermediate link with X509Certificate.checkIssued(), which validates the issuer/subject linkage and signature but does NOT enforce the basicConstraints CA bit; only the final tail-to-root anchor used the framework's cA-enforcing helper. An attacker holding a non-CA (cA:FALSE) end-entity certificate that legitimately chains to a trusted MDS3 root (and its private key) could splice it in as an intermediate, sign a forged leaf with it, and have fido-mds3 accept an attacker-authored MDS3 metadata BLOB as authentic — the classic basicConstraints bypass (CVE-2002-0862 class). Every intermediate link now routes through x509Chain.issuerValidlyIssued, which enforces basicConstraints cA:TRUE in addition to the issuance and signature linkage, matching the mdoc / tsa / bimi / content-credentials / S-MIME chain walkers. A codebase guard now fails the build if any chain walker uses a bare issuance checkIssued() instead. **Security:** *fido-mds3 x5c intermediate links enforce the CA bit (basicConstraints bypass closed)* — b.auth.fidoMds3 validated each intermediate x5c link with X509Certificate.checkIssued(), which does not enforce basicConstraints cA:TRUE, so a non-CA certificate inserted as an intermediate was accepted as an issuer. An attacker with a cA:FALSE end-entity cert chaining to a trusted MDS3 root could sign a forged leaf and have a forged FIDO metadata BLOB accepted as authentic (CVE-2002-0862 class). Each intermediate link now routes through x509Chain.issuerValidlyIssued (cA:TRUE + issuance + signature), the same hardened test the framework's other certificate-chain walkers already use. The default GlobalSign MDS3 root and operator-supplied caCertificate roots are both covered. **Detectors:** *Build guard: a cert-chain issuance link must use issuerValidlyIssued, not bare checkIssued* — A codebase guard now fails the build if a certificate-chain walker validates an issuance link with a bare X509Certificate.checkIssued() (which omits the basicConstraints CA check) instead of x509Chain.issuerValidlyIssued, so the basicConstraints bypass cannot reappear at a new walker. A self-signed-root check (cert.checkIssued(cert)) is not an issuance link and is unaffected.
|
|
12
16
|
|
|
13
17
|
- v0.15.45 (2026-06-28) — **`b.selfUpdate.swap` now re-hashes the asset against the hash `verify` returned and refuses to install on a mismatch, closing the window where an asset swapped between verify and swap could be installed after the signature check passed.** selfUpdate.verify (signature-checks the asset and returns its hash) and selfUpdate.swap (renames the new asset into place) were two separate path-keyed operations with nothing binding the bytes swap installs to the bytes verify checked. An attacker able to write the asset path in the window between the two calls — or who pointed verify at a different inode via a symlink — could have the signature-verified bytes replaced before swap renamed the file into place, installing unverified code. swap now requires an expectedHash and re-hashes the from bytes against it immediately before the install, refusing with selfupdate/swap-hash-mismatch on any difference; the re-check runs right before the rename so the residual is a sub-millisecond window rather than the operator-controlled gap between verify and swap. Because swap now requires expectedHash, callers must pass the hash that verify returned (await verify(...) then swap({ ..., expectedHash: result.hash })). **Security:** *self-update install is bound to the signature-verified bytes (verify→swap TOCTOU closed)* — selfUpdate.swap renamed the new asset into place with no integrity re-check, so the bytes it installed were not bound to the bytes selfUpdate.verify had signature-checked: an attacker who could write the asset path between verify and swap (or repoint a symlink) could install unverified code. swap now re-hashes the from bytes against a required expectedHash (the hash verify returns) immediately before the install and refuses a mismatch (selfupdate/swap-hash-mismatch), with the check positioned right before the rename to minimize the residual window. A symlinked or differing-inode asset is caught the same way, because the install source is what gets re-hashed. **Migration:** *selfUpdate.swap requires expectedHash* — swap now requires an expectedHash option (refused at the call with selfupdate/bad-expected-hash if absent). Pass the hash that selfUpdate.verify returns: const v = await selfUpdate.verify({ assetPath, signaturePath, pubkeyPem }); await selfUpdate.swap({ from, to, backupTo, expectedHash: v.hash }). An optional hashAlgo (default sha3-512, matching verify's default) selects the digest when verify used a non-default algorithm.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Internal handshake token shared between the DKIM verifier (mail-dkim) and
|
|
4
|
+
* the ARC verifier (mail-auth). b.mail.arc.verify validates an
|
|
5
|
+
* ARC-Message-Signature by reusing the DKIM verifier against a synthetic
|
|
6
|
+
* message; this Symbol, set on the verify-options object, tells the DKIM
|
|
7
|
+
* verifier that the signature is an AMS — its i= is an RFC 8617 §4.1.2
|
|
8
|
+
* instance number (not a DKIM AUID), and its signature header is canonicalized
|
|
9
|
+
* under ARC-Message-Signature rather than DKIM-Signature.
|
|
10
|
+
*
|
|
11
|
+
* It lives in its OWN module, exported from neither primitive's public surface,
|
|
12
|
+
* so the reuse signal is unreachable from b.mail.dkim / b.mail.arc. A caller of
|
|
13
|
+
* the public b.mail.dkim.verify cannot obtain it, so the RFC 6376 §3.5 AUID/d=
|
|
14
|
+
* binding check stays a non-opt-out default on real DKIM verification — the
|
|
15
|
+
* reuse is available only to framework code that requires this module directly.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
module.exports = Symbol("blamejs.mail.arcAmsReuse");
|
package/lib/mail-arc-sign.js
CHANGED
|
@@ -310,10 +310,23 @@ function sign(opts) {
|
|
|
310
310
|
// Synthesize a virtual entry at the top of parsedHeaders so the
|
|
311
311
|
// header-name lookup below sees it; the canonicalizer reads
|
|
312
312
|
// parsedHeaders[idx] like any other header.
|
|
313
|
+
//
|
|
314
|
+
// The AMS covers only THIS hop's AAR ("most recent" per §5.1.1). Prior
|
|
315
|
+
// hops' ARC-* headers must be excluded: the h= lookup picks the last
|
|
316
|
+
// matching header, so a prior-hop AAR left in scope would be signed
|
|
317
|
+
// instead of this hop's, and arc.verify — which strips every prior ARC
|
|
318
|
+
// header and keeps only the current instance's AAR — would reconstruct a
|
|
319
|
+
// different input and the AMS would never verify past the first hop.
|
|
320
|
+
var priorArcStripped = parsedHeaders.filter(function (ph2) {
|
|
321
|
+
var lc = ph2.name.toLowerCase();
|
|
322
|
+
return lc !== "arc-authentication-results" &&
|
|
323
|
+
lc !== "arc-message-signature" &&
|
|
324
|
+
lc !== "arc-seal";
|
|
325
|
+
});
|
|
313
326
|
var amsParsedHeaders = [{
|
|
314
327
|
name: "ARC-Authentication-Results",
|
|
315
328
|
value: " " + aarValue,
|
|
316
|
-
}].concat(
|
|
329
|
+
}].concat(priorArcStripped);
|
|
317
330
|
var canonHeaders = "";
|
|
318
331
|
var headerNamesLc = amsParsedHeaders.map(function (h) { return h.name.toLowerCase(); });
|
|
319
332
|
for (var j = 0; j < headersToSign.length; j += 1) {
|
package/lib/mail-auth.js
CHANGED
|
@@ -54,6 +54,7 @@ var bCrypto = require("./crypto");
|
|
|
54
54
|
var C = require("./constants");
|
|
55
55
|
var numericBounds = require("./numeric-bounds");
|
|
56
56
|
var dkim = require("./mail-dkim");
|
|
57
|
+
var ARC_AMS_REUSE = require("./mail-arc-reuse-token");
|
|
57
58
|
var mimeParse = require("./mime-parse");
|
|
58
59
|
var safeXml = require("./parsers/safe-xml");
|
|
59
60
|
var ipUtils = require("./ip-utils");
|
|
@@ -1801,7 +1802,15 @@ async function _verifyAmsViaDkim(rfc822, hop, sigValue, tags, dkim, dnsLookup) {
|
|
|
1801
1802
|
rebuilt.unshift(renamedHeader);
|
|
1802
1803
|
var synthetic = rebuilt.join("\r\n") + (sep === -1 ? "" :
|
|
1803
1804
|
rfc822.slice(headerEnd));
|
|
1804
|
-
|
|
1805
|
+
// The AMS i= tag is an RFC 8617 §4.1.2 instance number, not a DKIM AUID;
|
|
1806
|
+
// signal the verifier (via the shared internal reuse token) to skip the
|
|
1807
|
+
// §3.5 AUID/d= binding check that would otherwise permerror every ARC chain
|
|
1808
|
+
// and to canonicalize the signature header under its ARC-Message-Signature
|
|
1809
|
+
// name. The token is exported from no public surface, so this reuse is
|
|
1810
|
+
// unreachable from b.mail.dkim.verify.
|
|
1811
|
+
var verifyOpts = { dnsLookup: dnsLookup };
|
|
1812
|
+
verifyOpts[ARC_AMS_REUSE] = true;
|
|
1813
|
+
var rv = await dkim.verify(synthetic, verifyOpts);
|
|
1805
1814
|
if (!Array.isArray(rv) || rv.length === 0) {
|
|
1806
1815
|
return { result: "permerror", errors: ["ams: dkim verifier returned no results"] };
|
|
1807
1816
|
}
|
package/lib/mail-dkim.js
CHANGED
|
@@ -44,6 +44,7 @@ var structuredFields = require("./structured-fields");
|
|
|
44
44
|
var nodeCrypto = require("node:crypto");
|
|
45
45
|
var safeBuffer = require("./safe-buffer");
|
|
46
46
|
var validateOpts = require("./validate-opts");
|
|
47
|
+
var ARC_AMS_REUSE = require("./mail-arc-reuse-token");
|
|
47
48
|
var C = require("./constants");
|
|
48
49
|
var networkDnsResolver = lazyRequire(function () { return require("./network-dns-resolver"); });
|
|
49
50
|
var { FrameworkError } = require("./framework-error");
|
|
@@ -684,7 +685,12 @@ function _verifySingleSignature(rfc822, parsedHeaders, sigHeader, keyTags, sigTa
|
|
|
684
685
|
// of d=. A signature whose i= claims `@evil.example.com` while d=
|
|
685
686
|
// is `example.org` is malformed and binds the signer's claim to a
|
|
686
687
|
// domain the verifier wouldn't otherwise associate. Refuse.
|
|
687
|
-
|
|
688
|
+
// arcAmsReuse: the caller is b.mail.arc.verify reusing this verifier for an
|
|
689
|
+
// ARC-Message-Signature, whose i= is an RFC 8617 instance number, not a DKIM
|
|
690
|
+
// AUID — skip the AUID/d= binding check for it. The flag rides a Symbol key
|
|
691
|
+
// unreachable from the public string-opts surface, so the check stays a
|
|
692
|
+
// non-opt-out default for every real DKIM signature.
|
|
693
|
+
if (!verifyOpts.arcAmsReuse && typeof sigTags.i === "string" && sigTags.i.length > 0) {
|
|
688
694
|
var iDomain = sigTags.i.indexOf("@") === -1
|
|
689
695
|
? sigTags.i
|
|
690
696
|
: sigTags.i.slice(sigTags.i.indexOf("@") + 1);
|
|
@@ -781,9 +787,15 @@ function _verifySingleSignature(rfc822, parsedHeaders, sigHeader, keyTags, sigTa
|
|
|
781
787
|
// name ends in `b` (`ab=`, `pub=`, `cb=` …). Anchor on the tag-list
|
|
782
788
|
// structure instead.
|
|
783
789
|
var unsignedSigValue = _stripBTagValue(sigHeader.value);
|
|
790
|
+
// The signature header is canonicalized under its true field name (§3.7).
|
|
791
|
+
// For the ARC reuse the header on the wire is ARC-Message-Signature, signed
|
|
792
|
+
// under that name; the synthetic renames it to DKIM-Signature only so this
|
|
793
|
+
// verifier finds it, so the canonical form must restore the real name or the
|
|
794
|
+
// b= value never matches what the relay signed.
|
|
795
|
+
var sigCanonName = verifyOpts.arcAmsReuse ? "ARC-Message-Signature" : "DKIM-Signature";
|
|
784
796
|
canonicalizedHeaders += canonHeader === "simple"
|
|
785
|
-
? _canonHeaderSimple(
|
|
786
|
-
: _canonHeaderRelaxed(
|
|
797
|
+
? _canonHeaderSimple(sigCanonName, " " + unsignedSigValue).replace(/\r\n$/, "")
|
|
798
|
+
: _canonHeaderRelaxed(sigCanonName, unsignedSigValue).replace(/\r\n$/, "");
|
|
787
799
|
|
|
788
800
|
// 3. Verify the signature.
|
|
789
801
|
var sigB64 = sigTags.b;
|
|
@@ -907,6 +919,14 @@ async function verify(rfc822, opts) {
|
|
|
907
919
|
maxSignatures = Math.floor(opts.maxSignatures);
|
|
908
920
|
}
|
|
909
921
|
var verifyOpts = { minRsaBits: opts.minRsaBits };
|
|
922
|
+
// Framework-internal ARC reuse: the key is a Symbol owned by
|
|
923
|
+
// mail-arc-reuse-token (exported from no public surface), so only framework
|
|
924
|
+
// code that requires that module can set it. When present, the i= tag is an
|
|
925
|
+
// RFC 8617 instance number, not a DKIM AUID — the §3.5 AUID-subdomain check
|
|
926
|
+
// is skipped and the signature header is canonicalized under its real
|
|
927
|
+
// ARC-Message-Signature name. The public b.mail.dkim.verify path can never
|
|
928
|
+
// reach this, so the AUID check stays a non-opt-out default for real DKIM.
|
|
929
|
+
if (opts[ARC_AMS_REUSE] === true) verifyOpts.arcAmsReuse = true;
|
|
910
930
|
|
|
911
931
|
var split = _splitHeadersBody(rfc822);
|
|
912
932
|
var parsedHeaders = _parseHeaders(split.headers);
|
package/lib/network-tsig.js
CHANGED
|
@@ -232,9 +232,19 @@ function sign(message, opts) {
|
|
|
232
232
|
var originalId = opts.originalId == null ? message.readUInt16BE(0) : opts.originalId;
|
|
233
233
|
var algName = alg.name + ".";
|
|
234
234
|
|
|
235
|
+
// RFC 8945 §5.3.1/§5.3.2 — the canonical digest is over the message with the
|
|
236
|
+
// header ID set to the Original ID (verify() restores it before digesting,
|
|
237
|
+
// line ~363, so a forwarder may rewrite the on-wire ID). Digest the same
|
|
238
|
+
// Original-ID form here; when originalId == the message's current ID (the
|
|
239
|
+
// default) this is byte-identical to digesting `message` as-is.
|
|
240
|
+
var digestMessage = message;
|
|
241
|
+
if (originalId !== message.readUInt16BE(0)) {
|
|
242
|
+
digestMessage = Buffer.from(message);
|
|
243
|
+
digestMessage.writeUInt16BE(originalId, 0);
|
|
244
|
+
}
|
|
235
245
|
var digest = Buffer.concat([
|
|
236
246
|
_requestMacPrefix(opts.requestMac),
|
|
237
|
-
|
|
247
|
+
digestMessage,
|
|
238
248
|
_tsigVariables(opts.keyName, algName, time, fudge, error, otherData),
|
|
239
249
|
]);
|
|
240
250
|
var mac = nodeCrypto.createHmac(alg.hash, secret).update(digest).digest();
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:90ecb6b7-5585-460c-8b63-abf23c45b39f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-06-
|
|
8
|
+
"timestamp": "2026-06-28T21:46:48.435Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.15.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.15.48",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.15.
|
|
25
|
+
"version": "0.15.48",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.15.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.15.48",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.15.
|
|
57
|
+
"ref": "@blamejs/core@0.15.48",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|