github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/p2p/conn/secret_connection.go (about) 1 package conn 2 3 import ( 4 "bytes" 5 "crypto/cipher" 6 crand "crypto/rand" 7 "crypto/sha256" 8 "crypto/subtle" 9 "encoding/binary" 10 "io" 11 "math" 12 "net" 13 "sync" 14 "time" 15 16 pool "github.com/libp2p/go-buffer-pool" 17 "golang.org/x/crypto/chacha20poly1305" 18 "golang.org/x/crypto/curve25519" 19 "golang.org/x/crypto/hkdf" 20 "golang.org/x/crypto/nacl/box" 21 22 "github.com/gnolang/gno/tm2/pkg/amino" 23 "github.com/gnolang/gno/tm2/pkg/async" 24 "github.com/gnolang/gno/tm2/pkg/crypto" 25 "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" 26 "github.com/gnolang/gno/tm2/pkg/errors" 27 ) 28 29 // 4 + 1024 == 1028 total frame size 30 const dataLenSize = 4 31 32 const ( 33 dataMaxSize = 1024 34 totalFrameSize = dataMaxSize + dataLenSize 35 aeadSizeOverhead = 16 // overhead of poly 1305 authentication tag 36 aeadKeySize = chacha20poly1305.KeySize 37 aeadNonceSize = chacha20poly1305.NonceSize 38 ) 39 40 var ( 41 ErrSmallOrderRemotePubKey = errors.New("detected low order point from remote peer") 42 ErrSharedSecretIsZero = errors.New("shared secret is all zeroes") 43 ) 44 45 // SecretConnection implements net.Conn. 46 // It is an implementation of the STS protocol. 47 // See https://github.com/tendermint/classic/blob/0.1/docs/sts-final.pdf for 48 // details on the protocol. 49 // 50 // Consumers of the SecretConnection are responsible for authenticating 51 // the remote peer's pubkey against known information, like a nodeID. 52 // Otherwise they are vulnerable to MITM. 53 // (TODO(ismail): see also https://github.com/tendermint/classic/issues/3010) 54 type SecretConnection struct { 55 // immutable 56 recvAead cipher.AEAD 57 sendAead cipher.AEAD 58 59 remPubKey crypto.PubKey 60 conn io.ReadWriteCloser 61 62 // net.Conn must be thread safe: 63 // https://golang.org/pkg/net/#Conn. 64 // Since we have internal mutable state, 65 // we need mtxs. But recv and send states 66 // are independent, so we can use two mtxs. 67 // All .Read are covered by recvMtx, 68 // all .Write are covered by sendMtx. 69 recvMtx sync.Mutex 70 recvBuffer []byte 71 recvNonce *[aeadNonceSize]byte 72 73 sendMtx sync.Mutex 74 sendNonce *[aeadNonceSize]byte 75 } 76 77 // MakeSecretConnection performs handshake and returns a new authenticated 78 // SecretConnection. 79 // Returns nil if there is an error in handshake. 80 // Caller should call conn.Close() 81 // See docs/sts-final.pdf for more information. 82 func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (*SecretConnection, error) { 83 locPubKey := locPrivKey.PubKey() 84 85 // Generate ephemeral keys for perfect forward secrecy. 86 locEphPub, locEphPriv := genEphKeys() 87 88 // Write local ephemeral pubkey and receive one too. 89 // NOTE: every 32-byte string is accepted as a Curve25519 public key 90 // (see DJB's Curve25519 paper: http://cr.yp.to/ecdh/curve25519-20060209.pdf) 91 remEphPub, err := shareEphPubKey(conn, locEphPub) 92 if err != nil { 93 return nil, err 94 } 95 96 // Sort by lexical order. 97 loEphPub, _ := sort32(locEphPub, remEphPub) 98 99 // Check if the local ephemeral public key 100 // was the least, lexicographically sorted. 101 locIsLeast := bytes.Equal(locEphPub[:], loEphPub[:]) 102 103 // Compute common diffie hellman secret using X25519. 104 dhSecret, err := computeDHSecret(remEphPub, locEphPriv) 105 if err != nil { 106 return nil, err 107 } 108 109 // generate the secret used for receiving, sending, challenge via hkdf-sha2 on dhSecret 110 recvSecret, sendSecret, challenge := deriveSecretAndChallenge(dhSecret, locIsLeast) 111 112 sendAead, err := chacha20poly1305.New(sendSecret[:]) 113 if err != nil { 114 return nil, errors.New("invalid send SecretConnection Key") 115 } 116 recvAead, err := chacha20poly1305.New(recvSecret[:]) 117 if err != nil { 118 return nil, errors.New("invalid receive SecretConnection Key") 119 } 120 // Construct SecretConnection. 121 sc := &SecretConnection{ 122 conn: conn, 123 recvBuffer: nil, 124 recvNonce: new([aeadNonceSize]byte), 125 sendNonce: new([aeadNonceSize]byte), 126 recvAead: recvAead, 127 sendAead: sendAead, 128 } 129 130 // Sign the challenge bytes for authentication. 131 locSignature := signChallenge(challenge, locPrivKey) 132 133 // Share (in secret) each other's pubkey & challenge signature 134 authSigMsg, err := shareAuthSignature(sc, locPubKey, locSignature) 135 if err != nil { 136 return nil, err 137 } 138 139 remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig 140 141 if _, ok := remPubKey.(ed25519.PubKeyEd25519); !ok { 142 return nil, errors.New("expected ed25519 pubkey, got %T", remPubKey) 143 } 144 145 if !remPubKey.VerifyBytes(challenge[:], remSignature) { 146 return nil, errors.New("challenge verification failed") 147 } 148 149 // We've authorized. 150 sc.remPubKey = remPubKey 151 return sc, nil 152 } 153 154 // RemotePubKey returns authenticated remote pubkey 155 func (sc *SecretConnection) RemotePubKey() crypto.PubKey { 156 return sc.remPubKey 157 } 158 159 // Writes encrypted frames of `totalFrameSize + aeadSizeOverhead`. 160 // CONTRACT: data smaller than dataMaxSize is written atomically. 161 func (sc *SecretConnection) Write(data []byte) (n int, err error) { 162 sc.sendMtx.Lock() 163 defer sc.sendMtx.Unlock() 164 165 for 0 < len(data) { 166 if err := func() error { 167 sealedFrame := pool.Get(aeadSizeOverhead + totalFrameSize) 168 frame := pool.Get(totalFrameSize) 169 defer func() { 170 pool.Put(sealedFrame) 171 pool.Put(frame) 172 }() 173 var chunk []byte 174 if dataMaxSize < len(data) { 175 chunk = data[:dataMaxSize] 176 data = data[dataMaxSize:] 177 } else { 178 chunk = data 179 data = nil 180 } 181 chunkLength := len(chunk) 182 binary.LittleEndian.PutUint32(frame, uint32(chunkLength)) 183 copy(frame[dataLenSize:], chunk) 184 185 // encrypt the frame 186 sc.sendAead.Seal(sealedFrame[:0], sc.sendNonce[:], frame, nil) 187 incrNonce(sc.sendNonce) 188 // end encryption 189 190 _, err = sc.conn.Write(sealedFrame) 191 if err != nil { 192 return err 193 } 194 n += len(chunk) 195 return nil 196 }(); err != nil { 197 return n, err 198 } 199 } 200 return n, err 201 } 202 203 // CONTRACT: data smaller than dataMaxSize is read atomically. 204 func (sc *SecretConnection) Read(data []byte) (n int, err error) { 205 sc.recvMtx.Lock() 206 defer sc.recvMtx.Unlock() 207 208 // read off and update the recvBuffer, if non-empty 209 if 0 < len(sc.recvBuffer) { 210 n = copy(data, sc.recvBuffer) 211 sc.recvBuffer = sc.recvBuffer[n:] 212 return 213 } 214 215 // read off the conn 216 sealedFrame := pool.Get(aeadSizeOverhead + totalFrameSize) 217 defer pool.Put(sealedFrame) 218 _, err = io.ReadFull(sc.conn, sealedFrame) 219 if err != nil { 220 return 221 } 222 223 // decrypt the frame. 224 // reads and updates the sc.recvNonce 225 frame := pool.Get(totalFrameSize) 226 defer pool.Put(frame) 227 _, err = sc.recvAead.Open(frame[:0], sc.recvNonce[:], sealedFrame, nil) 228 if err != nil { 229 return n, errors.New("failed to decrypt SecretConnection") 230 } 231 incrNonce(sc.recvNonce) 232 // end decryption 233 234 // copy checkLength worth into data, 235 // set recvBuffer to the rest. 236 chunkLength := binary.LittleEndian.Uint32(frame) // read the first four bytes 237 if chunkLength > dataMaxSize { 238 return 0, errors.New("chunkLength is greater than dataMaxSize") 239 } 240 chunk := frame[dataLenSize : dataLenSize+chunkLength] 241 n = copy(data, chunk) 242 if n < len(chunk) { 243 sc.recvBuffer = make([]byte, len(chunk)-n) 244 copy(sc.recvBuffer, chunk[n:]) 245 } 246 return n, err 247 } 248 249 // Implements net.Conn 250 func (sc *SecretConnection) Close() error { return sc.conn.Close() } 251 func (sc *SecretConnection) LocalAddr() net.Addr { return sc.conn.(net.Conn).LocalAddr() } 252 func (sc *SecretConnection) RemoteAddr() net.Addr { return sc.conn.(net.Conn).RemoteAddr() } 253 func (sc *SecretConnection) SetDeadline(t time.Time) error { return sc.conn.(net.Conn).SetDeadline(t) } 254 func (sc *SecretConnection) SetReadDeadline(t time.Time) error { 255 return sc.conn.(net.Conn).SetReadDeadline(t) 256 } 257 258 func (sc *SecretConnection) SetWriteDeadline(t time.Time) error { 259 return sc.conn.(net.Conn).SetWriteDeadline(t) 260 } 261 262 func genEphKeys() (ephPub, ephPriv *[32]byte) { 263 var err error 264 // TODO: Probably not a problem but ask Tony: different from the rust implementation (uses x25519-dalek), 265 // we do not "clamp" the private key scalar: 266 // see: https://github.com/dalek-cryptography/x25519-dalek/blob/34676d336049df2bba763cc076a75e47ae1f170f/src/x25519.rs#L56-L74 267 ephPub, ephPriv, err = box.GenerateKey(crand.Reader) 268 if err != nil { 269 panic("Could not generate ephemeral key-pair") 270 } 271 return 272 } 273 274 func shareEphPubKey(conn io.ReadWriteCloser, locEphPub *[32]byte) (remEphPub *[32]byte, err error) { 275 // Send our pubkey and receive theirs in tandem. 276 trs, _ := async.Parallel( 277 func(_ int) (val interface{}, err error, abort bool) { 278 _, err1 := amino.MarshalSizedWriter(conn, locEphPub) 279 if err1 != nil { 280 return nil, err1, true // abort 281 } 282 return nil, nil, false 283 }, 284 func(_ int) (val interface{}, err error, abort bool) { 285 var _remEphPub [32]byte 286 _, err2 := amino.UnmarshalSizedReader(conn, &_remEphPub, 1024*1024) // TODO 287 if err2 != nil { 288 return nil, err2, true // abort 289 } 290 if hasSmallOrder(_remEphPub) { 291 return nil, ErrSmallOrderRemotePubKey, true 292 } 293 return _remEphPub, nil, false 294 }, 295 ) 296 297 // If error: 298 if trs.FirstError() != nil { 299 err = trs.FirstError() 300 return 301 } 302 303 // Otherwise: 304 _remEphPub := trs.FirstValue().([32]byte) 305 return &_remEphPub, nil 306 } 307 308 // use the same blacklist as lib sodium (see https://eprint.iacr.org/2017/806.pdf for reference): 309 // https://github.com/jedisct1/libsodium/blob/536ed00d2c5e0c65ac01e29141d69a30455f2038/src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c#L11-L17 310 var blacklist = [][32]byte{ 311 // 0 (order 4) 312 { 313 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 314 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 315 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 316 }, 317 // 1 (order 1) 318 { 319 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 320 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 321 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 322 }, 323 // 325606250916557431795983626356110631294008115727848805560023387167927233504 324 // (order 8) 325 { 326 0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 327 0xfa, 0xf1, 0x9f, 0xc4, 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 328 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8, 0x00, 329 }, 330 // 39382357235489614581723060781553021112529911719440698176882885853963445705823 331 // (order 8) 332 { 333 0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 334 0x55, 0x9c, 0x83, 0xef, 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 335 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f, 0x11, 0x57, 336 }, 337 // p-1 (order 2) 338 { 339 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 340 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 341 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 342 }, 343 // p (=0, order 4) 344 { 345 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 346 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 347 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 348 }, 349 // p+1 (=1, order 1) 350 { 351 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 352 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 353 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 354 }, 355 } 356 357 func hasSmallOrder(pubKey [32]byte) bool { 358 isSmallOrderPoint := false 359 for _, bl := range blacklist { 360 if subtle.ConstantTimeCompare(pubKey[:], bl[:]) == 1 { 361 isSmallOrderPoint = true 362 break 363 } 364 } 365 return isSmallOrderPoint 366 } 367 368 func deriveSecretAndChallenge(dhSecret *[32]byte, locIsLeast bool) (recvSecret, sendSecret *[aeadKeySize]byte, challenge *[32]byte) { 369 hash := sha256.New 370 hkdf := hkdf.New(hash, dhSecret[:], nil, []byte("TENDERMINT_SECRET_CONNECTION_KEY_AND_CHALLENGE_GEN")) 371 // get enough data for 2 aead keys, and a 32 byte challenge 372 res := new([2*aeadKeySize + 32]byte) 373 _, err := io.ReadFull(hkdf, res[:]) 374 if err != nil { 375 panic(err) 376 } 377 378 challenge = new([32]byte) 379 recvSecret = new([aeadKeySize]byte) 380 sendSecret = new([aeadKeySize]byte) 381 // Use the last 32 bytes as the challenge 382 copy(challenge[:], res[2*aeadKeySize:2*aeadKeySize+32]) 383 384 // bytes 0 through aeadKeySize - 1 are one aead key. 385 // bytes aeadKeySize through 2*aeadKeySize -1 are another aead key. 386 // which key corresponds to sending and receiving key depends on whether 387 // the local key is less than the remote key. 388 if locIsLeast { 389 copy(recvSecret[:], res[0:aeadKeySize]) 390 copy(sendSecret[:], res[aeadKeySize:aeadKeySize*2]) 391 } else { 392 copy(sendSecret[:], res[0:aeadKeySize]) 393 copy(recvSecret[:], res[aeadKeySize:aeadKeySize*2]) 394 } 395 396 return 397 } 398 399 // computeDHSecret computes a Diffie-Hellman shared secret key 400 // from our own local private key and the other's public key. 401 // 402 // It returns an error if the computed shared secret is all zeroes. 403 func computeDHSecret(remPubKey, locPrivKey *[32]byte) (shrKey *[32]byte, err error) { 404 shrKey = new([32]byte) 405 curve25519.ScalarMult(shrKey, locPrivKey, remPubKey) 406 407 // reject if the returned shared secret is all zeroes 408 // related to: https://github.com/tendermint/classic/issues/3010 409 zero := new([32]byte) 410 if subtle.ConstantTimeCompare(shrKey[:], zero[:]) == 1 { 411 return nil, ErrSharedSecretIsZero 412 } 413 return 414 } 415 416 func sort32(foo, bar *[32]byte) (lo, hi *[32]byte) { 417 if bytes.Compare(foo[:], bar[:]) < 0 { 418 lo = foo 419 hi = bar 420 } else { 421 lo = bar 422 hi = foo 423 } 424 return 425 } 426 427 func signChallenge(challenge *[32]byte, locPrivKey crypto.PrivKey) (signature []byte) { 428 signature, err := locPrivKey.Sign(challenge[:]) 429 // TODO(ismail): let signChallenge return an error instead 430 if err != nil { 431 panic(err) 432 } 433 return 434 } 435 436 type authSigMessage struct { 437 Key crypto.PubKey 438 Sig []byte 439 } 440 441 func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKey, signature []byte) (recvMsg authSigMessage, err error) { 442 // Send our info and receive theirs in tandem. 443 trs, _ := async.Parallel( 444 func(_ int) (val interface{}, err error, abort bool) { 445 _, err1 := amino.MarshalSizedWriter(sc, authSigMessage{pubKey, signature}) 446 if err1 != nil { 447 return nil, err1, true // abort 448 } 449 return nil, nil, false 450 }, 451 func(_ int) (val interface{}, err error, abort bool) { 452 var _recvMsg authSigMessage 453 _, err2 := amino.UnmarshalSizedReader(sc, &_recvMsg, 1024*1024) // TODO 454 if err2 != nil { 455 return nil, err2, true // abort 456 } 457 return _recvMsg, nil, false 458 }, 459 ) 460 461 // If error: 462 if trs.FirstError() != nil { 463 err = trs.FirstError() 464 return 465 } 466 467 _recvMsg := trs.FirstValue().(authSigMessage) 468 return _recvMsg, nil 469 } 470 471 // -------------------------------------------------------------------------------- 472 473 // Increment nonce little-endian by 1 with wraparound. 474 // Due to chacha20poly1305 expecting a 12 byte nonce we do not use the first four 475 // bytes. We only increment a 64 bit unsigned int in the remaining 8 bytes 476 // (little-endian in nonce[4:]). 477 func incrNonce(nonce *[aeadNonceSize]byte) { 478 counter := binary.LittleEndian.Uint64(nonce[4:]) 479 if counter == math.MaxUint64 { 480 // Terminates the session and makes sure the nonce would not re-used. 481 // See https://github.com/tendermint/classic/issues/3531 482 panic("can't increase nonce without overflow") 483 } 484 counter++ 485 binary.LittleEndian.PutUint64(nonce[4:], counter) 486 }