# How QR Verification Binds Nicknames to Peer Fingerprints in BitChat

> Discover how BitChat's QR verification links nicknames to peer fingerprints by cryptographically signing a payload containing both your nickname and the peer's Noise key for secure authentication.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: how-to-guide
- Published: 2026-08-05

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/VerificationService.swift) and [`ChatVerificationCoordinator.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/VerificationService.swift#L53-L55)) and included in the canonical byte stream used for signing ([lines 38-40](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/VerificationService.swift#L38-L40)).

### Canonical Bytes and Signature Generation

The `canonicalBytes()` method produces a deterministic, ordered byte representation:

```swift
// 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](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/VerificationService.swift#L106-L115)).

### 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.

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatVerificationCoordinator.swift#L85-L87)) uses the `noiseKeyHex` from the QR payload to find the corresponding peer:

```swift
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](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatVerificationCoordinator.swift#L68-L73)) 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

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/VerificationService.swift) | Constructs signed QR payloads; validates signatures on scanned codes |
| [`bitchat/ViewModels/ChatVerificationCoordinator.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/ViewModels/ChatVerificationCoordinator.swift) | Orchestrates peer lookup, challenge-response, and final nickname↔fingerprint binding |
| [`bitchat/Views/VerificationViews.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Views/VerificationViews.swift) | UI for QR generation (`MyQRView`) and scanning (`QRScanView`) |
| [`bitchatTests/Services/VerificationServiceTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/VerificationServiceTests.swift) | Unit tests covering QR building, caching, freshness, and signature validation |
| [`bitchatTests/ChatVerificationCoordinatorContextTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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.