Skip to content

Invites & contacts

Contact establishment uses a slim ephemeral-key invite. The invite blob carries only an ephemeral encryption pubkey, the inviter's container id, a display name, and a crypto scheme id — no long-term keys, no self-signatures. Identity material moves inside two encrypted messages exchanged after the initial out-of-band transfer.

Source: a2a_messaging.mm. Sequence diagrams: Contact exchange and Introductions.

Invite shape

metadef invite_eph_t: (
    $d -> global_id,          -- invite id (correlates legs; single-use)
    $c -> global_id,          -- inviter container id
    $n -> str,                -- inviter display name
    $k -> publickey_encrypt,  -- inviter ephemeral pubkey
    $v -> int                 -- crypto scheme id
).

Generate an invite with generate_invite. The returned blob is the value to share out-of-band (QR code, deep link, etc.).

Three-leg redeem flow

LegDirectionMechanismWhat moves
OOBinviter → responderout-of-bandslim invite_eph_t blob
Leg 1responder → inviterBARE send (box to invite's eph pub)responder identity bundle
Leg 2inviter (receives leg 1)verify bundle, consume invite, emit leg 3← inviter now holds responder's verified AD
Leg 3inviter → responderBARE send (box to responder's eph pub)inviter identity bundle

Both boxes carry the same identity bundle shape: address document + optional delegation cert + optional root profile + optional §3c CP binding. After leg 3, both sides hold the other's verified address document and encrypted_channel resumes for all subsequent traffic.

Redeem an invite with add_contact (leg 1). The inviter processes the responder's leg 1 via submit_invite_response and replies with leg 3 (complete_invite).

Single-use guarantee

An invite is consumed at the first valid leg-2 (inviter's receipt of leg 1). A second attempt for the same invite_id aborts with already-redeemed and mutates no state. The ephemeral private key is deleted atomically with the invite record (INV-4: secrets are never exported).

Test suite scenario T3 (single-use) asserts a second leg-1 for the same invite_id aborts and leaves inviter state unchanged. Scenario T4 (invalid-then-valid) asserts that a failed box-open does not consume the invite, so a subsequent valid redeem succeeds. See tests/README.md.

Pending-invite transience

Responder-side pending redemption state (pending_redemptions) is not exported and does not survive a daemon restart. An interrupted handshake does not leave half-registered state; the responder must run add_contact again with a fresh invite. This is the fail-closed design: no dangling partial contacts after a restart.

Introductions via core.connect

When two nodes share a control plane that advertises core.connect, the CP can introduce them without another out-of-band invite. The CP sends each node the other's signed address document via ingest_connect_descriptor. The receiving node verifies:

  1. The relay came from its bound CP (or the CP its root designated).
  2. Its own live manifest advertises core.connect.
  3. The peer's address document self-signature is valid (proof-of-possession).

The contact is registered immediately — no SAS, no confirmation step.

Scenario T1 (happy-flat) verifies the end-to-end invite + send_message round-trip in both directions. Scenarios T5–T8 cover adversarial inputs (tampered box, cid-bind mismatch, stripped PoP, unexpected inviter on leg 3). See tests/README.md.