@blamejs/core 0.12.50 → 0.12.52
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/README.md +2 -1
- package/index.js +2 -0
- package/lib/network-dane.js +159 -0
- package/lib/network.js +1 -0
- package/lib/privacy-pass.js +267 -0
- 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.12.x
|
|
10
10
|
|
|
11
|
+
- v0.12.52 (2026-05-25) — **`b.privacyPass` — Privacy Pass origin-side token verification (RFC 9577 / 9578).** Anonymous, publicly verifiable authorization: an origin issues a WWW-Authenticate: PrivateToken challenge and verifies a presented token cryptographically, without learning who the client is and without a callback to the issuer. b.privacyPass implements the publicly verifiable token type 0x0002 (Blind RSA, 2048-bit): the token's authenticator is an RSA Blind Signature (RFC 9474) checked as RSASSA-PSS (SHA-384, 48-byte salt) over token_input = token_type ‖ nonce ‖ challenge_digest ‖ token_key_id, using only the issuer's public key. The token is bound to that key (token_key_id) and, optionally, to the challenge it answers, so a token minted for another origin is refused. Blind RSA is the algorithm Privacy Pass defines on the wire — like the DNSSEC / DANE verifiers it validates an external protocol's signatures rather than introducing classical crypto as a framework default. Verified against the RFC 9578 §8.2 test vector. **Added:** *`b.privacyPass.verifyToken(opts)` / `parseToken` / `buildChallenge`* — `buildChallenge` builds a TokenChallenge (RFC 9577 §2.1) and the matching `WWW-Authenticate: PrivateToken challenge=…, token-key=…` header an origin returns to request a token, scoped to an issuer (and optionally an origin and a 32-byte redemption context). `parseToken` splits a token into its fields (type / nonce / challenge_digest / token_key_id / authenticator). `verifyToken` verifies a type 0x0002 (Blind RSA) token: it confirms the token's `token_key_id` is the SHA-256 of the supplied issuer public key, optionally that its `challenge_digest` matches `opts.challenge`, and that the authenticator is a valid RSASSA-PSS signature over the token input. Refuses unknown / privately verifiable token types (the VOPRF type 0x0001 needs the issuer secret and is an issuer-side operation), key-id and challenge mismatches, and tampered authenticators. Marked experimental while the issuance protocols see deployment.
|
|
12
|
+
|
|
13
|
+
- v0.12.51 (2026-05-25) — **`b.network.dns.dane.matchCertificate` — DANE / TLSA certificate matching (RFC 6698 / 7671).** Pin a service's certificate through DNS instead of a public CA. matchCertificate checks a server certificate against a set of TLSA records: the selected data — the full certificate (selector 0) or its subjectPublicKeyInfo (selector 1) — is hashed per the matching type (exact / SHA-256 / SHA-512) and compared in constant time to the record's association data. For a DANE-EE (usage 3) record a match is self-authenticating — the TLSA pins the key, so no public-CA path is needed (the common SMTP-DANE case, RFC 7672); for the PKIX usages a match is reported as necessary-but-not-sufficient so the caller still runs PKIX. This is the payoff of the DNSSEC verifier: verify the TLSA RRset with b.network.dns.dnssec, then match the certificate. Verified against a live DNSSEC-signed TLSA record and the matching server certificate. **Added:** *`b.network.dns.dane.matchCertificate(opts)`* — Matches a leaf certificate (and optional `chain`) against a TLSA RRset (`{ usage, selector, matchingType, data }`). Selector 0 hashes the full certificate DER, selector 1 the subjectPublicKeyInfo; matching type 0 is an exact comparison, 1 SHA-256, 2 SHA-512 (SHA-1 and any other type are refused, not guessed). End-entity usages (PKIX-EE 1, DANE-EE 3) match the leaf; trust-anchor usages (PKIX-TA 0, DANE-TA 2) match the leaf or any supplied chain certificate. Returns `{ ok, matched, daneAuthenticated, trustAnchorMatch, pkixRequired, matchedCertIndex }` — `daneAuthenticated` is true only for a DANE-EE match (the key is pinned, no CA needed); `pkixRequired` flags the PKIX usages. Throws `dane/no-match` when nothing matches, and refuses unknown usage / selector / matching values and unparseable certificates. Verify the TLSA RRset with `b.network.dns.dnssec` first — an unauthenticated TLSA record proves nothing.
|
|
14
|
+
|
|
11
15
|
- v0.12.50 (2026-05-25) — **`b.network.dns.dnssec.verifyChain` — validate a DNSSEC delegation chain to a pinned root anchor.** Completes local DNSSEC verification: validate a full delegation chain from the root down to a zone against a pinned trust anchor (RFC 4035 §5), instead of trusting any single resolver. For each link, the zone's DNSKEY RRset must be self-signed by one of its keys, and that key must be vouched for either by a pinned anchor (at the root) or by a DS record served + signed by the already-trusted parent — so trust flows root → TLD → zone with no gap. The IANA root KSKs (KSK-2017 tag 20326, KSK-2024 tag 38696) ship as the default anchors; override with opts.trustAnchors for a private root. verifyChain returns the leaf zone's trusted DNSKEY set, which you then hand to verifyRrset / verifyDenial for the actual answer. Composes verifyRrset + verifyDs + the key tag; verified end-to-end against a live root→org chain. **Added:** *`b.network.dns.dnssec.verifyChain(opts)`* — Walks an ordered, root-first list of `links` ({ zone, dnskeys, dnskeyRrsig, dsRdatas?, dsRrsig? }). At each link it verifies the DNSKEY RRset's self-signature (composing `verifyRrset`), then establishes trust in the signing key: at the root by matching a pinned anchor's DS digest (`verifyDs`), at every delegation by verifying the parent-served DS RRset's signature with the already-trusted parent key and confirming the signing KSK matches one of those DS records. Returns `{ ok, zone, keys, path }` with the leaf zone's trusted DNSKEY set. Refuses a root key that matches no anchor (`dnssec/chain-anchor-mismatch`), a KSK that matches no parent DS (`dnssec/chain-ds-mismatch`), and a missing parent key (`dnssec/chain-no-parent-key`). The default `DEFAULT_ROOT_ANCHORS` are the published IANA root KSK DS records; `opts.trustAnchors` overrides them for a private or test root.
|
|
12
16
|
|
|
13
17
|
- v0.12.49 (2026-05-25) — **`b.network.dns.dnssec.verifyDenial` — NSEC / NSEC3 denial-of-existence.** Prove a DNS name does not exist, or has no records of a given type, from the signed NSEC (RFC 4034 §4) or NSEC3 (RFC 5155) records a server returns. This is the other half of local DNSSEC verification: verifyRrset proves a positive answer, verifyDenial proves a negative — so a resolver client can confirm an NXDOMAIN / NODATA itself instead of trusting the upstream resolver. NSEC3 proofs run the closest-encloser / next-closer / covering-range logic over iterated-SHA-1 hashes, with the iteration count capped (default 500) to bound the work an attacker can force, and an Opt-Out NXDOMAIN refused unless explicitly accepted (opt-out only proves 'no signed records', not non-existence). The companion b.network.dns.dnssec.nsec3Hash computes the RFC 5155 §5 hash directly. NSEC verifyRrset support is also enabled: per RFC 6840 §5.1 the NSEC Next Domain Name is not downcased, so its RDATA is verbatim-canonical. **Added:** *`b.network.dns.dnssec.verifyDenial(opts)`* — Proves NXDOMAIN or NODATA from already-verified NSEC / NSEC3 records (supply one of `opts.nsec3` or `opts.nsec`). Like `verifyDs`, it checks the denial RELATION — closest-encloser matching, covering ranges, and type-bitmap absence — not the record signatures, which the caller verifies with `verifyRrset` first. NSEC3 supports name-error proofs (matching closest encloser + covered next-closer + covered wildcard), NODATA (matching record with the type and CNAME absent from the bitmap), Opt-Out DS NODATA, and wildcard NODATA. The iterated-SHA-1 count is capped by `opts.maxIterations` (default 500); an NXDOMAIN proof that depends on an Opt-Out NSEC3 is refused unless `opts.allowOptOut` is set. NSEC supports covering-name NXDOMAIN (with the source-of-synthesis wildcard) and matching-name NODATA. Verified end-to-end against a live iana.org NXDOMAIN proof. · *`b.network.dns.dnssec.nsec3Hash(name, opts)`* — Computes the RFC 5155 §5 NSEC3 hash of a name — iterated SHA-1 over the canonical (lowercased, root-terminated) wire form with the zone salt. The base32hex encoding of the result is the NSEC3 owner label. SHA-1 is the only hash IANA registers for NSEC3, so this is a wire-protocol constant rather than a cryptographic default. Useful for checking an owner label or analyzing a zone's hashing parameters. **Changed:** *`verifyRrset` now accepts NSEC and NSEC3 RRsets* — NSEC (type 47) and NSEC3 (type 50) are no longer refused as uncanonicalizable: NSEC3's next-owner is a hash, and per RFC 6840 §5.1 the NSEC Next Domain Name field is not downcased for DNSSEC canonical form, so both RDATAs are verbatim-canonical. This lets a caller verify the signatures on the records that `verifyDenial` then reasons over.
|
package/README.md
CHANGED
|
@@ -89,6 +89,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
89
89
|
- **Financial / Open Banking** — FAPI 2.0 Final composite posture (PAR + PKCE-S256 + DPoP-or-mTLS + RFC 9207); runtime enforcement helpers `b.fapi2.assertCallback` (refuses missing iss + bare-param under message-signing) and `b.fapi2.assertAuthzRequest` (refuses non-JAR); CFPB §1033 / FDX 6.0 consumer-financial-data-sharing wrapper (`b.fdx`)
|
|
90
90
|
- **Data-subject coordination** — cross-table export / rectify / erase / restrict / objection (`b.subject`, `b.subject.eraseHard`); subject-level legal-hold registry consulted by erase + retention paths (FRCP Rule 26/37(e), GDPR Art 17(3)(e), SEC Rule 17a-4, HIPAA §164.530(j)(2)) (`b.legalHold`)
|
|
91
91
|
- **Account safety** — adaptive bot-challenge staircase (`b.authBotChallenge`); session-to-device-posture binding with fail-closed verify (`b.sessionDeviceBinding`)
|
|
92
|
+
- **Anonymous authorization** — Privacy Pass origin side (RFC 9577/9578 — `b.privacyPass`): issue a `WWW-Authenticate: PrivateToken` challenge and verify a presented Blind-RSA (type 0x0002) token against the issuer public key, with no issuer callback and no client identity
|
|
92
93
|
### Crypto
|
|
93
94
|
|
|
94
95
|
- **At-rest envelope** — envelope-versioned PQC (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256); vault sealing (`b.crypto`, `b.vault`)
|
|
@@ -120,7 +121,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
120
121
|
- In-process CIDR fence (`b.middleware.networkAllowlist`)
|
|
121
122
|
- `Cache-Control: no-store` on every 401 from `requireAuth` / `requireAal` / `requireStepUp` per RFC 9111 §5.2.2.5
|
|
122
123
|
- **Outbound HTTP client** — HTTP/1.1 + HTTP/2 with SSRF gate (cloud-metadata IPs hard-denied; private / loopback / link-local overridable per call); scheme + userinfo + per-host destination allowlist; redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`)
|
|
123
|
-
- **Network configurability (`b.network`)** — env-driven NTP / NTS (RFC 8915), IPv4/IPv6 NTP, DNS with IPv6 / DoH / DoT (private-CA pinning) / cache / lookup timeout; local DNSSEC signature verification (RFC 4035 — `b.network.dns.dnssec.verifyRrset` over a canonicalised RRset against RSA / ECDSA P-256·P-384 / Ed25519 DNSKEYs, plus DS-digest + key-tag, plus `verifyDenial` for NSEC / NSEC3 (RFC 5155) NXDOMAIN / NODATA proofs with iteration caps + Opt-Out handling, plus `verifyChain` to validate a full root→TLD→zone delegation chain against the pinned IANA root anchors) so a resolver client can verify both positive and negative answers instead of trusting the upstream AD bit; outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
|
|
124
|
+
- **Network configurability (`b.network`)** — env-driven NTP / NTS (RFC 8915), IPv4/IPv6 NTP, DNS with IPv6 / DoH / DoT (private-CA pinning) / cache / lookup timeout; local DNSSEC signature verification (RFC 4035 — `b.network.dns.dnssec.verifyRrset` over a canonicalised RRset against RSA / ECDSA P-256·P-384 / Ed25519 DNSKEYs, plus DS-digest + key-tag, plus `verifyDenial` for NSEC / NSEC3 (RFC 5155) NXDOMAIN / NODATA proofs with iteration caps + Opt-Out handling, plus `verifyChain` to validate a full root→TLD→zone delegation chain against the pinned IANA root anchors) so a resolver client can verify both positive and negative answers instead of trusting the upstream AD bit; DANE / TLSA certificate matching (RFC 6698/7671 — `b.network.dns.dane.matchCertificate`) to pin a service's key through DNSSEC instead of a public CA; outbound HTTP proxy (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`); runtime DPI trust-store CA additions; application-level heartbeats; TCP socket defaults
|
|
124
125
|
- **Error pages** — operator-rendered, no app-frame leakage (`b.errorPage`)
|
|
125
126
|
### Defensive parsers
|
|
126
127
|
|
package/index.js
CHANGED
|
@@ -394,6 +394,7 @@ var webPush = require("./lib/web-push-vapid");
|
|
|
394
394
|
var fedcm = require("./lib/fedcm");
|
|
395
395
|
var dbsc = require("./lib/dbsc");
|
|
396
396
|
var importmapIntegrity = require("./lib/importmap-integrity");
|
|
397
|
+
var privacyPass = require("./lib/privacy-pass");
|
|
397
398
|
var standardWebhooks = require("./lib/standard-webhooks");
|
|
398
399
|
var lro = require("./lib/lro");
|
|
399
400
|
var jsonApi = require("./lib/jsonapi");
|
|
@@ -409,6 +410,7 @@ module.exports = {
|
|
|
409
410
|
fedcm: fedcm,
|
|
410
411
|
dbsc: dbsc,
|
|
411
412
|
importmapIntegrity: importmapIntegrity,
|
|
413
|
+
privacyPass: privacyPass,
|
|
412
414
|
standardWebhooks: standardWebhooks,
|
|
413
415
|
lro: lro,
|
|
414
416
|
jsonApi: jsonApi,
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.network.dns.dane
|
|
4
|
+
* @nav Network
|
|
5
|
+
* @title DANE / TLSA
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* DNS-Based Authentication of Named Entities (RFC 6698, updated by
|
|
9
|
+
* RFC 7671) — match a server certificate against a TLSA record so the
|
|
10
|
+
* DNS, not a public CA, vouches for which key a service uses. This is
|
|
11
|
+
* the payoff of DNSSEC: verify the TLSA RRset with
|
|
12
|
+
* <code>b.network.dns.dnssec</code> first, then
|
|
13
|
+
* <code>matchCertificate</code> checks the certificate against it.
|
|
14
|
+
*
|
|
15
|
+
* A TLSA record carries a certificate usage (PKIX-TA 0, PKIX-EE 1,
|
|
16
|
+
* DANE-TA 2, DANE-EE 3 — RFC 7218 mnemonics), a selector (full
|
|
17
|
+
* certificate 0, or subjectPublicKeyInfo 1), and a matching type
|
|
18
|
+
* (exact 0, SHA-256 1, SHA-512 2). The selected certificate data is
|
|
19
|
+
* hashed per the matching type and compared, in constant time, to the
|
|
20
|
+
* record's association data. For DANE-EE(3) a match means the
|
|
21
|
+
* certificate IS the pinned end-entity key — no public-CA path is
|
|
22
|
+
* needed (the common SMTP-DANE case, RFC 7672). For the PKIX usages a
|
|
23
|
+
* match is necessary but the caller still performs PKIX validation.
|
|
24
|
+
*
|
|
25
|
+
* @card
|
|
26
|
+
* DANE / TLSA certificate matching (RFC 6698 / 7671). Pin a service's
|
|
27
|
+
* key through DNSSEC instead of a public CA — verify the TLSA RRset,
|
|
28
|
+
* then match the certificate (DANE-EE / DANE-TA / PKIX usages,
|
|
29
|
+
* full-cert or SPKI selector, SHA-256 / SHA-512).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
var nodeCrypto = require("node:crypto");
|
|
33
|
+
var bCrypto = require("./crypto");
|
|
34
|
+
var validateOpts = require("./validate-opts");
|
|
35
|
+
var { defineClass } = require("./framework-error");
|
|
36
|
+
|
|
37
|
+
var DaneError = defineClass("DaneError", { alwaysPermanent: true });
|
|
38
|
+
|
|
39
|
+
// RFC 6698 §2.1 + RFC 7218 mnemonics.
|
|
40
|
+
var USAGES = { 0: "PKIX-TA", 1: "PKIX-EE", 2: "DANE-TA", 3: "DANE-EE" };
|
|
41
|
+
var SELECTORS = { 0: "Cert", 1: "SPKI" };
|
|
42
|
+
// Matching types: 0 = exact match on the selected data, 1 = SHA-256,
|
|
43
|
+
// 2 = SHA-512. SHA-1 is not registered for TLSA, so anything else is
|
|
44
|
+
// refused rather than guessed.
|
|
45
|
+
var MATCHING = { 0: null, 1: "sha256", 2: "sha512" };
|
|
46
|
+
|
|
47
|
+
function _bytes(x, what) {
|
|
48
|
+
if (Buffer.isBuffer(x)) return x;
|
|
49
|
+
if (x instanceof Uint8Array) return Buffer.from(x);
|
|
50
|
+
if (typeof x === "string") return Buffer.from(x, "hex");
|
|
51
|
+
throw new DaneError("dane/bad-bytes", "dane: " + what + " must be a Buffer / Uint8Array / hex string");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _selectedData(x509, selector) {
|
|
55
|
+
if (selector === 0) return Buffer.from(x509.raw); // full certificate DER
|
|
56
|
+
if (selector === 1) return x509.publicKey.export({ format: "der", type: "spki" }); // subjectPublicKeyInfo DER
|
|
57
|
+
throw new DaneError("dane/unsupported-selector", "dane: unsupported TLSA selector " + selector + " (0 = full cert, 1 = SPKI)");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function _associationOf(selected, matchingType) {
|
|
61
|
+
if (matchingType === 0) return selected;
|
|
62
|
+
var hashName = MATCHING[matchingType];
|
|
63
|
+
if (!hashName) throw new DaneError("dane/unsupported-matching", "dane: unsupported TLSA matching type " + matchingType + " (0 = exact, 1 = SHA-256, 2 = SHA-512)");
|
|
64
|
+
return nodeCrypto.createHash(hashName).update(selected).digest();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _parseCert(der, what) {
|
|
68
|
+
try { return new nodeCrypto.X509Certificate(_bytes(der, what)); }
|
|
69
|
+
catch (e) { throw new DaneError("dane/bad-certificate", "dane: could not parse " + what + ": " + ((e && e.message) || e)); }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Validate a TLSA enum field: it must be an actual integer that is an
|
|
73
|
+
// OWN key of the lookup table. Rejecting non-numbers stops a string like
|
|
74
|
+
// "1" (which coerces on key lookup but then fails the strict-=== usage
|
|
75
|
+
// checks below), and the own-property test stops prototype keys such as
|
|
76
|
+
// "__proto__" that `in` / `[x] !== undefined` would wrongly accept.
|
|
77
|
+
function _enumField(v, table, code, label, i) {
|
|
78
|
+
if (typeof v !== "number" || !Number.isInteger(v) || !Object.prototype.hasOwnProperty.call(table, v)) {
|
|
79
|
+
throw new DaneError(code, "dane: tlsa[" + i + "] " + label + " must be a numeric " + Object.keys(table).join(" / ") + " (got " + JSON.stringify(v) + ")");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function _normaliseTlsa(rec, i) {
|
|
83
|
+
if (!rec || typeof rec !== "object") throw new DaneError("dane/bad-tlsa", "dane: tlsa[" + i + "] must be an object");
|
|
84
|
+
_enumField(rec.usage, USAGES, "dane/unsupported-usage", "certificate usage", i);
|
|
85
|
+
_enumField(rec.selector, SELECTORS, "dane/unsupported-selector", "selector", i);
|
|
86
|
+
_enumField(rec.matchingType, MATCHING, "dane/unsupported-matching", "matching type", i);
|
|
87
|
+
return { usage: rec.usage, selector: rec.selector, matchingType: rec.matchingType, data: _bytes(rec.data, "tlsa[" + i + "].data") };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @primitive b.network.dns.dane.matchCertificate
|
|
92
|
+
* @signature b.network.dns.dane.matchCertificate(opts)
|
|
93
|
+
* @since 0.12.51
|
|
94
|
+
* @status stable
|
|
95
|
+
* @compliance soc2
|
|
96
|
+
* @related b.network.dns.dnssec.verifyChain, b.network.dns.dnssec.verifyRrset
|
|
97
|
+
*
|
|
98
|
+
* Match a server certificate against a set of (DNSSEC-verified) TLSA
|
|
99
|
+
* records (RFC 6698 / 7671). For each record the selected data — the
|
|
100
|
+
* full certificate DER (selector 0) or its subjectPublicKeyInfo
|
|
101
|
+
* (selector 1) — is hashed per the matching type (exact / SHA-256 /
|
|
102
|
+
* SHA-512) and compared, constant-time, to the record's association
|
|
103
|
+
* data. End-entity usages (PKIX-EE 1, DANE-EE 3) are matched against the
|
|
104
|
+
* leaf certificate; trust-anchor usages (PKIX-TA 0, DANE-TA 2) are
|
|
105
|
+
* matched against the leaf and any supplied <code>chain</code>.
|
|
106
|
+
*
|
|
107
|
+
* Returns the matching record plus what the caller must still do: a
|
|
108
|
+
* DANE-EE match is self-sufficient (the TLSA pins the key); a DANE-TA
|
|
109
|
+
* match still needs chain-to-anchor verification; PKIX usages still need
|
|
110
|
+
* full PKIX validation. Throws <code>dane/no-match</code> if nothing
|
|
111
|
+
* matches. Verify the TLSA RRset with <code>b.network.dns.dnssec</code>
|
|
112
|
+
* before trusting the records — an unauthenticated TLSA proves nothing.
|
|
113
|
+
*
|
|
114
|
+
* @opts
|
|
115
|
+
* {
|
|
116
|
+
* tlsa: [ { usage, selector, matchingType, data: Buffer|hex } ], // the TLSA RRset
|
|
117
|
+
* certificate: Buffer, // leaf certificate (DER)
|
|
118
|
+
* chain?: Buffer[], // intermediate / CA certs (DER), for TA usages
|
|
119
|
+
* }
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* var r = b.network.dns.dane.matchCertificate({ tlsa: records, certificate: leafDer });
|
|
123
|
+
* // → { ok: true, matched: { usage: 3, selector: 1, matchingType: 1 }, daneAuthenticated: true, pkixRequired: false }
|
|
124
|
+
*/
|
|
125
|
+
function matchCertificate(opts) {
|
|
126
|
+
validateOpts.requireObject(opts, "dane.matchCertificate", DaneError);
|
|
127
|
+
validateOpts(opts, ["tlsa", "certificate", "chain"], "dane.matchCertificate");
|
|
128
|
+
if (!Array.isArray(opts.tlsa) || opts.tlsa.length === 0) throw new DaneError("dane/bad-arg", "dane.matchCertificate: opts.tlsa must be a non-empty array");
|
|
129
|
+
var records = opts.tlsa.map(_normaliseTlsa);
|
|
130
|
+
var leaf = _parseCert(opts.certificate, "certificate");
|
|
131
|
+
var chain = Array.isArray(opts.chain) ? opts.chain.map(function (c, i) { return _parseCert(c, "chain[" + i + "]"); }) : [];
|
|
132
|
+
|
|
133
|
+
for (var i = 0; i < records.length; i++) {
|
|
134
|
+
var rec = records[i];
|
|
135
|
+
var eeUsage = rec.usage === 1 || rec.usage === 3; // PKIX-EE / DANE-EE → leaf only
|
|
136
|
+
var certs = eeUsage ? [leaf] : [leaf].concat(chain); // TA usages may match a chain cert
|
|
137
|
+
for (var c = 0; c < certs.length; c++) {
|
|
138
|
+
var assoc = _associationOf(_selectedData(certs[c], rec.selector), rec.matchingType);
|
|
139
|
+
if (bCrypto.timingSafeEqual(assoc, rec.data)) {
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
matched: { usage: rec.usage, usageName: USAGES[rec.usage], selector: rec.selector, matchingType: rec.matchingType },
|
|
143
|
+
matchedCertIndex: c, // 0 = leaf, >0 = chain[c-1]
|
|
144
|
+
daneAuthenticated: rec.usage === 3, // DANE-EE: TLSA pins the key, no CA path needed
|
|
145
|
+
trustAnchorMatch: rec.usage === 0 || rec.usage === 2,
|
|
146
|
+
pkixRequired: rec.usage === 0 || rec.usage === 1,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
throw new DaneError("dane/no-match", "dane.matchCertificate: no TLSA record matched the certificate" + (chain.length ? " or chain" : ""));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
matchCertificate: matchCertificate,
|
|
156
|
+
USAGES: USAGES,
|
|
157
|
+
SELECTORS: SELECTORS,
|
|
158
|
+
DaneError: DaneError,
|
|
159
|
+
};
|
package/lib/network.js
CHANGED
|
@@ -36,6 +36,7 @@ var nts = require("./network-nts");
|
|
|
36
36
|
var networkDns = require("./network-dns");
|
|
37
37
|
networkDns.resolver = require("./network-dns-resolver");
|
|
38
38
|
networkDns.dnssec = require("./network-dnssec");
|
|
39
|
+
networkDns.dane = require("./network-dane");
|
|
39
40
|
var networkProxy = require("./network-proxy");
|
|
40
41
|
var networkTls = require("./network-tls");
|
|
41
42
|
var heartbeat = require("./network-heartbeat");
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.privacyPass
|
|
4
|
+
* @nav Identity
|
|
5
|
+
* @title Privacy Pass
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Origin / relying-party side of Privacy Pass (RFC 9577 HTTP
|
|
9
|
+
* authentication scheme, RFC 9578 issuance protocols) — issue a token
|
|
10
|
+
* challenge and verify a presented token without learning who the
|
|
11
|
+
* client is. An origin asks for a token with a
|
|
12
|
+
* <code>WWW-Authenticate: PrivateToken</code> challenge; the client
|
|
13
|
+
* obtains a token from an issuer and presents it; the origin verifies
|
|
14
|
+
* it cryptographically.
|
|
15
|
+
*
|
|
16
|
+
* This implements the publicly verifiable token type
|
|
17
|
+
* <strong>0x0002 (Blind RSA, 2048-bit)</strong>: the token's
|
|
18
|
+
* authenticator is an RSA Blind Signature (RFC 9474) that any party
|
|
19
|
+
* holding the issuer's public key can verify with RSASSA-PSS — so the
|
|
20
|
+
* origin verifies tokens itself, with no issuer secret and no callback.
|
|
21
|
+
* The privately verifiable VOPRF type (0x0001) requires the issuer's
|
|
22
|
+
* secret key and is an issuer-side operation, not implemented here.
|
|
23
|
+
*
|
|
24
|
+
* Blind RSA is the algorithm Privacy Pass defines on the wire; like
|
|
25
|
+
* the framework's DNSSEC / DANE verifiers it validates an external
|
|
26
|
+
* protocol's signatures (RSASSA-PSS, SHA-384) rather than introducing
|
|
27
|
+
* classical crypto as a framework default.
|
|
28
|
+
*
|
|
29
|
+
* @card
|
|
30
|
+
* Privacy Pass origin side (RFC 9577 / 9578). Issue a
|
|
31
|
+
* <code>WWW-Authenticate: PrivateToken</code> challenge and verify a
|
|
32
|
+
* presented Blind-RSA (type 0x0002) token against the issuer public
|
|
33
|
+
* key — anonymous, publicly verifiable authorization with no issuer
|
|
34
|
+
* callback.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var nodeCrypto = require("node:crypto");
|
|
38
|
+
var bCrypto = require("./crypto");
|
|
39
|
+
var validateOpts = require("./validate-opts");
|
|
40
|
+
var { defineClass } = require("./framework-error");
|
|
41
|
+
|
|
42
|
+
var PrivacyPassError = defineClass("PrivacyPassError", { alwaysPermanent: true });
|
|
43
|
+
|
|
44
|
+
var TOKEN_TYPE_BLIND_RSA = 0x0002;
|
|
45
|
+
// RFC 9578 §5.3 token type 0x0002: RSABSSA-SHA384-PSS, salt length 48.
|
|
46
|
+
var PSS_HASH = "sha384";
|
|
47
|
+
var PSS_SALT_LEN = 48; // allow:raw-byte-literal — RFC 9578 §5.3 PSS salt length (= SHA-384 digest size)
|
|
48
|
+
// Fixed-size token fields (RFC 9577 §2.2): type(2) nonce(32)
|
|
49
|
+
// challenge_digest(32) token_key_id(32), then the authenticator.
|
|
50
|
+
var TOKEN_PREFIX_LEN = 98; // allow:raw-byte-literal — 2 + 32 + 32 + 32 (token_input length)
|
|
51
|
+
|
|
52
|
+
// RFC 9577 §2.1 sends the challenge / token-key auth-params as base64url
|
|
53
|
+
// WITH padding; Node's "base64url" output is unpadded, so pad to a
|
|
54
|
+
// multiple of 4 so strict clients / proxies accept the header.
|
|
55
|
+
function _b64urlPadded(buf) {
|
|
56
|
+
var s = Buffer.from(buf).toString("base64url");
|
|
57
|
+
while (s.length % 4 !== 0) s += "="; // allow:raw-byte-literal — base64 quantum is 4 chars
|
|
58
|
+
return s;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _bytes(x, what) {
|
|
62
|
+
if (Buffer.isBuffer(x)) return x;
|
|
63
|
+
if (x instanceof Uint8Array) return Buffer.from(x);
|
|
64
|
+
if (typeof x === "string") return Buffer.from(x, "base64");
|
|
65
|
+
throw new PrivacyPassError("privacy-pass/bad-bytes", "privacyPass: " + what + " must be a Buffer / Uint8Array / base64 string");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Import the issuer public key and capture the SubjectPublicKeyInfo
|
|
69
|
+
// bytes used to derive token_key_id. When the caller supplies the
|
|
70
|
+
// published SPKI DER directly, hash THOSE bytes — re-exporting an
|
|
71
|
+
// rsa-pss KeyObject can re-encode the AlgorithmIdentifier and change the
|
|
72
|
+
// digest. token_key_id is SHA-256 of the issuer's distributed key
|
|
73
|
+
// (RFC 9577 §2.2), which is the SPKI as published.
|
|
74
|
+
function _importIssuerKey(k) {
|
|
75
|
+
if (k && typeof k === "object" && typeof k.export === "function" && k.type === "public") {
|
|
76
|
+
return { key: k, spki: k.export({ format: "der", type: "spki" }) };
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
if (Buffer.isBuffer(k) || k instanceof Uint8Array) {
|
|
80
|
+
var der = Buffer.from(k);
|
|
81
|
+
return { key: nodeCrypto.createPublicKey({ key: der, format: "der", type: "spki" }), spki: der };
|
|
82
|
+
}
|
|
83
|
+
// A "PUBLIC KEY" PEM body IS the SubjectPublicKeyInfo DER — decode it
|
|
84
|
+
// directly so token_key_id is SHA-256 of the issuer's exact bytes,
|
|
85
|
+
// not a re-encoding (Node can re-emit rsa-pss AlgorithmIdentifier
|
|
86
|
+
// parameters differently on export).
|
|
87
|
+
if (typeof k === "string" && /-----BEGIN PUBLIC KEY-----/.test(k)) {
|
|
88
|
+
var body = k.replace(/-----BEGIN PUBLIC KEY-----/, "").replace(/-----END PUBLIC KEY-----/, "").replace(/\s+/g, "");
|
|
89
|
+
var pemDer = Buffer.from(body, "base64");
|
|
90
|
+
return { key: nodeCrypto.createPublicKey(k), spki: pemDer };
|
|
91
|
+
}
|
|
92
|
+
var key = nodeCrypto.createPublicKey(k); // other key spec (best-effort SPKI export)
|
|
93
|
+
return { key: key, spki: key.export({ format: "der", type: "spki" }) };
|
|
94
|
+
} catch (e) {
|
|
95
|
+
throw new PrivacyPassError("privacy-pass/bad-key", "privacyPass: could not import issuerPublicKey: " + ((e && e.message) || e));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @primitive b.privacyPass.parseToken
|
|
101
|
+
* @signature b.privacyPass.parseToken(token)
|
|
102
|
+
* @since 0.12.52
|
|
103
|
+
* @status experimental
|
|
104
|
+
* @related b.privacyPass.verifyToken, b.privacyPass.buildChallenge
|
|
105
|
+
*
|
|
106
|
+
* Parse a Privacy Pass token (RFC 9577 §2.2) into its fields: the
|
|
107
|
+
* <code>tokenType</code>, the client <code>nonce</code>, the
|
|
108
|
+
* <code>challengeDigest</code> (SHA-256 of the TokenChallenge the token
|
|
109
|
+
* answers), the <code>tokenKeyId</code> (SHA-256 of the issuer public
|
|
110
|
+
* key), and the <code>authenticator</code>. Structural only — call
|
|
111
|
+
* <code>verifyToken</code> to check the signature.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* var t = b.privacyPass.parseToken(tokenBytes);
|
|
115
|
+
* // → { tokenType: 2, nonce, challengeDigest, tokenKeyId, authenticator }
|
|
116
|
+
*/
|
|
117
|
+
function parseToken(token) {
|
|
118
|
+
var b = _bytes(token, "token");
|
|
119
|
+
if (b.length < TOKEN_PREFIX_LEN + 1) throw new PrivacyPassError("privacy-pass/bad-token", "privacyPass.parseToken: token too short");
|
|
120
|
+
return {
|
|
121
|
+
tokenType: b.readUInt16BE(0),
|
|
122
|
+
nonce: b.slice(2, 34),
|
|
123
|
+
challengeDigest: b.slice(34, 66),
|
|
124
|
+
tokenKeyId: b.slice(66, 98),
|
|
125
|
+
authenticator: b.slice(98),
|
|
126
|
+
tokenInput: b.slice(0, TOKEN_PREFIX_LEN),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @primitive b.privacyPass.verifyToken
|
|
132
|
+
* @signature b.privacyPass.verifyToken(opts)
|
|
133
|
+
* @since 0.12.52
|
|
134
|
+
* @status experimental
|
|
135
|
+
* @compliance soc2
|
|
136
|
+
* @related b.privacyPass.buildChallenge, b.privacyPass.parseToken
|
|
137
|
+
*
|
|
138
|
+
* Verify a publicly verifiable Privacy Pass token (type 0x0002, Blind
|
|
139
|
+
* RSA — RFC 9578 §8.2). The authenticator is checked as an RSASSA-PSS
|
|
140
|
+
* (SHA-384, MGF1-SHA-384, 48-byte salt) signature over
|
|
141
|
+
* <code>token_input = token_type ‖ nonce ‖ challenge_digest ‖
|
|
142
|
+
* token_key_id</code> using the issuer's public key. The token is bound
|
|
143
|
+
* to that key — its <code>token_key_id</code> must equal the SHA-256 of
|
|
144
|
+
* the supplied key's SubjectPublicKeyInfo — and, when
|
|
145
|
+
* <code>opts.challenge</code> is given, to that challenge (its SHA-256
|
|
146
|
+
* must equal the token's <code>challenge_digest</code>), so a token
|
|
147
|
+
* minted for a different origin's challenge is refused.
|
|
148
|
+
*
|
|
149
|
+
* @opts
|
|
150
|
+
* {
|
|
151
|
+
* token: Buffer|base64, // the presented token
|
|
152
|
+
* issuerPublicKey: KeyObject|Buffer(SPKI DER)|PEM,
|
|
153
|
+
* challenge?: Buffer|base64, // the TokenChallenge this token must answer
|
|
154
|
+
* }
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* var r = b.privacyPass.verifyToken({ token: tok, issuerPublicKey: issuerSpki });
|
|
158
|
+
* // → { ok: true, tokenType: 2, nonce, challengeDigest, tokenKeyId }
|
|
159
|
+
*/
|
|
160
|
+
function verifyToken(opts) {
|
|
161
|
+
validateOpts.requireObject(opts, "privacyPass.verifyToken", PrivacyPassError);
|
|
162
|
+
validateOpts(opts, ["token", "issuerPublicKey", "challenge"], "privacyPass.verifyToken");
|
|
163
|
+
if (opts.issuerPublicKey === undefined || opts.issuerPublicKey === null) throw new PrivacyPassError("privacy-pass/bad-arg", "privacyPass.verifyToken: opts.issuerPublicKey is required");
|
|
164
|
+
|
|
165
|
+
var parsed = parseToken(opts.token);
|
|
166
|
+
if (parsed.tokenType !== TOKEN_TYPE_BLIND_RSA) {
|
|
167
|
+
throw new PrivacyPassError("privacy-pass/unsupported-token-type", "privacyPass.verifyToken: only token type 0x0002 (Blind RSA) is verifiable by the origin; got 0x" + parsed.tokenType.toString(16).padStart(4, "0")); // allow:raw-byte-literal — base-16 radix + 4-hex-digit pad, not a size
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
var imported = _importIssuerKey(opts.issuerPublicKey);
|
|
171
|
+
var key = imported.key;
|
|
172
|
+
if (key.asymmetricKeyType !== "rsa" && key.asymmetricKeyType !== "rsa-pss") {
|
|
173
|
+
throw new PrivacyPassError("privacy-pass/bad-key", "privacyPass.verifyToken: issuerPublicKey must be an RSA key for token type 0x0002");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Bind the token to the issuer key: token_key_id = SHA-256(SPKI).
|
|
177
|
+
var keyId = nodeCrypto.createHash("sha256").update(imported.spki).digest();
|
|
178
|
+
if (!bCrypto.timingSafeEqual(keyId, parsed.tokenKeyId)) {
|
|
179
|
+
throw new PrivacyPassError("privacy-pass/key-id-mismatch", "privacyPass.verifyToken: token_key_id does not match the issuer public key");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Bind the token to the challenge, when supplied.
|
|
183
|
+
if (opts.challenge !== undefined && opts.challenge !== null) {
|
|
184
|
+
var cd = nodeCrypto.createHash("sha256").update(_bytes(opts.challenge, "challenge")).digest();
|
|
185
|
+
if (!bCrypto.timingSafeEqual(cd, parsed.challengeDigest)) {
|
|
186
|
+
throw new PrivacyPassError("privacy-pass/challenge-mismatch", "privacyPass.verifyToken: challenge_digest does not match opts.challenge");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
var ok;
|
|
191
|
+
try {
|
|
192
|
+
ok = nodeCrypto.verify(PSS_HASH, parsed.tokenInput, { key: key, padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: PSS_SALT_LEN }, parsed.authenticator);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
throw new PrivacyPassError("privacy-pass/verify-threw", "privacyPass.verifyToken: signature verification threw: " + ((e && e.message) || e));
|
|
195
|
+
}
|
|
196
|
+
if (!ok) throw new PrivacyPassError("privacy-pass/bad-authenticator", "privacyPass.verifyToken: token authenticator did not verify");
|
|
197
|
+
return { ok: true, tokenType: parsed.tokenType, nonce: parsed.nonce, challengeDigest: parsed.challengeDigest, tokenKeyId: parsed.tokenKeyId };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @primitive b.privacyPass.buildChallenge
|
|
202
|
+
* @signature b.privacyPass.buildChallenge(opts)
|
|
203
|
+
* @since 0.12.52
|
|
204
|
+
* @status experimental
|
|
205
|
+
* @related b.privacyPass.verifyToken
|
|
206
|
+
*
|
|
207
|
+
* Build a TokenChallenge (RFC 9577 §2.1) and the matching
|
|
208
|
+
* <code>WWW-Authenticate: PrivateToken</code> header value an origin
|
|
209
|
+
* returns to ask a client for a token. The challenge binds the token to
|
|
210
|
+
* this issuer (and optionally this origin and a redemption context);
|
|
211
|
+
* its SHA-256 is the <code>challenge_digest</code> that
|
|
212
|
+
* <code>verifyToken</code> checks.
|
|
213
|
+
*
|
|
214
|
+
* @opts
|
|
215
|
+
* {
|
|
216
|
+
* issuerName: string, // the token issuer's name
|
|
217
|
+
* tokenType?: number, // default 0x0002 (Blind RSA)
|
|
218
|
+
* originInfo?: string, // origin name(s) the token is scoped to (default: any)
|
|
219
|
+
* redemptionContext?: Buffer, // 0 or 32 bytes (default: empty)
|
|
220
|
+
* tokenKey?: Buffer|KeyObject, // issuer SPKI, included as token-key= when given
|
|
221
|
+
* }
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* var c = b.privacyPass.buildChallenge({ issuerName: "issuer.example", originInfo: "origin.example" });
|
|
225
|
+
* res.setHeader("WWW-Authenticate", c.wwwAuthenticate);
|
|
226
|
+
*/
|
|
227
|
+
function buildChallenge(opts) {
|
|
228
|
+
validateOpts.requireObject(opts, "privacyPass.buildChallenge", PrivacyPassError);
|
|
229
|
+
validateOpts(opts, ["issuerName", "tokenType", "originInfo", "redemptionContext", "tokenKey"], "privacyPass.buildChallenge");
|
|
230
|
+
if (typeof opts.issuerName !== "string" || opts.issuerName === "") throw new PrivacyPassError("privacy-pass/bad-arg", "privacyPass.buildChallenge: opts.issuerName is required");
|
|
231
|
+
var tokenType = opts.tokenType === undefined ? TOKEN_TYPE_BLIND_RSA : opts.tokenType;
|
|
232
|
+
if (typeof tokenType !== "number" || !Number.isInteger(tokenType) || tokenType < 0 || tokenType > 0xffff) throw new PrivacyPassError("privacy-pass/bad-arg", "privacyPass.buildChallenge: tokenType must be a uint16");
|
|
233
|
+
|
|
234
|
+
var issuer = Buffer.from(opts.issuerName, "utf8");
|
|
235
|
+
if (issuer.length > 0xffff) throw new PrivacyPassError("privacy-pass/bad-arg", "privacyPass.buildChallenge: issuerName too long");
|
|
236
|
+
var origin = Buffer.alloc(0);
|
|
237
|
+
if (opts.originInfo !== undefined && opts.originInfo !== null) {
|
|
238
|
+
if (typeof opts.originInfo !== "string") throw new PrivacyPassError("privacy-pass/bad-arg", "privacyPass.buildChallenge: originInfo must be a string");
|
|
239
|
+
origin = Buffer.from(opts.originInfo, "utf8");
|
|
240
|
+
if (origin.length > 0xffff) throw new PrivacyPassError("privacy-pass/bad-arg", "privacyPass.buildChallenge: originInfo too long");
|
|
241
|
+
}
|
|
242
|
+
var rc = opts.redemptionContext !== undefined && opts.redemptionContext !== null ? _bytes(opts.redemptionContext, "redemptionContext") : Buffer.alloc(0);
|
|
243
|
+
if (rc.length !== 0 && rc.length !== 32) throw new PrivacyPassError("privacy-pass/bad-arg", "privacyPass.buildChallenge: redemptionContext must be empty or 32 bytes"); // allow:raw-byte-literal — RFC 9577 redemption_context is 0 or 32 bytes
|
|
244
|
+
|
|
245
|
+
var u16 = function (n) { return Buffer.from([(n >> 8) & 0xff, n & 0xff]); };
|
|
246
|
+
var challenge = Buffer.concat([
|
|
247
|
+
u16(tokenType),
|
|
248
|
+
u16(issuer.length), issuer,
|
|
249
|
+
Buffer.from([rc.length]), rc,
|
|
250
|
+
u16(origin.length), origin,
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
var parts = ['PrivateToken challenge="' + _b64urlPadded(challenge) + '"'];
|
|
254
|
+
if (opts.tokenKey !== undefined && opts.tokenKey !== null) {
|
|
255
|
+
var spki = (opts.tokenKey && typeof opts.tokenKey.export === "function") ? opts.tokenKey.export({ format: "der", type: "spki" }) : _bytes(opts.tokenKey, "tokenKey");
|
|
256
|
+
parts.push('token-key="' + _b64urlPadded(spki) + '"');
|
|
257
|
+
}
|
|
258
|
+
return { challenge: challenge, wwwAuthenticate: parts.join(", ") };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
parseToken: parseToken,
|
|
263
|
+
verifyToken: verifyToken,
|
|
264
|
+
buildChallenge: buildChallenge,
|
|
265
|
+
TOKEN_TYPE_BLIND_RSA: TOKEN_TYPE_BLIND_RSA,
|
|
266
|
+
PrivacyPassError: PrivacyPassError,
|
|
267
|
+
};
|
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:455224f5-2973-4bdf-8fc9-c8553709b68b",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-25T17:01:11.242Z",
|
|
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.12.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.12.52",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.12.
|
|
25
|
+
"version": "0.12.52",
|
|
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.12.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.12.52",
|
|
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.12.
|
|
57
|
+
"ref": "@blamejs/core@0.12.52",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|