How Identities Are Managed in BitChat Using Cryptographic Key Pairs
BitChat implements a dual-layer identity architecture that separates transport-level X25519 Noise keys from application-level Ed25519 signing keys, cryptographically binding them through signed binding messages to create verifiable, human-readable PeerIDs.
BitChat is a permissionless peer-to-peer messaging protocol that establishes trust without centralized certificate authorities. Unlike simple single-key systems, the permissionlesstech/bitchat repository manages identities through a sophisticated layered approach that isolates mesh transport encryption from signature authority. This separation ensures that network identifiers remain stable while cryptographic ownership proofs remain independently auditable.
The Dual-Layer Cryptographic Architecture
BitChat’s identity model deliberately separates concerns between two distinct cryptographic key pairs. This design allows the transport layer to handle confidentiality and authentication while the application layer manages persistent identity authority.
Transport Layer: Noise X25519 Static Keys
The foundation of every BitChat identity rests on a Curve25519 X25519 static key-agreement pair. This key provides confidentiality and authentication for the underlying mesh transport.
- The raw public key is stored as a 64-character hexadecimal string prefixed with
noise:. - A short, stable PeerID is derived from the SHA-256 fingerprint of this public key, specifically the first 16 hexadecimal characters.
- This derivation occurs in
PeerID.init(publicKey:)within [localPackages/BitFoundation/Sources/BitFoundation/PeerID.swift](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/PeerID.swift#L31-L38).
Application Layer: Ed25519 Signing Keys
Each device maintains a persistent Ed25519 sign-verify pair that serves as its signature authority, independent of the transport keys.
- The signing key is generated and persisted in the device keychain by
NoiseEncryptionService. - This key signs binding messages that cryptographically link the Ed25519 identity to the Noise static key.
- Key generation logic resides in [
bitchat/Services/NoiseEncryptionService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NoiseEncryptionService.swift#L158-L170).
Deriving and Binding Identities
BitChat converts raw cryptographic keys into usable identities through a derivation and binding process that creates tamper-evident ownership proofs.
Generating the Canonical PeerID
The PeerID structure encapsulates the public identifier exposed to the mesh network and UI. It consists of a prefix (e.g., noise:) and a bare component containing the raw or truncated hex representation.
- Canonical BitChat IDs always root in the Noise static key, producing a short 16-hex identifier.
- Helper methods in
PeerID.swiftalso support GeoDM (nostr_) and GeoChat (nostr:) IDs derived from Nostr public keys, though these remain secondary to the Noise-based identity. - See the implementation details in [
PeerID.swift](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/PeerID.swift#L24-L38).
Creating the Binding Message
To prove ownership of a PeerID, BitChat creates a cryptographically signed binding message that ties the Ed25519 signing key to the Noise static key.
The binding message is constructed by PeerIDRotation.bindingMessage and contains:
- The current epoch (time-based counter).
- The PeerID (as UTF-8 data).
- The Noise static public key.
This message is then signed with the Ed25519 private key. The implementation spans lines 58-66 and 71-81 in [localPackages/BitFoundation/Sources/BitFoundation/PeerIDRotation.swift](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/PeerIDRotation.swift#L58-L81).
Packaging with PrekeyBundle
The signed identity is packaged into a PrekeyBundle structure that combines the Noise static key with its Ed25519 signature for exchange during handshakes. This bundle allows peers to verify identity ownership before establishing encrypted sessions.
Verification Flow and Trust Establishment
When a peer receives an identity announcement, BitChat verifies the cryptographic binding through a strict validation process implemented in NoiseEncryptionService.
- Extract components: The recipient extracts the Noise static public key and Ed25519 signature from the announce packet.
- Reconstruct binding: Using the epoch and PeerID from the announcement, the recipient rebuilds the expected binding message via
PeerIDRotation.bindingMessage. - Signature verification: The recipient calls
NoiseEncryptionService.verifySignature(lines 559-597 in [NoiseEncryptionService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NoiseEncryptionService.swift#L559-L597)) to validate the Ed25519 signature against the reconstructed binding data. - Trust establishment: If verification succeeds, the peer can trust that the short PeerID corresponds to the static key and proceed with encrypted communication.
Forward Secrecy and Rotating Identities
BitChat implements a rotating ID mechanism to enhance forward secrecy while maintaining the same underlying static key.
- The rotation secret is derived from the Noise static private key using HKDF.
- This secret combines with the current epoch to produce an 8-byte rotating peer ID for use in announce packets.
- The derivation logic lives in
PeerIDRotation.rotationSecretat lines 89-99 of [PeerIDRotation.swift](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/PeerIDRotation.swift#L89-L99).
This allows peers to frequently change their visible network identifiers without invalidating their long-term cryptographic identity.
Implementation Examples
The following Swift code demonstrates the complete identity lifecycle, from generation to verification:
// 1️⃣ Generate a new device identity (once per install)
let noiseKeyPair = Curve25519.KeyAgreement.PrivateKey() // X25519 static pair
let edKeyPair = Curve25519.Signing.PrivateKey() // Ed25519 signing pair
// 2️⃣ Derive the canonical PeerID (short 16‑hex) from the Noise public key
let peerID = PeerID(publicKey: noiseKeyPair.publicKey.rawRepresentation)
// 3️⃣ Create the binding message for the current epoch
let epoch = PeerIDRotation.epoch(at: Date())
let binding = PeerIDRotation.bindingMessage(
epoch: epoch,
peerID: peerID.id.data(using: .utf8)!,
noiseStaticPublicKey: noiseKeyPair.publicKey.rawRepresentation
)
// 4️⃣ Sign the binding with the Ed25519 private key
let signature = try edKeyPair.signature(for: binding)
// 5️⃣ Attach the signature to the announce packet
let bundle = PrekeyBundle(
noisePublicKey: noiseKeyPair.publicKey.rawRepresentation,
edSignature: signature
)
Verifying a received identity:
func verify(bundle: PrekeyBundle, receivedPeerID: PeerID, epoch: UInt32) -> Bool {
// Re‑create binding message using the sender's Noise public key
let expectedBinding = PeerIDRotation.bindingMessage(
epoch: epoch,
peerID: receivedPeerID.id.data(using: .utf8)!,
noiseStaticPublicKey: bundle.noisePublicKey
)
// Verify Ed25519 signature using NoiseEncryptionService
return NoiseEncryptionService.verifySignature(
signature: bundle.edSignature,
data: expectedBinding,
publicKey: bundle.edPublicKey
)
}
Summary
- Dual-layer architecture: BitChat separates X25519 transport keys from Ed25519 signing keys to isolate encryption from identity authority.
- PeerID derivation: Short, human-readable identifiers are generated from SHA-256 fingerprints of Noise static public keys via
PeerID.init(publicKey:). - Cryptographic binding: Ed25519 signatures over structured binding messages prove ownership of Noise keys without exposing private material.
- Persistent verification:
NoiseEncryptionService.verifySignatureenables peers to validate identity claims before establishing trust. - Forward secrecy support: The
PeerIDRotationsystem enables periodic ID rotation while maintaining stable long-term identities.
Frequently Asked Questions
What cryptographic algorithms does BitChat use for identity management?
BitChat uses Curve25519 X25519 for static key-agreement (Noise protocol transport) and Ed25519 for digital signatures. The X25519 keys encrypt and authenticate the mesh transport, while Ed25519 keys provide persistent signing authority that can be verified independently of the transport layer.
How is the human-readable PeerID generated from key pairs?
The canonical PeerID is derived from the SHA-256 hash of the Noise static public key. Specifically, PeerID.init(publicKey:) in PeerID.swift takes the first 16 hexadecimal characters of this hash to create a short, stable identifier prefixed with noise:. This ensures the PeerID remains consistent across app restarts while being compact enough for human sharing.
What is the purpose of the binding message in BitChat?
The binding message cryptographically links the Ed25519 signing key to the Noise transport key. It contains the epoch, PeerID, and Noise public key, and is signed by the Ed25519 private key. This allows recipients to verify that the entity controlling the Ed25519 key (the persistent identity) genuinely owns the Noise static key (the transport endpoint), preventing identity spoofing during peer discovery.
How does BitChat handle key rotation and forward secrecy?
BitChat implements rotating peer IDs through the PeerIDRotation class, which uses HKDF to derive rotation secrets from the Noise static private key. Combined with epoch counters, these secrets generate temporary 8-byte identifiers for announce packets. This allows peers to frequently change their visible network addresses for forward secrecy while maintaining the same underlying cryptographic identity and trust relationships.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →