Signature-Based Wallet Authentication for Bitcoin Cash
1. Introduction
Decentralized applications on Bitcoin Cash require a mechanism for verifying that a user controls a particular wallet address. This verification is a prerequisite for any interaction where the application must associate actions with an identity — posting bounties, accessing services, authorizing payments, or managing on-chain assets.
The dominant solution in the broader cryptocurrency ecosystem routes all communication through centralized relay servers operated by a single entity [5]. The wallet and the application each maintain a persistent WebSocket connection to the relay. Messages are encrypted end-to-end, but all metadata — who connects, when, to which application, and how often — is observable by the relay operator. The relay is a single point of failure. If the relay service is discontinued, changes pricing, or restricts access, every application that depends on it is affected.
This architecture introduces a trusted third party into what should be a trustless interaction. It is equivalent, in structure, to routing all email through a single mail server: the content may be encrypted, but the operator knows who is communicating with whom.
Currently there is no Bitcoin Cash native protocol for connecting BCH wallets to BCH applications.
We propose a solution based on the same cryptographic primitive that secures Bitcoin Cash transactions: the digital signature. Wallet authentication is fundamentally a proof of key ownership. A user who can produce a valid signature for a given address has demonstrated, through mathematics alone, that they hold the corresponding private key. No relay server, persistent connection, or on-chain transaction is required. The math is the proof.
2. Background
2.1 Elliptic Curve Digital Signatures
Bitcoin Cash uses the secp256k1 elliptic curve [2] for all digital signatures. The curve is defined over a prime field of order p = 2256 − 232 − 977, with a generator point G of order n. A private key is a randomly selected integer d in the range [1, n−1]. The corresponding public key is the elliptic curve point Q = dG.
The Elliptic Curve Digital Signature Algorithm (ECDSA) produces a signature (r, s) over a message hash e using the private key d:
- Select a random integer k in [1, n−1].
- Compute the curve point (x₁, y₁) = kG.
- Compute r = x₁ mod n. If r = 0, select a new k.
- Compute s = k⁻¹(e + rd) mod n. If s = 0, select a new k.
- The signature is the pair (r, s).
Verification requires the public key Q, the message hash e, and the signature (r, s):
- Compute u₁ = es⁻¹ mod n and u₂ = rs⁻¹ mod n.
- Compute the curve point (x₁, y₁) = u₁G + u₂Q.
- The signature is valid if r ≡ x₁ (mod n).
2.2 Public Key Recovery
A property of ECDSA that is essential to this protocol is that the public key can be recovered from the signature without being provided in advance. Given a message hash e and a signature (r, s), there are at most four candidate public keys that could have produced the signature. The recovery flag, a single byte prepended to the compact signature, identifies which candidate is correct.
This means a verifier needs only the message and the signature to determine the signer's public key, and therefore their address. The signer does not need to transmit their public key separately.
2.3 Bitcoin Signed Message Standard
The Bitcoin Signed Message standard [1] defines a method for signing arbitrary text messages using the same keys that sign transactions. The message is prefixed with a fixed string and length-encoded before hashing:
prefixed = "\x18Bitcoin Signed Message:\n" + varint(len(message)) + message hash = SHA256(SHA256(prefixed))
The prefix "\x18Bitcoin Signed Message:\n" is 26 bytes. The varint encoding uses one byte for messages shorter than 253 bytes, three bytes for messages up to 65535 bytes, and five bytes for longer messages.
The signature is produced using the recoverable ECDSA variant and encoded in compact form: one byte for the recovery flag (27–30 for uncompressed public keys, 31–34 for compressed public keys) followed by 32 bytes for r and 32 bytes for s, for a total of 65 bytes, base64-encoded.
This standard is supported by all major Bitcoin Cash wallet implementations.
2.4 CashAddr Encoding
Bitcoin Cash uses the CashAddr format [3] for encoding addresses. An address is derived from a public key by computing RIPEMD-160(SHA-256(publicKey)), then encoding the resulting 20-byte hash with a human-readable prefix ("bitcoincash:") and a BCH error-detecting code.
The address derivation is a one-way function: given a public key, the address can be computed, but a public key cannot be recovered from an address alone. This is why public key recovery from the signature is necessary for verification.
3. Protocol
3.1 Participants
The protocol involves three participants:
- Application server. Generates challenges, verifies signatures, issues session tokens.
- Application client. Runs in the user's browser. Requests challenges, displays QR codes, polls for authentication results.
- Wallet. Runs on the user's device (typically a mobile phone). Scans QR codes, signs challenges, submits signatures.
The application server and client are operated by the same party (the dApp developer). The wallet is operated by the user. There is no third party. No relay, no oracle, no intermediary.
3.2 Authentication Flow
Authentication proceeds as follows:
Step 1: Challenge generation. The application client requests a challenge from the application server. The server generates a challenge string C containing the application name and a 128-bit random nonce N:
C = "CashNect to " + applicationName + ": " + hex(N)
The server stores C with a unique identifier I, a creation timestamp, and an expiration time T (recommended: 300 seconds). The server returns I and C to the client.
Step 2: QR encoding. The client encodes I, the server's verification URL U, and C into a URI:
cashnector:{I}@{encode(U)}?challenge={encode(C)}
This URI is rendered as a QR code.
Step 3: Scanning. The user scans the QR code with their wallet. The wallet parses the URI to extract I, U, and C.
Step 4: Signing. The wallet displays C to the user and requests approval to sign. Upon approval, the wallet computes:
prefixed = "\x18Bitcoin Signed Message:\n" + varint(len(C)) + C h = SHA256(SHA256(prefixed)) (r, s, v) = ECDSA_sign_recoverable(h, privateKey) sig = base64([v] + [r] + [s])
Where v is the recovery flag, and the wallet's address A is derived from its public key.
Step 5: Submission. The wallet sends (I, A, sig) to the server at URL U via HTTP POST.
Step 6: Verification. The server retrieves the stored challenge C using identifier I. It verifies that the challenge has not expired and has not been previously consumed. It then performs the verification procedure described in Section 4. If verification succeeds, the server marks the challenge as consumed, stores the authenticated address A, and generates a random session token.
Step 7: Notification. The application client, which has been polling the server for the status of challenge I, receives the authentication result: the verified address A and the session token. The user is authenticated.
3.3 Trust Model
The user trusts only the application they are authenticating with — the same trust they would extend by visiting the application's website. The application trusts only the mathematics of secp256k1. No third party is trusted by either side.
The application server performs the verification. It does not need to contact any external service, oracle, or relay. The verification is a pure computation over the signature, the message, and the secp256k1 curve parameters.
4. Verification Procedure
Given a challenge message C, a base64-encoded signature S, and a claimed CashAddr address A, verification proceeds as follows:
4.1. Decode S from base64 to obtain 65 bytes. If the decoded length is not exactly 65 bytes, reject.
4.2. Extract the recovery flag v = S[0]. Extract r = S[1..32] and s = S[33..64]. If v is not in the range [27, 34], reject.
4.3. Determine the recovery identifier: if v ∈ {27, 28, 29, 30}, then recoveryId = v − 27 and the key is uncompressed. If v ∈ {31, 32, 33, 34}, then recoveryId = v − 31 and the key is compressed.
4.4. Construct the prefixed message:
P = "\x18Bitcoin Signed Message:\n" + varint(len(C)) + C
4.5. Compute the message hash: h = SHA256(SHA256(P)).
4.6. Recover the public key Q from (r, s), recoveryId, and h using the secp256k1 curve. If recovery fails, reject.
4.7. If the key is compressed, use the compressed representation of Q (33 bytes). If uncompressed, use the uncompressed representation (65 bytes).
4.8. Compute the address payload: payload = RIPEMD-160(SHA-256(Q)).
4.9. Encode the payload as a CashAddr address: A' = CashAddr("bitcoincash", P2PKH, payload).
4.10. Compare A' to the claimed address A. If they are equal, the signature is valid and the wallet has proven ownership of the private key corresponding to A. Otherwise, reject.
5. Security Analysis
5.1 Cryptographic Security
The security of the protocol rests entirely on the hardness of the Elliptic Curve Discrete Logarithm Problem (ECDLP) on secp256k1. An attacker who cannot solve the ECDLP cannot forge a signature, and therefore cannot authenticate as another user.
The secp256k1 curve provides approximately 128 bits of security. This is the same security level that protects all Bitcoin Cash transactions. An attack on CashNector's signature verification would simultaneously be an attack on Bitcoin Cash itself.
5.2 Challenge Entropy
Each challenge contains 128 bits of cryptographic randomness. The probability of two challenges colliding is approximately 2⁻¹²⁸, which is negligible. Combined with the expiration time, the probability of a valid challenge being guessable is zero for practical purposes.
5.3 Replay Resistance
Challenges are single-use. After a signature is verified against a challenge, the challenge is marked as consumed and cannot be used again. Even if an attacker intercepts the signed challenge in transit, they cannot replay it because the server will reject consumed challenges.
Challenges also expire after a fixed period. An attacker who obtains a signed challenge after it has expired cannot use it.
5.4 Cross-Application Replay
A signed challenge for one application cannot be used at another application. Each challenge contains the application name and a unique nonce generated by that application's server. A different application's server will not have the corresponding challenge identifier in its store, so the signature will be rejected regardless of its cryptographic validity.
5.5 Man-in-the-Middle
An attacker who intercepts the QR code can read the challenge (it is not encrypted) but cannot produce a valid signature without the user's private key. If the attacker substitutes their own challenge, the user's wallet will display the attacker's application name in the signing prompt, alerting the user that something is wrong.
If the attacker intercepts the signed response between the wallet and the server, they obtain a valid signature for a consumed challenge. This signature is useless: the challenge is single-use, and the signature cannot be used to impersonate the user for any other challenge.
5.6 Denial of Service
An attacker can attempt to exhaust the server's challenge storage by requesting many challenges. The protocol mitigates this through per-IP rate limiting and a global cap on active challenges. Challenges expire automatically, so sustained resource consumption requires sustained attack traffic.
An attacker cannot prevent a legitimate user from authenticating unless they can prevent the user's HTTP request from reaching the server, which is a network-level attack outside the scope of the authentication protocol.
5.7 Phishing
An attacker can create a malicious application and display a CashNector QR code. If the user scans it, their wallet will sign a challenge for the attacker's application. This proves the user's address to the attacker but does not give the attacker any power over the user's funds or keys.
The primary mitigation is the wallet's signing prompt. The wallet should display the application name and the authentication URL before requesting approval. Users should verify that the displayed information matches their expectations. Wallet implementations may additionally maintain registries of known applications or warn when authenticating with unfamiliar URLs.
5.8 Key Reuse Across Applications
A user who authenticates with multiple applications reveals the same address to all of them. This is by design — the address is the user's identity. If a user desires different identities for different applications, they can use different addresses from their HD wallet.
The protocol does not create any linkage between applications beyond the shared address. Each application's challenges, sessions, and interactions are independent.
6. Privacy
The authentication process reveals the wallet's address to the application. This is inherent to the purpose of authentication — the user is proving which address they control. The address is already public on the blockchain.
No on-chain transaction is created during authentication. The authentication exchange is entirely off-chain: an HTTP request from the wallet to the server, and polling requests from the browser to the server. An observer of the blockchain cannot determine which applications a user has authenticated with.
The protocol does not require the wallet to reveal its balance, UTXO set, transaction history, or any information beyond the address. The protocol does not require the application to reveal its user base to any third party. There is no relay operator accumulating metadata about connections across the ecosystem.
The privacy properties of CashNector are strictly better than those of relay-based protocols, where the relay operator necessarily observes all connection metadata across all applications that use the service.
7. Protocol Extensions
7.1 Session Tokens
After successful verification, the server generates a cryptographically random session token (recommended: 256 bits). This token is returned to the application client and can be used as a Bearer token for subsequent authenticated requests. The session token bridges the gap between the one-time signature verification and ongoing application interactions.
7.2 CashToken Identity
The protocol can be extended to support identity based on CashToken NFTs [6]. In addition to proving address ownership, the wallet would present a CashToken category and commitment. The server would verify that the authenticated address holds the claimed token by querying a UTXO indexer. This enables token-gated access patterns without modifying the core authentication flow.
7.3 Schnorr Signatures
Bitcoin Cash supports Schnorr signatures at the consensus level [7]. Schnorr signatures are 64 bytes (no recovery flag) and require the public key to be transmitted alongside the signature, since recovery is not possible. A future version of the protocol could support Schnorr signatures by including the public key in the wallet's submission. The server would verify the signature directly against the provided public key, then derive the address from the public key and compare it to the claimed address.
7.4 On-Chain Verification
For applications hosted entirely on-chain (where no traditional server exists), the challenge and verification can be mediated through the blockchain itself. The application publishes a challenge via an OP_RETURN output. The wallet publishes its signature via another OP_RETURN output. The application reads the signature from the blockchain and verifies it. This variant requires on-chain transactions (and therefore transaction fees), but enables authentication for fully decentralized applications with no server component.
8. Comparison with Relay-Based Protocols
| Property | Relay-based | CashNector |
|---|---|---|
| Third-party infrastructure | Required (relay server) | None |
| Persistent connection | WebSocket to relay | None (single HTTP request) |
| Authentication method | Session key exchange | secp256k1 signature |
| Metadata visibility | Relay sees all connection metadata | No third party sees anything |
| Single point of failure | Relay server | None (application is self-contained) |
| On-chain transaction | Not required | Not required |
| Ongoing communication | Supported via relay | Not supported (auth only) |
| Network requirements | WebSocket support | HTTP |
The relay-based model provides ongoing communication between wallet and application after the initial connection. CashNector provides authentication only. Applications that require ongoing wallet interaction can build additional communication layers on top of the authenticated session using standard web protocols (WebSocket, Server-Sent Events, HTTP polling) secured by the session token.
9. Worked Example
The following is a complete authentication exchange with concrete values.
9.1 Challenge generation. The application "GitBCH" requests a challenge. The server generates:
challengeId = "7a3f9c2e1b4d6a8e" challenge = "CashNect to GitBCH: 4f2a8c1d9e3b7f0652d4a1c8e9f03b7a"
9.2 QR encoding. The client constructs the URI:
cashnector:7a3f9c2e1b4d6a8e@http%3A%2F%2Fgitbch.com%2Fcashnector?challenge=CashNect%20to%20GitBCH%3A%204f2a8c1d9e3b7f0652d4a1c8e9f03b7a
This URI is rendered as a QR code.
9.3 Signing. The user's wallet holds private key d (not shown). The wallet's address is:
bitcoincash:qzpvtca97n9gy744g55x0ctm30ppuad4kdy6k52f58c
The wallet constructs the prefixed message:
prefix = "\x18Bitcoin Signed Message:\n" (26 bytes) varint = [0x3e] (1 byte, message length = 62) message = "CashNect to GitBCH: 4f2a8c1d..." (62 bytes)
The wallet computes h = SHA256(SHA256(prefix + varint + message)) and signs h with its private key, producing a 65-byte compact signature with recovery flag 31 (compressed key, recoveryId 0). The signature is base64-encoded.
9.4 Submission. The wallet sends to http://gitbch.com/cashnector/verify:
{
"challengeId": "7a3f9c2e1b4d6a8e",
"address": "bitcoincash:qzpvtca97n9gy744g55x0ctm30ppuad4kdy6k52f58c",
"signature": "H3kB7xM2q9..."
}
9.5 Verification. The server retrieves the challenge, verifies it has not expired or been consumed, decodes the signature, recovers the public key from the signature and message hash, derives the CashAddr address from the recovered key, and compares it to the claimed address. The addresses match. The server marks the challenge as consumed, generates a session token, and returns:
{
"ok": true,
"address": "bitcoincash:qzpvtca97n9gy744g55x0ctm30ppuad4kdy6k52f58c",
"sessionToken": "a9f3c2..."
}
9.6 Completion. The application client polls the server, receives the authenticated address and session token, and displays the user as connected. The total exchange required one HTTP POST from the wallet and one or more HTTP GETs from the browser. No relay, no persistent connection, no on-chain transaction.
10. Conclusion
We have described a protocol for authenticating Bitcoin Cash wallets to decentralized applications using digital signatures. The protocol derives its security from the same secp256k1 elliptic curve cryptography that secures the Bitcoin Cash network. Authentication is a single signature over a random challenge, verified by recovering the public key and comparing the derived address to the claimed address.
The protocol requires no relay servers, no persistent connections, no WebSocket infrastructure, and no on-chain transactions. It introduces no third party into the authentication exchange. The trust model is minimal: the user trusts the application they are choosing to authenticate with, and the application trusts the mathematics of the secp256k1 curve.
The protocol is open. It can be implemented by any wallet using standard Bitcoin Cash cryptographic primitives. It can be verified by any application server using publicly available elliptic curve libraries. No registration, API keys, or subscription fees are required.
The problem of wallet authentication does not require complex infrastructure. It requires a signature.
References
- [1]Bitcoin Signed Message format. Bitcoin Wiki. https://en.bitcoin.it/wiki/Message_signing
- [2]Standards for Efficient Cryptography Group. SEC 2: Recommended Elliptic Curve Domain Parameters, version 2.0. Certicom Research, 2010.
- [3]CashAddr: Address format for Bitcoin Cash. bitcoincashorg/bitcoincash.org, specification, 2018.
- [4]C. Gilliard. BIP-137: Signatures of Messages using Private Keys. Bitcoin Improvement Proposal, 2019.
- [5]WalletConnect. WalletConnect v2.0 Protocol Specification. https://docs.walletconnect.com, 2022.
- [6]J. Dreyzehner. CHIP-2022-02-CashTokens: Token Primitives for Bitcoin Cash. Bitcoin Cash Improvement Proposal, 2022.
- [7]M. Lundkvist. Bitcoin Cash Schnorr Signature specification, activated May 2019.