Files
qinglong/test/back/ql3WorkerCredentialManagementLiveCeremony.test.cjs
T

417 lines
13 KiB
JavaScript

const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { afterEach, test } = require('node:test');
const {
WorkerCredentialManagementLiveCeremonyError,
assertionIdentity,
parseArguments,
runWorkerCredentialManagementLiveCeremony,
validateWorkerCredentialManagementLiveCeremony,
} = require('../../scripts/ql3-worker-credential-management-live-ceremony.cjs');
const NOW_MS = 1_700_000_000_000;
const temporaryDirectories = [];
function encode(value) {
return Buffer.from(JSON.stringify(value)).toString('base64url');
}
function assertion(subject, overrides = {}, headerOverrides = {}) {
const now = Math.floor(NOW_MS / 1000);
return `${encode({
alg: 'EdDSA',
kid: `key-${subject}`,
typ: 'ql3-worker-credential-management+jwt',
...headerOverrides,
})}.${encode({
acr: 'urn:production:mfa',
amr: ['pwd', 'otp'],
aud: 'qinglong3-worker-credential-management',
auth_time: now - 20,
exp: now + 120,
iat: now - 10,
iss: 'https://identity.production.example.org/',
jti: `session-${subject}`,
ql3_purpose: 'worker-credential-management',
sub: subject,
...overrides,
})}.${Buffer.alloc(64, 7).toString('base64url')}`;
}
function ceremony() {
return {
schemaVersion: 1,
planRequest: {
actionRef: 'worker-credential:worker-a:generation-2',
authorityProjectId: 'cluster-authority',
action: 'rotate',
deliveryId: '123e4567-e89b-42d3-a456-426614174901',
workerId: 'worker-a',
credentialId: 'credential-generation-2',
previousCredentialId: 'credential-generation-1',
credentialNotBeforeAtMs: NOW_MS + 1_000,
credentialExpiresAtMs: NOW_MS + 10 * 60_000,
deploymentTargetDigest: 'd'.repeat(64),
deploymentGeneration: 'generation-2',
},
approvalRequestId: 'approval-worker-a-generation-2',
approvalAuditEventId: '123e4567-e89b-42d3-a456-426614174902',
requesterDecisionId: 'requester-self-decision',
requesterDecisionAuditEventId: '123e4567-e89b-42d3-a456-426614174903',
reviewerDecisionId: 'reviewer-decision',
reviewerDecisionAuditEventId: '123e4567-e89b-42d3-a456-426614174904',
decisionReasonCode: 'reviewed',
inspectionId: 'inspection-worker-a-generation-2',
};
}
function writePrivate(directory, name, value) {
const filePath = path.join(directory, name);
fs.writeFileSync(
filePath,
typeof value === 'string' ? value : `${JSON.stringify(value)}\n`,
{ mode: 0o600 },
);
return filePath;
}
function fixture() {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-worker-ceremony-test-')),
);
fs.chmodSync(directory, 0o700);
temporaryDirectories.push(directory);
return {
configFile: writePrivate(directory, 'client.json', {}),
requesterAssertionFile: writePrivate(
directory,
'requester.jwt',
assertion('operator-a'),
),
reviewerAssertionFile: writePrivate(
directory,
'reviewer.jwt',
assertion('reviewer-b'),
),
ceremonyFile: writePrivate(directory, 'ceremony.json', ceremony()),
outputFile: path.join(directory, 'evidence.json'),
};
}
function approval(state, decidedBy = null) {
return {
id: 'approval-worker-a-generation-2',
version: state === 'pending' ? 1 : 2,
state,
requestedBy: { type: 'user', id: 'operator-a' },
decidedBy,
dispatchId: null,
consumedAtMs: null,
actionDigest: 'a'.repeat(64),
previewDigest: 'b'.repeat(64),
};
}
function liveDependencies(options = {}) {
const calls = [];
let sequence = 0;
const plan = {
actionRef: ceremony().planRequest.actionRef,
authorityProjectId: ceremony().planRequest.authorityProjectId,
requestedBy: { type: 'user', id: 'operator-a' },
planDigest: 'a'.repeat(64),
previewDigest: 'b'.repeat(64),
};
return {
calls,
dependencies: {
now: () => NOW_MS,
normalize(command) {
assert.equal(command.schemaVersion, 1);
assert.match(command.operation, /^worker-credential\./);
assert.notEqual(command.operation, 'worker-credential.execute');
return command;
},
async collectOidc(identity) {
assert.equal(
identity.issuer,
'https://identity.production.example.org/',
);
return {
discoveryDocumentSha256: `sha256:${'c'.repeat(64)}`,
jwksSha256: `sha256:${'d'.repeat(64)}`,
};
},
async execute(paths) {
const command = JSON.parse(fs.readFileSync(paths.commandFile, 'utf8'));
calls.push({
operation: command.operation,
actor: path.basename(paths.assertionFile),
});
sequence += 1;
if (command.operation === 'worker-credential.plan') {
return {
requestId: 'request-plan',
result: { status: 'created', plan },
};
}
if (command.operation === 'worker-credential.propose') {
return {
requestId: 'request-propose',
result: {
approvalStatus: 'created',
plan,
approval: approval('pending'),
},
};
}
if (
command.operation === 'worker-credential.decide' &&
path.basename(paths.assertionFile) === 'requester.jwt'
) {
if (options.acceptSelfDecision) {
return {
requestId: 'request-self',
result: {
status: 'decided',
approval: approval('approved', {
type: 'user',
id: 'operator-a',
}),
},
};
}
throw {
statusCode: 403,
responseCode: 'forbidden',
requestId: 'request-self',
};
}
if (command.operation === 'worker-credential.decide') {
return {
requestId: 'request-reviewer',
result: {
status: 'decided',
approval: approval('approved', {
type: 'user',
id: 'reviewer-b',
}),
},
};
}
assert.equal(command.operation, 'worker-credential.inspect');
return {
requestId: 'request-inspect',
result: {
stale: false,
approval: approval('approved', { type: 'user', id: 'reviewer-b' }),
},
};
},
},
};
}
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
fs.rmSync(directory, { recursive: true, force: true });
}
});
test('runs one external two-User ceremony without execution or sensitive evidence', async () => {
const paths = fixture();
const live = liveDependencies();
const report = await runWorkerCredentialManagementLiveCeremony(
paths,
live.dependencies,
);
assert.deepEqual(live.calls, [
{ operation: 'worker-credential.plan', actor: 'requester.jwt' },
{ operation: 'worker-credential.propose', actor: 'requester.jwt' },
{ operation: 'worker-credential.decide', actor: 'requester.jwt' },
{ operation: 'worker-credential.decide', actor: 'reviewer.jwt' },
{ operation: 'worker-credential.inspect', actor: 'reviewer.jwt' },
]);
assert.equal(report.gates.passed, true);
assert.equal(report.ceremony.dispatchCreated, false);
assert.equal(report.ceremony.approvalConsumed, false);
assert.equal(
validateWorkerCredentialManagementLiveCeremony(report).compatible,
true,
);
const serialized = JSON.stringify(report);
assert.doesNotMatch(serialized, /operator-a|reviewer-b|session-|eyJ/);
assert.equal(fs.statSync(paths.outputFile).mode & 0o777, 0o600);
assert.deepEqual(
JSON.parse(fs.readFileSync(paths.outputFile, 'utf8')),
report,
);
const audit = spawnSync(
process.execPath,
[
path.resolve(
__dirname,
'../../scripts/ql3-worker-credential-management-live-ceremony-audit.cjs',
),
`--report=${paths.outputFile}`,
],
{ encoding: 'utf8' },
);
assert.equal(audit.status, 0, audit.stderr);
assert.equal(JSON.parse(audit.stdout).compatible, true);
});
test('rejects same-User, wrong-purpose and non-external identities before requests', async () => {
for (const [requester, reviewer] of [
[assertion('same-user'), assertion('same-user')],
[
assertion('operator-a', { ql3_purpose: 'plugin-package-management' }),
assertion('reviewer-b'),
],
[
assertion('operator-a', { iss: 'https://identity.example.test/' }),
assertion('reviewer-b', { iss: 'https://identity.example.test/' }),
],
]) {
const paths = fixture();
fs.writeFileSync(paths.requesterAssertionFile, requester, { mode: 0o600 });
fs.writeFileSync(paths.reviewerAssertionFile, reviewer, { mode: 0o600 });
const live = liveDependencies();
await assert.rejects(
runWorkerCredentialManagementLiveCeremony(paths, live.dependencies),
WorkerCredentialManagementLiveCeremonyError,
);
assert.deepEqual(live.calls, []);
assert.equal(fs.existsSync(paths.outputFile), false);
}
});
test('fails closed when requester self-approval is accepted', async () => {
const paths = fixture();
const live = liveDependencies({ acceptSelfDecision: true });
await assert.rejects(
runWorkerCredentialManagementLiveCeremony(paths, live.dependencies),
/self-decision was accepted/,
);
assert.equal(fs.existsSync(paths.outputFile), false);
assert.equal(
live.calls.some(
({ operation }) => operation === 'worker-credential.inspect',
),
false,
);
});
test('rejects broad, symlinked and replaceable private evidence files', async () => {
{
const paths = fixture();
fs.chmodSync(paths.requesterAssertionFile, 0o640);
const live = liveDependencies();
await assert.rejects(
runWorkerCredentialManagementLiveCeremony(paths, live.dependencies),
WorkerCredentialManagementLiveCeremonyError,
);
assert.deepEqual(live.calls, []);
}
{
const paths = fixture();
const target = writePrivate(
path.dirname(paths.ceremonyFile),
'ceremony-target.json',
ceremony(),
);
fs.rmSync(paths.ceremonyFile);
fs.symlinkSync(target, paths.ceremonyFile);
const live = liveDependencies();
await assert.rejects(
runWorkerCredentialManagementLiveCeremony(paths, live.dependencies),
WorkerCredentialManagementLiveCeremonyError,
);
assert.deepEqual(live.calls, []);
}
{
const paths = fixture();
fs.writeFileSync(paths.outputFile, '{}\n', { mode: 0o600 });
const live = liveDependencies();
await assert.rejects(
runWorkerCredentialManagementLiveCeremony(paths, live.dependencies),
WorkerCredentialManagementLiveCeremonyError,
);
assert.deepEqual(live.calls, []);
}
});
test('offline audit rejects widened, false-gate and sensitive reports', async () => {
const paths = fixture();
const live = liveDependencies();
const report = await runWorkerCredentialManagementLiveCeremony(
paths,
live.dependencies,
);
for (const candidate of [
{ ...report, debug: true },
{ ...report, gates: { ...report.gates, passed: false } },
{ ...report, identity: { ...report.identity, secret: 'must-not-exist' } },
]) {
assert.equal(
validateWorkerCredentialManagementLiveCeremony(candidate).compatible,
false,
);
}
});
test('parses only the five path-based CLI arguments', () => {
assert.deepEqual(
parseArguments([
'--config=/private/client.json',
'--requester-assertion=/private/requester.jwt',
'--reviewer-assertion=/private/reviewer.jwt',
'--ceremony=/private/ceremony.json',
'--output=/private/evidence.json',
]),
{
configFile: '/private/client.json',
requesterAssertionFile: '/private/requester.jwt',
reviewerAssertionFile: '/private/reviewer.jwt',
ceremonyFile: '/private/ceremony.json',
outputFile: '/private/evidence.json',
},
);
assert.throws(
() => parseArguments(['--requester-assertion=raw-jwt']),
WorkerCredentialManagementLiveCeremonyError,
);
});
test('validates the Worker-specific assertion envelope and lifetime', () => {
assert.deepEqual(
assertionIdentity(assertion('operator-a'), NOW_MS).subject,
'operator-a',
);
assert.throws(
() =>
assertionIdentity(
assertion('operator-a', { exp: NOW_MS / 1000 - 1 }),
NOW_MS,
),
WorkerCredentialManagementLiveCeremonyError,
);
assert.throws(
() =>
assertionIdentity(
assertion(
'operator-a',
{},
{ typ: 'ql3-plugin-package-management+jwt' },
),
NOW_MS,
),
WorkerCredentialManagementLiveCeremonyError,
);
});