Skip to content

feat: negotiate encryption policy per peer - #9562

Merged
koenvanderveen merged 43 commits into
devfrom
pjwerneck/auto-encryption-settings
Oct 26, 2026
Merged

koenvanderveen merged 43 commits into
devfrom
pjwerneck/auto-encryption-settings

Conversation

@pjwerneck

@pjwerneck pjwerneck commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Each client now advertises an encryption policy (off, optional or required) in its version
file, and a pair encrypts when either side requires it. Keys and bundles are set up automatically,
so a client of an enclave that requires encryption needs no setting. The default stays optional.

Changes

  • login(encryption=...) takes True (required), False (optional) or "off". The policy is the
    new encryption_policy field of SYFT_version.json.
  • An optional client makes keys only when a peer requires encryption. The key file records a
    required policy, so later logins and syft-bg stay required. A key file without the field reads
    as required.
  • An off and a required client conflict: each side skips the other with a warning. A peer whose
    version file has no policy runs an outdated syft and is skipped with a warning to upgrade.
  • A pair that encrypts refuses plaintext: the owner archives such a message unread, the scientist
    skips it, both with a warning.
  • A send that waits for the peer's policy or keys stays in memory until the next sync, with a
    warning. A scientist's send to a non-peer raises; the owner drops an outbox message to a
    non-peer with a warning.
  • Files at rest are encrypted when the client is required. Files written before a switch stay
    plaintext and remain readable.
  • A sync raises when another process made the key file required after this one started.
  • The scientist sends one message per datasite owner. Before, all queued changes went to the owner
    of the last path.
  • The enclave makes keys at boot and binds them to the attestation, also when optional. Clients
    follow the enclave's policy, so the demo scripts drop their encryption flag.

Note

Release note: clients skip an enclave image built before this change as outdated. Rebuild and
deploy the enclave image with, or before, this client release.

Testing

  • New tests/unit/test_encryption_policy.py (57 tests, mock Drive) and enclave tests for the
    negotiated job flow and boot-time keys.
  • Full suites pass: tests/unit 626, migrations 108, syft-enclave 298, syft-rds 151,
    syft-job 121, syft-datasets 51.
  • Live run on real Drive (two test accounts): job round trips with the DO required, with the DS
    required, and with both optional; an off/required conflict; and a policy change while a
    process runs. All five passed, with the raw Drive files checked as encrypted or plaintext.

Asana task

Clients advertise an encryption policy (optional or required) in
SYFT_version.json. A pair encrypts when one side requires it, so clients
of an enclave need no encryption setting.

WIP: not ready for review.
- Load the key file without a nested lock in ensure_keys (deadlock).
- Keep held messages on disk; save after each send.
- Read an earlier peer without a bundle as unknown for one load, then off.
- Advertise a switch to required also from a process that never wrote the
  version file.
- Hold sends before the first peer load.
- Re-read an unknown peer through PeerManager, before the message is
  converted.
- Skip a bundle that is already published; warn on a Drive error.
- Send queued outbox messages directly; keep only pending ones on disk,
  with one write per flush and no loss on errors.
- Reload the key file policy before direct event writes.
- Catch every pending error per recipient when sending changes.
- Read earlier peers' bundles in the parallel version load.
- Decode held hashes again only when the held file changes.
- Document the encryption policy and the earlier-client limit.
…ites

- Remember peers seen as required in the key file; ignore an advertised
  downgrade until the user trusts a new key for the peer.
- Encrypt plaintext files at rest before direct event writes too.
- Run the at-rest pass under its own lock, with parallel downloads.
- Key the held-hash cache on file content.
- Read earlier peers without a bundle again only every tenth load.
- Download in parallel batches when rejecting plaintext inbox messages.
- Take an earlier peer as required only for a valid bundle; a failed
  check or a junk file keeps the policy unknown and is checked again.
- Record a missing bundle only in a peer load, so add_peer keeps the grace.
- Wait on plaintext while the peer policy is unknown; keep the last known
  policy when the version file disappears.
- Hold unsent proposals when a send fails.
- Keep a version when only the bundle check fails.
- Treat an unreadable own bundle as not published.
- Name test SyftBox folders with uuid4, so tests do not share caches.
- Treat a peer without an encryption policy as outdated: skip it with a
  warning to upgrade, and raise on a send to it.
- Remove the use_encryption keyword shim; pass encryption_policy.
- Share the hold-or-drop rule through PeerStore.should_hold.
- Remove the held-hash cache, the parallel downloads and the holding of
  sends before the first peer load.
- Merge redundant tests and rename tests without filler words.
An off client keeps no record of required peers; say so in the docs.
…ected_ prefix

- Refuse plaintext at rest once the files at rest are encrypted
- Write only changes to remembered required peers
- Use the effective peer policy in add_peer
- Find rejected messages with a Drive name query
- Read the key file again only after it changes
- Share the file lock and atomic write helpers
- Record required_since; accept older plaintext outbox history
…ejected lookup

- Record when the files at rest were encrypted; accept older plaintext
- Read the key file once per batch of files at rest
- Take key file data in a process without keys
- Escape names in Drive queries; read every page
- Check resent names in one query per batch
- Drop plaintext to an encrypting pair with a warning; remove reject-and-resend
- Keep pending sends in memory until the next sync; remove held message files
- Encrypt only new files at rest; remove the one-time pass and required_since
- Remove remembered required peers (moved to a follow-up)
- Take the policy from the key file at startup only
- Warn when a message waits in memory for encryption, or is dropped
- Warn when messages from a peer wait to be read
- Refuse to sync when the key file became required after start
- Split the send loop out of process_file_changes_queue
- Remove unused PeerManagerConfig.use_encryption
- Rename tests with filler words; fix a docstring and an import
- Restore the original place of _with_pin and BUNDLE_FILE_KEY
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

- Integration test: the DS syncs after approval to send the job it held
- Apply prettier and nbstripout to files the branch changed
@pjwerneck
pjwerneck requested a review from rasswanth-s October 14, 2026 22:04
Comment thread syft/sync/sync/datasite_watcher_syncer.py Outdated

@rasswanth-s rasswanth-s left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor Question , Great work @pjwerneck

…ion-settings

# Conflicts:
#	syft/sync/connections/connection_router.py
…ion-settings

# Conflicts:
#	syft/sync/syftbox_manager.py
#	syft/sync/sync/datasite_owner_syncer.py
Comment thread syft/sync/peers/peer_store.py Outdated
Comment thread syft/sync/peers/peer_store.py Outdated
The enclave login takes a key_file. Login copies it to the key file path
of the datasite before it builds the client. This replaces the
crypto_keys_path override, so syft-bg finds the same keys.

The Colab notebooks go back to one login cell that uploads or downloads
the key file.
Remove the hold and retry code. A message that cannot go now is dropped
with a warning. A DO inbox message that cannot be read stays in the
inbox. The format of a message must match the pair, and a required
client always decrypts its own files.
The policy of a client is fixed when it is built. Test clients get their
policy in the config, and nothing replaces the peer store later.

syft-bg takes the policy in init, so it writes the datasite with the same
policy as the notebook.
A DS send raises when the peer policy or bundle is not known, so a job
submission no longer reports success for a dropped job. The first peer
load raises when a bundle this client published carries other keys.

syft-bg init takes a required encryption policy. Remove the dead queue
call in sync, poll instead of sleep in the Drive test, and fix the docs.
A peer whose version file has no encryption policy now has a policy that
is not known, like any other peer. The version file a client shares
always carries its own version.
The enclave no longer has a use_encryption setting: it always requires
encryption. A published bundle that does not parse now raises. The key
file is a frozen model that is checked when it is read.
Remove the grouping of DS changes per DO from the encryption work. The
next commit adds it again as its own fix.
All queued changes went in one message to the last recipient, so one DO
got the changes for the datasite of another DO.
A client is now optional or required, and a pair cannot conflict. The
encryption setting at login is a bool again.
Every login takes a key_file. delete_syftbox writes the keys back. A DO
send that cannot go raises, and the change stays pending. Re-running
syft-bg init updates the policy of every service. from_bool refuses a
value that is not a bool, and reading our own bundle creates no folder.
A pair that does not encrypt never uses the bundle, so new keys on a new
runtime are fine for it. The check runs after the peer versions load.
Base automatically changed from pjwerneck/revert-peer-key-pinning to dev October 26, 2026 09:43
… VersionInfo

VersionInfo stays about the syft version, so the local version marker, the
login check and the release artifacts carry no peer settings. The published
SYFT_version.json is now a versioned PeerInfo that nests the VersionInfo and
the encryption policy. A file from an older syft reads as OPTIONAL.

Methods that read or write the published file are named after PeerInfo.
@koenvanderveen

Copy link
Copy Markdown
Collaborator

I split the encryption policy out of VersionInfo into a new versioned PeerInfo object (d0f8827), which nests the VersionInfo and the policy and is now what we publish as SYFT_version.json, so VersionInfo stays about the syft version only and current() needs no policy. A version file from an older syft still loads, as PeerInfo with an OPTIONAL policy, so the migration fixture tests keep checking older formats. Methods that read or write the published file are renamed after PeerInfo, e.g. write_own_version → write_own_peer_info and load_peer_version → load_peer_info.

- Store the policy in <syftbox>/<email>/private/syft_settings.json. The
  first client writes it; later clients (notebook, syft-bg) log in with
  encryption=None and take it. Asking for another policy raises
  EncryptionPolicyMismatchError, and so does a sync after the file changed.
- syft-bg no longer stores a policy: init --encryption is optional and
  writes the SyftBox file, the orchestrators follow it.
- The enclave runner writes its keys back after the fresh_state wipe,
  instead of delete_syftbox() doing it for every client.
- Rename the remaining version-named peer info helpers and messages.
@koenvanderveen

Copy link
Copy Markdown
Collaborator

Pushed 383e81f, which keeps one encryption policy per SyftBox:

  • One policy per SyftBox. It lives in <syftbox>/<email>/private/syft_settings.json. The first client to start writes it: the encryption it logged in with, or optional. Later clients, the notebook or syft-bg, log in with encryption=None (now the default) and take the policy on disk. Asking for another policy raises EncryptionPolicyMismatchError, and so does a sync() after the file changed under a running client. PeerStore.encryption_policy stays a plain EncryptionPolicy: resolve_encryption_policy() settles it before the store is built.
  • To change the policy, delete the SyftBox and log in again. Files the owner keeps on Drive for itself are encrypted under the policy in force when they were written, so the policy can't be switched in place.
  • syft-bg no longer stores a policy. It's gone from SyftBgConfig, SyncConfig and AutoApproveConfig, and from both orchestrators. syft-bg init --encryption ... is optional and writes the SyftBox file, and the interactive init only asks on a new SyftBox.
  • Enclave keys: the runner writes its keys back after the fresh_state wipe, because the attestation server signs with the key file. delete_syftbox() no longer does this for every client.
  • Renames: connection_for_version_read() is now connection_for_peer_file_read(), the Drive file-id helpers are now _get_own_peer_info_file_id() / _get_peer_info_file_id_of(), and the docstrings and warnings say "peer info". The file is still SYFT_version.json.
  • Docs and tests: docs/connections.md is updated, and there are new tests in test_encryption_policy.py, syft_bg/test_init.py and test_runner.py.

…ies there

- PeerStore.pair_encrypts() and _peer_for_message() replace
  pair_uses_encryption, peer_policy, policy_of, _last_known_policies,
  _raise_unless_policy_known and peer_uses_encryption. use_encryption is
  now requires_encryption.
- PeerStore.protocol_schema() replaces the live schema maps
  (_peer_protocol_schemas, _loaded_peer_versions, live_peer_schemas);
  JobStorage and DatasetStorage take a peer_schema lookup function.
- PeerStore.set_peers() keeps each peer's info and state_at_start across
  reloads, and a read that finds no info file keeps the last one.
- Peer.state_at_start replaces PeerManager._states_at_start.
@koenvanderveen

Copy link
Copy Markdown
Collaborator

Pushed 8c21942, which simplifies how per-peer state is kept. These are the decisions behind it:

  • State about a peer lives on its Peer, in the PeerStore. PeerManager no longer keeps per-peer side dicts. _loaded_peer_versions, _peer_protocol_schemas, _states_at_start and _last_known_policies repeated what Peer.info already holds, or were a flag per peer.
  • One rule for encryption: a pair encrypts when one side requires it. That rule is PeerStore.pair_encrypts(peer), a plain yes/no in which a peer with an unknown policy counts as not requiring it. The check that raises is kept apart, in PeerStore._peer_for_message(email), and only sending and reading call it. This replaces pair_uses_encryption, peer_policy, policy_of, _raise_unless_policy_known and peer_uses_encryption.
  • A client that requires encryption doesn't wait for the peer's policy. The pair encrypts either way. It still waits for the peer's public key.
  • use_encryption is renamed to requires_encryption, which says what it means.
  • Peer schemas are looked up, not copied. PeerStore.protocol_schema(email, protocol_name) reads them from the peer's info. JobStorage and DatasetStorage take a peer_schema lookup function instead of a shared dict that has to be updated in place, and the router asks the peer store directly. Tests that stand in for a peer on an older release use the advertised_schemas fixture.
  • A peer keeps its last info. load_peers() still replaces the peer list, but set_peers() keeps each peer's info and state_at_start, and a read that finds no info file keeps the last one. This keeps a peer that deleted its SyftBox readable, so a DS still applies the delete messages it sent.
  • The warning about leftover connections stays. It is now Peer.state_at_start, which is never saved, instead of a dict on PeerManager.

@koenvanderveen
koenvanderveen merged commit febab9f into dev Oct 26, 2026
21 checks passed
@koenvanderveen
koenvanderveen deleted the pjwerneck/auto-encryption-settings branch October 26, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants