mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
294 lines
9.4 KiB
JavaScript
294 lines
9.4 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
const { test } = require('node:test');
|
|
const {
|
|
LocalOwnerPepperKeyringFileProvider,
|
|
localOwnerPepperKeyPath,
|
|
provisionLocalOwnerPepperKey,
|
|
} = require('@qinglong/local-owner-console/pepper-custody');
|
|
const {
|
|
destroyLocalOwnerPepperKey,
|
|
} = require('@qinglong/local-owner-console/pepper-custody/destructive');
|
|
const {
|
|
openLocalSqlitePepperGcDatabase,
|
|
} = require('@qinglong/local-sqlite/pepper-gc');
|
|
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
|
const {
|
|
MIN_LOCAL_OWNER_PEPPER_ACK_RETENTION_MS,
|
|
MIN_LOCAL_OWNER_PEPPER_AUDIT_RETENTION_MS,
|
|
MIN_LOCAL_OWNER_PEPPER_BACKUP_RETENTION_MS,
|
|
} = require('@qinglong/runtime-core/local-owner-pepper-material-gc');
|
|
const {
|
|
LocalOwnerPepperMaterialGcMaterialUnavailableError,
|
|
openLocalOwnerPepperMaterialGc,
|
|
} = require('../dist/security-maintenance/pepperGc');
|
|
|
|
const RETIRED_KEY_ID = 'owner-key-retired';
|
|
const ACTIVE_KEY_ID = 'owner-key-active';
|
|
const REQUESTED_AT_MS = 3_000_000_000;
|
|
|
|
function policy() {
|
|
return {
|
|
version: 1,
|
|
acknowledgementRetentionMs: MIN_LOCAL_OWNER_PEPPER_ACK_RETENTION_MS,
|
|
auditRetentionMs: MIN_LOCAL_OWNER_PEPPER_AUDIT_RETENTION_MS,
|
|
backupRetentionMs: MIN_LOCAL_OWNER_PEPPER_BACKUP_RETENTION_MS,
|
|
};
|
|
}
|
|
|
|
function request() {
|
|
return {
|
|
prepareMutationId: '00000000-0000-4000-8000-000000000801',
|
|
prepareRequestId: 'pepper-gc-prepare',
|
|
completeMutationId: '00000000-0000-4000-8000-000000000802',
|
|
completeRequestId: 'pepper-gc-complete',
|
|
pepperKeyId: RETIRED_KEY_ID,
|
|
};
|
|
}
|
|
|
|
function audit(eventId, requestId, operation) {
|
|
return {
|
|
eventId,
|
|
requestId,
|
|
operationId: `owner.pepper.material_gc.${operation}`,
|
|
projectId: null,
|
|
subject: { type: 'system', id: 'owner-pepper-gc' },
|
|
authenticationId: 'local-owner-console',
|
|
outcome: 'allowed',
|
|
reasons: ['pepper_material_gc'],
|
|
fence: null,
|
|
occurredAtMs: REQUESTED_AT_MS,
|
|
};
|
|
}
|
|
|
|
async function fixture(t) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-pepper-gc-e2e-'));
|
|
const keyringDirectory = path.join(root, 'keyring');
|
|
const backupDirectory = path.join(root, 'backup');
|
|
const databasePath = path.join(root, 'qinglong3.sqlite');
|
|
fs.mkdirSync(keyringDirectory, { mode: 0o700 });
|
|
fs.mkdirSync(backupDirectory, { mode: 0o700 });
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const retiredRuntime = provisionLocalOwnerPepperKey({
|
|
keyringDirectory,
|
|
pepperKeyId: RETIRED_KEY_ID,
|
|
randomBytes: () => Buffer.alloc(32, 41),
|
|
});
|
|
const retiredBackup = provisionLocalOwnerPepperKey({
|
|
keyringDirectory: backupDirectory,
|
|
pepperKeyId: RETIRED_KEY_ID,
|
|
randomBytes: () => Buffer.alloc(32, 41),
|
|
});
|
|
const activeRuntime = provisionLocalOwnerPepperKey({
|
|
keyringDirectory,
|
|
pepperKeyId: ACTIVE_KEY_ID,
|
|
randomBytes: () => Buffer.alloc(32, 43),
|
|
});
|
|
const activeBackup = provisionLocalOwnerPepperKey({
|
|
keyringDirectory: backupDirectory,
|
|
pepperKeyId: ACTIVE_KEY_ID,
|
|
randomBytes: () => Buffer.alloc(32, 43),
|
|
});
|
|
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
|
|
const client = new DatabaseSync(databasePath);
|
|
client.exec('PRAGMA foreign_keys = ON');
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalOwnerPepperKeys" (
|
|
pepper_key_id, material_digest, backup_digest, state, version,
|
|
register_mutation_id, activate_mutation_id, retire_mutation_id,
|
|
registered_at_ms, activated_at_ms, retired_at_ms
|
|
) VALUES (?, ?, ?, 'retired', 3, ?, ?, ?, 10, 50, 100)`,
|
|
)
|
|
.run(
|
|
RETIRED_KEY_ID,
|
|
retiredRuntime.digest,
|
|
retiredBackup.digest,
|
|
'00000000-0000-4000-8000-000000000811',
|
|
'00000000-0000-4000-8000-000000000812',
|
|
'00000000-0000-4000-8000-000000000813',
|
|
);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalOwnerPepperKeys" (
|
|
pepper_key_id, material_digest, backup_digest, state, version,
|
|
register_mutation_id, activate_mutation_id, registered_at_ms,
|
|
activated_at_ms
|
|
) VALUES (?, ?, ?, 'active', 2, ?, ?, 60, 100)`,
|
|
)
|
|
.run(
|
|
ACTIVE_KEY_ID,
|
|
activeRuntime.digest,
|
|
activeBackup.digest,
|
|
'00000000-0000-4000-8000-000000000821',
|
|
'00000000-0000-4000-8000-000000000822',
|
|
);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalOwnerPepperActivations" (
|
|
generation, mutation_id, expected_generation,
|
|
previous_pepper_key_id, active_pepper_key_id,
|
|
material_digest, backup_digest, activated_at_ms
|
|
) VALUES (1, ?, 0, NULL, ?, ?, ?, 50)`,
|
|
)
|
|
.run(
|
|
'00000000-0000-4000-8000-000000000812',
|
|
RETIRED_KEY_ID,
|
|
retiredRuntime.digest,
|
|
retiredBackup.digest,
|
|
);
|
|
client
|
|
.prepare(
|
|
`INSERT INTO "QingLong3LocalOwnerPepperActivations" (
|
|
generation, mutation_id, expected_generation,
|
|
previous_pepper_key_id, active_pepper_key_id,
|
|
material_digest, backup_digest, activated_at_ms
|
|
) VALUES (2, ?, 1, ?, ?, ?, ?, 100)`,
|
|
)
|
|
.run(
|
|
'00000000-0000-4000-8000-000000000822',
|
|
RETIRED_KEY_ID,
|
|
ACTIVE_KEY_ID,
|
|
activeRuntime.digest,
|
|
activeBackup.digest,
|
|
);
|
|
client.close();
|
|
return {
|
|
databasePath,
|
|
keyringDirectory,
|
|
backupDirectory,
|
|
retiredRuntime,
|
|
retiredBackup,
|
|
activeRuntime,
|
|
};
|
|
}
|
|
|
|
function openOptions(fixture) {
|
|
return {
|
|
databasePath: fixture.databasePath,
|
|
profile: 'edge',
|
|
keyringDirectory: fixture.keyringDirectory,
|
|
backupDirectory: fixture.backupDirectory,
|
|
retentionPolicy: policy(),
|
|
};
|
|
}
|
|
|
|
test('destroys runtime and backup material then replays from durable absence', async (t) => {
|
|
const prepared = await fixture(t);
|
|
const authority = await openLocalOwnerPepperMaterialGc(openOptions(prepared));
|
|
t.after(() => authority.close());
|
|
const first = await authority.collect(request());
|
|
assert.equal(first.status, 'inserted');
|
|
assert.equal(first.record.state, 'completed');
|
|
assert.equal(first.runtimeMaterial.status, 'destroyed');
|
|
assert.equal(first.backupMaterial.status, 'destroyed');
|
|
assert.equal(
|
|
fs.existsSync(
|
|
localOwnerPepperKeyPath(prepared.keyringDirectory, RETIRED_KEY_ID),
|
|
),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(
|
|
localOwnerPepperKeyPath(prepared.backupDirectory, RETIRED_KEY_ID),
|
|
),
|
|
false,
|
|
);
|
|
assert.ok(
|
|
new LocalOwnerPepperKeyringFileProvider(prepared.keyringDirectory).resolve(
|
|
ACTIVE_KEY_ID,
|
|
),
|
|
);
|
|
|
|
const replay = await authority.collect(request());
|
|
assert.equal(replay.status, 'existing');
|
|
assert.equal(replay.runtimeMaterial.status, 'absent');
|
|
assert.equal(replay.backupMaterial.status, 'absent');
|
|
assert.equal(
|
|
replay.record.destructionProofDigest,
|
|
first.record.destructionProofDigest,
|
|
);
|
|
});
|
|
|
|
test('recovers after runtime deletion but before backup deletion and completion', async (t) => {
|
|
const prepared = await fixture(t);
|
|
const command = request();
|
|
const database = await openLocalSqlitePepperGcDatabase({
|
|
databasePath: prepared.databasePath,
|
|
profile: 'edge',
|
|
});
|
|
await database.materialGc.prepare({
|
|
mutationId: command.prepareMutationId,
|
|
requestId: command.prepareRequestId,
|
|
pepperKeyId: command.pepperKeyId,
|
|
expectedMaterialDigest: prepared.retiredRuntime.digest,
|
|
expectedBackupMaterialDigest: prepared.retiredBackup.digest,
|
|
expectedActivePepperKeyId: ACTIVE_KEY_ID,
|
|
expectedActiveGeneration: 2,
|
|
expectedActiveMaterialDigest: prepared.activeRuntime.digest,
|
|
retentionPolicy: policy(),
|
|
preparedAtMs: REQUESTED_AT_MS,
|
|
audit: audit(
|
|
command.prepareMutationId,
|
|
command.prepareRequestId,
|
|
'prepare',
|
|
),
|
|
});
|
|
await database.close();
|
|
destroyLocalOwnerPepperKey({
|
|
keyringDirectory: prepared.keyringDirectory,
|
|
pepperKeyId: RETIRED_KEY_ID,
|
|
materialRole: 'runtime',
|
|
expectedMaterialDigest: prepared.retiredRuntime.digest,
|
|
prepareMutationId: command.prepareMutationId,
|
|
});
|
|
|
|
const authority = await openLocalOwnerPepperMaterialGc(openOptions(prepared));
|
|
t.after(() => authority.close());
|
|
const recovered = await authority.collect(command);
|
|
assert.equal(recovered.record.state, 'completed');
|
|
assert.equal(recovered.runtimeMaterial.status, 'absent');
|
|
assert.equal(recovered.backupMaterial.status, 'destroyed');
|
|
});
|
|
|
|
test('fails before prepare when the active independent backup is missing', async (t) => {
|
|
const prepared = await fixture(t);
|
|
fs.unlinkSync(
|
|
localOwnerPepperKeyPath(prepared.backupDirectory, ACTIVE_KEY_ID),
|
|
);
|
|
const authority = await openLocalOwnerPepperMaterialGc(openOptions(prepared));
|
|
t.after(() => authority.close());
|
|
await assert.rejects(
|
|
authority.collect(request()),
|
|
LocalOwnerPepperMaterialGcMaterialUnavailableError,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(
|
|
localOwnerPepperKeyPath(prepared.keyringDirectory, RETIRED_KEY_ID),
|
|
),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test('rejects caller-controlled time before opening a destructive operation', async (t) => {
|
|
const prepared = await fixture(t);
|
|
const authority = await openLocalOwnerPepperMaterialGc(openOptions(prepared));
|
|
t.after(() => authority.close());
|
|
await assert.rejects(
|
|
authority.collect({
|
|
...request(),
|
|
requestedAtMs: Number.MAX_SAFE_INTEGER,
|
|
}),
|
|
/request shape is invalid/,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(
|
|
localOwnerPepperKeyPath(prepared.keyringDirectory, RETIRED_KEY_ID),
|
|
),
|
|
true,
|
|
);
|
|
});
|