Security Architecture
Identity Keys
Overview
Each device in Semafore holds three types of cryptographic keys. Together, they form the device’s “key bundle”—the public half of which is uploaded to the server and used to route messages to that device. The three key types differ in lifetime, purpose, and rotation cadence.
Identity Key (IK)
Lifetime: Permanent (device lifetime)
Curve form: Curve25519 (Montgomery form)
Role: Long-term cryptographic anchor for the device
The Identity Key is generated once when the device is first registered and never changes. It is never transmitted to the server—only its public component is uploaded. The Identity Key serves two purposes:
ECDH agreement: Used in two of the four DH operations in X3DH session establishment. Any sender establishing a session with this device performs DH with the recipient’s IK public key.
Anchor for SPK signing: The device uses its IK private key to sign the Signed Pre-Key, binding the SPK to the device’s long-term identity. Recipients verify the SPK signature against the IK public key to ensure the SPK has not been forged or replaced by an attacker.
The private Identity Key never leaves the device and is protected by the device’s secure enclave (on iOS) or keystore (on Android).
Signed Pre-Key (SPK)
Lifetime: Medium-term (default: 7 days, configurable via SPK_MAX_AGE_HOURS)
Curve form: Curve25519 (Montgomery form)
Role: Enables session establishment and binds sender identity to the session
The Signed Pre-Key is a Curve25519 key pair generated and signed by the device’s Identity Key. The server holds the public key and the IK signature. The private key is held only on the device.
Rotation Schedule
The SPK is rotated when it exceeds SPK_MAX_AGE_HOURS (default: 168 hours, or 7 days) in age. When rotation occurs:
- The device generates a new Curve25519 key pair
- The device signs the new SPK’s public key with its IK private key
- The device uploads the new public SPK and signature to the server via
PUT /api/mobile/key-bundles/{deviceID}/signed-pre-key - The server stores the new SPK as the current key and retains the previous SPK in a grace period
Grace Period
For SPK_GRACE_PERIOD_HOURS (default: 168 hours, or 7 days) after rotation, the previous SPK remains in the server’s key bundle. This grace period allows in-flight X3DH sessions that were initiated before rotation to complete using the old SPK without failing. After the grace period expires, the old SPK is deleted.
Failure Mode
If a device does not rotate its SPK and it exceeds SPK_MAX_AGE_HOURS, the server will refuse new session initiations and respond with HTTP 428 Precondition Required, signaling spk_expired. The initiating client receives a clear error; the device owner is notified via WebSocket to rotate. This is not a security failure—it is an operational constraint designed to encourage regular key rotation.
One-Time Pre-Keys (OPKs)
Lifetime: Ephemeral (used once per session)
Curve form: Curve25519 (Montgomery form)
Role: Increases forward secrecy in the X3DH handshake
One-Time Pre-Keys are a batch of Curve25519 key pairs. The device generates them locally, uploads the public keys to the server in batches, and deletes the private keys after use. Each new session establishment consumes one OPK.
Replenishment
The server tracks the count of OPK public keys available for each device. When the count falls below KEY_BUNDLE_REPLENISHMENT_THRESHOLD (default: 5), the server notifies the device via WebSocket with a key_bundle.replenishment_needed event. The device responds by generating a fresh batch of OPK key pairs and uploading them via the key-bundle publish endpoint.
Replenishment is asynchronous and best-effort. If a device is offline, the notification is not queued; the device will replenish when it reconnects.
When No OPKs Are Available
If a sender initiates a session with a recipient that has no OPK available, X3DH proceeds with only three DH operations instead of four:
- DH1:
DH(IK_A, SPK_B)— sender’s long-term key with recipient’s pre-key - DH2:
DH(EK_A, IK_B)— sender’s ephemeral key with recipient’s long-term key - DH3:
DH(EK_A, SPK_B)— sender’s ephemeral key with recipient’s pre-key
The missing DH operation is DH4: DH(EK_A, OPK_B), which would have been performed if an OPK was available.
This three-key variant is still cryptographically secure—the Signal Protocol specification documents it as a valid mode. The session maintains forward secrecy and break-in recovery. However, having four DH operations is stronger; clients should aim to keep OPKs in stock.