mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
feat(ql3): make release evidence replay deterministic
This commit is contained in:
@@ -99,6 +99,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
sourceDerived: true,
|
||||
sameRunRecords: true,
|
||||
sameRunPrivateEvidenceReceipts: true,
|
||||
deterministicPrivateEvidenceReceipts: true,
|
||||
exactScopeClosure: true,
|
||||
standaloneInspection: true,
|
||||
tagPromotionAuthority: 'complete_verified_release_set',
|
||||
@@ -108,7 +109,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
||||
},
|
||||
durableCatalog: {
|
||||
repository: 'qinglong3-release-catalog',
|
||||
artifactType: 'application/vnd.qinglong.release-set.v2+json',
|
||||
artifactType: 'application/vnd.qinglong.release-set.v3+json',
|
||||
basenameOnly: true,
|
||||
crossRunnerDeterministic: true,
|
||||
byteExactRoundTrip: true,
|
||||
|
||||
@@ -119,6 +119,70 @@ test('rejects publishing private material in an otherwise re-digested receipt',
|
||||
);
|
||||
});
|
||||
|
||||
test('omits the ephemeral validation clock and replays identical source evidence byte-for-byte', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const reportPath = path.join(directory, 'private-report.json');
|
||||
fs.writeFileSync(
|
||||
reportPath,
|
||||
`${JSON.stringify({ observedAt: '2026-08-18T00:00:00.000Z' })}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
const createAt = (validationTime, outputFile) =>
|
||||
createWorkerReceipt(
|
||||
{
|
||||
root: path.resolve(__dirname, '../..'),
|
||||
version: RELEASE.version,
|
||||
sourceRevision: RELEASE.sourceRevision,
|
||||
sourceRef: RELEASE.sourceRef,
|
||||
releaseScope: RELEASE.scope,
|
||||
reportFile: reportPath,
|
||||
ceremonyReportFile: path.join(directory, 'private-ceremony.json'),
|
||||
durableAuditReportFile: path.join(directory, 'private-durable.json'),
|
||||
pkiRotationReportFile: path.join(directory, 'private-pki.json'),
|
||||
caRolloverReportFile: path.join(directory, 'private-ca.json'),
|
||||
outputFile,
|
||||
},
|
||||
{
|
||||
now: () => Date.parse(validationTime),
|
||||
auditGate(_options, dependencies) {
|
||||
assert.equal(dependencies.now(), Date.parse(validationTime));
|
||||
return {
|
||||
fixture: 'qinglong/worker-credential-management-release-gate@v1',
|
||||
evidenceReportSha256: `sha256:${'5'.repeat(64)}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
const firstPath = path.join(directory, 'first.json');
|
||||
const secondPath = path.join(directory, 'second.json');
|
||||
const first = createAt('2026-08-18T00:05:00.000Z', firstPath);
|
||||
const second = createAt('2026-08-18T00:15:00.000Z', secondPath);
|
||||
assert.deepEqual(second, first);
|
||||
assert.equal(
|
||||
fs.readFileSync(secondPath, 'utf8'),
|
||||
fs.readFileSync(firstPath, 'utf8'),
|
||||
);
|
||||
assert.equal(Object.hasOwn(first.evidence, 'validatedAt'), false);
|
||||
assert.equal(first.verification.freshnessValidatedAtCreation, true);
|
||||
assert.equal(first.verification.durableValidationClockPublished, false);
|
||||
assert.throws(
|
||||
() =>
|
||||
createAt('2026-08-19T00:00:01.000Z', path.join(directory, 'stale.json')),
|
||||
/freshness window/,
|
||||
);
|
||||
|
||||
const widened = JSON.parse(JSON.stringify(first));
|
||||
widened.evidence.validatedAt = '2026-08-18T00:05:00.000Z';
|
||||
assert.throws(
|
||||
() =>
|
||||
inspectPrivateReleaseEvidenceReceipt(
|
||||
widened,
|
||||
options('worker-management'),
|
||||
),
|
||||
/receipt shape/,
|
||||
);
|
||||
});
|
||||
|
||||
test('audit CLI accepts a canonical receipt and rejects open argument shapes', (t) => {
|
||||
const directory = temporaryDirectory(t);
|
||||
const receiptPath = path.join(directory, 'worker-management.json');
|
||||
|
||||
@@ -79,7 +79,7 @@ function releaseSet(scope) {
|
||||
const names = images.map((entry) => entry.name);
|
||||
const unsigned = {
|
||||
schemaVersion: 1,
|
||||
schema: 'qinglong/release-set@v2',
|
||||
schema: 'qinglong/release-set@v3',
|
||||
release,
|
||||
candidate: {
|
||||
schema: 'qinglong/release-candidate-contract@v1',
|
||||
|
||||
@@ -121,6 +121,10 @@ test('plans deterministic Local, Cluster and All OCI catalog entries', () => {
|
||||
`ghcr.io/qinglong-release/qinglong3-release-catalog:v${version}-${scope}`,
|
||||
);
|
||||
assert.equal(plan.catalog.artifactType, ARTIFACT_TYPE);
|
||||
assert.equal(
|
||||
plan.publicationPolicy.recovery,
|
||||
'republish_deterministic_content_then_verify_digest',
|
||||
);
|
||||
assert.equal(
|
||||
plan.releaseSet.bytes,
|
||||
Buffer.byteLength(`${JSON.stringify(set)}\n`),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
const RECEIPT_SCHEMA = 'qinglong/private-release-evidence-receipt@v1';
|
||||
const RECEIPT_SCHEMA = 'qinglong/private-release-evidence-receipt@v2';
|
||||
const FIXTURES = Object.freeze({
|
||||
'worker-management': 'qinglong/worker-credential-management-release-gate@v1',
|
||||
'cloudnativepg-disaster-recovery':
|
||||
@@ -28,14 +28,13 @@ function privateReleaseEvidenceReceipt(release, evidenceKind) {
|
||||
}))
|
||||
: [];
|
||||
const unsigned = {
|
||||
schemaVersion: 1,
|
||||
schemaVersion: 2,
|
||||
schema: RECEIPT_SCHEMA,
|
||||
release: { ...release },
|
||||
evidenceKind,
|
||||
evidence: {
|
||||
fixture: FIXTURES[evidenceKind],
|
||||
observedAt: '2026-08-18T00:00:00.000Z',
|
||||
validatedAt: '2026-08-18T00:05:00.000Z',
|
||||
maximumAgeSeconds: 86_400,
|
||||
reportDigest: `sha256:${
|
||||
evidenceKind === 'worker-management' ? '5' : '6'
|
||||
@@ -46,6 +45,8 @@ function privateReleaseEvidenceReceipt(release, evidenceKind) {
|
||||
verification: {
|
||||
sourceAwareAudit: true,
|
||||
privateEvidenceReplayed: true,
|
||||
freshnessValidatedAtCreation: true,
|
||||
durableValidationClockPublished: false,
|
||||
publicConsumerReplay: 'not_possible_without_private_reports',
|
||||
privateReportContentPublished: false,
|
||||
compatible: true,
|
||||
|
||||
Reference in New Issue
Block a user