Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions scripts/data-pipeline/candidate-restore.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export const PINNED_PRE_ATTESTATION_SNAPSHOT = Object.freeze({
"0x5921ceacba6b7d3c636d3571fd7ebe9fad599626d03372836d0e6293e358c597",
structuralManifestSha256:
"0x1546ad4cf2312e3143cf8cd57422f4040924521db4531d2ef2b1a9875f662ef8",
portableStructuralManifestSha256:
"0x0b95ed1e28d2684aa920be5058c7815b604986a611f67d7900c42d181875e80b",
});
export const CANDIDATE_PG_RESTRICT_KEY =
"b3679e8b178535bbc58f9c9c43690a8c7e310ade8bd93360f15004be385b02d2";
Expand Down Expand Up @@ -470,6 +472,9 @@ function validateBackupEvidence(value, {
repositoryCommit,
} = {}) {
const evidence = plainObject(value, "database backup evidence");
const hasPortableStructuralManifest =
Object.hasOwn(evidence, "sourcePortableStructuralManifestSha256") ||
Object.hasOwn(evidence, "restoredPortableStructuralManifestSha256");
if (
evidence.kind !== "programmable-database-backup-restore-evidence" ||
evidence.schemaVersion !== 1 ||
Expand All @@ -485,8 +490,14 @@ function validateBackupEvidence(value, {
evidence.restoredManifestSha256 !== evidence.sourceManifestSha256 ||
(evidence.sourceStructuralManifestSha256 !== undefined &&
(!SHA256.test(evidence.sourceStructuralManifestSha256) ||
!SHA256.test(evidence.restoredStructuralManifestSha256 ?? ""))) ||
(hasPortableStructuralManifest
? !SHA256.test(evidence.sourcePortableStructuralManifestSha256 ?? "") ||
evidence.restoredPortableStructuralManifestSha256 !==
evidence.sourcePortableStructuralManifestSha256
: evidence.sourceStructuralManifestSha256 !== undefined &&
evidence.restoredStructuralManifestSha256 !==
evidence.sourceStructuralManifestSha256)) ||
evidence.sourceStructuralManifestSha256) ||
!Number.isSafeInteger(evidence.tableCount) ||
evidence.tableCount <= 0 ||
!Number.isSafeInteger(evidence.rowCount) ||
Expand Down Expand Up @@ -535,6 +546,12 @@ function backupReference(evidence) {
structuralManifestSha256:
validated.sourceStructuralManifestSha256,
}),
...(validated.sourcePortableStructuralManifestSha256 === undefined
? {}
: {
portableStructuralManifestSha256:
validated.sourcePortableStructuralManifestSha256,
}),
tableCount: validated.tableCount,
rowCount: validated.rowCount,
postgresVersion: validated.postgresVersion,
Expand Down Expand Up @@ -620,6 +637,14 @@ export function buildCandidateSafetyBackupEvidence(input) {
) {
throw new Error("Candidate structure changed after the safety backup");
}
if (
!SHA256.test(backupEvidence.sourcePortableStructuralManifestSha256 ?? "") ||
!SHA256.test(input.currentManifest?.portableStructuralManifestSha256 ?? "") ||
backupEvidence.sourcePortableStructuralManifestSha256 !==
input.currentManifest.portableStructuralManifestSha256
) {
throw new Error("Candidate portable structure changed after the safety backup");
}
if (
input.currentManifest?.tableCount !== backupEvidence.tableCount ||
input.currentManifest?.rowCount !== backupEvidence.rowCount
Expand Down Expand Up @@ -709,6 +734,7 @@ export function validateCandidateSafetyBackupEvidence(value, {
!SHA256.test(backup.archiveListSha256 ?? "") ||
!SHA256.test(backup.manifestSha256 ?? "") ||
!SHA256.test(backup.structuralManifestSha256 ?? "") ||
!SHA256.test(backup.portableStructuralManifestSha256 ?? "") ||
!Number.isSafeInteger(backup.bytes) ||
backup.bytes <= 0 ||
!Number.isSafeInteger(backup.tableCount) ||
Expand Down Expand Up @@ -2084,6 +2110,8 @@ export async function createCandidateRestorePlan(input) {
migrationSourceClosure,
structuralManifestSha256:
PINNED_PRE_ATTESTATION_SNAPSHOT.structuralManifestSha256,
portableStructuralManifestSha256:
PINNED_PRE_ATTESTATION_SNAPSHOT.portableStructuralManifestSha256,
}),
safetyBackup: Object.freeze({
...safety,
Expand All @@ -2099,6 +2127,8 @@ export async function createCandidateRestorePlan(input) {
manifestSha256: snapshot.manifestSha256,
structuralManifestSha256:
PINNED_PRE_ATTESTATION_SNAPSHOT.structuralManifestSha256,
portableStructuralManifestSha256:
PINNED_PRE_ATTESTATION_SNAPSHOT.portableStructuralManifestSha256,
tableCount: snapshot.tableCount,
rowCount: snapshot.rowCount,
databaseMode: "candidate-only",
Expand Down Expand Up @@ -2165,20 +2195,25 @@ export function validateCandidateRestorePlan(value) {
canonicalJson(PINNED_BASELINE_MIGRATION_SOURCE_CLOSURE) ||
plan.snapshot?.structuralManifestSha256 !==
PINNED_PRE_ATTESTATION_SNAPSHOT.structuralManifestSha256 ||
plan.snapshot?.portableStructuralManifestSha256 !==
PINNED_PRE_ATTESTATION_SNAPSHOT.portableStructuralManifestSha256 ||
plan.snapshot?.bytes !== PINNED_PRE_ATTESTATION_SNAPSHOT.bytes ||
canonicalJson(plan.snapshot?.schemas) !== canonicalJson(CANDIDATE_RESTORE_SCHEMAS) ||
!SHA256.test(plan.safetyBackup?.safetyEvidenceSha256 ?? "") ||
!SHA256.test(plan.safetyBackup?.sha256 ?? "") ||
!SHA256.test(plan.safetyBackup?.archiveListSha256 ?? "") ||
!SHA256.test(plan.safetyBackup?.manifestSha256 ?? "") ||
!SHA256.test(plan.safetyBackup?.structuralManifestSha256 ?? "") ||
!SHA256.test(plan.safetyBackup?.portableStructuralManifestSha256 ?? "") ||
!Number.isSafeInteger(plan.safetyBackup?.bytes) ||
plan.safetyBackup.bytes <= 0 ||
canonicalJson(plan.postgresToolchain) !==
canonicalJson(OFFICIAL_TOOLCHAIN_EVIDENCE) ||
plan.postRestore?.manifestSha256 !== plan.snapshot.manifestSha256 ||
plan.postRestore?.structuralManifestSha256 !==
plan.snapshot.structuralManifestSha256 ||
plan.postRestore?.portableStructuralManifestSha256 !==
plan.snapshot.portableStructuralManifestSha256 ||
plan.postRestore?.tableCount !== plan.snapshot.tableCount ||
plan.postRestore?.rowCount !== plan.snapshot.rowCount ||
plan.postRestore?.databaseMode !== "candidate-only" ||
Expand Down Expand Up @@ -2287,8 +2322,9 @@ function assertPostRestore({ plan, manifest, state }) {
const fence = assertCandidateFence(state, "fenced");
if (
manifest?.manifestSha256 !== plan.postRestore.manifestSha256 ||
manifest?.structuralManifestSha256 !==
plan.postRestore.structuralManifestSha256 ||
!SHA256.test(manifest?.structuralManifestSha256 ?? "") ||
manifest?.portableStructuralManifestSha256 !==
plan.postRestore.portableStructuralManifestSha256 ||
manifest?.tableCount !== plan.postRestore.tableCount ||
manifest?.rowCount !== plan.postRestore.rowCount ||
fence.databaseMode !== plan.postRestore.databaseMode ||
Expand Down Expand Up @@ -2481,6 +2517,8 @@ export async function applyCandidateRestore(input) {
currentManifest.manifestSha256 === plan.safetyBackup.manifestSha256 &&
currentManifest.structuralManifestSha256 ===
plan.safetyBackup.structuralManifestSha256 &&
currentManifest.portableStructuralManifestSha256 ===
plan.safetyBackup.portableStructuralManifestSha256 &&
currentManifest.tableCount === plan.safetyBackup.tableCount &&
currentManifest.rowCount === plan.safetyBackup.rowCount;
let restoredStateMatches = false;
Expand Down Expand Up @@ -2570,6 +2608,8 @@ export async function applyCandidateRestore(input) {
tableCount: manifest.tableCount,
rowCount: manifest.rowCount,
structuralManifestSha256: manifest.structuralManifestSha256,
portableStructuralManifestSha256:
manifest.portableStructuralManifestSha256,
}),
migrationCount: restoredMigrationCount,
candidateFence: fence,
Expand Down Expand Up @@ -2771,6 +2811,8 @@ export async function createCandidateSafetyRecoveryPlan(input) {
candidateState: safetyEvidence.currentCandidateState,
manifestSha256: safety.manifestSha256,
structuralManifestSha256: safety.structuralManifestSha256,
portableStructuralManifestSha256:
safety.portableStructuralManifestSha256,
tableCount: safety.tableCount,
rowCount: safety.rowCount,
}),
Expand Down Expand Up @@ -2811,6 +2853,7 @@ export function validateCandidateSafetyRecoveryPlan(value) {
!SHA256.test(plan.safetyBackup?.archiveListSha256 ?? "") ||
!SHA256.test(plan.safetyBackup?.manifestSha256 ?? "") ||
!SHA256.test(plan.safetyBackup?.structuralManifestSha256 ?? "") ||
!SHA256.test(plan.safetyBackup?.portableStructuralManifestSha256 ?? "") ||
!SHA256.test(plan.safetyBackup?.cleanClosureSha256 ?? "") ||
!Number.isSafeInteger(plan.safetyBackup?.cleanClosureStatementCount) ||
plan.safetyBackup.cleanClosureStatementCount < 1 ||
Expand All @@ -2822,9 +2865,12 @@ export function validateCandidateSafetyRecoveryPlan(value) {
plan.safetyBackup.securityClosureStatementCount < 1 ||
!SHA256.test(plan.postRestore?.manifestSha256 ?? "") ||
!SHA256.test(plan.postRestore?.structuralManifestSha256 ?? "") ||
!SHA256.test(plan.postRestore?.portableStructuralManifestSha256 ?? "") ||
plan.postRestore.manifestSha256 !== plan.safetyBackup.manifestSha256 ||
plan.postRestore.structuralManifestSha256 !==
plan.safetyBackup.structuralManifestSha256 ||
plan.postRestore.portableStructuralManifestSha256 !==
plan.safetyBackup.portableStructuralManifestSha256 ||
plan.postRestore.tableCount !== plan.safetyBackup.tableCount ||
plan.postRestore.rowCount !== plan.safetyBackup.rowCount
) {
Expand Down Expand Up @@ -3031,8 +3077,9 @@ export async function applyCandidateSafetyRecovery(input) {
promotedCandidateState(state, plan.currentProductCommit),
) === canonicalJson(plan.postRestore.candidateState) &&
manifest.manifestSha256 === plan.postRestore.manifestSha256 &&
manifest.structuralManifestSha256 ===
plan.postRestore.structuralManifestSha256 &&
SHA256.test(manifest.structuralManifestSha256 ?? "") &&
manifest.portableStructuralManifestSha256 ===
plan.postRestore.portableStructuralManifestSha256 &&
manifest.tableCount === plan.postRestore.tableCount &&
manifest.rowCount === plan.postRestore.rowCount;
} catch {
Expand Down Expand Up @@ -3084,8 +3131,9 @@ export async function applyCandidateSafetyRecovery(input) {
promotedCandidateState(state, plan.currentProductCommit),
) !== canonicalJson(plan.postRestore.candidateState) ||
manifest.manifestSha256 !== plan.postRestore.manifestSha256 ||
manifest.structuralManifestSha256 !==
plan.postRestore.structuralManifestSha256 ||
!SHA256.test(manifest.structuralManifestSha256 ?? "") ||
manifest.portableStructuralManifestSha256 !==
plan.postRestore.portableStructuralManifestSha256 ||
manifest.tableCount !== plan.postRestore.tableCount ||
manifest.rowCount !== plan.postRestore.rowCount
) {
Expand Down Expand Up @@ -3186,8 +3234,9 @@ export function validateCandidateRestoreResult(value) {
PINNED_PRE_ATTESTATION_SNAPSHOT.schemaSqlSha256 ||
result.snapshot?.manifestSha256 !==
PINNED_PRE_ATTESTATION_SNAPSHOT.manifestSha256 ||
result.snapshot?.structuralManifestSha256 !==
PINNED_PRE_ATTESTATION_SNAPSHOT.structuralManifestSha256 ||
!SHA256.test(result.snapshot?.structuralManifestSha256 ?? "") ||
result.snapshot?.portableStructuralManifestSha256 !==
PINNED_PRE_ATTESTATION_SNAPSHOT.portableStructuralManifestSha256 ||
!Number.isSafeInteger(result.snapshot?.tableCount) ||
!Number.isSafeInteger(result.snapshot?.rowCount) ||
!Number.isSafeInteger(result.migrationCount) ||
Expand Down
68 changes: 63 additions & 5 deletions scripts/data-pipeline/candidate-restore.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ const TOOLCHAIN_EVIDENCE = Object.freeze({
...OFFICIAL_POSTGRES_17_TOOLCHAIN,
toolchainSha256: sha256(canonicalJson(OFFICIAL_POSTGRES_17_TOOLCHAIN)),
});
const RESTORED_STRUCTURAL_MANIFEST =
PINNED_PRE_ATTESTATION_SNAPSHOT.structuralManifestSha256;
const HOSTED_RESTORED_STRUCTURAL_MANIFEST = `0x${"9".repeat(64)}`;
const RESTORED_PORTABLE_STRUCTURAL_MANIFEST =
PINNED_PRE_ATTESTATION_SNAPSHOT.portableStructuralManifestSha256;
const SAFETY_MANIFEST = `0x${"e".repeat(64)}`;
const SAFETY_STRUCTURAL_MANIFEST = `0x${"f".repeat(64)}`;
const SAFETY_PORTABLE_STRUCTURAL_MANIFEST = `0x${"d".repeat(64)}`;

const PINNED_SNAPSHOT_EVIDENCE = Object.freeze({
kind: "programmable-database-backup-restore-evidence",
Expand Down Expand Up @@ -161,13 +163,17 @@ function manifest({ restored = false } = {}) {
return restored
? {
manifestSha256: PINNED_PRE_ATTESTATION_SNAPSHOT.manifestSha256,
structuralManifestSha256: RESTORED_STRUCTURAL_MANIFEST,
structuralManifestSha256: HOSTED_RESTORED_STRUCTURAL_MANIFEST,
portableStructuralManifestSha256:
RESTORED_PORTABLE_STRUCTURAL_MANIFEST,
tableCount: PINNED_SNAPSHOT_EVIDENCE.tableCount,
rowCount: PINNED_SNAPSHOT_EVIDENCE.rowCount,
}
: {
manifestSha256: SAFETY_MANIFEST,
structuralManifestSha256: SAFETY_STRUCTURAL_MANIFEST,
portableStructuralManifestSha256:
SAFETY_PORTABLE_STRUCTURAL_MANIFEST,
tableCount: 121,
rowCount: 147_999,
};
Expand Down Expand Up @@ -199,6 +205,10 @@ function rawSafetyEvidence(archive) {
restoredManifestSha256: current.manifestSha256,
sourceStructuralManifestSha256: current.structuralManifestSha256,
restoredStructuralManifestSha256: current.structuralManifestSha256,
sourcePortableStructuralManifestSha256:
current.portableStructuralManifestSha256,
restoredPortableStructuralManifestSha256:
current.portableStructuralManifestSha256,
tableCount: current.tableCount,
rowCount: current.rowCount,
postgresVersion: "PostgreSQL 17.10",
Expand Down Expand Up @@ -543,6 +553,8 @@ test("pinned pre-attestation evidence is exact and rejects every mutation", () =
"0x5921ceacba6b7d3c636d3571fd7ebe9fad599626d03372836d0e6293e358c597",
structuralManifestSha256:
"0x1546ad4cf2312e3143cf8cd57422f4040924521db4531d2ef2b1a9875f662ef8",
portableStructuralManifestSha256:
"0x0b95ed1e28d2684aa920be5058c7815b604986a611f67d7900c42d181875e80b",
});
assert.equal(PINNED_BASELINE_MIGRATION_SOURCE_CLOSURE.length, 29);
assert.deepEqual(
Expand Down Expand Up @@ -608,6 +620,14 @@ test("restore plan binds CA, raw safety evidence, three tools, schemas and flags
plan.safetyBackup.structuralManifestSha256,
SAFETY_STRUCTURAL_MANIFEST,
);
assert.equal(
plan.safetyBackup.portableStructuralManifestSha256,
SAFETY_PORTABLE_STRUCTURAL_MANIFEST,
);
assert.equal(
plan.postRestore.portableStructuralManifestSha256,
RESTORED_PORTABLE_STRUCTURAL_MANIFEST,
);
assert.deepEqual(plan.postgresToolchain, TOOLCHAIN_EVIDENCE);
assert.deepEqual(plan.restore.schemas, CANDIDATE_RESTORE_SCHEMAS);
assert.deepEqual(plan.restore.flags, CANDIDATE_RESTORE_FLAGS);
Expand Down Expand Up @@ -851,6 +871,10 @@ test("safety backup binds structural manifest, CA and exact official toolchain",
evidence.backup.structuralManifestSha256,
SAFETY_STRUCTURAL_MANIFEST,
);
assert.equal(
evidence.backup.portableStructuralManifestSha256,
SAFETY_PORTABLE_STRUCTURAL_MANIFEST,
);
assert.equal(
validateCandidateSafetyBackupEvidence(evidence, {
operatorCommit: OPERATOR_COMMIT,
Expand All @@ -859,6 +883,21 @@ test("safety backup binds structural manifest, CA and exact official toolchain",
}),
evidence,
);
const withoutPortable = structuredClone(files.safetyRawEvidence);
delete withoutPortable.sourcePortableStructuralManifestSha256;
delete withoutPortable.restoredPortableStructuralManifestSha256;
assert.throws(
() => buildSafetyEvidence(withoutPortable),
/portable structure/u,
);
const driftedPortable = structuredClone(files.safetyRawEvidence);
driftedPortable.sourcePortableStructuralManifestSha256 = `0x${"7".repeat(64)}`;
driftedPortable.restoredPortableStructuralManifestSha256 =
driftedPortable.sourcePortableStructuralManifestSha256;
assert.throws(
() => buildSafetyEvidence(driftedPortable),
/portable structure/u,
);
});

test("restore apply resumes postchecks without replaying pg_restore", async (t) => {
Expand Down Expand Up @@ -906,7 +945,14 @@ test("restore apply resumes postchecks without replaying pg_restore", async (t)
});
assert.equal(result.executionMode, "resumed-post-restore-verification");
assert.equal(result.runtimeLoginFence.remainsFenced, true);
assert.equal(result.snapshot.structuralManifestSha256, RESTORED_STRUCTURAL_MANIFEST);
assert.equal(
result.snapshot.structuralManifestSha256,
HOSTED_RESTORED_STRUCTURAL_MANIFEST,
);
assert.equal(
result.snapshot.portableStructuralManifestSha256,
RESTORED_PORTABLE_STRUCTURAL_MANIFEST,
);
assert.equal(validateCandidateRestoreResult(result), result);
assert.equal(stateReads, 2);
assert.equal(manifestReads, 3);
Expand Down Expand Up @@ -1045,6 +1091,10 @@ test("safety recovery plan binds raw evidence, CA, tools and immutable restore s
sha256(canonicalJson(files.safetyRawEvidence)),
);
assert.equal(plan.postRestore.structuralManifestSha256, SAFETY_STRUCTURAL_MANIFEST);
assert.equal(
plan.postRestore.portableStructuralManifestSha256,
SAFETY_PORTABLE_STRUCTURAL_MANIFEST,
);
assert.deepEqual(plan.postgresToolchain, TOOLCHAIN_EVIDENCE);
assert.deepEqual(plan.restore.schemas, CANDIDATE_RESTORE_SCHEMAS);
assert.deepEqual(plan.restore.flags, CANDIDATE_SAFETY_RECOVERY_FLAGS);
Expand Down Expand Up @@ -1095,7 +1145,10 @@ test("safety recovery apply is idempotent and never opens runtime logins", async
fences += 1;
return loginFence();
},
captureDatabaseManifest: async () => manifest(),
captureDatabaseManifest: async () => ({
...manifest(),
structuralManifestSha256: `0x${"8".repeat(64)}`,
}),
}, { recovery: true }),
validateOfficialToolchain: toolchainDependency(files),
fileCommitment: commitmentDependency(files),
Expand All @@ -1105,6 +1158,11 @@ test("safety recovery apply is idempotent and never opens runtime logins", async
},
});
assert.equal(result.executionMode, "already-recovered");
assert.equal(result.manifest.structuralManifestSha256, `0x${"8".repeat(64)}`);
assert.equal(
result.manifest.portableStructuralManifestSha256,
SAFETY_PORTABLE_STRUCTURAL_MANIFEST,
);
assert.equal(result.runtimeLoginFence.remainsFenced, true);
assert.equal(fences, 1);
assert.match(result.evidenceSha256, /^0x[0-9a-f]{64}$/u);
Expand Down
Loading