Files

446 lines
13 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const test = require('node:test');
const {
BUILTIN_RUN_READ_TOOL,
BUILTIN_RUN_READ_TOOL_DEFINITION,
} = require('../dist/tool-execution/builtin-run-read/builtInRunReadTool');
const {
TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
} = require('../dist/tool-execution/trustedToolExecution');
const {
TOOL_EXECUTION_RESULT_KEY_BINDING_SCHEMA,
createToolExecutionResultArtifact,
normalizeToolExecutionResultKeyBinding,
} = require('../dist/tool-execution/toolExecutionCompletion');
const {
createToolResultKeyCatalogBootstrapCommand,
createToolResultKeyRotationCommand,
normalizeToolResultKeyCatalogRecord,
requireActiveToolResultKey,
toolResultKeyCatalogFence,
toolResultKeyMaterialProof,
} = require('../dist/tool-execution/toolResultKeyCatalog');
const {
TOOL_EXECUTION_RESULT_REKEY_OVERLAY_SCHEMA,
InvalidToolExecutionResultRekeyError,
ToolResultKeyRetirementCoverageBuilder,
ToolExecutionResultRekeyConflictError,
ToolExecutionResultRekeyUnavailableError,
createToolExecutionResultRekeyCommand,
createToolResultKeyRetirementReceipt,
createToolResultKeyRetirementReceiptCommand,
normalizeToolExecutionResultRekeyCommand,
normalizeToolResultKeyRetirementReceiptCommand,
normalizeToolResultKeyRetirementReceipt,
openToolExecutionResultRekeyOverlay,
} = require('../dist/tool-execution/toolResultRekey');
const {
ToolDefinitionRegistry,
} = require('../dist/tool-execution/tool-registry/toolRegistry');
const KEY_A = Buffer.alloc(32, 1);
const KEY_B = Buffer.alloc(32, 2);
const KEY_C = Buffer.alloc(32, 3);
const OUTPUT_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-output-digest@v1\0',
'utf8',
);
const RESULT_DIGEST_DOMAIN = Buffer.from(
'qinglong/trusted-tool-execution-result-digest@v1\0',
'utf8',
);
const BINDING_DIGEST_DOMAIN = Buffer.from(
'qinglong/tool-execution-result-key-binding-digest@v1\0',
'utf8',
);
function hash(domain, value) {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
function registry() {
return new ToolDefinitionRegistry([BUILTIN_RUN_READ_TOOL_DEFINITION]);
}
function output() {
return {
createdAtMs: 1_000,
eventSequence: 3,
executionOrigin: 'manual',
executionOwner: 'runtime',
found: true,
id: 'run-rekey-001',
priority: 10,
queuedAtMs: 1_100,
startedAtMs: 1_200,
status: 'succeeded',
taskId: 'task-rekey-001',
taskRevision: 'task-rekey-001@1',
version: 2,
};
}
function executionResult() {
const value = output();
const unsigned = Object.freeze({
schema: TRUSTED_TOOL_EXECUTION_RESULT_SCHEMA,
startId: 'tool-start-rekey-001',
barrierDigest: 'a'.repeat(64),
adapterDigest: 'b'.repeat(64),
output: value,
outputDigest: hash(OUTPUT_DIGEST_DOMAIN, value),
completedAtMs: 1_500,
});
return Object.freeze({
...unsigned,
resultDigest: hash(RESULT_DIGEST_DOMAIN, unsigned),
});
}
function artifact() {
return createToolExecutionResultArtifact(
{
artifactId: 'artifact-result-rekey-001',
projectId: 'project-rekey-001',
runId: 'run-host-rekey-001',
stepRunId: 'step-run-rekey-001',
tool: BUILTIN_RUN_READ_TOOL,
executionResult: executionResult(),
keyId: 'result-key-a',
key: KEY_A,
},
registry(),
() => Buffer.alloc(12, 4),
);
}
function binding(source) {
const unsigned = Object.freeze({
schema: TOOL_EXECUTION_RESULT_KEY_BINDING_SCHEMA,
startId: source.startId,
artifactId: source.artifactId,
artifactDigest: source.artifactDigest,
catalogGeneration: 1,
catalogDigest: 'c'.repeat(64),
keyId: source.keyId,
materialProof: toolResultKeyMaterialProof(source.keyId, KEY_A),
});
return normalizeToolExecutionResultKeyBinding({
...unsigned,
bindingDigest: hash(BINDING_DIGEST_DOMAIN, unsigned),
});
}
function committed(command, committedAtMs) {
return normalizeToolResultKeyCatalogRecord({
...command.next,
committedAtMs,
});
}
function targetCatalogs() {
const first = committed(
createToolResultKeyCatalogBootstrapCommand({
keyId: 'result-key-b',
materialProof: toolResultKeyMaterialProof('result-key-b', KEY_B),
mutationId: 'result-key-bootstrap-b',
}),
1_600,
);
const second = committed(
createToolResultKeyRotationCommand(first, {
keyId: 'result-key-c',
materialProof: toolResultKeyMaterialProof('result-key-c', KEY_C),
mutationId: 'result-key-rotate-c',
}),
1_800,
);
return { first, second };
}
test('creates an immutable rekey head without rewriting the source Artifact', () => {
const source = artifact();
const sourceJson = JSON.stringify(source);
const sourceBinding = binding(source);
const { first } = targetCatalogs();
const command = createToolExecutionResultRekeyCommand({
artifact: source,
binding: sourceBinding,
previousOverlay: null,
overlayId: 'result-rekey-overlay-001',
mutationId: 'result-rekey-mutation-001',
targetCatalogFence: toolResultKeyCatalogFence(
first,
requireActiveToolResultKey(first),
),
targetKey: KEY_B,
output: output(),
rekeyedAtMs: 1_700,
registry: registry(),
nonceFactory: () => Buffer.alloc(12, 5),
});
assert.equal(command.expectedRevision, 0);
assert.equal(command.expectedOverlayDigest, null);
assert.equal(
command.overlay.schema,
TOOL_EXECUTION_RESULT_REKEY_OVERLAY_SCHEMA,
);
assert.equal(command.overlay.revision, 1);
assert.equal(command.overlay.fromKeyId, 'result-key-a');
assert.equal(command.overlay.targetCatalogFence.keyId, 'result-key-b');
assert.equal(
JSON.stringify(command.overlay).includes('run-rekey-001'),
false,
);
assert.equal(JSON.stringify(source), sourceJson);
assert.deepEqual(
openToolExecutionResultRekeyOverlay(
command.overlay,
KEY_B,
registry(),
source,
),
output(),
);
assert.throws(
() =>
openToolExecutionResultRekeyOverlay(
command.overlay,
KEY_C,
registry(),
source,
),
ToolExecutionResultRekeyUnavailableError,
);
});
test('chains rekey revisions with an exact head fence', () => {
const source = artifact();
const sourceBinding = binding(source);
const { first, second } = targetCatalogs();
const firstCommand = createToolExecutionResultRekeyCommand({
artifact: source,
binding: sourceBinding,
previousOverlay: null,
overlayId: 'result-rekey-overlay-001',
mutationId: 'result-rekey-mutation-001',
targetCatalogFence: toolResultKeyCatalogFence(
first,
requireActiveToolResultKey(first),
),
targetKey: KEY_B,
output: output(),
rekeyedAtMs: 1_700,
registry: registry(),
nonceFactory: () => Buffer.alloc(12, 5),
});
const secondCommand = createToolExecutionResultRekeyCommand({
artifact: source,
binding: sourceBinding,
previousOverlay: firstCommand.overlay,
overlayId: 'result-rekey-overlay-002',
mutationId: 'result-rekey-mutation-002',
targetCatalogFence: toolResultKeyCatalogFence(
second,
requireActiveToolResultKey(second),
),
targetKey: KEY_C,
output: output(),
rekeyedAtMs: 1_900,
registry: registry(),
nonceFactory: () => Buffer.alloc(12, 6),
});
assert.equal(secondCommand.expectedRevision, 1);
assert.equal(
secondCommand.expectedOverlayDigest,
firstCommand.overlay.overlayDigest,
);
assert.equal(secondCommand.overlay.revision, 2);
assert.equal(secondCommand.overlay.fromKeyId, 'result-key-b');
assert.deepEqual(
openToolExecutionResultRekeyOverlay(
secondCommand.overlay,
KEY_C,
registry(),
source,
),
output(),
);
assert.throws(
() =>
normalizeToolExecutionResultRekeyCommand({
...secondCommand,
expectedRevision: 0,
}),
InvalidToolExecutionResultRekeyError,
);
assert.throws(
() =>
createToolExecutionResultRekeyCommand({
artifact: { ...source, artifactId: 'artifact-result-rekey-other' },
binding: sourceBinding,
previousOverlay: firstCommand.overlay,
overlayId: 'result-rekey-overlay-bad',
mutationId: 'result-rekey-mutation-bad',
targetCatalogFence: toolResultKeyCatalogFence(
second,
requireActiveToolResultKey(second),
),
targetKey: KEY_C,
output: output(),
rekeyedAtMs: 1_900,
registry: registry(),
}),
TypeError,
);
assert.throws(
() =>
createToolExecutionResultRekeyCommand({
artifact: source,
binding: { ...sourceBinding, artifactId: 'artifact-result-other' },
previousOverlay: firstCommand.overlay,
overlayId: 'result-rekey-overlay-bad',
mutationId: 'result-rekey-mutation-bad',
targetCatalogFence: toolResultKeyCatalogFence(
second,
requireActiveToolResultKey(second),
),
targetKey: KEY_C,
output: output(),
rekeyedAtMs: 1_900,
registry: registry(),
}),
TypeError,
);
});
test('binds retirement evidence to an exact catalog and zero uncovered heads', () => {
const coverage = new ToolResultKeyRetirementCoverageBuilder({
catalogGeneration: 2,
catalogDigest: 'd'.repeat(64),
keyId: 'result-key-a',
decryptableKeyIds: ['result-key-b'],
});
coverage.add({
artifactId: 'artifact-result-rekey-001',
bindingDigest: 'f'.repeat(64),
bindingKeyId: 'result-key-a',
headOverlayDigest: '1'.repeat(64),
headTargetKeyId: 'result-key-b',
headTargetCatalogGeneration: 2,
headTargetCatalogDigest: 'd'.repeat(64),
});
const coverageResult = coverage.finish();
assert.deepEqual(
{
bindingCount: coverageResult.bindingCount,
overlayHeadCount: coverageResult.overlayHeadCount,
uncoveredBindingCount: coverageResult.uncoveredBindingCount,
uncoveredOverlayHeadCount: coverageResult.uncoveredOverlayHeadCount,
},
{
bindingCount: 1,
overlayHeadCount: 1,
uncoveredBindingCount: 0,
uncoveredOverlayHeadCount: 0,
},
);
const receipt = createToolResultKeyRetirementReceipt({
catalogGeneration: 2,
catalogDigest: 'd'.repeat(64),
keyId: 'result-key-a',
materialProof: toolResultKeyMaterialProof('result-key-a', KEY_A),
mutationId: 'result-key-retirement-receipt-001',
bindingCount: coverageResult.bindingCount,
overlayHeadCount: coverageResult.overlayHeadCount,
coverageDigest: coverageResult.coverageDigest,
createdAtMs: 2_000,
});
assert.equal(receipt.uncoveredBindingCount, 0);
assert.equal(receipt.uncoveredOverlayHeadCount, 0);
assert.throws(
() =>
normalizeToolResultKeyRetirementReceipt({
...receipt,
uncoveredBindingCount: 1,
}),
InvalidToolExecutionResultRekeyError,
);
const command = createToolResultKeyRetirementReceiptCommand({
expectedCatalogGeneration: 2,
expectedCatalogDigest: 'd'.repeat(64),
keyId: 'result-key-a',
mutationId: 'result-key-retirement-receipt-001',
});
assert.throws(
() =>
normalizeToolResultKeyRetirementReceiptCommand({
...command,
expectedCatalogGeneration: 3,
}),
InvalidToolExecutionResultRekeyError,
);
});
test('retirement coverage rejects missing and self-key heads', () => {
const missing = new ToolResultKeyRetirementCoverageBuilder({
catalogGeneration: 2,
catalogDigest: 'd'.repeat(64),
keyId: 'result-key-a',
decryptableKeyIds: ['result-key-b'],
});
missing.add({
artifactId: 'artifact-result-rekey-001',
bindingDigest: 'f'.repeat(64),
bindingKeyId: 'result-key-a',
headOverlayDigest: null,
headTargetKeyId: null,
headTargetCatalogGeneration: null,
headTargetCatalogDigest: null,
});
assert.equal(missing.finish().uncoveredBindingCount, 1);
const self = new ToolResultKeyRetirementCoverageBuilder({
catalogGeneration: 2,
catalogDigest: 'd'.repeat(64),
keyId: 'result-key-a',
decryptableKeyIds: ['result-key-b'],
});
self.add({
artifactId: 'artifact-result-rekey-002',
bindingDigest: '2'.repeat(64),
bindingKeyId: 'result-key-b',
headOverlayDigest: '3'.repeat(64),
headTargetKeyId: 'result-key-a',
headTargetCatalogGeneration: 1,
headTargetCatalogDigest: 'c'.repeat(64),
});
assert.equal(self.finish().uncoveredOverlayHeadCount, 1);
});
test('exposes rekey only from its explicit subpath', () => {
const root = require('../dist');
const authority = require('@qinglong/runtime-core/tool-result-rekey');
assert.equal(root.createToolExecutionResultRekeyCommand, undefined);
assert.equal(
authority.createToolExecutionResultRekeyCommand,
createToolExecutionResultRekeyCommand,
);
assert.equal(
authority.ToolExecutionResultRekeyConflictError,
ToolExecutionResultRekeyConflictError,
);
});