feat(ql3): make release evidence replay deterministic

This commit is contained in:
whyour
2026-08-18 02:17:26 +08:00
parent 3e7bfa8087
commit cc6d6b7b48
13 changed files with 196 additions and 31 deletions
+2 -1
View File
@@ -1303,6 +1303,7 @@ function auditReleaseWorkflow(source) {
sourceDerived: true,
sameRunRecords: true,
sameRunPrivateEvidenceReceipts: true,
deterministicPrivateEvidenceReceipts: true,
exactScopeClosure: true,
standaloneInspection: true,
tagPromotionAuthority: 'complete_verified_release_set',
@@ -1312,7 +1313,7 @@ function auditReleaseWorkflow(source) {
},
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,
@@ -21,7 +21,7 @@ const {
auditCertManagerSelection,
} = require('./ql3-cert-manager-selection-audit.cjs');
const RECEIPT_SCHEMA = 'qinglong/private-release-evidence-receipt@v1';
const RECEIPT_SCHEMA = 'qinglong/private-release-evidence-receipt@v2';
const EVIDENCE_KINDS = Object.freeze([
'worker-management',
'cloudnativepg-disaster-recovery',
@@ -182,12 +182,12 @@ function assembleReceipt(options) {
fail('private evidence report digest is invalid');
}
const observedAtMs = Date.parse(options.observedAt);
const validatedAtMs = options.nowMs;
const validationClockMs = options.nowMs;
if (
!Number.isSafeInteger(validatedAtMs) ||
!Number.isSafeInteger(validationClockMs) ||
!Number.isFinite(observedAtMs) ||
observedAtMs > validatedAtMs + MAX_FUTURE_SKEW_MS ||
validatedAtMs - observedAtMs > MAX_EVIDENCE_AGE_SECONDS * 1000
observedAtMs > validationClockMs + MAX_FUTURE_SKEW_MS ||
validationClockMs - observedAtMs > MAX_EVIDENCE_AGE_SECONDS * 1000
) {
fail('private evidence is outside the release freshness window');
}
@@ -196,7 +196,7 @@ function assembleReceipt(options) {
? staticAuditReceipts(options.root)
: Object.freeze([]);
const unsigned = {
schemaVersion: 1,
schemaVersion: 2,
schema: RECEIPT_SCHEMA,
release: {
version: options.version,
@@ -208,7 +208,6 @@ function assembleReceipt(options) {
evidence: {
fixture: options.fixture,
observedAt: options.observedAt,
validatedAt: new Date(validatedAtMs).toISOString(),
maximumAgeSeconds: MAX_EVIDENCE_AGE_SECONDS,
reportDigest: options.reportDigest,
sourceReportsUploaded: false,
@@ -217,6 +216,8 @@ function assembleReceipt(options) {
verification: {
sourceAwareAudit: true,
privateEvidenceReplayed: true,
freshnessValidatedAtCreation: true,
durableValidationClockPublished: false,
publicConsumerReplay: 'not_possible_without_private_reports',
privateReportContentPublished: false,
compatible: true,
@@ -323,7 +324,7 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
'verification',
'receiptDigest',
]) ||
actual.schemaVersion !== 1 ||
actual.schemaVersion !== 2 ||
actual.schema !== RECEIPT_SCHEMA ||
!exactKeys(actual.release, [
'version',
@@ -342,7 +343,6 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
!exactKeys(actual.evidence, [
'fixture',
'observedAt',
'validatedAt',
'maximumAgeSeconds',
'reportDigest',
'sourceReportsUploaded',
@@ -363,6 +363,8 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
!exactKeys(actual.verification, [
'sourceAwareAudit',
'privateEvidenceReplayed',
'freshnessValidatedAtCreation',
'durableValidationClockPublished',
'publicConsumerReplay',
'privateReportContentPublished',
'compatible',
@@ -371,6 +373,8 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
JSON.stringify({
sourceAwareAudit: true,
privateEvidenceReplayed: true,
freshnessValidatedAtCreation: true,
durableValidationClockPublished: false,
publicConsumerReplay: 'not_possible_without_private_reports',
privateReportContentPublished: false,
compatible: true,
@@ -378,14 +382,7 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
) {
fail('receipt shape or release binding is invalid');
}
const observedAtMs = Date.parse(actual.evidence.observedAt);
const validatedAtMs = Date.parse(actual.evidence.validatedAt);
if (
!Number.isFinite(observedAtMs) ||
!Number.isFinite(validatedAtMs) ||
observedAtMs > validatedAtMs + MAX_FUTURE_SKEW_MS ||
validatedAtMs - observedAtMs > MAX_EVIDENCE_AGE_SECONDS * 1000
) {
if (!Number.isFinite(Date.parse(actual.evidence.observedAt))) {
fail('receipt freshness binding is invalid');
}
const { receiptDigest, ...unsigned } = actual;
+2 -2
View File
@@ -12,7 +12,7 @@ const {
const CATALOG_PLAN_SCHEMA = 'qinglong/release-catalog-plan@v1';
const CATALOG_RECEIPT_SCHEMA = 'qinglong/release-catalog-receipt@v1';
const ARTIFACT_TYPE = 'application/vnd.qinglong.release-set.v2+json';
const ARTIFACT_TYPE = 'application/vnd.qinglong.release-set.v3+json';
const FILE_MEDIA_TYPE = ARTIFACT_TYPE;
const OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.v1+json';
const OCI_EMPTY_CONFIG_MEDIA_TYPE = 'application/vnd.oci.empty.v1+json';
@@ -173,7 +173,7 @@ function createCatalogPlan(releaseSet, options) {
discoveryTagAuthority: 'none',
immutableDigestAuthority: 'required',
roundTrip: 'byte_exact',
recovery: 'republish_same_content_then_verify_digest',
recovery: 'republish_deterministic_content_then_verify_digest',
},
};
return Object.freeze({
+1 -1
View File
@@ -20,7 +20,7 @@ const { VERSION_PATTERN } = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const IMAGE_RECORD_SCHEMA = 'qinglong/release-set-image-record@v1';
const RELEASE_SET_SCHEMA = 'qinglong/release-set@v2';
const RELEASE_SET_SCHEMA = 'qinglong/release-set@v3';
const MAX_JSON_BYTES = 1024 * 1024;
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
const OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/u;