feat(device-core): add restricted identity references

This commit is contained in:
Codex
2026-08-10 18:31:30 +03:00
parent fceaca9546
commit 422ddb020f
20 changed files with 1413 additions and 7 deletions
@@ -116,6 +116,84 @@ test("management API forwards claim as evidence references without identity inpu
}
});
test("management API accepts only a canonical credential reference", async () => {
let executed;
const runtime = await startServer({
managementApiEnabled: true,
managementToken,
repository: {
health: async () => "ready",
executeManagementCommand: async (input) => {
executed = input;
return {
replayed: false,
result: {
credentialBinding: {
credentialBindingRef:
"credential-binding:44444444-4444-4444-8444-444444444444",
},
},
};
},
},
});
try {
const response = await fetch(
`${runtime.baseUrl}/internal/v1/management/device-credential-bindings:upsert`,
{
method: "POST",
headers: managementHeaders(),
body: JSON.stringify({
projectRef: "project:11111111-1111-4111-8111-111111111111",
deviceRef: "device:22222222-2222-4222-8222-222222222222",
purpose: "tracker.command",
credentialRef: {
owner: "ndc_l2_credentials",
reference: "ndc-credref:pilot-command-0001",
},
}),
},
);
assert.equal(response.status, 200);
assert.equal(
executed.commandKind,
"device_credential_binding.upsert",
);
assert.deepEqual(executed.command.credentialRef, {
owner: "ndc_l2_credentials",
reference: "ndc-credref:pilot-command-0001",
});
const rejected = await fetch(
`${runtime.baseUrl}/internal/v1/management/device-credential-bindings:upsert`,
{
method: "POST",
headers: {
...managementHeaders(),
"Idempotency-Key": "phase24-credential-invalid-0001",
},
body: JSON.stringify({
projectRef: "project:11111111-1111-4111-8111-111111111111",
deviceRef: "device:22222222-2222-4222-8222-222222222222",
purpose: "tracker.command",
credentialRef: {
owner: "device_core",
reference: "ndc-credref:pilot-command-0001",
},
}),
},
);
assert.equal(rejected.status, 400);
assert.equal(
(await rejected.json()).error,
"ndc_credential_reference_owner_invalid",
);
} finally {
await runtime.close();
}
});
async function startServer(options) {
const server = createControlCoreApp(options);
await new Promise((resolve, reject) => {
@@ -45,6 +45,7 @@ test("claims only matching observed enrollment evidence into direct ownership",
display_name: command.displayName,
})],
}),
step("insert into device_restricted_identifiers"),
step("update device_discoveries", { rows: [{ id: discoveryId }] }),
step("update device_enrollment_intents", { rows: [{ id: enrollmentId }] }),
step("insert into device_ownership_transitions"),
@@ -109,6 +110,7 @@ test("authorized transfer preserves history and detaches source collections", as
projectStep(targetProjectId, targetOwnerId),
grantsStep(actor, "owner"),
step("from device_sessions", { rows: [{ active: false }] }),
step("from device_credential_bindings", { rows: [{ active: false }] }),
step("delete from device_collection_members", { rows: [], rowCount: 2 }),
step("update device_instances", {
rows: [deviceRow({
@@ -117,6 +119,7 @@ test("authorized transfer preserves history and detaches source collections", as
device_key: command.targetDeviceKey,
})],
}),
step("update device_restricted_identifiers", { rows: [], rowCount: 1 }),
step("insert into device_ownership_transitions"),
step("insert into device_audit_events"),
step("insert into device_audit_events"),
@@ -135,6 +138,37 @@ test("authorized transfer preserves history and detaches source collections", as
assert.equal(result.result.transferred, true);
assert.equal(result.result.device.projectRef, `project:${targetProjectId}`);
assert.equal(result.result.detachedCollectionCount, 2);
assert.equal(result.result.transferredIdentifierCount, 1);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("transfer fails closed while a credential binding is active", async () => {
const actor = managementActor("owner");
const command = transferCommand();
const client = scriptedClient([
step("begin"),
receiptStep("receipt-transfer-credential-bound"),
step("from device_instances", { rows: [deviceRow()] }),
projectStep(sourceProjectId, sourceOwnerId),
grantsStep(actor, "owner"),
projectStep(targetProjectId, targetOwnerId),
grantsStep(actor, "owner"),
step("from device_sessions", { rows: [{ active: false }] }),
step("from device_credential_bindings", { rows: [{ active: true }] }),
step("rollback"),
]);
const repository = repositoryWithClient(client);
await assert.rejects(
repository.executeManagementCommand(commandInput({
actor,
commandKind: "device.transfer",
command,
digestCharacter: "f",
})),
/device_transfer_active_credential_binding/,
);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
@@ -0,0 +1,105 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
DEVICE_SENSITIVE_REFERENCE_COMMAND_KINDS,
normalizeSensitiveReferenceManagementCommand,
} from "../src/sensitive-reference-management.mjs";
import {
ALL_DEVICE_MANAGEMENT_COMMAND_KINDS,
normalizeDeviceManagementCommand,
} from "../src/management-command.mjs";
const projectRef = "project:11111111-1111-4111-8111-111111111111";
const deviceRef = "device:22222222-2222-4222-8222-222222222222";
test("credential binding commands share the strict management surface", () => {
for (const kind of DEVICE_SENSITIVE_REFERENCE_COMMAND_KINDS) {
assert.equal(ALL_DEVICE_MANAGEMENT_COMMAND_KINDS.includes(kind), true);
}
assert.equal(
normalizeDeviceManagementCommand(
"device_credential_binding.upsert",
upsertInput(),
).projectId,
projectRef.slice("project:".length),
);
});
test("credential binding accepts only the platform canonical opaque ref", () => {
const command = normalizeSensitiveReferenceManagementCommand(
"device_credential_binding.upsert",
upsertInput(),
);
assert.deepEqual(command.credentialRef, {
owner: "ndc_l2_credentials",
reference: "ndc-credref:pilot-command-0001",
});
assert.equal(Object.isFrozen(command.credentialRef), true);
assert.throws(
() => normalizeSensitiveReferenceManagementCommand(
"device_credential_binding.upsert",
{
...upsertInput(),
credentialRef: {
owner: "device_core",
reference: "ndc-credref:pilot-command-0001",
},
},
),
/ndc_credential_reference_owner_invalid/,
);
assert.throws(
() => normalizeSensitiveReferenceManagementCommand(
"device_credential_binding.upsert",
{
...upsertInput(),
credentialRef: {
owner: "ndc_l2_credentials",
reference: "Bearer plaintext-is-forbidden",
},
},
),
/ndc_credential_reference_value_invalid/,
);
});
test("credential binding rejects raw secret-shaped fields", () => {
for (const field of ["password", "token", "secretValue", "endpoint"]) {
assert.throws(
() => normalizeSensitiveReferenceManagementCommand(
"device_credential_binding.upsert",
{ ...upsertInput(), [field]: "forbidden" },
),
new RegExp(`device_management_command_field_unexpected:${field}`),
);
}
});
test("credential revoke has no credential reference input", () => {
const command = normalizeSensitiveReferenceManagementCommand(
"device_credential_binding.revoke",
{
projectRef,
deviceRef,
purpose: "tracker.command",
resolutionCode: "operator.rotation",
},
);
assert.equal(command.resolutionCode, "operator.rotation");
assert.equal("credentialRef" in command, false);
});
function upsertInput() {
return {
projectRef,
deviceRef,
purpose: "tracker.command",
credentialRef: {
owner: "ndc_l2_credentials",
reference: "ndc-credref:pilot-command-0001",
},
};
}
@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
const schemaUrl = new URL(
"../migrations/008_device_sensitive_references.sql",
import.meta.url,
);
const commandsUrl = new URL(
"../migrations/009_device_sensitive_reference_commands.sql",
import.meta.url,
);
const repositoryUrl = new URL("../src/postgres-repository.mjs", import.meta.url);
test("sensitive reference schema stores only digest, mask and canonical refs", async () => {
const sql = await readFile(schemaUrl, "utf8");
assert.match(sql, /create table if not exists device_restricted_identifiers/);
assert.match(sql, /identifier_digest text not null/);
assert.match(sql, /identifier_masked text not null/);
assert.match(sql, /device_restricted_identifiers_active_identity_idx/);
assert.match(sql, /device_restricted_identifiers_primary_idx/);
assert.match(sql, /device_identifier_ownership_mismatch/);
assert.match(sql, /device_active_identifier_ownership_mismatch/);
assert.match(sql, /deferrable initially deferred/);
assert.match(sql, /create table if not exists device_credential_bindings/);
assert.match(sql, /credential_owner = 'ndc_l2_credentials'/);
assert.match(sql, /\^ndc-credref:/);
assert.match(sql, /device_credential_binding_ownership_mismatch/);
assert.match(sql, /device_transfer_active_credential_binding/);
assert.match(sql, /owner_scope_id is null or credential_ref is null/);
assert.doesNotMatch(sql, /imei\s+text|serial\s+text|password\s+text|token\s+text/i);
assert.doesNotMatch(sql, /insert\s+into/i);
});
test("credential commands extend durable receipts after their schema", async () => {
const commands = await readFile(commandsUrl, "utf8");
const repository = await readFile(repositoryUrl, "utf8");
assert.match(commands, /'device_credential_binding\.upsert'/);
assert.match(commands, /'device_credential_binding\.revoke'/);
const schemaIndex = repository.indexOf("008_device_sensitive_references.sql");
const commandsIndex = repository.indexOf(
"009_device_sensitive_reference_commands.sql",
);
assert.notEqual(schemaIndex, -1);
assert.notEqual(commandsIndex, -1);
assert.ok(schemaIndex < commandsIndex);
});
@@ -0,0 +1,271 @@
import assert from "node:assert/strict";
import test from "node:test";
import { normalizeDeviceManagementCommand } from "../src/management-command.mjs";
import { PostgresDeviceRepository } from "../src/postgres-repository.mjs";
import { normalizeManagementActor } from "../src/project-management.mjs";
const now = new Date("2026-08-10T00:00:00.000Z");
const projectId = "11111111-1111-4111-8111-111111111111";
const ownerId = "22222222-2222-4222-8222-222222222222";
const deviceId = "33333333-3333-4333-8333-333333333333";
const bindingId = "44444444-4444-4444-8444-444444444444";
const canonicalRef = "ndc-credref:pilot-command-0001";
test("creates a canonical binding without returning or auditing its reference", async () => {
const actor = managementActor();
const command = upsertCommand();
const client = scriptedClient([
step("begin"),
receiptStep("receipt-credential-upsert"),
projectStep(),
grantsStep(actor),
step("from device_instances", { rows: [deviceRow()] }),
step("from device_credential_bindings", { rows: [] }),
step("insert into device_credential_bindings", {
rows: [bindingRow()],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device_credential_binding.upsert",
command,
digestCharacter: "a",
}));
assert.equal(result.result.created, true);
assert.equal(result.result.rotated, false);
assert.equal(
result.result.credentialBinding.credentialBindingRef,
`credential-binding:${bindingId}`,
);
assert.equal(JSON.stringify(result.result).includes(canonicalRef), false);
const auditCall = client.calls.find((call) =>
String(call.sql).includes("insert into device_audit_events")
);
assert.ok(auditCall);
assert.equal(JSON.stringify(auditCall.params).includes(canonicalRef), false);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("revokes by device and purpose without accepting a credential ref", async () => {
const actor = managementActor();
const command = normalizeDeviceManagementCommand(
"device_credential_binding.revoke",
{
projectRef: `project:${projectId}`,
deviceRef: `device:${deviceId}`,
purpose: "tracker.command",
resolutionCode: "operator.rotation",
},
);
const client = scriptedClient([
step("begin"),
receiptStep("receipt-credential-revoke"),
projectStep(),
grantsStep(actor),
step("from device_instances", { rows: [deviceRow()] }),
step("update device_credential_bindings", {
rows: [bindingRow({ lifecycle_state: "revoked" })],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device_credential_binding.revoke",
command,
digestCharacter: "b",
}));
assert.equal(result.result.revoked, true);
assert.equal(result.result.credentialBinding.lifecycleState, "revoked");
assert.equal("credentialRef" in command, false);
assert.equal(JSON.stringify(result.result).includes(canonicalRef), false);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
test("rotates an active binding atomically and keeps both refs out of audit", async () => {
const actor = managementActor();
const command = upsertCommand();
const oldRef = "ndc-credref:pilot-command-old-0001";
const client = scriptedClient([
step("begin"),
receiptStep("receipt-credential-rotate"),
projectStep(),
grantsStep(actor),
step("from device_instances", { rows: [deviceRow()] }),
step("from device_credential_bindings", {
rows: [bindingRow({ credential_ref: oldRef })],
}),
step("update device_credential_bindings"),
step("insert into device_credential_bindings", {
rows: [bindingRow({
id: "66666666-6666-4666-8666-666666666666",
})],
}),
step("insert into device_audit_events"),
step("update device_management_command_receipts"),
step("commit"),
]);
const repository = repositoryWithClient(client);
const result = await repository.executeManagementCommand(commandInput({
actor,
commandKind: "device_credential_binding.upsert",
command,
digestCharacter: "c",
}));
assert.equal(result.result.created, true);
assert.equal(result.result.rotated, true);
const auditCall = client.calls.find((call) =>
String(call.sql).includes("insert into device_audit_events")
);
assert.ok(auditCall);
assert.equal(JSON.stringify(auditCall.params).includes(oldRef), false);
assert.equal(JSON.stringify(auditCall.params).includes(canonicalRef), false);
assert.equal(client.remaining(), 0);
assert.equal(client.released, true);
});
function upsertCommand() {
return normalizeDeviceManagementCommand(
"device_credential_binding.upsert",
{
projectRef: `project:${projectId}`,
deviceRef: `device:${deviceId}`,
purpose: "tracker.command",
credentialRef: {
owner: "ndc_l2_credentials",
reference: canonicalRef,
},
},
);
}
function managementActor() {
return normalizeManagementActor({
userRef: "user:credential-operator",
hubRole: "admin",
groupRefs: [],
ownerScopes: [],
});
}
function projectStep() {
return step("from device_projects p", {
rows: [{
id: projectId,
owner_scope_id: ownerId,
lifecycle_state: "active",
scope_kind: "company",
owner_ref: "client:example-company",
owner_display_name: "Example Company",
owner_lifecycle_state: "active",
}],
});
}
function grantsStep(actor) {
return step("from device_project_grants", {
rows: [{
id: "55555555-5555-4555-8555-555555555555",
principal_kind: "user",
principal_ref: actor.userRef,
project_role: "admin",
capability_allow: [],
capability_deny: [],
lifecycle_state: "active",
}],
});
}
function deviceRow() {
return {
id: deviceId,
owner_scope_id: ownerId,
project_id: projectId,
lifecycle_state: "claimed",
};
}
function bindingRow(overrides = {}) {
return {
id: bindingId,
device_id: deviceId,
owner_scope_id: ownerId,
project_id: projectId,
purpose: "tracker.command",
credential_owner: "ndc_l2_credentials",
lifecycle_state: "active",
created_at: now,
updated_at: now,
...overrides,
};
}
function receiptStep(id) {
return step("insert into device_management_command_receipts", {
rows: [{ id }],
});
}
function commandInput({ actor, commandKind, command, digestCharacter }) {
return {
idempotencyKey: `phase24-${commandKind.replaceAll(".", "-")}-0001`,
commandKind,
requestDigest: `sha256:${digestCharacter.repeat(64)}`,
actor,
command,
};
}
function repositoryWithClient(client) {
return new PostgresDeviceRepository({
pool: {
query: async () => ({ rows: [] }),
connect: async () => client,
end: async () => undefined,
},
});
}
function step(includes, result = { rows: [] }) {
return { includes, result };
}
function scriptedClient(steps) {
const queue = [...steps];
return {
calls: [],
released: false,
async query(sql, params = []) {
this.calls.push({ sql, params });
const next = queue.shift();
assert.ok(next, `Unexpected query: ${sql}`);
assert.match(String(sql), new RegExp(escapeRegExp(next.includes), "i"));
return next.result;
},
release() {
this.released = true;
},
remaining() {
return queue.length;
},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}