feat(device-plane): harden edge channel rotation
This commit is contained in:
@@ -94,6 +94,66 @@ test("keeps the channel alive and reconnects without losing idempotency", async
|
||||
}
|
||||
});
|
||||
|
||||
test("rotates the Edge certificate through staged overlap and rejects retired identity", async () => {
|
||||
const edge = createEdgeServer();
|
||||
const address = await edge.start();
|
||||
let registration = edgeRegistration(address, [
|
||||
{
|
||||
generationRef: "trust-generation:1",
|
||||
fingerprint: certificates.edge.fingerprint,
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
generationRef: "trust-generation:2",
|
||||
fingerprint: certificates.edgeNext.fingerprint,
|
||||
status: "staged",
|
||||
},
|
||||
]);
|
||||
const core = createCoreClient({
|
||||
address,
|
||||
registrationProvider: async () => registration,
|
||||
});
|
||||
try {
|
||||
await core.start();
|
||||
await core.waitForReady(2_000);
|
||||
await waitFor(() => edge.status().channel === "accepted", 2_000);
|
||||
assert.equal(core.status().edgeTrustGeneration, "trust-generation:1");
|
||||
|
||||
edge.rotateTrust({
|
||||
generationRef: "trust-generation:2",
|
||||
key: certificates.edgeNext.key,
|
||||
cert: certificates.edgeNext.cert,
|
||||
ca: certificates.ca,
|
||||
allowedCoreFingerprints: [certificates.core.fingerprint],
|
||||
});
|
||||
await waitFor(() => core.status().connectionAttempts >= 2
|
||||
&& core.status().channel === "accepted"
|
||||
&& core.status().edgeTrustGeneration === "trust-generation:2"
|
||||
&& edge.status().channel === "accepted", 2_000);
|
||||
|
||||
registration = edgeRegistration(address, [{
|
||||
generationRef: "trust-generation:2",
|
||||
fingerprint: certificates.edgeNext.fingerprint,
|
||||
status: "active",
|
||||
}]);
|
||||
assert.equal((await edge.submitAdapterMessage(adapterMessage())).status, "accepted");
|
||||
|
||||
const failuresBeforeRollback = core.status().protocolFailures;
|
||||
edge.rotateTrust({
|
||||
generationRef: "trust-generation:3",
|
||||
key: certificates.edge.key,
|
||||
cert: certificates.edge.cert,
|
||||
ca: certificates.ca,
|
||||
allowedCoreFingerprints: [certificates.core.fingerprint],
|
||||
});
|
||||
await waitFor(() => core.status().protocolFailures > failuresBeforeRollback, 2_000);
|
||||
assert.notEqual(core.status().channel, "accepted");
|
||||
} finally {
|
||||
await core.stop();
|
||||
await edge.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects a revoked Edge registration before opening a channel", async () => {
|
||||
const edge = createEdgeServer();
|
||||
const address = await edge.start();
|
||||
@@ -167,6 +227,92 @@ test("returns a conclusive rejection when Core cannot accept a package", async (
|
||||
}
|
||||
});
|
||||
|
||||
test("isolates tracker session ordering while allowing cross-session progress", async () => {
|
||||
let releaseSlow;
|
||||
const slowGate = new Promise((resolve) => {
|
||||
releaseSlow = resolve;
|
||||
});
|
||||
const calls = [];
|
||||
const pair = await startPair({
|
||||
acceptMessage: async (message) => {
|
||||
calls.push(message.sessionRef);
|
||||
if (message.sessionRef === "session:slow") await slowGate;
|
||||
return acceptanceFor(message, false);
|
||||
},
|
||||
});
|
||||
try {
|
||||
let slowResolved = false;
|
||||
const slow = pair.edge.submitAdapterMessage(adapterMessage({
|
||||
sessionRef: "session:slow",
|
||||
messageRef: "message:slow-1",
|
||||
idempotencyKey: `sha256:${"c".repeat(64)}`,
|
||||
})).then((value) => {
|
||||
slowResolved = true;
|
||||
return value;
|
||||
});
|
||||
await waitFor(() => calls.includes("session:slow"), 500);
|
||||
const fast = await pair.edge.submitAdapterMessage(adapterMessage({
|
||||
sessionRef: "session:fast",
|
||||
messageRef: "message:fast-1",
|
||||
idempotencyKey: `sha256:${"d".repeat(64)}`,
|
||||
}));
|
||||
assert.equal(fast.status, "accepted");
|
||||
assert.equal(slowResolved, false);
|
||||
releaseSlow();
|
||||
assert.equal((await slow).status, "accepted");
|
||||
} finally {
|
||||
releaseSlow?.();
|
||||
await stopPair(pair);
|
||||
}
|
||||
});
|
||||
|
||||
test("preserves per-session order and applies a bounded acceptance window", async () => {
|
||||
let releaseFirst;
|
||||
const firstGate = new Promise((resolve) => {
|
||||
releaseFirst = resolve;
|
||||
});
|
||||
const calls = [];
|
||||
const pair = await startPair({
|
||||
maxPendingAcceptances: 2,
|
||||
acceptMessage: async (message) => {
|
||||
calls.push(message.messageRef);
|
||||
if (message.messageRef === "message:ordered-1") await firstGate;
|
||||
return acceptanceFor(message, false);
|
||||
},
|
||||
});
|
||||
try {
|
||||
const first = pair.edge.submitAdapterMessage(adapterMessage({
|
||||
sessionRef: "session:ordered",
|
||||
messageRef: "message:ordered-1",
|
||||
idempotencyKey: `sha256:${"e".repeat(64)}`,
|
||||
sequence: 1,
|
||||
}));
|
||||
await waitFor(() => calls.length === 1, 500);
|
||||
const second = pair.edge.submitAdapterMessage(adapterMessage({
|
||||
sessionRef: "session:ordered",
|
||||
messageRef: "message:ordered-2",
|
||||
idempotencyKey: `sha256:${"f".repeat(64)}`,
|
||||
sequence: 2,
|
||||
}));
|
||||
await delay(30);
|
||||
assert.deepEqual(calls, ["message:ordered-1"]);
|
||||
await assert.rejects(
|
||||
pair.edge.submitAdapterMessage(adapterMessage({
|
||||
sessionRef: "session:overflow",
|
||||
messageRef: "message:overflow-1",
|
||||
idempotencyKey: `sha256:${"1".repeat(64)}`,
|
||||
})),
|
||||
/acceptance_window_full/,
|
||||
);
|
||||
releaseFirst();
|
||||
await Promise.all([first, second]);
|
||||
assert.deepEqual(calls, ["message:ordered-1", "message:ordered-2"]);
|
||||
} finally {
|
||||
releaseFirst?.();
|
||||
await stopPair(pair);
|
||||
}
|
||||
});
|
||||
|
||||
test("closes the logical session on an unknown message kind", async () => {
|
||||
const edge = createEdgeServer();
|
||||
const address = await edge.start();
|
||||
@@ -221,34 +367,41 @@ async function startPair(options = {}) {
|
||||
}
|
||||
|
||||
function createEdgeServer(options = {}) {
|
||||
const edgeCertificate = options.edgeCertificate ?? certificates.edge;
|
||||
return createDeviceEdgeChannelServer({
|
||||
edgeRegistrationId: "edge:pilot-1",
|
||||
channelGeneration: "generation:pilot-1",
|
||||
trustGeneration: options.edgeTrustGeneration ?? "trust-generation:1",
|
||||
host: "127.0.0.1",
|
||||
port: 0,
|
||||
tls: {
|
||||
key: certificates.edge.key,
|
||||
cert: certificates.edge.cert,
|
||||
key: edgeCertificate.key,
|
||||
cert: edgeCertificate.cert,
|
||||
ca: certificates.ca,
|
||||
allowedCoreFingerprints: [certificates.core.fingerprint],
|
||||
allowedCoreFingerprints: options.allowedCoreFingerprints
|
||||
?? [certificates.core.fingerprint],
|
||||
},
|
||||
keepaliveMs: options.keepaliveMs ?? 50,
|
||||
deadPeerMs: options.deadPeerMs ?? 150,
|
||||
acceptanceTimeoutMs: 500,
|
||||
maxPendingAcceptances: options.maxPendingAcceptances,
|
||||
});
|
||||
}
|
||||
|
||||
function createCoreClient(options) {
|
||||
const clientCertificate = options.clientCertificate ?? certificates.core;
|
||||
return createDeviceGatewayCoreChannelClient({
|
||||
registration: {
|
||||
edgeRegistrationId: "edge:pilot-1",
|
||||
endpoint: `https://127.0.0.1:${options.address.port}/`,
|
||||
servername: "localhost",
|
||||
certificateFingerprint: options.expectedEdgeFingerprint
|
||||
const registration = edgeRegistration(options.address,
|
||||
options.edgeCertificateIdentities ?? [{
|
||||
generationRef: "trust-generation:1",
|
||||
fingerprint: options.expectedEdgeFingerprint
|
||||
?? certificates.edge.fingerprint,
|
||||
lifecycleState: options.lifecycleState ?? "active",
|
||||
},
|
||||
status: "active",
|
||||
}],
|
||||
options.lifecycleState ?? "active");
|
||||
return createDeviceGatewayCoreChannelClient({
|
||||
...(options.registrationProvider
|
||||
? { registrationProvider: options.registrationProvider }
|
||||
: { registration }),
|
||||
tls: {
|
||||
key: clientCertificate.key,
|
||||
cert: clientCertificate.cert,
|
||||
@@ -271,6 +424,16 @@ function createCoreClient(options) {
|
||||
});
|
||||
}
|
||||
|
||||
function edgeRegistration(address, certificateIdentities, lifecycleState = "active") {
|
||||
return {
|
||||
edgeRegistrationId: "edge:pilot-1",
|
||||
endpoint: `https://127.0.0.1:${address.port}/`,
|
||||
servername: "localhost",
|
||||
certificateIdentities,
|
||||
lifecycleState,
|
||||
};
|
||||
}
|
||||
|
||||
async function stopPair(pair) {
|
||||
await pair.core.stop();
|
||||
await pair.edge.stop();
|
||||
@@ -293,7 +456,7 @@ function discoverySignal() {
|
||||
};
|
||||
}
|
||||
|
||||
function adapterMessage() {
|
||||
function adapterMessage(overrides = {}) {
|
||||
return {
|
||||
schemaVersion: DEVICE_ADAPTER_MESSAGE_SCHEMA,
|
||||
edgeRef: "edge:pilot-1",
|
||||
@@ -314,6 +477,7 @@ function adapterMessage() {
|
||||
byteLength: 11,
|
||||
packageDigest: `sha256:${"b".repeat(64)}`,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -340,6 +504,10 @@ async function generateCertificateFixture(directory) {
|
||||
"subjectAltName=DNS:localhost,IP:127.0.0.1",
|
||||
"extendedKeyUsage=serverAuth",
|
||||
]);
|
||||
await issueCertificate(directory, "edge-next", "localhost", [
|
||||
"subjectAltName=DNS:localhost,IP:127.0.0.1",
|
||||
"extendedKeyUsage=serverAuth",
|
||||
]);
|
||||
await issueCertificate(directory, "core", "nodedc-device-gateway-core", [
|
||||
"extendedKeyUsage=clientAuth",
|
||||
]);
|
||||
@@ -350,6 +518,7 @@ async function generateCertificateFixture(directory) {
|
||||
return {
|
||||
ca,
|
||||
edge: await readCertificate(directory, "edge"),
|
||||
edgeNext: await readCertificate(directory, "edge-next"),
|
||||
core: await readCertificate(directory, "core"),
|
||||
intruder: await readCertificate(directory, "intruder"),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user