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 key
  • signKeyHex — the Noise signing key for signature verification
  • npub — optional Nostr public key
  • nickname — the user-chosen display name
  • timestamp and nonce — 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:

  1. Parses the QR URL using VerificationQR.fromURL
  2. Validates the signature against signKeyHex carried in the payload itself
  3. 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →