mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
213 lines
6.3 KiB
JavaScript
213 lines
6.3 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const { generateKeyPairSync } = require('node:crypto');
|
|
const { test } = require('node:test');
|
|
|
|
const {
|
|
consumeApprovalRequest,
|
|
createApprovalRequest,
|
|
decideApprovalRequest,
|
|
} = require('@qinglong/runtime-core/approved-action');
|
|
const {
|
|
createPluginPackagePublisherTrustSnapshot,
|
|
} = require('@qinglong/runtime-core/plugin-package-publisher-trust');
|
|
const {
|
|
InvalidPluginPackagePublisherRevocationProposalError,
|
|
PluginPackagePublisherRevocationProposalBindingConflictError,
|
|
createPluginPackagePublisherRevocationProposal,
|
|
normalizePluginPackagePublisherRevocationProposal,
|
|
resolvePluginPackagePublisherRevocationProposal,
|
|
} = require('@qinglong/runtime-core/plugin-package-publisher-revocation-proposal');
|
|
|
|
const REQUESTER = Object.freeze({ type: 'user', id: 'usr_owner' });
|
|
const REVIEWER = Object.freeze({ type: 'user', id: 'usr_security' });
|
|
const SYSTEM = Object.freeze({ type: 'system', id: 'package_executor' });
|
|
const FENCE = Object.freeze({ projectVersion: 4, bindingVersion: 7 });
|
|
|
|
function trustSnapshot() {
|
|
const { publicKey } = generateKeyPairSync('ed25519');
|
|
return createPluginPackagePublisherTrustSnapshot([
|
|
{
|
|
publisher: 'publisher-a.example',
|
|
keyId: 'key-a',
|
|
publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }),
|
|
notBeforeMs: 1_000,
|
|
notAfterMs: 10_000,
|
|
},
|
|
]);
|
|
}
|
|
|
|
function proposal(overrides = {}) {
|
|
return createPluginPackagePublisherRevocationProposal({
|
|
actionRef: 'publisher-revoke:publisher-a.example:key-a',
|
|
authorityProjectId: 'cluster-trust-authority',
|
|
trustAuthorityId: 'cluster',
|
|
trustGeneration: 3,
|
|
trustSnapshot: trustSnapshot(),
|
|
publisher: 'publisher-a.example',
|
|
keyId: 'key-a',
|
|
authorizationMode: 'dual_control',
|
|
reasonCode: 'suspected_key_compromise',
|
|
proposedBy: REQUESTER,
|
|
proposerAssurance: 'multi_factor',
|
|
proposalFence: FENCE,
|
|
createdAtMs: 5,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function dispatch(candidate, options = {}) {
|
|
const {
|
|
approvedBy = REVIEWER,
|
|
assurance = 'multi_factor',
|
|
decisionMode = 'separation_of_duty',
|
|
} = options;
|
|
const action = {
|
|
permission: candidate.permission,
|
|
actionType: candidate.actionType,
|
|
actionRef: candidate.actionRef,
|
|
actionDigest: candidate.actionDigest,
|
|
previewDigest: candidate.previewDigest,
|
|
};
|
|
const pending = createApprovalRequest({
|
|
id: `approval-${candidate.actionInput.authorizationMode}`,
|
|
projectId: candidate.projectId,
|
|
action,
|
|
risk: 'critical',
|
|
decisionMode,
|
|
requestedBy: REQUESTER,
|
|
requestedAtMs: 10,
|
|
expiresAtMs: 1_000,
|
|
requestFence: FENCE,
|
|
});
|
|
const approved = decideApprovalRequest(pending, {
|
|
expectedVersion: 1,
|
|
decisionId: `decision-${candidate.actionInput.authorizationMode}`,
|
|
decision: 'approved',
|
|
reasonCode: 'publisher_key_reviewed',
|
|
principal: {
|
|
subject: approvedBy,
|
|
authenticationId: `auth-${approvedBy.id}`,
|
|
authenticatedAtMs: 15,
|
|
expiresAtMs: 500,
|
|
assurance,
|
|
},
|
|
decidedAtMs: 20,
|
|
authorizationFence: FENCE,
|
|
});
|
|
return consumeApprovalRequest(approved, {
|
|
expectedVersion: 2,
|
|
consumptionId: `consume-${candidate.actionInput.authorizationMode}`,
|
|
dispatchId: `dispatch-${candidate.actionInput.authorizationMode}`,
|
|
action,
|
|
requestedBy: REQUESTER,
|
|
consumedBy: SYSTEM,
|
|
consumedAtMs: 30,
|
|
authorizationFence: FENCE,
|
|
}).dispatch;
|
|
}
|
|
|
|
test('derives low-sensitive trust transition and resolves dual-control receipt', () => {
|
|
const candidate = proposal();
|
|
assert.deepEqual(
|
|
normalizePluginPackagePublisherRevocationProposal(candidate),
|
|
candidate,
|
|
);
|
|
assert.notEqual(
|
|
candidate.actionInput.previousTrustDigest,
|
|
candidate.actionInput.currentTrustDigest,
|
|
);
|
|
assert.equal('trustSnapshot' in candidate, false);
|
|
assert.equal('publicKeyPem' in candidate.actionInput, false);
|
|
|
|
const receipt = resolvePluginPackagePublisherRevocationProposal(
|
|
candidate,
|
|
dispatch(candidate),
|
|
40,
|
|
);
|
|
assert.equal(receipt.mutationId, 'dispatch-dual_control');
|
|
assert.deepEqual(receipt.proposer, REQUESTER);
|
|
assert.deepEqual(receipt.confirmer, REVIEWER);
|
|
assert.equal(receipt.authorizationMode, 'dual_control');
|
|
assert.equal(
|
|
receipt.previousTrustDigest,
|
|
candidate.actionInput.previousTrustDigest,
|
|
);
|
|
});
|
|
|
|
test('allows same-subject break-glass only with hardware assurance end to end', () => {
|
|
const candidate = proposal({
|
|
authorizationMode: 'break_glass',
|
|
reasonCode: 'confirmed_key_compromise',
|
|
proposerAssurance: 'hardware',
|
|
});
|
|
const receipt = resolvePluginPackagePublisherRevocationProposal(
|
|
candidate,
|
|
dispatch(candidate, {
|
|
approvedBy: REQUESTER,
|
|
assurance: 'hardware',
|
|
decisionMode: 'human_confirmation',
|
|
}),
|
|
40,
|
|
);
|
|
assert.deepEqual(receipt.proposer, receipt.confirmer);
|
|
assert.throws(
|
|
() =>
|
|
resolvePluginPackagePublisherRevocationProposal(
|
|
candidate,
|
|
{
|
|
...dispatch(candidate, {
|
|
approvedBy: REQUESTER,
|
|
assurance: 'hardware',
|
|
decisionMode: 'human_confirmation',
|
|
}),
|
|
approvalAssurance: 'multi_factor',
|
|
},
|
|
40,
|
|
),
|
|
PluginPackagePublisherRevocationProposalBindingConflictError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
proposal({
|
|
authorizationMode: 'break_glass',
|
|
proposerAssurance: 'multi_factor',
|
|
}),
|
|
InvalidPluginPackagePublisherRevocationProposalError,
|
|
);
|
|
});
|
|
|
|
test('rejects client digest injection, proposal drift and dispatch substitution', () => {
|
|
assert.throws(
|
|
() =>
|
|
proposal({
|
|
previousTrustDigest: '0'.repeat(64),
|
|
currentTrustDigest: '1'.repeat(64),
|
|
}),
|
|
InvalidPluginPackagePublisherRevocationProposalError,
|
|
);
|
|
const candidate = proposal();
|
|
assert.throws(
|
|
() =>
|
|
normalizePluginPackagePublisherRevocationProposal({
|
|
...candidate,
|
|
actionInput: {
|
|
...candidate.actionInput,
|
|
reasonCode: 'confirmed_key_compromise',
|
|
},
|
|
}),
|
|
InvalidPluginPackagePublisherRevocationProposalError,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
resolvePluginPackagePublisherRevocationProposal(
|
|
candidate,
|
|
{
|
|
...dispatch(candidate),
|
|
requestedBy: { type: 'user', id: 'usr_other' },
|
|
},
|
|
40,
|
|
),
|
|
PluginPackagePublisherRevocationProposalBindingConflictError,
|
|
);
|
|
});
|