mirror of
https://github.com/luxfi/proofs.git
synced 2026-10-30 15:21:05 +00:00
265 lines
8.2 KiB
Plaintext
265 lines
8.2 KiB
Plaintext
/*
|
|
* FROST Threshold Signing Protocol
|
|
* Formal model in Tamarin Prover
|
|
*
|
|
* Protocol source:
|
|
* ~/work/lux/consensus/protocol/quasar/quasar.go (GenerateDualKeys, BLS threshold)
|
|
* FROST: Flexible Round-Optimized Schnorr Threshold Signatures (RFC 9591)
|
|
*
|
|
* Protocol:
|
|
* DKG phase (trusted dealer model, matching GenerateDualKeys):
|
|
* 1. Dealer generates random polynomial f(x) of degree t-1
|
|
* 2. Group secret s = f(0), group public key Y = g^s
|
|
* 3. Share for party i: s_i = f(i), verification key Y_i = g^{s_i}
|
|
* 4. Shares distributed to n parties over secure channels
|
|
*
|
|
* Signing phase (t-of-n, matching DualSignRound1 / TripleSignRound1):
|
|
* Round 1:
|
|
* Each signer i generates nonce pair (d_i, e_i), commits H(D_i, E_i)
|
|
* Broadcasts (D_i = g^{d_i}, E_i = g^{e_i})
|
|
* Round 2:
|
|
* Binding factor rho_i = H(i, msg, B) where B = {(D_j, E_j)}
|
|
* Signer i computes z_i = d_i + e_i * rho_i + lambda_i * s_i * c
|
|
* where c = H(R, Y, msg) and R = product(D_j * E_j^{rho_j})
|
|
* Aggregation:
|
|
* sigma = (R, z) where z = sum(z_i)
|
|
* Verification: g^z == R * Y^c
|
|
*
|
|
* Tamarin abstraction choices:
|
|
* - Polynomial evaluation modeled via share generation function
|
|
* - Nonces as fresh values per signing session
|
|
* - Lagrange coefficient folded into signing equation
|
|
* - Threshold check: aggregator collects exactly t valid shares
|
|
* - t-1 shares reveal nothing: modeled by absence of derivation rule
|
|
*
|
|
* Security goals:
|
|
* 1. Unforgeability: t valid shares needed for valid signature
|
|
* 2. Secrecy: t-1 compromised shares reveal nothing about group secret
|
|
* 3. Correctness: t honest signers always produce valid signature
|
|
*
|
|
* Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
*/
|
|
|
|
theory FROSTThreshold
|
|
begin
|
|
|
|
builtins: hashing, signing, multiset
|
|
|
|
/*
|
|
* ==================================================================
|
|
* Function symbols
|
|
* ==================================================================
|
|
*/
|
|
|
|
functions:
|
|
/* Key generation / shares */
|
|
frost_group_pk/1, /* frost_group_pk(group_secret) -> group_pk */
|
|
frost_share/2, /* frost_share(group_secret, party_id) -> share_i */
|
|
frost_share_pk/1, /* frost_share_pk(share_i) -> verification_key_i */
|
|
|
|
/* Signing */
|
|
frost_sign_share/4, /* frost_sign_share(msg, share_i, nonce_d, nonce_e) -> z_i */
|
|
frost_nonce_commit/2, /* frost_nonce_commit(nonce_d, nonce_e) -> (D_i, E_i) */
|
|
frost_partial_R/2, /* frost_partial_R(nonce_d, nonce_e) -> R_i contribution */
|
|
|
|
/* Aggregation (2-of-n shown) */
|
|
frost_aggregate/2, /* frost_aggregate(z_1, z_2) -> z */
|
|
frost_group_R/2, /* frost_group_R(R_1, R_2) -> R */
|
|
|
|
/* Verification */
|
|
frost_verify/4, /* frost_verify(msg, z, R, group_pk) -> true/fail */
|
|
|
|
/* Reconstruction attempt (requires t shares) */
|
|
frost_reconstruct/2 /* frost_reconstruct(share_1, share_2) -> group_secret */
|
|
|
|
/*
|
|
* ==================================================================
|
|
* Equations
|
|
* ==================================================================
|
|
*/
|
|
|
|
equations:
|
|
/* Share public key derivation */
|
|
frost_share_pk(frost_share(s, i)) = frost_share_pk(frost_share(s, i)),
|
|
|
|
/* Correct verification: aggregate of t valid sign_shares verifies */
|
|
frost_verify(m,
|
|
frost_aggregate(
|
|
frost_sign_share(m, frost_share(s, i1), d1, e1),
|
|
frost_sign_share(m, frost_share(s, i2), d2, e2)),
|
|
frost_group_R(
|
|
frost_partial_R(d1, e1),
|
|
frost_partial_R(d2, e2)),
|
|
frost_group_pk(s)) = true,
|
|
|
|
/* Reconstruction: t shares reconstruct the secret (2-of-n) */
|
|
frost_reconstruct(frost_share(s, i1), frost_share(s, i2)) = s
|
|
|
|
/*
|
|
* ==================================================================
|
|
* Restrictions
|
|
* ==================================================================
|
|
*/
|
|
|
|
restriction Equality:
|
|
"All x y #i. Eq(x, y) @ i ==> x = y"
|
|
|
|
restriction DistinctParties:
|
|
"All i1 i2 s #t. DistinctShares(i1, i2, s) @ t ==> not(i1 = i2)"
|
|
|
|
/*
|
|
* ==================================================================
|
|
* Rules
|
|
* ==================================================================
|
|
*/
|
|
|
|
/*
|
|
* Trusted Dealer DKG: generates group secret and distributes shares.
|
|
* Models GenerateDualKeys in quasar.go.
|
|
*/
|
|
rule DKG_TrustedDealer:
|
|
[ Fr(~group_secret) ]
|
|
--[ DKGComplete(~group_secret),
|
|
SecretGenerated(~group_secret) ]->
|
|
[ !GroupPK(frost_group_pk(~group_secret)),
|
|
!GroupSecret(~group_secret),
|
|
Out(frost_group_pk(~group_secret)) ]
|
|
|
|
/*
|
|
* Share Distribution: dealer sends share to party i over secure channel.
|
|
*/
|
|
rule Distribute_Share:
|
|
let share_i = frost_share(group_secret, ~party_id)
|
|
in
|
|
[ !GroupSecret(group_secret), Fr(~party_id) ]
|
|
--[ ShareDistributed(~party_id, share_i),
|
|
HonestParty(~party_id) ]->
|
|
[ !PartyShare(~party_id, share_i),
|
|
!PartyVerificationKey(~party_id, frost_share_pk(share_i)) ]
|
|
|
|
/*
|
|
* Signing Round 1: party generates nonces and commits.
|
|
*/
|
|
rule Sign_Round1:
|
|
[ !PartyShare(pid, share_i), Fr(~nonce_d), Fr(~nonce_e), In(msg) ]
|
|
--[ Round1(pid, msg, ~nonce_d, ~nonce_e) ]->
|
|
[ SigningState(pid, msg, share_i, ~nonce_d, ~nonce_e),
|
|
Out(frost_nonce_commit(~nonce_d, ~nonce_e)) ]
|
|
|
|
/*
|
|
* Signing Round 2: party produces partial signature.
|
|
*/
|
|
rule Sign_Round2:
|
|
let z_i = frost_sign_share(msg, share_i, nonce_d, nonce_e)
|
|
R_i = frost_partial_R(nonce_d, nonce_e)
|
|
in
|
|
[ SigningState(pid, msg, share_i, nonce_d, nonce_e) ]
|
|
--[ SignedShare(pid, msg, z_i) ]->
|
|
[ PartialSig(pid, msg, z_i, R_i) ]
|
|
|
|
/*
|
|
* Aggregation: collect t=2 partial signatures and produce group signature.
|
|
*/
|
|
rule Aggregate_Threshold:
|
|
let z = frost_aggregate(z1, z2)
|
|
R = frost_group_R(R1, R2)
|
|
in
|
|
[ PartialSig(p1, msg, z1, R1),
|
|
PartialSig(p2, msg, z2, R2) ]
|
|
--[ Aggregated(msg, z, R),
|
|
DistinctShares(p1, p2, msg) ]->
|
|
[ Out(<z, R>) ]
|
|
|
|
/*
|
|
* Verification: verifier checks aggregate signature against group key.
|
|
*/
|
|
rule Verify_Signature:
|
|
[ In(<z, R>), !GroupPK(gpk), In(msg) ]
|
|
--[ Eq(frost_verify(msg, z, R, gpk), true),
|
|
Verified(msg, z, gpk) ]->
|
|
[ ]
|
|
|
|
/*
|
|
* Corrupt Party: adversary learns a party's share.
|
|
*/
|
|
rule Corrupt_Party:
|
|
[ !PartyShare(pid, share_i) ]
|
|
--[ Corrupted(pid, share_i) ]->
|
|
[ Out(share_i) ]
|
|
|
|
/*
|
|
* ==================================================================
|
|
* Lemmas
|
|
* ==================================================================
|
|
*/
|
|
|
|
/*
|
|
* Correctness: two honest signers can produce a valid signature.
|
|
*/
|
|
lemma correctness:
|
|
exists-trace
|
|
"Ex msg z gpk #i.
|
|
Verified(msg, z, gpk) @ i"
|
|
|
|
/*
|
|
* THEOREM 1: Threshold Unforgeability
|
|
*
|
|
* A valid signature requires contributions from at least t=2 parties
|
|
* who each possessed a valid share. If the adversary never corrupted
|
|
* t parties, they cannot forge.
|
|
*/
|
|
lemma threshold_unforgeability:
|
|
"All msg z gpk #v.
|
|
Verified(msg, z, gpk) @ v
|
|
==>
|
|
( (Ex p1 p2 z1 z2 #s1 #s2.
|
|
SignedShare(p1, msg, z1) @ s1 &
|
|
SignedShare(p2, msg, z2) @ s2 &
|
|
not(p1 = p2) &
|
|
s1 < v & s2 < v)
|
|
| (Ex p1 sh1 p2 sh2 #c1 #c2.
|
|
Corrupted(p1, sh1) @ c1 &
|
|
Corrupted(p2, sh2) @ c2 &
|
|
not(p1 = p2)) )"
|
|
|
|
/*
|
|
* THEOREM 2: Single Share Reveals Nothing
|
|
*
|
|
* Compromising exactly one share (t-1 = 1) does not allow the
|
|
* adversary to reconstruct the group secret. The group secret
|
|
* remains secret if fewer than t parties are corrupted.
|
|
*
|
|
* Modeled as: group secret s is secret unless t shares are leaked.
|
|
* With only 1 share, the adversary cannot derive s.
|
|
*/
|
|
lemma group_secret_secrecy:
|
|
"All s #g.
|
|
SecretGenerated(s) @ g
|
|
==>
|
|
( not(Ex #k. K(s) @ k)
|
|
| (Ex p1 sh1 p2 sh2 #c1 #c2.
|
|
Corrupted(p1, sh1) @ c1 &
|
|
Corrupted(p2, sh2) @ c2 &
|
|
not(p1 = p2)) )"
|
|
|
|
/*
|
|
* THEOREM 3: t-1 Shares Insufficient for Signing
|
|
*
|
|
* If only one party is corrupted, the adversary cannot produce
|
|
* a valid aggregate signature (needs t=2 shares to sign).
|
|
*/
|
|
lemma subtreshold_cannot_sign:
|
|
"All msg z gpk #v.
|
|
Verified(msg, z, gpk) @ v &
|
|
/* exactly one corruption */
|
|
(Ex p1 sh1 #c1.
|
|
Corrupted(p1, sh1) @ c1 &
|
|
not(Ex p2 sh2 #c2. Corrupted(p2, sh2) @ c2 & not(p1 = p2)))
|
|
==>
|
|
/* then an honest signer must have participated */
|
|
(Ex p_h z_h #s.
|
|
SignedShare(p_h, msg, z_h) @ s &
|
|
not(Ex sh_h #ch. Corrupted(p_h, sh_h) @ ch))"
|
|
|
|
end
|