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
@@ -216,6 +216,45 @@ export function normalizeCertificateFingerprint(value) {
return compact.match(/.{2}/g).join(":");
}
export function normalizeCertificateIdentities(value) {
if (!Array.isArray(value) || value.length < 1 || value.length > 2) {
throw new TypeError("device_edge_channel_certificate_identities_invalid");
}
const generations = new Set();
const fingerprints = new Set();
let activeCount = 0;
const identities = value.map((identity) => {
assertPlainObject(identity, "device_edge_channel_certificate_identity");
rejectUnexpectedKeys(
identity,
new Set(["generationRef", "fingerprint", "status"]),
);
if (!["active", "staged"].includes(identity.status)) {
throw new TypeError("device_edge_channel_certificate_identity_status_invalid");
}
if (identity.status === "active") activeCount += 1;
const generationRef = normalizeRef(
identity.generationRef,
"certificate_generation_ref",
);
const fingerprint = normalizeCertificateFingerprint(identity.fingerprint);
if (generations.has(generationRef) || fingerprints.has(fingerprint)) {
throw new TypeError("device_edge_channel_certificate_identity_duplicate");
}
generations.add(generationRef);
fingerprints.add(fingerprint);
return Object.freeze({
generationRef,
fingerprint,
status: identity.status,
});
});
if (activeCount !== 1) {
throw new TypeError("device_edge_channel_active_certificate_identity_invalid");
}
return Object.freeze(identities);
}
export function nextReconnectDelay(attempt, options = {}) {
const normalizedAttempt = Number(attempt);
if (!Number.isSafeInteger(normalizedAttempt) || normalizedAttempt < 0) {
@@ -6,6 +6,7 @@ import {
createChannelEnvelopeDecoder,
encodeChannelEnvelope,
nextReconnectDelay,
normalizeCertificateIdentities,
normalizeChannelEnvelope,
} from "../src/index.mjs";
@@ -75,3 +76,31 @@ test("uses bounded jittered exponential reconnect delays", () => {
random: () => 0,
}), 15_000);
});
test("allows exactly one active and at most one staged certificate generation", () => {
const activeFingerprint = "AA:".repeat(31) + "AA";
const stagedFingerprint = "BB:".repeat(31) + "BB";
const identities = normalizeCertificateIdentities([
{
generationRef: "trust-generation:1",
fingerprint: activeFingerprint,
status: "active",
},
{
generationRef: "trust-generation:2",
fingerprint: stagedFingerprint,
status: "staged",
},
]);
assert.equal(identities.length, 2);
assert.equal(identities[0].status, "active");
assert.equal(identities[1].status, "staged");
assert.throws(() => normalizeCertificateIdentities([
{ ...identities[0], status: "staged" },
identities[1],
]), /active_certificate_identity_invalid/);
assert.throws(() => normalizeCertificateIdentities([
identities[0],
{ ...identities[1], status: "active" },
]), /active_certificate_identity_invalid/);
});
@@ -74,6 +74,11 @@ test("records source acceptance without opening an Edge or tracker port", async
assert.equal(acceptance.transport.tls, "TLSv1.3-mutual-authentication");
assert.equal(acceptance.identity.privateKeysInSource, false);
assert.equal(acceptance.identity.privateKeysInArtifact, false);
assert.equal(
acceptance.identity.rotation,
"one-active-plus-one-staged-generation",
);
assert.equal(acceptance.identity.retiredFingerprint, "reject");
assert.equal(acceptance.runtime.mutationInThisTransition, false);
assert.equal(acceptance.runtime.edgePort8443Published, false);
assert.equal(acceptance.runtime.trackerPort9921Published, false);