feat(device-plane): harden edge channel rotation

This commit is contained in:
Codex
2026-08-11 19:37:32 +03:00
parent 50b9179fe4
commit 1124c15216
7 changed files with 365 additions and 34 deletions
@@ -1,4 +1,4 @@
import { randomUUID } from "node:crypto";
import { randomUUID, X509Certificate } from "node:crypto";
import { createSecureServer } from "node:http2";
import {
@@ -20,6 +20,7 @@ const CHANNEL_PROFILE_REF = "channel.control.v1";
export function createDeviceEdgeChannelServer(options = {}) {
const config = normalizeConfig(options);
let trust = config.trust;
const pending = new Map();
let active = null;
let started = false;
@@ -32,9 +33,9 @@ export function createDeviceEdgeChannelServer(options = {}) {
let totalProtocolFailures = 0;
const server = createSecureServer({
key: config.tls.key,
cert: config.tls.cert,
ca: config.tls.ca,
key: trust.key,
cert: trust.cert,
ca: trust.ca,
minVersion: "TLSv1.3",
maxVersion: "TLSv1.3",
allowHTTP1: false,
@@ -88,6 +89,7 @@ export function createDeviceEdgeChannelServer(options = {}) {
send(state, "channel.hello", {
status: "ready",
transport: "http2-mtls",
trustGeneration: trust.generationRef,
commandTransport: "disabled",
}, {
trackerSessionId: CHANNEL_TRACKER_SESSION_ID,
@@ -179,6 +181,8 @@ export function createDeviceEdgeChannelServer(options = {}) {
channel: active?.accepted ? "accepted" : active ? "negotiating" : "absent",
edgeRegistrationId: config.edgeRegistrationId,
channelGeneration: config.channelGeneration,
trustGeneration: trust.generationRef,
edgeCertificateFingerprint: trust.certificateFingerprint,
pendingAcceptances: pending.size,
channelsAccepted: totalChannelsAccepted,
channelsRejected: totalChannelsRejected,
@@ -190,6 +194,30 @@ export function createDeviceEdgeChannelServer(options = {}) {
commandTransport: "disabled",
});
},
rotateTrust(next) {
const nextTrust = normalizeTrust(next);
if (nextTrust.generationRef === trust.generationRef) {
throw new TypeError("device_edge_channel_trust_generation_unchanged");
}
server.setSecureContext({
key: nextTrust.key,
cert: nextTrust.cert,
ca: nextTrust.ca,
minVersion: "TLSv1.3",
maxVersion: "TLSv1.3",
});
trust = nextTrust;
const current = active;
if (current) {
current.stream.close();
closeActive(current);
}
return Object.freeze({
trustGeneration: trust.generationRef,
edgeCertificateFingerprint: trust.certificateFingerprint,
channel: "reconnect-required",
});
},
disconnectActiveChannel() {
active?.stream.close();
},
@@ -328,7 +356,7 @@ export function createDeviceEdgeChannelServer(options = {}) {
} catch {
return false;
}
return config.tls.allowedCoreFingerprints.has(fingerprint);
return trust.allowedCoreFingerprints.has(fingerprint);
}
function protocolFailure(state) {
@@ -368,7 +396,10 @@ function normalizeConfig(options) {
options.channelGeneration,
"channel_generation",
);
const tls = normalizeTls(options.tls);
const trust = normalizeTrust({
...options.tls,
generationRef: options.trustGeneration,
});
const keepaliveMs = normalizeDuration(options.keepaliveMs, 10,
DEVICE_EDGE_CHANNEL_LIMITS.keepaliveMs, "keepalive");
const deadPeerMs = normalizeDuration(options.deadPeerMs, keepaliveMs * 2,
@@ -379,7 +410,7 @@ function normalizeConfig(options) {
return Object.freeze({
edgeRegistrationId,
channelGeneration,
tls,
trust,
host: normalizeHost(options.host ?? "127.0.0.1"),
port: normalizePort(options.port ?? 8443),
keepaliveMs,
@@ -411,7 +442,7 @@ function normalizeConfig(options) {
});
}
function normalizeTls(value) {
function normalizeTrust(value) {
if (!value || typeof value !== "object") {
throw new TypeError("device_edge_channel_tls_invalid");
}
@@ -420,16 +451,38 @@ function normalizeTls(value) {
throw new TypeError(`device_edge_channel_tls_${key}_invalid`);
}
}
if (!Array.isArray(value.allowedCoreFingerprints) || value.allowedCoreFingerprints.length < 1) {
if (
!Array.isArray(value.allowedCoreFingerprints)
|| value.allowedCoreFingerprints.length < 1
|| value.allowedCoreFingerprints.length > 2
) {
throw new TypeError("device_edge_channel_core_identity_allowlist_invalid");
}
const generationRef = normalizeRef(
value.generationRef,
"trust_generation",
);
let certificateFingerprint;
try {
certificateFingerprint = normalizeCertificateFingerprint(
new X509Certificate(value.cert).fingerprint256,
);
} catch {
throw new TypeError("device_edge_channel_tls_cert_invalid");
}
const allowedCoreFingerprints = new Set(
value.allowedCoreFingerprints.map(normalizeCertificateFingerprint),
);
if (allowedCoreFingerprints.size !== value.allowedCoreFingerprints.length) {
throw new TypeError("device_edge_channel_core_identity_allowlist_duplicate");
}
return Object.freeze({
generationRef,
key: value.key,
cert: value.cert,
ca: value.ca,
allowedCoreFingerprints: new Set(
value.allowedCoreFingerprints.map(normalizeCertificateFingerprint),
),
certificateFingerprint,
allowedCoreFingerprints,
});
}