How QR Verification Binds Nicknames to Peer Fingerprints in BitChat
QR verification in BitChat cryptographically ties a user's nickname to their peer fingerprint by signing a structured payload containing both the nickname and the peer's static Noise key, which verifiers use to locate and authenticate the specific peer in the mesh network.
BitChat implements a QR-based out-of-band (OOB) verification flow that prevents impersonation and nickname spoofing. This mechanism ensures that when you scan a contact's QR code, the human-readable nickname you see is provably linked to that peer's immutable cryptographic identity. The binding process spans three distinct stages across VerificationService.swift and ChatVerificationCoordinator.swift.
How the QR Payload Is Constructed and Signed
The foundation of nickname-to-fingerprint binding lies in a signed, canonical data structure that cannot be forged.
Building the VerificationQR Struct
In bitchat/Services/VerificationService.swift, the buildMyQRString method assembles a VerificationQR struct containing:
noiseKeyHex— the peer's static Noise public keysignKeyHex— the Noise signing key for signature verificationnpub— optional Nostr public key- nickname — the user-chosen display name
timestampandnonce— freshness and replay protection
The nickname is incorporated in two critical locations: appended to the URL query string (lines 53-55) and included in the canonical byte stream used for signing (lines 38-40).
Canonical Bytes and Signature Generation
The canonicalBytes() method produces a deterministic, ordered byte representation:
// Pseudocode representation of canonicalBytes() ordering
context + version + noiseKeyHex + signKeyHex + npub + nickname + timestamp + nonce
This canonical format guarantees that identical data always produces identical signatures. The signature itself is generated via transport.noiseSignData(msg), creating a sigHex value stored within the QR payload. Only the holder of the corresponding private signing key can produce valid signatures, making nickname forgery impossible.
Verifying Scanned QR Codes and Extracting Bound Identities
When a peer scans a QR code, VerificationService.verifyScannedQR performs cryptographic validation (lines 106-115).
Signature and Freshness Validation
The verification process:
- Parses the QR URL using
VerificationQR.fromURL - Validates the signature against
signKeyHexcarried in the payload itself - Checks the timestamp to reject stale QR codes
Upon successful validation, the scanning device possesses both the peer's static Noise key (noiseKeyHex) and the nickname the peer chose for themselves—cryptographically proven to originate from that key's owner.
// Verifying a scanned QR and extracting the bound nickname
if let payload = VerificationService.shared.verifyScannedQR(qrString) {
// payload.nickname is now cryptographically verified
// payload.noiseKeyHex identifies the specific peer
}
Binding Nickname to Fingerprint in the Mesh
The final binding occurs in ChatVerificationCoordinator.swift, where the verified QR payload is matched to an actual peer in the network.
Locating the Target Peer
The beginQRVerification method (lines 85-87) uses the noiseKeyHex from the QR payload to find the corresponding peer:
let coordinator = ChatVerificationCoordinator(context: viewModel)
_ = coordinator.beginQRVerification(with: payload)
// Coordinator searches for peer where noisePublicKey == payload.noiseKeyHex
The fingerprint—derived as a hash or base-58 encoding of the static Noise public key—serves as the immutable peer identifier throughout this lookup.
Challenge-Response and Final Binding
After locating the peer, the coordinator initiates a challenge/response exchange. Upon successful response, handleVerifyResponsePayload (lines 68-73) completes the binding:
- Marks the fingerprint as verified via
context.setIdentityVerified - Stores the nickname in the UI-facing peer model
- Propagates verification status through the mesh network
This triggers a vouch mechanism, allowing other connected peers to learn of the verified nickname-to-fingerprint association.
Complete Verification Flow Example
// 1. Generate your QR code with chosen nickname
let myQR = VerificationService.shared.buildMyQRString(
nickname: "alice",
npub: nil
)
// Produces: bitchat://verify?v=1&noise=ab12…&sign=cd34…&nick=alice&ts=1725580234&nonce=…
// 2. Peer scans and validates
if let verifiedPayload = VerificationService.shared.verifyScannedQR(scannedQR) {
// verifiedPayload.nickname == "alice" — cryptographically guaranteed
// 3. Coordinator binds to fingerprint
let coordinator = ChatVerificationCoordinator(context: viewModel)
coordinator.beginQRVerification(with: verifiedPayload)
}
// 4. After challenge-response, nickname is permanently bound to peer's fingerprint
Key Files and Their Roles
| File | Responsibility |
|---|---|
bitchat/Services/VerificationService.swift |
Constructs signed QR payloads; validates signatures on scanned codes |
bitchat/ViewModels/ChatVerificationCoordinator.swift |
Orchestrates peer lookup, challenge-response, and final nickname↔fingerprint binding |
bitchat/Views/VerificationViews.swift |
UI for QR generation (MyQRView) and scanning (QRScanView) |
bitchatTests/Services/VerificationServiceTests.swift |
Unit tests covering QR building, caching, freshness, and signature validation |
bitchatTests/ChatVerificationCoordinatorContextTests.swift |
End-to-end verification of nickname-to-fingerprint binding behavior |
Summary
- QR payloads are signed with the peer's Noise signing key, making nickname forgery cryptographically infeasible
- Canonical byte ordering ensures deterministic signatures across all implementations
- Fingerprint derivation from static Noise keys provides immutable peer identifiers
- Freshness checks via timestamps prevent replay attacks using old QR codes
- Challenge-response after QR scanning prevents man-in-the-middle attacks during verification
Frequently Asked Questions
How does BitChat prevent someone from using my nickname on a different identity?
The signature in the QR payload binds the nickname specifically to the signer's Noise static key. An attacker cannot produce a valid signature without the private signing key corresponding to signKeyHex in the payload. When verifyScannedQR validates the signature, it proves the nickname originated from that exact key holder.
What happens if I scan an old QR code?
QR verification includes timestamp-based freshness validation. The verifyScannedQR method checks that the QR's timestamp falls within an acceptable window, rejecting codes that are too old. This prevents replay attacks where an adversary might try to reuse a previously valid QR code.
Can I verify a nickname without the peer being online?
No. While the QR signature itself is validated offline in VerificationService, the actual binding in ChatVerificationCoordinator requires a challenge-response exchange with the peer. This live verification step prevents an attacker from presenting a legitimate QR code while impersonating the peer in network communication.
Where does the fingerprint actually come from?
The peer fingerprint is derived from the static Noise public key—specifically a hash or base-58 encoding of noiseKeyHex. This derivation is consistent across the codebase, ensuring that when beginQRVerification matches noiseKeyHex from the QR to a peer's noisePublicKey, the resulting fingerprint is identical to the one used throughout BitChat's trust model.
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 →