mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 12:05:27 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
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 {
|
||||
LocalSecretMutationConflictError,
|
||||
LocalSecretUnavailableError,
|
||||
LocalSecretVersionConflictError,
|
||||
createLocalSecretRef,
|
||||
parseLocalSecretRef,
|
||||
} = require('@qinglong/runtime-core/local-secret');
|
||||
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
||||
const {
|
||||
openLocalSqliteRuntimeDatabase,
|
||||
} = require('@qinglong/local-sqlite/runtime');
|
||||
const {
|
||||
EncryptedLocalSecretService,
|
||||
LocalSecretKeyringConflictError,
|
||||
LocalSecretKeyringFileProvider,
|
||||
decryptLocalSecretEnvelopeToBuffer,
|
||||
encryptLocalSecretEnvelope,
|
||||
provisionLocalSecretKeyring,
|
||||
rotateLocalSecretKeyring,
|
||||
} = require('../dist');
|
||||
|
||||
const KEY = Buffer.alloc(32, 0x11);
|
||||
|
||||
function fixture(t, profile = 'edge') {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-secret-'));
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
return {
|
||||
directory,
|
||||
profile,
|
||||
databasePath: path.join(directory, 'qinglong3.sqlite'),
|
||||
keyringPath: path.join(directory, 'secret-keyring.json'),
|
||||
};
|
||||
}
|
||||
|
||||
function candidate(projectId = 'default') {
|
||||
return {
|
||||
runId: 'run-secret',
|
||||
attemptId: 'attempt-secret',
|
||||
projectId,
|
||||
taskId: 'task-secret',
|
||||
taskRevision: 'revision-secret',
|
||||
attemptNumber: 1,
|
||||
executorType: 'local_process',
|
||||
priority: 0,
|
||||
queuedAtMs: 1_760_000_000_000,
|
||||
attemptCreatedAtMs: 1_760_000_000_000,
|
||||
};
|
||||
}
|
||||
|
||||
async function store(t, profile = 'edge') {
|
||||
const value = fixture(t, profile);
|
||||
await migrateLocalSqlitePath(value);
|
||||
await provisionLocalSecretKeyring(value.keyringPath);
|
||||
const runtime = await openLocalSqliteRuntimeDatabase(value);
|
||||
t.after(() => runtime.close());
|
||||
const keys = new LocalSecretKeyringFileProvider(value.keyringPath);
|
||||
return {
|
||||
...value,
|
||||
runtime,
|
||||
keys,
|
||||
service: new EncryptedLocalSecretService(runtime.localSecrets, keys),
|
||||
};
|
||||
}
|
||||
|
||||
test('uses canonical Project-bound refs and metadata-authenticated AES-GCM', () => {
|
||||
const ref = createLocalSecretRef({
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
version: 2,
|
||||
});
|
||||
assert.deepEqual(parseLocalSecretRef(ref), {
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
version: 2,
|
||||
});
|
||||
const unknown = Buffer.from(
|
||||
JSON.stringify({ projectId: 'default', name: 'TOKEN', extra: true }),
|
||||
).toString('base64url');
|
||||
assert.throws(() => parseLocalSecretRef(`qlsecret:v1:${unknown}`));
|
||||
|
||||
const envelope = encryptLocalSecretEnvelope(
|
||||
{
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
version: 1,
|
||||
mutationId: 'create-token',
|
||||
keyId: 'edge-key-1',
|
||||
algorithm: 'aes-256-gcm',
|
||||
createdAtMs: 100,
|
||||
},
|
||||
'fixed-secret',
|
||||
KEY,
|
||||
() => Buffer.alloc(12, 0x22),
|
||||
);
|
||||
assert.deepEqual(
|
||||
{
|
||||
nonce: envelope.nonce,
|
||||
ciphertext: envelope.ciphertext,
|
||||
authTag: envelope.authTag,
|
||||
},
|
||||
{
|
||||
nonce: 'IiIiIiIiIiIiIiIi',
|
||||
ciphertext: 'cZ5_LKTi7DqGTbtI',
|
||||
authTag: '2af0_G7xHiOEBvMlsBSr0Q',
|
||||
},
|
||||
);
|
||||
const plaintext = decryptLocalSecretEnvelopeToBuffer(envelope, KEY);
|
||||
assert.equal(plaintext.toString('utf8'), 'fixed-secret');
|
||||
plaintext.fill(0);
|
||||
assert.throws(
|
||||
() =>
|
||||
decryptLocalSecretEnvelopeToBuffer(
|
||||
{ ...envelope, projectId: 'another-project' },
|
||||
KEY,
|
||||
),
|
||||
LocalSecretUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('provisions once, reloads active rotation and keeps historical keys', async (t) => {
|
||||
const value = fixture(t);
|
||||
const first = await provisionLocalSecretKeyring(value.keyringPath);
|
||||
assert.equal(first.keyIds.length, 1);
|
||||
assert.equal(fs.statSync(value.keyringPath).mode & 0o777, 0o600);
|
||||
await assert.rejects(
|
||||
provisionLocalSecretKeyring(value.keyringPath),
|
||||
LocalSecretUnavailableError,
|
||||
);
|
||||
|
||||
const provider = new LocalSecretKeyringFileProvider(value.keyringPath);
|
||||
const firstMaterial = await provider.active();
|
||||
assert.equal(firstMaterial.keyId, first.activeKeyId);
|
||||
firstMaterial.key.fill(0);
|
||||
const second = await rotateLocalSecretKeyring({
|
||||
filePath: value.keyringPath,
|
||||
expectedActiveKeyId: first.activeKeyId,
|
||||
});
|
||||
assert.equal(second.keyIds.length, 2);
|
||||
assert.notEqual(second.activeKeyId, first.activeKeyId);
|
||||
const activeMaterial = await provider.active();
|
||||
assert.equal(activeMaterial.keyId, second.activeKeyId);
|
||||
activeMaterial.key.fill(0);
|
||||
const historicalMaterial = await provider.resolve(first.activeKeyId);
|
||||
assert.equal(historicalMaterial.key.length, 32);
|
||||
historicalMaterial.key.fill(0);
|
||||
await assert.rejects(
|
||||
rotateLocalSecretKeyring({
|
||||
filePath: value.keyringPath,
|
||||
expectedActiveKeyId: first.activeKeyId,
|
||||
}),
|
||||
LocalSecretKeyringConflictError,
|
||||
);
|
||||
|
||||
fs.chmodSync(value.keyringPath, 0o644);
|
||||
await assert.rejects(provider.active(), LocalSecretUnavailableError);
|
||||
});
|
||||
|
||||
test('rejects keyring symlinks and preserves an existing rotation lock', async (t) => {
|
||||
const value = fixture(t);
|
||||
const first = await provisionLocalSecretKeyring(value.keyringPath);
|
||||
const symlinkPath = path.join(value.directory, 'secret-keyring-link.json');
|
||||
fs.symlinkSync(value.keyringPath, symlinkPath);
|
||||
const symlinkProvider = new LocalSecretKeyringFileProvider(symlinkPath);
|
||||
await assert.rejects(symlinkProvider.active(), LocalSecretUnavailableError);
|
||||
await assert.rejects(
|
||||
rotateLocalSecretKeyring({
|
||||
filePath: symlinkPath,
|
||||
expectedActiveKeyId: first.activeKeyId,
|
||||
}),
|
||||
LocalSecretUnavailableError,
|
||||
);
|
||||
|
||||
const lockPath = `${value.keyringPath}.lock`;
|
||||
fs.writeFileSync(lockPath, 'external-manager\n', { mode: 0o600, flag: 'wx' });
|
||||
await assert.rejects(
|
||||
rotateLocalSecretKeyring({
|
||||
filePath: value.keyringPath,
|
||||
expectedActiveKeyId: first.activeKeyId,
|
||||
}),
|
||||
LocalSecretUnavailableError,
|
||||
);
|
||||
assert.equal(fs.readFileSync(lockPath, 'utf8'), 'external-manager\n');
|
||||
assert.equal(
|
||||
(await new LocalSecretKeyringFileProvider(value.keyringPath).inspect())
|
||||
.activeKeyId,
|
||||
first.activeKeyId,
|
||||
);
|
||||
});
|
||||
|
||||
test('creates and rotates append-only ciphertext through the Node 24 SQLite authority', async (t) => {
|
||||
const value = await store(t);
|
||||
const firstPlaintext = 'first-plaintext-never-persisted';
|
||||
const secondPlaintext = 'second-plaintext-never-persisted';
|
||||
const first = await value.service.put({
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
plaintext: firstPlaintext,
|
||||
mutationId: 'create-token',
|
||||
expectedCurrentVersion: 0,
|
||||
createdAtMs: 100,
|
||||
});
|
||||
const rotatedKeyring = await rotateLocalSecretKeyring({
|
||||
filePath: value.keyringPath,
|
||||
expectedActiveKeyId: (await value.keys.inspect()).activeKeyId,
|
||||
});
|
||||
const second = await value.service.put({
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
plaintext: secondPlaintext,
|
||||
mutationId: 'rotate-token',
|
||||
expectedCurrentVersion: 1,
|
||||
createdAtMs: 200,
|
||||
});
|
||||
assert.equal(first.version, 1);
|
||||
assert.equal(second.version, 2);
|
||||
assert.equal(
|
||||
(await value.keys.inspect()).activeKeyId,
|
||||
rotatedKeyring.activeKeyId,
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
await value.service.resolveLocalSecretEnvironment({
|
||||
candidate: candidate(),
|
||||
secretRefs: [
|
||||
createLocalSecretRef({ projectId: 'default', name: 'TOKEN' }),
|
||||
first.secretRef,
|
||||
second.secretRef,
|
||||
],
|
||||
}),
|
||||
[secondPlaintext, firstPlaintext, secondPlaintext],
|
||||
);
|
||||
const material = await value.service.resolveProjectSecretMaterial({
|
||||
projectId: 'default',
|
||||
secretRef: createLocalSecretRef({
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
}),
|
||||
});
|
||||
assert.ok(material);
|
||||
assert.equal(material.secretRef.includes(secondPlaintext), false);
|
||||
assert.equal(Buffer.from(material.bytes).toString('utf8'), secondPlaintext);
|
||||
const ownedBytes = material.bytes;
|
||||
await material.dispose();
|
||||
assert.deepEqual([...ownedBytes], new Array(ownedBytes.length).fill(0));
|
||||
await material.dispose();
|
||||
|
||||
const database = new DatabaseSync(value.databasePath, { readOnly: true });
|
||||
try {
|
||||
const rows = database
|
||||
.prepare(
|
||||
`SELECT ciphertext, key_id FROM "QingLong3LocalSecretEnvelopes"
|
||||
ORDER BY version`,
|
||||
)
|
||||
.all();
|
||||
assert.equal(rows.length, 2);
|
||||
assert.notEqual(rows[0].key_id, rows[1].key_id);
|
||||
assert.equal(JSON.stringify(rows).includes(firstPlaintext), false);
|
||||
assert.equal(JSON.stringify(rows).includes(secondPlaintext), false);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('replays semantic mutations, fences stale versions and checks Project before storage', async (t) => {
|
||||
const value = await store(t, 'standalone');
|
||||
const command = {
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
plaintext: 'same-value',
|
||||
mutationId: 'create-token',
|
||||
expectedCurrentVersion: 0,
|
||||
createdAtMs: 100,
|
||||
};
|
||||
assert.equal((await value.service.put(command)).status, 'inserted');
|
||||
assert.equal(
|
||||
(await value.service.put({ ...command, createdAtMs: 999 })).status,
|
||||
'existing',
|
||||
);
|
||||
await assert.rejects(
|
||||
value.service.put({ ...command, plaintext: 'different-value' }),
|
||||
LocalSecretMutationConflictError,
|
||||
);
|
||||
await assert.rejects(
|
||||
value.service.put({ ...command, mutationId: 'stale-create' }),
|
||||
LocalSecretVersionConflictError,
|
||||
);
|
||||
await assert.rejects(
|
||||
value.service.resolveLocalSecretEnvironment({
|
||||
candidate: candidate('default'),
|
||||
secretRefs: [
|
||||
createLocalSecretRef({ projectId: 'another', name: 'TOKEN' }),
|
||||
],
|
||||
}),
|
||||
LocalSecretUnavailableError,
|
||||
);
|
||||
await assert.rejects(
|
||||
value.service.resolveProjectSecretMaterial({
|
||||
projectId: 'default',
|
||||
secretRef: createLocalSecretRef({
|
||||
projectId: 'another',
|
||||
name: 'TOKEN',
|
||||
}),
|
||||
}),
|
||||
LocalSecretUnavailableError,
|
||||
);
|
||||
});
|
||||
|
||||
test('two SQLite authorities allow exactly one rotation for one expected version', async (t) => {
|
||||
const value = fixture(t, 'standalone');
|
||||
await migrateLocalSqlitePath(value);
|
||||
await provisionLocalSecretKeyring(value.keyringPath);
|
||||
const firstRuntime = await openLocalSqliteRuntimeDatabase(value);
|
||||
const secondRuntime = await openLocalSqliteRuntimeDatabase(value);
|
||||
t.after(() => Promise.all([firstRuntime.close(), secondRuntime.close()]));
|
||||
const keys = new LocalSecretKeyringFileProvider(value.keyringPath);
|
||||
const first = new EncryptedLocalSecretService(
|
||||
firstRuntime.localSecrets,
|
||||
keys,
|
||||
);
|
||||
const second = new EncryptedLocalSecretService(
|
||||
secondRuntime.localSecrets,
|
||||
keys,
|
||||
);
|
||||
await first.put({
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
plaintext: 'initial',
|
||||
mutationId: 'initial',
|
||||
expectedCurrentVersion: 0,
|
||||
createdAtMs: 1,
|
||||
});
|
||||
const results = await Promise.allSettled([
|
||||
first.put({
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
plaintext: 'winner-a',
|
||||
mutationId: 'rotate-a',
|
||||
expectedCurrentVersion: 1,
|
||||
createdAtMs: 2,
|
||||
}),
|
||||
second.put({
|
||||
projectId: 'default',
|
||||
name: 'TOKEN',
|
||||
plaintext: 'winner-b',
|
||||
mutationId: 'rotate-b',
|
||||
expectedCurrentVersion: 1,
|
||||
createdAtMs: 2,
|
||||
}),
|
||||
]);
|
||||
assert.equal(
|
||||
results.filter(({ status }) => status === 'fulfilled').length,
|
||||
1,
|
||||
);
|
||||
assert.equal(results.filter(({ status }) => status === 'rejected').length, 1);
|
||||
assert.equal(
|
||||
results.find(({ status }) => status === 'rejected').reason.constructor,
|
||||
LocalSecretVersionConflictError,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user