mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
337 lines
11 KiB
JavaScript
337 lines
11 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const {
|
|
chmodSync,
|
|
mkdtempSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { createHash, generateKeyPairSync, sign } = require('node:crypto');
|
|
const { afterEach, test } = require('node:test');
|
|
|
|
const {
|
|
createPluginPackagePromptOutputArtifact,
|
|
} = require('../../ql3-ai/dist/prompt-output/pluginPackagePromptOutputArtifact.js');
|
|
const {
|
|
createPluginPackagePromptOutputExternalCustodyReceipt,
|
|
} = require('../../ql3-ai/dist/prompt-output/custody/pluginPackagePromptOutputExternalCustody.js');
|
|
const {
|
|
createPluginPackagePromptOutputExternalCustodyBundle,
|
|
} = require('../../ql3-ai/dist/prompt-output/custody/pluginPackagePromptOutputExternalCustodyBundle.js');
|
|
const {
|
|
createPluginPackagePromptOutputExternalRecoveryAuthorization,
|
|
} = require('../../ql3-ai/dist/prompt-output/custody/pluginPackagePromptOutputExternalRecoveryAuthorization.js');
|
|
const {
|
|
pluginPackagePromptOutputKeyRotationMaterialProof,
|
|
} = require('../../ql3-ai/dist/prompt-output/key-management/pluginPackagePromptOutputKeyRotation.js');
|
|
const {
|
|
disposeClusterPromptOutputExternalRecoveryInput,
|
|
readClusterPromptOutputExternalRecoveryCommand,
|
|
readClusterPromptOutputExternalRecoveryInput,
|
|
} = require('../dist/prompt-output/external-recovery/promptOutputExternalRecoveryInput.js');
|
|
const {
|
|
ClusterPromptOutputExternalRecoveryVerifierConfigError,
|
|
runClusterPromptOutputExternalRecoveryVerifier,
|
|
} = require('../dist/prompt-output/external-recovery/promptOutputExternalRecoveryVerifier.js');
|
|
|
|
const temporaryDirectories = [];
|
|
afterEach(() => {
|
|
for (const directory of temporaryDirectories.splice(0)) {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function privateFile(directory, name, value, mode = 0o440) {
|
|
const target = path.join(directory, name);
|
|
writeFileSync(target, value, { mode: 0o600 });
|
|
chmodSync(target, mode);
|
|
return target;
|
|
}
|
|
|
|
function fixture() {
|
|
const directory = mkdtempSync(path.join(os.tmpdir(), 'ql3-recovery-'));
|
|
temporaryDirectories.push(directory);
|
|
const now = Date.now();
|
|
const material = Buffer.alloc(32, 0x5c);
|
|
const wrapped = Buffer.from('provider-wrapped-key-material');
|
|
const custodyKeys = generateKeyPairSync('ed25519');
|
|
const approverAKeys = generateKeyPairSync('ed25519');
|
|
const approverBKeys = generateKeyPairSync('ed25519');
|
|
const catalogDigest = '1'.repeat(64);
|
|
const artifact = createPluginPackagePromptOutputArtifact(
|
|
{
|
|
projectId: 'project-offline-recovery',
|
|
runId: 'run-offline-recovery',
|
|
stepRunId: 'step-offline-recovery',
|
|
invocationId: 'invocation-offline-recovery',
|
|
requestedBy: { type: 'user', id: 'requester-user' },
|
|
result: {
|
|
provider: 'openai-compatible',
|
|
model: 'bounded-model',
|
|
text: 'private offline recovery output',
|
|
finishReason: 'stop',
|
|
usage: {
|
|
inputTokens: 4,
|
|
outputTokens: 5,
|
|
totalTokens: 9,
|
|
costMicros: 10,
|
|
},
|
|
},
|
|
retentionPolicy: {
|
|
revision: 'offline-recovery-v1',
|
|
retentionMs: 86_400_000,
|
|
},
|
|
keyId: 'offline-recovery-key',
|
|
key: Buffer.from(material),
|
|
sealedAtMs: now - 60_000,
|
|
},
|
|
() => Buffer.alloc(12, 0x39),
|
|
);
|
|
const receipt = createPluginPackagePromptOutputExternalCustodyReceipt(
|
|
{
|
|
custodyId: 'offline-custody',
|
|
keyId: 'offline-recovery-key',
|
|
materialProof: pluginPackagePromptOutputKeyRotationMaterialProof(
|
|
'offline-recovery-key',
|
|
material,
|
|
),
|
|
sourceGeneration: 4,
|
|
sourceCatalogDigest: catalogDigest,
|
|
wrappingProvider: 'external-kms',
|
|
wrappingKeyRefDigest: '2'.repeat(64),
|
|
wrappedMaterialDigest: createHash('sha256').update(wrapped).digest('hex'),
|
|
wrappedMaterialBytes: wrapped.length,
|
|
createdAtMs: now - 120_000,
|
|
},
|
|
{
|
|
publicKey: custodyKeys.publicKey,
|
|
sign: (message) => sign(null, message, custodyKeys.privateKey),
|
|
},
|
|
);
|
|
const custodyBundle = createPluginPackagePromptOutputExternalCustodyBundle(
|
|
receipt,
|
|
custodyKeys.publicKey,
|
|
wrapped,
|
|
);
|
|
const approvalSigner = (userId, authenticationId, keys, approvedAtMs) => ({
|
|
userId,
|
|
authenticationId,
|
|
authenticatedAtMs: approvedAtMs - 1_000,
|
|
approvedAtMs,
|
|
publicKey: keys.publicKey,
|
|
sign: (message) => sign(null, message, keys.privateKey),
|
|
});
|
|
const authorization =
|
|
createPluginPackagePromptOutputExternalRecoveryAuthorization(
|
|
{
|
|
recoveryId: 'offline-recovery-001',
|
|
requestId: 'offline-request-001',
|
|
custodyId: receipt.custodyId,
|
|
custodyReceiptDigest: receipt.receiptDigest,
|
|
keyId: receipt.keyId,
|
|
artifactId: artifact.artifactId,
|
|
artifactDigest: artifact.artifactDigest,
|
|
policyDigest: '3'.repeat(64),
|
|
requestedBy: {
|
|
userId: 'requester-user',
|
|
authenticationId: 'requester-auth',
|
|
authenticatedAtMs: now - 4_000,
|
|
},
|
|
requestedAtMs: now - 3_000,
|
|
expiresAtMs: now + 10 * 60_000,
|
|
},
|
|
[
|
|
approvalSigner(
|
|
'reviewer-a',
|
|
'reviewer-a-auth',
|
|
approverAKeys,
|
|
now - 2_000,
|
|
),
|
|
approvalSigner(
|
|
'reviewer-b',
|
|
'reviewer-b-auth',
|
|
approverBKeys,
|
|
now - 1_000,
|
|
),
|
|
],
|
|
);
|
|
const files = {
|
|
authorizationFile: privateFile(
|
|
directory,
|
|
'authorization.json',
|
|
JSON.stringify(authorization),
|
|
),
|
|
custodyBundleFile: privateFile(
|
|
directory,
|
|
'custody-bundle.json',
|
|
JSON.stringify(custodyBundle),
|
|
),
|
|
recoveredMaterialFile: privateFile(directory, 'material.bin', material),
|
|
durableKeyFactFile: privateFile(
|
|
directory,
|
|
'durable-fact.json',
|
|
JSON.stringify({
|
|
keyId: receipt.keyId,
|
|
materialProof: receipt.materialProof,
|
|
catalogDigest: receipt.sourceCatalogDigest,
|
|
}),
|
|
),
|
|
artifactFile: privateFile(
|
|
directory,
|
|
'artifact.json',
|
|
JSON.stringify(artifact),
|
|
),
|
|
custodyPublicKeyFile: privateFile(
|
|
directory,
|
|
'custody-public.pem',
|
|
custodyKeys.publicKey.export({ format: 'pem', type: 'spki' }),
|
|
),
|
|
approverAFile: privateFile(
|
|
directory,
|
|
'reviewer-a-public.pem',
|
|
approverAKeys.publicKey.export({ format: 'pem', type: 'spki' }),
|
|
),
|
|
approverBFile: privateFile(
|
|
directory,
|
|
'reviewer-b-public.pem',
|
|
approverBKeys.publicKey.export({ format: 'pem', type: 'spki' }),
|
|
),
|
|
};
|
|
const command = {
|
|
schemaVersion: 1,
|
|
operation: 'cluster.prompt-output-key.verify-recovery',
|
|
authorizationFile: files.authorizationFile,
|
|
custodyBundleFile: files.custodyBundleFile,
|
|
recoveredMaterialFile: files.recoveredMaterialFile,
|
|
durableKeyFactFile: files.durableKeyFactFile,
|
|
artifactFile: files.artifactFile,
|
|
custodyPublicKeyFile: files.custodyPublicKeyFile,
|
|
approverPublicKeyFiles: [
|
|
{ userId: 'reviewer-b', filePath: files.approverBFile },
|
|
{ userId: 'reviewer-a', filePath: files.approverAFile },
|
|
],
|
|
};
|
|
const commandFile = privateFile(
|
|
directory,
|
|
'command.json',
|
|
JSON.stringify(command),
|
|
0o444,
|
|
);
|
|
return { directory, now, files, command, commandFile };
|
|
}
|
|
|
|
test('reads one private recovery workspace and emits a content-free proof', () => {
|
|
const value = fixture();
|
|
const command = readClusterPromptOutputExternalRecoveryCommand(
|
|
value.commandFile,
|
|
);
|
|
assert.deepEqual(
|
|
command.approverPublicKeyFiles.map(({ userId }) => userId),
|
|
['reviewer-a', 'reviewer-b'],
|
|
);
|
|
const input = readClusterPromptOutputExternalRecoveryInput(command);
|
|
const proof = runClusterPromptOutputExternalRecoveryVerifier(
|
|
input,
|
|
value.now + 3_000,
|
|
);
|
|
assert.equal(proof.schema.includes('authorized'), true);
|
|
assert.equal(proof.authorizationDigest.length, 64);
|
|
assert.equal(JSON.stringify(proof).includes('private offline'), false);
|
|
disposeClusterPromptOutputExternalRecoveryInput(input);
|
|
assert.equal(
|
|
input.material.every((byte) => byte === 0),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
input.wrappedMaterial.every((byte) => byte === 0),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('CLI verifies the workspace without database, Kubernetes or KMS input', () => {
|
|
const value = fixture();
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
path.resolve(
|
|
__dirname,
|
|
'../dist/prompt-output/external-recovery/promptOutputExternalRecoveryCli.js',
|
|
),
|
|
'run',
|
|
'--command-file',
|
|
value.commandFile,
|
|
],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
const report = JSON.parse(result.stdout);
|
|
assert.equal(report.event, 'recovery_verified');
|
|
assert.equal(report.proofDigest.length, 64);
|
|
assert.equal(result.stdout.includes('private offline'), false);
|
|
});
|
|
|
|
test('rejects writable or symlinked recovery material and custody bundles', () => {
|
|
for (const mutation of [
|
|
(value) => chmodSync(value.files.recoveredMaterialFile, 0o640),
|
|
(value) => chmodSync(value.files.custodyBundleFile, 0o640),
|
|
(value) => {
|
|
const link = path.join(value.directory, 'material-link.bin');
|
|
symlinkSync(value.files.recoveredMaterialFile, link);
|
|
const command = {
|
|
...value.command,
|
|
recoveredMaterialFile: link,
|
|
};
|
|
chmodSync(value.commandFile, 0o600);
|
|
writeFileSync(value.commandFile, JSON.stringify(command));
|
|
chmodSync(value.commandFile, 0o444);
|
|
},
|
|
(value) => {
|
|
const link = path.join(value.directory, 'custody-bundle-link.json');
|
|
symlinkSync(value.files.custodyBundleFile, link);
|
|
const command = {
|
|
...value.command,
|
|
custodyBundleFile: link,
|
|
};
|
|
chmodSync(value.commandFile, 0o600);
|
|
writeFileSync(value.commandFile, JSON.stringify(command));
|
|
chmodSync(value.commandFile, 0o444);
|
|
},
|
|
]) {
|
|
const value = fixture();
|
|
mutation(value);
|
|
const command = readClusterPromptOutputExternalRecoveryCommand(
|
|
value.commandFile,
|
|
);
|
|
assert.throws(() => readClusterPromptOutputExternalRecoveryInput(command));
|
|
}
|
|
});
|
|
|
|
test('rejects authorization and Artifact drift with a content-free error', () => {
|
|
const value = fixture();
|
|
const command = readClusterPromptOutputExternalRecoveryCommand(
|
|
value.commandFile,
|
|
);
|
|
const input = readClusterPromptOutputExternalRecoveryInput(command);
|
|
try {
|
|
assert.throws(
|
|
() =>
|
|
runClusterPromptOutputExternalRecoveryVerifier(
|
|
{
|
|
...input,
|
|
artifact: {
|
|
...input.artifact,
|
|
artifactDigest: '4'.repeat(64),
|
|
},
|
|
},
|
|
value.now + 3_000,
|
|
),
|
|
ClusterPromptOutputExternalRecoveryVerifierConfigError,
|
|
);
|
|
} finally {
|
|
disposeClusterPromptOutputExternalRecoveryInput(input);
|
|
}
|
|
});
|