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