mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
303 lines
9.6 KiB
JavaScript
303 lines
9.6 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { createHash, generateKeyPairSync, sign } = require('node:crypto');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
PluginPackagePromptOutputRecoveryAuthorizationUntrustedError,
|
|
createPluginPackagePromptOutputExternalRecoveryAuthorization,
|
|
normalizePluginPackagePromptOutputExternalRecoveryAuthorization,
|
|
verifyAuthorizedPluginPackagePromptOutputRecoveredMaterial,
|
|
verifyPluginPackagePromptOutputExternalRecoveryAuthorization,
|
|
} = require('../dist/prompt-output/custody/pluginPackagePromptOutputExternalRecoveryAuthorization.js');
|
|
const {
|
|
createPluginPackagePromptOutputExternalCustodyReceipt,
|
|
} = 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 NOW = 1_700_000_000_000;
|
|
const MATERIAL = Buffer.alloc(32, 0x4a);
|
|
const WRAPPED = Buffer.from('external-kms-wrapped-material');
|
|
const CATALOG_DIGEST = '1'.repeat(64);
|
|
const POLICY_DIGEST = '2'.repeat(64);
|
|
const WRAPPING_KEY_REF_DIGEST = '3'.repeat(64);
|
|
const custodyKeys = generateKeyPairSync('ed25519');
|
|
const approverAKeys = generateKeyPairSync('ed25519');
|
|
const approverBKeys = generateKeyPairSync('ed25519');
|
|
|
|
function artifact() {
|
|
return createPluginPackagePromptOutputArtifact(
|
|
{
|
|
projectId: 'project-recovery',
|
|
runId: 'run-recovery',
|
|
stepRunId: 'step-recovery',
|
|
invocationId: 'invocation-recovery',
|
|
requestedBy: { type: 'user', id: 'requester-user' },
|
|
result: {
|
|
provider: 'openai-compatible',
|
|
model: 'bounded-model',
|
|
text: 'historical private answer',
|
|
finishReason: 'stop',
|
|
usage: {
|
|
inputTokens: 5,
|
|
outputTokens: 4,
|
|
totalTokens: 9,
|
|
costMicros: 12,
|
|
},
|
|
},
|
|
retentionPolicy: {
|
|
revision: 'recovery-retention-v1',
|
|
retentionMs: 86_400_000,
|
|
},
|
|
keyId: 'prompt-key-recovery',
|
|
key: Buffer.from(MATERIAL),
|
|
sealedAtMs: NOW - 60_000,
|
|
},
|
|
() => Buffer.alloc(12, 0x31),
|
|
);
|
|
}
|
|
|
|
function receipt() {
|
|
return createPluginPackagePromptOutputExternalCustodyReceipt(
|
|
{
|
|
custodyId: 'custody-recovery',
|
|
keyId: 'prompt-key-recovery',
|
|
materialProof: pluginPackagePromptOutputKeyRotationMaterialProof(
|
|
'prompt-key-recovery',
|
|
MATERIAL,
|
|
),
|
|
sourceGeneration: 7,
|
|
sourceCatalogDigest: CATALOG_DIGEST,
|
|
wrappingProvider: 'external-kms',
|
|
wrappingKeyRefDigest: WRAPPING_KEY_REF_DIGEST,
|
|
wrappedMaterialDigest: createHash('sha256').update(WRAPPED).digest('hex'),
|
|
wrappedMaterialBytes: WRAPPED.length,
|
|
createdAtMs: NOW - 120_000,
|
|
},
|
|
{
|
|
publicKey: custodyKeys.publicKey,
|
|
sign: (message) => sign(null, message, custodyKeys.privateKey),
|
|
},
|
|
);
|
|
}
|
|
|
|
function signer(userId, authenticationId, keys, approvedAtMs) {
|
|
return {
|
|
userId,
|
|
authenticationId,
|
|
authenticatedAtMs: approvedAtMs - 1_000,
|
|
approvedAtMs,
|
|
publicKey: keys.publicKey,
|
|
sign: (message) => sign(null, message, keys.privateKey),
|
|
};
|
|
}
|
|
|
|
function authorization(encrypted, custody, overrides = {}) {
|
|
return createPluginPackagePromptOutputExternalRecoveryAuthorization(
|
|
{
|
|
recoveryId: 'recovery-001',
|
|
requestId: 'request-001',
|
|
custodyId: custody.custodyId,
|
|
custodyReceiptDigest: custody.receiptDigest,
|
|
keyId: custody.keyId,
|
|
artifactId: encrypted.artifactId,
|
|
artifactDigest: encrypted.artifactDigest,
|
|
policyDigest: POLICY_DIGEST,
|
|
requestedBy: {
|
|
userId: 'requester-user',
|
|
authenticationId: 'requester-auth-001',
|
|
authenticatedAtMs: NOW - 5_000,
|
|
},
|
|
requestedAtMs: NOW,
|
|
expiresAtMs: NOW + 10 * 60_000,
|
|
...overrides,
|
|
},
|
|
[
|
|
signer('reviewer-b', 'reviewer-b-auth', approverBKeys, NOW + 2_000),
|
|
signer('reviewer-a', 'reviewer-a-auth', approverAKeys, NOW + 1_000),
|
|
],
|
|
);
|
|
}
|
|
|
|
function trustedApprovers() {
|
|
return [
|
|
{ userId: 'reviewer-a', publicKey: approverAKeys.publicKey },
|
|
{ userId: 'reviewer-b', publicKey: approverBKeys.publicKey },
|
|
];
|
|
}
|
|
|
|
test('binds two distinct strong approvers to one exact recovery request', () => {
|
|
const encrypted = artifact();
|
|
const custody = receipt();
|
|
const value = authorization(encrypted, custody);
|
|
assert.deepEqual(
|
|
normalizePluginPackagePromptOutputExternalRecoveryAuthorization(value),
|
|
value,
|
|
);
|
|
assert.deepEqual(
|
|
verifyPluginPackagePromptOutputExternalRecoveryAuthorization(
|
|
value,
|
|
trustedApprovers(),
|
|
NOW + 3_000,
|
|
),
|
|
value,
|
|
);
|
|
assert.deepEqual(
|
|
value.approvals.map(({ userId }) => userId),
|
|
['reviewer-a', 'reviewer-b'],
|
|
);
|
|
assert.equal(value.permission, 'artifact.read');
|
|
assert.equal(value.purpose, 'lost-key-recovery-verification');
|
|
});
|
|
|
|
test('returns only an authorization-bound content-free recovery proof', () => {
|
|
const encrypted = artifact();
|
|
const custody = receipt();
|
|
const proof = verifyAuthorizedPluginPackagePromptOutputRecoveredMaterial({
|
|
authorization: authorization(encrypted, custody),
|
|
trustedApprovers: trustedApprovers(),
|
|
receipt: custody,
|
|
trustedCustodyPublicKey: custodyKeys.publicKey,
|
|
wrappedMaterial: WRAPPED,
|
|
durableKeyFact: {
|
|
keyId: custody.keyId,
|
|
materialProof: custody.materialProof,
|
|
catalogDigest: custody.sourceCatalogDigest,
|
|
},
|
|
material: MATERIAL,
|
|
artifact: encrypted,
|
|
verifiedAtMs: NOW + 3_000,
|
|
});
|
|
assert.equal(proof.artifactId, encrypted.artifactId);
|
|
assert.equal(proof.authorizationDigest.length, 64);
|
|
assert.equal(proof.recoveryProofDigest.length, 64);
|
|
assert.equal(proof.proofDigest.length, 64);
|
|
const serialized = JSON.stringify(proof);
|
|
assert.equal(serialized.includes('historical private answer'), false);
|
|
assert.equal(serialized.includes(MATERIAL.toString('base64url')), false);
|
|
assert.equal(serialized.includes(WRAPPED.toString('base64url')), false);
|
|
});
|
|
|
|
test('rejects requester self-approval, duplicate identity and stale authentication', () => {
|
|
const encrypted = artifact();
|
|
const custody = receipt();
|
|
for (const signers of [
|
|
[
|
|
signer(
|
|
'requester-user',
|
|
'requester-review-auth',
|
|
approverAKeys,
|
|
NOW + 1_000,
|
|
),
|
|
signer('reviewer-b', 'reviewer-b-auth', approverBKeys, NOW + 2_000),
|
|
],
|
|
[
|
|
signer('reviewer-a', 'same-auth', approverAKeys, NOW + 1_000),
|
|
signer('reviewer-b', 'same-auth', approverBKeys, NOW + 2_000),
|
|
],
|
|
[
|
|
{
|
|
...signer('reviewer-a', 'reviewer-a-auth', approverAKeys, NOW + 1_000),
|
|
authenticatedAtMs: NOW - 10 * 60_000,
|
|
},
|
|
signer('reviewer-b', 'reviewer-b-auth', approverBKeys, NOW + 2_000),
|
|
],
|
|
]) {
|
|
assert.throws(() =>
|
|
createPluginPackagePromptOutputExternalRecoveryAuthorization(
|
|
{
|
|
recoveryId: 'recovery-001',
|
|
requestId: 'request-001',
|
|
custodyId: custody.custodyId,
|
|
custodyReceiptDigest: custody.receiptDigest,
|
|
keyId: custody.keyId,
|
|
artifactId: encrypted.artifactId,
|
|
artifactDigest: encrypted.artifactDigest,
|
|
policyDigest: POLICY_DIGEST,
|
|
requestedBy: {
|
|
userId: 'requester-user',
|
|
authenticationId: 'requester-auth-001',
|
|
authenticatedAtMs: NOW - 5_000,
|
|
},
|
|
requestedAtMs: NOW,
|
|
expiresAtMs: NOW + 10 * 60_000,
|
|
},
|
|
signers,
|
|
),
|
|
);
|
|
}
|
|
});
|
|
|
|
test('rejects signature, trusted approver, expiry and exact fact drift', () => {
|
|
const encrypted = artifact();
|
|
const custody = receipt();
|
|
const approved = authorization(encrypted, custody);
|
|
const signature = Buffer.from(approved.approvals[0].signature, 'base64url');
|
|
signature[0] ^= 1;
|
|
assert.throws(
|
|
() =>
|
|
verifyPluginPackagePromptOutputExternalRecoveryAuthorization(
|
|
{
|
|
...approved,
|
|
approvals: [
|
|
{
|
|
...approved.approvals[0],
|
|
signature: signature.toString('base64url'),
|
|
},
|
|
approved.approvals[1],
|
|
],
|
|
},
|
|
trustedApprovers(),
|
|
NOW + 3_000,
|
|
),
|
|
PluginPackagePromptOutputRecoveryAuthorizationUntrustedError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
verifyPluginPackagePromptOutputExternalRecoveryAuthorization(
|
|
approved,
|
|
[
|
|
trustedApprovers()[0],
|
|
{
|
|
userId: 'reviewer-b',
|
|
publicKey: generateKeyPairSync('ed25519').publicKey,
|
|
},
|
|
],
|
|
NOW + 3_000,
|
|
),
|
|
PluginPackagePromptOutputRecoveryAuthorizationUntrustedError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
verifyPluginPackagePromptOutputExternalRecoveryAuthorization(
|
|
approved,
|
|
trustedApprovers(),
|
|
NOW + 11 * 60_000,
|
|
),
|
|
PluginPackagePromptOutputRecoveryAuthorizationUntrustedError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
verifyAuthorizedPluginPackagePromptOutputRecoveredMaterial({
|
|
authorization: approved,
|
|
trustedApprovers: trustedApprovers(),
|
|
receipt: custody,
|
|
trustedCustodyPublicKey: custodyKeys.publicKey,
|
|
wrappedMaterial: WRAPPED,
|
|
durableKeyFact: {
|
|
keyId: custody.keyId,
|
|
materialProof: custody.materialProof,
|
|
catalogDigest: custody.sourceCatalogDigest,
|
|
},
|
|
material: MATERIAL,
|
|
artifact: { ...encrypted, artifactDigest: '4'.repeat(64) },
|
|
verifiedAtMs: NOW + 3_000,
|
|
}),
|
|
PluginPackagePromptOutputRecoveryAuthorizationUntrustedError,
|
|
);
|
|
});
|