From 1bd439f9899b8e572583e86af3276a47e4858fbf Mon Sep 17 00:00:00 2001 From: eyedeekay Date: Mon, 31 Mar 2025 17:33:01 -0400 Subject: [PATCH] Implement Sender for Handshake Message 1 --- .../curve25519/curve25519_private_key.go | 16 ++-- lib/transport/ntcp/handshake.go | 32 ++++++++ lib/transport/ntcp/outgoing_handshake.go | 78 ------------------- lib/transport/ntcp/session.go | 73 +++++++++++++++++ 4 files changed, 113 insertions(+), 86 deletions(-) diff --git a/lib/crypto/curve25519/curve25519_private_key.go b/lib/crypto/curve25519/curve25519_private_key.go index 6e7ad74..31ffa2a 100644 --- a/lib/crypto/curve25519/curve25519_private_key.go +++ b/lib/crypto/curve25519/curve25519_private_key.go @@ -10,20 +10,20 @@ import ( type Curve25519PrivateKey []byte // Bytes implements types.PrivateKey. -func (k *Curve25519PrivateKey) Bytes() []byte { - return []byte(*k) // Return the byte slice representation of the private key +func (k Curve25519PrivateKey) Bytes() []byte { + return k // Return the byte slice representation of the private key } // Public implements types.PrivateKey. -func (k *Curve25519PrivateKey) Public() (types.SigningPublicKey, error) { +func (k Curve25519PrivateKey) Public() (types.SigningPublicKey, error) { // Create a proper x25519.PrivateKey from the byte slice - if len(*k) != x25519.PrivateKeySize { + if len(k) != x25519.PrivateKeySize { // Handle invalid private key length return nil, ErrInvalidPrivateKey } // Create a proper x25519.PrivateKey from the byte slice privKey := make(x25519.PrivateKey, x25519.PrivateKeySize) - copy(privKey, *k) + copy(privKey, k) // Derive the public key from the private key pubKey := privKey.Public() // This will return the corresponding public key x25519PubKey := pubKey.(curve25519.PublicKey) @@ -32,10 +32,10 @@ func (k *Curve25519PrivateKey) Public() (types.SigningPublicKey, error) { } // Zero implements types.PrivateKey. -func (k *Curve25519PrivateKey) Zero() { +func (k Curve25519PrivateKey) Zero() { // replace the slice with zeroes - for i := range *k { - (*k)[i] = 0 + for i := range k { + (k)[i] = 0 } } diff --git a/lib/transport/ntcp/handshake.go b/lib/transport/ntcp/handshake.go index 9a9f6b4..67ea090 100644 --- a/lib/transport/ntcp/handshake.go +++ b/lib/transport/ntcp/handshake.go @@ -70,6 +70,37 @@ func (c *NTCP2Session) sendSessionRequest(conn net.Conn, hs *HandshakeState) err // Implement according to NTCP2 spec // 1. Create and send X (ephemeral key) | Padding // uses CreateSessionRequest from session_request.go + sessionRequestMessage, err := c.CreateSessionRequest() + if err != nil { + return oops.Errorf("failed to create session request: %v", err) + } + // 2. Set deadline for the connection + if err := conn.SetDeadline(time.Now().Add(NTCP2_HANDSHAKE_TIMEOUT)); err != nil { + return oops.Errorf("failed to set deadline: %v", err) + } + // 3. Obfuscate the session request message + obfuscatedX, err := c.ObfuscateEphemeral(sessionRequestMessage.XContent[:]) + if err != nil { + return oops.Errorf("failed to obfuscate ephemeral key: %v", err) + } + // 4. ChaChaPoly Frame + // Encrypt options block and authenticate both options and padding + ciphertext, err := c.encryptSessionRequestOptions(sessionRequestMessage, obfuscatedX) + if err != nil { + return err + } + + // Combine all components into final message + // 1. Obfuscated X (already in obfuscatedX) + // 2. ChaCha20-Poly1305 encrypted options with auth tag + // 3. Authenticated but unencrypted padding + message := append(obfuscatedX, ciphertext...) + message = append(message, sessionRequestMessage.Padding...) + + // 5. Write the message to the connection + if _, err := conn.Write(message); err != nil { + return oops.Errorf("failed to send session request: %v", err) + } return nil } @@ -77,6 +108,7 @@ func (c *NTCP2Session) sendSessionRequest(conn net.Conn, hs *HandshakeState) err func (c *NTCP2Session) receiveSessionRequest(conn net.Conn, hs *HandshakeState) error { // Implement according to NTCP2 spec // TODO: Implement Message 1 processing + return nil } diff --git a/lib/transport/ntcp/outgoing_handshake.go b/lib/transport/ntcp/outgoing_handshake.go index 3277735..0901187 100644 --- a/lib/transport/ntcp/outgoing_handshake.go +++ b/lib/transport/ntcp/outgoing_handshake.go @@ -1,90 +1,12 @@ package ntcp import ( - "bytes" - "crypto/rand" "net" "time" - "github.com/flynn/noise" "github.com/samber/oops" ) -// Modify ComposeInitiatorHandshakeMessage in outgoing_handshake.go -// At the moment, remoteStatic is stored in the NTCP2Session() and doesn't need to be passed as an argument. -// You actually get it directly out of the remote RouterInfo, which the NoiseSession also has access to. -// So maybe, the interface should change so that we: -// - A: get the localStatic out of the parent NTCP2Transport's routerInfo, which is the "local" routerInfo -// - B: get the remoteStatic out of the NTCP2Session router, which is the "remote" routerInfo -func (c *NTCP2Session) ComposeInitiatorHandshakeMessage( - localStatic noise.DHKey, - remoteStatic []byte, - payload []byte, - ephemeralPrivate []byte, -) ( - negotiationData, - handshakeMessage []byte, - handshakeState *noise.HandshakeState, - err error, -) { - // Create session request with obfuscated ephemeral key - request, err := c.CreateSessionRequest() - if err != nil { - return nil, nil, nil, err - } - - // Initialize negotiation data with NTCP2 protocol specifics - negotiationData = initNegotiationData(nil) - - // Buffer for the complete message - buf := new(bytes.Buffer) - - obfuscatedKey, err := c.ObfuscateEphemeral(request.XContent[:]) - if err != nil { - return nil, nil, nil, err - } - if wrote, err := buf.Write(obfuscatedKey); err != nil { - return nil, nil, nil, err - } else { - log.Debugf("Wrote %d bytes of obfuscated key", wrote) - } - - // Write options block - if wrote, err := buf.Write(request.Options.Data()); err != nil { - return nil, nil, nil, err - } else { - log.Debugf("Wrote %d bytes of obfuscated key", wrote) - } - - // Initialize Noise - config := noise.Config{ - CipherSuite: noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashSHA256), - Pattern: noise.HandshakeXK, - Initiator: true, - StaticKeypair: localStatic, - PeerStatic: remoteStatic, // Add the peer's static key - Random: rand.Reader, - } - - handshakeState, err = noise.NewHandshakeState(config) - if err != nil { - return nil, nil, nil, err - } - - // Create Noise message - this contains the encrypted payload (options block) - // WriteMessage encrypts the payload and returns the message - handshakeMessage, _, _, err = handshakeState.WriteMessage(nil, buf.Bytes()) - if err != nil { - return nil, nil, nil, err - } - - // Add padding - handshakeMessage = append(handshakeMessage, request.Padding...) - - // Return the complete handshake message - return negotiationData, handshakeMessage, handshakeState, nil -} - // PerformOutboundHandshake initiates and completes a handshake as the initiator func (c *NTCP2Session) PerformOutboundHandshake(conn net.Conn, hs *HandshakeState) error { // Set deadline for the entire handshake process diff --git a/lib/transport/ntcp/session.go b/lib/transport/ntcp/session.go index cde8dd0..137a387 100644 --- a/lib/transport/ntcp/session.go +++ b/lib/transport/ntcp/session.go @@ -1,8 +1,15 @@ package ntcp import ( + "crypto" + "crypto/hmac" + + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/curve25519" + "github.com/go-i2p/go-i2p/lib/common/router_info" "github.com/go-i2p/go-i2p/lib/crypto/aes" + "github.com/go-i2p/go-i2p/lib/transport/messages" "github.com/go-i2p/go-i2p/lib/transport/noise" "github.com/go-i2p/go-i2p/lib/transport/obfs" "github.com/go-i2p/go-i2p/lib/transport/padding" @@ -124,3 +131,69 @@ func (s *NTCP2Session) buildAesStaticKey() (*aes.AESSymmetricKey, error) { AESStaticKey.IV = staticIV[:] return &AESStaticKey, nil } + +func (s *NTCP2Session) deriveChacha20Key(ephemeralKey []byte) ([]byte, error) { + remoteStaticKey, err := s.peerStaticKey() + if err != nil { + return nil, err + } + // Perform DH between Alice's ephemeral key and Bob's static key + // This is the "es" operation in Noise XK + sharedSecret, err := s.computeSharedSecret(ephemeralKey, remoteStaticKey[:]) + if err != nil { + return nil, err + } + + // Apply KDF to derive the key + // This typically involves HKDF with appropriate info string + hashProtocol := crypto.SHA256 + h := hmac.New(hashProtocol.New, []byte("NTCP2-KDF1")) + h.Write(sharedSecret) + return h.Sum(nil)[:32], nil // ChaCha20 requires a 32-byte key +} + +func (c *NTCP2Session) computeSharedSecret(ephemeralKey, param []byte) ([]byte, error) { + if len(ephemeralKey) != 32 || len(param) != 32 { + return nil, oops.Errorf("invalid key length, expected 32 bytes") + } + + // Convert byte slices to X25519 keys + var ephKey, staticKey [32]byte + copy(ephKey[:], ephemeralKey) + copy(staticKey[:], param) + // Compute the shared secret using X25519 + var sharedSecret [32]byte + shared, err := curve25519.X25519(ephKey[:], staticKey[:]) + if err != nil { + return nil, err + } + copy(sharedSecret[:], shared) + + return sharedSecret[:], nil +} + +func (c *NTCP2Session) encryptSessionRequestOptions(sessionRequestMessage *messages.SessionRequest, obfuscatedX []byte) ([]byte, error) { + chacha20Key, err := c.deriveChacha20Key(sessionRequestMessage.XContent[:]) + if err != nil { + return nil, oops.Errorf("failed to derive ChaCha20 key: %v", err) + } + + // Create AEAD cipher + aead, err := chacha20poly1305.New(chacha20Key) + if err != nil { + return nil, oops.Errorf("failed to create ChaCha20-Poly1305 cipher: %v", err) + } + + // Prepare the nonce (all zeros for first message) + nonce := make([]byte, chacha20poly1305.NonceSize) + + // Create associated data (AD) according to NTCP2 spec: + // AD = obfuscated X value (ensures binding between the AES and ChaCha layers) + ad := obfuscatedX + + // Encrypt options block and authenticate both options and padding + // ChaCha20-Poly1305 encrypts plaintext and appends auth tag + optionsData := sessionRequestMessage.Options.Data() + ciphertext := aead.Seal(nil, nonce, optionsData, ad) + return ciphertext, nil +}