mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} = require('node:fs');
|
||||
const { generateKeyPairSync } = require('node:crypto');
|
||||
const http = require('node:http');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { afterEach, test } = require('node:test');
|
||||
|
||||
const {
|
||||
BUNDLE_SCHEMA,
|
||||
VaultTransitPromptOutputCustodyError,
|
||||
readCommand,
|
||||
run,
|
||||
vaultRequest,
|
||||
} = require('../../scripts/ql3-vault-transit-prompt-output-custody.cjs');
|
||||
const {
|
||||
pluginPackagePromptOutputKeyRotationMaterialProof,
|
||||
} = require('../../packages/ql3-ai/dist/prompt-output/key-management/pluginPackagePromptOutputKeyRotation.js');
|
||||
|
||||
const directories = [];
|
||||
afterEach(() => {
|
||||
for (const directory of directories.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-vault-custody-'));
|
||||
directories.push(directory);
|
||||
const material = Buffer.alloc(32, 0x6a);
|
||||
const signingKeys = generateKeyPairSync('ed25519');
|
||||
const files = {
|
||||
token: privateFile(directory, 'vault-token', 'fixture-vault-token\n'),
|
||||
material: privateFile(directory, 'material.bin', material),
|
||||
signingPrivate: privateFile(
|
||||
directory,
|
||||
'custody-private.pem',
|
||||
signingKeys.privateKey.export({ format: 'pem', type: 'pkcs8' }),
|
||||
),
|
||||
signingPublic: privateFile(
|
||||
directory,
|
||||
'custody-public.pem',
|
||||
signingKeys.publicKey.export({ format: 'pem', type: 'spki' }),
|
||||
),
|
||||
bundle: path.join(directory, 'custody-bundle.json'),
|
||||
recovered: path.join(directory, 'recovered-material.bin'),
|
||||
};
|
||||
const vault = {
|
||||
transport: 'loopback-http-fixture',
|
||||
endpoint: 'http://127.0.0.1:18200',
|
||||
caFile: null,
|
||||
tokenFile: files.token,
|
||||
transitMount: 'prompt-output-transit',
|
||||
keyName: 'prompt-output-custody',
|
||||
};
|
||||
const wrapValue = {
|
||||
schemaVersion: 1,
|
||||
operation: 'external.prompt-output-key.vault-transit.wrap',
|
||||
vault,
|
||||
materialFile: files.material,
|
||||
bundleOutputFile: files.bundle,
|
||||
custody: {
|
||||
custodyId: 'vault-custody-001',
|
||||
keyId: 'prompt-key-vault-001',
|
||||
sourceGeneration: 8,
|
||||
sourceCatalogDigest: '1'.repeat(64),
|
||||
receiptSigningPrivateKeyFile: files.signingPrivate,
|
||||
receiptSigningPublicKeyFile: files.signingPublic,
|
||||
},
|
||||
};
|
||||
const unwrapValue = {
|
||||
schemaVersion: 1,
|
||||
operation: 'external.prompt-output-key.vault-transit.unwrap',
|
||||
vault,
|
||||
bundleFile: files.bundle,
|
||||
custodyPublicKeyFile: files.signingPublic,
|
||||
recoveredMaterialOutputFile: files.recovered,
|
||||
};
|
||||
const wrapCommand = privateFile(
|
||||
directory,
|
||||
'wrap-command.json',
|
||||
JSON.stringify(wrapValue),
|
||||
);
|
||||
const unwrapCommand = privateFile(
|
||||
directory,
|
||||
'unwrap-command.json',
|
||||
JSON.stringify(unwrapValue),
|
||||
);
|
||||
return {
|
||||
directory,
|
||||
material,
|
||||
files,
|
||||
wrapValue,
|
||||
unwrapValue,
|
||||
wrapCommand,
|
||||
unwrapCommand,
|
||||
};
|
||||
}
|
||||
|
||||
function transit(material, options = {}) {
|
||||
let calls = 0;
|
||||
const ciphertext = 'vault:v7:QUJDREVGRw==';
|
||||
return {
|
||||
get calls() {
|
||||
return calls;
|
||||
},
|
||||
request: async (vault, operation, token, body) => {
|
||||
calls += 1;
|
||||
assert.equal(vault.transitMount, 'prompt-output-transit');
|
||||
assert.equal(vault.keyName, 'prompt-output-custody');
|
||||
assert.equal(token, 'fixture-vault-token');
|
||||
if (operation === 'encrypt') {
|
||||
assert.equal(body.plaintext, material.toString('base64'));
|
||||
return { data: { ciphertext } };
|
||||
}
|
||||
assert.equal(operation, 'decrypt');
|
||||
assert.equal(body.ciphertext, ciphertext);
|
||||
return {
|
||||
data: {
|
||||
plaintext: (options.recoveredMaterial ?? material).toString('base64'),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('creates one immutable signed Vault Transit custody bundle', async () => {
|
||||
const value = fixture();
|
||||
const provider = transit(value.material);
|
||||
const result = await run(readCommand(value.wrapCommand), {
|
||||
request: provider.request,
|
||||
now: () => 1_700_000_000_000,
|
||||
});
|
||||
assert.equal(result.status, 'completed');
|
||||
assert.equal(provider.calls, 1);
|
||||
assert.equal(statSync(value.files.bundle).mode & 0o777, 0o400);
|
||||
const bundleSource = readFileSync(value.files.bundle, 'utf8');
|
||||
const bundle = JSON.parse(bundleSource);
|
||||
assert.equal(bundle.schema, BUNDLE_SCHEMA);
|
||||
assert.equal(bundle.receipt.wrappingProvider, 'vault-transit');
|
||||
assert.equal(bundle.receipt.materialProof.length, 64);
|
||||
assert.equal(bundle.bundleDigest.length, 64);
|
||||
assert.equal(bundleSource.includes('fixture-vault-token'), false);
|
||||
assert.equal(bundleSource.includes(value.material.toString('base64')), false);
|
||||
assert.equal(bundleSource.includes('prompt-output-custody'), false);
|
||||
assert.equal(bundleSource.includes('127.0.0.1'), false);
|
||||
});
|
||||
|
||||
test('exact wrap replay verifies the immutable bundle without Vault access', async () => {
|
||||
const value = fixture();
|
||||
const provider = transit(value.material);
|
||||
const command = readCommand(value.wrapCommand);
|
||||
const first = await run(command, {
|
||||
request: provider.request,
|
||||
now: () => 1_700_000_000_000,
|
||||
});
|
||||
const second = await run(command, {
|
||||
request: () => {
|
||||
throw new Error('Vault must not be called for replay');
|
||||
},
|
||||
});
|
||||
assert.equal(first.status, 'completed');
|
||||
assert.equal(second.status, 'existing');
|
||||
assert.equal(second.receipt.receiptDigest, first.receipt.receiptDigest);
|
||||
assert.equal(provider.calls, 1);
|
||||
});
|
||||
|
||||
test('unwrap writes one 0400 material and exact replay avoids Vault', async () => {
|
||||
const value = fixture();
|
||||
const provider = transit(value.material);
|
||||
await run(readCommand(value.wrapCommand), {
|
||||
request: provider.request,
|
||||
now: () => 1_700_000_000_000,
|
||||
});
|
||||
const command = readCommand(value.unwrapCommand);
|
||||
const first = await run(command, { request: provider.request });
|
||||
assert.equal(first.status, 'completed');
|
||||
assert.equal(provider.calls, 2);
|
||||
assert.equal(statSync(value.files.recovered).mode & 0o777, 0o400);
|
||||
assert.deepEqual(readFileSync(value.files.recovered), value.material);
|
||||
const second = await run(command, {
|
||||
request: () => {
|
||||
throw new Error('Vault must not be called for replay');
|
||||
},
|
||||
});
|
||||
assert.equal(second.status, 'existing');
|
||||
assert.equal(provider.calls, 2);
|
||||
});
|
||||
|
||||
test('rejects wrong recovered material and key authority drift before output', async () => {
|
||||
const value = fixture();
|
||||
const provider = transit(value.material, {
|
||||
recoveredMaterial: Buffer.alloc(32, 0x6b),
|
||||
});
|
||||
await run(readCommand(value.wrapCommand), {
|
||||
request: provider.request,
|
||||
now: () => 1_700_000_000_000,
|
||||
});
|
||||
await assert.rejects(
|
||||
() => run(readCommand(value.unwrapCommand), { request: provider.request }),
|
||||
(error) =>
|
||||
error instanceof VaultTransitPromptOutputCustodyError &&
|
||||
error.code === 'QL3_VAULT_CUSTODY_RECOVERED_MATERIAL_UNTRUSTED',
|
||||
);
|
||||
assert.equal(existsSync(value.files.recovered), false);
|
||||
|
||||
const drifted = {
|
||||
...value.unwrapValue,
|
||||
vault: { ...value.unwrapValue.vault, keyName: 'other-key' },
|
||||
};
|
||||
chmodSync(value.unwrapCommand, 0o600);
|
||||
writeFileSync(value.unwrapCommand, JSON.stringify(drifted));
|
||||
chmodSync(value.unwrapCommand, 0o440);
|
||||
await assert.rejects(
|
||||
() =>
|
||||
run(readCommand(value.unwrapCommand), {
|
||||
request: () => {
|
||||
throw new Error('drift must fail before Vault access');
|
||||
},
|
||||
}),
|
||||
(error) =>
|
||||
error instanceof VaultTransitPromptOutputCustodyError &&
|
||||
error.code === 'QL3_VAULT_CUSTODY_BUNDLE_UNTRUSTED',
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects insecure production transport and unsafe authority files', () => {
|
||||
for (const mutation of [
|
||||
(value) => {
|
||||
value.vault.transport = 'https';
|
||||
value.vault.caFile = null;
|
||||
},
|
||||
(value) => {
|
||||
value.vault.endpoint = 'http://vault.example.test:8200';
|
||||
},
|
||||
]) {
|
||||
const value = fixture();
|
||||
const command = structuredClone(value.wrapValue);
|
||||
mutation(command);
|
||||
chmodSync(value.wrapCommand, 0o600);
|
||||
writeFileSync(value.wrapCommand, JSON.stringify(command));
|
||||
chmodSync(value.wrapCommand, 0o440);
|
||||
assert.throws(
|
||||
() => readCommand(value.wrapCommand),
|
||||
VaultTransitPromptOutputCustodyError,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('CLI replay emits only content-free custody facts', async () => {
|
||||
const value = fixture();
|
||||
const provider = transit(value.material);
|
||||
await run(readCommand(value.wrapCommand), {
|
||||
request: provider.request,
|
||||
now: () => 1_700_000_000_000,
|
||||
});
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../../scripts/ql3-vault-transit-prompt-output-custody.cjs',
|
||||
),
|
||||
'wrap',
|
||||
'--command-file',
|
||||
value.wrapCommand,
|
||||
],
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const report = JSON.parse(result.stdout);
|
||||
assert.equal(report.event, 'custody_bundle_created');
|
||||
assert.equal(report.status, 'existing');
|
||||
assert.equal(result.stdout.includes('fixture-vault-token'), false);
|
||||
assert.equal(
|
||||
result.stdout.includes(value.material.toString('base64')),
|
||||
false,
|
||||
);
|
||||
assert.equal(result.stdout.includes(value.files.material), false);
|
||||
});
|
||||
|
||||
test('material proof remains bound to the original exact 32-byte key', async () => {
|
||||
const value = fixture();
|
||||
const provider = transit(value.material);
|
||||
const result = await run(readCommand(value.wrapCommand), {
|
||||
request: provider.request,
|
||||
now: () => 1_700_000_000_000,
|
||||
});
|
||||
assert.equal(
|
||||
result.receipt.materialProof,
|
||||
pluginPackagePromptOutputKeyRotationMaterialProof(
|
||||
result.receipt.keyId,
|
||||
value.material,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('loopback transport sends one bounded Vault request and rejects oversized responses', async () => {
|
||||
const requests = [];
|
||||
const server = http.createServer((request, response) => {
|
||||
const chunks = [];
|
||||
request.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
request.on('end', () => {
|
||||
const body = Buffer.concat(chunks);
|
||||
chunks.forEach((chunk) => chunk.fill(0));
|
||||
try {
|
||||
requests.push({
|
||||
url: request.url,
|
||||
token: request.headers['x-vault-token'],
|
||||
body: JSON.parse(body.toString('utf8')),
|
||||
});
|
||||
} finally {
|
||||
body.fill(0);
|
||||
}
|
||||
response.statusCode = 200;
|
||||
response.setHeader('connection', 'close');
|
||||
if (request.url.includes('/decrypt/')) {
|
||||
response.end(Buffer.alloc(129 * 1024, 0x61));
|
||||
} else {
|
||||
response.end(
|
||||
JSON.stringify({ data: { ciphertext: 'vault:v9:QUJDRA==' } }),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
const vault = {
|
||||
endpoint: `http://127.0.0.1:${address.port}`,
|
||||
caFile: null,
|
||||
transitMount: 'prompt-output-transit',
|
||||
keyName: 'prompt-output-custody',
|
||||
};
|
||||
try {
|
||||
const response = await vaultRequest(
|
||||
vault,
|
||||
'encrypt',
|
||||
'fixture-vault-token',
|
||||
{ plaintext: 'cHJpdmF0ZS1maXh0dXJl' },
|
||||
);
|
||||
assert.equal(response.data.ciphertext, 'vault:v9:QUJDRA==');
|
||||
assert.deepEqual(requests[0], {
|
||||
url: '/v1/prompt-output-transit/encrypt/prompt-output-custody',
|
||||
token: 'fixture-vault-token',
|
||||
body: { plaintext: 'cHJpdmF0ZS1maXh0dXJl' },
|
||||
});
|
||||
await assert.rejects(
|
||||
() =>
|
||||
vaultRequest(vault, 'decrypt', 'fixture-vault-token', {
|
||||
ciphertext: 'vault:v9:QUJDRA==',
|
||||
}),
|
||||
(error) =>
|
||||
error instanceof VaultTransitPromptOutputCustodyError &&
|
||||
error.code === 'QL3_VAULT_CUSTODY_RESPONSE_TOO_LARGE',
|
||||
);
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user