mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
206 lines
6.4 KiB
JavaScript
206 lines
6.4 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { createHash, generateKeyPairSync, sign } = require('node:crypto');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
|
createPluginPackagePromptOutputExternalCustodyReceipt,
|
|
normalizePluginPackagePromptOutputExternalCustodyReceipt,
|
|
verifyPluginPackagePromptOutputExternalCustodyReceipt,
|
|
verifyPluginPackagePromptOutputRecoveredMaterial,
|
|
verifyPluginPackagePromptOutputWrappedBackup,
|
|
} = require('../dist/prompt-output/custody/pluginPackagePromptOutputExternalCustody.js');
|
|
const {
|
|
createPluginPackagePromptOutputArtifact,
|
|
} = require('../dist/prompt-output/pluginPackagePromptOutputArtifact.js');
|
|
const {
|
|
pluginPackagePromptOutputKeyRotationMaterialProof,
|
|
} = require('../dist/prompt-output/key-management/pluginPackagePromptOutputKeyRotation.js');
|
|
|
|
const MATERIAL = Buffer.alloc(32, 0x5a);
|
|
const WRAPPED = Buffer.from('kms-wrapped-prompt-output-key-material');
|
|
const CATALOG_DIGEST = '1'.repeat(64);
|
|
const WRAPPING_KEY_REF_DIGEST = '2'.repeat(64);
|
|
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
|
|
function receipt(overrides = {}) {
|
|
return createPluginPackagePromptOutputExternalCustodyReceipt(
|
|
{
|
|
custodyId: 'custody-001',
|
|
keyId: 'prompt-key-001',
|
|
materialProof: pluginPackagePromptOutputKeyRotationMaterialProof(
|
|
'prompt-key-001',
|
|
MATERIAL,
|
|
),
|
|
sourceGeneration: 4,
|
|
sourceCatalogDigest: CATALOG_DIGEST,
|
|
wrappingProvider: 'vault-transit',
|
|
wrappingKeyRefDigest: WRAPPING_KEY_REF_DIGEST,
|
|
wrappedMaterialDigest: createHash('sha256').update(WRAPPED).digest('hex'),
|
|
wrappedMaterialBytes: WRAPPED.length,
|
|
createdAtMs: 1_700_000_000_000,
|
|
...overrides,
|
|
},
|
|
{
|
|
publicKey,
|
|
sign: (digest) => sign(null, digest, privateKey),
|
|
},
|
|
);
|
|
}
|
|
|
|
function artifact(overrides = {}) {
|
|
return createPluginPackagePromptOutputArtifact(
|
|
{
|
|
projectId: 'project-a',
|
|
runId: 'run-a',
|
|
stepRunId: 'step-a',
|
|
invocationId: 'invocation-a',
|
|
requestedBy: { type: 'user', id: 'user-a' },
|
|
result: {
|
|
provider: 'openai-compatible',
|
|
model: 'bounded-model',
|
|
text: 'private recovered answer',
|
|
finishReason: 'stop',
|
|
usage: {
|
|
inputTokens: 5,
|
|
outputTokens: 3,
|
|
totalTokens: 8,
|
|
costMicros: 11,
|
|
},
|
|
},
|
|
retentionPolicy: {
|
|
revision: 'retention-v1',
|
|
retentionMs: 86_400_000,
|
|
},
|
|
keyId: 'prompt-key-001',
|
|
key: Buffer.from(MATERIAL),
|
|
sealedAtMs: 1_700_000_000_000,
|
|
...overrides,
|
|
},
|
|
() => Buffer.alloc(12, 0x33),
|
|
);
|
|
}
|
|
|
|
test('binds a signed content-free custody receipt to exact wrapped bytes', () => {
|
|
const value = receipt();
|
|
assert.deepEqual(
|
|
normalizePluginPackagePromptOutputExternalCustodyReceipt(value),
|
|
value,
|
|
);
|
|
assert.deepEqual(
|
|
verifyPluginPackagePromptOutputExternalCustodyReceipt(value, publicKey),
|
|
value,
|
|
);
|
|
const verified = verifyPluginPackagePromptOutputWrappedBackup(
|
|
value,
|
|
publicKey,
|
|
WRAPPED,
|
|
);
|
|
assert.equal(verified.custodyId, 'custody-001');
|
|
assert.equal(verified.wrappedMaterialBytes, WRAPPED.length);
|
|
const serialized = JSON.stringify(value);
|
|
assert.equal(serialized.includes(MATERIAL.toString('base64url')), false);
|
|
assert.equal(serialized.includes(WRAPPED.toString('base64url')), false);
|
|
assert.equal(serialized.includes('kms-wrapped-prompt'), false);
|
|
});
|
|
|
|
test('verifies recovered material against durable fact and opens an Artifact', () => {
|
|
const custody = receipt();
|
|
const encrypted = artifact();
|
|
const proof = verifyPluginPackagePromptOutputRecoveredMaterial({
|
|
recoveryId: 'recovery-001',
|
|
requestId: 'request-001',
|
|
receipt: custody,
|
|
trustedPublicKey: publicKey,
|
|
durableKeyFact: {
|
|
keyId: custody.keyId,
|
|
materialProof: custody.materialProof,
|
|
catalogDigest: custody.sourceCatalogDigest,
|
|
},
|
|
material: MATERIAL,
|
|
artifact: encrypted,
|
|
verifiedAtMs: 1_700_000_001_000,
|
|
});
|
|
assert.equal(proof.artifactId, encrypted.artifactId);
|
|
assert.equal(proof.artifactDigest, encrypted.artifactDigest);
|
|
assert.equal(proof.contentDigest, encrypted.contentDigest);
|
|
assert.equal(proof.keyId, custody.keyId);
|
|
assert.equal(proof.proofDigest.length, 64);
|
|
const serialized = JSON.stringify(proof);
|
|
assert.equal(serialized.includes('private recovered answer'), false);
|
|
assert.equal(serialized.includes(MATERIAL.toString('base64url')), false);
|
|
});
|
|
|
|
test('rejects untrusted receipt, wrapped bytes, durable fact and material drift', () => {
|
|
const custody = receipt();
|
|
const otherKeys = generateKeyPairSync('ed25519');
|
|
assert.throws(
|
|
() =>
|
|
verifyPluginPackagePromptOutputExternalCustodyReceipt(
|
|
custody,
|
|
otherKeys.publicKey,
|
|
),
|
|
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
verifyPluginPackagePromptOutputWrappedBackup(
|
|
custody,
|
|
publicKey,
|
|
Buffer.from('different wrapped material'),
|
|
),
|
|
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
|
);
|
|
for (const candidate of [
|
|
{
|
|
durableKeyFact: {
|
|
keyId: custody.keyId,
|
|
materialProof: '3'.repeat(64),
|
|
catalogDigest: custody.sourceCatalogDigest,
|
|
},
|
|
material: MATERIAL,
|
|
},
|
|
{
|
|
durableKeyFact: {
|
|
keyId: custody.keyId,
|
|
materialProof: custody.materialProof,
|
|
catalogDigest: custody.sourceCatalogDigest,
|
|
},
|
|
material: Buffer.alloc(32, 0x5b),
|
|
},
|
|
]) {
|
|
assert.throws(
|
|
() =>
|
|
verifyPluginPackagePromptOutputRecoveredMaterial({
|
|
recoveryId: 'recovery-001',
|
|
requestId: 'request-001',
|
|
receipt: custody,
|
|
trustedPublicKey: publicKey,
|
|
durableKeyFact: candidate.durableKeyFact,
|
|
material: candidate.material,
|
|
artifact: artifact(),
|
|
verifiedAtMs: 1_700_000_001_000,
|
|
}),
|
|
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('rejects a custody receipt with a tampered signature', () => {
|
|
const custody = receipt();
|
|
const signature = Buffer.from(custody.signature, 'base64url');
|
|
signature[0] ^= 0x01;
|
|
|
|
assert.throws(
|
|
() =>
|
|
verifyPluginPackagePromptOutputExternalCustodyReceipt(
|
|
{
|
|
...custody,
|
|
signature: signature.toString('base64url'),
|
|
},
|
|
publicKey,
|
|
),
|
|
PluginPackagePromptOutputExternalCustodyUntrustedError,
|
|
);
|
|
});
|