@camstack/system 1.2.437 → 1.2.439
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/dist/addon-runner.js +64 -12
- package/dist/addon-runner.mjs +65 -13
- package/dist/{agent-https-BZIwS4xF.js → agent-https-DlpTMDqA.js} +252 -22
- package/dist/{agent-https-BPfHrI4c.mjs → agent-https-U0d4uvn2.mjs} +211 -23
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +7 -1
- package/dist/builtins/alerts/alerts.addon.mjs +7 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +9 -1
- package/dist/builtins/camera-grid/index.mjs +9 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +12 -5
- package/dist/builtins/console-logging/index.mjs +12 -5
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.d.ts +5 -0
- package/dist/builtins/device-manager/device-manager.addon.js +717 -28
- package/dist/builtins/device-manager/device-manager.addon.mjs +717 -28
- package/dist/builtins/device-manager/device-meta-types.d.ts +4 -0
- package/dist/builtins/device-manager/device-provider-context.d.ts +9 -0
- package/dist/builtins/device-manager/device-removal-hooks.d.ts +103 -0
- package/dist/builtins/device-manager/device-row-store.d.ts +1 -0
- package/dist/builtins/device-manager/device-tombstone-store.d.ts +68 -0
- package/dist/builtins/device-manager/removal-cleanup.d.ts +50 -0
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +37 -4
- package/dist/builtins/keychain/index.mjs +37 -4
- package/dist/builtins/keychain/keychain-service.d.ts +14 -0
- package/dist/builtins/keychain/keychain-store.d.ts +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +12 -12
- package/dist/builtins/local-auth/local-auth.addon.mjs +12 -12
- package/dist/builtins/local-network/local-network.addon.js +16 -2
- package/dist/builtins/local-network/local-network.addon.mjs +16 -2
- package/dist/builtins/loki-logging/index.js +28 -5
- package/dist/builtins/loki-logging/index.mjs +28 -5
- package/dist/builtins/native-metrics/native-metrics.addon.js +11 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +11 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +11 -1
- package/dist/builtins/snapshot/index.mjs +11 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +4 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +4 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +3 -3
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +22 -4
- package/dist/builtins/system-config/system-config.addon.mjs +22 -4
- package/dist/builtins/winston-logging/index.js +14 -5
- package/dist/builtins/winston-logging/index.mjs +14 -5
- package/dist/{composition-sources-BIT8IUKm.js → composition-sources-B4jC2FC7.js} +1 -1
- package/dist/{composition-sources-tpYR0f1x.mjs → composition-sources-BGOSCGWa.mjs} +1 -1
- package/dist/{constants-Bc6trhoW.mjs → constants-B92BjObl.mjs} +1 -1
- package/dist/{constants-CwRY32Nx.js → constants-BRsjQwi-.js} +1 -1
- package/dist/{device-ca-BhdBByGD.mjs → device-ca-46BUsWD1.mjs} +2 -2
- package/dist/{device-ca-DImD8IzR.js → device-ca-DHG9OW2m.js} +2 -2
- package/dist/{dist-CEAyXW3Q.js → dist-BOWdLjoK.js} +341 -13
- package/dist/{dist-m5JkT8UN.mjs → dist-yI6kF6r6.mjs} +306 -14
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-CBLvq2oj.js → hub-mdns-ciao-D7S5AJRU.js} +377 -69
- package/dist/{hub-mdns-ciao-DMlQTdPU.mjs → hub-mdns-ciao-DGwVc2uh.mjs} +344 -72
- package/dist/i18n/catalog-registry.d.ts +35 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +698 -22
- package/dist/index.mjs +681 -24
- package/dist/kernel/gc-census.d.ts +18 -1
- package/dist/kernel/settings-view-types.d.ts +2 -0
- package/dist/kernel/transport/local-transport.d.ts +3 -0
- package/dist/kernel/transport/peer-answered-error.d.ts +5 -1
- package/dist/{lib-NNxHSOIE.js → lib-8oZrELLm.js} +1 -1
- package/dist/{lib-BeGzVQcd.mjs → lib-DK38ompL.mjs} +1 -1
- package/dist/{restore-archive-DNxAStrJ.mjs → restore-archive-CpdUO-uS.mjs} +1 -1
- package/dist/{restore-archive-C75srVbG.js → restore-archive-DiGiI6Jp.js} +1 -1
- package/dist/{retired-settings-keys-DNfV28R_.js → retired-settings-keys-CyQ7uvta.js} +1 -1
- package/dist/{retired-settings-keys-C5qP9QIf.mjs → retired-settings-keys-UsVtLOT0.mjs} +1 -1
- package/dist/tls/agent-https.d.ts +2 -2
- package/dist/tls/boot-tls.d.ts +6 -0
- package/dist/tls/cert-evaluation.d.ts +1 -1
- package/dist/tls/cert-issue.d.ts +5 -1
- package/dist/tls/cert-manager.d.ts +18 -0
- package/dist/tls/cert-name-constraints.d.ts +53 -0
- package/dist/tls/index.d.ts +5 -4
- package/dist/tls/tls-actions.d.ts +27 -7
- package/dist/tls/tls-status.d.ts +22 -0
- package/dist/tls/uploaded-cert.d.ts +36 -4
- package/dist/{wal-maintenance-Ch79gy9S.js → wal-maintenance-DEL_jeG0.js} +0 -0
- package/dist/{wal-maintenance-DvnVO5IK.mjs → wal-maintenance-xsvdT5Go.mjs} +0 -0
- package/dist/{x509.es-r7Ht1thA.js → x509.es-C4cOo48M.js} +36 -0
- package/dist/{x509.es-CDvj67p1.mjs → x509.es-DK__ETIe.mjs} +1 -1
- package/package.json +3 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
const require_x509_es = require("./x509.es-
|
|
2
|
+
const require_x509_es = require("./x509.es-C4cOo48M.js");
|
|
3
3
|
let node_crypto = require("node:crypto");
|
|
4
4
|
let _camstack_types_node = require("@camstack/types/node");
|
|
5
5
|
let node_net = require("node:net");
|
|
@@ -101,6 +101,188 @@ function getProcessAgentHttpsNames() {
|
|
|
101
101
|
return value !== null && typeof value === "object" && typeof Reflect.get(value, "takenByOthers") === "function" ? value : null;
|
|
102
102
|
}
|
|
103
103
|
//#endregion
|
|
104
|
+
//#region src/tls/cert-name-constraints.ts
|
|
105
|
+
/**
|
|
106
|
+
* Name constraints of the hub's generated local CA (D873).
|
|
107
|
+
*
|
|
108
|
+
* An unconstrained private CA vouches for EVERY site on the internet to any
|
|
109
|
+
* device that trusts it, so a leaked CA key would let its holder impersonate
|
|
110
|
+
* a bank. The generated CA therefore carries a CRITICAL `nameConstraints`
|
|
111
|
+
* extension (RFC 5280 4.2.1.10) that limits what it can ever certify to the
|
|
112
|
+
* private address space and local-only names. A verifier that does not
|
|
113
|
+
* understand a critical extension must reject the certificate, so the
|
|
114
|
+
* constraint cannot be silently ignored by a conforming client.
|
|
115
|
+
*
|
|
116
|
+
* Pure: no filesystem, no clock. `cert-issue.ts` embeds the extension,
|
|
117
|
+
* `cert-manager.ts` filters leaf names through {@link permitsIdentity}.
|
|
118
|
+
*/
|
|
119
|
+
/** `id-ce-nameConstraints`. */
|
|
120
|
+
var NAME_CONSTRAINTS_OID = "2.5.29.30";
|
|
121
|
+
/** The address space the generated CA may certify. 100.64.0.0/10 is CGNAT, which Tailscale hands out. Loopback is for the hub's own `127.0.0.1` / `::1` SANs. */
|
|
122
|
+
var PERMITTED_IP_RANGES = [
|
|
123
|
+
"10.0.0.0/8",
|
|
124
|
+
"172.16.0.0/12",
|
|
125
|
+
"192.168.0.0/16",
|
|
126
|
+
"100.64.0.0/10",
|
|
127
|
+
"127.0.0.0/8",
|
|
128
|
+
"fc00::/7",
|
|
129
|
+
"::1/128"
|
|
130
|
+
];
|
|
131
|
+
/**
|
|
132
|
+
* Local-only DNS suffixes: none of them is delegated in public DNS. A leading
|
|
133
|
+
* dot permits every name under the suffix and not the suffix itself.
|
|
134
|
+
*/
|
|
135
|
+
var PERMITTED_DNS_SUFFIXES = [
|
|
136
|
+
".local",
|
|
137
|
+
".lan",
|
|
138
|
+
".home.arpa",
|
|
139
|
+
".internal",
|
|
140
|
+
".localdomain",
|
|
141
|
+
"localhost"
|
|
142
|
+
];
|
|
143
|
+
var LABEL_RE = /^(?!-)[a-z0-9-]{1,63}(?<!-)$/i;
|
|
144
|
+
/** A name that cannot collide with public DNS: a single label, or under a local-only suffix. */
|
|
145
|
+
function isLocalOnlyHostname(name) {
|
|
146
|
+
const lower = name.toLowerCase();
|
|
147
|
+
if (lower.length === 0 || lower.length > 253) return false;
|
|
148
|
+
if (!lower.includes(".")) return LABEL_RE.test(lower);
|
|
149
|
+
if (!PERMITTED_DNS_SUFFIXES.some((suffix) => suffix.startsWith(".") && lower.endsWith(suffix))) return false;
|
|
150
|
+
return lower.split(".").every((label) => LABEL_RE.test(label));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The constraint set for a new CA. `hostnames` are the hub's own local-only
|
|
154
|
+
* names at creation time (a single-label OS hostname has no suffix to hide
|
|
155
|
+
* under), added as exact entries; anything that could collide with public DNS
|
|
156
|
+
* is dropped here, never constrained in.
|
|
157
|
+
*/
|
|
158
|
+
function buildNameConstraintSet(hostnames) {
|
|
159
|
+
const dns = new Set(PERMITTED_DNS_SUFFIXES);
|
|
160
|
+
for (const name of hostnames) {
|
|
161
|
+
const lower = name.trim().toLowerCase();
|
|
162
|
+
if ((0, node_net.isIP)(lower) === 0 && isLocalOnlyHostname(lower)) dns.add(lower);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ipRanges: PERMITTED_IP_RANGES,
|
|
166
|
+
dnsNames: [...dns],
|
|
167
|
+
critical: true
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** The `nameConstraints` extension for a CA: permitted subtrees only, marked critical. */
|
|
171
|
+
function createNameConstraintsExtension(set) {
|
|
172
|
+
const subtrees = [...set.ipRanges.map((range) => new require_x509_es.GeneralSubtree({ base: new require_x509_es.GeneralName({ iPAddress: range }) })), ...set.dnsNames.map((name) => new require_x509_es.GeneralSubtree({ base: new require_x509_es.GeneralName({ dNSName: name }) }))];
|
|
173
|
+
const value = require_x509_es.AsnConvert.serialize(new require_x509_es.NameConstraints({ permittedSubtrees: new require_x509_es.GeneralSubtrees(subtrees) }));
|
|
174
|
+
return new require_x509_es.Extension(NAME_CONSTRAINTS_OID, set.critical, value);
|
|
175
|
+
}
|
|
176
|
+
/** The CA's constraint set, or `null` when the certificate carries no `nameConstraints`. */
|
|
177
|
+
function readNameConstraints(cert) {
|
|
178
|
+
const ext = cert.extensions.find((e) => e.type === NAME_CONSTRAINTS_OID);
|
|
179
|
+
if (ext === void 0) return null;
|
|
180
|
+
const parsed = require_x509_es.AsnConvert.parse(ext.value, require_x509_es.NameConstraints);
|
|
181
|
+
const ipRanges = [];
|
|
182
|
+
const dnsNames = [];
|
|
183
|
+
for (const subtree of parsed.permittedSubtrees ?? []) {
|
|
184
|
+
if (subtree.base.iPAddress !== void 0) ipRanges.push(subtree.base.iPAddress);
|
|
185
|
+
if (subtree.base.dNSName !== void 0) dnsNames.push(subtree.base.dNSName.toLowerCase());
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
ipRanges,
|
|
189
|
+
dnsNames,
|
|
190
|
+
critical: ext.critical
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* True when a CA is safe to keep as the hub's anchor: constrained to a
|
|
195
|
+
* non-empty permitted set AND marked critical. A non-critical constraint is
|
|
196
|
+
* ignored by clients that do not know the extension, which is the same as none.
|
|
197
|
+
*/
|
|
198
|
+
function isConstrainedCa(cert) {
|
|
199
|
+
const set = readNameConstraints(cert);
|
|
200
|
+
return set !== null && set.critical && (set.ipRanges.length > 0 || set.dnsNames.length > 0);
|
|
201
|
+
}
|
|
202
|
+
function ipv4ToBigInt(address) {
|
|
203
|
+
return address.split(".").reduce((acc, octet) => acc << 8n | BigInt(Number(octet)), 0n);
|
|
204
|
+
}
|
|
205
|
+
function ipv6ToBigInt(address) {
|
|
206
|
+
const mapped = /^(.*:)(\d+\.\d+\.\d+\.\d+)$/.exec(address);
|
|
207
|
+
let text = address;
|
|
208
|
+
if (mapped !== null) {
|
|
209
|
+
const v4 = ipv4ToBigInt(mapped[2] ?? "0.0.0.0");
|
|
210
|
+
text = `${mapped[1] ?? ""}${(v4 >> 16n & 65535n).toString(16)}:${(v4 & 65535n).toString(16)}`;
|
|
211
|
+
}
|
|
212
|
+
const halves = text.split("::");
|
|
213
|
+
if (halves.length > 2) return null;
|
|
214
|
+
const head = (halves[0] ?? "") === "" ? [] : (halves[0] ?? "").split(":");
|
|
215
|
+
const tail = halves.length === 2 && (halves[1] ?? "") !== "" ? (halves[1] ?? "").split(":") : [];
|
|
216
|
+
const fill = halves.length === 2 ? 8 - head.length - tail.length : 0;
|
|
217
|
+
if (fill < 0 || halves.length === 1 && head.length !== 8) return null;
|
|
218
|
+
return [
|
|
219
|
+
...head,
|
|
220
|
+
...Array(fill).fill("0"),
|
|
221
|
+
...tail
|
|
222
|
+
].reduce((acc, group) => acc << 16n | BigInt(Number.parseInt(group, 16)), 0n);
|
|
223
|
+
}
|
|
224
|
+
function toBigInt(address) {
|
|
225
|
+
const family = (0, node_net.isIP)(address);
|
|
226
|
+
if (family === 4) return {
|
|
227
|
+
value: ipv4ToBigInt(address),
|
|
228
|
+
bits: 32
|
|
229
|
+
};
|
|
230
|
+
if (family === 6) {
|
|
231
|
+
const value = ipv6ToBigInt(address);
|
|
232
|
+
return value === null ? null : {
|
|
233
|
+
value,
|
|
234
|
+
bits: 128
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
function ipInRange(address, range) {
|
|
240
|
+
const [base, prefixText] = range.split("/");
|
|
241
|
+
if (base === void 0 || prefixText === void 0) return false;
|
|
242
|
+
const target = toBigInt(address);
|
|
243
|
+
const net = toBigInt(base);
|
|
244
|
+
const prefix = Number(prefixText);
|
|
245
|
+
if (target === null || net === null || target.bits !== net.bits) return false;
|
|
246
|
+
if (!Number.isInteger(prefix) || prefix < 0 || prefix > net.bits) return false;
|
|
247
|
+
const shift = BigInt(net.bits - prefix);
|
|
248
|
+
return target.value >> shift === net.value >> shift;
|
|
249
|
+
}
|
|
250
|
+
/** RFC 5280 DNS matching: a leading dot is a strict subtree, a bare name is itself and everything under it. */
|
|
251
|
+
function dnsPermitted(name, constraint) {
|
|
252
|
+
const lower = name.toLowerCase();
|
|
253
|
+
if (constraint.startsWith(".")) return lower.endsWith(constraint);
|
|
254
|
+
return lower === constraint || lower.endsWith(`.${constraint}`);
|
|
255
|
+
}
|
|
256
|
+
/** Is this SAN value (an IP literal or a DNS name) inside the permitted set? */
|
|
257
|
+
function permitsName(set, value) {
|
|
258
|
+
if ((0, node_net.isIP)(value) !== 0) return set.ipRanges.some((range) => ipInRange(value, range));
|
|
259
|
+
return set.dnsNames.some((constraint) => dnsPermitted(value, constraint));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The part of an identity the CA is allowed to certify. A leaf naming
|
|
263
|
+
* anything outside the constraints makes the WHOLE chain fail validation in
|
|
264
|
+
* OpenSSL, so such names are dropped (and reported), never signed. The
|
|
265
|
+
* required lists shrink the same way: otherwise a name the CA cannot certify
|
|
266
|
+
* would count as a permanent SAN gap and reissue the leaf on every boot.
|
|
267
|
+
*/
|
|
268
|
+
function permitsIdentity(set, identity) {
|
|
269
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
270
|
+
const keep = (values) => values.filter((value) => {
|
|
271
|
+
if (permitsName(set, value)) return true;
|
|
272
|
+
dropped.add(value);
|
|
273
|
+
return false;
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
identity: {
|
|
277
|
+
dnsNames: keep(identity.dnsNames),
|
|
278
|
+
ipAddresses: keep(identity.ipAddresses),
|
|
279
|
+
requiredDnsNames: keep(identity.requiredDnsNames),
|
|
280
|
+
requiredIpAddresses: keep(identity.requiredIpAddresses)
|
|
281
|
+
},
|
|
282
|
+
dropped: [...dropped]
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
104
286
|
//#region src/tls/cert-evaluation.ts
|
|
105
287
|
/**
|
|
106
288
|
* The regeneration rule (D227), as a pure function.
|
|
@@ -129,7 +311,8 @@ var REASONS_REQUIRING_NEW_CA = [
|
|
|
129
311
|
"missing",
|
|
130
312
|
"unreadable",
|
|
131
313
|
"no-local-ca",
|
|
132
|
-
"ca-expiring"
|
|
314
|
+
"ca-expiring",
|
|
315
|
+
"ca-unconstrained"
|
|
133
316
|
];
|
|
134
317
|
function reasonRequiresNewCa(reason) {
|
|
135
318
|
return REASONS_REQUIRING_NEW_CA.includes(reason);
|
|
@@ -141,6 +324,14 @@ function parse(pem) {
|
|
|
141
324
|
return null;
|
|
142
325
|
}
|
|
143
326
|
}
|
|
327
|
+
/** A root without a CRITICAL name constraint (every pre-D873 install) can vouch for any site. */
|
|
328
|
+
function caIsUnconstrained(caPem) {
|
|
329
|
+
try {
|
|
330
|
+
return !isConstrainedCa(new require_x509_es.X509Certificate(caPem));
|
|
331
|
+
} catch {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
144
335
|
function daysBetween(from, to) {
|
|
145
336
|
return (to.getTime() - from.getTime()) / MS_PER_DAY$1;
|
|
146
337
|
}
|
|
@@ -171,6 +362,7 @@ function evaluateExistingCert(input) {
|
|
|
171
362
|
const ca = parse(input.caPem);
|
|
172
363
|
if (leaf === null || ca === null) return "unreadable";
|
|
173
364
|
if (daysBetween(input.now, new Date(ca.validTo)) < 427) return "ca-expiring";
|
|
365
|
+
if (caIsUnconstrained(input.caPem)) return "ca-unconstrained";
|
|
174
366
|
if (!keyMatches(leaf, input.keyPem)) return "key-mismatch";
|
|
175
367
|
if (daysBetween(input.now, new Date(leaf.validTo)) < 30) return "expiring";
|
|
176
368
|
if (!hasServerAuthEku(leaf)) return "missing-server-auth-eku";
|
|
@@ -383,13 +575,18 @@ function subjectAltNames(identity) {
|
|
|
383
575
|
* A local root: 10 years, `pathlen:0` so it can sign leaves and nothing
|
|
384
576
|
* else, and **no EKU** — Apple rejects an anchor asserting
|
|
385
577
|
* `anyExtendedKeyUsage`, and a root with no EKU constrains nothing.
|
|
578
|
+
*
|
|
579
|
+
* It carries a CRITICAL `nameConstraints` extension (D873): private address
|
|
580
|
+
* ranges and local-only DNS names, plus `hostnames` (the hub's own single-label
|
|
581
|
+
* names at creation). A leaked key therefore cannot vouch for a public site.
|
|
386
582
|
*/
|
|
387
|
-
async function generateCa(now) {
|
|
583
|
+
async function generateCa(now, hostnames = []) {
|
|
388
584
|
const keys = await node_crypto.webcrypto.subtle.generateKey(SIGNING_ALGORITHM, true, ["sign", "verify"]);
|
|
389
585
|
const extensions = [
|
|
390
586
|
new require_x509_es.BasicConstraintsExtension(true, 0, true),
|
|
391
587
|
new require_x509_es.KeyUsagesExtension(require_x509_es.KeyUsageFlags.keyCertSign | require_x509_es.KeyUsageFlags.cRLSign, true),
|
|
392
|
-
await require_x509_es.SubjectKeyIdentifierExtension.create(keys.publicKey)
|
|
588
|
+
await require_x509_es.SubjectKeyIdentifierExtension.create(keys.publicKey),
|
|
589
|
+
createNameConstraintsExtension(buildNameConstraintSet(hostnames))
|
|
393
590
|
];
|
|
394
591
|
const cert = await require_x509_es.X509CertificateGenerator.createSelfSigned({
|
|
395
592
|
serialNumber: freshSerial(),
|
|
@@ -652,14 +849,6 @@ function describeHttpsLeaf(certPem) {
|
|
|
652
849
|
notAfter: Date.parse(x509.validTo)
|
|
653
850
|
};
|
|
654
851
|
}
|
|
655
|
-
var LOCAL_SUFFIXES = [
|
|
656
|
-
".local",
|
|
657
|
-
".lan",
|
|
658
|
-
".home.arpa",
|
|
659
|
-
".internal",
|
|
660
|
-
".localdomain"
|
|
661
|
-
];
|
|
662
|
-
var LABEL_RE = /^(?!-)[a-z0-9-]{1,63}(?<!-)$/i;
|
|
663
852
|
/** Private-network IPv4/IPv6: RFC 1918, CGNAT (Tailscale), link-local, ULA. */
|
|
664
853
|
function isPrivateAddress(address) {
|
|
665
854
|
const family = (0, node_net.isIP)(address);
|
|
@@ -677,14 +866,6 @@ function isPrivateAddress(address) {
|
|
|
677
866
|
function isContainerBridgeAddress(address) {
|
|
678
867
|
return /^172\.17\.\d{1,3}\.\d{1,3}$/.test(address);
|
|
679
868
|
}
|
|
680
|
-
/** A name that cannot collide with public DNS. */
|
|
681
|
-
function isLocalOnlyHostname(name) {
|
|
682
|
-
const lower = name.toLowerCase();
|
|
683
|
-
if (lower.length === 0 || lower.length > 253) return false;
|
|
684
|
-
if (!lower.includes(".")) return LABEL_RE.test(lower);
|
|
685
|
-
if (!LOCAL_SUFFIXES.some((suffix) => lower.endsWith(suffix))) return false;
|
|
686
|
-
return lower.split(".").every((label) => LABEL_RE.test(label));
|
|
687
|
-
}
|
|
688
869
|
/**
|
|
689
870
|
* The names the hub will sign for `nodeId`: its node id and the address of its live mesh
|
|
690
871
|
* socket - nothing the agent says about itself. `excluded` holds every name already taken
|
|
@@ -737,14 +918,21 @@ async function issueAgentHttpsLeaf(input) {
|
|
|
737
918
|
if (!(0, node_fs.existsSync)(caCertPath) || !(0, node_fs.existsSync)(caKeyPath)) throw new Error("the hub has no local CA to sign with (no tls/camstack-ca.crt + key)");
|
|
738
919
|
const excluded = new Set(input.takenByOthers ?? []);
|
|
739
920
|
if ((0, node_fs.existsSync)(hubLeafPath)) for (const name of certificateNames((0, node_fs.readFileSync)(hubLeafPath, "utf8"))) excluded.add(name);
|
|
740
|
-
const
|
|
921
|
+
const ca = await loadCa((0, node_fs.readFileSync)(caCertPath, "utf8"), (0, node_fs.readFileSync)(caKeyPath, "utf8"));
|
|
922
|
+
const policy = acceptAgentSans({
|
|
741
923
|
nodeId: input.nodeId,
|
|
742
924
|
observedAddress: input.observedAddress,
|
|
743
925
|
excluded,
|
|
744
926
|
...input.reportedNames !== void 0 ? { reported: input.reportedNames } : {}
|
|
745
927
|
});
|
|
928
|
+
const constraints = readNameConstraints(ca.cert);
|
|
929
|
+
const constrained = constraints === null ? null : permitsIdentity(constraints, policy.identity);
|
|
930
|
+
const sans = constrained === null ? policy : {
|
|
931
|
+
identity: constrained.identity,
|
|
932
|
+
accepted: policy.accepted.filter((name) => !constrained.dropped.includes(name)),
|
|
933
|
+
rejected: [...policy.rejected, ...constrained.dropped.map((name) => `${name} (outside the local CA's name constraints)`)]
|
|
934
|
+
};
|
|
746
935
|
if (sans.accepted.length === 0) throw new Error(`no name of node '${input.nodeId}' can be certified (${sans.rejected.join("; ") || "nothing offered"})`);
|
|
747
|
-
const ca = await loadCa((0, node_fs.readFileSync)(caCertPath, "utf8"), (0, node_fs.readFileSync)(caKeyPath, "utf8"));
|
|
748
936
|
return {
|
|
749
937
|
certPem: await issueLeafFromCsr(ca, input.csrPem, sans.identity, input.nodeId, 397, input.now ?? /* @__PURE__ */ new Date()),
|
|
750
938
|
caCertPem: ca.certPem,
|
|
@@ -805,6 +993,18 @@ Object.defineProperty(exports, "MAX_LEAF_VALIDITY_DAYS", {
|
|
|
805
993
|
return MAX_LEAF_VALIDITY_DAYS;
|
|
806
994
|
}
|
|
807
995
|
});
|
|
996
|
+
Object.defineProperty(exports, "PERMITTED_DNS_SUFFIXES", {
|
|
997
|
+
enumerable: true,
|
|
998
|
+
get: function() {
|
|
999
|
+
return PERMITTED_DNS_SUFFIXES;
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
Object.defineProperty(exports, "PERMITTED_IP_RANGES", {
|
|
1003
|
+
enumerable: true,
|
|
1004
|
+
get: function() {
|
|
1005
|
+
return PERMITTED_IP_RANGES;
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
808
1008
|
Object.defineProperty(exports, "SERVER_AUTH_OID", {
|
|
809
1009
|
enumerable: true,
|
|
810
1010
|
get: function() {
|
|
@@ -823,6 +1023,12 @@ Object.defineProperty(exports, "agentHttpsPaths", {
|
|
|
823
1023
|
return agentHttpsPaths;
|
|
824
1024
|
}
|
|
825
1025
|
});
|
|
1026
|
+
Object.defineProperty(exports, "buildNameConstraintSet", {
|
|
1027
|
+
enumerable: true,
|
|
1028
|
+
get: function() {
|
|
1029
|
+
return buildNameConstraintSet;
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
826
1032
|
Object.defineProperty(exports, "certificateNames", {
|
|
827
1033
|
enumerable: true,
|
|
828
1034
|
get: function() {
|
|
@@ -877,6 +1083,12 @@ Object.defineProperty(exports, "installAgentHttps", {
|
|
|
877
1083
|
return installAgentHttps;
|
|
878
1084
|
}
|
|
879
1085
|
});
|
|
1086
|
+
Object.defineProperty(exports, "isConstrainedCa", {
|
|
1087
|
+
enumerable: true,
|
|
1088
|
+
get: function() {
|
|
1089
|
+
return isConstrainedCa;
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
880
1092
|
Object.defineProperty(exports, "isContainerBridgeAddress", {
|
|
881
1093
|
enumerable: true,
|
|
882
1094
|
get: function() {
|
|
@@ -925,12 +1137,30 @@ Object.defineProperty(exports, "pemDigest", {
|
|
|
925
1137
|
return pemDigest;
|
|
926
1138
|
}
|
|
927
1139
|
});
|
|
1140
|
+
Object.defineProperty(exports, "permitsIdentity", {
|
|
1141
|
+
enumerable: true,
|
|
1142
|
+
get: function() {
|
|
1143
|
+
return permitsIdentity;
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
Object.defineProperty(exports, "permitsName", {
|
|
1147
|
+
enumerable: true,
|
|
1148
|
+
get: function() {
|
|
1149
|
+
return permitsName;
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
928
1152
|
Object.defineProperty(exports, "pinnedHttpsCaFingerprint", {
|
|
929
1153
|
enumerable: true,
|
|
930
1154
|
get: function() {
|
|
931
1155
|
return pinnedHttpsCaFingerprint;
|
|
932
1156
|
}
|
|
933
1157
|
});
|
|
1158
|
+
Object.defineProperty(exports, "readNameConstraints", {
|
|
1159
|
+
enumerable: true,
|
|
1160
|
+
get: function() {
|
|
1161
|
+
return readNameConstraints;
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
934
1164
|
Object.defineProperty(exports, "reasonRequiresNewCa", {
|
|
935
1165
|
enumerable: true,
|
|
936
1166
|
get: function() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { S as backdatedNotBefore, _ as NameConstraints, a as KeyUsageFlags, c as Pkcs10CertificateRequest, d as SubjectKeyIdentifierExtension, f as X509Certificate$1, g as GeneralSubtrees, h as GeneralSubtree, i as Extension, l as Pkcs10CertificateRequestGenerator, m as cryptoProvider, n as BasicConstraintsExtension, o as KeyUsagesExtension, p as X509CertificateGenerator, r as ExtendedKeyUsageExtension, s as PemConverter, t as AuthorityKeyIdentifierExtension, u as SubjectAlternativeNameExtension, v as GeneralName, x as AsnConvert } from "./x509.es-DK__ETIe.mjs";
|
|
2
2
|
import { X509Certificate, createHash, createPrivateKey, randomBytes, webcrypto } from "node:crypto";
|
|
3
3
|
import { writeOwnerOnlyFileSync } from "@camstack/types/node";
|
|
4
4
|
import { isIP } from "node:net";
|
|
@@ -99,6 +99,188 @@ function getProcessAgentHttpsNames() {
|
|
|
99
99
|
return value !== null && typeof value === "object" && typeof Reflect.get(value, "takenByOthers") === "function" ? value : null;
|
|
100
100
|
}
|
|
101
101
|
//#endregion
|
|
102
|
+
//#region src/tls/cert-name-constraints.ts
|
|
103
|
+
/**
|
|
104
|
+
* Name constraints of the hub's generated local CA (D873).
|
|
105
|
+
*
|
|
106
|
+
* An unconstrained private CA vouches for EVERY site on the internet to any
|
|
107
|
+
* device that trusts it, so a leaked CA key would let its holder impersonate
|
|
108
|
+
* a bank. The generated CA therefore carries a CRITICAL `nameConstraints`
|
|
109
|
+
* extension (RFC 5280 4.2.1.10) that limits what it can ever certify to the
|
|
110
|
+
* private address space and local-only names. A verifier that does not
|
|
111
|
+
* understand a critical extension must reject the certificate, so the
|
|
112
|
+
* constraint cannot be silently ignored by a conforming client.
|
|
113
|
+
*
|
|
114
|
+
* Pure: no filesystem, no clock. `cert-issue.ts` embeds the extension,
|
|
115
|
+
* `cert-manager.ts` filters leaf names through {@link permitsIdentity}.
|
|
116
|
+
*/
|
|
117
|
+
/** `id-ce-nameConstraints`. */
|
|
118
|
+
var NAME_CONSTRAINTS_OID = "2.5.29.30";
|
|
119
|
+
/** The address space the generated CA may certify. 100.64.0.0/10 is CGNAT, which Tailscale hands out. Loopback is for the hub's own `127.0.0.1` / `::1` SANs. */
|
|
120
|
+
var PERMITTED_IP_RANGES = [
|
|
121
|
+
"10.0.0.0/8",
|
|
122
|
+
"172.16.0.0/12",
|
|
123
|
+
"192.168.0.0/16",
|
|
124
|
+
"100.64.0.0/10",
|
|
125
|
+
"127.0.0.0/8",
|
|
126
|
+
"fc00::/7",
|
|
127
|
+
"::1/128"
|
|
128
|
+
];
|
|
129
|
+
/**
|
|
130
|
+
* Local-only DNS suffixes: none of them is delegated in public DNS. A leading
|
|
131
|
+
* dot permits every name under the suffix and not the suffix itself.
|
|
132
|
+
*/
|
|
133
|
+
var PERMITTED_DNS_SUFFIXES = [
|
|
134
|
+
".local",
|
|
135
|
+
".lan",
|
|
136
|
+
".home.arpa",
|
|
137
|
+
".internal",
|
|
138
|
+
".localdomain",
|
|
139
|
+
"localhost"
|
|
140
|
+
];
|
|
141
|
+
var LABEL_RE = /^(?!-)[a-z0-9-]{1,63}(?<!-)$/i;
|
|
142
|
+
/** A name that cannot collide with public DNS: a single label, or under a local-only suffix. */
|
|
143
|
+
function isLocalOnlyHostname(name) {
|
|
144
|
+
const lower = name.toLowerCase();
|
|
145
|
+
if (lower.length === 0 || lower.length > 253) return false;
|
|
146
|
+
if (!lower.includes(".")) return LABEL_RE.test(lower);
|
|
147
|
+
if (!PERMITTED_DNS_SUFFIXES.some((suffix) => suffix.startsWith(".") && lower.endsWith(suffix))) return false;
|
|
148
|
+
return lower.split(".").every((label) => LABEL_RE.test(label));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The constraint set for a new CA. `hostnames` are the hub's own local-only
|
|
152
|
+
* names at creation time (a single-label OS hostname has no suffix to hide
|
|
153
|
+
* under), added as exact entries; anything that could collide with public DNS
|
|
154
|
+
* is dropped here, never constrained in.
|
|
155
|
+
*/
|
|
156
|
+
function buildNameConstraintSet(hostnames) {
|
|
157
|
+
const dns = new Set(PERMITTED_DNS_SUFFIXES);
|
|
158
|
+
for (const name of hostnames) {
|
|
159
|
+
const lower = name.trim().toLowerCase();
|
|
160
|
+
if (isIP(lower) === 0 && isLocalOnlyHostname(lower)) dns.add(lower);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
ipRanges: PERMITTED_IP_RANGES,
|
|
164
|
+
dnsNames: [...dns],
|
|
165
|
+
critical: true
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The `nameConstraints` extension for a CA: permitted subtrees only, marked critical. */
|
|
169
|
+
function createNameConstraintsExtension(set) {
|
|
170
|
+
const subtrees = [...set.ipRanges.map((range) => new GeneralSubtree({ base: new GeneralName({ iPAddress: range }) })), ...set.dnsNames.map((name) => new GeneralSubtree({ base: new GeneralName({ dNSName: name }) }))];
|
|
171
|
+
const value = AsnConvert.serialize(new NameConstraints({ permittedSubtrees: new GeneralSubtrees(subtrees) }));
|
|
172
|
+
return new Extension(NAME_CONSTRAINTS_OID, set.critical, value);
|
|
173
|
+
}
|
|
174
|
+
/** The CA's constraint set, or `null` when the certificate carries no `nameConstraints`. */
|
|
175
|
+
function readNameConstraints(cert) {
|
|
176
|
+
const ext = cert.extensions.find((e) => e.type === NAME_CONSTRAINTS_OID);
|
|
177
|
+
if (ext === void 0) return null;
|
|
178
|
+
const parsed = AsnConvert.parse(ext.value, NameConstraints);
|
|
179
|
+
const ipRanges = [];
|
|
180
|
+
const dnsNames = [];
|
|
181
|
+
for (const subtree of parsed.permittedSubtrees ?? []) {
|
|
182
|
+
if (subtree.base.iPAddress !== void 0) ipRanges.push(subtree.base.iPAddress);
|
|
183
|
+
if (subtree.base.dNSName !== void 0) dnsNames.push(subtree.base.dNSName.toLowerCase());
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
ipRanges,
|
|
187
|
+
dnsNames,
|
|
188
|
+
critical: ext.critical
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* True when a CA is safe to keep as the hub's anchor: constrained to a
|
|
193
|
+
* non-empty permitted set AND marked critical. A non-critical constraint is
|
|
194
|
+
* ignored by clients that do not know the extension, which is the same as none.
|
|
195
|
+
*/
|
|
196
|
+
function isConstrainedCa(cert) {
|
|
197
|
+
const set = readNameConstraints(cert);
|
|
198
|
+
return set !== null && set.critical && (set.ipRanges.length > 0 || set.dnsNames.length > 0);
|
|
199
|
+
}
|
|
200
|
+
function ipv4ToBigInt(address) {
|
|
201
|
+
return address.split(".").reduce((acc, octet) => acc << 8n | BigInt(Number(octet)), 0n);
|
|
202
|
+
}
|
|
203
|
+
function ipv6ToBigInt(address) {
|
|
204
|
+
const mapped = /^(.*:)(\d+\.\d+\.\d+\.\d+)$/.exec(address);
|
|
205
|
+
let text = address;
|
|
206
|
+
if (mapped !== null) {
|
|
207
|
+
const v4 = ipv4ToBigInt(mapped[2] ?? "0.0.0.0");
|
|
208
|
+
text = `${mapped[1] ?? ""}${(v4 >> 16n & 65535n).toString(16)}:${(v4 & 65535n).toString(16)}`;
|
|
209
|
+
}
|
|
210
|
+
const halves = text.split("::");
|
|
211
|
+
if (halves.length > 2) return null;
|
|
212
|
+
const head = (halves[0] ?? "") === "" ? [] : (halves[0] ?? "").split(":");
|
|
213
|
+
const tail = halves.length === 2 && (halves[1] ?? "") !== "" ? (halves[1] ?? "").split(":") : [];
|
|
214
|
+
const fill = halves.length === 2 ? 8 - head.length - tail.length : 0;
|
|
215
|
+
if (fill < 0 || halves.length === 1 && head.length !== 8) return null;
|
|
216
|
+
return [
|
|
217
|
+
...head,
|
|
218
|
+
...Array(fill).fill("0"),
|
|
219
|
+
...tail
|
|
220
|
+
].reduce((acc, group) => acc << 16n | BigInt(Number.parseInt(group, 16)), 0n);
|
|
221
|
+
}
|
|
222
|
+
function toBigInt(address) {
|
|
223
|
+
const family = isIP(address);
|
|
224
|
+
if (family === 4) return {
|
|
225
|
+
value: ipv4ToBigInt(address),
|
|
226
|
+
bits: 32
|
|
227
|
+
};
|
|
228
|
+
if (family === 6) {
|
|
229
|
+
const value = ipv6ToBigInt(address);
|
|
230
|
+
return value === null ? null : {
|
|
231
|
+
value,
|
|
232
|
+
bits: 128
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
function ipInRange(address, range) {
|
|
238
|
+
const [base, prefixText] = range.split("/");
|
|
239
|
+
if (base === void 0 || prefixText === void 0) return false;
|
|
240
|
+
const target = toBigInt(address);
|
|
241
|
+
const net = toBigInt(base);
|
|
242
|
+
const prefix = Number(prefixText);
|
|
243
|
+
if (target === null || net === null || target.bits !== net.bits) return false;
|
|
244
|
+
if (!Number.isInteger(prefix) || prefix < 0 || prefix > net.bits) return false;
|
|
245
|
+
const shift = BigInt(net.bits - prefix);
|
|
246
|
+
return target.value >> shift === net.value >> shift;
|
|
247
|
+
}
|
|
248
|
+
/** RFC 5280 DNS matching: a leading dot is a strict subtree, a bare name is itself and everything under it. */
|
|
249
|
+
function dnsPermitted(name, constraint) {
|
|
250
|
+
const lower = name.toLowerCase();
|
|
251
|
+
if (constraint.startsWith(".")) return lower.endsWith(constraint);
|
|
252
|
+
return lower === constraint || lower.endsWith(`.${constraint}`);
|
|
253
|
+
}
|
|
254
|
+
/** Is this SAN value (an IP literal or a DNS name) inside the permitted set? */
|
|
255
|
+
function permitsName(set, value) {
|
|
256
|
+
if (isIP(value) !== 0) return set.ipRanges.some((range) => ipInRange(value, range));
|
|
257
|
+
return set.dnsNames.some((constraint) => dnsPermitted(value, constraint));
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The part of an identity the CA is allowed to certify. A leaf naming
|
|
261
|
+
* anything outside the constraints makes the WHOLE chain fail validation in
|
|
262
|
+
* OpenSSL, so such names are dropped (and reported), never signed. The
|
|
263
|
+
* required lists shrink the same way: otherwise a name the CA cannot certify
|
|
264
|
+
* would count as a permanent SAN gap and reissue the leaf on every boot.
|
|
265
|
+
*/
|
|
266
|
+
function permitsIdentity(set, identity) {
|
|
267
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
268
|
+
const keep = (values) => values.filter((value) => {
|
|
269
|
+
if (permitsName(set, value)) return true;
|
|
270
|
+
dropped.add(value);
|
|
271
|
+
return false;
|
|
272
|
+
});
|
|
273
|
+
return {
|
|
274
|
+
identity: {
|
|
275
|
+
dnsNames: keep(identity.dnsNames),
|
|
276
|
+
ipAddresses: keep(identity.ipAddresses),
|
|
277
|
+
requiredDnsNames: keep(identity.requiredDnsNames),
|
|
278
|
+
requiredIpAddresses: keep(identity.requiredIpAddresses)
|
|
279
|
+
},
|
|
280
|
+
dropped: [...dropped]
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
102
284
|
//#region src/tls/cert-evaluation.ts
|
|
103
285
|
/**
|
|
104
286
|
* The regeneration rule (D227), as a pure function.
|
|
@@ -127,7 +309,8 @@ var REASONS_REQUIRING_NEW_CA = [
|
|
|
127
309
|
"missing",
|
|
128
310
|
"unreadable",
|
|
129
311
|
"no-local-ca",
|
|
130
|
-
"ca-expiring"
|
|
312
|
+
"ca-expiring",
|
|
313
|
+
"ca-unconstrained"
|
|
131
314
|
];
|
|
132
315
|
function reasonRequiresNewCa(reason) {
|
|
133
316
|
return REASONS_REQUIRING_NEW_CA.includes(reason);
|
|
@@ -139,6 +322,14 @@ function parse(pem) {
|
|
|
139
322
|
return null;
|
|
140
323
|
}
|
|
141
324
|
}
|
|
325
|
+
/** A root without a CRITICAL name constraint (every pre-D873 install) can vouch for any site. */
|
|
326
|
+
function caIsUnconstrained(caPem) {
|
|
327
|
+
try {
|
|
328
|
+
return !isConstrainedCa(new X509Certificate$1(caPem));
|
|
329
|
+
} catch {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
142
333
|
function daysBetween(from, to) {
|
|
143
334
|
return (to.getTime() - from.getTime()) / MS_PER_DAY$1;
|
|
144
335
|
}
|
|
@@ -169,6 +360,7 @@ function evaluateExistingCert(input) {
|
|
|
169
360
|
const ca = parse(input.caPem);
|
|
170
361
|
if (leaf === null || ca === null) return "unreadable";
|
|
171
362
|
if (daysBetween(input.now, new Date(ca.validTo)) < 427) return "ca-expiring";
|
|
363
|
+
if (caIsUnconstrained(input.caPem)) return "ca-unconstrained";
|
|
172
364
|
if (!keyMatches(leaf, input.keyPem)) return "key-mismatch";
|
|
173
365
|
if (daysBetween(input.now, new Date(leaf.validTo)) < 30) return "expiring";
|
|
174
366
|
if (!hasServerAuthEku(leaf)) return "missing-server-auth-eku";
|
|
@@ -381,13 +573,18 @@ function subjectAltNames(identity) {
|
|
|
381
573
|
* A local root: 10 years, `pathlen:0` so it can sign leaves and nothing
|
|
382
574
|
* else, and **no EKU** — Apple rejects an anchor asserting
|
|
383
575
|
* `anyExtendedKeyUsage`, and a root with no EKU constrains nothing.
|
|
576
|
+
*
|
|
577
|
+
* It carries a CRITICAL `nameConstraints` extension (D873): private address
|
|
578
|
+
* ranges and local-only DNS names, plus `hostnames` (the hub's own single-label
|
|
579
|
+
* names at creation). A leaked key therefore cannot vouch for a public site.
|
|
384
580
|
*/
|
|
385
|
-
async function generateCa(now) {
|
|
581
|
+
async function generateCa(now, hostnames = []) {
|
|
386
582
|
const keys = await webcrypto.subtle.generateKey(SIGNING_ALGORITHM, true, ["sign", "verify"]);
|
|
387
583
|
const extensions = [
|
|
388
584
|
new BasicConstraintsExtension(true, 0, true),
|
|
389
585
|
new KeyUsagesExtension(KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign, true),
|
|
390
|
-
await SubjectKeyIdentifierExtension.create(keys.publicKey)
|
|
586
|
+
await SubjectKeyIdentifierExtension.create(keys.publicKey),
|
|
587
|
+
createNameConstraintsExtension(buildNameConstraintSet(hostnames))
|
|
391
588
|
];
|
|
392
589
|
const cert = await X509CertificateGenerator.createSelfSigned({
|
|
393
590
|
serialNumber: freshSerial(),
|
|
@@ -650,14 +847,6 @@ function describeHttpsLeaf(certPem) {
|
|
|
650
847
|
notAfter: Date.parse(x509.validTo)
|
|
651
848
|
};
|
|
652
849
|
}
|
|
653
|
-
var LOCAL_SUFFIXES = [
|
|
654
|
-
".local",
|
|
655
|
-
".lan",
|
|
656
|
-
".home.arpa",
|
|
657
|
-
".internal",
|
|
658
|
-
".localdomain"
|
|
659
|
-
];
|
|
660
|
-
var LABEL_RE = /^(?!-)[a-z0-9-]{1,63}(?<!-)$/i;
|
|
661
850
|
/** Private-network IPv4/IPv6: RFC 1918, CGNAT (Tailscale), link-local, ULA. */
|
|
662
851
|
function isPrivateAddress(address) {
|
|
663
852
|
const family = isIP(address);
|
|
@@ -675,14 +864,6 @@ function isPrivateAddress(address) {
|
|
|
675
864
|
function isContainerBridgeAddress(address) {
|
|
676
865
|
return /^172\.17\.\d{1,3}\.\d{1,3}$/.test(address);
|
|
677
866
|
}
|
|
678
|
-
/** A name that cannot collide with public DNS. */
|
|
679
|
-
function isLocalOnlyHostname(name) {
|
|
680
|
-
const lower = name.toLowerCase();
|
|
681
|
-
if (lower.length === 0 || lower.length > 253) return false;
|
|
682
|
-
if (!lower.includes(".")) return LABEL_RE.test(lower);
|
|
683
|
-
if (!LOCAL_SUFFIXES.some((suffix) => lower.endsWith(suffix))) return false;
|
|
684
|
-
return lower.split(".").every((label) => LABEL_RE.test(label));
|
|
685
|
-
}
|
|
686
867
|
/**
|
|
687
868
|
* The names the hub will sign for `nodeId`: its node id and the address of its live mesh
|
|
688
869
|
* socket - nothing the agent says about itself. `excluded` holds every name already taken
|
|
@@ -735,14 +916,21 @@ async function issueAgentHttpsLeaf(input) {
|
|
|
735
916
|
if (!existsSync(caCertPath) || !existsSync(caKeyPath)) throw new Error("the hub has no local CA to sign with (no tls/camstack-ca.crt + key)");
|
|
736
917
|
const excluded = new Set(input.takenByOthers ?? []);
|
|
737
918
|
if (existsSync(hubLeafPath)) for (const name of certificateNames(readFileSync(hubLeafPath, "utf8"))) excluded.add(name);
|
|
738
|
-
const
|
|
919
|
+
const ca = await loadCa(readFileSync(caCertPath, "utf8"), readFileSync(caKeyPath, "utf8"));
|
|
920
|
+
const policy = acceptAgentSans({
|
|
739
921
|
nodeId: input.nodeId,
|
|
740
922
|
observedAddress: input.observedAddress,
|
|
741
923
|
excluded,
|
|
742
924
|
...input.reportedNames !== void 0 ? { reported: input.reportedNames } : {}
|
|
743
925
|
});
|
|
926
|
+
const constraints = readNameConstraints(ca.cert);
|
|
927
|
+
const constrained = constraints === null ? null : permitsIdentity(constraints, policy.identity);
|
|
928
|
+
const sans = constrained === null ? policy : {
|
|
929
|
+
identity: constrained.identity,
|
|
930
|
+
accepted: policy.accepted.filter((name) => !constrained.dropped.includes(name)),
|
|
931
|
+
rejected: [...policy.rejected, ...constrained.dropped.map((name) => `${name} (outside the local CA's name constraints)`)]
|
|
932
|
+
};
|
|
744
933
|
if (sans.accepted.length === 0) throw new Error(`no name of node '${input.nodeId}' can be certified (${sans.rejected.join("; ") || "nothing offered"})`);
|
|
745
|
-
const ca = await loadCa(readFileSync(caCertPath, "utf8"), readFileSync(caKeyPath, "utf8"));
|
|
746
934
|
return {
|
|
747
935
|
certPem: await issueLeafFromCsr(ca, input.csrPem, sans.identity, input.nodeId, 397, input.now ?? /* @__PURE__ */ new Date()),
|
|
748
936
|
caCertPem: ca.certPem,
|
|
@@ -755,4 +943,4 @@ function pemDigest(pem) {
|
|
|
755
943
|
return createHash("sha256").update(pem).digest("hex").slice(0, 16);
|
|
756
944
|
}
|
|
757
945
|
//#endregion
|
|
758
|
-
export {
|
|
946
|
+
export { isConstrainedCa as A, MAX_LEAF_VALIDITY_DAYS as C, PERMITTED_DNS_SUFFIXES as D, reasonRequiresNewCa as E, AGENT_HTTPS_NAMES_COLLECTION as F, AGENT_HTTPS_NAMES_COLUMNS as I, AgentHttpsNames as L, permitsIdentity as M, permitsName as N, PERMITTED_IP_RANGES as O, readNameConstraints as P, getProcessAgentHttpsNames as R, LEAF_RENEWAL_WINDOW_DAYS as S, evaluateExistingCert as T, issueLeaf as _, collectAgentNames as a, systemHostFacts as b, installAgentHttps as c, issueAgentHttpsLeaf as d, loadAgentHttps as f, generateCa as g, CA_COMMON_NAME as h, certificateNames as i, isLocalOnlyHostname as j, buildNameConstraintSet as k, isContainerBridgeAddress as l, pinnedHttpsCaFingerprint as m, acceptAgentSans as n, createAgentHttpsRequest as o, pemDigest as p, agentHttpsPaths as r, describeHttpsLeaf as s, AgentHttpsInstallError as t, isPrivateAddress as u, loadCa as v, SERVER_AUTH_OID as w, CA_VALIDITY_DAYS as x, collectCertIdentity as y, setProcessAgentHttpsNames as z };
|