Retain verified recovery proof independently and bind the protected point
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
This commit is contained in:
parent
e1918b3f88
commit
09f4fd33cb
7 changed files with 257 additions and 11 deletions
|
|
@ -41,5 +41,33 @@
|
|||
"automatic_expiry_enabled": false
|
||||
}
|
||||
},
|
||||
"native_application_recovery_verified": false
|
||||
"native_application_recovery_verified": true,
|
||||
"protected_recovered_point": {
|
||||
"captured_at": "2026-10-29T21:22:46Z",
|
||||
"primary_object_key": "platform-pg/application-archives/coulomb-social/2026/10/29/20261029T212246Z-fd128164c7bb22a3a6c42acf.tar.age",
|
||||
"primary_version_id": "1793309020705518",
|
||||
"ciphertext_sha256": "acbf92eda5074761bb6d00170e6e459e787359afcd377f2f18324ba17496510a",
|
||||
"protected_kit": {
|
||||
"automatic_expiry": false,
|
||||
"bucket": "railiance-coulomb-social-recovery",
|
||||
"bytes": 277585920,
|
||||
"object_key": "kits/db05c8f3b5587dc1fc09a5352c317dd3357527dbe72366ab7ca1041db578c834/recovery-kit.tar",
|
||||
"project_id": "06d107bc-9d84-4672-b2a0-95701cc0c07f",
|
||||
"schema": "platform.coulomb-protected-recovery-kit.v1",
|
||||
"sha256": "db05c8f3b5587dc1fc09a5352c317dd3357527dbe72366ab7ca1041db578c834",
|
||||
"version_id": "1793309971213507"
|
||||
},
|
||||
"recovery_seconds": 30.232,
|
||||
"proof": "docs/evidence/2026-10-29-coulomb-provider-kit-recovery.json",
|
||||
"protected_recovery_proof": {
|
||||
"project_id": "06d107bc-9d84-4672-b2a0-95701cc0c07f",
|
||||
"bucket": "railiance-coulomb-social-recovery",
|
||||
"object_key": "kits/receipts/a77477cac53086b83ad2cb512973ae0d1dfecba028a29a92e06a1bcfe5287f29/recovery-proof.json",
|
||||
"version_id": "1793310476676486",
|
||||
"sha256": "a77477cac53086b83ad2cb512973ae0d1dfecba028a29a92e06a1bcfe5287f29",
|
||||
"bytes": 4237,
|
||||
"independent_readback_verified": true,
|
||||
"automatic_expiry": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,10 +33,11 @@ Negative PUT probes use separate transports because a denied upload can leave
|
|||
the provider connection unusable for its next request.
|
||||
|
||||
Both buckets are private and versioned; no automatic expiry has been enabled.
|
||||
The protected bucket never participates in rolling expiry. Rolling retention,
|
||||
capacity, first coherent app capture, independent exact-version decryption and
|
||||
isolated application recovery, monitoring and timers remain WP-0053/CSOC-WP-0012
|
||||
acceptance work. Synthetic fixtures are boundary evidence, not app backups.
|
||||
The protected bucket never participates in rolling expiry. First coherent app
|
||||
capture, independent exact-version decryption and isolated application recovery
|
||||
are now verified below. Rolling retention, capacity, copy disposition, monitoring
|
||||
and timers remain WP-0053/CSOC-WP-0012 acceptance work. The earlier synthetic
|
||||
fixtures remain boundary evidence, distinct from this real application backup.
|
||||
|
||||
The routine pairs and age identity each have a separate protected, enabled,
|
||||
nonexpiring provider secret version. They survive node/workstation loss. If all
|
||||
|
|
@ -51,3 +52,18 @@ deadlines, using a bounded attended construction window. No standing organizatio
|
|||
administration key or publisher IAM administration is admitted. The construction
|
||||
key was revoked, its temporary pair removed and the personal browser context
|
||||
closed without saving state. Routine drills do not rerun identity bootstrap.
|
||||
|
||||
The first provider-only kit recovery is now verified in30.232s monotonic. The
|
||||
public JSON contract records the complete protected kit, exact primary recovery
|
||||
point and separately protected recovery-proof coordinates. Native receipt:
|
||||
../evidence/2026-10-29-coulomb-provider-kit-recovery.json. All2098kit members,
|
||||
13tables/112rows/sequences,82files and39rendered pages/access denials pass.
|
||||
Images/tools/data came from the kit; the age identity came from separate escrow.
|
||||
Held API callers and the existing Docker daemon were used. Account-recovery
|
||||
latency, a fresh daemon and real-browser identity journeys are excluded.
|
||||
|
||||
application_recovery_drill.py previews without effects and executes only with
|
||||
--execute, --kit-reference and a fresh --destination under an owner-private
|
||||
directory outside Git. It never restores production. Retention/copy disposition,
|
||||
monitoring and scheduled activation remain open; this receipt closes only the
|
||||
consumer's first-paired-recovery task.
|
||||
|
|
|
|||
117
docs/evidence/2026-10-29-coulomb-provider-kit-recovery.json
Normal file
117
docs/evidence/2026-10-29-coulomb-provider-kit-recovery.json
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
{
|
||||
"age_identity_from_separate_provider_escrow": true,
|
||||
"application": "coulomb-social",
|
||||
"application_checks": {
|
||||
"actual_pages_rendered_through_view": 39,
|
||||
"content_files_unchanged": 82,
|
||||
"database_restore": {
|
||||
"content_files": 82,
|
||||
"content_hashes_match": true,
|
||||
"content_was_empty": false,
|
||||
"database_container": "csoc-recovery-20261029214329",
|
||||
"database_rows": 112,
|
||||
"database_rows_and_sequences_match": true,
|
||||
"database_tables": 13,
|
||||
"elapsed_seconds": 4.43,
|
||||
"network": "csoc-recovery-20261029214329-net",
|
||||
"network_internal": true,
|
||||
"postgres_image_id": "sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70",
|
||||
"published_ports": 0,
|
||||
"restored_directory": "/tmp/csoc-recovery-20261029214329-0rvudi86",
|
||||
"status": "restored_verified"
|
||||
},
|
||||
"external_identity_provider_used": false,
|
||||
"fixture_transaction_rolled_back": true,
|
||||
"packaged_application_image": "sha256:9a646d8e01a61ba5316d3c2eb0273064e17f2d0300ad07af766ccedda65ecb18",
|
||||
"production_access": false,
|
||||
"real_browser_journey_verified": false,
|
||||
"recovered_spaces_with_content": 1,
|
||||
"restored_owner_access_verified": true,
|
||||
"same_tenant_ungranted_page_denials": 39,
|
||||
"status": "isolated_application_recovery_verified",
|
||||
"synthetic_viewer_mutation_denials": 1,
|
||||
"synthetic_viewer_reads": 1
|
||||
},
|
||||
"application_restore_verified": true,
|
||||
"captured_at": "2026-10-29T21:22:46Z",
|
||||
"completed_at": "2026-10-29T21:43:40.590019+00:00",
|
||||
"fresh_docker_daemon": false,
|
||||
"kit_members_verified": 2098,
|
||||
"operator_account_recovery_time_included": false,
|
||||
"original_node_or_plaintext_used": false,
|
||||
"personal_session_used": false,
|
||||
"protected_kit": {
|
||||
"automatic_expiry": false,
|
||||
"bucket": "railiance-coulomb-social-recovery",
|
||||
"bytes": 277585920,
|
||||
"object_key": "kits/db05c8f3b5587dc1fc09a5352c317dd3357527dbe72366ab7ca1041db578c834/recovery-kit.tar",
|
||||
"project_id": "06d107bc-9d84-4672-b2a0-95701cc0c07f",
|
||||
"schema": "platform.coulomb-protected-recovery-kit.v1",
|
||||
"sha256": "db05c8f3b5587dc1fc09a5352c317dd3357527dbe72366ab7ca1041db578c834",
|
||||
"version_id": "1793309971213507"
|
||||
},
|
||||
"real_browser_journey_verified": false,
|
||||
"recovery_seconds": 30.232,
|
||||
"registry_pulls_performed": false,
|
||||
"restore": {
|
||||
"content_files": 82,
|
||||
"content_hashes_match": true,
|
||||
"content_was_empty": false,
|
||||
"database_container": "csoc-recovery-20261029214329",
|
||||
"database_rows": 112,
|
||||
"database_rows_and_sequences_match": true,
|
||||
"database_tables": 13,
|
||||
"elapsed_seconds": 4.43,
|
||||
"network": "csoc-recovery-20261029214329-net",
|
||||
"network_internal": true,
|
||||
"postgres_image_id": "sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70",
|
||||
"published_ports": 0,
|
||||
"restored_directory": "/tmp/csoc-recovery-20261029214329-0rvudi86",
|
||||
"status": "restored_verified"
|
||||
},
|
||||
"schema": "platform.coulomb-provider-kit-recovery.v1",
|
||||
"stages_seconds": {
|
||||
"all_kit_member_hashes_and_extraction": 2.501,
|
||||
"application_image_load": 1.229,
|
||||
"decrypt_and_validate_pair": 0.121,
|
||||
"isolated_database_content_restore": 4.552,
|
||||
"packaged_app_views_and_permissions": 6.555,
|
||||
"postgres_image_load": 1.754,
|
||||
"provider_age_escrow_retrieval": 0.503,
|
||||
"provider_kit_retrieval": 12.373
|
||||
},
|
||||
"started_at": "2026-10-29T21:43:07.435367+00:00",
|
||||
"status": "application_restored_verified",
|
||||
"timers_activated": false,
|
||||
"unpack": {
|
||||
"application_restore_verified": false,
|
||||
"captured_at": "2026-10-29T21:22:46Z",
|
||||
"content_sha256": "2e81fa5fe2e1a82dfdec4df855a8c5643f59573cc0418a4a26f908d87727b125",
|
||||
"database_sha256": "93f8f01edc9b9a2cd9580d0bab2048d1f5a8e2394e7fa266d419636f502e15f1",
|
||||
"encrypted_sha256": "acbf92eda5074761bb6d00170e6e459e787359afcd377f2f18324ba17496510a",
|
||||
"status": "decrypted_pair_verified",
|
||||
"version_id": "1793309020705518"
|
||||
},
|
||||
"object_key": "platform-pg/application-archives/coulomb-social/2026/10/29/20261029T212246Z-fd128164c7bb22a3a6c42acf.tar.age",
|
||||
"version_id": "1793309020705518",
|
||||
"ciphertext_sha256": "acbf92eda5074761bb6d00170e6e459e787359afcd377f2f18324ba17496510a",
|
||||
"ciphertext_bytes": 2120392,
|
||||
"image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:9a646d8e01a61ba5316d3c2eb0273064e17f2d0300ad07af766ccedda65ecb18",
|
||||
"age_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3",
|
||||
"database_sha256": "93f8f01edc9b9a2cd9580d0bab2048d1f5a8e2394e7fa266d419636f502e15f1",
|
||||
"content_sha256": "2e81fa5fe2e1a82dfdec4df855a8c5643f59573cc0418a4a26f908d87727b125",
|
||||
"application_data_images_and_recovery_tools_from_provider_kit": true,
|
||||
"bootstrap_api_client_preinstalled": true,
|
||||
"executed_drill_source": "e1918b3",
|
||||
"metadata_enriched_from_hash_verified_kit": true,
|
||||
"protected_recovery_proof": {
|
||||
"project_id": "06d107bc-9d84-4672-b2a0-95701cc0c07f",
|
||||
"bucket": "railiance-coulomb-social-recovery",
|
||||
"object_key": "kits/receipts/a77477cac53086b83ad2cb512973ae0d1dfecba028a29a92e06a1bcfe5287f29/recovery-proof.json",
|
||||
"version_id": "1793310476676486",
|
||||
"sha256": "a77477cac53086b83ad2cb512973ae0d1dfecba028a29a92e06a1bcfe5287f29",
|
||||
"bytes": 4237,
|
||||
"independent_readback_verified": true,
|
||||
"automatic_expiry": false
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ from application_recovery_identities import (
|
|||
private_directory,
|
||||
private_json,
|
||||
)
|
||||
from application_recovery_kit import PROJECT, download
|
||||
from application_recovery_kit import PROJECT, download, protect_receipt
|
||||
from application_recovery_resources import API, s3, status
|
||||
|
||||
|
||||
|
|
@ -133,6 +133,7 @@ class Drill:
|
|||
"--output", str(self.root / "application-receipt.json")]))
|
||||
app = json.loads(app)
|
||||
baseline = json.loads((contents / "application-recovery.json").read_text())
|
||||
point = json.loads((contents / "primary-receipt.json").read_text())
|
||||
for key in ("actual_pages_rendered_through_view", "same_tenant_ungranted_page_denials", "content_files_unchanged"):
|
||||
if app.get(key) != baseline.get(key):
|
||||
raise Refused("kit_application_baseline_differs")
|
||||
|
|
@ -143,11 +144,16 @@ class Drill:
|
|||
"recovery_seconds": round(time.monotonic() - began, 3), "protected_kit": self.reference,
|
||||
"kit_members_verified": len(manifest["files"]) + 1, "application": "coulomb-social",
|
||||
"captured_at": manifest["captured_at"], "application_restore_verified": True,
|
||||
"all_data_images_and_tools_from_independent_provider_kit": True,
|
||||
"application_data_images_and_recovery_tools_from_provider_kit": True,
|
||||
"bootstrap_api_client_preinstalled": True,
|
||||
"age_identity_from_separate_provider_escrow": True, "original_node_or_plaintext_used": False,
|
||||
"personal_session_used": False, "registry_pulls_performed": False, "fresh_docker_daemon": False,
|
||||
"operator_account_recovery_time_included": False, "real_browser_journey_verified": False,
|
||||
"unpack": unpacked, "restore": restored, "application_checks": app, "timers_activated": False}
|
||||
result.update({k: point[k] for k in ("object_key", "version_id", "ciphertext_sha256", "ciphertext_bytes",
|
||||
"image", "age_recipient", "database_sha256", "content_sha256")})
|
||||
result["protected_recovery_proof"] = protect_receipt(
|
||||
s3(private_json(self.directory / "custodian-pair.json"), PROJECT), reader, result)
|
||||
checkpoint(self.root / "recovery-receipt.json", result)
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -49,8 +49,7 @@ def download(client, ref, output):
|
|||
return dict(ref, independently_retrieved_hash_verified=True, automatic_expiry=False)
|
||||
|
||||
|
||||
def publish(client, source):
|
||||
archive.regular_file(source)
|
||||
def require_protected_bucket(client):
|
||||
if client.get_bucket_versioning(Bucket=BUCKET).get("Status") != "Enabled":
|
||||
raise Refused("protected_bucket_versioning_required")
|
||||
try:
|
||||
|
|
@ -61,6 +60,40 @@ def publish(client, source):
|
|||
lifecycle = []
|
||||
if lifecycle:
|
||||
raise Refused("protected_bucket_must_have_no_lifecycle_expiry")
|
||||
|
||||
|
||||
def protect_receipt(writer, reader, receipt):
|
||||
"""Retain the recovery proof independently, without a self-hash cycle."""
|
||||
require_protected_bucket(writer)
|
||||
payload = json.dumps(receipt, sort_keys=True, separators=(",", ":")).encode()
|
||||
if len(payload) > 1024**2 or receipt.get("status") != "application_restored_verified":
|
||||
raise Refused("bounded_successful_recovery_proof_required")
|
||||
expected = hashlib.sha256(payload).hexdigest()
|
||||
key = "kits/receipts/" + expected + "/recovery-proof.json"
|
||||
try:
|
||||
head = writer.head_object(Bucket=BUCKET, Key=key)
|
||||
except Exception as error:
|
||||
if status(error) != 404:
|
||||
raise
|
||||
response = writer.put_object(Bucket=BUCKET, Key=key, Body=payload,
|
||||
IfNoneMatch="*", Metadata={"sha256": expected}, ContentType="application/json")
|
||||
version = archive.require_version(response.get("VersionId"))
|
||||
else:
|
||||
if head["ContentLength"] != len(payload) or head.get("Metadata", {}).get("sha256") != expected:
|
||||
raise Refused("existing_proof_binding_differs")
|
||||
version = archive.require_version(head.get("VersionId"))
|
||||
response = reader.get_object(Bucket=BUCKET, Key=key, VersionId=version)
|
||||
with response["Body"] as stream:
|
||||
if response.get("VersionId") != version or stream.read(len(payload) + 1) != payload:
|
||||
raise Refused("independent_proof_readback_differs")
|
||||
return {"project_id": PROJECT, "bucket": BUCKET, "object_key": key, "version_id": version,
|
||||
"sha256": expected, "bytes": len(payload), "independent_readback_verified": True,
|
||||
"automatic_expiry": False}
|
||||
|
||||
|
||||
def publish(client, source):
|
||||
archive.regular_file(source)
|
||||
require_protected_bucket(client)
|
||||
size = source.stat().st_size
|
||||
if not 0 < size <= MAX_BYTES:
|
||||
raise Refused("bounded_kit_required")
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ class Provider:
|
|||
def put_object(self, **args):
|
||||
self.calls.append("put")
|
||||
assert args["Bucket"] == kit.BUCKET and args["IfNoneMatch"] == "*"
|
||||
self.data = args["Body"].read()
|
||||
body = args["Body"]
|
||||
self.data = body.read() if hasattr(body, "read") else body
|
||||
return {"VersionId": self.version}
|
||||
|
||||
def get_object(self, **args):
|
||||
|
|
@ -104,3 +105,16 @@ def test_protected_bucket_expiry_refuses_before_upload(fixture):
|
|||
with pytest.raises(kit.Refused, match="no_lifecycle_expiry"):
|
||||
kit.publish(client, source)
|
||||
assert client.calls == []
|
||||
|
||||
|
||||
def test_recovery_proof_is_conditionally_retained_and_independently_verified():
|
||||
client = Provider()
|
||||
receipt = {"status": "application_restored_verified", "application": "coulomb-social"}
|
||||
ref = kit.protect_receipt(client, client, receipt)
|
||||
assert ref["independent_readback_verified"] is True
|
||||
assert ref["object_key"] == "kits/receipts/" + ref["sha256"] + "/recovery-proof.json"
|
||||
assert kit.protect_receipt(client, client, receipt) == ref
|
||||
assert client.calls.count("put") == 1
|
||||
client.corrupt = True
|
||||
with pytest.raises(kit.Refused, match="proof_readback_differs"):
|
||||
kit.protect_receipt(client, client, receipt)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Do not mark the warden offsite workload identity resolved without a declaration.
|
|||
id: RPF-WP-0053-T02
|
||||
status: progress
|
||||
priority: high
|
||||
intervention_note: "Publisher delivery and independent scoped API custody are approved and native-verified. Retention/capacity, protected app kit/last recovered point and historical-copy disposition remain open; no repeat credential or schedule approval is needed."
|
||||
intervention_note: "Publisher delivery, independent scoped API custody and protected app kit/last recovered point are native-verified. Retention/capacity and historical-copy disposition remain open; no repeat credential or schedule approval is needed."
|
||||
state_hub_task_id: "b06f1a44-1a55-588c-9c6c-3c1e290544df"
|
||||
```
|
||||
|
||||
|
|
@ -401,3 +401,35 @@ as generic-api-key because of its metadata field name. The exact finding was
|
|||
line2, containing only the already public approval reference. Renamed that field
|
||||
to programmatic_direction_reference; no scanner rule, ignore or bypass changed.
|
||||
Only unpublished commits are amended; published history is preserved.
|
||||
|
||||
## First paired and protected provider-only recovery — 2026-10-29
|
||||
|
||||
Admitted worker capture resumed the app before encryption/upload;54.3s pause/capture,
|
||||
2,120,392byte ciphertext, immutable primary exact-version/hash readback. The new
|
||||
application_recovery_mirror.py reuses the existing multipart/conditional-completion
|
||||
engine through an exact Coulomb-only bucket/key adapter. Its independent reader
|
||||
fetched the mirror; custodian escrow supplied only the dedicated age identity.
|
||||
SQL restore matches13tables/112rows/sequences and82files. Actual packaged app views
|
||||
render39pages; restored owner, viewer read/edit403 and ungranted404 checks pass.
|
||||
|
||||
application_recovery_kit.py publishes only new hash-addressed kits and checks live
|
||||
versioning/no lifecycle before publication. The full277,585,920byte kit includes
|
||||
encrypted data, exact images/tools/SDK/receipts and no private identity/API pair.
|
||||
application_recovery_drill.py independently downloads/verifies all2098members,
|
||||
retrieves the separate key, loads the retained images without registry pulls,
|
||||
decrypts/restores and repeats all actual app checks in30.232s monotonic. Original
|
||||
node/plaintext not used. Existing Docker daemon and held API callers were used;
|
||||
operator account recovery time/fresh-daemon proof and real-browser identity
|
||||
journeys are excluded. No production restore/expiry/timer activation.
|
||||
|
||||
The successful proof is itself hash-addressed/conditionally created under the
|
||||
protected kits/receipts/ prefix and independently version/hash read back. The
|
||||
API contract records that proof coordinate as well as the kit and recovered
|
||||
primary point. Later automated drills retain the same independent proof.
|
||||
|
||||
Exact protected point and kit coordinates are in the public API contract; receipt:
|
||||
docs/evidence/2026-10-29-coulomb-provider-kit-recovery.json. Consumer CSOC-WP-0012-T02
|
||||
is done. Owner T02 retention/capacity/copy disposition and T03/T04 scheduled and
|
||||
monitored operation remain open. Preserve isolated recovery fixtures for the
|
||||
pending actual-data storage-migration rehearsal; do not treat their copies as
|
||||
another independent offsite location or delete historical plaintext pairs.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue