Files
qinglong/packages/ql3-local-owner-maintenance/test/pepperGc.test.cjs
T

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,
);
});