@camstack/system 1.2.382 → 1.2.383
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 +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- 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 +5 -5
- package/dist/builtins/keychain/index.mjs +4 -4
- package/dist/builtins/keychain/system-held-secrets.d.ts +2 -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 +9 -178
- package/dist/builtins/local-auth/local-auth.addon.mjs +4 -161
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- 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/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- 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/index.d.ts +1 -0
- package/dist/builtins/system-config/index.js +2 -0
- package/dist/builtins/system-config/index.mjs +2 -2
- package/dist/builtins/system-config/mesh-actions.d.ts +19 -0
- package/dist/builtins/system-config/system-config.addon.d.ts +34 -1
- package/dist/builtins/system-config/system-config.addon.js +87538 -9
- package/dist/builtins/system-config/system-config.addon.mjs +87505 -4
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{child-cap-dispatch-BrMdKHfp.js → child-cap-dispatch-BD8fhJ1b.js} +2 -2
- package/dist/{child-cap-dispatch-WsYPNvnK.mjs → child-cap-dispatch-C68N2a6Y.mjs} +2 -2
- package/dist/{composition-sources-C6ZynCZ2.js → composition-sources-ChpT_Yw4.js} +1 -1
- package/dist/{composition-sources-XqRX51db.mjs → composition-sources-DM7Ft_tp.mjs} +1 -1
- package/dist/{cluster-secret-BEPLh3LL.js → constants-CDpry1_T.js} +82 -1
- package/dist/{cluster-secret-DhPdZ0VA.mjs → constants-e3mCa1jG.mjs} +23 -2
- package/dist/{dist-tnAO12ak.js → dist-CUNGH0fs.js} +34 -0
- package/dist/{dist-CP5TTjwd.mjs → dist-osl3bkF0.mjs} +23 -1
- package/dist/{event-loop-stall-monitor-WkssIXjq.mjs → event-loop-stall-monitor-BeeWMnyT.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-9kR0sAPS.js → event-loop-stall-monitor-Dk28EF8B.js} +1 -1
- package/dist/index.js +284 -86048
- package/dist/index.mjs +513 -86302
- package/dist/{jwt-signing-key-CAbozwvF.mjs → jwt-signing-key-MQLxLfgF.mjs} +1 -1
- package/dist/{jwt-signing-key-Dgf8sIXY.js → jwt-signing-key-WI6z7JBo.js} +1 -1
- package/dist/kernel/index.d.ts +6 -2
- package/dist/kernel/moleculer/agent-mesh-authenticator.d.ts +41 -0
- package/dist/kernel/moleculer/broker-factory.d.ts +19 -3
- package/dist/kernel/moleculer/mesh-access-middleware.d.ts +7 -2
- package/dist/kernel/moleculer/mesh-mtls/constants.d.ts +20 -0
- package/dist/kernel/moleculer/mesh-mtls/index.d.ts +9 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-ca-keeper.d.ts +55 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-ca.d.ts +45 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-crl.d.ts +16 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-enroll.d.ts +27 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-join-token.d.ts +55 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-tls-enabler.d.ts +5 -0
- package/dist/kernel/moleculer/mesh-mtls/tls-tcp-transporter.d.ts +92 -0
- package/dist/ms-DlIWNkkk.mjs +160 -0
- package/dist/ms-t68_FaqE.js +171 -0
- package/dist/{restore-archive-CbvrfPuB.js → restore-archive-C5f9yX90.js} +3 -3
- package/dist/{restore-archive-BV39nY46.mjs → restore-archive-CZRtI1k9.mjs} +2 -2
- package/dist/{retired-settings-keys-CzpIkwS-.mjs → retired-settings-keys-LuDd4y3M.mjs} +1 -1
- package/dist/{retired-settings-keys-4bHpL-0m.js → retired-settings-keys-WLrbzdPR.js} +1 -1
- package/dist/tls-BWpwU-d5.js +1160 -0
- package/dist/tls-C2NT-Orr.mjs +949 -0
- package/dist/{tls-CljF057D.js → x509.es-BhTzcFhZ.js} +65 -1108
- package/dist/{tls-4cRH24Id.mjs → x509.es-Xf3K_brW.mjs} +40 -950
- package/package.json +3 -2
|
@@ -1,345 +1,4 @@
|
|
|
1
1
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
2
|
-
let node_crypto = require("node:crypto");
|
|
3
|
-
let node_net = require("node:net");
|
|
4
|
-
let node_fs_promises = require("node:fs/promises");
|
|
5
|
-
let node_path = require("node:path");
|
|
6
|
-
let node_os = require("node:os");
|
|
7
|
-
node_os = require_chunk.__toESM(node_os);
|
|
8
|
-
let node_fs = require("node:fs");
|
|
9
|
-
let node_http = require("node:http");
|
|
10
|
-
//#region src/http/lan-http-bind.ts
|
|
11
|
-
/**
|
|
12
|
-
* Second HTTP listener that shares the hub's existing request handler.
|
|
13
|
-
*
|
|
14
|
-
* Fastify is one protocol per instance; HTTPS stays on :4443 and this
|
|
15
|
-
* binds a plain `http.Server` that emits `request` AND `upgrade` onto the
|
|
16
|
-
* same handler graph (tRPC HTTP, cookies, data-plane, static, tRPC WS).
|
|
17
|
-
*
|
|
18
|
-
* Forwarding only `request` is not enough: the viewer speaks tRPC exclusively
|
|
19
|
-
* over WebSocket (`wsLink`). `/health` wins the LAN race, then `ws://…/trpc`
|
|
20
|
-
* hangs because nobody handles the upgrade on this socket.
|
|
21
|
-
*/
|
|
22
|
-
var DEFAULT_HTTP_PORT = 4480;
|
|
23
|
-
var handlers = null;
|
|
24
|
-
var server = null;
|
|
25
|
-
var pending = null;
|
|
26
|
-
var state = {
|
|
27
|
-
listening: false,
|
|
28
|
-
error: null,
|
|
29
|
-
port: DEFAULT_HTTP_PORT
|
|
30
|
-
};
|
|
31
|
-
var boundRequestedHost = null;
|
|
32
|
-
var DEFAULT_LAN_HTTP_PORT = DEFAULT_HTTP_PORT;
|
|
33
|
-
/** Map "all IPv4" to dual-stack IPv6 (`::`, ipv6Only false) so IPv4 still works. */
|
|
34
|
-
function allFamiliesListenHost(host) {
|
|
35
|
-
if (host === "0.0.0.0" || host === "*" || host === "") return {
|
|
36
|
-
host: "::",
|
|
37
|
-
ipv6Only: false
|
|
38
|
-
};
|
|
39
|
-
return {
|
|
40
|
-
host,
|
|
41
|
-
ipv6Only: false
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function registerLanHttpHandler(next) {
|
|
45
|
-
handlers = next;
|
|
46
|
-
}
|
|
47
|
-
function readLanHttpState() {
|
|
48
|
-
return state;
|
|
49
|
-
}
|
|
50
|
-
async function applyLanHttp(options) {
|
|
51
|
-
pending = options;
|
|
52
|
-
if (!options.enabled) {
|
|
53
|
-
await closeLanHttp();
|
|
54
|
-
state = {
|
|
55
|
-
listening: false,
|
|
56
|
-
error: null,
|
|
57
|
-
port: options.port
|
|
58
|
-
};
|
|
59
|
-
return state;
|
|
60
|
-
}
|
|
61
|
-
if (handlers === null) {
|
|
62
|
-
state = {
|
|
63
|
-
listening: false,
|
|
64
|
-
error: "HTTP listener is not registered yet",
|
|
65
|
-
port: options.port
|
|
66
|
-
};
|
|
67
|
-
return state;
|
|
68
|
-
}
|
|
69
|
-
if (server !== null && state.listening && (options.port === 0 || state.port === options.port) && boundRequestedHost === options.host) return state;
|
|
70
|
-
await closeLanHttp();
|
|
71
|
-
try {
|
|
72
|
-
const bound = await listenHttp(options.port, options.host, handlers);
|
|
73
|
-
server = bound;
|
|
74
|
-
boundRequestedHost = options.host;
|
|
75
|
-
const addr = bound.address();
|
|
76
|
-
state = {
|
|
77
|
-
listening: true,
|
|
78
|
-
error: null,
|
|
79
|
-
port: typeof addr === "object" && addr !== null ? addr.port : options.port
|
|
80
|
-
};
|
|
81
|
-
return state;
|
|
82
|
-
} catch (err) {
|
|
83
|
-
server = null;
|
|
84
|
-
state = {
|
|
85
|
-
listening: false,
|
|
86
|
-
error: err instanceof Error ? err.message : String(err),
|
|
87
|
-
port: options.port
|
|
88
|
-
};
|
|
89
|
-
return state;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
/** After the hub registers the Fastify request handler, bind whatever was pending. */
|
|
93
|
-
async function bindPendingLanHttp() {
|
|
94
|
-
if (pending === null) return applyLanHttp({
|
|
95
|
-
enabled: true,
|
|
96
|
-
port: DEFAULT_HTTP_PORT,
|
|
97
|
-
host: "0.0.0.0"
|
|
98
|
-
});
|
|
99
|
-
return applyLanHttp(pending);
|
|
100
|
-
}
|
|
101
|
-
async function closeLanHttp() {
|
|
102
|
-
const current = server;
|
|
103
|
-
server = null;
|
|
104
|
-
boundRequestedHost = null;
|
|
105
|
-
if (current === null) return;
|
|
106
|
-
await new Promise((resolve) => {
|
|
107
|
-
current.close(() => resolve());
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
function listenHttp(port, host, next) {
|
|
111
|
-
const resolved = allFamiliesListenHost(host);
|
|
112
|
-
return listenOn(port, resolved, next).catch((err) => {
|
|
113
|
-
if (resolved.host === "::" && host !== "::") return listenOn(port, {
|
|
114
|
-
host: "0.0.0.0",
|
|
115
|
-
ipv6Only: false
|
|
116
|
-
}, next);
|
|
117
|
-
throw err;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
function listenOn(port, bind, next) {
|
|
121
|
-
return new Promise((resolve, reject) => {
|
|
122
|
-
const created = (0, node_http.createServer)(next.onRequest);
|
|
123
|
-
created.on("upgrade", next.onUpgrade);
|
|
124
|
-
const onError = (err) => {
|
|
125
|
-
created.off("listening", onListening);
|
|
126
|
-
created.close();
|
|
127
|
-
reject(err);
|
|
128
|
-
};
|
|
129
|
-
const onListening = () => {
|
|
130
|
-
created.off("error", onError);
|
|
131
|
-
resolve(created);
|
|
132
|
-
};
|
|
133
|
-
created.once("error", onError);
|
|
134
|
-
created.once("listening", onListening);
|
|
135
|
-
created.listen({
|
|
136
|
-
port,
|
|
137
|
-
host: bind.host,
|
|
138
|
-
ipv6Only: bind.ipv6Only
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
//#endregion
|
|
143
|
-
//#region src/tls/cert-evaluation.ts
|
|
144
|
-
/**
|
|
145
|
-
* The regeneration rule (D227), as a pure function.
|
|
146
|
-
*
|
|
147
|
-
* `ensureTlsCert` reissues **only** when the material on disk cannot do
|
|
148
|
-
* its job. Never because it is old, never because the machine picked up
|
|
149
|
-
* an address the certificate does not need, never because a new build
|
|
150
|
-
* shipped. The decision is a function of (files on disk, machine
|
|
151
|
-
* identity, clock) alone, so the same inputs always produce the same
|
|
152
|
-
* answer — which is what makes a reissue happen once and then stop.
|
|
153
|
-
*/
|
|
154
|
-
/** OID `id-kp-serverAuth`. Apple's trustd requires it on the leaf. */
|
|
155
|
-
var SERVER_AUTH_OID = "1.3.6.1.5.5.7.3.1";
|
|
156
|
-
/**
|
|
157
|
-
* Apple's `SecPolicyCheckOtherTrustValidityPeriod` ceiling is 825 days;
|
|
158
|
-
* the CA/Browser Forum's recommended maximum is 397. We sit at 397.
|
|
159
|
-
*/
|
|
160
|
-
var MAX_LEAF_VALIDITY_DAYS = 397;
|
|
161
|
-
/** Reissue this long before the leaf dies, so nobody serves an expired one. */
|
|
162
|
-
var LEAF_RENEWAL_WINDOW_DAYS = 30;
|
|
163
|
-
/** A local root outlives many leaves — it is the anchor an operator trusts. */
|
|
164
|
-
var CA_VALIDITY_DAYS = 3650;
|
|
165
|
-
var MS_PER_DAY$1 = 864e5;
|
|
166
|
-
/** Reasons that invalidate the root as well as the leaf. */
|
|
167
|
-
var REASONS_REQUIRING_NEW_CA = [
|
|
168
|
-
"missing",
|
|
169
|
-
"unreadable",
|
|
170
|
-
"no-local-ca",
|
|
171
|
-
"ca-expiring"
|
|
172
|
-
];
|
|
173
|
-
function reasonRequiresNewCa(reason) {
|
|
174
|
-
return REASONS_REQUIRING_NEW_CA.includes(reason);
|
|
175
|
-
}
|
|
176
|
-
function parse(pem) {
|
|
177
|
-
try {
|
|
178
|
-
return new node_crypto.X509Certificate(pem);
|
|
179
|
-
} catch {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
function daysBetween(from, to) {
|
|
184
|
-
return (to.getTime() - from.getTime()) / MS_PER_DAY$1;
|
|
185
|
-
}
|
|
186
|
-
function hasServerAuthEku(leaf) {
|
|
187
|
-
const eku = leaf.keyUsage;
|
|
188
|
-
return eku !== void 0 && eku.length === 1 && eku[0] === "1.3.6.1.5.5.7.3.1";
|
|
189
|
-
}
|
|
190
|
-
function keyMatches(leaf, keyPem) {
|
|
191
|
-
try {
|
|
192
|
-
return leaf.checkPrivateKey((0, node_crypto.createPrivateKey)(keyPem));
|
|
193
|
-
} catch {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
function coversIdentity(leaf, identity) {
|
|
198
|
-
for (const name of identity.requiredDnsNames) if (leaf.checkHost(name) === void 0) return false;
|
|
199
|
-
for (const ip of identity.requiredIpAddresses) if (leaf.checkIP(ip) === void 0) return false;
|
|
200
|
-
return true;
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* `null` when the material on disk is fit to keep serving.
|
|
204
|
-
*
|
|
205
|
-
* Order matters only for the quality of the reported reason; any
|
|
206
|
-
* non-null answer produces the same action.
|
|
207
|
-
*/
|
|
208
|
-
function evaluateExistingCert(input) {
|
|
209
|
-
const leaf = parse(input.chainPem);
|
|
210
|
-
const ca = parse(input.caPem);
|
|
211
|
-
if (leaf === null || ca === null) return "unreadable";
|
|
212
|
-
if (daysBetween(input.now, new Date(ca.validTo)) < 427) return "ca-expiring";
|
|
213
|
-
if (!keyMatches(leaf, input.keyPem)) return "key-mismatch";
|
|
214
|
-
if (daysBetween(input.now, new Date(leaf.validTo)) < 30) return "expiring";
|
|
215
|
-
if (!hasServerAuthEku(leaf)) return "missing-server-auth-eku";
|
|
216
|
-
if (leaf.ca || !leaf.checkIssued(ca) || !leaf.verify(ca.publicKey)) return "not-issued-by-local-ca";
|
|
217
|
-
if (daysBetween(new Date(leaf.validFrom), new Date(leaf.validTo)) > 398) return "validity-too-long";
|
|
218
|
-
if (!coversIdentity(leaf, input.identity)) return "san-coverage-gap";
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
//#endregion
|
|
222
|
-
//#region src/tls/cert-identity.ts
|
|
223
|
-
/**
|
|
224
|
-
* What names and addresses the hub's certificate has to answer for.
|
|
225
|
-
*
|
|
226
|
-
* Split out of `cert-manager.ts` because it is pure and because the
|
|
227
|
-
* regeneration rule (D227) is decided entirely by the difference between
|
|
228
|
-
* *this* and what the certificate on disk already carries.
|
|
229
|
-
*/
|
|
230
|
-
/** The real machine. The only place in `tls/` that touches `os`. */
|
|
231
|
-
function systemHostFacts() {
|
|
232
|
-
return {
|
|
233
|
-
hostname: node_os.hostname(),
|
|
234
|
-
networkInterfaces: node_os.networkInterfaces()
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Interfaces whose addresses are certified but never gate a reissue:
|
|
239
|
-
* container bridges, VPN tunnels and mesh overlays hand out addresses
|
|
240
|
-
* that change without the hub moving.
|
|
241
|
-
*/
|
|
242
|
-
var VOLATILE_INTERFACE_PREFIXES = [
|
|
243
|
-
"docker",
|
|
244
|
-
"br-",
|
|
245
|
-
"veth",
|
|
246
|
-
"virbr",
|
|
247
|
-
"cni",
|
|
248
|
-
"flannel",
|
|
249
|
-
"tun",
|
|
250
|
-
"utun",
|
|
251
|
-
"tap",
|
|
252
|
-
"wg",
|
|
253
|
-
"zt",
|
|
254
|
-
"tailscale",
|
|
255
|
-
"ppp",
|
|
256
|
-
"awdl",
|
|
257
|
-
"llw"
|
|
258
|
-
];
|
|
259
|
-
/** A DNS name we are willing to put in a SAN. */
|
|
260
|
-
var DNS_NAME_PATTERN = /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
261
|
-
function isIpv4(value) {
|
|
262
|
-
return (0, node_net.isIP)(value) === 4;
|
|
263
|
-
}
|
|
264
|
-
function isIpv6(value) {
|
|
265
|
-
return (0, node_net.isIP)(value) === 6;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* `fe80::/10` and `169.254.0.0/16`. An IPv6 link-local address needs a
|
|
269
|
-
* zone index to be usable and a SAN cannot carry one, so such an entry
|
|
270
|
-
* can never match anything — it is noise that makes the certificate look
|
|
271
|
-
* broader than it is.
|
|
272
|
-
*/
|
|
273
|
-
function isLinkLocal(value) {
|
|
274
|
-
if (isIpv4(value)) return value.startsWith("169.254.");
|
|
275
|
-
if (!isIpv6(value)) return false;
|
|
276
|
-
const firstGroup = value.split(":")[0] ?? "";
|
|
277
|
-
if (firstGroup === "") return false;
|
|
278
|
-
const parsed = Number.parseInt(firstGroup, 16);
|
|
279
|
-
return Number.isFinite(parsed) && (parsed & 65472) === 65152;
|
|
280
|
-
}
|
|
281
|
-
function isVolatileInterface(name) {
|
|
282
|
-
const lower = name.toLowerCase();
|
|
283
|
-
return VOLATILE_INTERFACE_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Only IPv4 gates a reissue. Every IPv6 address we see — global **or**
|
|
287
|
-
* ULA — is certified but never required: `os` cannot tell a temporary
|
|
288
|
-
* address from a permanent one, and the live hub's ULA interface-id
|
|
289
|
-
* rotated on every restart (eight leaf reissues in one day under
|
|
290
|
-
* `san-coverage-gap`). Gating on ULA reproduced the churn D227 exists
|
|
291
|
-
* to prevent. Operators who need a fixed IPv6 declare it via `extraSans`.
|
|
292
|
-
*/
|
|
293
|
-
function isStableGateAddress(address) {
|
|
294
|
-
if (isLinkLocal(address)) return false;
|
|
295
|
-
return isIpv4(address);
|
|
296
|
-
}
|
|
297
|
-
function addSan(dns, ips, value) {
|
|
298
|
-
const trimmed = value.trim();
|
|
299
|
-
if (trimmed === "") return;
|
|
300
|
-
if ((0, node_net.isIP)(trimmed) !== 0) {
|
|
301
|
-
if (!isLinkLocal(trimmed)) ips.add(trimmed);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (DNS_NAME_PATTERN.test(trimmed)) dns.add(trimmed);
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Snapshot the machine's identity. Called once per `ensureTlsCert`; the
|
|
308
|
-
* result is both the SAN list for a new leaf and the yardstick the
|
|
309
|
-
* existing leaf is measured against.
|
|
310
|
-
*/
|
|
311
|
-
function collectCertIdentity(options) {
|
|
312
|
-
const host = options.hostFacts ?? systemHostFacts();
|
|
313
|
-
const dnsNames = /* @__PURE__ */ new Set();
|
|
314
|
-
const ipAddresses = new Set(["127.0.0.1", "::1"]);
|
|
315
|
-
const requiredDnsNames = /* @__PURE__ */ new Set();
|
|
316
|
-
const requiredIpAddresses = new Set(["127.0.0.1"]);
|
|
317
|
-
for (const name of [
|
|
318
|
-
"localhost",
|
|
319
|
-
options.commonName,
|
|
320
|
-
host.hostname
|
|
321
|
-
]) {
|
|
322
|
-
addSan(dnsNames, ipAddresses, name);
|
|
323
|
-
addSan(requiredDnsNames, requiredIpAddresses, name);
|
|
324
|
-
}
|
|
325
|
-
for (const [ifaceName, addrs] of Object.entries(host.networkInterfaces)) for (const addr of addrs ?? []) {
|
|
326
|
-
if (addr.internal) continue;
|
|
327
|
-
if (isLinkLocal(addr.address)) continue;
|
|
328
|
-
ipAddresses.add(addr.address);
|
|
329
|
-
if (isVolatileInterface(ifaceName)) continue;
|
|
330
|
-
if (isStableGateAddress(addr.address)) requiredIpAddresses.add(addr.address);
|
|
331
|
-
}
|
|
332
|
-
for (const san of options.extraSans) {
|
|
333
|
-
addSan(dnsNames, ipAddresses, san);
|
|
334
|
-
addSan(requiredDnsNames, requiredIpAddresses, san);
|
|
335
|
-
}
|
|
336
|
-
return {
|
|
337
|
-
dnsNames: [...dnsNames],
|
|
338
|
-
ipAddresses: [...ipAddresses],
|
|
339
|
-
requiredDnsNames: [...requiredDnsNames],
|
|
340
|
-
requiredIpAddresses: [...requiredIpAddresses]
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
2
|
/*! *****************************************************************************
|
|
344
3
|
Copyright (C) Microsoft. All rights reserved.
|
|
345
4
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
@@ -11248,8 +10907,44 @@ var Pkcs10CertificateRequest = class extends PemData {
|
|
|
11248
10907
|
};
|
|
11249
10908
|
_Pkcs10CertificateRequest_tbs = /* @__PURE__ */ new WeakMap(), _Pkcs10CertificateRequest_subjectName = /* @__PURE__ */ new WeakMap(), _Pkcs10CertificateRequest_subject = /* @__PURE__ */ new WeakMap(), _Pkcs10CertificateRequest_signatureAlgorithm = /* @__PURE__ */ new WeakMap(), _Pkcs10CertificateRequest_signature = /* @__PURE__ */ new WeakMap(), _Pkcs10CertificateRequest_publicKey = /* @__PURE__ */ new WeakMap(), _Pkcs10CertificateRequest_attributes = /* @__PURE__ */ new WeakMap(), _Pkcs10CertificateRequest_extensions = /* @__PURE__ */ new WeakMap();
|
|
11250
10909
|
Pkcs10CertificateRequest.NAME = "PKCS#10 Certificate Request";
|
|
10910
|
+
var Pkcs10CertificateRequestGenerator = class {
|
|
10911
|
+
static async create(params, crypto = cryptoProvider.get()) {
|
|
10912
|
+
if (!params.keys.privateKey) throw new Error("Bad field 'keys' in 'params' argument. 'privateKey' is empty");
|
|
10913
|
+
if (!params.keys.publicKey) throw new Error("Bad field 'keys' in 'params' argument. 'publicKey' is empty");
|
|
10914
|
+
const spki = await crypto.subtle.exportKey("spki", params.keys.publicKey);
|
|
10915
|
+
const asnReq = new CertificationRequest({ certificationRequestInfo: new CertificationRequestInfo({ subjectPKInfo: AsnConvert.parse(spki, SubjectPublicKeyInfo) }) });
|
|
10916
|
+
if (params.name) {
|
|
10917
|
+
const name = params.name instanceof Name ? params.name : new Name(params.name);
|
|
10918
|
+
asnReq.certificationRequestInfo.subject = AsnConvert.parse(name.toArrayBuffer(), Name$1);
|
|
10919
|
+
}
|
|
10920
|
+
if (params.attributes) for (const o of params.attributes) asnReq.certificationRequestInfo.attributes.push(AsnConvert.parse(o.rawData, Attribute$2));
|
|
10921
|
+
if (params.extensions && params.extensions.length) {
|
|
10922
|
+
const attr = new Attribute$2({ type: id_pkcs9_at_extensionRequest });
|
|
10923
|
+
const extensions = new Extensions();
|
|
10924
|
+
for (const o of params.extensions) extensions.push(AsnConvert.parse(o.rawData, Extension$1));
|
|
10925
|
+
attr.values.push(AsnConvert.serialize(extensions));
|
|
10926
|
+
asnReq.certificationRequestInfo.attributes.push(attr);
|
|
10927
|
+
}
|
|
10928
|
+
const signingAlgorithm = {
|
|
10929
|
+
...params.signingAlgorithm,
|
|
10930
|
+
...params.keys.privateKey.algorithm
|
|
10931
|
+
};
|
|
10932
|
+
asnReq.signatureAlgorithm = instance.resolve(diAlgorithmProvider).toAsnAlgorithm(signingAlgorithm);
|
|
10933
|
+
const tbs = AsnConvert.serialize(asnReq.certificationRequestInfo);
|
|
10934
|
+
const signature = await crypto.subtle.sign(signingAlgorithm, params.keys.privateKey, tbs);
|
|
10935
|
+
const signatureFormatters = instance.resolveAll(diAsnSignatureFormatter).reverse();
|
|
10936
|
+
let asnSignature = null;
|
|
10937
|
+
for (const signatureFormatter of signatureFormatters) {
|
|
10938
|
+
asnSignature = signatureFormatter.toAsnSignature(signingAlgorithm, signature);
|
|
10939
|
+
if (asnSignature) break;
|
|
10940
|
+
}
|
|
10941
|
+
if (!asnSignature) throw Error("Cannot convert WebCrypto signature value to ASN.1 format");
|
|
10942
|
+
asnReq.signature = asnSignature;
|
|
10943
|
+
return new Pkcs10CertificateRequest(AsnConvert.serialize(asnReq));
|
|
10944
|
+
}
|
|
10945
|
+
};
|
|
11251
10946
|
var _X509Certificate_tbs, _X509Certificate_serialNumber, _X509Certificate_subjectName, _X509Certificate_subject, _X509Certificate_issuerName, _X509Certificate_issuer, _X509Certificate_notBefore, _X509Certificate_notAfter, _X509Certificate_signatureAlgorithm, _X509Certificate_signature, _X509Certificate_extensions, _X509Certificate_publicKey;
|
|
11252
|
-
var X509Certificate
|
|
10947
|
+
var X509Certificate = class extends PemData {
|
|
11253
10948
|
get publicKey() {
|
|
11254
10949
|
if (!__classPrivateFieldGet(this, _X509Certificate_publicKey, "f")) __classPrivateFieldSet(this, _X509Certificate_publicKey, new PublicKey(this.asn.tbsCertificate.subjectPublicKeyInfo), "f");
|
|
11255
10950
|
return __classPrivateFieldGet(this, _X509Certificate_publicKey, "f");
|
|
@@ -11449,7 +11144,7 @@ var X509Certificate$4 = class extends PemData {
|
|
|
11449
11144
|
}
|
|
11450
11145
|
};
|
|
11451
11146
|
_X509Certificate_tbs = /* @__PURE__ */ new WeakMap(), _X509Certificate_serialNumber = /* @__PURE__ */ new WeakMap(), _X509Certificate_subjectName = /* @__PURE__ */ new WeakMap(), _X509Certificate_subject = /* @__PURE__ */ new WeakMap(), _X509Certificate_issuerName = /* @__PURE__ */ new WeakMap(), _X509Certificate_issuer = /* @__PURE__ */ new WeakMap(), _X509Certificate_notBefore = /* @__PURE__ */ new WeakMap(), _X509Certificate_notAfter = /* @__PURE__ */ new WeakMap(), _X509Certificate_signatureAlgorithm = /* @__PURE__ */ new WeakMap(), _X509Certificate_signature = /* @__PURE__ */ new WeakMap(), _X509Certificate_extensions = /* @__PURE__ */ new WeakMap(), _X509Certificate_publicKey = /* @__PURE__ */ new WeakMap();
|
|
11452
|
-
X509Certificate
|
|
11147
|
+
X509Certificate.NAME = "Certificate";
|
|
11453
11148
|
function generateCertificateSerialNumber(input, crypto = cryptoProvider.get()) {
|
|
11454
11149
|
const inputView = BufferSourceConverter.toUint8Array(Convert.FromHex(input || ""));
|
|
11455
11150
|
let serialNumber = inputView && inputView.length && inputView.some((o) => o > 0) ? new Uint8Array(inputView) : void 0;
|
|
@@ -11530,7 +11225,7 @@ var X509CertificateGenerator = class {
|
|
|
11530
11225
|
}
|
|
11531
11226
|
if (!asnSignature) throw Error("Cannot convert ASN.1 signature value to WebCrypto format");
|
|
11532
11227
|
asnX509.signatureValue = asnSignature;
|
|
11533
|
-
return new X509Certificate
|
|
11228
|
+
return new X509Certificate(AsnConvert.serialize(asnX509));
|
|
11534
11229
|
}
|
|
11535
11230
|
};
|
|
11536
11231
|
var X509CrlReason;
|
|
@@ -11564,819 +11259,81 @@ AsnEcSignatureFormatter.namedCurveSize.set("K-256", 32);
|
|
|
11564
11259
|
AsnEcSignatureFormatter.namedCurveSize.set("P-384", 48);
|
|
11565
11260
|
AsnEcSignatureFormatter.namedCurveSize.set("P-521", 66);
|
|
11566
11261
|
//#endregion
|
|
11567
|
-
|
|
11568
|
-
cryptoProvider.set(node_crypto.webcrypto);
|
|
11569
|
-
/** Subject of the local root. Distinct from the leaf's CN by construction. */
|
|
11570
|
-
var CA_COMMON_NAME = "CamStack Local CA";
|
|
11571
|
-
var MS_PER_DAY = 864e5;
|
|
11572
|
-
/**
|
|
11573
|
-
* RSA-2048 / SHA-256. Unchanged from the openssl invocation this replaces:
|
|
11574
|
-
* every client that trusted the previous material still understands this.
|
|
11575
|
-
*/
|
|
11576
|
-
var SIGNING_ALGORITHM = {
|
|
11577
|
-
name: "RSASSA-PKCS1-v1_5",
|
|
11578
|
-
hash: "SHA-256",
|
|
11579
|
-
publicExponent: new Uint8Array([
|
|
11580
|
-
1,
|
|
11581
|
-
0,
|
|
11582
|
-
1
|
|
11583
|
-
]),
|
|
11584
|
-
modulusLength: 2048
|
|
11585
|
-
};
|
|
11586
|
-
/**
|
|
11587
|
-
* A serial number that is positive and unpredictable. `00` prefix for the
|
|
11588
|
-
* same reason the openssl invocation carried one: a leading bit of 1 would
|
|
11589
|
-
* make the DER INTEGER negative.
|
|
11590
|
-
*/
|
|
11591
|
-
function freshSerial() {
|
|
11592
|
-
return `00${(0, node_crypto.randomBytes)(16).toString("hex")}`;
|
|
11593
|
-
}
|
|
11594
|
-
function toPkcs8Pem(pkcs8) {
|
|
11595
|
-
return `${PemConverter.encode(pkcs8, "PRIVATE KEY")}\n`;
|
|
11596
|
-
}
|
|
11597
|
-
function toCertPem(cert) {
|
|
11598
|
-
return `${cert.toString("pem")}\n`;
|
|
11599
|
-
}
|
|
11600
|
-
async function exportKeyPem(key) {
|
|
11601
|
-
return toPkcs8Pem(await node_crypto.webcrypto.subtle.exportKey("pkcs8", key));
|
|
11602
|
-
}
|
|
11603
|
-
/** SAN entries, in the order `cert-identity` produced them. */
|
|
11604
|
-
function subjectAltNames(identity) {
|
|
11605
|
-
const names = [];
|
|
11606
|
-
for (const dns of identity.dnsNames) names.push({
|
|
11607
|
-
type: "dns",
|
|
11608
|
-
value: dns
|
|
11609
|
-
});
|
|
11610
|
-
for (const ip of identity.ipAddresses) if ((0, node_net.isIP)(ip) !== 0) names.push({
|
|
11611
|
-
type: "ip",
|
|
11612
|
-
value: ip
|
|
11613
|
-
});
|
|
11614
|
-
return names;
|
|
11615
|
-
}
|
|
11616
|
-
/**
|
|
11617
|
-
* A local root: 10 years, `pathlen:0` so it can sign leaves and nothing
|
|
11618
|
-
* else, and **no EKU** — Apple rejects an anchor asserting
|
|
11619
|
-
* `anyExtendedKeyUsage`, and a root with no EKU constrains nothing.
|
|
11620
|
-
*/
|
|
11621
|
-
async function generateCa(now) {
|
|
11622
|
-
const keys = await node_crypto.webcrypto.subtle.generateKey(SIGNING_ALGORITHM, true, ["sign", "verify"]);
|
|
11623
|
-
const extensions = [
|
|
11624
|
-
new BasicConstraintsExtension(true, 0, true),
|
|
11625
|
-
new KeyUsagesExtension(KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign, true),
|
|
11626
|
-
await SubjectKeyIdentifierExtension.create(keys.publicKey)
|
|
11627
|
-
];
|
|
11628
|
-
const cert = await X509CertificateGenerator.createSelfSigned({
|
|
11629
|
-
serialNumber: freshSerial(),
|
|
11630
|
-
name: `CN=${CA_COMMON_NAME}`,
|
|
11631
|
-
notBefore: now,
|
|
11632
|
-
notAfter: new Date(now.getTime() + CA_VALIDITY_DAYS * MS_PER_DAY),
|
|
11633
|
-
keys,
|
|
11634
|
-
signingAlgorithm: SIGNING_ALGORITHM,
|
|
11635
|
-
extensions
|
|
11636
|
-
});
|
|
11637
|
-
return {
|
|
11638
|
-
certPem: toCertPem(cert),
|
|
11639
|
-
keyPem: await exportKeyPem(keys.privateKey),
|
|
11640
|
-
cert,
|
|
11641
|
-
privateKey: keys.privateKey
|
|
11642
|
-
};
|
|
11643
|
-
}
|
|
11644
|
-
/** Re-open a root already on disk so a leaf can be issued under it. */
|
|
11645
|
-
async function loadCa(certPem, keyPem) {
|
|
11646
|
-
return {
|
|
11647
|
-
certPem,
|
|
11648
|
-
keyPem,
|
|
11649
|
-
cert: new X509Certificate$4(certPem),
|
|
11650
|
-
privateKey: await node_crypto.webcrypto.subtle.importKey("pkcs8", PemConverter.decodeFirst(keyPem), SIGNING_ALGORITHM, true, ["sign"])
|
|
11651
|
-
};
|
|
11652
|
-
}
|
|
11653
|
-
/**
|
|
11654
|
-
* A server leaf signed by the local root: `CA:FALSE`, and
|
|
11655
|
-
* `extendedKeyUsage=serverAuth` — not `anyExtendedKeyUsage`, which
|
|
11656
|
-
* Apple's `testAppTrustRoot_AnyEKU_AfterJul2019` asserts must fail.
|
|
11657
|
-
*/
|
|
11658
|
-
async function issueLeaf(ca, identity, commonName, validDays, now) {
|
|
11659
|
-
const keys = await node_crypto.webcrypto.subtle.generateKey(SIGNING_ALGORITHM, true, ["sign", "verify"]);
|
|
11660
|
-
const extensions = [
|
|
11661
|
-
new BasicConstraintsExtension(false, void 0, true),
|
|
11662
|
-
new KeyUsagesExtension(KeyUsageFlags.digitalSignature | KeyUsageFlags.keyEncipherment, true),
|
|
11663
|
-
new ExtendedKeyUsageExtension([SERVER_AUTH_OID]),
|
|
11664
|
-
await SubjectKeyIdentifierExtension.create(keys.publicKey),
|
|
11665
|
-
await AuthorityKeyIdentifierExtension.create(ca.cert),
|
|
11666
|
-
new SubjectAlternativeNameExtension(subjectAltNames(identity))
|
|
11667
|
-
];
|
|
11668
|
-
return {
|
|
11669
|
-
certPem: toCertPem(await X509CertificateGenerator.create({
|
|
11670
|
-
serialNumber: freshSerial(),
|
|
11671
|
-
subject: `CN=${commonName}`,
|
|
11672
|
-
issuer: ca.cert.subject,
|
|
11673
|
-
notBefore: now,
|
|
11674
|
-
notAfter: new Date(now.getTime() + validDays * MS_PER_DAY),
|
|
11675
|
-
publicKey: keys.publicKey,
|
|
11676
|
-
signingKey: ca.privateKey,
|
|
11677
|
-
signingAlgorithm: SIGNING_ALGORITHM,
|
|
11678
|
-
extensions
|
|
11679
|
-
})),
|
|
11680
|
-
keyPem: await exportKeyPem(keys.privateKey)
|
|
11681
|
-
};
|
|
11682
|
-
}
|
|
11683
|
-
//#endregion
|
|
11684
|
-
//#region src/tls/cert-manager.ts
|
|
11685
|
-
var DEFAULT_COMMON_NAME = "camstack.local";
|
|
11686
|
-
/**
|
|
11687
|
-
* Ensure the hub has a usable TLS identity under `<dataDir>/tls`, made of
|
|
11688
|
-
* a long-lived local **root** (`camstack-ca.crt`) and a short **leaf**
|
|
11689
|
-
* chained to it (`camstack.crt` = leaf + root, `camstack.key`).
|
|
11690
|
-
*
|
|
11691
|
-
* ## The regeneration rule (D227)
|
|
11692
|
-
*
|
|
11693
|
-
* Reissue happens when, and only when, the material on disk **cannot do
|
|
11694
|
-
* its job**:
|
|
11695
|
-
*
|
|
11696
|
-
* - it is absent, unreadable, or the key does not match the certificate;
|
|
11697
|
-
* - the root is absent (every pre-2026-08 install: one self-signed
|
|
11698
|
-
* `CA:TRUE` leaf and no root) or is inside its renewal window;
|
|
11699
|
-
* - the leaf is inside its 30-day renewal window;
|
|
11700
|
-
* - the leaf lacks `extendedKeyUsage = serverAuth` — Apple's trustd
|
|
11701
|
-
* rejects an app-supplied anchor without it;
|
|
11702
|
-
* - the leaf is not actually issued by the root on disk;
|
|
11703
|
-
* - the leaf's validity span exceeds {@link MAX_LEAF_VALIDITY_DAYS};
|
|
11704
|
-
* - **a name or address the hub must answer for is missing from the SAN
|
|
11705
|
-
* list** — a DHCP move, a new interface or a new hostname.
|
|
11706
|
-
*
|
|
11707
|
-
* And explicitly NOT when: the certificate names an address the machine
|
|
11708
|
-
* no longer has (a superset still works — the check is one-directional),
|
|
11709
|
-
* a volatile interface (docker/VPN/mesh) or **any** IPv6 address changed
|
|
11710
|
-
* (global and ULA both rotate under privacy extensions / interface-id
|
|
11711
|
-
* churn — `os` cannot tell temporary from permanent), or a new build shipped.
|
|
11712
|
-
*
|
|
11713
|
-
* Every trigger is derived from the files plus the machine, so once a
|
|
11714
|
-
* reissue clears them it clears them for good: the next boot returns
|
|
11715
|
-
* `generated: false`. A certificate that moved on every boot would
|
|
11716
|
-
* invalidate every trust an operator granted by hand — worse than the
|
|
11717
|
-
* bugs this rule fixes.
|
|
11718
|
-
*
|
|
11719
|
-
* **The root is rotated only for root-level reasons.** SAN drift rotates
|
|
11720
|
-
* the leaf alone, so trust granted to `camstack-ca.crt` survives every
|
|
11721
|
-
* address change the hub will ever see.
|
|
11722
|
-
*/
|
|
11723
|
-
async function ensureTlsCert(dataDir, options) {
|
|
11724
|
-
const tlsDir = (0, node_path.join)(dataDir, "tls");
|
|
11725
|
-
const paths = {
|
|
11726
|
-
certPath: (0, node_path.join)(tlsDir, "camstack.crt"),
|
|
11727
|
-
keyPath: (0, node_path.join)(tlsDir, "camstack.key"),
|
|
11728
|
-
caCertPath: (0, node_path.join)(tlsDir, "camstack-ca.crt"),
|
|
11729
|
-
caKeyPath: (0, node_path.join)(tlsDir, "camstack-ca.key")
|
|
11730
|
-
};
|
|
11731
|
-
const commonName = options?.commonName ?? DEFAULT_COMMON_NAME;
|
|
11732
|
-
const validDays = Math.min(options?.validDays ?? 397, 397);
|
|
11733
|
-
const identity = collectCertIdentity({
|
|
11734
|
-
commonName,
|
|
11735
|
-
extraSans: options?.extraSans ?? [],
|
|
11736
|
-
...options?.hostFacts === void 0 ? {} : { hostFacts: options.hostFacts }
|
|
11737
|
-
});
|
|
11738
|
-
const reason = decideRegeneration(paths, identity);
|
|
11739
|
-
if (reason === null) return describe(paths, false, null, false, null, null);
|
|
11740
|
-
const previousFingerprint = readFingerprint(paths.certPath);
|
|
11741
|
-
const caRotated = reasonRequiresNewCa(reason);
|
|
11742
|
-
(0, node_fs.mkdirSync)(tlsDir, { recursive: true });
|
|
11743
|
-
try {
|
|
11744
|
-
await regenerate(paths, identity, commonName, validDays, caRotated);
|
|
11745
|
-
} catch (err) {
|
|
11746
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
11747
|
-
if (!canKeepServing(paths)) throw err;
|
|
11748
|
-
return describe(paths, false, null, false, null, message);
|
|
11749
|
-
}
|
|
11750
|
-
return describe(paths, true, reason, caRotated, previousFingerprint, null);
|
|
11751
|
-
}
|
|
11752
|
-
/**
|
|
11753
|
-
* Operator "Regenerate": issue a new leaf under the existing local CA.
|
|
11754
|
-
* Never rotates the root — trust granted to `camstack-ca.crt` survives.
|
|
11755
|
-
*/
|
|
11756
|
-
async function reissueTlsLeaf(dataDir, options) {
|
|
11757
|
-
const tlsDir = (0, node_path.join)(dataDir, "tls");
|
|
11758
|
-
const paths = {
|
|
11759
|
-
certPath: (0, node_path.join)(tlsDir, "camstack.crt"),
|
|
11760
|
-
keyPath: (0, node_path.join)(tlsDir, "camstack.key"),
|
|
11761
|
-
caCertPath: (0, node_path.join)(tlsDir, "camstack-ca.crt"),
|
|
11762
|
-
caKeyPath: (0, node_path.join)(tlsDir, "camstack-ca.key")
|
|
11763
|
-
};
|
|
11764
|
-
if (!(0, node_fs.existsSync)(paths.caCertPath) || !(0, node_fs.existsSync)(paths.caKeyPath)) return ensureTlsCert(dataDir, options);
|
|
11765
|
-
const commonName = options?.commonName ?? DEFAULT_COMMON_NAME;
|
|
11766
|
-
const validDays = Math.min(options?.validDays ?? 397, 397);
|
|
11767
|
-
const identity = collectCertIdentity({
|
|
11768
|
-
commonName,
|
|
11769
|
-
extraSans: options?.extraSans ?? [],
|
|
11770
|
-
...options?.hostFacts === void 0 ? {} : { hostFacts: options.hostFacts }
|
|
11771
|
-
});
|
|
11772
|
-
const previousFingerprint = readFingerprint(paths.certPath);
|
|
11773
|
-
(0, node_fs.mkdirSync)(tlsDir, { recursive: true });
|
|
11774
|
-
await regenerate(paths, identity, commonName, validDays, false);
|
|
11775
|
-
return describe(paths, true, "san-coverage-gap", false, previousFingerprint, null);
|
|
11776
|
-
}
|
|
11777
|
-
/** True when the material already on disk can still terminate TLS. */
|
|
11778
|
-
function canKeepServing(paths) {
|
|
11779
|
-
if (!(0, node_fs.existsSync)(paths.certPath) || !(0, node_fs.existsSync)(paths.keyPath)) return false;
|
|
11780
|
-
if (!(0, node_fs.existsSync)(paths.caCertPath)) return false;
|
|
11781
|
-
return readFingerprint(paths.certPath) !== null;
|
|
11782
|
-
}
|
|
11783
|
-
/** Fingerprint of whatever is on disk, or `null` if there is nothing readable. */
|
|
11784
|
-
function readFingerprint(certPath) {
|
|
11785
|
-
try {
|
|
11786
|
-
return new node_crypto.X509Certificate((0, node_fs.readFileSync)(certPath)).fingerprint256;
|
|
11787
|
-
} catch {
|
|
11788
|
-
return null;
|
|
11789
|
-
}
|
|
11790
|
-
}
|
|
11791
|
-
function decideRegeneration(paths, identity) {
|
|
11792
|
-
if (!(0, node_fs.existsSync)(paths.certPath) || !(0, node_fs.existsSync)(paths.keyPath)) return "missing";
|
|
11793
|
-
if (!(0, node_fs.existsSync)(paths.caCertPath) || !(0, node_fs.existsSync)(paths.caKeyPath)) return "no-local-ca";
|
|
11794
|
-
try {
|
|
11795
|
-
return evaluateExistingCert({
|
|
11796
|
-
chainPem: (0, node_fs.readFileSync)(paths.certPath, "utf-8"),
|
|
11797
|
-
keyPem: (0, node_fs.readFileSync)(paths.keyPath, "utf-8"),
|
|
11798
|
-
caPem: (0, node_fs.readFileSync)(paths.caCertPath, "utf-8"),
|
|
11799
|
-
identity,
|
|
11800
|
-
now: /* @__PURE__ */ new Date()
|
|
11801
|
-
});
|
|
11802
|
-
} catch {
|
|
11803
|
-
return "unreadable";
|
|
11804
|
-
}
|
|
11805
|
-
}
|
|
11806
|
-
/**
|
|
11807
|
-
* Build every byte in memory, then move the files into place. A crash
|
|
11808
|
-
* mid-way leaves the previous material untouched; a crash between the
|
|
11809
|
-
* moves leaves a key/certificate mismatch, which the evaluator reports as
|
|
11810
|
-
* `key-mismatch` and heals on the next boot.
|
|
11811
|
-
*/
|
|
11812
|
-
async function regenerate(paths, identity, commonName, validDays, newCa) {
|
|
11813
|
-
const now = /* @__PURE__ */ new Date();
|
|
11814
|
-
const ca = newCa ? await generateCa(now) : await loadCa((0, node_fs.readFileSync)(paths.caCertPath, "utf-8"), (0, node_fs.readFileSync)(paths.caKeyPath, "utf-8"));
|
|
11815
|
-
const leaf = await issueLeaf(ca, identity, commonName, validDays, now);
|
|
11816
|
-
const chainPem = `${leaf.certPem.trimEnd()}\n${ca.certPem.trimEnd()}\n`;
|
|
11817
|
-
if (newCa) {
|
|
11818
|
-
await (0, node_fs_promises.writeFile)(`${paths.caCertPath}.new`, ca.certPem, "utf-8");
|
|
11819
|
-
await (0, node_fs_promises.writeFile)(`${paths.caKeyPath}.new`, ca.keyPem, {
|
|
11820
|
-
encoding: "utf-8",
|
|
11821
|
-
mode: 384
|
|
11822
|
-
});
|
|
11823
|
-
(0, node_fs.renameSync)(`${paths.caCertPath}.new`, paths.caCertPath);
|
|
11824
|
-
(0, node_fs.renameSync)(`${paths.caKeyPath}.new`, paths.caKeyPath);
|
|
11825
|
-
await (0, node_fs_promises.chmod)(paths.caKeyPath, 384);
|
|
11826
|
-
}
|
|
11827
|
-
await (0, node_fs_promises.writeFile)(`${paths.keyPath}.new`, leaf.keyPem, {
|
|
11828
|
-
encoding: "utf-8",
|
|
11829
|
-
mode: 384
|
|
11830
|
-
});
|
|
11831
|
-
await (0, node_fs_promises.writeFile)(`${paths.certPath}.new`, chainPem, "utf-8");
|
|
11832
|
-
(0, node_fs.renameSync)(`${paths.keyPath}.new`, paths.keyPath);
|
|
11833
|
-
(0, node_fs.renameSync)(`${paths.certPath}.new`, paths.certPath);
|
|
11834
|
-
await (0, node_fs_promises.chmod)(paths.keyPath, 384);
|
|
11835
|
-
}
|
|
11836
|
-
function describe(paths, generated, reason, caRotated, previousFingerprintSha256, reissueError) {
|
|
11837
|
-
const leaf = new node_crypto.X509Certificate((0, node_fs.readFileSync)(paths.certPath));
|
|
11838
|
-
const ca = new node_crypto.X509Certificate((0, node_fs.readFileSync)(paths.caCertPath));
|
|
11839
|
-
const san = leaf.subjectAltName ?? "";
|
|
11840
|
-
return {
|
|
11841
|
-
...paths,
|
|
11842
|
-
generated,
|
|
11843
|
-
reason,
|
|
11844
|
-
caRotated,
|
|
11845
|
-
fingerprintSha256: leaf.fingerprint256,
|
|
11846
|
-
previousFingerprintSha256,
|
|
11847
|
-
caFingerprintSha256: ca.fingerprint256,
|
|
11848
|
-
validTo: new Date(leaf.validTo).toISOString(),
|
|
11849
|
-
sans: san === "" ? [] : san.split(", "),
|
|
11850
|
-
reissueError
|
|
11851
|
-
};
|
|
11852
|
-
}
|
|
11853
|
-
/**
|
|
11854
|
-
* Load TLS cert+key from files. Returns Buffers suitable for Node.js TLS
|
|
11855
|
-
* options. `certPath` is a full chain, so the listener presents the local
|
|
11856
|
-
* root alongside the leaf.
|
|
11857
|
-
*/
|
|
11858
|
-
function loadTlsCert(certPath, keyPath) {
|
|
11859
|
-
return {
|
|
11860
|
-
cert: (0, node_fs.readFileSync)(certPath),
|
|
11861
|
-
key: (0, node_fs.readFileSync)(keyPath)
|
|
11862
|
-
};
|
|
11863
|
-
}
|
|
11864
|
-
//#endregion
|
|
11865
|
-
//#region src/tls/tls-status.ts
|
|
11866
|
-
function tlsDir(dataDir) {
|
|
11867
|
-
return (0, node_path.join)(dataDir, "tls");
|
|
11868
|
-
}
|
|
11869
|
-
function readTlsMode(dataDir) {
|
|
11870
|
-
const marker = (0, node_path.join)(tlsDir(dataDir), "mode");
|
|
11871
|
-
if ((0, node_fs.existsSync)(marker)) {
|
|
11872
|
-
const raw = (0, node_fs.readFileSync)(marker, "utf-8").trim();
|
|
11873
|
-
if (raw === "uploaded") return "uploaded";
|
|
11874
|
-
if (raw === "disabled") return "disabled";
|
|
11875
|
-
}
|
|
11876
|
-
return (0, node_fs.existsSync)((0, node_path.join)(tlsDir(dataDir), "camstack.crt")) ? "generated" : "disabled";
|
|
11877
|
-
}
|
|
11878
|
-
function writeTlsMode(dataDir, mode) {
|
|
11879
|
-
(0, node_fs.mkdirSync)(tlsDir(dataDir), { recursive: true });
|
|
11880
|
-
(0, node_fs.writeFileSync)((0, node_path.join)(tlsDir(dataDir), "mode"), `${mode}\n`, "utf-8");
|
|
11881
|
-
}
|
|
11882
|
-
function readExtraSans(dataDir) {
|
|
11883
|
-
const path = (0, node_path.join)(tlsDir(dataDir), "extra-sans.json");
|
|
11884
|
-
if (!(0, node_fs.existsSync)(path)) return [];
|
|
11885
|
-
try {
|
|
11886
|
-
const parsed = JSON.parse((0, node_fs.readFileSync)(path, "utf-8"));
|
|
11887
|
-
if (!Array.isArray(parsed)) return [];
|
|
11888
|
-
return parsed.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
11889
|
-
} catch {
|
|
11890
|
-
return [];
|
|
11891
|
-
}
|
|
11892
|
-
}
|
|
11893
|
-
function writeExtraSans(dataDir, sans) {
|
|
11894
|
-
(0, node_fs.mkdirSync)(tlsDir(dataDir), { recursive: true });
|
|
11895
|
-
(0, node_fs.writeFileSync)((0, node_path.join)(tlsDir(dataDir), "extra-sans.json"), `${JSON.stringify(sans)}\n`, "utf-8");
|
|
11896
|
-
}
|
|
11897
|
-
/** Where an upload's own CA is kept — never over the local CA's `camstack-ca.crt`. */
|
|
11898
|
-
var UPLOADED_CA_FILE = "uploaded-ca.crt";
|
|
11899
|
-
/** Every certificate in a PEM bundle, in order. */
|
|
11900
|
-
function splitPemCertificates(pem) {
|
|
11901
|
-
const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g);
|
|
11902
|
-
return blocks === null ? [] : blocks.map((block) => `${block}\n`);
|
|
11903
|
-
}
|
|
11904
|
-
/**
|
|
11905
|
-
* The certificate that signed `leaf`, from the upload itself: the CA the
|
|
11906
|
-
* operator supplied, else the rest of the uploaded chain. `null` when the
|
|
11907
|
-
* upload did not include it — and then nothing may stand in for it: the
|
|
11908
|
-
* local CA did not sign an uploaded leaf, so handing it out as "the CA" would
|
|
11909
|
-
* tell a client to trust a root that verifies nothing it is served.
|
|
11910
|
-
*/
|
|
11911
|
-
function uploadedIssuer(dir, leaf, chainPem) {
|
|
11912
|
-
const caPath = (0, node_path.join)(dir, UPLOADED_CA_FILE);
|
|
11913
|
-
const candidates = [...(0, node_fs.existsSync)(caPath) ? splitPemCertificates((0, node_fs.readFileSync)(caPath, "utf-8")) : [], ...splitPemCertificates(chainPem).slice(1)];
|
|
11914
|
-
for (const pem of candidates) try {
|
|
11915
|
-
const cert = new node_crypto.X509Certificate(pem);
|
|
11916
|
-
if (leaf.checkIssued(cert) && leaf.verify(cert.publicKey)) return {
|
|
11917
|
-
pem,
|
|
11918
|
-
cert
|
|
11919
|
-
};
|
|
11920
|
-
} catch {}
|
|
11921
|
-
return null;
|
|
11922
|
-
}
|
|
11923
|
-
function generatedIssuer(dir) {
|
|
11924
|
-
const caPath = (0, node_path.join)(dir, "camstack-ca.crt");
|
|
11925
|
-
if (!(0, node_fs.existsSync)(caPath)) return null;
|
|
11926
|
-
const pem = (0, node_fs.readFileSync)(caPath, "utf-8");
|
|
11927
|
-
return {
|
|
11928
|
-
pem,
|
|
11929
|
-
cert: new node_crypto.X509Certificate(pem)
|
|
11930
|
-
};
|
|
11931
|
-
}
|
|
11932
|
-
/**
|
|
11933
|
-
* What the node's certificate is, and whether the listener serves it.
|
|
11934
|
-
* `servedLeafFingerprint` is the leaf the HTTPS listener presents right now
|
|
11935
|
-
* (`servedHttpsLeafFingerprint()`), or `null` when unknown; a restart is
|
|
11936
|
-
* required exactly when the listener serves a different leaf than the one
|
|
11937
|
-
* on disk — a measured fact, not a flag a mutation remembered to set.
|
|
11938
|
-
*/
|
|
11939
|
-
function readTlsAccessStatus(dataDir, servedLeafFingerprint) {
|
|
11940
|
-
const dir = tlsDir(dataDir);
|
|
11941
|
-
const certPath = (0, node_path.join)(dir, "camstack.crt");
|
|
11942
|
-
const mode = readTlsMode(dataDir);
|
|
11943
|
-
if (!(0, node_fs.existsSync)(certPath)) return {
|
|
11944
|
-
mode: "disabled",
|
|
11945
|
-
leafFingerprintSha256: null,
|
|
11946
|
-
caFingerprintSha256: null,
|
|
11947
|
-
validTo: null,
|
|
11948
|
-
sans: [],
|
|
11949
|
-
caCertPem: (mode === "uploaded" ? null : generatedIssuer(dir))?.pem ?? null,
|
|
11950
|
-
reissueError: null,
|
|
11951
|
-
restartRequired: false
|
|
11952
|
-
};
|
|
11953
|
-
try {
|
|
11954
|
-
const chainPem = (0, node_fs.readFileSync)(certPath, "utf-8");
|
|
11955
|
-
const leaf = new node_crypto.X509Certificate(chainPem);
|
|
11956
|
-
const ca = mode === "uploaded" ? uploadedIssuer(dir, leaf, chainPem) : generatedIssuer(dir);
|
|
11957
|
-
const san = leaf.subjectAltName ?? "";
|
|
11958
|
-
return {
|
|
11959
|
-
mode,
|
|
11960
|
-
leafFingerprintSha256: leaf.fingerprint256,
|
|
11961
|
-
caFingerprintSha256: ca?.cert.fingerprint256 ?? null,
|
|
11962
|
-
validTo: new Date(leaf.validTo).toISOString(),
|
|
11963
|
-
sans: san === "" ? [] : san.split(", "),
|
|
11964
|
-
caCertPem: ca?.pem ?? null,
|
|
11965
|
-
reissueError: null,
|
|
11966
|
-
restartRequired: servedLeafFingerprint !== null && servedLeafFingerprint !== leaf.fingerprint256
|
|
11967
|
-
};
|
|
11968
|
-
} catch {
|
|
11969
|
-
return {
|
|
11970
|
-
mode,
|
|
11971
|
-
leafFingerprintSha256: null,
|
|
11972
|
-
caFingerprintSha256: null,
|
|
11973
|
-
validTo: null,
|
|
11974
|
-
sans: [],
|
|
11975
|
-
caCertPem: null,
|
|
11976
|
-
reissueError: "Certificate on disk could not be parsed.",
|
|
11977
|
-
restartRequired: false
|
|
11978
|
-
};
|
|
11979
|
-
}
|
|
11980
|
-
}
|
|
11981
|
-
//#endregion
|
|
11982
|
-
//#region src/tls/boot-tls.ts
|
|
11983
|
-
/**
|
|
11984
|
-
* Decide what the listener serves at boot. The admin UI (Network → Local
|
|
11985
|
-
* access) is the only way to set a certificate (D726): it either leaves the
|
|
11986
|
-
* hub on its generated local-CA leaf (D227) or uploads one, recorded by the
|
|
11987
|
-
* `tls/mode` marker. An uploaded certificate is served verbatim — running it
|
|
11988
|
-
* through `ensureTlsCert` would replace it on the first restart, because a
|
|
11989
|
-
* leaf the local root did not issue is one of that rule's reissue triggers.
|
|
11990
|
-
*/
|
|
11991
|
-
async function prepareBootTls(dataDir, options) {
|
|
11992
|
-
const certPath = (0, node_path.join)(dataDir, "tls", "camstack.crt");
|
|
11993
|
-
const keyPath = (0, node_path.join)(dataDir, "tls", "camstack.key");
|
|
11994
|
-
const marked = readTlsMode(dataDir);
|
|
11995
|
-
if (marked === "uploaded" && (0, node_fs.existsSync)(certPath) && (0, node_fs.existsSync)(keyPath)) return {
|
|
11996
|
-
mode: "uploaded",
|
|
11997
|
-
pair: loadTlsCert(certPath, keyPath),
|
|
11998
|
-
ensured: void 0,
|
|
11999
|
-
uploadedMissing: false
|
|
12000
|
-
};
|
|
12001
|
-
const uploadedMissing = marked === "uploaded";
|
|
12002
|
-
if (uploadedMissing) writeTlsMode(dataDir, "generated");
|
|
12003
|
-
const ensured = await ensureTlsCert(dataDir, options);
|
|
12004
|
-
return {
|
|
12005
|
-
mode: "generated",
|
|
12006
|
-
pair: loadTlsCert(ensured.certPath, ensured.keyPath),
|
|
12007
|
-
ensured,
|
|
12008
|
-
uploadedMissing
|
|
12009
|
-
};
|
|
12010
|
-
}
|
|
12011
|
-
//#endregion
|
|
12012
|
-
//#region src/tls/https-listener.ts
|
|
12013
|
-
var SLOT_KEY = Symbol.for("camstack.tls.https-listener");
|
|
12014
|
-
var EMPTY_SLOT = {
|
|
12015
|
-
reload: null,
|
|
12016
|
-
servedLeafFingerprint: null
|
|
12017
|
-
};
|
|
12018
|
-
function isSlot(value) {
|
|
12019
|
-
if (typeof value !== "object" || value === null) return false;
|
|
12020
|
-
if (!("reload" in value) || !("servedLeafFingerprint" in value)) return false;
|
|
12021
|
-
const reload = value.reload;
|
|
12022
|
-
const served = value.servedLeafFingerprint;
|
|
12023
|
-
return (reload === null || typeof reload === "function") && (served === null || typeof served === "string");
|
|
12024
|
-
}
|
|
12025
|
-
function readSlot() {
|
|
12026
|
-
const current = Reflect.get(globalThis, SLOT_KEY);
|
|
12027
|
-
return isSlot(current) ? current : EMPTY_SLOT;
|
|
12028
|
-
}
|
|
12029
|
-
function writeSlot(next) {
|
|
12030
|
-
Reflect.set(globalThis, SLOT_KEY, next);
|
|
12031
|
-
}
|
|
12032
|
-
/** SHA-256 fingerprint of the first certificate (the leaf) in a PEM chain. */
|
|
12033
|
-
function leafFingerprintOf(chain) {
|
|
12034
|
-
return new node_crypto.X509Certificate(chain).fingerprint256;
|
|
12035
|
-
}
|
|
12036
|
-
/**
|
|
12037
|
-
* Called once by the server after its HTTPS listener is up, with the pair it
|
|
12038
|
-
* was started on. From here on a certificate action swaps the served
|
|
12039
|
-
* certificate in place (`setSecureContext`) — new handshakes get the new
|
|
12040
|
-
* leaf, open connections keep theirs, nothing restarts.
|
|
12041
|
-
*/
|
|
12042
|
-
function registerHttpsListener(server, pair) {
|
|
12043
|
-
writeSlot({
|
|
12044
|
-
reload: (next) => server.setSecureContext({
|
|
12045
|
-
cert: next.cert,
|
|
12046
|
-
key: next.key
|
|
12047
|
-
}),
|
|
12048
|
-
servedLeafFingerprint: leafFingerprintOf(pair.cert)
|
|
12049
|
-
});
|
|
12050
|
-
}
|
|
12051
|
-
/** Forget the listener — on shutdown, and between specs. */
|
|
12052
|
-
function clearHttpsListener() {
|
|
12053
|
-
writeSlot(EMPTY_SLOT);
|
|
12054
|
-
}
|
|
12055
|
-
/**
|
|
12056
|
-
* The leaf the HTTPS listener serves now, or `null` when this process has no
|
|
12057
|
-
* registered listener (TLS off, or a process that does not own one) — in
|
|
12058
|
-
* which case nobody can say, and `null` is the honest answer.
|
|
12059
|
-
*/
|
|
12060
|
-
function servedHttpsLeafFingerprint() {
|
|
12061
|
-
return readSlot().servedLeafFingerprint;
|
|
12062
|
-
}
|
|
12063
|
-
/** Serve `pair` from now on. Never throws: the outcome says what happened. */
|
|
12064
|
-
function reloadHttpsListener(pair) {
|
|
12065
|
-
const slot = readSlot();
|
|
12066
|
-
if (slot.reload === null) return { kind: "no-listener" };
|
|
12067
|
-
try {
|
|
12068
|
-
const servedLeafFingerprint = leafFingerprintOf(pair.cert);
|
|
12069
|
-
slot.reload(pair);
|
|
12070
|
-
writeSlot({
|
|
12071
|
-
reload: slot.reload,
|
|
12072
|
-
servedLeafFingerprint
|
|
12073
|
-
});
|
|
12074
|
-
return {
|
|
12075
|
-
kind: "reloaded",
|
|
12076
|
-
servedLeafFingerprint
|
|
12077
|
-
};
|
|
12078
|
-
} catch (err) {
|
|
12079
|
-
return {
|
|
12080
|
-
kind: "failed",
|
|
12081
|
-
error: err instanceof Error ? err.message : String(err)
|
|
12082
|
-
};
|
|
12083
|
-
}
|
|
12084
|
-
}
|
|
12085
|
-
//#endregion
|
|
12086
|
-
//#region src/tls/uploaded-cert.ts
|
|
12087
|
-
/**
|
|
12088
|
-
* Operator-supplied material must parse, match its key, and carry
|
|
12089
|
-
* `extendedKeyUsage = serverAuth` — the same leaf defects D227 refused
|
|
12090
|
-
* to keep generating.
|
|
12091
|
-
*/
|
|
12092
|
-
function validateUploadedTls(certPem, keyPem) {
|
|
12093
|
-
let leaf;
|
|
12094
|
-
try {
|
|
12095
|
-
leaf = new node_crypto.X509Certificate(certPem);
|
|
12096
|
-
} catch {
|
|
12097
|
-
return {
|
|
12098
|
-
ok: false,
|
|
12099
|
-
error: "Certificate PEM could not be parsed."
|
|
12100
|
-
};
|
|
12101
|
-
}
|
|
12102
|
-
try {
|
|
12103
|
-
if (!leaf.checkPrivateKey((0, node_crypto.createPrivateKey)(keyPem))) return {
|
|
12104
|
-
ok: false,
|
|
12105
|
-
error: "Private key does not match the certificate."
|
|
12106
|
-
};
|
|
12107
|
-
} catch {
|
|
12108
|
-
return {
|
|
12109
|
-
ok: false,
|
|
12110
|
-
error: "Private key PEM could not be parsed."
|
|
12111
|
-
};
|
|
12112
|
-
}
|
|
12113
|
-
const eku = leaf.keyUsage;
|
|
12114
|
-
if (eku === void 0 || !eku.includes("1.3.6.1.5.5.7.3.1")) return {
|
|
12115
|
-
ok: false,
|
|
12116
|
-
error: "Certificate is missing extendedKeyUsage = serverAuth."
|
|
12117
|
-
};
|
|
12118
|
-
return {
|
|
12119
|
-
ok: true,
|
|
12120
|
-
fingerprintSha256: leaf.fingerprint256,
|
|
12121
|
-
validTo: new Date(leaf.validTo).toISOString()
|
|
12122
|
-
};
|
|
12123
|
-
}
|
|
12124
|
-
//#endregion
|
|
12125
|
-
//#region src/tls/tls-actions.ts
|
|
12126
|
-
function withNewline(pem) {
|
|
12127
|
-
return pem.endsWith("\n") ? pem : `${pem}\n`;
|
|
12128
|
-
}
|
|
12129
|
-
/**
|
|
12130
|
-
* Store an operator's certificate. The leaf (or chain) and key replace the
|
|
12131
|
-
* served pair; a CA, when given, goes to its own file. The local CA
|
|
12132
|
-
* (`camstack-ca.crt` + key) is never touched — it did not sign the upload,
|
|
12133
|
-
* and it is what "Revert to generated" and every device that trusts it
|
|
12134
|
-
* still need.
|
|
12135
|
-
*/
|
|
12136
|
-
async function storeUploadedCertificate(dataDir, input) {
|
|
12137
|
-
const valid = validateUploadedTls(input.certPem, input.keyPem);
|
|
12138
|
-
if (!valid.ok) throw new Error(valid.error);
|
|
12139
|
-
const dir = (0, node_path.join)(dataDir, "tls");
|
|
12140
|
-
await (0, node_fs_promises.mkdir)(dir, { recursive: true });
|
|
12141
|
-
await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "camstack.crt"), withNewline(input.certPem));
|
|
12142
|
-
await (0, node_fs_promises.writeFile)((0, node_path.join)(dir, "camstack.key"), withNewline(input.keyPem), { mode: 384 });
|
|
12143
|
-
const caPath = (0, node_path.join)(dir, UPLOADED_CA_FILE);
|
|
12144
|
-
if (input.caPem !== void 0 && input.caPem.trim() !== "") await (0, node_fs_promises.writeFile)(caPath, withNewline(input.caPem));
|
|
12145
|
-
else (0, node_fs.rmSync)(caPath, { force: true });
|
|
12146
|
-
writeTlsMode(dataDir, "uploaded");
|
|
12147
|
-
}
|
|
12148
|
-
/** Issue a new generated leaf under the local CA. Refused while an upload is served. */
|
|
12149
|
-
async function regenerateCertificate(dataDir, options) {
|
|
12150
|
-
if (readTlsMode(dataDir) === "uploaded") throw new Error("Cannot regenerate an uploaded certificate — revert to generated first.");
|
|
12151
|
-
await reissueTlsLeaf(dataDir, options);
|
|
12152
|
-
}
|
|
12153
|
-
/** Drop the upload and serve the generated local-CA leaf again (D227). */
|
|
12154
|
-
async function revertToGeneratedCertificate(dataDir, options) {
|
|
12155
|
-
writeTlsMode(dataDir, "generated");
|
|
12156
|
-
(0, node_fs.rmSync)((0, node_path.join)(dataDir, "tls", UPLOADED_CA_FILE), { force: true });
|
|
12157
|
-
await ensureTlsCert(dataDir, options);
|
|
12158
|
-
}
|
|
12159
|
-
/**
|
|
12160
|
-
* Put the pair on disk on the HTTPS listener. Called after every action, so
|
|
12161
|
-
* "the certificate changed" and "clients see it" are the same moment.
|
|
12162
|
-
*/
|
|
12163
|
-
function applyCertificateToListener(dataDir) {
|
|
12164
|
-
const certPath = (0, node_path.join)(dataDir, "tls", "camstack.crt");
|
|
12165
|
-
const keyPath = (0, node_path.join)(dataDir, "tls", "camstack.key");
|
|
12166
|
-
if (!(0, node_fs.existsSync)(certPath) || !(0, node_fs.existsSync)(keyPath)) return {
|
|
12167
|
-
kind: "failed",
|
|
12168
|
-
error: `No certificate pair under ${(0, node_path.join)(dataDir, "tls")}`
|
|
12169
|
-
};
|
|
12170
|
-
return reloadHttpsListener(loadTlsCert(certPath, keyPath));
|
|
12171
|
-
}
|
|
12172
|
-
//#endregion
|
|
12173
|
-
Object.defineProperty(exports, "CA_COMMON_NAME", {
|
|
12174
|
-
enumerable: true,
|
|
12175
|
-
get: function() {
|
|
12176
|
-
return CA_COMMON_NAME;
|
|
12177
|
-
}
|
|
12178
|
-
});
|
|
12179
|
-
Object.defineProperty(exports, "CA_VALIDITY_DAYS", {
|
|
12180
|
-
enumerable: true,
|
|
12181
|
-
get: function() {
|
|
12182
|
-
return CA_VALIDITY_DAYS;
|
|
12183
|
-
}
|
|
12184
|
-
});
|
|
12185
|
-
Object.defineProperty(exports, "DEFAULT_LAN_HTTP_PORT", {
|
|
12186
|
-
enumerable: true,
|
|
12187
|
-
get: function() {
|
|
12188
|
-
return DEFAULT_LAN_HTTP_PORT;
|
|
12189
|
-
}
|
|
12190
|
-
});
|
|
12191
|
-
Object.defineProperty(exports, "LEAF_RENEWAL_WINDOW_DAYS", {
|
|
12192
|
-
enumerable: true,
|
|
12193
|
-
get: function() {
|
|
12194
|
-
return LEAF_RENEWAL_WINDOW_DAYS;
|
|
12195
|
-
}
|
|
12196
|
-
});
|
|
12197
|
-
Object.defineProperty(exports, "MAX_LEAF_VALIDITY_DAYS", {
|
|
12198
|
-
enumerable: true,
|
|
12199
|
-
get: function() {
|
|
12200
|
-
return MAX_LEAF_VALIDITY_DAYS;
|
|
12201
|
-
}
|
|
12202
|
-
});
|
|
12203
|
-
Object.defineProperty(exports, "SERVER_AUTH_OID", {
|
|
12204
|
-
enumerable: true,
|
|
12205
|
-
get: function() {
|
|
12206
|
-
return SERVER_AUTH_OID;
|
|
12207
|
-
}
|
|
12208
|
-
});
|
|
12209
|
-
Object.defineProperty(exports, "UPLOADED_CA_FILE", {
|
|
12210
|
-
enumerable: true,
|
|
12211
|
-
get: function() {
|
|
12212
|
-
return UPLOADED_CA_FILE;
|
|
12213
|
-
}
|
|
12214
|
-
});
|
|
12215
|
-
Object.defineProperty(exports, "allFamiliesListenHost", {
|
|
12216
|
-
enumerable: true,
|
|
12217
|
-
get: function() {
|
|
12218
|
-
return allFamiliesListenHost;
|
|
12219
|
-
}
|
|
12220
|
-
});
|
|
12221
|
-
Object.defineProperty(exports, "applyCertificateToListener", {
|
|
12222
|
-
enumerable: true,
|
|
12223
|
-
get: function() {
|
|
12224
|
-
return applyCertificateToListener;
|
|
12225
|
-
}
|
|
12226
|
-
});
|
|
12227
|
-
Object.defineProperty(exports, "applyLanHttp", {
|
|
12228
|
-
enumerable: true,
|
|
12229
|
-
get: function() {
|
|
12230
|
-
return applyLanHttp;
|
|
12231
|
-
}
|
|
12232
|
-
});
|
|
12233
|
-
Object.defineProperty(exports, "bindPendingLanHttp", {
|
|
12234
|
-
enumerable: true,
|
|
12235
|
-
get: function() {
|
|
12236
|
-
return bindPendingLanHttp;
|
|
12237
|
-
}
|
|
12238
|
-
});
|
|
12239
|
-
Object.defineProperty(exports, "clearHttpsListener", {
|
|
12240
|
-
enumerable: true,
|
|
12241
|
-
get: function() {
|
|
12242
|
-
return clearHttpsListener;
|
|
12243
|
-
}
|
|
12244
|
-
});
|
|
12245
|
-
Object.defineProperty(exports, "closeLanHttp", {
|
|
12246
|
-
enumerable: true,
|
|
12247
|
-
get: function() {
|
|
12248
|
-
return closeLanHttp;
|
|
12249
|
-
}
|
|
12250
|
-
});
|
|
12251
|
-
Object.defineProperty(exports, "collectCertIdentity", {
|
|
12252
|
-
enumerable: true,
|
|
12253
|
-
get: function() {
|
|
12254
|
-
return collectCertIdentity;
|
|
12255
|
-
}
|
|
12256
|
-
});
|
|
12257
|
-
Object.defineProperty(exports, "ensureTlsCert", {
|
|
12258
|
-
enumerable: true,
|
|
12259
|
-
get: function() {
|
|
12260
|
-
return ensureTlsCert;
|
|
12261
|
-
}
|
|
12262
|
-
});
|
|
12263
|
-
Object.defineProperty(exports, "evaluateExistingCert", {
|
|
12264
|
-
enumerable: true,
|
|
12265
|
-
get: function() {
|
|
12266
|
-
return evaluateExistingCert;
|
|
12267
|
-
}
|
|
12268
|
-
});
|
|
12269
|
-
Object.defineProperty(exports, "leafFingerprintOf", {
|
|
12270
|
-
enumerable: true,
|
|
12271
|
-
get: function() {
|
|
12272
|
-
return leafFingerprintOf;
|
|
12273
|
-
}
|
|
12274
|
-
});
|
|
12275
|
-
Object.defineProperty(exports, "loadTlsCert", {
|
|
12276
|
-
enumerable: true,
|
|
12277
|
-
get: function() {
|
|
12278
|
-
return loadTlsCert;
|
|
12279
|
-
}
|
|
12280
|
-
});
|
|
12281
|
-
Object.defineProperty(exports, "prepareBootTls", {
|
|
12282
|
-
enumerable: true,
|
|
12283
|
-
get: function() {
|
|
12284
|
-
return prepareBootTls;
|
|
12285
|
-
}
|
|
12286
|
-
});
|
|
12287
|
-
Object.defineProperty(exports, "readExtraSans", {
|
|
12288
|
-
enumerable: true,
|
|
12289
|
-
get: function() {
|
|
12290
|
-
return readExtraSans;
|
|
12291
|
-
}
|
|
12292
|
-
});
|
|
12293
|
-
Object.defineProperty(exports, "readLanHttpState", {
|
|
12294
|
-
enumerable: true,
|
|
12295
|
-
get: function() {
|
|
12296
|
-
return readLanHttpState;
|
|
12297
|
-
}
|
|
12298
|
-
});
|
|
12299
|
-
Object.defineProperty(exports, "readTlsAccessStatus", {
|
|
12300
|
-
enumerable: true,
|
|
12301
|
-
get: function() {
|
|
12302
|
-
return readTlsAccessStatus;
|
|
12303
|
-
}
|
|
12304
|
-
});
|
|
12305
|
-
Object.defineProperty(exports, "readTlsMode", {
|
|
11262
|
+
Object.defineProperty(exports, "AuthorityKeyIdentifierExtension", {
|
|
12306
11263
|
enumerable: true,
|
|
12307
11264
|
get: function() {
|
|
12308
|
-
return
|
|
11265
|
+
return AuthorityKeyIdentifierExtension;
|
|
12309
11266
|
}
|
|
12310
11267
|
});
|
|
12311
|
-
Object.defineProperty(exports, "
|
|
11268
|
+
Object.defineProperty(exports, "BasicConstraintsExtension", {
|
|
12312
11269
|
enumerable: true,
|
|
12313
11270
|
get: function() {
|
|
12314
|
-
return
|
|
11271
|
+
return BasicConstraintsExtension;
|
|
12315
11272
|
}
|
|
12316
11273
|
});
|
|
12317
|
-
Object.defineProperty(exports, "
|
|
11274
|
+
Object.defineProperty(exports, "ExtendedKeyUsageExtension", {
|
|
12318
11275
|
enumerable: true,
|
|
12319
11276
|
get: function() {
|
|
12320
|
-
return
|
|
11277
|
+
return ExtendedKeyUsageExtension;
|
|
12321
11278
|
}
|
|
12322
11279
|
});
|
|
12323
|
-
Object.defineProperty(exports, "
|
|
11280
|
+
Object.defineProperty(exports, "KeyUsageFlags", {
|
|
12324
11281
|
enumerable: true,
|
|
12325
11282
|
get: function() {
|
|
12326
|
-
return
|
|
11283
|
+
return KeyUsageFlags;
|
|
12327
11284
|
}
|
|
12328
11285
|
});
|
|
12329
|
-
Object.defineProperty(exports, "
|
|
11286
|
+
Object.defineProperty(exports, "KeyUsagesExtension", {
|
|
12330
11287
|
enumerable: true,
|
|
12331
11288
|
get: function() {
|
|
12332
|
-
return
|
|
11289
|
+
return KeyUsagesExtension;
|
|
12333
11290
|
}
|
|
12334
11291
|
});
|
|
12335
|
-
Object.defineProperty(exports, "
|
|
11292
|
+
Object.defineProperty(exports, "PemConverter", {
|
|
12336
11293
|
enumerable: true,
|
|
12337
11294
|
get: function() {
|
|
12338
|
-
return
|
|
11295
|
+
return PemConverter;
|
|
12339
11296
|
}
|
|
12340
11297
|
});
|
|
12341
|
-
Object.defineProperty(exports, "
|
|
11298
|
+
Object.defineProperty(exports, "Pkcs10CertificateRequest", {
|
|
12342
11299
|
enumerable: true,
|
|
12343
11300
|
get: function() {
|
|
12344
|
-
return
|
|
11301
|
+
return Pkcs10CertificateRequest;
|
|
12345
11302
|
}
|
|
12346
11303
|
});
|
|
12347
|
-
Object.defineProperty(exports, "
|
|
11304
|
+
Object.defineProperty(exports, "Pkcs10CertificateRequestGenerator", {
|
|
12348
11305
|
enumerable: true,
|
|
12349
11306
|
get: function() {
|
|
12350
|
-
return
|
|
11307
|
+
return Pkcs10CertificateRequestGenerator;
|
|
12351
11308
|
}
|
|
12352
11309
|
});
|
|
12353
|
-
Object.defineProperty(exports, "
|
|
11310
|
+
Object.defineProperty(exports, "SubjectAlternativeNameExtension", {
|
|
12354
11311
|
enumerable: true,
|
|
12355
11312
|
get: function() {
|
|
12356
|
-
return
|
|
11313
|
+
return SubjectAlternativeNameExtension;
|
|
12357
11314
|
}
|
|
12358
11315
|
});
|
|
12359
|
-
Object.defineProperty(exports, "
|
|
11316
|
+
Object.defineProperty(exports, "SubjectKeyIdentifierExtension", {
|
|
12360
11317
|
enumerable: true,
|
|
12361
11318
|
get: function() {
|
|
12362
|
-
return
|
|
11319
|
+
return SubjectKeyIdentifierExtension;
|
|
12363
11320
|
}
|
|
12364
11321
|
});
|
|
12365
|
-
Object.defineProperty(exports, "
|
|
11322
|
+
Object.defineProperty(exports, "X509Certificate", {
|
|
12366
11323
|
enumerable: true,
|
|
12367
11324
|
get: function() {
|
|
12368
|
-
return
|
|
11325
|
+
return X509Certificate;
|
|
12369
11326
|
}
|
|
12370
11327
|
});
|
|
12371
|
-
Object.defineProperty(exports, "
|
|
11328
|
+
Object.defineProperty(exports, "X509CertificateGenerator", {
|
|
12372
11329
|
enumerable: true,
|
|
12373
11330
|
get: function() {
|
|
12374
|
-
return
|
|
11331
|
+
return X509CertificateGenerator;
|
|
12375
11332
|
}
|
|
12376
11333
|
});
|
|
12377
|
-
Object.defineProperty(exports, "
|
|
11334
|
+
Object.defineProperty(exports, "cryptoProvider", {
|
|
12378
11335
|
enumerable: true,
|
|
12379
11336
|
get: function() {
|
|
12380
|
-
return
|
|
11337
|
+
return cryptoProvider;
|
|
12381
11338
|
}
|
|
12382
11339
|
});
|