mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
507 lines
14 KiB
JavaScript
507 lines
14 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
PluginPackageManagementLiveExerciseError,
|
|
assertionIdentity,
|
|
beforePhase,
|
|
exerciseFromState,
|
|
ingressProbes,
|
|
overlapPhase,
|
|
probePod,
|
|
revokedPhase,
|
|
stateDigest,
|
|
validateKeyset,
|
|
validateState,
|
|
} = require('../../scripts/ql3-plugin-package-management-live-exercise.cjs');
|
|
const {
|
|
validateExercise,
|
|
} = require('../../scripts/ql3-plugin-package-management-live-evidence-collect.cjs');
|
|
|
|
const NOW = Date.parse('2026-07-25T12:00:00.000Z');
|
|
const ISSUER = 'https://login.example.com/';
|
|
const AUDIENCE = 'qinglong3-package-management';
|
|
|
|
function compact(value) {
|
|
return Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
}
|
|
|
|
function assertion({ kid, subject, jti, assurance = 'mfa', now = NOW }) {
|
|
const seconds = Math.floor(now / 1000);
|
|
return `${compact({
|
|
alg: 'EdDSA',
|
|
kid,
|
|
typ: 'ql3-plugin-package-management+jwt',
|
|
})}.${compact({
|
|
acr: assurance,
|
|
amr: assurance === 'mfa' ? ['pwd', 'otp'] : ['hwk'],
|
|
aud: AUDIENCE,
|
|
auth_time: seconds - 10,
|
|
exp: seconds + 120,
|
|
iat: seconds - 10,
|
|
iss: ISSUER,
|
|
jti,
|
|
ql3_purpose: 'plugin-package-management',
|
|
sub: subject,
|
|
})}.c2lnbmF0dXJl`;
|
|
}
|
|
|
|
function keyset(generation, activeKids, revokedKids = []) {
|
|
const document = {
|
|
schemaVersion: 1,
|
|
generation,
|
|
issuer: ISSUER,
|
|
audience: AUDIENCE,
|
|
keys: [...new Set([...activeKids, ...revokedKids])].map((kid) => ({
|
|
alg: 'EdDSA',
|
|
crv: 'Ed25519',
|
|
kid,
|
|
kty: 'OKP',
|
|
use: 'sig',
|
|
x: 'a'.repeat(43),
|
|
})),
|
|
revokedKids,
|
|
assuranceMappings: [
|
|
{ acr: 'mfa', assurance: 'multi_factor', requiredAmr: ['pwd', 'otp'] },
|
|
{ acr: 'hardware', assurance: 'hardware', requiredAmr: ['hwk'] },
|
|
],
|
|
constraints: {
|
|
maxAssertionBytes: 8192,
|
|
maxLifetimeMs: 300000,
|
|
maxAuthenticationAgeMs: 300000,
|
|
clockSkewMs: 5000,
|
|
},
|
|
};
|
|
return {
|
|
...validateKeyset(document),
|
|
resourceVersion: `keyset-${generation}`,
|
|
};
|
|
}
|
|
|
|
function snapshot(generation, activeKids, revokedKids, tls) {
|
|
return {
|
|
clusterIdentitySha256: `sha256:${'1'.repeat(64)}`,
|
|
replicas: 2,
|
|
readyReplicas: 2,
|
|
unavailableReplicas: 0,
|
|
pods: [
|
|
{ name: 'management-a', uid: 'pod-a', ip: '10.0.0.10' },
|
|
{ name: 'management-b', uid: 'pod-b', ip: '10.0.0.11' },
|
|
],
|
|
image: `registry.example/qinglong3@sha256:${'2'.repeat(64)}`,
|
|
imagePullPolicy: 'IfNotPresent',
|
|
imagePullSecrets: [],
|
|
keyset: keyset(generation, activeKids, revokedKids),
|
|
tls: {
|
|
serial: tls.serial,
|
|
resourceVersion: tls.resourceVersion,
|
|
},
|
|
};
|
|
}
|
|
|
|
function http(snapshotValue, status, payload) {
|
|
return {
|
|
status,
|
|
payload,
|
|
tlsProtocol: 'TLSv1.3',
|
|
tlsSerial: snapshotValue.tls.serial,
|
|
};
|
|
}
|
|
|
|
function success(snapshotValue, operation, extra = {}) {
|
|
return http(snapshotValue, 200, {
|
|
schemaVersion: 1,
|
|
requestId: 'request-id',
|
|
result: { schemaVersion: 1, operation, ...extra },
|
|
});
|
|
}
|
|
|
|
function failure(snapshotValue, status, code) {
|
|
return http(snapshotValue, status, {
|
|
schemaVersion: 1,
|
|
requestId: 'request-id',
|
|
error: { code },
|
|
});
|
|
}
|
|
|
|
function exactIsolation() {
|
|
return {
|
|
labelledClientOutcome: 'tls13_connected',
|
|
unlabelledClientOutcome: 'timeout',
|
|
wrongPortOutcome: 'timeout',
|
|
kubernetesApiEgressOutcome: 'timeout',
|
|
publicInternetEgressOutcome: 'timeout',
|
|
postgresEgressOutcome: 'postgres_ready',
|
|
};
|
|
}
|
|
|
|
test('derives strong identities from the active keyset assurance mapping', () => {
|
|
const reviewed = keyset(7, ['old-key']);
|
|
const identity = assertionIdentity(
|
|
assertion({
|
|
kid: 'old-key',
|
|
subject: 'tenant/requester',
|
|
jti: 'requester-jti',
|
|
}),
|
|
reviewed,
|
|
NOW,
|
|
);
|
|
assert.deepEqual(identity, {
|
|
kid: 'old-key',
|
|
subject: 'tenant/requester',
|
|
assurance: 'multi_factor',
|
|
issuer: ISSUER,
|
|
audience: AUDIENCE,
|
|
expiresAt: Math.floor(NOW / 1000) + 120,
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
assertionIdentity(
|
|
assertion({
|
|
kid: 'old-key',
|
|
subject: 'tenant/requester',
|
|
jti: 'expired-jti',
|
|
now: NOW - 10 * 60 * 1000,
|
|
}),
|
|
reviewed,
|
|
NOW,
|
|
),
|
|
PluginPackageManagementLiveExerciseError,
|
|
);
|
|
});
|
|
|
|
test('rejects any private JWK material in the projected identity document', () => {
|
|
const value = keyset(7, ['old-key']).document;
|
|
const privateValue = structuredClone(value);
|
|
privateValue.keys[0].d = 'private-component';
|
|
assert.throws(
|
|
() => validateKeyset(privateValue),
|
|
PluginPackageManagementLiveExerciseError,
|
|
);
|
|
});
|
|
|
|
test('runs before, overlap and revoked as an ordered digest-linked ceremony', async () => {
|
|
const requester = assertion({
|
|
kid: 'old-key',
|
|
subject: 'tenant/requester',
|
|
jti: 'requester-jti',
|
|
});
|
|
const reviewer = assertion({
|
|
kid: 'old-key',
|
|
subject: 'tenant/reviewer',
|
|
jti: 'reviewer-jti',
|
|
});
|
|
const oldOverlap = assertion({
|
|
kid: 'old-key',
|
|
subject: 'tenant/requester',
|
|
jti: 'old-overlap-jti',
|
|
now: NOW + 60_000,
|
|
});
|
|
const newOverlap = assertion({
|
|
kid: 'new-key',
|
|
subject: 'tenant/reviewer',
|
|
jti: 'new-overlap-jti',
|
|
now: NOW + 60_000,
|
|
});
|
|
const beforeSnapshot = snapshot(7, ['old-key'], [], {
|
|
serial: 'A1',
|
|
resourceVersion: 'tls-1',
|
|
});
|
|
let beforeRequest = 0;
|
|
const before = await beforePhase(
|
|
{
|
|
endpoint: `https://management.example.com${'/api/v3/plugin-packages/management'}`,
|
|
requesterAssertion: requester,
|
|
reviewerAssertion: reviewer,
|
|
actionInput: { evidence: true },
|
|
nowMs: NOW,
|
|
},
|
|
{
|
|
async snapshot() {
|
|
return beforeSnapshot;
|
|
},
|
|
async request(assertionValue, command) {
|
|
beforeRequest += 1;
|
|
if (command.operation === 'plugin-package.propose') {
|
|
assert.equal(assertionValue, requester);
|
|
return success(beforeSnapshot, command.operation, {
|
|
approval: { version: 1, state: 'pending' },
|
|
});
|
|
}
|
|
if (
|
|
command.operation === 'plugin-package.decide' &&
|
|
assertionValue === requester
|
|
) {
|
|
return failure(beforeSnapshot, 403, 'forbidden');
|
|
}
|
|
if (command.operation === 'plugin-package.decide') {
|
|
assert.equal(assertionValue, reviewer);
|
|
return success(beforeSnapshot, command.operation, {
|
|
approval: { version: 2, state: 'approved' },
|
|
});
|
|
}
|
|
assert.equal(command.operation, 'plugin-package.inspect');
|
|
return success(beforeSnapshot, command.operation, {
|
|
approval: { version: 2, state: 'approved' },
|
|
});
|
|
},
|
|
async network() {
|
|
return exactIsolation();
|
|
},
|
|
},
|
|
);
|
|
assert.equal(beforeRequest, 4);
|
|
assert.equal(validateState(before, 'before'), before);
|
|
|
|
const overlapSnapshot = snapshot(8, ['old-key', 'new-key'], [], {
|
|
serial: 'B2',
|
|
resourceVersion: 'tls-2',
|
|
});
|
|
const overlap = await overlapPhase(
|
|
{
|
|
endpoint: before.endpoint,
|
|
state: before,
|
|
oldAssertion: oldOverlap,
|
|
newAssertion: newOverlap,
|
|
nowMs: NOW + 60_000,
|
|
},
|
|
{
|
|
async snapshot() {
|
|
return overlapSnapshot;
|
|
},
|
|
async request(_assertionValue, command) {
|
|
return success(overlapSnapshot, command.operation, {
|
|
approval: { version: 2, state: 'approved' },
|
|
});
|
|
},
|
|
},
|
|
);
|
|
assert.equal(overlap.previousStateSha256, before.stateSha256);
|
|
assert.equal(validateState(overlap, 'overlap'), overlap);
|
|
await assert.rejects(
|
|
() =>
|
|
revokedPhase(
|
|
{
|
|
endpoint: overlap.endpoint,
|
|
state: overlap,
|
|
oldAssertion: assertion({
|
|
kid: 'old-key',
|
|
subject: 'tenant/requester',
|
|
jti: 'replacement-old-jti',
|
|
now: NOW + 60_000,
|
|
}),
|
|
newAssertion: newOverlap,
|
|
nowMs: NOW + 120_000,
|
|
},
|
|
{
|
|
async snapshot() {
|
|
return snapshot(9, ['new-key'], ['old-key'], {
|
|
serial: 'B2',
|
|
resourceVersion: 'tls-2',
|
|
});
|
|
},
|
|
async request() {
|
|
assert.fail('a substituted assertion must fail before HTTP');
|
|
},
|
|
},
|
|
),
|
|
/append-only old-key revocation/,
|
|
);
|
|
|
|
const revokedSnapshot = snapshot(9, ['new-key'], ['old-key'], {
|
|
serial: 'B2',
|
|
resourceVersion: 'tls-2',
|
|
});
|
|
const revoked = await revokedPhase(
|
|
{
|
|
endpoint: overlap.endpoint,
|
|
state: overlap,
|
|
oldAssertion: oldOverlap,
|
|
newAssertion: newOverlap,
|
|
nowMs: NOW + 120_000,
|
|
},
|
|
{
|
|
async snapshot() {
|
|
return revokedSnapshot;
|
|
},
|
|
async request(assertionValue, command) {
|
|
return assertionValue === oldOverlap
|
|
? failure(revokedSnapshot, 401, 'authentication_required')
|
|
: success(revokedSnapshot, command.operation, {
|
|
approval: { version: 2, state: 'approved' },
|
|
});
|
|
},
|
|
},
|
|
);
|
|
assert.equal(revoked.previousStateSha256, overlap.stateSha256);
|
|
assert.equal(validateState(revoked, 'revoked'), revoked);
|
|
assert.deepEqual(revoked.phaseObservedAt, [
|
|
'2026-07-25T12:00:00.000Z',
|
|
'2026-07-25T12:01:00.000Z',
|
|
'2026-07-25T12:02:00.000Z',
|
|
]);
|
|
|
|
const exercise = exerciseFromState(revoked, NOW + 121_000);
|
|
assert.equal(
|
|
validateExercise(exercise, NOW + 121_000).fixture,
|
|
'qinglong/plugin-package-management-live-exercise@v1',
|
|
);
|
|
assert.deepEqual(exercise.identity.keysetGenerations, [7, 8, 9]);
|
|
assert.equal(exercise.rotation.revokedOldStatus, 401);
|
|
assert.equal(exercise.rotation.readinessSamples.length, 3);
|
|
assert.throws(
|
|
() => exerciseFromState(revoked, NOW + 25 * 60 * 60 * 1000),
|
|
/timeline is stale/,
|
|
);
|
|
});
|
|
|
|
test('rejects tampered state and cannot skip directly to revoked', async () => {
|
|
const state = {
|
|
schemaVersion: 1,
|
|
fixture: 'qinglong/plugin-package-management-live-exercise-state@v1',
|
|
phase: 'before',
|
|
recordedAt: new Date(NOW).toISOString(),
|
|
phaseObservedAt: [new Date(NOW).toISOString()],
|
|
previousStateSha256: null,
|
|
clusterIdentitySha256: `sha256:${'1'.repeat(64)}`,
|
|
endpoint:
|
|
'https://management.example.com/api/v3/plugin-packages/management',
|
|
action: {
|
|
actionRef: 'ql3-live-evidence:123e4567-e89b-42d3-a456-426614174000',
|
|
approvalRequestId: '123e4567-e89b-42d3-a456-426614174001',
|
|
proposalAuditEventId: '123e4567-e89b-42d3-a456-426614174002',
|
|
approvalAuditEventId: '123e4567-e89b-42d3-a456-426614174003',
|
|
decisionAuditEventId: '123e4567-e89b-42d3-a456-426614174004',
|
|
decisionId: '123e4567-e89b-42d3-a456-426614174005',
|
|
approvalVersion: 1,
|
|
},
|
|
identity: {
|
|
issuer: ISSUER,
|
|
audience: AUDIENCE,
|
|
requesterSubject: 'tenant/requester',
|
|
reviewerSubject: 'tenant/reviewer',
|
|
requesterAssurance: 'multi_factor',
|
|
reviewerAssurance: 'hardware',
|
|
oldKid: 'old-key',
|
|
newKid: null,
|
|
overlapOldAssertionSha256: null,
|
|
newAssertionSha256: null,
|
|
keysetGenerations: [7],
|
|
},
|
|
ceremony: {
|
|
proposalAuditEventId: '123e4567-e89b-42d3-a456-426614174002',
|
|
approvalAuditEventId: '123e4567-e89b-42d3-a456-426614174003',
|
|
decisionAuditEventId: '123e4567-e89b-42d3-a456-426614174004',
|
|
proposeStatus: 200,
|
|
proposeOperation: 'plugin-package.propose',
|
|
selfDecisionStatus: 403,
|
|
selfDecisionError: 'forbidden',
|
|
reviewerDecisionStatus: 200,
|
|
reviewerDecisionOperation: 'plugin-package.decide',
|
|
inspectionStatus: 200,
|
|
inspectionOperation: 'plugin-package.inspect',
|
|
},
|
|
isolation: exactIsolation(),
|
|
rotation: {
|
|
overlapOldStatus: null,
|
|
newStatus: null,
|
|
revokedOldStatus: null,
|
|
revokedOldError: null,
|
|
previousTlsSerial: 'A1',
|
|
currentTlsSerial: null,
|
|
previousTlsSecretResourceVersion: 'tls-1',
|
|
currentTlsSecretResourceVersion: null,
|
|
readinessSamples: [
|
|
{
|
|
phase: 'before',
|
|
replicas: 2,
|
|
readyReplicas: 2,
|
|
unavailableReplicas: 0,
|
|
tlsProtocol: 'TLSv1.3',
|
|
},
|
|
],
|
|
},
|
|
stateSha256: '',
|
|
};
|
|
state.stateSha256 = stateDigest(state);
|
|
state.identity.requesterSubject = 'tampered';
|
|
assert.throws(
|
|
() => validateState(state, 'before'),
|
|
PluginPackageManagementLiveExerciseError,
|
|
);
|
|
await assert.rejects(
|
|
revokedPhase(
|
|
{
|
|
endpoint: state.endpoint,
|
|
state,
|
|
oldAssertion: 'invalid',
|
|
newAssertion: 'invalid',
|
|
nowMs: NOW,
|
|
},
|
|
{ async snapshot() {} },
|
|
),
|
|
PluginPackageManagementLiveExerciseError,
|
|
);
|
|
});
|
|
|
|
test('probe Pods are tokenless, non-root, bounded and carry only public probe arguments', () => {
|
|
const pod = probePod(
|
|
'probe-a',
|
|
`registry.example/qinglong3@sha256:${'2'.repeat(64)}`,
|
|
'IfNotPresent',
|
|
[],
|
|
{ 'qinglong.io/plugin-package-management-client': 'true' },
|
|
[
|
|
'tls',
|
|
'service.namespace.svc',
|
|
'8443',
|
|
'connected',
|
|
'service.namespace.svc',
|
|
],
|
|
);
|
|
assert.equal(pod.spec.automountServiceAccountToken, false);
|
|
assert.equal(pod.spec.securityContext.runAsNonRoot, true);
|
|
assert.deepEqual(pod.spec.containers[0].securityContext.capabilities.drop, [
|
|
'ALL',
|
|
]);
|
|
assert.equal(
|
|
pod.spec.containers[0].securityContext.readOnlyRootFilesystem,
|
|
true,
|
|
);
|
|
assert.equal(pod.spec.containers[0].resources.limits.memory, '64Mi');
|
|
assert.doesNotMatch(JSON.stringify(pod), /assertion|authorization|bearer/i);
|
|
});
|
|
|
|
test('cleans only probe Pods created by the current exercise', async () => {
|
|
const deleted = [];
|
|
let creates = 0;
|
|
await assert.rejects(
|
|
() =>
|
|
ingressProbes(
|
|
{
|
|
run(args) {
|
|
if (args[0] === 'create') {
|
|
creates += 1;
|
|
if (creates === 2) throw new Error('name already exists');
|
|
return { status: 0, stdout: '', stderr: '' };
|
|
}
|
|
if (args.includes('delete')) {
|
|
deleted.push(args[4]);
|
|
return { status: 0, stdout: '', stderr: '' };
|
|
}
|
|
assert.fail(`unexpected kubectl operation: ${args.join(' ')}`);
|
|
},
|
|
},
|
|
{
|
|
pods: [{ ip: '10.0.0.10' }],
|
|
image: `registry.example/qinglong3@sha256:${'2'.repeat(64)}`,
|
|
imagePullPolicy: 'IfNotPresent',
|
|
imagePullSecrets: [],
|
|
},
|
|
'123e4567-e89b-42d3-a456-426614174000',
|
|
),
|
|
/name already exists/,
|
|
);
|
|
assert.deepEqual(deleted, ['ql3-management-evidence-123e4567-allowed']);
|
|
});
|