Skip to content

Instantly share code, notes, and snippets.

@moonsettler
Last active April 8, 2024 20:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moonsettler/05f5948291ba8dba63a3985b786233bb to your computer and use it in GitHub Desktop.
Save moonsettler/05f5948291ba8dba63a3985b786233bb to your computer and use it in GitHub Desktop.
Blinded Schnorr 2FA Co-Signing with MuSig

Blinded Schnorr 2FA Co-Signing with MuSig

Motivation

Co-signers can provide economy of scale for both cyber and physical security beyond the means of the ordinary users, however traditional multisig comes at a heavy price regarding privacy. A co-signer would learn about the user's bitcoin holdings and all transactions. The user could blind a message, send it to the co-signer, authenticate via 2FA, receive blinded sig, unblind, then aggregate with the users signature piece, and calculate a signature the co-signer can't recognize, but satisfies the 2-of-2 shared public key. The co-signer would not know the public key of the user, nor would be able to recognize any signatures on-chain.

Signing

X = X1 + X2
K1 = k1·G
K2 = k2·G

R = K1 + K2 + b·X
e = hash(R||X||m)

e' = e + b
s = (k1 + e'·x1) + (k2 + e'·x2)
s = (k1 + k2 + b(x1 + x2)) + e(x1 + x2)

sG = (K1 + K2 + b·X) + e·X
sG = R + e·X

Verification

Rv = s·G - e·X
ev = hash(R||X||m)
e ?= ev

Story

Alice being the user and Bob the blind co-signer:

Preparation

  • Alice contracts Bob as her co-signer.
  • Bob gives Alice his public key share X2 he generated specifically for Alice
  • Alice generates the shared public key X and the corresponding taproot address t
  • Alice keeps X private and sends funds to t

Transaction

  • Alice creates transaction image m, and generates b random scalar
  • Alice initiates a secure session with Bob and authenticates herself with the chosen 2FA method
  • Bob reveales Alice his single use nonce point K2
  • Alice computes R = K1 + K2 + bX, computes e = hash(R||X||m), then blinds e' = e + b
  • Alice calculates signature part k1 + e'*x1 and sends e' to Bob
  • Bob responds with signature part k2 + e'*x2 to Alice
  • Alice aggregates the signature parts to s = (k1 + e'*x1) + (k2 + e'*x2) signature
  • Alice broadcasts her signed transaction

Bob never learns X or R or e or s so he can't identify the broadcasted transaction.

Security considerations

Blind Schnorr signatures could be easily attacked in a general setting, but in case of 2FA co-signers that do not reuse public keys between customers this is not an issue. Signing is tied to identity, rate limiting signing is trivial and even inherent to the multi-factor authentication. The user has no reasonable motivation to attack the co-signer but even for users it would be unfeasible to run multiple signing requests per second. A third party attacker is expected to fail the 2FA authentication.

Threshold schemes

Schnorr threshold key and signature aggregation may also be possible in a blinded way, in the meantime it can trivially be emulated:

Taproot contract example

  • Keypath:
    • pk(A+B)
  • Scriptpath or(...)
    • pk(B+C1)
    • pk(B+C2+C3)
    • and( p(A), thresh(2, pk(C1), pk(C2), pk(C3)) )
    • and( older(4 years), tresh(2, pk(A), pk(B), pk(C1), pk(C2), pk(C3)) )

With public keys (or rather xpubs) A for Alice, B for Bob and spare keys C1, C2, C3. Alice could for example hide C1, put C2 in a safe deposite box or embed it in her will, and hand out C3 to each family member or inheritor. Normally Alice would spend in a single sig keyspend transaction that Bob co-signs. In case Bob stops cooperating, Alice can recover using her master key and any 2 of the 3 spare keys. Alice has some blind signed options with Bob as co-signer and spare keys to recover if she loses access to her master key A. Finally after 4 years Alice or her inheritors could recover with any 2 of the 5 keys.

Wallet Descriptor Backup

Redundant descriptor backups that help retain privacy, using BIP-32/39/44 with the purpose 5719106 ("WDB" is 0x574442 bigendian). For example 2 of 5 Shamir's secret sharing pieces may be required to reconstruct the recovery phrase. This recovery phrase is a seed to generate the encrypted backups. Having the recovery phrase and any of the encrypted wallet descriptor backup files created with it, is enough to recover the wallet descriptor. With that, the addresses used and the onchain balances and transactions are identifiable. The encrypted backups can be safely sent over email or uploaded to cloud. Even if their filename is changed or truncated the index allows for generating the corresponting private_key. Example derivation path: m/5719106'/0'/0'/0'/1' for the second backup file.

Value Description
recovery_phrase 128 bit entropy / 12 bip-39 words with nonstandard checksum
s1..s5 2/5 shamir's secret sharing pieces of the recovery phrase
wallet_descriptor {xpubs A, B, C1, C2, C3, parameters, taproot address construction template}
derivation_path f"m/5719106'/{coin}'/{account}'/0'/{index}'"
private_key dk(recovery_phrase, derivation_path)
backup_id ripemd160(sha256(pubkey(private_key)))
wallet_descriptor_backup index||aes(wallet_descriptor, private_key)
filename f"{backup_id}.bak"
@moonsettler
Copy link
Author

moonsettler commented Apr 8, 2024

Alternative Blind Schnorr?

# multiplicative key aggregation (Diffie-Hellman):
X = X1·x2 = X2·x1

# server shares K2 nonce point
K1 = k1·G   # client's nonce
K2 = k2·G   # server's nonce

# client prepares signature, blinds and signs
R = K1 + K2 + b·X
e = hash(R||X||m)
e' = e + b
s1 = e'·x1

# server signs and adds nonce:
s2 = k2 + s1·x2

# client adds nonce:
s = k1 + s2
s = k1 + k2 + (e + b)·x1·x2

# signature (R, s) is correct:
sG = K1 + K2 + (e + b)·X
sG = K1 + K2 + b·X + e·X
sG = R + e·X

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment