Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Identity & Key Hierarchy

This chapter specifies, for an interoperable Konstruct implementation, what keys exist, how long they live, where they are stored, and what each one is used for. The structure mirrors the Signal Protocol's X3DH / Double Ratchet decomposition into long-term, medium-term, and ephemeral key material, extended with a parallel set of post-quantum keys.

The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this chapter are to be interpreted as described in RFC 2119.

3.1 Identity model

A Konstruct identity is a 36-character UUID issued by the registration service. It MUST be treated as opaque by the protocol — all cryptographic operations are bound to keys, not to the identifier.

An identity is bound to exactly one device at a time. (Multi-device support is a planned feature; until it ships, "identity" and "device" are 1-to-1.) A device is uniquely identified by a 32-character hex string derived deterministically from its identity public key (see §3.4).

Implementations MUST NOT mix the two identifier spaces — using a device-id where a user-id is expected (or vice versa) breaks the Associated Data (AD) check in the Double Ratchet AEAD and causes permanent decryption failure. The reference implementation enforces this through distinct Rust types (ServerUserId, CryptoDeviceId).

3.2 Key inventory

A registered identity holds, at minimum, the following key material. All keys MUST be generated with cryptographically secure randomness; the reference uses rand::rngs::OsRng.

KeyAlgorithmLifetimeSize (pub / priv)Role
Identity key (IK)X25519Permanent32 B / 32 BLong-term key agreement seed
Signing key (SK)Ed25519Permanent32 B / 32 BSigns prekey bundles
Signed prekey (SPK)X25519≤ 10 days32 B / 32 BMedium-term, rotated periodically
One-time prekeys (OPK)X25519Single use32 B / 32 BConsumed on first message
Kyber signed prekey (Kyber-SPK)ML-KEM-768≤ 10 days1184 B / 2400 BPost-quantum medium-term (Suite 2)
Kyber one-time prekeys (Kyber-OPK)ML-KEM-768Single use1184 B / 2400 BPost-quantum, consumed on use

The X25519 and Ed25519 sizes are fixed by the underlying curves (Curve25519, Edwards25519). The ML-KEM-768 sizes are fixed by NIST FIPS 203 (public key 1184 bytes, ciphertext 1088 bytes, secret key 2400 bytes in expanded form as exposed by the ml-kem crate).

3.3 Long-term keys

Identity key (IK)

The X25519 long-term key. It MUST be generated once at registration and never rotated for the lifetime of the identity. Rotating it constitutes destroying the identity.

The private half MUST be stored in the platform's secure key store under an "after-first-unlock" access class equivalent (iOS: kSecAttrAccessibleAfterFirstUnlock; Android: hardware-backed Keystore with equivalent flag). The public half is published in the registration bundle (§3.6) and is what other parties run X25519 against.

Signing key (SK)

A separate Ed25519 keypair used only to sign prekey bundles. It MUST NOT be reused for any other purpose. Like the IK, it is permanent and stored in the platform key store. The split between IK (key agreement) and SK (signatures) follows the Signal convention and lets each algorithm be replaced independently.

A Konstruct signature, where it appears in this specification, is the 64-byte EdDSA signature over the canonical encoding of the signed artefact, verified with ed25519_dalek::VerifyingKey::verify.

3.4 Device identifier

Both peers in a Double Ratchet AEAD must agree on the AD construction (§5.4). The AD includes a stable per-identity tag that survives session resets. The reference implementation derives this tag as:

device_id = LOWER_HEX(SHA-256(IK_pub))[0..32]

i.e. the lower-case hexadecimal SHA-256 of the 32-byte X25519 identity public key, truncated to 32 characters. An interoperable implementation MUST produce the same value from the same IK_pub.

3.5 Medium-term keys: signed prekeys

X25519 signed prekey (SPK)

The SPK is an X25519 keypair generated at registration and rotated at most every 10 days. The reference enforces this with SPK_MAX_AGE_SECS = 10 * 24 * 3600 (construct-core/src/crypto/client_api.rs:65). A bundle whose SPK is older than this MUST be rejected by a responder.

The SPK carries a spk_rotation_epoch: u32. Each rotation increments this counter monotonically. The epoch is bound into the bundle signature (§3.6) so that an attacker cannot replay an older valid SPK into a new bundle.

When the application rotates the SPK, it MUST upload the new (public-key, signature, epoch) tuple to the server and SHOULD continue to accept incoming X3DH initiations using the previous SPK for a grace window (default: until the bundle TTL expires) to avoid dropping in-flight handshakes.

Kyber signed prekey (Kyber-SPK)

An ML-KEM-768 encapsulation key serving the analogous role for the post-quantum extension. Lifetime, rotation cadence, and epoch are identical to the X25519 SPK. A bundle whose Kyber-SPK is older than SPK_MAX_AGE_SECS MUST be rejected when Suite 2 is in use.

The Kyber-SPK signature is computed over the entire (Kyber-SPK || rotation_epoch_be_bytes) payload with the identity's Ed25519 signing key; PQ signatures (ML-DSA-65) are planned but not in the reference today, so the signature on the Kyber bundle is classical Ed25519 even in Suite 2.

3.6 Ephemeral / one-time prekeys

X25519 one-time prekeys (OPK)

A pool of single-use X25519 keypairs uploaded with the registration bundle. Each OPK has a u32 identifier; the X3DH initiator (Alice) selects one and the server marks it consumed.

If the OPK pool drops below an implementation-defined threshold (the reference uses 20), the client MUST upload fresh OPKs. Running out of OPKs falls back to a 3-DH handshake without the OPK component, which has reduced forward secrecy (the protocol's BS-3 open issue — see Implementation Status).

Kyber one-time prekeys (Kyber-OPK)

A pool of single-use ML-KEM-768 encapsulation keys serving the same purpose for the post-quantum extension. The server SHOULD maintain the Kyber-OPK pool with the same threshold logic as classical OPKs.

A Suite 2 initiation in which no Kyber-OPK is available MUST NOT silently downgrade to Suite 1. The reference enforces this via SEC-002 (mandatory Kyber epoch validation in Suite 2 handshakes).

3.7 Per-session keys (Double Ratchet)

Once X3DH completes (Chapter 4), the session state machine maintains:

SymbolRole
RK32-byte root key, mixed into each DH ratchet step
CK_s, CK_rSending and receiving chain keys (32 B each)
MK_nPer-message keys (32 B), derived from a chain key, used once, then deleted
DHs, DHrLocal sending DH keypair and remote DH public
Ns, Nr, PNSending counter, receiving counter, previous-chain length

The Double Ratchet operations on these are specified in Chapter 5. Per-message keys MUST be zeroed immediately after AEAD decrypt; the reference uses zeroize::Zeroizing.

3.8 Key storage at rest

The reference implementation stores keys with the following access classes; an interoperable implementation SHOULD provide an equivalent guarantee on its platform:

KeyAccess class
IK_priv, SK_privAfter-first-unlock, device-only, non-syncable
Session JSON (incl. dh_ratchet_private, RK, chain keys)When-unlocked, device-only
Refresh / auth tokensAfter-first-unlock
device_id (derived value)After-first-unlock

The session blob currently stores dh_ratchet_private in cleartext within the platform-protected blob. Adding an additional encryption-at-rest layer is open work tracked as SEC-009 in the implementation status.

3.9 Public key bundle (wire format)

The bundle a responder publishes to the directory MUST contain:

RegistrationBundle ::=
    suite_id              : u16
    identity_pub          : [u8; 32]              -- X25519
    signing_pub           : [u8; 32]              -- Ed25519
    signed_prekey_pub     : [u8; 32]              -- X25519
    signed_prekey_sig     : [u8; 64]              -- Ed25519(SK_priv,
                                                      SPK_pub || epoch_be)
    spk_rotation_epoch    : u32
    one_time_prekeys      : Vec<(u32, [u8; 32])>  -- (id, pub)

    -- Suite 2 (PQXDH) extension, omitted in Suite 1 bundles:
    kyber_pre_key_pub          : Option<[u8; 1184]>
    kyber_pre_key_sig          : Option<[u8; 64]>     -- Ed25519
    kyber_spk_rotation_epoch   : Option<u32>
    kyber_one_time_prekeys     : Option<Vec<(u32, [u8; 1184])>>

All multi-byte integers are big-endian. The exact serialised form crossing the FFI boundary is a packed binary struct (no JSON), see Chapter 6 §6.2.

A responder MUST verify the SPK signature against signing_pub before performing any DH against the SPK. A bundle that fails signature verification MUST be rejected; the handshake MUST NOT proceed.

3.10 Key rotation summary

KeyRotation triggerCadence
IK, SKNone (rotating destroys the identity)Never
SPK, Kyber-SPKPeriodic + on app launch if stale≤ 10 days
OPK, Kyber-OPKConsumed on each handshakeRe-uploaded when pool < threshold
RK, CK_s, CK_rEvery Double Ratchet stepPer message / per round-trip
MKEvery messageUsed once, then deleted
Session as a wholeEND_SESSION, healing fallbackOn demand

These rotation invariants are what give the protocol its forward secrecy and post-compromise security properties (Chapters 1 and 5).