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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ jobs:
run: sudo notify/tests/runtime-packaging/run-linux-host.sh

m5-syncthing-upload-e2e:
name: M5 Syncthing Upload E2E
name: M5/M6 Syncthing Transfer E2E
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
Expand Down Expand Up @@ -148,7 +148,7 @@ jobs:
GOMODCACHE: /tmp/vaultsync-m5-notify-mod
run: go mod download

- name: Prove upload through two isolated ephemeral instances
- name: Prove upload and response transfer through two isolated ephemeral instances
run: |
docker run --rm --network none --read-only --cap-drop ALL \
--security-opt no-new-privileges \
Expand All @@ -162,7 +162,7 @@ jobs:
-e VAULTSYNC_M5_SYNCTHING_BIN=/opt/vaultsync-m5-syncthing \
golang@sha256:079e59808d2d252516e27e3f3a9c003740dee7f75e55aa71528766d52bcfc16a \
go test -tags diagnostics_m5_syncthing_e2e \
-run '^TestDiagnosticsUploadThroughTwoEphemeralSyncthingInstances$' \
-run '^TestDiagnostics(Upload|Download)ThroughTwoEphemeralSyncthingInstances$' \
-count=1 -v

install-script:
Expand Down
30 changes: 21 additions & 9 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,13 @@ digest, opaque binding, nonce, transcript fingerprint, signed body, namespace
path, mount alias, operation value, or artifact name. It creates no diagnostics
telemetry, crash annotation, support-bundle export, Cloud Relay/APNs/StoreKit
call, discovery request, trust adoption, share, rescan, or Syncthing
configuration/ignore change. No response has been accepted after a fresh local
apply on an iPhone. The unreleased app source can set only upload evidence after
an exact pinned helper attestation; controlled download and roundtrip evidence
remain unset.
configuration/ignore change. The unreleased app source can set upload evidence
after an exact pinned helper attestation and, only after an accepted upload,
download evidence from a fresh local apply of the exact authorized helper
response in the same active operation; roundtrip evidence remains unset. A
complete download acceptance has run only against injected test event streams
plus byte-exact artifacts from isolated local Syncthing instances; no download
has been observed on a physical device.

Before the supported installer creates the namespace, the app must send a valid
signed enablement and the local operator must choose an exact existing Syncthing
Expand Down Expand Up @@ -256,17 +259,26 @@ on at most eight bounded polls to the fixed TLS-1.3/SPKI-pinned helper endpoint.
Only the exact paired-helper signature and complete Decision 024 binding for
that active request/query can set the separate in-memory `upload observed`
field. HTTP status, reachability, request creation, rescan, timestamps, index or
idle state, and a synchronized attestation copy cannot set it. Download and
roundtrip fields remain false and cannot be inferred from upload.
idle state, and a synchronized attestation copy cannot set it. After an accepted
upload the same operation captures a fresh event-cursor, wall-clock, and
engine-generation baseline, sends one signed response authorization over the
pinned endpoint, and sets the separate `download observed` field only after a
fresh successful local apply of the exact expected response path plus complete
signature, binding, digest, nonce, payload, and TTL validation of that file. A
response existing before the baseline, arriving after an engine restart, or
failing any validation can never set it; an invalid file at the exact path ends
the operation as a conflict, and every terminal outcome after upload keeps the
upload field visible as a partial result. The roundtrip field remains false and
cannot be inferred from upload or download.

The active operation, request/query bytes, random values, digests, poll state,
and evidence are not persisted in preferences, Keychain, logs, telemetry,
crash reports, support bundles, Relay, APNs, StoreKit, or pairing records.
Leaving the view, cancellation, refresh, app/engine restart, target or
credential change, timeout, or conflict ends the operation, and a late response
cannot upgrade it. The request and helper attestation are synchronized opaque
files and may remain in live folders, peers, backups, versions, conflict copies,
remote history, deletion records, or tombstones. Expiry, app rollback, or live
cannot upgrade it. The request, helper attestation, and helper response are
synchronized opaque files and may remain in live folders, peers, backups,
versions, conflict copies, remote history, deletion records, or tombstones. Expiry, app rollback, or live
cleanup does not promise removal of those retained copies; they never regain
validity or become evidence.

Expand Down
54 changes: 34 additions & 20 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,18 @@ succeeded” flag:
| Background sync started | This iPhone, unattributed | Entry into the silent-push sync path |
| Local data progress observed | Background run, or one eligible server/folder check | A fresh, successful incoming file application (`ItemFinished`) |
| Upload observed | Exact app/helper/homeserver/folder/operation correlation | Only an explicit foreground check in unreleased M5 app source can accept the exact paired-helper attestation for its active request/query. |
| Download observed | Exact controlled response correlation | Not implemented in the app; a helper response or synchronized file alone cannot set it. |
| Full roundtrip confirmed | One matching upload-then-download correlation | Not implemented; it requires the later controlled download from the same active chain. |
| Download observed | Exact controlled response correlation | Only the same active operation in unreleased M6 app source can set it: after an accepted upload, the authorized helper response must pass a fresh post-authorization cursor/wall-clock/generation `ItemFinished` gate plus complete validation. A helper response or synchronized file alone cannot set it. |
| Full roundtrip confirmed | One matching upload-then-download correlation | Not implemented; it requires the later causal derivation from the same active chain's upload and download. |

None automatically implies the next. Relay reachability is not a trigger;
trigger observation is not APNs delivery; push receipt is not background start;
engine reachability, scans, index updates, `idle`, and 100% completion are not
local data progress. A successful incoming file application proves that this
iPhone applied a file change, but not that network bytes moved, which peer
supplied every block, or that the check caused the change. Upload is a separate,
explicitly initiated Decision 024 field in unreleased source. Controlled download
remains independent and unset, so a full roundtrip cannot be derived.
supplied every block, or that the check caused the change. Upload and controlled
download are separate, explicitly initiated Decision 024 fields in unreleased
source; download can derive only after an accepted upload inside the same active
operation. Roundtrip is not implemented, so it cannot be derived.

Server snapshots contain only entitlement, provisioning, backend, and
per-homeserver Relay observation. The v1 push contains no homeserver/folder
Expand Down Expand Up @@ -86,21 +87,21 @@ background sync.

Ignore rules, missing paths, an event-buffer overflow, or a runtime folder error
can prevent an observation and therefore end conservatively as incomplete; they
never create a false success. It remains separate from the explicit upload-only
operation below and cannot populate that operation's evidence. Controlled
download and roundtrip require their later Decision 024 milestones. Relay v1 is
unchanged.
never create a false success. It remains separate from the explicit controlled
operation below and cannot populate that operation's evidence. Causal roundtrip
requires its later Decision 024 milestone. Relay v1 is unchanged.

#### Opt-in correlated-roundtrip helper runtime — foreground upload only
#### Opt-in correlated-roundtrip helper runtime — upload and controlled download

[Decisions 021–024](decisions/021-capability-negotiated-helper-contract-for-correlated-roundtrip-proof.md)
define the proof and rollout boundaries. Helper 2.0.2 is published and its
immutable digest plus upgrade, downgrade, and forward-recovery path are
verified. The source tree now also contains the unreleased app-side explicit
capability, pairing, credential-lifecycle, and namespace-authorization control
plane plus the explicit M5 foreground upload operation. Product upload is
implemented only to the exact signed-attestation boundary and remains
unreleased; controlled download and causal roundtrip are unset. VaultSync 2.0
plane plus the explicit M5 foreground upload operation and the M6 controlled
download leg. Product upload is implemented to the exact signed-attestation
boundary and controlled download to the exact fresh-apply response boundary;
both remain unreleased, and causal roundtrip is unset. VaultSync 2.0
remains NO-GO.

One upload operation begins only after a user tap and a second localized
Expand All @@ -120,7 +121,17 @@ gate valid for the still-active tuple, sets `upload observed`. HTTP status,
request creation, rescan, index/idle/completion state, timestamps, and a
synchronized attestation copy cannot do so. Cancellation, view exit, refresh,
app/engine restart, target or credential change, timeout, and conflict are
terminal; late responses never upgrade them. Download and roundtrip remain
terminal; late responses never upgrade them.

After upload acceptance the same operation captures a fresh event-cursor,
wall-clock, and engine-generation baseline, sends one signed type-6 response
authorization over the pinned endpoint, and watches only the exact expected
response path. A fresh successful local apply plus complete type-7 signature,
binding, digest, nonce, payload, and TTL validation sets `download observed`.
A response predating the baseline or authorization, an engine restart, a
changed binding, or any validation failure cannot set it; an invalid file at
the exact path ends the operation as a conflict. Every terminal outcome after
upload keeps the upload field visible as a partial result. Roundtrip remains
immutable false in this milestone.

The runtime is gated by an operator-authored read-only configuration plus a
Expand Down Expand Up @@ -214,12 +225,15 @@ namespace, mappings, backups, versions, conflict copies, and tombstones; an old
helper yields capability unavailable and never a weaker success.

The helper can reconstruct an exact authorized runtime session and process the
existing D024 foundations. The unreleased M5 app can use only its capability and
upload-attestation paths, but signatures establish only authorship and causal
bindings—not transport route, exact network bytes, direct peer, block
provenance, future delivery, or global sync health. No response has passed a
fresh post-authorization iPhone cursor/nanosecond/generation/`ItemFinished`
baseline. Cleanup remains evidence-orthogonal. Helper-first publication,
existing D024 foundations. The unreleased app can use only its capability,
upload-attestation, and response-authorization paths, but signatures establish
only authorship and causal bindings—not transport route, exact network bytes,
direct peer, block provenance, future delivery, or global sync health. The
fresh post-authorization cursor/wall-clock/generation/`ItemFinished` download
gate exists in unreleased source and has been exercised only with injected
event streams plus byte-exact artifacts from isolated local Syncthing
instances; no response has passed it on a physical iPhone. Cleanup remains
evidence-orthogonal. Helper-first publication,
production rollout, rollback, the M5 real-device/PR gate, and the later download
and roundtrip app milestones remain mandatory.
See [helper runtime and packaging readiness](helper-runtime-packaging-readiness.md).
Expand Down
88 changes: 88 additions & 0 deletions docs/m6-controlled-download-readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# M6 controlled-download readiness

**Status:** Unreleased app source. The controlled download leg of
[Decision 024](decisions/024-canonical-correlated-roundtrip-contract-and-threat-model.md)
is implemented against the published, unchanged helper 2.0.2 runtime. Causal
roundtrip remains unset and is a later, separately gated milestone. VaultSync
2.0 remains NO-GO.

## Scope

One explicit user tap with localized confirmation starts one operation for one
paired app/homeserver/folder/helper tuple. The upload leg is unchanged from the
M5 readiness boundary. This milestone adds only the app-side response leg:

- a signed type-6 response authorization is created only after the exact
type-5 upload attestation was accepted for the still-active operation;
- the authorization is sent once over the fixed TLS-1.3/SPKI-pinned endpoint
`POST /api/v1/diagnostics/authorize-response`; the 202 acknowledgement is
transport diagnostics only and never evidence;
- before the authorization is sent, the app captures a fresh response baseline:
the current bridge event cursor, the wall clock, and the engine generation
from the exact preflight boundary;
- `download observed` is set only after a fresh successful `ItemFinished`
apply of the exact expected response path — newer than the cursor and
wall-clock baselines inside the unchanged engine generation — followed by a
complete read of that exact file and full canonical, signature, key, epoch,
binding, operation, digest, nonce, payload, and TTL validation of the type-7
response artifact against the active request, attestation, and
authorization;
- upload and download are separate evidence fields; roundtrip remains
immutable false; cleanup stays evidence-orthogonal.

## Failure semantics

A response that exists before the baseline or authorization, arrives after an
engine restart or generation change, appears at any other path, or fails any
validation can never set download evidence. Invalid bytes at the exact
expected namespace path terminate the operation as a conflict. Cancellation,
view exit, refresh, app or engine restart, binding or credential change,
timeout, and rate limits stay terminal; after an accepted upload every
terminal outcome preserves the upload field and is presented as a partial
result that no late artifact can upgrade. An app restart destroys the active
correlation and nothing resumes.

## Compatibility and rollback

The helper wire surface is unchanged: helper 2.0.2 already implements the
dormant response foundation, and no helper, Relay, Trigger v1, APNs, or
StoreKit change ships with this milestone. Old or downgraded helpers yield
capability unavailable without fallback. App or helper rollback preserves
credentials, namespace authorization, opaque artifact copies, backups,
versions, conflicts, history, tombstones, mappings, and user data. The
request, attestation, and response artifacts are synchronized opaque files
and may remain in peers, backups, versions, conflict copies, and tombstones;
retained copies never regain validity.

## Verification

All Xcode results and derived data are outside the repository under `/tmp`.
The local gate for this milestone includes:

- production Swift response protocol bound to the cross-language
`diagnostics-response-m6.json` golden vectors, including full-chain
validation and per-byte tamper rejection;
- the M6 controlled-download runtime suite: stale pre-baseline responses,
tampered artifacts at the exact path, engine-generation changes,
cancellation during the download leg, and restart non-resumption never set
evidence, while the exact fresh chain sets upload then download;
- the M5 foreground upload runtime suite re-run with the download leg,
including partial-result preservation and rate limiting;
- `TestDiagnosticsDownloadThroughTwoEphemeralSyncthingInstances` in the
isolated no-network Linux container: the exact upload chain propagates
app→helper through real Syncthing, the real helper response foundation
creates the one signed response artifact, those exact bytes propagate
helper→app and validate through the full D024 chain, and a helper restart
replays idempotently without rewriting the artifact;
- the complete iOS plan, a Release-configuration simulator build,
design-token lint, string-key parity, and the sync-proof privacy lint.

The signed owner-device test was not executed — owner-approved
physical-device waiver (2026-07-15). Simulator and isolated local Syncthing
evidence substitute for it; no hardware keychain behavior, real APNs
delivery, real background waking, or TestFlight installation on hardware is
claimed, and simulator evidence is never described as real-device evidence.

Decision 024 remains the unchanged canonical contract. The next milestone may
derive the causal roundtrip only from this operation's upload and download
legs after this PR and its review/CI gates complete.
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,18 @@ enum DiagnosticsNamespaceProtocol {
]
}

static func operationResponseComponents(
installationBinding: Data,
operationID: Data
) throws -> [String] {
var components = try operationRequestComponents(
installationBinding: installationBinding,
operationID: operationID
)
components[components.count - 1] = base32LowerNoPadding(operationID) + ".response.cbor"
return components
}

private static func unixSeconds(_ date: Date) throws -> UInt64 {
let value = date.timeIntervalSince1970.rounded(.down)
guard value >= 0, value < Double(UInt64.max) else {
Expand Down
Loading
Loading