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