@camstack/system 1.2.432 → 1.2.434
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/{tls-CpcGkeJV.mjs → agent-https-B8lKYVtU.mjs} +123 -617
- package/dist/{tls-C5IYg3V5.js → agent-https-BZIwS4xF.js} +157 -765
- package/dist/auth/device-ca.d.ts +0 -10
- 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 +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -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 +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -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 +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- 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.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +3 -3
- package/dist/builtins/keychain/index.mjs +3 -3
- 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 +9 -4
- package/dist/builtins/local-auth/local-auth.addon.mjs +9 -4
- package/dist/builtins/local-network/address-book.d.ts +13 -0
- package/dist/builtins/local-network/agent-mdns.d.ts +51 -0
- package/dist/builtins/local-network/local-network.addon.js +74 -53
- package/dist/builtins/local-network/local-network.addon.mjs +56 -36
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -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 +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -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 +1388 -31
- package/dist/builtins/system-config/system-config.addon.mjs +1362 -28
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-CFlUdpO7.js → composition-sources-B7R1oQnY.js} +1 -1
- package/dist/{composition-sources-DpbL38z3.mjs → composition-sources-CqrDyHM1.mjs} +1 -1
- package/dist/{constants-B89vjrSm.mjs → constants-CrMPWOug.mjs} +2 -2
- package/dist/{constants-DfP8V3eX.js → constants-CuOfMdYe.js} +2 -2
- package/dist/{device-ca-4IY98a_-.mjs → device-ca-CDjGQQPd.mjs} +17 -6
- package/dist/{device-ca-BCyAPmIj.js → device-ca-D21ZP00q.js} +17 -6
- package/dist/{dist-BCKOeWu_.js → dist-CCH3XPqO.js} +86 -6
- package/dist/{dist-Bt8GTRHB.mjs → dist-CYcBqnXk.mjs} +69 -7
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/hub-mdns-ciao-CBLvq2oj.js +821 -0
- package/dist/hub-mdns-ciao-DJRwtlA8.mjs +648 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +176 -67
- package/dist/index.mjs +96 -13
- package/dist/kernel/index.d.ts +5 -4
- package/dist/kernel/moleculer/agent-mesh-authenticator.d.ts +12 -1
- package/dist/kernel/moleculer/agent-ui-unlock.d.ts +40 -0
- package/dist/kernel/moleculer/mesh-authenticator.d.ts +6 -0
- package/dist/kernel/moleculer/mesh-mtls/constants.d.ts +1 -1
- package/dist/kernel/moleculer/mesh-mtls/index.d.ts +3 -2
- package/dist/kernel/moleculer/mesh-mtls/mesh-ca.d.ts +4 -9
- package/dist/kernel/moleculer/mesh-mtls/mesh-certificate-registry.d.ts +2 -2
- package/dist/kernel/moleculer/mesh-mtls/mesh-join-token.d.ts +3 -2
- package/dist/kernel/moleculer/mesh-mtls/mesh-peer-transport.d.ts +7 -0
- package/dist/kernel/moleculer/mesh-mtls/pairing/agent-pairing.d.ts +204 -0
- package/dist/kernel/moleculer/mesh-mtls/pairing/hub-pairing.d.ts +115 -0
- package/dist/kernel/moleculer/mesh-mtls/pairing/index.d.ts +3 -0
- package/dist/kernel/moleculer/mesh-mtls/pairing/pairing-protocol.d.ts +107 -0
- package/dist/kernel/moleculer/mesh-mtls/tls-tcp-transporter.d.ts +18 -0
- package/dist/{lib-B_R6sPC3.mjs → lib-BeGzVQcd.mjs} +1 -1
- package/dist/{lib-DOJ5yyWA.js → lib-NNxHSOIE.js} +1 -1
- package/dist/{restore-archive-Bh34W8J8.mjs → restore-archive-5ml7uYMf.mjs} +1 -1
- package/dist/{restore-archive-DhHH7VQp.js → restore-archive-Do0QwAik.js} +1 -1
- package/dist/{retired-settings-keys-u3Wwkdrq.js → retired-settings-keys-CEiCaI_j.js} +1 -1
- package/dist/{retired-settings-keys-CmcEitC2.mjs → retired-settings-keys-CyDdyjLP.mjs} +1 -1
- package/dist/tls/agent-https-names.d.ts +2 -0
- package/dist/tls/cert-backdate.d.ts +10 -0
- package/dist/tls/cert-issue.d.ts +0 -20
- package/dist/{wal-maintenance-uQGuStFW.mjs → wal-maintenance-DgLLPK14.mjs} +0 -0
- package/dist/{wal-maintenance-ChzCPUi2.js → wal-maintenance-LZYd8iT-.js} +0 -0
- package/dist/{x509.es-CAjSsrkk.mjs → x509.es-CDvj67p1.mjs} +14 -1
- package/dist/{x509.es-Dzl0BbmG.js → x509.es-r7Ht1thA.js} +19 -0
- package/package.json +1 -1
- package/dist/agent-https-names-CpUSuP4w.js +0 -120
- package/dist/agent-https-names-uetr4OQl.mjs +0 -91
|
@@ -1,145 +1,104 @@
|
|
|
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-r7Ht1thA.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");
|
|
6
|
-
let node_fs_promises = require("node:fs/promises");
|
|
7
6
|
let node_path = require("node:path");
|
|
8
7
|
let node_os = require("node:os");
|
|
9
8
|
node_os = require_chunk.__toESM(node_os);
|
|
10
9
|
let node_fs = require("node:fs");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
function allFamiliesListenHost(host) {
|
|
37
|
-
if (host === "0.0.0.0" || host === "*" || host === "") return {
|
|
38
|
-
host: "::",
|
|
39
|
-
ipv6Only: false
|
|
40
|
-
};
|
|
41
|
-
return {
|
|
42
|
-
host,
|
|
43
|
-
ipv6Only: false
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function registerLanHttpHandler(next) {
|
|
47
|
-
handlers = next;
|
|
48
|
-
}
|
|
49
|
-
function readLanHttpState() {
|
|
50
|
-
return state;
|
|
51
|
-
}
|
|
52
|
-
async function applyLanHttp(options) {
|
|
53
|
-
pending = options;
|
|
54
|
-
if (!options.enabled) {
|
|
55
|
-
await closeLanHttp();
|
|
56
|
-
state = {
|
|
57
|
-
listening: false,
|
|
58
|
-
error: null,
|
|
59
|
-
port: options.port
|
|
60
|
-
};
|
|
61
|
-
return state;
|
|
10
|
+
//#region src/tls/agent-https-names.ts
|
|
11
|
+
/**
|
|
12
|
+
* @durable class=config owner=system-config
|
|
13
|
+
* write="the hub records the names it signed into an agent's HTTPS leaf, replacing that
|
|
14
|
+
* node's previous row"
|
|
15
|
+
* retention="one row per node, replaced on every issue; a forgotten node's row stays until
|
|
16
|
+
* the node is issued again or an operator clears it (it only blocks reuse of its names)"
|
|
17
|
+
*/
|
|
18
|
+
var AGENT_HTTPS_NAMES_COLLECTION = "agent_https_names";
|
|
19
|
+
var AGENT_HTTPS_NAMES_COLUMNS = [
|
|
20
|
+
{
|
|
21
|
+
name: "id",
|
|
22
|
+
type: "TEXT",
|
|
23
|
+
primaryKey: true,
|
|
24
|
+
notNull: true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "names",
|
|
28
|
+
type: "JSON",
|
|
29
|
+
notNull: true
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "issuedAt",
|
|
33
|
+
type: "INTEGER",
|
|
34
|
+
notNull: true
|
|
62
35
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
36
|
+
];
|
|
37
|
+
var AgentHttpsNames = class {
|
|
38
|
+
store;
|
|
39
|
+
byNode = /* @__PURE__ */ new Map();
|
|
40
|
+
constructor(store) {
|
|
41
|
+
this.store = store;
|
|
42
|
+
}
|
|
43
|
+
async load() {
|
|
44
|
+
const rows = await this.store.query.query({ collection: AGENT_HTTPS_NAMES_COLLECTION });
|
|
45
|
+
for (const row of rows) {
|
|
46
|
+
const names = row.data["names"];
|
|
47
|
+
if (Array.isArray(names)) this.byNode.set(row.id, names.filter((n) => typeof n === "string").map((n) => n.toLowerCase()));
|
|
48
|
+
}
|
|
70
49
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const bound = await listenHttp(options.port, options.host, handlers);
|
|
75
|
-
server = bound;
|
|
76
|
-
boundRequestedHost = options.host;
|
|
77
|
-
const addr = bound.address();
|
|
78
|
-
state = {
|
|
79
|
-
listening: true,
|
|
80
|
-
error: null,
|
|
81
|
-
port: typeof addr === "object" && addr !== null ? addr.port : options.port
|
|
82
|
-
};
|
|
83
|
-
return state;
|
|
84
|
-
} catch (err) {
|
|
85
|
-
server = null;
|
|
86
|
-
state = {
|
|
87
|
-
listening: false,
|
|
88
|
-
error: err instanceof Error ? err.message : String(err),
|
|
89
|
-
port: options.port
|
|
90
|
-
};
|
|
91
|
-
return state;
|
|
50
|
+
/** The names certified to `nodeId` (its id and the address of its live mesh socket), or `null`. */
|
|
51
|
+
namesOf(nodeId) {
|
|
52
|
+
return this.byNode.get(nodeId) ?? null;
|
|
92
53
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
async
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}, next);
|
|
119
|
-
throw err;
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
function listenOn(port, bind, next) {
|
|
123
|
-
return new Promise((resolve, reject) => {
|
|
124
|
-
const created = (0, node_http.createServer)(next.onRequest);
|
|
125
|
-
created.on("upgrade", next.onUpgrade);
|
|
126
|
-
const onError = (err) => {
|
|
127
|
-
created.off("listening", onListening);
|
|
128
|
-
created.close();
|
|
129
|
-
reject(err);
|
|
130
|
-
};
|
|
131
|
-
const onListening = () => {
|
|
132
|
-
created.off("error", onError);
|
|
133
|
-
resolve(created);
|
|
54
|
+
/** Every name certified to a node other than `nodeId`. */
|
|
55
|
+
takenByOthers(nodeId) {
|
|
56
|
+
const out = /* @__PURE__ */ new Set();
|
|
57
|
+
for (const [id, names] of this.byNode) {
|
|
58
|
+
if (id === nodeId) continue;
|
|
59
|
+
for (const n of names) out.add(n);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/** Free every name certified to a node that was forgotten. */
|
|
64
|
+
async release(nodeId) {
|
|
65
|
+
if (!this.byNode.delete(nodeId)) return false;
|
|
66
|
+
await this.store.delete.mutate({
|
|
67
|
+
collection: AGENT_HTTPS_NAMES_COLLECTION,
|
|
68
|
+
key: nodeId
|
|
69
|
+
});
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
async record(nodeId, names) {
|
|
73
|
+
const lowered = names.map((n) => n.toLowerCase());
|
|
74
|
+
const existed = this.byNode.has(nodeId);
|
|
75
|
+
this.byNode.set(nodeId, lowered);
|
|
76
|
+
const data = {
|
|
77
|
+
names: lowered,
|
|
78
|
+
issuedAt: Date.now()
|
|
134
79
|
};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
host: bind.host,
|
|
140
|
-
ipv6Only: bind.ipv6Only
|
|
80
|
+
if (existed) await this.store.update.mutate({
|
|
81
|
+
collection: AGENT_HTTPS_NAMES_COLLECTION,
|
|
82
|
+
id: nodeId,
|
|
83
|
+
data
|
|
141
84
|
});
|
|
142
|
-
|
|
85
|
+
else await this.store.insert.mutate({
|
|
86
|
+
collection: AGENT_HTTPS_NAMES_COLLECTION,
|
|
87
|
+
record: {
|
|
88
|
+
id: nodeId,
|
|
89
|
+
data
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var SLOT = Symbol.for("camstack.agent-https.names");
|
|
95
|
+
function setProcessAgentHttpsNames(names) {
|
|
96
|
+
if (names === null) Reflect.deleteProperty(globalThis, SLOT);
|
|
97
|
+
else Reflect.set(globalThis, SLOT, names);
|
|
98
|
+
}
|
|
99
|
+
function getProcessAgentHttpsNames() {
|
|
100
|
+
const value = Reflect.get(globalThis, SLOT);
|
|
101
|
+
return value !== null && typeof value === "object" && typeof Reflect.get(value, "takenByOthers") === "function" ? value : null;
|
|
143
102
|
}
|
|
144
103
|
//#endregion
|
|
145
104
|
//#region src/tls/cert-evaluation.ts
|
|
@@ -344,11 +303,39 @@ function collectCertIdentity(options) {
|
|
|
344
303
|
}
|
|
345
304
|
//#endregion
|
|
346
305
|
//#region src/tls/cert-issue.ts
|
|
306
|
+
/**
|
|
307
|
+
* Certificate issuance. Isolated so `cert-manager.ts` reads as policy and
|
|
308
|
+
* this file reads as mechanics.
|
|
309
|
+
*
|
|
310
|
+
* **In-process, by decision (D551).** This used to shell out to `openssl`
|
|
311
|
+
* three times. The hub's own TLS identity is not allowed to be a function
|
|
312
|
+
* of which openssl happens to sit on the host: this fleet alone runs four
|
|
313
|
+
* of them, and one — Ubuntu 20.04's 1.1.1f — silently ADDED the ambient
|
|
314
|
+
* `/etc/ssl/openssl.cnf` `[req] x509_extensions = v3_ca` block on top of
|
|
315
|
+
* our `-addext` flags, producing a root with two `basicConstraints` and
|
|
316
|
+
* two `subjectKeyIdentifier` extensions. RFC 5280 §4.2 allows each at
|
|
317
|
+
* most once, so OpenSSL 3 (which is what `node:crypto` is) marked that
|
|
318
|
+
* root invalid: `X509Certificate.ca` read `false` on our own CA and
|
|
319
|
+
* `checkIssued()` refused the pair. Under D227 that means "not issued by
|
|
320
|
+
* the local CA", i.e. rotate the root — on every boot, forever, voiding
|
|
321
|
+
* every trust an operator had granted by hand.
|
|
322
|
+
*
|
|
323
|
+
* Keys come from WebCrypto (`node:crypto`'s own implementation) and the
|
|
324
|
+
* DER is assembled by `@peculiar/x509`. Nothing here reads the host.
|
|
325
|
+
*/
|
|
347
326
|
require_x509_es.cryptoProvider.set(node_crypto.webcrypto);
|
|
348
327
|
/** Subject of the local root. Distinct from the leaf's CN by construction. */
|
|
349
328
|
var CA_COMMON_NAME = "CamStack Local CA";
|
|
350
329
|
var MS_PER_DAY = 864e5;
|
|
351
330
|
/**
|
|
331
|
+
* Expiry of a served leaf. Browsers cap the TOTAL span (notBefore to notAfter) at 397 days, so
|
|
332
|
+
* the span is `validDays` counted from the backdated start: the backdate costs five minutes of
|
|
333
|
+
* expiry here, never extra span.
|
|
334
|
+
*/
|
|
335
|
+
function leafNotAfter(now, validDays) {
|
|
336
|
+
return new Date(require_x509_es.backdatedNotBefore(now).getTime() + validDays * MS_PER_DAY);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
352
339
|
* RSA-2048 / SHA-256. Unchanged from the openssl invocation this replaces:
|
|
353
340
|
* every client that trusted the previous material still understands this.
|
|
354
341
|
*/
|
|
@@ -407,7 +394,7 @@ async function generateCa(now) {
|
|
|
407
394
|
const cert = await require_x509_es.X509CertificateGenerator.createSelfSigned({
|
|
408
395
|
serialNumber: freshSerial(),
|
|
409
396
|
name: `CN=${CA_COMMON_NAME}`,
|
|
410
|
-
notBefore: now,
|
|
397
|
+
notBefore: require_x509_es.backdatedNotBefore(now),
|
|
411
398
|
notAfter: new Date(now.getTime() + CA_VALIDITY_DAYS * MS_PER_DAY),
|
|
412
399
|
keys,
|
|
413
400
|
signingAlgorithm: SIGNING_ALGORITHM,
|
|
@@ -449,8 +436,8 @@ async function issueLeaf(ca, identity, commonName, validDays, now) {
|
|
|
449
436
|
serialNumber: freshSerial(),
|
|
450
437
|
subject: `CN=${commonName}`,
|
|
451
438
|
issuer: ca.cert.subject,
|
|
452
|
-
notBefore: now,
|
|
453
|
-
notAfter:
|
|
439
|
+
notBefore: require_x509_es.backdatedNotBefore(now),
|
|
440
|
+
notAfter: leafNotAfter(now, validDays),
|
|
454
441
|
publicKey: keys.publicKey,
|
|
455
442
|
signingKey: ca.privateKey,
|
|
456
443
|
signingAlgorithm: SIGNING_ALGORITHM,
|
|
@@ -480,8 +467,8 @@ async function issueLeafFromCsr(ca, csrPem, identity, commonName, validDays, now
|
|
|
480
467
|
serialNumber: freshSerial(),
|
|
481
468
|
subject: `CN=${commonName}`,
|
|
482
469
|
issuer: ca.cert.subject,
|
|
483
|
-
notBefore: now,
|
|
484
|
-
notAfter:
|
|
470
|
+
notBefore: require_x509_es.backdatedNotBefore(now),
|
|
471
|
+
notAfter: leafNotAfter(now, validDays),
|
|
485
472
|
publicKey: csr.publicKey,
|
|
486
473
|
signingKey: ca.privateKey,
|
|
487
474
|
signingAlgorithm: SIGNING_ALGORITHM,
|
|
@@ -489,487 +476,6 @@ async function issueLeafFromCsr(ca, csrPem, identity, commonName, validDays, now
|
|
|
489
476
|
}));
|
|
490
477
|
}
|
|
491
478
|
//#endregion
|
|
492
|
-
//#region src/tls/cert-manager.ts
|
|
493
|
-
var DEFAULT_COMMON_NAME = "camstack.local";
|
|
494
|
-
/**
|
|
495
|
-
* Ensure the hub has a usable TLS identity under `<dataDir>/tls`, made of
|
|
496
|
-
* a long-lived local **root** (`camstack-ca.crt`) and a short **leaf**
|
|
497
|
-
* chained to it (`camstack.crt` = leaf + root, `camstack.key`).
|
|
498
|
-
*
|
|
499
|
-
* ## The regeneration rule (D227)
|
|
500
|
-
*
|
|
501
|
-
* Reissue happens when, and only when, the material on disk **cannot do
|
|
502
|
-
* its job**:
|
|
503
|
-
*
|
|
504
|
-
* - it is absent, unreadable, or the key does not match the certificate;
|
|
505
|
-
* - the root is absent (every pre-2026-08 install: one self-signed
|
|
506
|
-
* `CA:TRUE` leaf and no root) or is inside its renewal window;
|
|
507
|
-
* - the leaf is inside its 30-day renewal window;
|
|
508
|
-
* - the leaf lacks `extendedKeyUsage = serverAuth` — Apple's trustd
|
|
509
|
-
* rejects an app-supplied anchor without it;
|
|
510
|
-
* - the leaf is not actually issued by the root on disk;
|
|
511
|
-
* - the leaf's validity span exceeds {@link MAX_LEAF_VALIDITY_DAYS};
|
|
512
|
-
* - **a name or address the hub must answer for is missing from the SAN
|
|
513
|
-
* list** — a DHCP move, a new interface or a new hostname.
|
|
514
|
-
*
|
|
515
|
-
* And explicitly NOT when: the certificate names an address the machine
|
|
516
|
-
* no longer has (a superset still works — the check is one-directional),
|
|
517
|
-
* a volatile interface (docker/VPN/mesh) or **any** IPv6 address changed
|
|
518
|
-
* (global and ULA both rotate under privacy extensions / interface-id
|
|
519
|
-
* churn — `os` cannot tell temporary from permanent), or a new build shipped.
|
|
520
|
-
*
|
|
521
|
-
* Every trigger is derived from the files plus the machine, so once a
|
|
522
|
-
* reissue clears them it clears them for good: the next boot returns
|
|
523
|
-
* `generated: false`. A certificate that moved on every boot would
|
|
524
|
-
* invalidate every trust an operator granted by hand — worse than the
|
|
525
|
-
* bugs this rule fixes.
|
|
526
|
-
*
|
|
527
|
-
* **The root is rotated only for root-level reasons.** SAN drift rotates
|
|
528
|
-
* the leaf alone, so trust granted to `camstack-ca.crt` survives every
|
|
529
|
-
* address change the hub will ever see.
|
|
530
|
-
*/
|
|
531
|
-
async function ensureTlsCert(dataDir, options) {
|
|
532
|
-
const tlsDir = (0, node_path.join)(dataDir, "tls");
|
|
533
|
-
const paths = {
|
|
534
|
-
certPath: (0, node_path.join)(tlsDir, "camstack.crt"),
|
|
535
|
-
keyPath: (0, node_path.join)(tlsDir, "camstack.key"),
|
|
536
|
-
caCertPath: (0, node_path.join)(tlsDir, "camstack-ca.crt"),
|
|
537
|
-
caKeyPath: (0, node_path.join)(tlsDir, "camstack-ca.key")
|
|
538
|
-
};
|
|
539
|
-
const commonName = options?.commonName ?? DEFAULT_COMMON_NAME;
|
|
540
|
-
const validDays = Math.min(options?.validDays ?? 397, 397);
|
|
541
|
-
const identity = collectCertIdentity({
|
|
542
|
-
commonName,
|
|
543
|
-
extraSans: options?.extraSans ?? [],
|
|
544
|
-
...options?.hostFacts === void 0 ? {} : { hostFacts: options.hostFacts }
|
|
545
|
-
});
|
|
546
|
-
const reason = decideRegeneration(paths, identity);
|
|
547
|
-
if (reason === null) return describe(paths, false, null, false, null, null);
|
|
548
|
-
const previousFingerprint = readFingerprint(paths.certPath);
|
|
549
|
-
const caRotated = reasonRequiresNewCa(reason);
|
|
550
|
-
(0, node_fs.mkdirSync)(tlsDir, { recursive: true });
|
|
551
|
-
try {
|
|
552
|
-
await regenerate(paths, identity, commonName, validDays, caRotated);
|
|
553
|
-
} catch (err) {
|
|
554
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
555
|
-
if (!canKeepServing(paths)) throw err;
|
|
556
|
-
return describe(paths, false, null, false, null, message);
|
|
557
|
-
}
|
|
558
|
-
return describe(paths, true, reason, caRotated, previousFingerprint, null);
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Operator "Regenerate": issue a new leaf under the existing local CA.
|
|
562
|
-
* Never rotates the root — trust granted to `camstack-ca.crt` survives.
|
|
563
|
-
*/
|
|
564
|
-
async function reissueTlsLeaf(dataDir, options) {
|
|
565
|
-
const tlsDir = (0, node_path.join)(dataDir, "tls");
|
|
566
|
-
const paths = {
|
|
567
|
-
certPath: (0, node_path.join)(tlsDir, "camstack.crt"),
|
|
568
|
-
keyPath: (0, node_path.join)(tlsDir, "camstack.key"),
|
|
569
|
-
caCertPath: (0, node_path.join)(tlsDir, "camstack-ca.crt"),
|
|
570
|
-
caKeyPath: (0, node_path.join)(tlsDir, "camstack-ca.key")
|
|
571
|
-
};
|
|
572
|
-
if (!(0, node_fs.existsSync)(paths.caCertPath) || !(0, node_fs.existsSync)(paths.caKeyPath)) return ensureTlsCert(dataDir, options);
|
|
573
|
-
const commonName = options?.commonName ?? DEFAULT_COMMON_NAME;
|
|
574
|
-
const validDays = Math.min(options?.validDays ?? 397, 397);
|
|
575
|
-
const identity = collectCertIdentity({
|
|
576
|
-
commonName,
|
|
577
|
-
extraSans: options?.extraSans ?? [],
|
|
578
|
-
...options?.hostFacts === void 0 ? {} : { hostFacts: options.hostFacts }
|
|
579
|
-
});
|
|
580
|
-
const previousFingerprint = readFingerprint(paths.certPath);
|
|
581
|
-
(0, node_fs.mkdirSync)(tlsDir, { recursive: true });
|
|
582
|
-
await regenerate(paths, identity, commonName, validDays, false);
|
|
583
|
-
return describe(paths, true, "san-coverage-gap", false, previousFingerprint, null);
|
|
584
|
-
}
|
|
585
|
-
/** True when the material already on disk can still terminate TLS. */
|
|
586
|
-
function canKeepServing(paths) {
|
|
587
|
-
if (!(0, node_fs.existsSync)(paths.certPath) || !(0, node_fs.existsSync)(paths.keyPath)) return false;
|
|
588
|
-
if (!(0, node_fs.existsSync)(paths.caCertPath)) return false;
|
|
589
|
-
return readFingerprint(paths.certPath) !== null;
|
|
590
|
-
}
|
|
591
|
-
/** Fingerprint of whatever is on disk, or `null` if there is nothing readable. */
|
|
592
|
-
function readFingerprint(certPath) {
|
|
593
|
-
try {
|
|
594
|
-
return new node_crypto.X509Certificate((0, node_fs.readFileSync)(certPath)).fingerprint256;
|
|
595
|
-
} catch {
|
|
596
|
-
return null;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
function decideRegeneration(paths, identity) {
|
|
600
|
-
if (!(0, node_fs.existsSync)(paths.certPath) || !(0, node_fs.existsSync)(paths.keyPath)) return "missing";
|
|
601
|
-
if (!(0, node_fs.existsSync)(paths.caCertPath) || !(0, node_fs.existsSync)(paths.caKeyPath)) return "no-local-ca";
|
|
602
|
-
try {
|
|
603
|
-
return evaluateExistingCert({
|
|
604
|
-
chainPem: (0, node_fs.readFileSync)(paths.certPath, "utf-8"),
|
|
605
|
-
keyPem: (0, node_fs.readFileSync)(paths.keyPath, "utf-8"),
|
|
606
|
-
caPem: (0, node_fs.readFileSync)(paths.caCertPath, "utf-8"),
|
|
607
|
-
identity,
|
|
608
|
-
now: /* @__PURE__ */ new Date()
|
|
609
|
-
});
|
|
610
|
-
} catch {
|
|
611
|
-
return "unreadable";
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
/**
|
|
615
|
-
* Build every byte in memory, then move the files into place. A crash
|
|
616
|
-
* mid-way leaves the previous material untouched; a crash between the
|
|
617
|
-
* moves leaves a key/certificate mismatch, which the evaluator reports as
|
|
618
|
-
* `key-mismatch` and heals on the next boot.
|
|
619
|
-
*/
|
|
620
|
-
async function regenerate(paths, identity, commonName, validDays, newCa) {
|
|
621
|
-
const now = /* @__PURE__ */ new Date();
|
|
622
|
-
const ca = newCa ? await generateCa(now) : await loadCa((0, node_fs.readFileSync)(paths.caCertPath, "utf-8"), (0, node_fs.readFileSync)(paths.caKeyPath, "utf-8"));
|
|
623
|
-
const leaf = await issueLeaf(ca, identity, commonName, validDays, now);
|
|
624
|
-
const chainPem = `${leaf.certPem.trimEnd()}\n${ca.certPem.trimEnd()}\n`;
|
|
625
|
-
if (newCa) {
|
|
626
|
-
await (0, node_fs_promises.writeFile)(`${paths.caCertPath}.new`, ca.certPem, "utf-8");
|
|
627
|
-
(0, _camstack_types_node.writeOwnerOnlyFileSync)(`${paths.caKeyPath}.new`, ca.keyPem);
|
|
628
|
-
(0, node_fs.renameSync)(`${paths.caCertPath}.new`, paths.caCertPath);
|
|
629
|
-
(0, node_fs.renameSync)(`${paths.caKeyPath}.new`, paths.caKeyPath);
|
|
630
|
-
}
|
|
631
|
-
(0, _camstack_types_node.writeOwnerOnlyFileSync)(`${paths.keyPath}.new`, leaf.keyPem);
|
|
632
|
-
await (0, node_fs_promises.writeFile)(`${paths.certPath}.new`, chainPem, "utf-8");
|
|
633
|
-
(0, node_fs.renameSync)(`${paths.keyPath}.new`, paths.keyPath);
|
|
634
|
-
(0, node_fs.renameSync)(`${paths.certPath}.new`, paths.certPath);
|
|
635
|
-
}
|
|
636
|
-
function describe(paths, generated, reason, caRotated, previousFingerprintSha256, reissueError) {
|
|
637
|
-
const leaf = new node_crypto.X509Certificate((0, node_fs.readFileSync)(paths.certPath));
|
|
638
|
-
const ca = new node_crypto.X509Certificate((0, node_fs.readFileSync)(paths.caCertPath));
|
|
639
|
-
const san = leaf.subjectAltName ?? "";
|
|
640
|
-
return {
|
|
641
|
-
...paths,
|
|
642
|
-
generated,
|
|
643
|
-
reason,
|
|
644
|
-
caRotated,
|
|
645
|
-
fingerprintSha256: leaf.fingerprint256,
|
|
646
|
-
previousFingerprintSha256,
|
|
647
|
-
caFingerprintSha256: ca.fingerprint256,
|
|
648
|
-
validTo: new Date(leaf.validTo).toISOString(),
|
|
649
|
-
sans: san === "" ? [] : san.split(", "),
|
|
650
|
-
reissueError
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
/**
|
|
654
|
-
* Load TLS cert+key from files. Returns Buffers suitable for Node.js TLS
|
|
655
|
-
* options. `certPath` is a full chain, so the listener presents the local
|
|
656
|
-
* root alongside the leaf.
|
|
657
|
-
*/
|
|
658
|
-
function loadTlsCert(certPath, keyPath) {
|
|
659
|
-
return {
|
|
660
|
-
cert: (0, node_fs.readFileSync)(certPath),
|
|
661
|
-
key: (0, node_fs.readFileSync)(keyPath)
|
|
662
|
-
};
|
|
663
|
-
}
|
|
664
|
-
//#endregion
|
|
665
|
-
//#region src/tls/tls-status.ts
|
|
666
|
-
function tlsDir(dataDir) {
|
|
667
|
-
return (0, node_path.join)(dataDir, "tls");
|
|
668
|
-
}
|
|
669
|
-
function readTlsMode(dataDir) {
|
|
670
|
-
const marker = (0, node_path.join)(tlsDir(dataDir), "mode");
|
|
671
|
-
if ((0, node_fs.existsSync)(marker)) {
|
|
672
|
-
const raw = (0, node_fs.readFileSync)(marker, "utf-8").trim();
|
|
673
|
-
if (raw === "uploaded") return "uploaded";
|
|
674
|
-
if (raw === "disabled") return "disabled";
|
|
675
|
-
}
|
|
676
|
-
return (0, node_fs.existsSync)((0, node_path.join)(tlsDir(dataDir), "camstack.crt")) ? "generated" : "disabled";
|
|
677
|
-
}
|
|
678
|
-
function writeTlsMode(dataDir, mode) {
|
|
679
|
-
(0, node_fs.mkdirSync)(tlsDir(dataDir), { recursive: true });
|
|
680
|
-
(0, node_fs.writeFileSync)((0, node_path.join)(tlsDir(dataDir), "mode"), `${mode}\n`, "utf-8");
|
|
681
|
-
}
|
|
682
|
-
function readExtraSans(dataDir) {
|
|
683
|
-
const path = (0, node_path.join)(tlsDir(dataDir), "extra-sans.json");
|
|
684
|
-
if (!(0, node_fs.existsSync)(path)) return [];
|
|
685
|
-
try {
|
|
686
|
-
const parsed = JSON.parse((0, node_fs.readFileSync)(path, "utf-8"));
|
|
687
|
-
if (!Array.isArray(parsed)) return [];
|
|
688
|
-
return parsed.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
689
|
-
} catch {
|
|
690
|
-
return [];
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
function writeExtraSans(dataDir, sans) {
|
|
694
|
-
(0, node_fs.mkdirSync)(tlsDir(dataDir), { recursive: true });
|
|
695
|
-
(0, node_fs.writeFileSync)((0, node_path.join)(tlsDir(dataDir), "extra-sans.json"), `${JSON.stringify(sans)}\n`, "utf-8");
|
|
696
|
-
}
|
|
697
|
-
/** Where an upload's own CA is kept — never over the local CA's `camstack-ca.crt`. */
|
|
698
|
-
var UPLOADED_CA_FILE = "uploaded-ca.crt";
|
|
699
|
-
/** Every certificate in a PEM bundle, in order. */
|
|
700
|
-
function splitPemCertificates(pem) {
|
|
701
|
-
const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
|
|
702
|
-
return blocks === null ? [] : blocks.map((block) => `${block}\n`);
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* The certificate that signed `leaf`, from the upload itself: the CA the
|
|
706
|
-
* operator supplied, else the rest of the uploaded chain. `null` when the
|
|
707
|
-
* upload did not include it — and then nothing may stand in for it: the
|
|
708
|
-
* local CA did not sign an uploaded leaf, so handing it out as "the CA" would
|
|
709
|
-
* tell a client to trust a root that verifies nothing it is served.
|
|
710
|
-
*/
|
|
711
|
-
function uploadedIssuer(dir, leaf, chainPem) {
|
|
712
|
-
const caPath = (0, node_path.join)(dir, UPLOADED_CA_FILE);
|
|
713
|
-
const candidates = [...(0, node_fs.existsSync)(caPath) ? splitPemCertificates((0, node_fs.readFileSync)(caPath, "utf-8")) : [], ...splitPemCertificates(chainPem).slice(1)];
|
|
714
|
-
for (const pem of candidates) try {
|
|
715
|
-
const cert = new node_crypto.X509Certificate(pem);
|
|
716
|
-
if (leaf.checkIssued(cert) && leaf.verify(cert.publicKey)) return {
|
|
717
|
-
pem,
|
|
718
|
-
cert
|
|
719
|
-
};
|
|
720
|
-
} catch {}
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
function generatedIssuer(dir) {
|
|
724
|
-
const caPath = (0, node_path.join)(dir, "camstack-ca.crt");
|
|
725
|
-
if (!(0, node_fs.existsSync)(caPath)) return null;
|
|
726
|
-
const pem = (0, node_fs.readFileSync)(caPath, "utf-8");
|
|
727
|
-
return {
|
|
728
|
-
pem,
|
|
729
|
-
cert: new node_crypto.X509Certificate(pem)
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* What the node's certificate is, and whether the listener serves it.
|
|
734
|
-
* `servedLeafFingerprint` is the leaf the HTTPS listener presents right now
|
|
735
|
-
* (`servedHttpsLeafFingerprint()`), or `null` when unknown; a restart is
|
|
736
|
-
* required exactly when the listener serves a different leaf than the one
|
|
737
|
-
* on disk — a measured fact, not a flag a mutation remembered to set.
|
|
738
|
-
*/
|
|
739
|
-
function readTlsAccessStatus(dataDir, servedLeafFingerprint) {
|
|
740
|
-
const dir = tlsDir(dataDir);
|
|
741
|
-
const certPath = (0, node_path.join)(dir, "camstack.crt");
|
|
742
|
-
const mode = readTlsMode(dataDir);
|
|
743
|
-
if (!(0, node_fs.existsSync)(certPath)) return {
|
|
744
|
-
mode: "disabled",
|
|
745
|
-
leafFingerprintSha256: null,
|
|
746
|
-
caFingerprintSha256: null,
|
|
747
|
-
validTo: null,
|
|
748
|
-
sans: [],
|
|
749
|
-
caCertPem: (mode === "uploaded" ? null : generatedIssuer(dir))?.pem ?? null,
|
|
750
|
-
reissueError: null,
|
|
751
|
-
restartRequired: false
|
|
752
|
-
};
|
|
753
|
-
try {
|
|
754
|
-
const chainPem = (0, node_fs.readFileSync)(certPath, "utf-8");
|
|
755
|
-
const leaf = new node_crypto.X509Certificate(chainPem);
|
|
756
|
-
const ca = mode === "uploaded" ? uploadedIssuer(dir, leaf, chainPem) : generatedIssuer(dir);
|
|
757
|
-
const san = leaf.subjectAltName ?? "";
|
|
758
|
-
return {
|
|
759
|
-
mode,
|
|
760
|
-
leafFingerprintSha256: leaf.fingerprint256,
|
|
761
|
-
caFingerprintSha256: ca?.cert.fingerprint256 ?? null,
|
|
762
|
-
validTo: new Date(leaf.validTo).toISOString(),
|
|
763
|
-
sans: san === "" ? [] : san.split(", "),
|
|
764
|
-
caCertPem: ca?.pem ?? null,
|
|
765
|
-
reissueError: null,
|
|
766
|
-
restartRequired: servedLeafFingerprint !== null && servedLeafFingerprint !== leaf.fingerprint256
|
|
767
|
-
};
|
|
768
|
-
} catch {
|
|
769
|
-
return {
|
|
770
|
-
mode,
|
|
771
|
-
leafFingerprintSha256: null,
|
|
772
|
-
caFingerprintSha256: null,
|
|
773
|
-
validTo: null,
|
|
774
|
-
sans: [],
|
|
775
|
-
caCertPem: null,
|
|
776
|
-
reissueError: "Certificate on disk could not be parsed.",
|
|
777
|
-
restartRequired: false
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
//#endregion
|
|
782
|
-
//#region src/tls/boot-tls.ts
|
|
783
|
-
/**
|
|
784
|
-
* Decide what the listener serves at boot. The admin UI (Network → Local
|
|
785
|
-
* access) is the only way to set a certificate (D726): it either leaves the
|
|
786
|
-
* hub on its generated local-CA leaf (D227) or uploads one, recorded by the
|
|
787
|
-
* `tls/mode` marker. An uploaded certificate is served verbatim — running it
|
|
788
|
-
* through `ensureTlsCert` would replace it on the first restart, because a
|
|
789
|
-
* leaf the local root did not issue is one of that rule's reissue triggers.
|
|
790
|
-
*/
|
|
791
|
-
async function prepareBootTls(dataDir, options) {
|
|
792
|
-
const certPath = (0, node_path.join)(dataDir, "tls", "camstack.crt");
|
|
793
|
-
const keyPath = (0, node_path.join)(dataDir, "tls", "camstack.key");
|
|
794
|
-
const marked = readTlsMode(dataDir);
|
|
795
|
-
if (marked === "uploaded" && (0, node_fs.existsSync)(certPath) && (0, node_fs.existsSync)(keyPath)) return {
|
|
796
|
-
mode: "uploaded",
|
|
797
|
-
pair: loadTlsCert(certPath, keyPath),
|
|
798
|
-
ensured: void 0,
|
|
799
|
-
uploadedMissing: false
|
|
800
|
-
};
|
|
801
|
-
const uploadedMissing = marked === "uploaded";
|
|
802
|
-
if (uploadedMissing) writeTlsMode(dataDir, "generated");
|
|
803
|
-
const ensured = await ensureTlsCert(dataDir, options);
|
|
804
|
-
return {
|
|
805
|
-
mode: "generated",
|
|
806
|
-
pair: loadTlsCert(ensured.certPath, ensured.keyPath),
|
|
807
|
-
ensured,
|
|
808
|
-
uploadedMissing
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
//#endregion
|
|
812
|
-
//#region src/tls/https-listener.ts
|
|
813
|
-
var SLOT_KEY = Symbol.for("camstack.tls.https-listener");
|
|
814
|
-
var EMPTY_SLOT = {
|
|
815
|
-
reload: null,
|
|
816
|
-
servedLeafFingerprint: null
|
|
817
|
-
};
|
|
818
|
-
function isSlot(value) {
|
|
819
|
-
if (typeof value !== "object" || value === null) return false;
|
|
820
|
-
if (!("reload" in value) || !("servedLeafFingerprint" in value)) return false;
|
|
821
|
-
const reload = value.reload;
|
|
822
|
-
const served = value.servedLeafFingerprint;
|
|
823
|
-
return (reload === null || typeof reload === "function") && (served === null || typeof served === "string");
|
|
824
|
-
}
|
|
825
|
-
function readSlot() {
|
|
826
|
-
const current = Reflect.get(globalThis, SLOT_KEY);
|
|
827
|
-
return isSlot(current) ? current : EMPTY_SLOT;
|
|
828
|
-
}
|
|
829
|
-
function writeSlot(next) {
|
|
830
|
-
Reflect.set(globalThis, SLOT_KEY, next);
|
|
831
|
-
}
|
|
832
|
-
/** SHA-256 fingerprint of the first certificate (the leaf) in a PEM chain. */
|
|
833
|
-
function leafFingerprintOf(chain) {
|
|
834
|
-
return new node_crypto.X509Certificate(chain).fingerprint256;
|
|
835
|
-
}
|
|
836
|
-
/**
|
|
837
|
-
* Called once by the server after its HTTPS listener is up, with the pair it
|
|
838
|
-
* was started on. From here on a certificate action swaps the served
|
|
839
|
-
* certificate in place (`setSecureContext`) — new handshakes get the new
|
|
840
|
-
* leaf, open connections keep theirs, nothing restarts.
|
|
841
|
-
*/
|
|
842
|
-
function registerHttpsListener(server, pair) {
|
|
843
|
-
writeSlot({
|
|
844
|
-
reload: (next) => server.setSecureContext({
|
|
845
|
-
cert: next.cert,
|
|
846
|
-
key: next.key
|
|
847
|
-
}),
|
|
848
|
-
servedLeafFingerprint: leafFingerprintOf(pair.cert)
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
/** Forget the listener — on shutdown, and between specs. */
|
|
852
|
-
function clearHttpsListener() {
|
|
853
|
-
writeSlot(EMPTY_SLOT);
|
|
854
|
-
}
|
|
855
|
-
/**
|
|
856
|
-
* The leaf the HTTPS listener serves now, or `null` when this process has no
|
|
857
|
-
* registered listener (TLS off, or a process that does not own one) — in
|
|
858
|
-
* which case nobody can say, and `null` is the honest answer.
|
|
859
|
-
*/
|
|
860
|
-
function servedHttpsLeafFingerprint() {
|
|
861
|
-
return readSlot().servedLeafFingerprint;
|
|
862
|
-
}
|
|
863
|
-
/** Serve `pair` from now on. Never throws: the outcome says what happened. */
|
|
864
|
-
function reloadHttpsListener(pair) {
|
|
865
|
-
const slot = readSlot();
|
|
866
|
-
if (slot.reload === null) return { kind: "no-listener" };
|
|
867
|
-
try {
|
|
868
|
-
const servedLeafFingerprint = leafFingerprintOf(pair.cert);
|
|
869
|
-
slot.reload(pair);
|
|
870
|
-
writeSlot({
|
|
871
|
-
reload: slot.reload,
|
|
872
|
-
servedLeafFingerprint
|
|
873
|
-
});
|
|
874
|
-
return {
|
|
875
|
-
kind: "reloaded",
|
|
876
|
-
servedLeafFingerprint
|
|
877
|
-
};
|
|
878
|
-
} catch (err) {
|
|
879
|
-
return {
|
|
880
|
-
kind: "failed",
|
|
881
|
-
error: err instanceof Error ? err.message : String(err)
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
//#endregion
|
|
886
|
-
//#region src/tls/uploaded-cert.ts
|
|
887
|
-
/**
|
|
888
|
-
* Operator-supplied material must parse, match its key, and carry
|
|
889
|
-
* `extendedKeyUsage = serverAuth` — the same leaf defects D227 refused
|
|
890
|
-
* to keep generating.
|
|
891
|
-
*/
|
|
892
|
-
function validateUploadedTls(certPem, keyPem) {
|
|
893
|
-
let leaf;
|
|
894
|
-
try {
|
|
895
|
-
leaf = new node_crypto.X509Certificate(certPem);
|
|
896
|
-
} catch {
|
|
897
|
-
return {
|
|
898
|
-
ok: false,
|
|
899
|
-
error: "Certificate PEM could not be parsed."
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
try {
|
|
903
|
-
if (!leaf.checkPrivateKey((0, node_crypto.createPrivateKey)(keyPem))) return {
|
|
904
|
-
ok: false,
|
|
905
|
-
error: "Private key does not match the certificate."
|
|
906
|
-
};
|
|
907
|
-
} catch {
|
|
908
|
-
return {
|
|
909
|
-
ok: false,
|
|
910
|
-
error: "Private key PEM could not be parsed."
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
const eku = leaf.keyUsage;
|
|
914
|
-
if (eku === void 0 || !eku.includes("1.3.6.1.5.5.7.3.1")) return {
|
|
915
|
-
ok: false,
|
|
916
|
-
error: "Certificate is missing extendedKeyUsage = serverAuth."
|
|
917
|
-
};
|
|
918
|
-
return {
|
|
919
|
-
ok: true,
|
|
920
|
-
fingerprintSha256: leaf.fingerprint256,
|
|
921
|
-
validTo: new Date(leaf.validTo).toISOString()
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
//#endregion
|
|
925
|
-
//#region src/tls/tls-actions.ts
|
|
926
|
-
function withNewline(pem) {
|
|
927
|
-
return pem.endsWith("\n") ? pem : `${pem}\n`;
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Store an operator's certificate. The leaf (or chain) and key replace the
|
|
931
|
-
* served pair; a CA, when given, goes to its own file. The local CA
|
|
932
|
-
* (`camstack-ca.crt` + key) is never touched — it did not sign the upload,
|
|
933
|
-
* and it is what "Revert to generated" and every device that trusts it
|
|
934
|
-
* still need.
|
|
935
|
-
*/
|
|
936
|
-
async function storeUploadedCertificate(dataDir, input) {
|
|
937
|
-
const valid = validateUploadedTls(input.certPem, input.keyPem);
|
|
938
|
-
if (!valid.ok) throw new Error(valid.error);
|
|
939
|
-
const dir = (0, node_path.join)(dataDir, "tls");
|
|
940
|
-
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
941
|
-
await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "camstack.crt"), withNewline(input.certPem));
|
|
942
|
-
(0, _camstack_types_node.writeOwnerOnlyFileSync)((0, node_path.join)(dir, "camstack.key"), withNewline(input.keyPem));
|
|
943
|
-
const caPath = (0, node_path.join)(dir, UPLOADED_CA_FILE);
|
|
944
|
-
if (input.caPem !== void 0 && input.caPem.trim() !== "") await (0, node_fs_promises.writeFile)(caPath, withNewline(input.caPem));
|
|
945
|
-
else (0, node_fs.rmSync)(caPath, { force: true });
|
|
946
|
-
writeTlsMode(dataDir, "uploaded");
|
|
947
|
-
}
|
|
948
|
-
/** Issue a new generated leaf under the local CA. Refused while an upload is served. */
|
|
949
|
-
async function regenerateCertificate(dataDir, options) {
|
|
950
|
-
if (readTlsMode(dataDir) === "uploaded") throw new Error("Cannot regenerate an uploaded certificate — revert to generated first.");
|
|
951
|
-
await reissueTlsLeaf(dataDir, options);
|
|
952
|
-
}
|
|
953
|
-
/** Drop the upload and serve the generated local-CA leaf again (D227). */
|
|
954
|
-
async function revertToGeneratedCertificate(dataDir, options) {
|
|
955
|
-
writeTlsMode(dataDir, "generated");
|
|
956
|
-
(0, node_fs.rmSync)((0, node_path.join)(dataDir, "tls", UPLOADED_CA_FILE), { force: true });
|
|
957
|
-
await ensureTlsCert(dataDir, options);
|
|
958
|
-
}
|
|
959
|
-
/**
|
|
960
|
-
* Put the pair on disk on the HTTPS listener. Called after every action, so
|
|
961
|
-
* "the certificate changed" and "clients see it" are the same moment.
|
|
962
|
-
*/
|
|
963
|
-
function applyCertificateToListener(dataDir) {
|
|
964
|
-
const certPath = (0, node_path.join)(dataDir, "tls", "camstack.crt");
|
|
965
|
-
const keyPath = (0, node_path.join)(dataDir, "tls", "camstack.key");
|
|
966
|
-
if (!(0, node_fs.existsSync)(certPath) || !(0, node_fs.existsSync)(keyPath)) return {
|
|
967
|
-
kind: "failed",
|
|
968
|
-
error: `No certificate pair under ${(0, node_path.join)(dataDir, "tls")}`
|
|
969
|
-
};
|
|
970
|
-
return reloadHttpsListener(loadTlsCert(certPath, keyPath));
|
|
971
|
-
}
|
|
972
|
-
//#endregion
|
|
973
479
|
//#region src/tls/agent-https.ts
|
|
974
480
|
/**
|
|
975
481
|
* HTTPS for agents (D856): a leaf from the hub's LOCAL CA, so a device that
|
|
@@ -1251,28 +757,40 @@ function pemDigest(pem) {
|
|
|
1251
757
|
return (0, node_crypto.createHash)("sha256").update(pem).digest("hex").slice(0, 16);
|
|
1252
758
|
}
|
|
1253
759
|
//#endregion
|
|
760
|
+
Object.defineProperty(exports, "AGENT_HTTPS_NAMES_COLLECTION", {
|
|
761
|
+
enumerable: true,
|
|
762
|
+
get: function() {
|
|
763
|
+
return AGENT_HTTPS_NAMES_COLLECTION;
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
Object.defineProperty(exports, "AGENT_HTTPS_NAMES_COLUMNS", {
|
|
767
|
+
enumerable: true,
|
|
768
|
+
get: function() {
|
|
769
|
+
return AGENT_HTTPS_NAMES_COLUMNS;
|
|
770
|
+
}
|
|
771
|
+
});
|
|
1254
772
|
Object.defineProperty(exports, "AgentHttpsInstallError", {
|
|
1255
773
|
enumerable: true,
|
|
1256
774
|
get: function() {
|
|
1257
775
|
return AgentHttpsInstallError;
|
|
1258
776
|
}
|
|
1259
777
|
});
|
|
1260
|
-
Object.defineProperty(exports, "
|
|
778
|
+
Object.defineProperty(exports, "AgentHttpsNames", {
|
|
1261
779
|
enumerable: true,
|
|
1262
780
|
get: function() {
|
|
1263
|
-
return
|
|
781
|
+
return AgentHttpsNames;
|
|
1264
782
|
}
|
|
1265
783
|
});
|
|
1266
|
-
Object.defineProperty(exports, "
|
|
784
|
+
Object.defineProperty(exports, "CA_COMMON_NAME", {
|
|
1267
785
|
enumerable: true,
|
|
1268
786
|
get: function() {
|
|
1269
|
-
return
|
|
787
|
+
return CA_COMMON_NAME;
|
|
1270
788
|
}
|
|
1271
789
|
});
|
|
1272
|
-
Object.defineProperty(exports, "
|
|
790
|
+
Object.defineProperty(exports, "CA_VALIDITY_DAYS", {
|
|
1273
791
|
enumerable: true,
|
|
1274
792
|
get: function() {
|
|
1275
|
-
return
|
|
793
|
+
return CA_VALIDITY_DAYS;
|
|
1276
794
|
}
|
|
1277
795
|
});
|
|
1278
796
|
Object.defineProperty(exports, "LEAF_RENEWAL_WINDOW_DAYS", {
|
|
@@ -1293,12 +811,6 @@ Object.defineProperty(exports, "SERVER_AUTH_OID", {
|
|
|
1293
811
|
return SERVER_AUTH_OID;
|
|
1294
812
|
}
|
|
1295
813
|
});
|
|
1296
|
-
Object.defineProperty(exports, "UPLOADED_CA_FILE", {
|
|
1297
|
-
enumerable: true,
|
|
1298
|
-
get: function() {
|
|
1299
|
-
return UPLOADED_CA_FILE;
|
|
1300
|
-
}
|
|
1301
|
-
});
|
|
1302
814
|
Object.defineProperty(exports, "acceptAgentSans", {
|
|
1303
815
|
enumerable: true,
|
|
1304
816
|
get: function() {
|
|
@@ -1311,48 +823,12 @@ Object.defineProperty(exports, "agentHttpsPaths", {
|
|
|
1311
823
|
return agentHttpsPaths;
|
|
1312
824
|
}
|
|
1313
825
|
});
|
|
1314
|
-
Object.defineProperty(exports, "allFamiliesListenHost", {
|
|
1315
|
-
enumerable: true,
|
|
1316
|
-
get: function() {
|
|
1317
|
-
return allFamiliesListenHost;
|
|
1318
|
-
}
|
|
1319
|
-
});
|
|
1320
|
-
Object.defineProperty(exports, "applyCertificateToListener", {
|
|
1321
|
-
enumerable: true,
|
|
1322
|
-
get: function() {
|
|
1323
|
-
return applyCertificateToListener;
|
|
1324
|
-
}
|
|
1325
|
-
});
|
|
1326
|
-
Object.defineProperty(exports, "applyLanHttp", {
|
|
1327
|
-
enumerable: true,
|
|
1328
|
-
get: function() {
|
|
1329
|
-
return applyLanHttp;
|
|
1330
|
-
}
|
|
1331
|
-
});
|
|
1332
|
-
Object.defineProperty(exports, "bindPendingLanHttp", {
|
|
1333
|
-
enumerable: true,
|
|
1334
|
-
get: function() {
|
|
1335
|
-
return bindPendingLanHttp;
|
|
1336
|
-
}
|
|
1337
|
-
});
|
|
1338
826
|
Object.defineProperty(exports, "certificateNames", {
|
|
1339
827
|
enumerable: true,
|
|
1340
828
|
get: function() {
|
|
1341
829
|
return certificateNames;
|
|
1342
830
|
}
|
|
1343
831
|
});
|
|
1344
|
-
Object.defineProperty(exports, "clearHttpsListener", {
|
|
1345
|
-
enumerable: true,
|
|
1346
|
-
get: function() {
|
|
1347
|
-
return clearHttpsListener;
|
|
1348
|
-
}
|
|
1349
|
-
});
|
|
1350
|
-
Object.defineProperty(exports, "closeLanHttp", {
|
|
1351
|
-
enumerable: true,
|
|
1352
|
-
get: function() {
|
|
1353
|
-
return closeLanHttp;
|
|
1354
|
-
}
|
|
1355
|
-
});
|
|
1356
832
|
Object.defineProperty(exports, "collectAgentNames", {
|
|
1357
833
|
enumerable: true,
|
|
1358
834
|
get: function() {
|
|
@@ -1377,16 +853,22 @@ Object.defineProperty(exports, "describeHttpsLeaf", {
|
|
|
1377
853
|
return describeHttpsLeaf;
|
|
1378
854
|
}
|
|
1379
855
|
});
|
|
1380
|
-
Object.defineProperty(exports, "
|
|
856
|
+
Object.defineProperty(exports, "evaluateExistingCert", {
|
|
1381
857
|
enumerable: true,
|
|
1382
858
|
get: function() {
|
|
1383
|
-
return
|
|
859
|
+
return evaluateExistingCert;
|
|
1384
860
|
}
|
|
1385
861
|
});
|
|
1386
|
-
Object.defineProperty(exports, "
|
|
862
|
+
Object.defineProperty(exports, "generateCa", {
|
|
1387
863
|
enumerable: true,
|
|
1388
864
|
get: function() {
|
|
1389
|
-
return
|
|
865
|
+
return generateCa;
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
Object.defineProperty(exports, "getProcessAgentHttpsNames", {
|
|
869
|
+
enumerable: true,
|
|
870
|
+
get: function() {
|
|
871
|
+
return getProcessAgentHttpsNames;
|
|
1390
872
|
}
|
|
1391
873
|
});
|
|
1392
874
|
Object.defineProperty(exports, "installAgentHttps", {
|
|
@@ -1419,10 +901,10 @@ Object.defineProperty(exports, "issueAgentHttpsLeaf", {
|
|
|
1419
901
|
return issueAgentHttpsLeaf;
|
|
1420
902
|
}
|
|
1421
903
|
});
|
|
1422
|
-
Object.defineProperty(exports, "
|
|
904
|
+
Object.defineProperty(exports, "issueLeaf", {
|
|
1423
905
|
enumerable: true,
|
|
1424
906
|
get: function() {
|
|
1425
|
-
return
|
|
907
|
+
return issueLeaf;
|
|
1426
908
|
}
|
|
1427
909
|
});
|
|
1428
910
|
Object.defineProperty(exports, "loadAgentHttps", {
|
|
@@ -1431,10 +913,10 @@ Object.defineProperty(exports, "loadAgentHttps", {
|
|
|
1431
913
|
return loadAgentHttps;
|
|
1432
914
|
}
|
|
1433
915
|
});
|
|
1434
|
-
Object.defineProperty(exports, "
|
|
916
|
+
Object.defineProperty(exports, "loadCa", {
|
|
1435
917
|
enumerable: true,
|
|
1436
918
|
get: function() {
|
|
1437
|
-
return
|
|
919
|
+
return loadCa;
|
|
1438
920
|
}
|
|
1439
921
|
});
|
|
1440
922
|
Object.defineProperty(exports, "pemDigest", {
|
|
@@ -1449,88 +931,16 @@ Object.defineProperty(exports, "pinnedHttpsCaFingerprint", {
|
|
|
1449
931
|
return pinnedHttpsCaFingerprint;
|
|
1450
932
|
}
|
|
1451
933
|
});
|
|
1452
|
-
Object.defineProperty(exports, "
|
|
1453
|
-
enumerable: true,
|
|
1454
|
-
get: function() {
|
|
1455
|
-
return prepareBootTls;
|
|
1456
|
-
}
|
|
1457
|
-
});
|
|
1458
|
-
Object.defineProperty(exports, "readExtraSans", {
|
|
1459
|
-
enumerable: true,
|
|
1460
|
-
get: function() {
|
|
1461
|
-
return readExtraSans;
|
|
1462
|
-
}
|
|
1463
|
-
});
|
|
1464
|
-
Object.defineProperty(exports, "readLanHttpState", {
|
|
1465
|
-
enumerable: true,
|
|
1466
|
-
get: function() {
|
|
1467
|
-
return readLanHttpState;
|
|
1468
|
-
}
|
|
1469
|
-
});
|
|
1470
|
-
Object.defineProperty(exports, "readTlsAccessStatus", {
|
|
1471
|
-
enumerable: true,
|
|
1472
|
-
get: function() {
|
|
1473
|
-
return readTlsAccessStatus;
|
|
1474
|
-
}
|
|
1475
|
-
});
|
|
1476
|
-
Object.defineProperty(exports, "readTlsMode", {
|
|
934
|
+
Object.defineProperty(exports, "reasonRequiresNewCa", {
|
|
1477
935
|
enumerable: true,
|
|
1478
936
|
get: function() {
|
|
1479
|
-
return
|
|
937
|
+
return reasonRequiresNewCa;
|
|
1480
938
|
}
|
|
1481
939
|
});
|
|
1482
|
-
Object.defineProperty(exports, "
|
|
940
|
+
Object.defineProperty(exports, "setProcessAgentHttpsNames", {
|
|
1483
941
|
enumerable: true,
|
|
1484
942
|
get: function() {
|
|
1485
|
-
return
|
|
1486
|
-
}
|
|
1487
|
-
});
|
|
1488
|
-
Object.defineProperty(exports, "registerHttpsListener", {
|
|
1489
|
-
enumerable: true,
|
|
1490
|
-
get: function() {
|
|
1491
|
-
return registerHttpsListener;
|
|
1492
|
-
}
|
|
1493
|
-
});
|
|
1494
|
-
Object.defineProperty(exports, "registerLanHttpHandler", {
|
|
1495
|
-
enumerable: true,
|
|
1496
|
-
get: function() {
|
|
1497
|
-
return registerLanHttpHandler;
|
|
1498
|
-
}
|
|
1499
|
-
});
|
|
1500
|
-
Object.defineProperty(exports, "reissueTlsLeaf", {
|
|
1501
|
-
enumerable: true,
|
|
1502
|
-
get: function() {
|
|
1503
|
-
return reissueTlsLeaf;
|
|
1504
|
-
}
|
|
1505
|
-
});
|
|
1506
|
-
Object.defineProperty(exports, "reloadHttpsListener", {
|
|
1507
|
-
enumerable: true,
|
|
1508
|
-
get: function() {
|
|
1509
|
-
return reloadHttpsListener;
|
|
1510
|
-
}
|
|
1511
|
-
});
|
|
1512
|
-
Object.defineProperty(exports, "revertToGeneratedCertificate", {
|
|
1513
|
-
enumerable: true,
|
|
1514
|
-
get: function() {
|
|
1515
|
-
return revertToGeneratedCertificate;
|
|
1516
|
-
}
|
|
1517
|
-
});
|
|
1518
|
-
Object.defineProperty(exports, "servedHttpsLeafFingerprint", {
|
|
1519
|
-
enumerable: true,
|
|
1520
|
-
get: function() {
|
|
1521
|
-
return servedHttpsLeafFingerprint;
|
|
1522
|
-
}
|
|
1523
|
-
});
|
|
1524
|
-
Object.defineProperty(exports, "splitPemCertificates", {
|
|
1525
|
-
enumerable: true,
|
|
1526
|
-
get: function() {
|
|
1527
|
-
return splitPemCertificates;
|
|
1528
|
-
}
|
|
1529
|
-
});
|
|
1530
|
-
Object.defineProperty(exports, "storeUploadedCertificate", {
|
|
1531
|
-
enumerable: true,
|
|
1532
|
-
get: function() {
|
|
1533
|
-
return storeUploadedCertificate;
|
|
943
|
+
return setProcessAgentHttpsNames;
|
|
1534
944
|
}
|
|
1535
945
|
});
|
|
1536
946
|
Object.defineProperty(exports, "systemHostFacts", {
|
|
@@ -1539,21 +949,3 @@ Object.defineProperty(exports, "systemHostFacts", {
|
|
|
1539
949
|
return systemHostFacts;
|
|
1540
950
|
}
|
|
1541
951
|
});
|
|
1542
|
-
Object.defineProperty(exports, "validateUploadedTls", {
|
|
1543
|
-
enumerable: true,
|
|
1544
|
-
get: function() {
|
|
1545
|
-
return validateUploadedTls;
|
|
1546
|
-
}
|
|
1547
|
-
});
|
|
1548
|
-
Object.defineProperty(exports, "writeExtraSans", {
|
|
1549
|
-
enumerable: true,
|
|
1550
|
-
get: function() {
|
|
1551
|
-
return writeExtraSans;
|
|
1552
|
-
}
|
|
1553
|
-
});
|
|
1554
|
-
Object.defineProperty(exports, "writeTlsMode", {
|
|
1555
|
-
enumerable: true,
|
|
1556
|
-
get: function() {
|
|
1557
|
-
return writeTlsMode;
|
|
1558
|
-
}
|
|
1559
|
-
});
|