onepub-dev / onepub-dev/reVault
Design a trustworthy reciprocal contact key exchange
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 4
- Forks
- 0
- Avg merge
- 1h 55m
- Merged PRs (30d)
- 1
Description
Context
The current key server sends a profile key bundle in one direction. That is
enough for Alice to obtain Bob's encryption public key before creating a
lockbox for Bob, but Bob also needs Alice's independently trusted signing
public key before he can authenticate the lockbox as authored by Alice.
Contacts are global. A profile option selects which local identity bundle a
participant discloses; it does not select where the received contact is stored.
Related: #211 and #225.
Goals
- Exchange exactly one explicitly selected profile bundle in each direction.
- Include both encryption and signing public keys in each bundle.
- Keep contacts global while preserving the remote profile/generation metadata.
- Support asynchronous operation; both users need not be online together.
- Bind both exchange legs into one versioned transcript.
- Ensure the server transports keys but never confers human identity trust.
- Require each party to independently verify the other bundle's fingerprint.
- Store each complete contact bundle and trust state atomically.
- Allow one-way receive when the recipient does not want to disclose a profile.
Terminology
Use exchange invitation, not exchange session. This is an asynchronous,
expiring rendezvous record rather than a live connection.
Suggested CLI language:
exchange-offer --from-profile <profile> --recipient <email>
exchange-accept <invitation> --reply-with-profile <profile>
exchange-accept <invitation> --receive-only
exchange-status
exchange-complete
exchange-cancel
--reply-with-profile identifies the local profile whose public keys Bob
returns. Alice is stored as a global contact regardless of this choice.
Proposed flow
Alice Key server Bob
create invitation
-- Alice bundle ----------> OFFERED
share invitation link/code ----------------------------------->
<-- redeem + Bob bundle --------
ACCEPTED
--- Alice bundle -------------->
poll with management token
<-- Bob bundle ------------ RESPONSE DELIVERED
verify Bob independently verify Alice independently
trust Bob locally trust Alice locally
acknowledge delivery
--------------------------> DELETED
The two trust decisions are independent. The server records delivery state,
never trusted state.
Temporal behavior
Recommended initial policy:
Invitation lifetime: 24 hours
Maximum configurable TTL: 7 days
Poll interval: 5-30 seconds with backoff
Post-delivery retention: until acknowledgement, at most 1 hour
Accepted response: immutable
Completed invitation: delete immediately after acknowledgement
Expired invitation: purge promptly
Server state:
OFFERED -> ACCEPTED -> DELIVERED -> DELETED
| | |
+----------+-----------+-> EXPIRED
+------------------------> CANCELLED (before acceptance)
Local contact state, maintained independently by each participant:
received -> pending verification -> trusted | rejected
Expiry or deletion of the server record must not remove a locally verified
contact. Cancellation after acceptance cannot retract keys already delivered.
Invitation identity and capabilities
Email identifies a participant but not a specific invitation. A user can have
multiple profiles, recipients, rotations, and concurrent offers. The protocol
therefore needs an opaque exchange ID even if the normal UX is an emailed link
rather than a manually entered code.
Alice receives:
- an invitation link/code safe to give Bob;
- a private management token for poll, retrieve, cancel, acknowledge, and
delete operations.
Bob uses a private recipient capability for idempotent acceptance retries.
The public invitation identifier must not permit response retrieval,
replacement, cancellation, lifetime extension, or deletion.
The first successful acceptance freezes the recipient bundle. A retry from the
same recipient capability returns the same result; another party cannot replace
it. This must tolerate a response being lost after the server commits Bob's
bundle.
Canonical profile bundle
ProfileBundle {
protocol_version
stable_profile_id
profile_generation
encryption_public_key
signing_public_key
normalized_verified_email
created_at
nonce
proof_of_possession
bundle_fingerprint
}
Never send all local profiles implicitly. Human-readable profile names are
untrusted labels unless separately authenticated.
Transcript binding
Bind at least:
- protocol version and exchange ID;
- initiator and recipient roles;
- expected recipient email, when used;
- both stable profile IDs and generations;
- canonical digests of both complete bundles;
- nonces, creation time, and expiry;
- state transition and replay identifiers.
Each participant signs its leg. These signatures prove possession and prevent
mix-and-match/replay after trust is established; they do not establish the
person behind a previously unknown key.
Trust model
- Email verification proves control of an address, not human identity.
- Server delivery proves only that the server returned a bundle.
- The receiver computes the fingerprint over email, encryption key, signing
key, stable profile ID/generation, and protocol context. - Each receiver independently verifies the fingerprint by phone, QR, in person,
or another already trusted channel. - A bundle remains pending/untrusted until that local verification succeeds.
- The same call may verify both directions, but the software records two
separate decisions.
Relationship to archive delivery
Alice may encrypt an archive for Bob only after she has accepted Bob's
encryption public key for the intended profile. Bob may call the archive
authored by Alice only when:
- the archive signature is cryptographically valid;
- its signer matches Alice's independently verified signing key;
- Alice was authorized by the archive's author policy.
Names asserted by archive contents cannot establish signer identity.
Threats and failure cases
- Malicious-server substitution or split views.
- Invitation interception and denial by first acceptance.
- Crossed exchanges and bundle mix-and-match.
- Replay of expired or previous-generation bundles.
- Reflection of Alice's bundle as Bob's response.
- Email-account compromise.
- Lost accept/retrieve responses.
- Partial exchange where only one party becomes trusted.
- Wrong-profile disclosure.
- Duplicate acceptance and concurrent polling.
- Abuse of verification email and invitation creation.
- Retention of expired bundles, tokens, or relationship metadata.
Plan
- Write the reciprocal-exchange threat model and trust boundaries.
- Specify the versioned canonical profile bundle and fingerprint.
- Specify the transcript, state machine, capabilities, TTL, idempotency, and
deletion semantics. - Decide whether invitations are delivered as opaque links, short codes, or a
verified-recipient inbox while retaining an internal opaque exchange ID. - Define the global atomic contact record and pending/trusted/rejected states.
- Define explicit profile-disclosure and one-way fallback UX.
- Implement typed protocol messages and server storage separately from the
current one-way payload path. - Implement offer, accept, poll, complete, cancel, and status CLI operations.
- Bind received signing keys into archive signer verification.
- Add signed rotation using the previously trusted signing key and an
independently verified recovery path. - Add end-to-end, retry, expiry, failover, and adversarial tests.
- Complete independent security review before enabling reciprocal exchange by
default.
Acceptance criteria
- Each direction transfers one encryption key and one signing key atomically.
- Each participant explicitly chooses the local profile disclosed in that
direction; all profiles are never sent implicitly. - Contacts remain global and retain remote profile/generation metadata.
- Server delivery and email verification never mark a contact trusted.
- Each direction has an independent local fingerprint-verification decision.
- Accepted responses are immutable and retries are idempotent.
- Tokens have least privilege and records expire/delete as documented.
- The transcript resists replay, reflection, and cross-session substitution.
- An archive received after exchange is matched against the sender's trusted
signing key and author policy. - Partial, rejected, cancelled, expired, and interrupted exchanges have tested
recovery behavior.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
The issue names the current one-way payload path and related issues #211 and #225, but no implementation files or tests. Start by reading that path and those related issues, then map the threat model, canonical bundle, state machine, capabilities, and trust states. Done means the protocol and UX are specified, implemented across offer, accept, poll, complete, cancel, and status, and covered by adversarial and recovery tests plus security review.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend-api-design, cli, cryptography, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100