Implement Sender for Handshake Message 1

This commit is contained in:
eyedeekay
2025-03-31 17:33:01 -04:00
parent 6158eb68f3
commit 1bd439f989
4 changed files with 113 additions and 86 deletions

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}