Align Vergabe publication with existing Nextcloud DAV routes
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1abf1-7e95-7141-87c9-ee78332bf790
This commit is contained in:
parent
473187c114
commit
84907a39f5
6 changed files with 158 additions and 30 deletions
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"schema": "railiance.vergabe-pilot-provider-recovery.v1",
|
||||
"run_id": "20261105T233543Z-89bfbe04dece",
|
||||
"status": "failed",
|
||||
"provider_publication_verified": false,
|
||||
"decryption_verified": false,
|
||||
"restore_verified": false,
|
||||
"application_changed": false,
|
||||
"stage": "owner_custody",
|
||||
"owner_handoff_checks": {
|
||||
"routing_marker_present": true,
|
||||
"operator_tunnel_matches": true,
|
||||
"no_ambient_token_variables": true,
|
||||
"isolated_home_shape": true,
|
||||
"isolated_home_private": true
|
||||
},
|
||||
"failure": "upload_lane_binding_changed",
|
||||
"elapsed_seconds": 0.6,
|
||||
"warden_exit_code": 11,
|
||||
"receipt_file": "/home/worsch/.local/state/vergabe-pilot-recovery/20261105T233543Z-89bfbe04dece/provider-receipt-603935659e98.json",
|
||||
"session_cleanup_confirmed": null,
|
||||
"session_revocation_confirmed": null,
|
||||
"session_storage_cleanup_confirmed": true,
|
||||
"evidence_kind": "user_reported_terminal_output",
|
||||
"reported_on": "2026-11-06",
|
||||
"workplan": "RAPPS-WP-0014",
|
||||
"task": "RAPPS-WP-0014-T03",
|
||||
"source_revision": "473187c1141a87c8e05ca01a4cf3daae22a81b10",
|
||||
"review": {
|
||||
"login_and_handoff_passed": true,
|
||||
"session_revoked_user_reported": true,
|
||||
"owner_identity_and_two_custody_reads_succeeded_per_source_order": true,
|
||||
"provider_calls_reached": false,
|
||||
"obsolete_validator_accepted_only": "filesdrop",
|
||||
"existing_owner_contract_routes": [
|
||||
"files",
|
||||
"filesdrop"
|
||||
],
|
||||
"actual_credential_bearing_url_not_requested_or_logged": true,
|
||||
"actual_route_not_reported_in_original_failure": true,
|
||||
"owner_sources": [
|
||||
"railiance-platform/scripts/root_custody_finish.py:roundtrip",
|
||||
"railiance-platform/scripts/openbao-reboot-snapshot.py"
|
||||
],
|
||||
"fix": "Accept only the two existing exact provider/share-token-bound route forms; retain provider, object, credential and no-overwrite scope."
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,16 @@ offline test through the installed Warden envelope reproduces the inherited
|
|||
alias refusal and passes with the corrected preparation. Retry the same command
|
||||
below; the previous session was revoked. Provider scope and ciphertext are unchanged.
|
||||
|
||||
Second native attempt: [the founder return](../../evidence/2026-11-06-vergabe-upload-binding-failure-return.json)
|
||||
passes all five handoff checks and the custody reads, then stops at
|
||||
`upload_lane_binding_changed` before any provider call. The publisher had copied
|
||||
the older migration helper's filesdrop-only assumption. Existing platform owner
|
||||
recovery tools accept both `/public.php/dav/files/{share_token}` and the legacy
|
||||
`/public.php/dav/filesdrop/{share_token}`. Revision 3 accepts exactly those two
|
||||
forms on the approved provider with the same share token. Foreign origins,
|
||||
different tokens, extra path segments, queries and fragments refuse. No raw
|
||||
credential-bearing address was requested or logged. Retry the same command.
|
||||
|
||||
## Founder workstation command
|
||||
|
||||
```bash
|
||||
|
|
@ -121,7 +131,7 @@ No second application pause or recapture is needed for this point.
|
|||
|
||||
## Validation
|
||||
|
||||
Fifteen publisher checks cover real age encrypt/decrypt and complete packet/media/
|
||||
Seventeen publisher checks cover real age encrypt/decrypt and complete packet/media/
|
||||
SQLite verification with mocked custody/provider; separate upload/readback
|
||||
credentials; identical-object retry; different-object preservation; wrong escrow,
|
||||
quota and tampered-download refusal; credential-safe silent failure; ambient
|
||||
|
|
@ -129,5 +139,6 @@ token refusal; pinned owner helpers; foreign-origin denial and bounded readback.
|
|||
The additional cases test all token aliases, API/effective-policy handling,
|
||||
separate session outcomes and the installed Warden envelope using an offline
|
||||
fake Bao client. They issue no native session and inspect no real helper or
|
||||
credential. All 45 pilot helper checks pass. Native provider publication and
|
||||
credential. Both existing URL forms pass the synthetic publication/readback/
|
||||
decryption flow. All 47 pilot helper checks pass. Native provider publication and
|
||||
decryption remain unverified until a successful founder receipt returns.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"run_id": "20261105T233543Z-89bfbe04dece",
|
||||
"ciphertext_sha256": "4915288384e3ba8a87f1427b8e10106ee7241c04184adb97f439e57846bef76f",
|
||||
"ciphertext_bytes": 182521,
|
||||
"publisher_source_sha256": "8815272ed09ffce91e2a1551c646285d8aea844f318fc188c986d15f4093f807",
|
||||
"publisher_source_sha256": "4340f386e448984b2b34d13d59f318ca4dda35b218a2b28cd034dcd00881244e",
|
||||
"platform_helper_pins": {
|
||||
"openbao-attended-exec.py": "53fb5fb38ad17c4b06d7969a4d8968d976c8ebc8f1abfeb9a191aa43dc54a5c3",
|
||||
"migrate_nextcloud_backup_account.py": "d3285f403f3c947f75c3288d989152e59ab9a8f5542bcf901bd0a06b0f3e050c",
|
||||
|
|
@ -53,18 +53,19 @@
|
|||
"recovered_data": "Private provider-recovery-* directory inside the existing workstation kit, outside Git, retained for the isolated restore rehearsal",
|
||||
"failure_handling": "Safe receipt with phase flags; different object bytes refuse without overwrite. Retry reads the deterministic object name. Warden exit and cleanup confirmation are recorded separately from data verification.",
|
||||
"validation": {
|
||||
"publisher_cases": 15,
|
||||
"publisher_cases": 17,
|
||||
"synthetic_only": true,
|
||||
"actual_age_round_trip": true,
|
||||
"provider_and_custody_mocked": true,
|
||||
"native_provider_publication_executed": false,
|
||||
"total_pilot_cases": 45,
|
||||
"total_pilot_cases": 47,
|
||||
"real_installed_warden_offline_fixture": true,
|
||||
"synthetic_bao_only": true,
|
||||
"native_login_used_by_tests": false
|
||||
"native_login_used_by_tests": false,
|
||||
"both_existing_routes_end_to_end_synthetic": true
|
||||
},
|
||||
"previous_publisher_source_sha256": "845294c574692333b35e627f5d3194e6334136f14f4f699e27a680f9cd4604db",
|
||||
"revision": "r2-owner-handoff",
|
||||
"previous_publisher_source_sha256": "8815272ed09ffce91e2a1551c646285d8aea844f318fc188c986d15f4093f807",
|
||||
"revision": "r3-upload-route-contract",
|
||||
"handoff_fix": {
|
||||
"clear_all_token_aliases_before_login": [
|
||||
"BAO_TOKEN",
|
||||
|
|
@ -77,5 +78,19 @@
|
|||
"effective_policies": "Union token and identity policies; root/agent boundary still refuse",
|
||||
"nonzero_child_exit_does_not_imply_revocation_failure": true,
|
||||
"same_provider_scope": true
|
||||
},
|
||||
"upload_url_contract": {
|
||||
"origin": "https://nx4069.your-storageshare.de",
|
||||
"accepted_paths": [
|
||||
"/public.php/dav/files/{share_token}",
|
||||
"/public.php/dav/filesdrop/{share_token}"
|
||||
],
|
||||
"same_share_token_required": true,
|
||||
"extra_paths_query_fragments_foreign_origins_refused": true,
|
||||
"owner_sources": [
|
||||
"railiance-platform/scripts/root_custody_finish.py:roundtrip",
|
||||
"railiance-platform/scripts/openbao-reboot-snapshot.py"
|
||||
],
|
||||
"new_provider_scope": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,21 +97,26 @@ class PublisherTests(unittest.TestCase):
|
|||
return Fake()
|
||||
|
||||
def test_real_crypto_roundtrip_provider_readback_and_complete_packet_check(self):
|
||||
provider = self.provider()
|
||||
receipt = {"restore_verified": False}
|
||||
publish.publish(self.directory, self.credentials, provider, receipt)
|
||||
self.assertEqual(receipt["status"], "published_read_back_and_decrypted")
|
||||
self.assertTrue(receipt["independent_readback_verified"])
|
||||
self.assertTrue(receipt["sqlite_link_verified"])
|
||||
self.assertTrue(receipt["packet_file_hashes_verified"])
|
||||
self.assertFalse(receipt["database_restored"])
|
||||
self.assertFalse(receipt["restore_verified"])
|
||||
self.assertEqual(len(provider.put_calls), 1)
|
||||
self.assertEqual(len(provider.get_calls), 2)
|
||||
self.assertNotIn("synthetic private row", json.dumps(receipt))
|
||||
recovered = Path(receipt["recovery_directory"])
|
||||
self.assertEqual(recovered.stat().st_mode & 0o777, 0o700)
|
||||
self.assertEqual((recovered / "point/database.dump").stat().st_mode & 0o777, 0o600)
|
||||
for kind in ("files", "filesdrop"):
|
||||
with self.subTest(kind=kind):
|
||||
provider = self.provider()
|
||||
receipt = {"restore_verified": False}
|
||||
credentials = {**self.credentials, "upload_root": publish.HOST + "/public.php/dav/" + kind + "/synthetic",
|
||||
"upload_route_kind": kind}
|
||||
publish.publish(self.directory, credentials, provider, receipt)
|
||||
self.assertEqual(receipt["status"], "published_read_back_and_decrypted")
|
||||
self.assertEqual(receipt["upload_route_kind"], kind)
|
||||
self.assertTrue(receipt["independent_readback_verified"])
|
||||
self.assertTrue(receipt["sqlite_link_verified"])
|
||||
self.assertTrue(receipt["packet_file_hashes_verified"])
|
||||
self.assertFalse(receipt["database_restored"])
|
||||
self.assertFalse(receipt["restore_verified"])
|
||||
self.assertEqual(provider.put_calls, [credentials["upload_root"] + "/" + publish.PREFIX + "/" + publish.OBJECT])
|
||||
self.assertEqual(len(provider.get_calls), 2)
|
||||
self.assertNotIn("synthetic private row", json.dumps(receipt))
|
||||
recovered = Path(receipt["recovery_directory"])
|
||||
self.assertEqual(recovered.stat().st_mode & 0o777, 0o700)
|
||||
self.assertEqual((recovered / "point/database.dump").stat().st_mode & 0o777, 0o600)
|
||||
|
||||
def test_identical_existing_object_is_recovered_without_upload(self):
|
||||
provider = self.provider(existing=self.content, available=0)
|
||||
|
|
@ -160,6 +165,26 @@ class PublisherTests(unittest.TestCase):
|
|||
|
||||
|
||||
class BoundaryTests(unittest.TestCase):
|
||||
def test_upload_binding_accepts_both_existing_owner_contract_routes(self):
|
||||
token = "synthetic-share"
|
||||
for kind in ("files", "filesdrop"):
|
||||
url = publish.HOST + "/public.php/dav/" + kind + "/" + token
|
||||
with self.subTest(kind=kind):
|
||||
self.assertEqual(publish.upload_binding(token, url), (url, kind))
|
||||
self.assertEqual(publish.upload_binding(token, url + "/"), (url, kind))
|
||||
|
||||
def test_upload_binding_refuses_foreign_hosts_tokens_and_extra_paths(self):
|
||||
token = "synthetic-share"
|
||||
for url in ["https://foreign.invalid/public.php/dav/files/" + token,
|
||||
publish.HOST + "/public.php/dav/files/other-share",
|
||||
publish.HOST + "/public.php/dav/files/" + token + "/unexpected",
|
||||
publish.HOST + "/public.php/dav/files/" + token + "?query=1",
|
||||
publish.HOST + "/public.php/dav/files/" + token + "#fragment"]:
|
||||
with self.subTest(url=url), self.assertRaisesRegex(stores.Refused, "upload_lane_binding_changed"):
|
||||
publish.upload_binding(token, url)
|
||||
with self.assertRaisesRegex(stores.Refused, "upload_token_shape_invalid"):
|
||||
publish.upload_binding("unsafe?query", publish.HOST + "/public.php/dav/files/unsafe?query")
|
||||
|
||||
def test_owner_environment_discards_all_token_aliases_before_fresh_login(self):
|
||||
environment = {name: "synthetic" for name in publish.TOKEN_ENV}
|
||||
environment.update(HOME="/synthetic", PATH="/usr/bin", BAO_ADDR="wrong", VAULT_ADDR="wrong")
|
||||
|
|
@ -204,7 +229,7 @@ class BoundaryTests(unittest.TestCase):
|
|||
if args[2] == publish.LANE:
|
||||
return {"data": {"metadata": {"version": 1}, "data": {
|
||||
"NC_WEBDAV_TOKEN": "synthetic-upload",
|
||||
"NC_WEBDAV_URL": publish.HOST + "/public.php/dav/filesdrop/synthetic-upload",
|
||||
"NC_WEBDAV_URL": publish.HOST + "/public.php/dav/files/synthetic-upload",
|
||||
"AGE_PRIVATE_KEY": "synthetic-not-a-key"}}}
|
||||
if args[2] == publish.OPERATOR:
|
||||
return {"data": {"data": {"BACKUP_USERNAME": "Backup", "BACKUP_PASSWORD": "synthetic"}}}
|
||||
|
|
@ -218,6 +243,7 @@ class BoundaryTests(unittest.TestCase):
|
|||
"BAO_ADDR": publish.TUNNEL, "VAULT_ADDR": publish.TUNNEL}, clear=True):
|
||||
result = publish.custody(SimpleNamespace(bao=read, data=lambda value: value))
|
||||
self.assertEqual(result["kv_version"], 1)
|
||||
self.assertEqual(result["upload_route_kind"], "files")
|
||||
self.assertEqual([c[2] for c in calls], ["auth/token/lookup-self", publish.LANE, publish.OPERATOR])
|
||||
|
||||
def provider(self):
|
||||
|
|
|
|||
|
|
@ -108,6 +108,17 @@ def require_owner_handoff():
|
|||
require(checks[key], reason)
|
||||
|
||||
|
||||
def upload_binding(token, url):
|
||||
require(isinstance(token, str) and 0 < len(token) < 1024
|
||||
and not any(c in token for c in "\r\n\t/ \\?#"), "upload_token_shape_invalid")
|
||||
require(isinstance(url, str), "upload_lane_binding_changed")
|
||||
root = url.rstrip("/")
|
||||
for kind in ("filesdrop", "files"):
|
||||
if root == HOST + "/public.php/dav/" + kind + "/" + token:
|
||||
return root, kind
|
||||
raise Refused("upload_lane_binding_changed")
|
||||
|
||||
|
||||
def custody(helper):
|
||||
# Reuse the owner's contained OIDC session, never a token environment/file
|
||||
# handoff. Warden revokes and removes its private helper after the child.
|
||||
|
|
@ -125,13 +136,10 @@ def custody(helper):
|
|||
owner = read(["read", "-format=json", OPERATOR], "backup_account_custody_read_failed")["data"]["data"]
|
||||
values = lane["data"]
|
||||
token, url = values["NC_WEBDAV_TOKEN"], values["NC_WEBDAV_URL"]
|
||||
require(isinstance(token, str) and token and len(token) < 1024
|
||||
and not any(c in token for c in "\r\n/ ")
|
||||
and url.rstrip("/") == HOST + "/public.php/dav/filesdrop/" + token,
|
||||
"upload_lane_binding_changed")
|
||||
upload_root, route_kind = upload_binding(token, url)
|
||||
require(owner["BACKUP_USERNAME"] == "Backup" and bool(owner["BACKUP_PASSWORD"]),
|
||||
"backup_account_required")
|
||||
return {"upload_root": url.rstrip("/"), "upload_auth": (token, ""),
|
||||
return {"upload_root": upload_root, "upload_route_kind": route_kind, "upload_auth": (token, ""),
|
||||
"owner_auth": ("Backup", owner["BACKUP_PASSWORD"]),
|
||||
"identity": values["AGE_PRIVATE_KEY"], "kv_version": lane["metadata"]["version"]}
|
||||
|
||||
|
|
@ -249,10 +257,13 @@ def packet_checks(plain, directory, expected_packet):
|
|||
|
||||
|
||||
def publish(directory, credentials, provider, receipt):
|
||||
receipt["stage"] = "local_ciphertext_verification"
|
||||
encrypted = source(directory)
|
||||
content = encrypted.read_bytes()
|
||||
require(len(content) == CIPHER_BYTES and hashlib.sha256(content).hexdigest() == CIPHER_SHA256,
|
||||
"source_ciphertext_changed")
|
||||
receipt.update(stage="escrow_recipient_verification", lane_kv_version=credentials["kv_version"],
|
||||
upload_route_kind=credentials.get("upload_route_kind", "unspecified"))
|
||||
crypto_identity_matches(credentials["identity"])
|
||||
receipt["escrow_recipient_verified"] = True
|
||||
auth = credentials["owner_auth"]
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ id: RAPPS-WP-0014-T03
|
|||
status: wait
|
||||
needs_human: true
|
||||
decision_id: d06307e5-2737-461a-a2f4-a12c3b492a9a
|
||||
intervention_note: "Capture/restart and encrypted workstation copy pass. First provider attempt logged in but the publisher handoff guard refused before credential/provider access; Warden reported revocation. Environment preparation and lifecycle reporting fixed and tested through the installed Warden envelope offline. Retry the same publisher --execute command with fresh founder login. Provider evidence, isolated restore, access/rollback and operating ownership remain open."
|
||||
intervention_note: "Capture/restart and encrypted workstation copy pass. Second provider attempt passes handoff and custody but stops before provider calls on a stale filesdrop-only URL check. Aligned with both existing platform recovery routes (files and filesdrop) with exact provider/share-token binding; 47 checks pass. Retry same publisher --execute command. Provider evidence, isolated restore, access/rollback and operating ownership remain open."
|
||||
priority: high
|
||||
assignee: the-custodian
|
||||
blocking_reason: "Matched populated capture, restart and encrypted workstation copy pass with overview UI follow-up. Await provider publication/independent readback, escrow decryption, isolated restored workflow, access checks, compatible rollback and operating ownership."
|
||||
|
|
@ -472,6 +472,24 @@ occurs in those tests. Retry the same reviewed provider command; the earlier
|
|||
go persists, while fresh login is needed because that session was revoked. No
|
||||
provider scope, role, policy, credential grant or live app change is introduced.
|
||||
|
||||
2026-11-06 second provider attempt: all handoff checks are true; owner identity
|
||||
and both custody reads complete before `upload_lane_binding_changed` (0.6
|
||||
seconds, Warden exit 11). No provider call or app change occurs. Warden again
|
||||
reports session revocation. Receipt:
|
||||
`docs/evidence/2026-11-06-vergabe-upload-binding-failure-return.json`.
|
||||
The publisher's URL validator accepted only filesdrop, copied from the older
|
||||
migration helper; `root_custody_finish.py:roundtrip` and the platform reboot
|
||||
snapshot helper already accept the repaired files route as well. The actual
|
||||
credential-bearing URL is not exposed by this receipt or requested from the
|
||||
founder. Revision 3 matches those two exact owner-contract forms with the same
|
||||
provider/share token, retaining the object, credentials, create-only and
|
||||
independent readback scope. Receipt metadata records only a route-kind label
|
||||
and KV version. Seventeen publisher / 47 total pilot checks pass, including
|
||||
both forms through synthetic publication/readback/decryption and rejection of
|
||||
foreign origin, mismatched token or extra paths/query/fragment. Retry the same
|
||||
reviewed command; the earlier go remains valid. Native provider and restore
|
||||
acceptance remain open.
|
||||
|
||||
|
||||
## Validate synthetic sizing against the exact release and shared contention
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue