mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
319 lines
9.2 KiB
JavaScript
319 lines
9.2 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { createHash } = require('node:crypto');
|
|
const { spawnSync } = require('node:child_process');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { test } = require('node:test');
|
|
const {
|
|
openLocalSqliteBootstrapDatabase,
|
|
} = require('@qinglong/local-sqlite/bootstrap');
|
|
const { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
|
const {
|
|
LocalOwnerCliConfigurationError,
|
|
createLocalOwnerCommandRunner,
|
|
} = require('@qinglong/local-owner-cli');
|
|
|
|
async function fixture(t) {
|
|
const deploymentRoot = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'ql3-owner-cli-'),
|
|
);
|
|
fs.chmodSync(deploymentRoot, 0o700);
|
|
t.after(() => fs.rmSync(deploymentRoot, { recursive: true, force: true }));
|
|
const databasePath = path.join(deploymentRoot, 'qinglong3.sqlite');
|
|
const pepperPath = path.join(deploymentRoot, 'owner.pepper');
|
|
const secretDeliveryDirectory = path.join(deploymentRoot, 'secrets');
|
|
const commandsDirectory = path.join(deploymentRoot, 'commands');
|
|
fs.mkdirSync(secretDeliveryDirectory, { mode: 0o700 });
|
|
fs.mkdirSync(commandsDirectory, { mode: 0o700 });
|
|
fs.writeFileSync(pepperPath, Buffer.alloc(32, 83).toString('base64url'), {
|
|
mode: 0o600,
|
|
});
|
|
const options = {
|
|
deploymentRoot,
|
|
databasePath,
|
|
pepperPath,
|
|
pepperKeyId: 'legacy-v1',
|
|
secretDeliveryDirectory,
|
|
profile: 'edge',
|
|
};
|
|
await migrateLocalSqlitePath(options);
|
|
const material = fs.readFileSync(pepperPath);
|
|
const materialDigest = createHash('sha256')
|
|
.update('qinglong.local-owner-pepper.summary.v1\0', 'utf8')
|
|
.update(material)
|
|
.digest('hex');
|
|
material.fill(0);
|
|
const database = await openLocalSqliteBootstrapDatabase(options);
|
|
await database.ownerPepper.register({
|
|
mutationId: '00000000-0000-4000-8000-000000000c91',
|
|
pepperKeyId: 'legacy-v1',
|
|
materialDigest,
|
|
backupDigest: 'b'.repeat(64),
|
|
registeredAtMs: 1,
|
|
});
|
|
await database.ownerPepper.activate({
|
|
mutationId: '00000000-0000-4000-8000-000000000c92',
|
|
pepperKeyId: 'legacy-v1',
|
|
expectedGeneration: 0,
|
|
activatedAtMs: 2,
|
|
});
|
|
await database.close();
|
|
return { options, commandsDirectory };
|
|
}
|
|
|
|
function commandFile(state, operation, request, suffix) {
|
|
const filePath = path.join(state.commandsDirectory, `${suffix}.json`);
|
|
fs.writeFileSync(
|
|
filePath,
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
operation,
|
|
options: state.options,
|
|
request,
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
return filePath;
|
|
}
|
|
|
|
function assertNoSecretFields(value) {
|
|
if (!value || typeof value !== 'object') return;
|
|
for (const [key, nested] of Object.entries(value)) {
|
|
assert.doesNotMatch(key, /secret|token/i);
|
|
assertNoSecretFields(nested);
|
|
}
|
|
}
|
|
|
|
test('completes fresh Owner and credential recovery ceremonies without returning secrets', async (t) => {
|
|
const state = await fixture(t);
|
|
const runner = createLocalOwnerCommandRunner();
|
|
const credentialMutationId = '00000000-0000-4000-8000-000000000c01';
|
|
const challengeMutationId = '00000000-0000-4000-8000-000000000c02';
|
|
const claimMutationId = '00000000-0000-4000-8000-000000000c03';
|
|
const provisioned = await runner.run(
|
|
commandFile(
|
|
state,
|
|
'owner.identity.provision',
|
|
{
|
|
mutationId: credentialMutationId,
|
|
requestId: 'owner-cli-provision-c01',
|
|
},
|
|
'01-provision',
|
|
),
|
|
);
|
|
assert.equal(provisioned.status, 'inserted');
|
|
assert.equal(provisioned.delivery.kind, 'credential');
|
|
const issued = await runner.run(
|
|
commandFile(
|
|
state,
|
|
'owner.challenge.issue',
|
|
{
|
|
projectId: 'default',
|
|
mutationId: challengeMutationId,
|
|
requestId: 'owner-cli-issue-c02',
|
|
},
|
|
'02-issue',
|
|
),
|
|
);
|
|
assert.equal(issued.delivery.kind, 'challenge');
|
|
const inspected = await runner.run(
|
|
commandFile(
|
|
state,
|
|
'owner.delivery.inspect',
|
|
{ kind: 'challenge', mutationId: challengeMutationId },
|
|
'03-inspect',
|
|
),
|
|
);
|
|
assert.equal(
|
|
inspected.delivery.deliveryDigest,
|
|
issued.delivery.deliveryDigest,
|
|
);
|
|
const claimFile = commandFile(
|
|
state,
|
|
'owner.claim.from-deliveries',
|
|
{
|
|
projectId: 'default',
|
|
mutationId: claimMutationId,
|
|
requestId: 'owner-cli-claim-c03',
|
|
credentialMutationId,
|
|
challengeMutationId,
|
|
},
|
|
'04-claim',
|
|
);
|
|
const claimed = await runner.run(claimFile);
|
|
assert.equal(claimed.status, 'inserted');
|
|
assert.equal(claimed.role, 'owner');
|
|
assert.equal(JSON.stringify(claimed).includes('secret'), false);
|
|
const credentialFilePath = path.join(
|
|
state.options.deploymentRoot,
|
|
'owner-credential.json',
|
|
);
|
|
const installed = await runner.run(
|
|
commandFile(
|
|
state,
|
|
'owner.credential-presentation.install-from-delivery',
|
|
{ credentialMutationId, destinationFilePath: credentialFilePath },
|
|
'04b-install-presentation',
|
|
),
|
|
);
|
|
assert.deepEqual(installed, {
|
|
schemaVersion: 1,
|
|
operation: 'owner.credential-presentation.install-from-delivery',
|
|
status: 'installed',
|
|
credentialMutationId,
|
|
});
|
|
const presentation = JSON.parse(fs.readFileSync(credentialFilePath, 'utf8'));
|
|
assert.equal(
|
|
presentation.kind,
|
|
'qinglong3-local-identity-credential-presentation',
|
|
);
|
|
assert.match(
|
|
presentation.token,
|
|
/^ql3c_own_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{43}$/,
|
|
);
|
|
assert.equal(fs.statSync(credentialFilePath).mode & 0o777, 0o600);
|
|
const replayedInstall = await runner.run(
|
|
commandFile(
|
|
state,
|
|
'owner.credential-presentation.install-from-delivery',
|
|
{ credentialMutationId, destinationFilePath: credentialFilePath },
|
|
'04c-replay-presentation',
|
|
),
|
|
);
|
|
assert.equal(replayedInstall.status, 'existing');
|
|
assertNoSecretFields(installed);
|
|
assertNoSecretFields(replayedInstall);
|
|
const escapedDestination = path.join(
|
|
os.tmpdir(),
|
|
`ql3-owner-credential-escape-${process.pid}.json`,
|
|
);
|
|
await assert.rejects(
|
|
runner.run(
|
|
commandFile(
|
|
state,
|
|
'owner.credential-presentation.install-from-delivery',
|
|
{
|
|
credentialMutationId,
|
|
destinationFilePath: escapedDestination,
|
|
},
|
|
'04d-reject-escaped-presentation',
|
|
),
|
|
),
|
|
/destinationFilePath must be a descendant of deploymentRoot/,
|
|
);
|
|
assert.equal(fs.existsSync(escapedDestination), false);
|
|
for (const [purpose, mutationId, digest, suffix] of [
|
|
[
|
|
'credential-provisioning',
|
|
credentialMutationId,
|
|
provisioned.delivery.deliveryDigest,
|
|
'05-ack-credential',
|
|
],
|
|
[
|
|
'challenge',
|
|
challengeMutationId,
|
|
issued.delivery.deliveryDigest,
|
|
'06-ack-challenge',
|
|
],
|
|
]) {
|
|
const acknowledged = await runner.run(
|
|
commandFile(
|
|
state,
|
|
'owner.delivery.acknowledge',
|
|
{ purpose, mutationId, expectedDeliveryDigest: digest },
|
|
suffix,
|
|
),
|
|
);
|
|
assert.equal(acknowledged.mutationId, mutationId);
|
|
}
|
|
const replay = await runner.run(claimFile);
|
|
assert.equal(replay.status, 'existing');
|
|
const recoveryMutationId = '00000000-0000-4000-8000-000000000c04';
|
|
const recovery = await runner.run(
|
|
commandFile(
|
|
state,
|
|
'owner.credential-recovery.issue',
|
|
{
|
|
mutationId: recoveryMutationId,
|
|
requestId: 'owner-cli-recovery-c04',
|
|
previousCredentialId: provisioned.credentialId,
|
|
expectedPreviousVersion: 1,
|
|
},
|
|
'07-recovery-issue',
|
|
),
|
|
);
|
|
assert.equal(recovery.state, 'issued');
|
|
assert.equal(recovery.delivery.kind, 'credential');
|
|
await runner.run(
|
|
commandFile(
|
|
state,
|
|
'owner.delivery.acknowledge',
|
|
{
|
|
purpose: 'credential-recovery',
|
|
mutationId: recoveryMutationId,
|
|
expectedDeliveryDigest: recovery.delivery.deliveryDigest,
|
|
},
|
|
'08-recovery-ack',
|
|
),
|
|
);
|
|
const completed = await runner.run(
|
|
commandFile(
|
|
state,
|
|
'owner.credential-recovery.complete',
|
|
{
|
|
issueMutationId: recoveryMutationId,
|
|
mutationId: '00000000-0000-4000-8000-000000000c05',
|
|
requestId: 'owner-cli-recovery-complete-c05',
|
|
},
|
|
'09-recovery-complete',
|
|
),
|
|
);
|
|
assert.equal(completed.state, 'completed');
|
|
for (const result of [
|
|
provisioned,
|
|
issued,
|
|
inspected,
|
|
claimed,
|
|
recovery,
|
|
completed,
|
|
]) {
|
|
assertNoSecretFields(result);
|
|
}
|
|
});
|
|
|
|
test('rejects widened command intent and exposes only a command-file binary', async (t) => {
|
|
const state = await fixture(t);
|
|
const widened = commandFile(
|
|
state,
|
|
'owner.delivery.inspect',
|
|
{
|
|
kind: 'credential',
|
|
mutationId: '00000000-0000-4000-8000-000000000d01',
|
|
secret: 'forbidden',
|
|
},
|
|
'widened',
|
|
);
|
|
await assert.rejects(
|
|
createLocalOwnerCommandRunner().run(widened),
|
|
LocalOwnerCliConfigurationError,
|
|
);
|
|
const help = spawnSync(
|
|
process.execPath,
|
|
[path.join(__dirname, '../dist/cli.js'), '--help'],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(help.status, 0);
|
|
assert.match(help.stdout, /^Usage: ql3-owner run --command-file /);
|
|
const invalid = spawnSync(
|
|
process.execPath,
|
|
[path.join(__dirname, '../dist/cli.js'), 'run'],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
assert.equal(invalid.status, 64);
|
|
assert.equal(
|
|
JSON.parse(invalid.stderr).code,
|
|
'LOCAL_OWNER_CLI_USAGE_INVALID',
|
|
);
|
|
});
|