Signing keypair enrolment (silent; enforces nothing) #39

Closed
opened 2026-09-21 18:02:32 +01:00 by cruelacid · 1 comment
Owner

Part of #37.

What Relay does

"Advanced permissions — granular read/write controls at folder level" is
Active on their roadmap (read 21 Sep 2026), tracking their issue #26, open
since 2025. They do not ship it: their docs state read-only access is
unsupported, and enableReaderRole in src/flags.ts defaults to false.

What we do today

No read-only membership. docs/sync-limitations.md:555 records that a viewer
role was built and removed — the editor half worked, but Obsidian offers no
cancellable file-operation hook, so a viewer could still delete files from the
explorer and the deletion would propagate.

That same section sketches the E2EE-compatible answer: sign each update with
the author's identity key
so the relay refuses a non-writer's write without
decrypting anything.

Why enrolment is separate from enforcement

This issue is enrolment only. It ships a signing key to every device and
enforces nothing. That matters because it removes the deadline: once every
device carries a signing key, enforcement can be switched on per folder whenever
someone wants it, with coverage already in hand.

And enrolment can be silent. The master key is cached per-device in the OS
credential store, not merely per session (docs/security-model.md: "Your key is
kept for the device, not just the session"
; main.ts:613, main.ts:1801). So a
signed-in device can generate a signing keypair, wrap the private half under the
master key it already holds, and upload the public half with no passphrase
prompt and no user-visible step
. A device with no credential store migrates the
next time it asks for the passphrase anyway.

This is why the product does not need to be taken offline.

What changes, and the two traps

It must be a second keypair. packages/shared/src/crypto.ts:254:

crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits'])

ECDH with deriveBits cannot sign. So: a new signing_public_key column, a
second wrapped private key, and changes to EnrollKeysRequest,
ReplaceCredentialsRequest and the recovery flow.

Recovery must preserve it byte-for-byte. docs/sync-limitations.md records
that recovery deliberately preserves the identity keypair so shared folders
survive a password reset. The signing key needs identical treatment, or a
password reset silently invalidates every signature the user ever made.

Risk

risk:contract. The wire change is additive and MIN_PLUGIN_VERSION
(config.ts:169, enforced at ws-server.ts:370 with WS_CLOSE_UPDATE_PLUGIN)
already exists as a floor mechanism. What changes is the account contract: every
account acquires a signing key. Silent today; a forced passphrase-flow migration
if left until the user base is large.

Verification

  • Assert enrolment completes with no passphrase prompt on a signed-in device.
  • Assert recovery preserves the signing key byte-for-byte, exactly as it does the
    identity keypair. Both tests fail when inverted.
  • Both are data-loss-adjacent: state what happens to unsaved work in the failure
    case before changing anything, and update docs/sync-limitations.md.
Part of #37. ## What Relay does "Advanced permissions — granular read/write controls at folder level" is **Active** on their roadmap (read 21 Sep 2026), tracking their issue #26, open since 2025. They do **not** ship it: their docs state read-only access is unsupported, and `enableReaderRole` in `src/flags.ts` defaults to `false`. ## What we do today No read-only membership. `docs/sync-limitations.md:555` records that a `viewer` role was built and removed — the editor half worked, but Obsidian offers no cancellable file-operation hook, so a viewer could still delete files from the explorer and the deletion would propagate. That same section sketches the E2EE-compatible answer: **sign each update with the author's identity key** so the relay refuses a non-writer's write without decrypting anything. ## Why enrolment is separate from enforcement This issue is **enrolment only**. It ships a signing key to every device and enforces nothing. That matters because it removes the deadline: once every device carries a signing key, enforcement can be switched on per folder whenever someone wants it, with coverage already in hand. **And enrolment can be silent.** The master key is cached per-device in the OS credential store, not merely per session (`docs/security-model.md`: *"Your key is kept for the device, not just the session"*; `main.ts:613`, `main.ts:1801`). So a signed-in device can generate a signing keypair, wrap the private half under the master key it already holds, and upload the public half **with no passphrase prompt and no user-visible step**. A device with no credential store migrates the next time it asks for the passphrase anyway. This is why the product does not need to be taken offline. ## What changes, and the two traps **It must be a second keypair.** `packages/shared/src/crypto.ts:254`: ```js crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']) ``` ECDH with `deriveBits` **cannot sign**. So: a new `signing_public_key` column, a second wrapped private key, and changes to `EnrollKeysRequest`, `ReplaceCredentialsRequest` and the recovery flow. **Recovery must preserve it byte-for-byte.** `docs/sync-limitations.md` records that recovery deliberately preserves the identity keypair so shared folders survive a password reset. The signing key needs identical treatment, or a password reset silently invalidates every signature the user ever made. ## Risk `risk:contract`. The wire change is additive and `MIN_PLUGIN_VERSION` (`config.ts:169`, enforced at `ws-server.ts:370` with `WS_CLOSE_UPDATE_PLUGIN`) already exists as a floor mechanism. What changes is the account contract: every account acquires a signing key. Silent today; a forced passphrase-flow migration if left until the user base is large. ## Verification - Assert enrolment completes with **no passphrase prompt** on a signed-in device. - Assert recovery preserves the signing key byte-for-byte, exactly as it does the identity keypair. Both tests fail when inverted. - Both are data-loss-adjacent: state what happens to unsaved work in the failure case before changing anything, and update `docs/sync-limitations.md`.
Author
Owner

Moved to the Vikunja board as NEC-16: https://projectron.nerchure.com/tasks/16

Moved to the Vikunja board as **NEC-16**: https://projectron.nerchure.com/tasks/16
Sign in to join this conversation.
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Nectenda/nectenda#39
No description provided.