# How Identities Are Managed in BitChat Using Cryptographic Key Pairs

> Discover how BitChat manages identities with cryptographic key pairs. Learn about its dual-layer architecture for secure, verifiable PeerIDs, combining X25519 and Ed25519 keys.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-21

---

**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)](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)](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.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerID.swift) also 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/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:

1. The current **epoch** (time-based counter).
2. The **PeerID** (as UTF-8 data).
3. 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)](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`.

1. **Extract components**: The recipient extracts the Noise static public key and Ed25519 signature from the announce packet.
2. **Reconstruct binding**: Using the epoch and PeerID from the announcement, the recipient rebuilds the expected binding message via `PeerIDRotation.bindingMessage`.
3. **Signature verification**: The recipient calls `NoiseEncryptionService.verifySignature` (lines 559-597 in [[`NoiseEncryptionService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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.
4. **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.rotationSecret` at lines 89-99 of [[`PeerIDRotation.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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:

```swift
// 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:

```swift
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.verifySignature` enables peers to validate identity claims before establishing trust.
- **Forward secrecy support**: The `PeerIDRotation` system 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`](https://github.com/permissionlesstech/bitchat/blob/main/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.