mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
811 lines
28 KiB
JavaScript
811 lines
28 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { createHash, randomUUID } = require('node:crypto');
|
|
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 { migrateLocalSqlitePath } = require('@qinglong/local-sqlite/migration');
|
|
const {
|
|
openLocalSqliteBootstrapDatabase,
|
|
} = require('@qinglong/local-sqlite/bootstrap');
|
|
const {
|
|
createLocalOwnerBootstrapService,
|
|
LOCAL_IDENTITY_BOOTSTRAP_DEFAULT_TTL_MS,
|
|
LocalOwnerBootstrapServiceUnavailableError,
|
|
} = require('../dist/bootstrap');
|
|
const {
|
|
formatApiCredentialToken,
|
|
} = require('@qinglong/runtime-core/api-credential-token');
|
|
const {
|
|
FileLocalOwnerBootstrapSecretDelivery,
|
|
LocalOwnerConsoleConfigurationError,
|
|
LocalOwnerSecretDeliveryError,
|
|
openLocalOwnerConsole,
|
|
} = require('@qinglong/local-owner-console');
|
|
|
|
function fixture(t) {
|
|
const deploymentRoot = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'ql3-owner-console-'),
|
|
);
|
|
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');
|
|
fs.mkdirSync(secretDeliveryDirectory, { mode: 0o700 });
|
|
fs.writeFileSync(pepperPath, Buffer.alloc(32, 73).toString('base64url'), {
|
|
mode: 0o600,
|
|
});
|
|
return {
|
|
deploymentRoot,
|
|
databasePath,
|
|
pepperPath,
|
|
secretDeliveryDirectory,
|
|
profile: 'edge',
|
|
};
|
|
}
|
|
|
|
async function ready(t) {
|
|
const options = fixture(t);
|
|
await migrateLocalSqlitePath({
|
|
databasePath: options.databasePath,
|
|
profile: options.profile,
|
|
});
|
|
const material = fs.readFileSync(options.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({
|
|
databasePath: options.databasePath,
|
|
profile: options.profile,
|
|
});
|
|
await database.ownerPepper.register({
|
|
mutationId: '00000000-0000-4000-8000-000000000191',
|
|
pepperKeyId: 'legacy-v1',
|
|
materialDigest,
|
|
backupDigest: 'b'.repeat(64),
|
|
registeredAtMs: 1,
|
|
});
|
|
await database.ownerPepper.activate({
|
|
mutationId: '00000000-0000-4000-8000-000000000192',
|
|
pepperKeyId: 'legacy-v1',
|
|
expectedGeneration: 0,
|
|
activatedAtMs: 2,
|
|
});
|
|
await database.close();
|
|
return options;
|
|
}
|
|
|
|
function authorityFor(options) {
|
|
const root = fs.lstatSync(options.deploymentRoot, { bigint: true });
|
|
const uid = process.getuid();
|
|
const proofDigest = createHash('sha256')
|
|
.update('qinglong.local-owner-console.proof.v1\0', 'utf8')
|
|
.update(process.platform, 'utf8')
|
|
.update('\0', 'utf8')
|
|
.update(String(uid), 'utf8')
|
|
.update('\0', 'utf8')
|
|
.update(root.dev.toString(), 'utf8')
|
|
.update('\0', 'utf8')
|
|
.update(root.ino.toString(), 'utf8')
|
|
.digest('hex');
|
|
const authenticatedAtMs = Date.now();
|
|
return {
|
|
subject: { type: 'system', id: 'owner-bootstrap' },
|
|
authenticationId: `local-console:${proofDigest}`,
|
|
authenticatedAtMs,
|
|
expiresAtMs: authenticatedAtMs + 60_000,
|
|
assurance: 'local_console',
|
|
};
|
|
}
|
|
|
|
test('proves one bounded delivery crash bridge is clear', (t) => {
|
|
const options = fixture(t);
|
|
const delivery = new FileLocalOwnerBootstrapSecretDelivery(
|
|
options.secretDeliveryDirectory,
|
|
);
|
|
const mutationId = '00000000-0000-4000-8000-000000000b01';
|
|
const evidence = delivery.inspectBridgeClear('credential', mutationId);
|
|
assert.equal(evidence.kind, 'credential');
|
|
assert.equal(evidence.acknowledgementMutationId, mutationId);
|
|
assert.match(evidence.evidenceDigest, /^[0-9a-f]{64}$/);
|
|
|
|
fs.writeFileSync(
|
|
path.join(
|
|
options.secretDeliveryDirectory,
|
|
`credential-${mutationId}.pending.json`,
|
|
),
|
|
'{}',
|
|
{ mode: 0o600 },
|
|
);
|
|
assert.throws(
|
|
() => delivery.inspectBridgeClear('credential', mutationId),
|
|
/crash bridge is not clear/,
|
|
);
|
|
});
|
|
|
|
async function directService(options, secretDelivery) {
|
|
const database = await openLocalSqliteBootstrapDatabase({
|
|
databasePath: options.databasePath,
|
|
profile: options.profile,
|
|
});
|
|
const service = createLocalOwnerBootstrapService(
|
|
database.ownerBootstrap,
|
|
database.apiCredentials,
|
|
fs.readFileSync(options.pepperPath, 'utf8'),
|
|
authorityFor(options),
|
|
{ secretDelivery },
|
|
);
|
|
return { database, service };
|
|
}
|
|
|
|
test('binds POSIX proof at composition time and removes issuer from requests', async (t) => {
|
|
const options = await ready(t);
|
|
const console = await openLocalOwnerConsole(options);
|
|
t.after(() => console.close());
|
|
assert.deepEqual(console.recovery, {
|
|
inspectedPendingRecords: 0,
|
|
publishedRecords: 0,
|
|
retainedUncommittedRecords: 0,
|
|
orphanTemporaryRecords: 0,
|
|
});
|
|
const provisioned = await console.service.provision({
|
|
mutationId: '00000000-0000-4000-8000-000000000201',
|
|
requestId: 'console-provision-201',
|
|
});
|
|
assert.equal(provisioned.status, 'inserted');
|
|
assert.equal(provisioned.credentialToken, null);
|
|
const credentialPath = console.credentialDeliveryPath(
|
|
'00000000-0000-4000-8000-000000000201',
|
|
);
|
|
assert.equal(fs.statSync(credentialPath).mode & 0o777, 0o600);
|
|
const credential = JSON.parse(fs.readFileSync(credentialPath, 'utf8'));
|
|
assert.equal(credential.kind, 'credential');
|
|
assert.equal(credential.credentialId, provisioned.credentialId);
|
|
|
|
const issued = await console.service.issue({
|
|
projectId: 'default',
|
|
mutationId: '00000000-0000-4000-8000-000000000202',
|
|
requestId: 'console-issue-202',
|
|
});
|
|
assert.equal(issued.status, 'inserted');
|
|
assert.equal(issued.challengeToken, null);
|
|
const challengePath = console.challengeDeliveryPath(
|
|
'00000000-0000-4000-8000-000000000202',
|
|
);
|
|
assert.equal(fs.statSync(challengePath).mode & 0o777, 0o600);
|
|
const challenge = JSON.parse(fs.readFileSync(challengePath, 'utf8'));
|
|
assert.equal(challenge.kind, 'challenge');
|
|
assert.equal(challenge.challengeId, issued.challengeId);
|
|
|
|
const claimed = await console.service.claim({
|
|
projectId: 'default',
|
|
mutationId: '00000000-0000-4000-8000-000000000205',
|
|
requestId: 'console-claim-205',
|
|
challengeId: challenge.challengeId,
|
|
challengeToken: challenge.secret,
|
|
credentialToken: formatApiCredentialToken(
|
|
credential.credentialId,
|
|
credential.secret,
|
|
),
|
|
});
|
|
assert.equal(claimed.status, 'inserted');
|
|
const databaseBytes = fs.readFileSync(options.databasePath);
|
|
assert.equal(databaseBytes.includes(credential.secret), false);
|
|
assert.equal(databaseBytes.includes(challenge.secret), false);
|
|
await assert.rejects(
|
|
console.service.issue({
|
|
projectId: 'default',
|
|
mutationId: '00000000-0000-4000-8000-000000000206',
|
|
requestId: 'console-issue-206',
|
|
issuer: {
|
|
subject: { type: 'system', id: 'owner-bootstrap' },
|
|
authenticationId: 'forged',
|
|
authenticatedAtMs: Date.now(),
|
|
expiresAtMs: Date.now() + 60_000,
|
|
assurance: 'local_console',
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('claims the first Owner from staged deliveries without crossing the transport with secrets', async (t) => {
|
|
const options = await ready(t);
|
|
const console = await openLocalOwnerConsole(options);
|
|
t.after(() => console.close());
|
|
const credentialMutationId = '00000000-0000-4000-8000-000000000711';
|
|
const challengeMutationId = '00000000-0000-4000-8000-000000000712';
|
|
const claimMutationId = '00000000-0000-4000-8000-000000000713';
|
|
await console.service.provision({
|
|
mutationId: credentialMutationId,
|
|
requestId: 'console-provision-711',
|
|
});
|
|
await console.service.issue({
|
|
projectId: 'default',
|
|
mutationId: challengeMutationId,
|
|
requestId: 'console-issue-712',
|
|
});
|
|
const credentialDelivery =
|
|
console.inspectCredentialDelivery(credentialMutationId);
|
|
const challengeDelivery =
|
|
console.inspectChallengeDelivery(challengeMutationId);
|
|
const claimed = await console.claimOwnerFromDeliveries({
|
|
projectId: 'default',
|
|
mutationId: claimMutationId,
|
|
requestId: 'console-claim-713',
|
|
credentialMutationId,
|
|
challengeMutationId,
|
|
});
|
|
assert.equal(claimed.status, 'inserted');
|
|
assert.equal(claimed.binding.role, 'owner');
|
|
assert.equal(JSON.stringify(claimed).includes('secret'), false);
|
|
await console.acknowledgeCredentialDelivery(
|
|
credentialMutationId,
|
|
credentialDelivery.deliveryDigest,
|
|
);
|
|
await console.acknowledgeChallengeDelivery(
|
|
challengeMutationId,
|
|
challengeDelivery.deliveryDigest,
|
|
);
|
|
const replay = await console.claimOwnerFromDeliveries({
|
|
projectId: 'default',
|
|
mutationId: claimMutationId,
|
|
requestId: 'console-claim-713',
|
|
credentialMutationId,
|
|
challengeMutationId,
|
|
});
|
|
assert.equal(replay.status, 'existing');
|
|
await assert.rejects(
|
|
console.claimOwnerFromDeliveries({
|
|
projectId: 'default',
|
|
mutationId: '00000000-0000-4000-8000-000000000714',
|
|
requestId: 'console-claim-714',
|
|
credentialMutationId,
|
|
challengeMutationId,
|
|
challengeToken: 'forbidden',
|
|
}),
|
|
LocalOwnerSecretDeliveryError,
|
|
);
|
|
});
|
|
|
|
test('recovers one credential without revoking the old token before delivery acknowledgement', async (t) => {
|
|
const options = await ready(t);
|
|
const first = await openLocalOwnerConsole(options);
|
|
const provisionMutationId = '00000000-0000-4000-8000-000000000701';
|
|
const provisioned = await first.service.provision({
|
|
mutationId: provisionMutationId,
|
|
requestId: 'console-provision-701',
|
|
});
|
|
const provisionDelivery =
|
|
first.inspectCredentialDelivery(provisionMutationId);
|
|
await first.acknowledgeCredentialDelivery(
|
|
provisionMutationId,
|
|
provisionDelivery.deliveryDigest,
|
|
);
|
|
|
|
const issueMutationId = '00000000-0000-4000-8000-000000000702';
|
|
const issued = await first.credentialRecovery.issue({
|
|
mutationId: issueMutationId,
|
|
requestId: 'console-recover-issue-702',
|
|
previousCredentialId: provisioned.credentialId,
|
|
expectedPreviousVersion: 1,
|
|
});
|
|
assert.equal(issued.status, 'inserted');
|
|
assert.equal(issued.state, 'issued');
|
|
assert.equal(issued.replacementCredentialToken, null);
|
|
const recoveryDelivery = first.inspectCredentialDelivery(issueMutationId);
|
|
await assert.rejects(
|
|
first.credentialRecovery.complete({
|
|
issueMutationId,
|
|
mutationId: '00000000-0000-4000-8000-000000000703',
|
|
requestId: 'console-recover-complete-703',
|
|
}),
|
|
);
|
|
const beforeAcknowledgement = await openLocalSqliteBootstrapDatabase({
|
|
databasePath: options.databasePath,
|
|
profile: options.profile,
|
|
});
|
|
assert.equal(
|
|
(
|
|
await beforeAcknowledgement.apiCredentials.resolve(
|
|
provisioned.credentialId,
|
|
)
|
|
).state,
|
|
'active',
|
|
);
|
|
await beforeAcknowledgement.close();
|
|
|
|
await first.close();
|
|
const restarted = await openLocalOwnerConsole(options);
|
|
t.after(() => restarted.close());
|
|
assert.equal(fs.existsSync(recoveryDelivery.path), true);
|
|
await restarted.acknowledgeCredentialRecoveryDelivery(
|
|
issueMutationId,
|
|
recoveryDelivery.deliveryDigest,
|
|
);
|
|
const completed = await restarted.credentialRecovery.complete({
|
|
issueMutationId,
|
|
mutationId: '00000000-0000-4000-8000-000000000703',
|
|
requestId: 'console-recover-complete-703',
|
|
});
|
|
assert.equal(completed.state, 'completed');
|
|
assert.equal(fs.existsSync(recoveryDelivery.path), false);
|
|
|
|
const database = await openLocalSqliteBootstrapDatabase({
|
|
databasePath: options.databasePath,
|
|
profile: options.profile,
|
|
});
|
|
assert.equal(
|
|
(await database.apiCredentials.resolve(provisioned.credentialId)).state,
|
|
'revoked',
|
|
);
|
|
assert.equal(
|
|
(await database.apiCredentials.resolve(issued.replacementCredentialId))
|
|
.state,
|
|
'active',
|
|
);
|
|
await database.close();
|
|
const replay = await restarted.credentialRecovery.issue({
|
|
mutationId: issueMutationId,
|
|
requestId: 'console-recover-issue-702',
|
|
previousCredentialId: provisioned.credentialId,
|
|
expectedPreviousVersion: 1,
|
|
});
|
|
assert.equal(replay.status, 'existing');
|
|
assert.equal(replay.state, 'completed');
|
|
assert.equal(replay.replacementCredentialToken, null);
|
|
});
|
|
|
|
test('acknowledges exact ready records and replays without regenerating secrets', async (t) => {
|
|
const options = await ready(t);
|
|
const console = await openLocalOwnerConsole(options);
|
|
const credentialMutationId = '00000000-0000-4000-8000-000000000207';
|
|
const provisionRequestId = 'console-provision-207';
|
|
const provisioned = await console.service.provision({
|
|
mutationId: credentialMutationId,
|
|
requestId: provisionRequestId,
|
|
});
|
|
const credentialSummary =
|
|
console.inspectCredentialDelivery(credentialMutationId);
|
|
const credentialReady = fs.readFileSync(credentialSummary.path);
|
|
await assert.rejects(
|
|
console.acknowledgeCredentialDelivery(credentialMutationId, '0'.repeat(64)),
|
|
LocalOwnerSecretDeliveryError,
|
|
);
|
|
assert.equal(fs.existsSync(credentialSummary.path), true);
|
|
const acknowledgementDatabaseA = await openLocalSqliteBootstrapDatabase({
|
|
databasePath: options.databasePath,
|
|
profile: options.profile,
|
|
});
|
|
const acknowledgementDatabaseB = await openLocalSqliteBootstrapDatabase({
|
|
databasePath: options.databasePath,
|
|
profile: options.profile,
|
|
});
|
|
const deliveryA = new FileLocalOwnerBootstrapSecretDelivery(
|
|
options.secretDeliveryDirectory,
|
|
);
|
|
const deliveryB = new FileLocalOwnerBootstrapSecretDelivery(
|
|
options.secretDeliveryDirectory,
|
|
);
|
|
const pepper = fs.readFileSync(options.pepperPath, 'utf8');
|
|
const concurrentAcknowledgements = await Promise.all([
|
|
deliveryA.acknowledge(
|
|
acknowledgementDatabaseA.ownerBootstrap,
|
|
pepper,
|
|
'credential',
|
|
credentialMutationId,
|
|
credentialSummary.deliveryDigest,
|
|
1,
|
|
),
|
|
deliveryB.acknowledge(
|
|
acknowledgementDatabaseB.ownerBootstrap,
|
|
pepper,
|
|
'credential',
|
|
credentialMutationId,
|
|
credentialSummary.deliveryDigest,
|
|
2,
|
|
),
|
|
]);
|
|
await Promise.all([
|
|
acknowledgementDatabaseA.close(),
|
|
acknowledgementDatabaseB.close(),
|
|
]);
|
|
assert.deepEqual(
|
|
concurrentAcknowledgements[0],
|
|
concurrentAcknowledgements[1],
|
|
);
|
|
const credentialAcknowledgement = await console.acknowledgeCredentialDelivery(
|
|
credentialMutationId,
|
|
credentialSummary.deliveryDigest,
|
|
);
|
|
assert.deepEqual(credentialAcknowledgement, {
|
|
state: 'acknowledged',
|
|
kind: 'credential',
|
|
mutationId: credentialMutationId,
|
|
requestId: provisionRequestId,
|
|
ttlMs: LOCAL_IDENTITY_BOOTSTRAP_DEFAULT_TTL_MS,
|
|
});
|
|
assert.equal(fs.existsSync(credentialSummary.path), false);
|
|
const credentialAcknowledgementPath = path.join(
|
|
options.secretDeliveryDirectory,
|
|
`credential-${credentialMutationId}.acknowledged.json`,
|
|
);
|
|
assert.equal(fs.existsSync(credentialAcknowledgementPath), false);
|
|
const ledgerDatabase = new DatabaseSync(options.databasePath);
|
|
const credentialTombstone = ledgerDatabase
|
|
.prepare(
|
|
`SELECT * FROM "QingLong3LocalOwnerDeliveryAcknowledgements"
|
|
WHERE "mutation_id" = ?`,
|
|
)
|
|
.get(credentialMutationId);
|
|
ledgerDatabase.close();
|
|
assert.equal(
|
|
credentialTombstone.delivery_digest,
|
|
credentialSummary.deliveryDigest,
|
|
);
|
|
assert.equal([1, 2].includes(credentialTombstone.acknowledged_at_ms), true);
|
|
assert.equal(Object.keys(credentialTombstone).includes('secret'), false);
|
|
const replayedProvision = await console.service.provision({
|
|
mutationId: credentialMutationId,
|
|
requestId: provisionRequestId,
|
|
});
|
|
assert.equal(replayedProvision.status, 'existing');
|
|
assert.equal(replayedProvision.subjectId, provisioned.subjectId);
|
|
assert.equal(replayedProvision.credentialId, provisioned.credentialId);
|
|
assert.equal(replayedProvision.credentialToken, null);
|
|
|
|
const challengeMutationId = '00000000-0000-4000-8000-000000000208';
|
|
const issueRequestId = 'console-issue-208';
|
|
const issued = await console.service.issue({
|
|
projectId: 'default',
|
|
mutationId: challengeMutationId,
|
|
requestId: issueRequestId,
|
|
});
|
|
const challengeSummary =
|
|
console.inspectChallengeDelivery(challengeMutationId);
|
|
const challengeAcknowledgement = await console.acknowledgeChallengeDelivery(
|
|
challengeMutationId,
|
|
challengeSummary.deliveryDigest,
|
|
);
|
|
assert.deepEqual(challengeAcknowledgement, {
|
|
state: 'acknowledged',
|
|
kind: 'challenge',
|
|
projectId: 'default',
|
|
mutationId: challengeMutationId,
|
|
requestId: issueRequestId,
|
|
ttlMs: 600_000,
|
|
});
|
|
const replayedIssue = await console.service.issue({
|
|
projectId: 'default',
|
|
mutationId: challengeMutationId,
|
|
requestId: issueRequestId,
|
|
});
|
|
assert.equal(replayedIssue.status, 'existing');
|
|
assert.equal(replayedIssue.challengeId, issued.challengeId);
|
|
assert.equal(replayedIssue.challengeToken, null);
|
|
|
|
fs.writeFileSync(credentialSummary.path, credentialReady, { mode: 0o600 });
|
|
await console.close();
|
|
const recovered = await openLocalOwnerConsole(options);
|
|
t.after(() => recovered.close());
|
|
assert.equal(fs.existsSync(credentialSummary.path), false);
|
|
assert.deepEqual(recovered.recovery, {
|
|
inspectedPendingRecords: 0,
|
|
publishedRecords: 0,
|
|
retainedUncommittedRecords: 0,
|
|
orphanTemporaryRecords: 0,
|
|
});
|
|
const recoveredReplay = await recovered.service.provision({
|
|
mutationId: credentialMutationId,
|
|
requestId: provisionRequestId,
|
|
});
|
|
assert.equal(recoveredReplay.status, 'existing');
|
|
assert.equal(recoveredReplay.credentialToken, null);
|
|
});
|
|
|
|
test('retains a pre-commit secret and publishes it after the matching commit', async (t) => {
|
|
const options = await ready(t);
|
|
const delivery = new FileLocalOwnerBootstrapSecretDelivery(
|
|
options.secretDeliveryDirectory,
|
|
);
|
|
const mutationId = '00000000-0000-4000-8000-000000000211';
|
|
const staged = await delivery.prepare({
|
|
kind: 'credential',
|
|
mutationId,
|
|
requestId: 'console-provision-211',
|
|
subjectId: `usr_${Buffer.alloc(16, 11).toString('base64url')}`,
|
|
credentialId: `own_${Buffer.alloc(16, 12).toString('base64url')}`,
|
|
secret: Buffer.alloc(32, 13).toString('base64url'),
|
|
ttlMs: LOCAL_IDENTITY_BOOTSTRAP_DEFAULT_TTL_MS,
|
|
});
|
|
const pendingPath = delivery
|
|
.readyPath('credential', mutationId)
|
|
.replace('.ready.json', '.pending.json');
|
|
assert.equal(fs.existsSync(pendingPath), true);
|
|
|
|
const console = await openLocalOwnerConsole(options);
|
|
t.after(() => console.close());
|
|
assert.deepEqual(console.recovery, {
|
|
inspectedPendingRecords: 1,
|
|
publishedRecords: 0,
|
|
retainedUncommittedRecords: 1,
|
|
orphanTemporaryRecords: 0,
|
|
});
|
|
const provisioned = await console.service.provision({
|
|
mutationId,
|
|
requestId: staged.requestId,
|
|
});
|
|
assert.equal(provisioned.status, 'inserted');
|
|
assert.equal(provisioned.subjectId, staged.subjectId);
|
|
assert.equal(provisioned.credentialId, staged.credentialId);
|
|
assert.equal(provisioned.credentialToken, null);
|
|
assert.equal(fs.existsSync(pendingPath), false);
|
|
assert.equal(fs.existsSync(console.credentialDeliveryPath(mutationId)), true);
|
|
});
|
|
|
|
test('recovers database-committed credential and challenge after publish failure', async (t) => {
|
|
const options = await ready(t);
|
|
const delivery = new FileLocalOwnerBootstrapSecretDelivery(
|
|
options.secretDeliveryDirectory,
|
|
);
|
|
const failingDelivery = {
|
|
prepare(candidate) {
|
|
return delivery.prepare(candidate);
|
|
},
|
|
async publish() {
|
|
throw new Error('injected publish failure');
|
|
},
|
|
};
|
|
|
|
const provisionMutationId = '00000000-0000-4000-8000-000000000221';
|
|
const first = await directService(options, failingDelivery);
|
|
await assert.rejects(
|
|
first.service.provision({
|
|
mutationId: provisionMutationId,
|
|
requestId: 'console-provision-221',
|
|
}),
|
|
LocalOwnerBootstrapServiceUnavailableError,
|
|
);
|
|
await first.database.close();
|
|
|
|
const recoveredCredential = await openLocalOwnerConsole(options);
|
|
assert.deepEqual(recoveredCredential.recovery, {
|
|
inspectedPendingRecords: 1,
|
|
publishedRecords: 1,
|
|
retainedUncommittedRecords: 0,
|
|
orphanTemporaryRecords: 0,
|
|
});
|
|
const replayedProvision = await recoveredCredential.service.provision({
|
|
mutationId: provisionMutationId,
|
|
requestId: 'console-provision-221',
|
|
});
|
|
assert.equal(replayedProvision.status, 'existing');
|
|
assert.equal(replayedProvision.credentialToken, null);
|
|
await recoveredCredential.close();
|
|
|
|
const challengeMutationId = '00000000-0000-4000-8000-000000000222';
|
|
const second = await directService(options, failingDelivery);
|
|
await assert.rejects(
|
|
second.service.issue({
|
|
projectId: 'default',
|
|
mutationId: challengeMutationId,
|
|
requestId: 'console-issue-222',
|
|
}),
|
|
LocalOwnerBootstrapServiceUnavailableError,
|
|
);
|
|
await second.database.close();
|
|
|
|
const recoveredChallenge = await openLocalOwnerConsole(options);
|
|
t.after(() => recoveredChallenge.close());
|
|
assert.deepEqual(recoveredChallenge.recovery, {
|
|
inspectedPendingRecords: 1,
|
|
publishedRecords: 1,
|
|
retainedUncommittedRecords: 0,
|
|
orphanTemporaryRecords: 0,
|
|
});
|
|
const replayedIssue = await recoveredChallenge.service.issue({
|
|
projectId: 'default',
|
|
mutationId: challengeMutationId,
|
|
requestId: 'console-issue-222',
|
|
});
|
|
assert.equal(replayedIssue.status, 'existing');
|
|
assert.equal(replayedIssue.challengeToken, null);
|
|
});
|
|
|
|
test('fails closed on tampered delivery records and bounded-directory overflow', async (t) => {
|
|
await t.test('private record mode', async (t) => {
|
|
const options = await ready(t);
|
|
const console = await openLocalOwnerConsole(options);
|
|
const mutationId = '00000000-0000-4000-8000-000000000231';
|
|
await console.service.provision({
|
|
mutationId,
|
|
requestId: 'console-provision-231',
|
|
});
|
|
const recordPath = console.credentialDeliveryPath(mutationId);
|
|
await console.close();
|
|
fs.chmodSync(recordPath, 0o644);
|
|
await assert.rejects(
|
|
openLocalOwnerConsole(options),
|
|
LocalOwnerSecretDeliveryError,
|
|
);
|
|
});
|
|
|
|
await t.test('database digest mismatch', async (t) => {
|
|
const options = await ready(t);
|
|
const console = await openLocalOwnerConsole(options);
|
|
const mutationId = '00000000-0000-4000-8000-000000000232';
|
|
await console.service.provision({
|
|
mutationId,
|
|
requestId: 'console-provision-232',
|
|
});
|
|
const recordPath = console.credentialDeliveryPath(mutationId);
|
|
const record = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
|
|
await console.close();
|
|
record.secret = Buffer.alloc(32, 99).toString('base64url');
|
|
fs.writeFileSync(recordPath, `${JSON.stringify(record)}\n`, {
|
|
mode: 0o600,
|
|
});
|
|
await assert.rejects(
|
|
openLocalOwnerConsole(options),
|
|
LocalOwnerSecretDeliveryError,
|
|
);
|
|
});
|
|
|
|
await t.test('entry budget', async (t) => {
|
|
const options = await ready(t);
|
|
for (let index = 0; index < 65; index += 1) {
|
|
const name = `.credential-${randomUUID()}.${randomUUID()}.tmp`;
|
|
fs.writeFileSync(path.join(options.secretDeliveryDirectory, name), 'x', {
|
|
mode: 0o600,
|
|
});
|
|
}
|
|
await assert.rejects(
|
|
openLocalOwnerConsole(options),
|
|
LocalOwnerSecretDeliveryError,
|
|
);
|
|
});
|
|
|
|
await t.test('tampered acknowledgement fact', async (t) => {
|
|
const options = await ready(t);
|
|
const console = await openLocalOwnerConsole(options);
|
|
const mutationId = '00000000-0000-4000-8000-000000000233';
|
|
await console.service.provision({
|
|
mutationId,
|
|
requestId: 'console-provision-233',
|
|
});
|
|
const summary = console.inspectCredentialDelivery(mutationId);
|
|
await console.acknowledgeCredentialDelivery(
|
|
mutationId,
|
|
summary.deliveryDigest,
|
|
);
|
|
await console.close();
|
|
const database = new DatabaseSync(options.databasePath);
|
|
database
|
|
.prepare(
|
|
`UPDATE "QingLong3LocalOwnerDeliveryAcknowledgements"
|
|
SET "fact_digest" = ? WHERE "mutation_id" = ?`,
|
|
)
|
|
.run('0'.repeat(64), mutationId);
|
|
database.close();
|
|
const reopened = await openLocalOwnerConsole(options);
|
|
t.after(() => reopened.close());
|
|
await assert.rejects(
|
|
reopened.service.provision({
|
|
mutationId,
|
|
requestId: 'console-provision-233',
|
|
}),
|
|
LocalOwnerBootstrapServiceUnavailableError,
|
|
);
|
|
});
|
|
|
|
await t.test('acknowledged mutation with pending record', async (t) => {
|
|
const options = await ready(t);
|
|
const console = await openLocalOwnerConsole(options);
|
|
const mutationId = '00000000-0000-4000-8000-000000000234';
|
|
await console.service.provision({
|
|
mutationId,
|
|
requestId: 'console-provision-234',
|
|
});
|
|
const summary = console.inspectCredentialDelivery(mutationId);
|
|
const readyMaterial = fs.readFileSync(summary.path);
|
|
await console.acknowledgeCredentialDelivery(
|
|
mutationId,
|
|
summary.deliveryDigest,
|
|
);
|
|
await console.close();
|
|
fs.writeFileSync(
|
|
summary.path.replace('.ready.json', '.pending.json'),
|
|
readyMaterial,
|
|
{ mode: 0o600 },
|
|
);
|
|
await assert.rejects(
|
|
openLocalOwnerConsole(options),
|
|
LocalOwnerSecretDeliveryError,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('cleans staged files after ENOSPC and read-only delivery failures', async (t) => {
|
|
for (const [index, code] of ['ENOSPC', 'EROFS'].entries()) {
|
|
await t.test(code, async (t) => {
|
|
const options = await ready(t);
|
|
const console = await openLocalOwnerConsole(options);
|
|
t.after(() => console.close());
|
|
const originalWriteFileSync = fs.writeFileSync;
|
|
fs.writeFileSync = function injectedWriteFailure(target, ...args) {
|
|
if (typeof target === 'number') {
|
|
throw Object.assign(new Error(`injected ${code}`), { code });
|
|
}
|
|
return originalWriteFileSync.call(this, target, ...args);
|
|
};
|
|
try {
|
|
await assert.rejects(
|
|
console.service.provision({
|
|
mutationId: `00000000-0000-4000-8000-00000000024${index}`,
|
|
requestId: `console-provision-24${index}`,
|
|
}),
|
|
LocalOwnerBootstrapServiceUnavailableError,
|
|
);
|
|
} finally {
|
|
fs.writeFileSync = originalWriteFileSync;
|
|
}
|
|
assert.deepEqual(fs.readdirSync(options.secretDeliveryDirectory), []);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('rejects broad deployment permissions and pepper symlinks', async (t) => {
|
|
const broad = await ready(t);
|
|
fs.chmodSync(broad.deploymentRoot, 0o755);
|
|
await assert.rejects(
|
|
openLocalOwnerConsole(broad),
|
|
LocalOwnerConsoleConfigurationError,
|
|
);
|
|
|
|
const linked = await ready(t);
|
|
const actualPepper = path.join(linked.deploymentRoot, 'actual.pepper');
|
|
fs.renameSync(linked.pepperPath, actualPepper);
|
|
fs.symlinkSync(actualPepper, linked.pepperPath);
|
|
await assert.rejects(
|
|
openLocalOwnerConsole(linked),
|
|
LocalOwnerConsoleConfigurationError,
|
|
);
|
|
});
|
|
|
|
test('rechecks database identity before every authority operation', async (t) => {
|
|
const options = await ready(t);
|
|
const console = await openLocalOwnerConsole(options);
|
|
t.after(() => console.close());
|
|
const moved = path.join(options.deploymentRoot, 'moved.sqlite');
|
|
fs.renameSync(options.databasePath, moved);
|
|
fs.copyFileSync(moved, options.databasePath);
|
|
fs.chmodSync(options.databasePath, 0o600);
|
|
await assert.rejects(
|
|
async () =>
|
|
console.service.provision({
|
|
mutationId: '00000000-0000-4000-8000-000000000203',
|
|
requestId: 'console-provision-203',
|
|
}),
|
|
LocalOwnerConsoleConfigurationError,
|
|
);
|
|
});
|
|
|
|
test('close is idempotent and no CLI or default-runtime authority is exported', async (t) => {
|
|
const options = await ready(t);
|
|
const console = await openLocalOwnerConsole(options);
|
|
await Promise.all([console.close(), console.close()]);
|
|
await assert.rejects(
|
|
console.service.provision({
|
|
mutationId: '00000000-0000-4000-8000-000000000204',
|
|
requestId: 'console-provision-204',
|
|
}),
|
|
);
|
|
const manifest = require('../package.json');
|
|
assert.equal('bin' in manifest, false);
|
|
const localRuntime = require('@qinglong/local-sqlite/runtime');
|
|
assert.equal('openLocalOwnerConsole' in localRuntime, false);
|
|
});
|