From ed91172d4f3f75fbda4c62eb851412d038d302c5 Mon Sep 17 00:00:00 2001 From: eyedeekay Date: Mon, 24 Nov 2025 11:02:32 -0500 Subject: [PATCH] Migrate to go-i2p/crypto package for cryptographic operations --- chacha20poly1305.go | 86 ++++++++++++++++---------- crypto.go | 80 ++++++++++++++++++------ dsa.go | 141 ++++++++++++++++++++++++++++++++++++++++++ ed25519.go | 108 +++++++++++++++++++++++++------- go.mod | 12 +++- go.sum | 21 +++++++ signature_key_pair.go | 3 + x25519.go | 140 +++++++++++++++++++++++++++++------------ 8 files changed, 477 insertions(+), 114 deletions(-) create mode 100644 dsa.go diff --git a/chacha20poly1305.go b/chacha20poly1305.go index e3d7300..890b02e 100644 --- a/chacha20poly1305.go +++ b/chacha20poly1305.go @@ -1,26 +1,37 @@ +// Migration Notes (November 24, 2025): +// Migrated to use github.com/go-i2p/crypto/chacha20poly1305 package which provides +// standardized ChaCha20-Poly1305 AEAD (Authenticated Encryption with Associated Data). +// +// Key Changes: +// - ChaCha20Poly1305Cipher now wraps crypto/chacha20poly1305.AEAD +// - NewChaCha20Poly1305Cipher() delegates to chacha20poly1305.GenerateKey() and NewAEAD() +// - Encrypt/Decrypt maintain [nonce][ciphertext+tag] format for backward compatibility +// - Crypto package separates tag from ciphertext; wrapper combines them for I2CP compatibility +// +// The crypto package uses explicit tag separation (AEAD standard), while I2CP traditionally +// combines nonce+ciphertext+tag. The wrapper maintains I2CP format compatibility. + package go_i2cp import ( - "crypto/cipher" - "crypto/rand" "fmt" - "golang.org/x/crypto/chacha20poly1305" + cryptoaead "github.com/go-i2p/crypto/chacha20poly1305" ) // ChaCha20Poly1305Cipher provides authenticated encryption using ChaCha20-Poly1305 +// Wraps github.com/go-i2p/crypto/chacha20poly1305.AEAD type ChaCha20Poly1305Cipher struct { algorithmType uint32 - aead cipher.AEAD + aead *cryptoaead.AEAD key [32]byte } // NewChaCha20Poly1305Cipher creates a new ChaCha20-Poly1305 cipher with a random key +// Delegates to github.com/go-i2p/crypto/chacha20poly1305.GenerateKey() and NewAEAD() func NewChaCha20Poly1305Cipher() (*ChaCha20Poly1305Cipher, error) { - var key [32]byte - - // Generate random 256-bit key - _, err := rand.Read(key[:]) + // Generate random 256-bit key using crypto package + key, err := cryptoaead.GenerateKey() if err != nil { return nil, fmt.Errorf("failed to generate ChaCha20-Poly1305 key: %w", err) } @@ -29,8 +40,9 @@ func NewChaCha20Poly1305Cipher() (*ChaCha20Poly1305Cipher, error) { } // NewChaCha20Poly1305CipherWithKey creates a new ChaCha20-Poly1305 cipher with the provided key +// Delegates to github.com/go-i2p/crypto/chacha20poly1305.NewAEAD() func NewChaCha20Poly1305CipherWithKey(key [32]byte) (*ChaCha20Poly1305Cipher, error) { - aead, err := chacha20poly1305.New(key[:]) + aead, err := cryptoaead.NewAEAD(key) if err != nil { return nil, fmt.Errorf("failed to create ChaCha20-Poly1305 AEAD: %w", err) } @@ -66,25 +78,31 @@ func ChaCha20Poly1305CipherFromStream(stream *Stream) (*ChaCha20Poly1305Cipher, } // Encrypt encrypts plaintext with optional associated data using ChaCha20-Poly1305 +// Returns [nonce][ciphertext+tag] format for I2CP compatibility +// Uses github.com/go-i2p/crypto/chacha20poly1305.AEAD.Encrypt() func (c *ChaCha20Poly1305Cipher) Encrypt(plaintext, additionalData []byte) ([]byte, error) { if c.aead == nil { return nil, fmt.Errorf("cipher not initialized") } - // Generate random nonce - nonce := make([]byte, c.aead.NonceSize()) - _, err := rand.Read(nonce) + // Generate random nonce using crypto package + nonce, err := cryptoaead.GenerateNonce() if err != nil { return nil, fmt.Errorf("failed to generate nonce: %w", err) } - // Encrypt plaintext - ciphertext := c.aead.Seal(nil, nonce, plaintext, additionalData) + // Encrypt plaintext - crypto package returns separate ciphertext and tag + ciphertext, tag, err := c.aead.Encrypt(plaintext, additionalData, nonce[:]) + if err != nil { + return nil, fmt.Errorf("failed to encrypt: %w", err) + } - // Prepend nonce to ciphertext for transmission - result := make([]byte, len(nonce)+len(ciphertext)) - copy(result, nonce) + // Combine nonce, ciphertext, and tag for I2CP compatibility + // Format: [nonce][ciphertext][tag] + result := make([]byte, len(nonce)+len(ciphertext)+len(tag)) + copy(result, nonce[:]) copy(result[len(nonce):], ciphertext) + copy(result[len(nonce)+len(ciphertext):], tag[:]) return result, nil } @@ -110,22 +128,32 @@ func (c *ChaCha20Poly1305Cipher) EncryptStream(src, dst *Stream, additionalData } // Decrypt decrypts ciphertext with optional associated data using ChaCha20-Poly1305 +// Expects [nonce][ciphertext+tag] format for I2CP compatibility +// Uses github.com/go-i2p/crypto/chacha20poly1305.AEAD.Decrypt() func (c *ChaCha20Poly1305Cipher) Decrypt(ciphertext, additionalData []byte) ([]byte, error) { if c.aead == nil { return nil, fmt.Errorf("cipher not initialized") } - nonceSize := c.aead.NonceSize() - if len(ciphertext) < nonceSize { - return nil, fmt.Errorf("ciphertext too short: need at least %d bytes for nonce", nonceSize) + nonceSize := cryptoaead.NonceSize + tagSize := cryptoaead.TagSize + minSize := nonceSize + tagSize + + if len(ciphertext) < minSize { + return nil, fmt.Errorf("ciphertext too short: need at least %d bytes (nonce + tag)", minSize) } - // Extract nonce and actual ciphertext + // Extract nonce, ciphertext, and tag from combined format nonce := ciphertext[:nonceSize] - actualCiphertext := ciphertext[nonceSize:] + actualCiphertextWithTag := ciphertext[nonceSize:] - // Decrypt ciphertext - plaintext, err := c.aead.Open(nil, nonce, actualCiphertext, additionalData) + // Split ciphertext and tag + actualCiphertext := actualCiphertextWithTag[:len(actualCiphertextWithTag)-tagSize] + var tag [cryptoaead.TagSize]byte + copy(tag[:], actualCiphertextWithTag[len(actualCiphertext):]) + + // Decrypt using crypto package - pass tag separately + plaintext, err := c.aead.Decrypt(actualCiphertext, tag[:], additionalData, nonce) if err != nil { return nil, fmt.Errorf("failed to decrypt: %w", err) } @@ -186,16 +214,10 @@ func (c *ChaCha20Poly1305Cipher) AlgorithmType() uint32 { // NonceSize returns the nonce size used by the cipher func (c *ChaCha20Poly1305Cipher) NonceSize() int { - if c.aead == nil { - return chacha20poly1305.NonceSize // Return standard size even if not initialized - } - return c.aead.NonceSize() + return cryptoaead.NonceSize } // Overhead returns the authentication tag overhead func (c *ChaCha20Poly1305Cipher) Overhead() int { - if c.aead == nil { - return chacha20poly1305.Overhead // Return standard overhead even if not initialized - } - return c.aead.Overhead() + return cryptoaead.TagSize } diff --git a/crypto.go b/crypto.go index 342fb2b..75415dc 100644 --- a/crypto.go +++ b/crypto.go @@ -28,6 +28,17 @@ func NewCrypto() *Crypto { // Sign a stream using the specified algorithm func (c *Crypto) SignStream(sgk *SignatureKeyPair, stream *Stream) (err error) { + // Use new DSA wrapper if available + if sgk.dsaKeyPair != nil { + signature, err := sgk.dsaKeyPair.Sign(stream.Bytes()) + if err != nil { + return fmt.Errorf("failed to sign stream with DSA: %w", err) + } + stream.Write(signature) + return nil + } + + // Fallback to legacy implementation for backward compatibility var r, s *big.Int out := NewStream(make([]byte, 40)) c.sh1.Reset() @@ -76,12 +87,21 @@ func (c *Crypto) VerifyStream(sgk *SignatureKeyPair, stream *Stream) (verified b Fatal(tAG|FATAL, "Stream length < 40 bytes (signature length)") return false, fmt.Errorf("stream too short for signature verification") } - var r, s big.Int + message := stream.Bytes()[:stream.Len()-40] - digest := stream.Bytes()[stream.Len()-40:] + signature := stream.Bytes()[stream.Len()-40:] + + // Use new DSA wrapper if available + if sgk.dsaKeyPair != nil { + verified = sgk.dsaKeyPair.Verify(message, signature) + return verified, nil + } + + // Fallback to legacy implementation for backward compatibility + var r, s big.Int // TODO not sure about this part... - r.SetBytes(digest[:20]) - s.SetBytes(digest[20:]) + r.SetBytes(signature[:20]) + s.SetBytes(signature[20:]) verified = dsa.Verify(&sgk.pub, message, &r, &s) return } @@ -190,24 +210,43 @@ func (c *Crypto) PublicKeyFromStream(keyType uint32, stream *Stream) (key *big.I func (c *Crypto) SignatureKeygen(algorithmTyp uint32) (sgk SignatureKeyPair, err error) { switch algorithmTyp { case DSA_SHA1: - var pkey dsa.PrivateKey - pkey.G = c.params.G - pkey.Q = c.params.Q - pkey.P = c.params.P - err = dsa.GenerateKey(&pkey, c.rng) - sgk.priv = pkey - sgk.pub.G = pkey.G - sgk.pub.P = pkey.P - sgk.pub.Q = pkey.Q - sgk.pub.Y = pkey.Y + // Use new DSA wrapper from crypto package + dsaKp, err := c.DSASignatureKeygen() + if err != nil { + return sgk, fmt.Errorf("failed to generate DSA key pair: %w", err) + } + + // Convert new DSAKeyPair to legacy SignatureKeyPair for backward compatibility + // This allows existing code to continue working while we migrate to the new types sgk.algorithmType = DSA_SHA1 + sgk.dsaKeyPair = dsaKp + + // Also populate legacy fields for backward compatibility + // Extract raw bytes and reconstruct old big.Int format + privKeyBytes := dsaKp.PrivateKey() + pubKeyBytes := dsaKp.PublicKey() + + // Initialize DSA parameters from crypto struct + sgk.priv.G = c.params.G + sgk.priv.Q = c.params.Q + sgk.priv.P = c.params.P + sgk.pub.G = c.params.G + sgk.pub.Q = c.params.Q + sgk.pub.P = c.params.P + + // Set private key X from bytes + sgk.priv.X = new(big.Int) + sgk.priv.X.SetBytes(privKeyBytes) + + // Set public key Y from bytes + sgk.pub.Y = new(big.Int) + sgk.pub.Y.SetBytes(pubKeyBytes) + sgk.priv.Y = sgk.pub.Y // Private key also stores public key Y default: err = fmt.Errorf("unsupported signature algorithm type: %d", algorithmTyp) } return -} - -// Ed25519SignatureKeygen generates a new Ed25519 signature key pair +} // Ed25519SignatureKeygen generates a new Ed25519 signature key pair func (c *Crypto) Ed25519SignatureKeygen() (*Ed25519KeyPair, error) { return NewEd25519KeyPair() } @@ -222,7 +261,12 @@ func (c *Crypto) ChaCha20Poly1305CipherKeygen() (*ChaCha20Poly1305Cipher, error) return NewChaCha20Poly1305Cipher() } -// Random32 generates a cryptographically secure random uint32 value +// DSASignatureKeygen generates a new DSA signature key pair +func (c *Crypto) DSASignatureKeygen() (*DSAKeyPair, error) { + return NewDSAKeyPair() +} + +// Random32 generates a cryptographically secure random uint32. // Used for I2CP message nonces and request IDs per protocol specification func (c *Crypto) Random32() uint32 { var bytes [4]byte diff --git a/dsa.go b/dsa.go new file mode 100644 index 0000000..b49e2af --- /dev/null +++ b/dsa.go @@ -0,0 +1,141 @@ +package go_i2cp + +import ( + "fmt" + + cryptodsa "github.com/go-i2p/crypto/dsa" +) + +// DSAKeyPair represents a DSA signature key pair using the crypto package +type DSAKeyPair struct { + privKey cryptodsa.DSAPrivateKey + pubKey cryptodsa.DSAPublicKey +} + +// NewDSAKeyPair generates a new DSA key pair using the crypto package +func NewDSAKeyPair() (*DSAKeyPair, error) { + var privKey cryptodsa.DSAPrivateKey + generatedKey, err := privKey.Generate() + if err != nil { + return nil, fmt.Errorf("failed to generate DSA key pair: %w", err) + } + + privKey = generatedKey.(cryptodsa.DSAPrivateKey) + + pubKeyInterface, err := privKey.Public() + if err != nil { + return nil, fmt.Errorf("failed to derive public key: %w", err) + } + + pubKey := pubKeyInterface.(cryptodsa.DSAPublicKey) + + return &DSAKeyPair{ + privKey: privKey, + pubKey: pubKey, + }, nil +} + +// AlgorithmType returns DSA_SHA1 algorithm type +func (kp *DSAKeyPair) AlgorithmType() uint32 { + return DSA_SHA1 +} + +// Sign creates a DSA signature for the given data +func (kp *DSAKeyPair) Sign(data []byte) ([]byte, error) { + signer, err := kp.privKey.NewSigner() + if err != nil { + return nil, fmt.Errorf("failed to create signer: %w", err) + } + + signature, err := signer.Sign(data) + if err != nil { + return nil, fmt.Errorf("signing failed: %w", err) + } + + return signature, nil +} + +// Verify verifies a DSA signature against the given data +func (kp *DSAKeyPair) Verify(data, signature []byte) bool { + verifier, err := kp.pubKey.NewVerifier() + if err != nil { + return false + } + + err = verifier.Verify(data, signature) + if err != nil { + return false + } + + return true +} + +// PrivateKey returns the private key bytes +func (kp *DSAKeyPair) PrivateKey() []byte { + return kp.privKey.Bytes() +} + +// PublicKey returns the public key bytes +func (kp *DSAKeyPair) PublicKey() []byte { + return kp.pubKey.Bytes() +} + +// WriteToStream writes the DSA key pair to a stream in I2CP format +func (kp *DSAKeyPair) WriteToStream(stream *Stream) error { + // Write algorithm type + err := stream.WriteUint32(DSA_SHA1) + if err != nil { + return fmt.Errorf("failed to write algorithm type: %w", err) + } + + // Write private key (20 bytes) + privKeyBytes := kp.privKey.Bytes() + if len(privKeyBytes) != 20 { + return fmt.Errorf("invalid DSA private key length: %d (expected 20)", len(privKeyBytes)) + } + _, err = stream.Write(privKeyBytes) + if err != nil { + return fmt.Errorf("failed to write private key: %w", err) + } + + // Write public key (128 bytes) + pubKeyBytes := kp.pubKey.Bytes() + if len(pubKeyBytes) != 128 { + return fmt.Errorf("invalid DSA public key length: %d (expected 128)", len(pubKeyBytes)) + } + _, err = stream.Write(pubKeyBytes) + if err != nil { + return fmt.Errorf("failed to write public key: %w", err) + } + + return nil +} + +// DSAKeyPairFromStream reads a DSA key pair from a stream +func DSAKeyPairFromStream(stream *Stream) (*DSAKeyPair, error) { + // Read private key (20 bytes) + privKeyBytes := make([]byte, 20) + _, err := stream.Read(privKeyBytes) + if err != nil { + return nil, fmt.Errorf("failed to read private key: %w", err) + } + + // Read public key (128 bytes) + pubKeyBytes := make([]byte, 128) + _, err = stream.Read(pubKeyBytes) + if err != nil { + return nil, fmt.Errorf("failed to read public key: %w", err) + } + + // Convert to crypto package types + var privKey cryptodsa.DSAPrivateKey + copy(privKey[:], privKeyBytes) + + var pubKey cryptodsa.DSAPublicKey + copy(pubKey[:], pubKeyBytes) + + return &DSAKeyPair{ + privKey: privKey, + pubKey: pubKey, + }, nil +} diff --git a/ed25519.go b/ed25519.go index 5a5fb9a..f76e391 100644 --- a/ed25519.go +++ b/ed25519.go @@ -2,33 +2,60 @@ package go_i2cp import ( "crypto/ed25519" - "crypto/rand" "crypto/sha256" "fmt" + + cryptoed25519 "github.com/go-i2p/crypto/ed25519" ) -// Ed25519KeyPair represents an Ed25519 signature key pair for modern I2P cryptography +// Ed25519 Implementation Migration Notes: +// +// This file has been migrated to use github.com/go-i2p/crypto/ed25519 (v0.0.5) +// for all cryptographic operations, following Phase 2.1 of the migration plan. +// +// Migration completed: November 24, 2025 +// +// Key changes: +// - Ed25519KeyPair now wraps crypto package types (Ed25519PrivateKey, Ed25519PublicKey) +// - Sign() delegates to crypto.Signer interface (NewSigner().SignHash()) +// - Verify() delegates to crypto.Verifier interface (NewVerifier().VerifyHash()) +// - NewEd25519KeyPair() uses crypto.GenerateEd25519KeyPair() +// - Maintains I2CP-specific SHA-256 pre-hashing for protocol compatibility +// - Backward compatible API - all existing tests pass without modification +// +// The crypto package provides: +// - Interface-based signing/verification (types.Signer, types.Verifier) +// - Secure key generation and management +// - Memory zeroing for private keys (privKey.Zero()) +// - Consistent cryptographic operations across go-i2p ecosystem + +// Ed25519KeyPair represents an Ed25519 signature key pair for modern I2P cryptography. +// This type wraps github.com/go-i2p/crypto/ed25519 keys to provide I2CP-specific +// functionality while delegating cryptographic operations to the specialized crypto package. type Ed25519KeyPair struct { algorithmType uint32 - privateKey ed25519.PrivateKey - publicKey ed25519.PublicKey + privateKey cryptoed25519.Ed25519PrivateKey + publicKey cryptoed25519.Ed25519PublicKey } -// NewEd25519KeyPair generates a new Ed25519 key pair +// NewEd25519KeyPair generates a new Ed25519 key pair using github.com/go-i2p/crypto. +// This function delegates to the crypto package for secure key generation. func NewEd25519KeyPair() (*Ed25519KeyPair, error) { - publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + publicKey, privateKey, err := cryptoed25519.GenerateEd25519KeyPair() if err != nil { return nil, fmt.Errorf("failed to generate Ed25519 key pair: %w", err) } + // Dereference pointers - crypto package keys are slices return &Ed25519KeyPair{ algorithmType: ED25519_SHA256, - privateKey: privateKey, - publicKey: publicKey, + privateKey: *privateKey, + publicKey: *publicKey, }, nil } -// Ed25519KeyPairFromStream reads an Ed25519 key pair from a stream +// Ed25519KeyPairFromStream reads an Ed25519 key pair from a stream. +// Uses github.com/go-i2p/crypto/ed25519 for key reconstruction. func Ed25519KeyPairFromStream(stream *Stream) (*Ed25519KeyPair, error) { var algorithmType uint32 var err error @@ -56,14 +83,27 @@ func Ed25519KeyPairFromStream(stream *Stream) (*Ed25519KeyPair, error) { return nil, fmt.Errorf("failed to read public key: %w", err) } + // Construct crypto package keys from raw bytes + privKey, err := cryptoed25519.CreateEd25519PrivateKeyFromBytes(privateKeyBytes) + if err != nil { + return nil, fmt.Errorf("failed to create private key: %w", err) + } + + pubKey, err := cryptoed25519.CreateEd25519PublicKeyFromBytes(publicKeyBytes) + if err != nil { + return nil, fmt.Errorf("failed to create public key: %w", err) + } + + // CreateFromBytes returns values, not pointers return &Ed25519KeyPair{ algorithmType: algorithmType, - privateKey: ed25519.PrivateKey(privateKeyBytes), - publicKey: ed25519.PublicKey(publicKeyBytes), + privateKey: privKey, + publicKey: pubKey, }, nil } -// Ed25519PublicKeyFromStream reads only the public key from a stream +// Ed25519PublicKeyFromStream reads only the public key from a stream. +// Returns stdlib ed25519.PublicKey for backward compatibility. func Ed25519PublicKeyFromStream(stream *Stream) (ed25519.PublicKey, error) { publicKeyBytes := make([]byte, ed25519.PublicKeySize) _, err := stream.Read(publicKeyBytes) @@ -73,18 +113,30 @@ func Ed25519PublicKeyFromStream(stream *Stream) (ed25519.PublicKey, error) { return ed25519.PublicKey(publicKeyBytes), nil } -// Sign creates a signature for the given message using Ed25519 +// Sign creates a signature for the given message using Ed25519. +// Uses github.com/go-i2p/crypto/ed25519 Signer interface for cryptographic operations. func (kp *Ed25519KeyPair) Sign(message []byte) ([]byte, error) { if kp.privateKey == nil { return nil, fmt.Errorf("private key is nil") } - // Ed25519 signs the message directly, but for I2CP compatibility we hash it first + // Create signer from private key + signer, err := kp.privateKey.NewSigner() + if err != nil { + return nil, fmt.Errorf("failed to create signer: %w", err) + } + + // For I2CP compatibility, hash the message first using SHA-256 hasher := sha256.New() hasher.Write(message) messageHash := hasher.Sum(nil) - signature := ed25519.Sign(kp.privateKey, messageHash) + // Sign the hash using the crypto package's interface-based signing + signature, err := signer.SignHash(messageHash) + if err != nil { + return nil, fmt.Errorf("failed to sign message: %w", err) + } + return signature, nil } @@ -107,18 +159,27 @@ func (kp *Ed25519KeyPair) SignStream(stream *Stream) error { return nil } -// Verify verifies a signature against the given message using Ed25519 +// Verify verifies a signature against the given message using Ed25519. +// Uses github.com/go-i2p/crypto/ed25519 Verifier interface for cryptographic operations. func (kp *Ed25519KeyPair) Verify(message, signature []byte) bool { if kp.publicKey == nil { return false } + // Create verifier from public key + verifier, err := kp.publicKey.NewVerifier() + if err != nil { + return false + } + // Hash the message for I2CP compatibility hasher := sha256.New() hasher.Write(message) messageHash := hasher.Sum(nil) - return ed25519.Verify(kp.publicKey, messageHash, signature) + // Verify using the crypto package's interface-based verification + err = verifier.VerifyHash(messageHash, signature) + return err == nil } // VerifyStream verifies a signature embedded in the stream data @@ -176,13 +237,14 @@ func (kp *Ed25519KeyPair) WriteToStream(stream *Stream) error { return nil } -// WritePublicKeyToStream writes only the public key to a stream +// WritePublicKeyToStream writes only the public key to a stream. +// Uses crypto package's Bytes() method for key serialization. func (kp *Ed25519KeyPair) WritePublicKeyToStream(stream *Stream) error { if stream == nil { return fmt.Errorf("stream cannot be nil") } - _, err := stream.Write(kp.publicKey) + _, err := stream.Write(kp.publicKey.Bytes()) if err != nil { return fmt.Errorf("failed to write Ed25519 public key: %w", err) } @@ -190,14 +252,14 @@ func (kp *Ed25519KeyPair) WritePublicKeyToStream(stream *Stream) error { return nil } -// PublicKey returns the public key +// PublicKey returns the public key as stdlib ed25519.PublicKey for backward compatibility. func (kp *Ed25519KeyPair) PublicKey() ed25519.PublicKey { - return kp.publicKey + return ed25519.PublicKey(kp.publicKey.Bytes()) } -// PrivateKey returns the private key +// PrivateKey returns the private key as stdlib ed25519.PrivateKey for backward compatibility. func (kp *Ed25519KeyPair) PrivateKey() ed25519.PrivateKey { - return kp.privateKey + return ed25519.PrivateKey(kp.privateKey.Bytes()) } // AlgorithmType returns the algorithm type constant diff --git a/go.mod b/go.mod index be4c332..71a3992 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,20 @@ module github.com/go-i2p/go-i2cp go 1.24.4 require ( + github.com/go-i2p/crypto v0.0.5 github.com/go-i2p/logger v0.0.1 + go.step.sm/crypto v0.67.0 golang.org/x/crypto v0.45.0 ) require ( - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/samber/lo v1.51.0 // indirect + github.com/samber/oops v1.19.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/stretchr/testify v1.10.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect ) diff --git a/go.sum b/go.sum index 704b0d1..28550ec 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,44 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-i2p/crypto v0.0.5 h1:i6EiQ4rKnNDDcACFfCkizgfMTwxFcMRj187w/VLiD9o= +github.com/go-i2p/crypto v0.0.5/go.mod h1:uerqW6jReQkeNgXXMM3n55wZbKbnvuZrbsDaNL9oLjo= github.com/go-i2p/logger v0.0.1 h1:OFDZMjqiNXbPIm+SDxiwYtI6ocC3mb9V/t5kvZ+6XQ0= github.com/go-i2p/logger v0.0.1/go.mod h1:te7Zj3g3oMeIl8uBXAgO62UKmZ6m6kHRNg1Mm+X8Hzk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= +github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/oops v1.19.0 h1:sfZAwC8MmTXBRRyNc4Z1utuTPBx+hFKF5fJ9DEQRZfw= +github.com/samber/oops v1.19.0/go.mod h1:+f+61dbiMxEMQ8gw/zTxW2pk+YGobaDM4glEHQtPOww= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.step.sm/crypto v0.67.0 h1:1km9LmxMKG/p+mKa1R4luPN04vlJYnRLlLQrWv7egGU= +go.step.sm/crypto v0.67.0/go.mod h1:+AoDpB0mZxbW/PmOXuwkPSpXRgaUaoIK+/Wx/HGgtAU= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/signature_key_pair.go b/signature_key_pair.go index 505f7a7..4f5df79 100644 --- a/signature_key_pair.go +++ b/signature_key_pair.go @@ -7,8 +7,11 @@ import ( ) // SignatureKeyPair represents a DSA signature key pair +// This is a legacy struct maintained for backward compatibility. +// New code should use DSAKeyPair directly from dsa.go type SignatureKeyPair struct { algorithmType uint32 pub dsa.PublicKey priv dsa.PrivateKey + dsaKeyPair *DSAKeyPair // New wrapper from crypto package } diff --git a/x25519.go b/x25519.go index 6dfddb5..6f850cf 100644 --- a/x25519.go +++ b/x25519.go @@ -1,40 +1,52 @@ +// Migration Notes (November 24, 2025): +// Migrated to use github.com/go-i2p/crypto/curve25519 package which provides +// standardized X25519 key generation and ECDH operations via go.step.sm/crypto/x25519. +// +// Key Changes: +// - X25519KeyPair now wraps curve25519.Curve25519PrivateKey and Curve25519PublicKey (slice types) +// - NewX25519KeyPair() delegates to curve25519.GenerateX25519KeyPair() +// - GenerateSharedSecret() uses PrivateKey.SharedKey() for ECDH operations +// - Maintains [32]byte return types for backward compatibility +// +// The crypto package uses []byte (slice) types for keys, while I2CP uses +// [32]byte arrays. The wrapper maintains I2CP compatibility. + package go_i2cp import ( - "crypto/rand" "fmt" - "golang.org/x/crypto/curve25519" + cryptox25519 "github.com/go-i2p/crypto/curve25519" + "go.step.sm/crypto/x25519" ) // X25519KeyPair represents an X25519 key exchange key pair for modern I2P ECDH +// Wraps github.com/go-i2p/crypto/curve25519 types type X25519KeyPair struct { algorithmType uint32 - privateKey [32]byte - publicKey [32]byte + privateKey cryptox25519.Curve25519PrivateKey + publicKey cryptox25519.Curve25519PublicKey } // NewX25519KeyPair generates a new X25519 key pair for ECDH operations +// Delegates to github.com/go-i2p/crypto/curve25519.GenerateX25519KeyPair() func NewX25519KeyPair() (*X25519KeyPair, error) { - var privateKey, publicKey [32]byte - - // Generate random private key - _, err := rand.Read(privateKey[:]) + // Generate key pair using crypto package + pubKey, privKey, err := cryptox25519.GenerateX25519KeyPair() if err != nil { - return nil, fmt.Errorf("failed to generate X25519 private key: %w", err) + return nil, fmt.Errorf("failed to generate X25519 key pair: %w", err) } - // Derive public key from private key - curve25519.ScalarBaseMult(&publicKey, &privateKey) - + // Dereference pointers to get slice values return &X25519KeyPair{ algorithmType: X25519, - privateKey: privateKey, - publicKey: publicKey, + privateKey: *privKey, + publicKey: *pubKey, }, nil } // X25519KeyPairFromStream reads an X25519 key pair from a stream +// Uses go.step.sm/crypto/x25519 for key reconstruction and validation func X25519KeyPairFromStream(stream *Stream) (*X25519KeyPair, error) { var algorithmType uint32 var err error @@ -48,22 +60,53 @@ func X25519KeyPairFromStream(stream *Stream) (*X25519KeyPair, error) { return nil, fmt.Errorf("unsupported algorithm type: %d", algorithmType) } - var kp X25519KeyPair - kp.algorithmType = algorithmType - // Read private key (32 bytes) - _, err = stream.Read(kp.privateKey[:]) + privKeyBytes := make([]byte, 32) + _, err = stream.Read(privKeyBytes) if err != nil { return nil, fmt.Errorf("failed to read X25519 private key: %w", err) } // Read public key (32 bytes) - _, err = stream.Read(kp.publicKey[:]) + pubKeyBytes := make([]byte, 32) + _, err = stream.Read(pubKeyBytes) if err != nil { return nil, fmt.Errorf("failed to read X25519 public key: %w", err) } - return &kp, nil + // Validate that the public key corresponds to the private key + privKey := make(x25519.PrivateKey, 32) + copy(privKey, privKeyBytes) + + expectedPubKeyInterface := privKey.Public() + expectedPubKey, ok := expectedPubKeyInterface.(x25519.PublicKey) + if !ok { + return nil, fmt.Errorf("failed to convert public key to x25519.PublicKey") + } + + // Compare public keys + if !bytesEqual(expectedPubKey, pubKeyBytes) { + return nil, fmt.Errorf("public key does not match private key") + } + + return &X25519KeyPair{ + algorithmType: algorithmType, + privateKey: cryptox25519.Curve25519PrivateKey(privKeyBytes), + publicKey: cryptox25519.Curve25519PublicKey(pubKeyBytes), + }, nil +} + +// bytesEqual compares two byte slices for equality +func bytesEqual(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true } // X25519PublicKeyFromStream reads only the public key from a stream @@ -77,19 +120,36 @@ func X25519PublicKeyFromStream(stream *Stream) ([32]byte, error) { } // GenerateSharedSecret performs ECDH to generate a shared secret with the peer's public key +// Uses go.step.sm/crypto/x25519 PrivateKey.SharedKey() for ECDH func (kp *X25519KeyPair) GenerateSharedSecret(peerPublicKey [32]byte) ([32]byte, error) { - var sharedSecret [32]byte + var result [32]byte - // Perform scalar multiplication: shared_secret = our_private * peer_public - curve25519.ScalarMult(&sharedSecret, &kp.privateKey, &peerPublicKey) - - // Check for weak shared secrets (all zeros) - var zero [32]byte - if sharedSecret == zero { - return sharedSecret, fmt.Errorf("generated weak shared secret (all zeros)") + // Validate the peer's public key first + if !ValidateX25519PublicKey(peerPublicKey) { + return result, fmt.Errorf("invalid peer public key") } - return sharedSecret, nil + // Convert slice to x25519 types for SharedKey operation + privKey := make(x25519.PrivateKey, 32) + copy(privKey, kp.privateKey) + + pubKey := make(x25519.PublicKey, 32) + copy(pubKey, peerPublicKey[:]) + + // Perform X25519 key exchange (ECDH) + sharedSecret, err := privKey.SharedKey(pubKey) + if err != nil { + return result, fmt.Errorf("X25519 key exchange failed: %w", err) + } + + // Additional security check: reject all-zero shared secrets + var zero [32]byte + copy(result[:], sharedSecret) + if result == zero { + return result, fmt.Errorf("generated weak shared secret (all zeros)") + } + + return result, nil } // WriteToStream writes the complete X25519 key pair to a stream @@ -104,14 +164,14 @@ func (kp *X25519KeyPair) WriteToStream(stream *Stream) error { return fmt.Errorf("failed to write algorithm type: %w", err) } - // Write private key - _, err = stream.Write(kp.privateKey[:]) + // Write private key (convert slice to bytes) + _, err = stream.Write(kp.privateKey) if err != nil { return fmt.Errorf("failed to write X25519 private key: %w", err) } - // Write public key - _, err = stream.Write(kp.publicKey[:]) + // Write public key (convert slice to bytes) + _, err = stream.Write(kp.publicKey) if err != nil { return fmt.Errorf("failed to write X25519 public key: %w", err) } @@ -125,7 +185,7 @@ func (kp *X25519KeyPair) WritePublicKeyToStream(stream *Stream) error { return fmt.Errorf("stream cannot be nil") } - _, err := stream.Write(kp.publicKey[:]) + _, err := stream.Write(kp.publicKey) if err != nil { return fmt.Errorf("failed to write X25519 public key: %w", err) } @@ -133,14 +193,18 @@ func (kp *X25519KeyPair) WritePublicKeyToStream(stream *Stream) error { return nil } -// PublicKey returns a copy of the public key +// PublicKey returns a copy of the public key as [32]byte for backward compatibility func (kp *X25519KeyPair) PublicKey() [32]byte { - return kp.publicKey + var result [32]byte + copy(result[:], kp.publicKey) + return result } -// PrivateKey returns a copy of the private key +// PrivateKey returns a copy of the private key as [32]byte for backward compatibility func (kp *X25519KeyPair) PrivateKey() [32]byte { - return kp.privateKey + var result [32]byte + copy(result[:], kp.privateKey) + return result } // AlgorithmType returns the algorithm type constant