diff --git a/README.md b/README.md index e05edad..129195c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ dsfx is a robust, secure, and distributed file exchange system written in Go. De ## Features -- **End-to-End Security:** Uses modern cryptographic primitives (ECDSA keys, for example) to ensure that all file exchanges are encrypted and authenticated. +- **End-to-End Security:** Uses modern cryptographic primitives (ED25519 keys, for example) to ensure that all file exchanges are encrypted and authenticated. - **Distributed Architecture:** Designed for secure file exchange across multiple nodes with built-in support for key-based authentication. - **High Performance:** Optimized for low latency and high throughput, with a focus on reliable and predictable behavior. - **Administrative and Test Tools:** The dsfx client can be used to test connectivity and perform preliminary administrative actions against the dsfx server. @@ -48,13 +48,13 @@ go install ./cmd/... ### Starting the Server -The dsfxnode requires a listening host, port, and an identity key (ECDSA private key in PEM format). For example: +The dsfxnode requires a listening host, port, and an identity key (ED25519 private key in Base64 format). For example: ```sh -dsfxnode -host localhost -port 8000 -key /path/to/serverkey.pem +dsfxnode -host localhost -port 8000 -key /path/to/serverkey ``` -> Note, if you need to generate a new ECDSA key, you can use the following command: `go run ./tool/genkey > path/to/masterkey.pem` +> Note, if you need to generate a new ED25519 key, you can use the following command: `go run ./tool/genkey > path/to/masterkey` Command-line flags for dsfx-server: @@ -65,18 +65,18 @@ The host interface on which the server will listen. The TCP port on which the server will accept connections. -key (required) -File path to the PEM-encoded ECDSA private key that serves as the server’s master key. +File path to the Base64-encoded ED25519 private key that serves as the server’s master key. Once started, the server will bind to the specified host and port and wait for incoming secure file exchange (or other test) connections. When a client connects, the initial payload (up to 1024 bytes) from the client is read and logged. ### Running the Admin Client -The dsfxctl uses a private key for the client (also an ECDSA key in PEM format) and currently supports only the “test” command for checking connectivity to the server. +The dsfxctl uses a private key for the client (also an ED25519 key in Base64 format) and currently supports only the “test” command for checking connectivity to the server. Client command usage: ```sh -dsfxctl -key /path/to/clientkey.pem test +dsfxctl -key /path/to/clientkey test ``` Where: diff --git a/cmd/dsfxctl/main.go b/cmd/dsfxctl/main.go index 7bff2a6..0cd4cfa 100644 --- a/cmd/dsfxctl/main.go +++ b/cmd/dsfxctl/main.go @@ -2,7 +2,7 @@ package main import ( "context" - "crypto/ecdsa" + "crypto/ed25519" "flag" "fmt" "log/slog" @@ -53,7 +53,7 @@ func main() { os.Exit(1) } - identity, err := identity.LoadSigningKeyFromFile(*flagKey) + id, err := identity.LoadSigningKeyFromFile(*flagKey) if err != nil { logger.ErrorContext(ctx, "failed to load private key", slog.Any("error", err)) os.Exit(1) @@ -62,7 +62,7 @@ func main() { laddr := network.NewAddr( net.ParseIP("0.0.0.0"), 0, // port 0 means any available port - &identity.PublicKey, + identity.ToPublicKey(id), ) logger.DebugContext(ctx, "using addr", slog.String("address", laddr.String())) @@ -74,7 +74,7 @@ func main() { logger.ErrorContext(ctx, "no remote address provided") os.Exit(1) } - testConnection(ctx, identity, laddr, raddrRaw) + testConnection(ctx, id, laddr, raddrRaw) case "": logger.InfoContext(ctx, "no command provided") os.Exit(1) @@ -84,7 +84,7 @@ func main() { } } -func testConnection(ctx context.Context, identity *ecdsa.PrivateKey, laddr *network.Addr, raddrRaw string) { +func testConnection(ctx context.Context, id ed25519.PrivateKey, laddr *network.Addr, raddrRaw string) { logger := logging.FromContext(context.Background()) raddr, err := network.ParseAddr(raddrRaw) @@ -93,7 +93,7 @@ func testConnection(ctx context.Context, identity *ecdsa.PrivateKey, laddr *netw return } - conn, err := network.Dial(ctx, identity, laddr, raddr) + conn, err := network.Dial(ctx, id, laddr, raddr) if err != nil { logger.ErrorContext(ctx, "failed to connect", slog.Any("error", err)) return diff --git a/cmd/dsfxnode/main.go b/cmd/dsfxnode/main.go index 8f7e97e..b7e88dc 100644 --- a/cmd/dsfxnode/main.go +++ b/cmd/dsfxnode/main.go @@ -60,7 +60,7 @@ func main() { slog.ErrorContext(ctx, "invalid host or port") os.Exit(1) } - addr := network.FromTCPAddr(tcpAddr, &masterKey.PublicKey) + addr := network.FromTCPAddr(tcpAddr, identity.ToPublicKey(masterKey)) // --------------------------------------------------------------------------- // Listener diff --git a/pkg/buffer/lenprefixed.go b/pkg/buffer/lenprefixed.go new file mode 100644 index 0000000..c4463c7 --- /dev/null +++ b/pkg/buffer/lenprefixed.go @@ -0,0 +1,57 @@ +package buffer + +import ( + "encoding/binary" + "errors" + "io" +) + +// MaxUint16 is the maximum value of a uint16. It is used to check if the +// length of the data is too large to be encoded in a uint16. +const MaxUint16 = 0xFFFF + +// ErrInvalidLength is the error message returned when the length of the data +// is too large to be encoded in a uint16. +var ErrInvalidLength = errors.New("data length is too large to be encoded in a uint16") + +// NewLenPrefixed returns a new buffer with a length prefix. The length prefix is +// 2 bytes long and is encoded in big-endian order. The length prefix is +// followed by the data. +func NewLenPrefixed(data []byte) ([]byte, error) { + length := len(data) + // Overflow Guard: If the length of the data is greater than the maximum + // value of a uint16, return an error. + if length > MaxUint16 { + return nil, ErrInvalidLength + } + buf := make([]byte, 2+len(data)) + binary.BigEndian.PutUint16(buf, uint16(len(data))) + copy(buf[2:], data) + return buf, nil +} + +func ReadLenPrefixed(maxSize uint16, r io.Reader) ([]byte, error) { + lenBuf := make([]byte, 2) + if _, err := io.ReadFull(r, lenBuf); err != nil { + return nil, err + } + if len(lenBuf) < 2 { + return nil, errors.New("buffer is too small to contain length prefix") + } + length := binary.BigEndian.Uint16(lenBuf) + + if length > maxSize { + return nil, errors.New("data length is too large to be encoded in a uint16") + } + + buf := make([]byte, length) + if _, err := io.ReadFull(r, buf); err != nil { + return nil, err + } + + if len(buf) < int(length) { + return nil, errors.New("buffer is too small to contain data") + } + + return buf, nil +} diff --git a/pkg/crypto/encryption/aead.go b/pkg/crypto/encryption/aead.go index d57eb00..18c15f5 100644 --- a/pkg/crypto/encryption/aead.go +++ b/pkg/crypto/encryption/aead.go @@ -12,6 +12,8 @@ import ( // Encrypt uses AES-GCM to encrypt the given plaintext with the given key. The // plaintext is sealed with a 12-byte nonce, which is prepended to the ciphertext. +// The nonce adds 28 bytes to the ciphertext, so the total length of the ciphertext +// is the length of the plaintext plus 28 bytes. func Encrypt(key, plaintext []byte) ([]byte, error) { switch len(key) { case 16, 24, 32: // AES-128, AES-192, AES-256 diff --git a/pkg/crypto/encryption/aead_test.go b/pkg/crypto/encryption/aead_test.go new file mode 100644 index 0000000..7e37026 --- /dev/null +++ b/pkg/crypto/encryption/aead_test.go @@ -0,0 +1,35 @@ +package encryption_test + +import ( + "crypto/rand" + "testing" + + "koti.casa/numenor-labs/dsfx/pkg/crypto/encryption" +) + +func TestEncryptDecrypt(t *testing.T) { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + t.Fatal(err) + return + } + + plaintext := []byte("Hello, Worlskljfsjflskfjlskjfjslkfjsfjslkfjsfd!") + ciphertext, err := encryption.Encrypt(key, plaintext) + if err != nil { + t.Fatal(err) + return + } + + decrypted, err := encryption.Decrypt(key, ciphertext) + if err != nil { + t.Fatal(err) + return + } + + if string(decrypted) != string(plaintext) { + t.Errorf("decrypted text does not match original plaintext") + return + } +} diff --git a/pkg/crypto/identity/ecdsa.go b/pkg/crypto/identity/ecdsa.go deleted file mode 100644 index 8b75b03..0000000 --- a/pkg/crypto/identity/ecdsa.go +++ /dev/null @@ -1,114 +0,0 @@ -package identity - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/x509" - "encoding/pem" - "fmt" - "os" -) - -var ( - // DefaultSigningCurve is the default elliptic curve used for signing. - DefaultSigningCurve = elliptic.P384 - - ExportedPublicKeySize = 215 -) - -func LoadSigningKeyFromFile(filePath string) (*ecdsa.PrivateKey, error) { - masterKeyFile, err := os.ReadFile(filePath) - if err != nil { - return nil, err - } - - // The second argument is not an error. - derEncoded, _ := pem.Decode(masterKeyFile) - if derEncoded == nil { - return nil, fmt.Errorf("failed to decode master key file") - } - - masterKey, err := x509.ParseECPrivateKey(derEncoded.Bytes) - if err != nil { - return nil, err - } - - return masterKey, nil -} - -// Generate generates a new ECDSA private key for signing. -func Generate() (*ecdsa.PrivateKey, error) { - return ecdsa.GenerateKey(DefaultSigningCurve(), rand.Reader) -} - -// Sign signs the data with the private key. -func Sign(priv *ecdsa.PrivateKey, data []byte) ([]byte, error) { - return ecdsa.SignASN1(rand.Reader, priv, data) -} - -// Verify verifies the signature of the data with the public key. -func Verify(pub *ecdsa.PublicKey, data, signature []byte) bool { - return ecdsa.VerifyASN1(pub, data, signature) -} - -// ExportPrivateKey exports the private key as a byte slice. -func ExportPrivateKey(key *ecdsa.PrivateKey) ([]byte, error) { - der, err := x509.MarshalECPrivateKey(key) - if err != nil { - return nil, err - } - - return pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", - Bytes: der, - }), nil -} - -// ExportPublicKey exports the public key as a byte slice. -func ExportPublicKey(key *ecdsa.PublicKey) ([]byte, error) { - der, err := x509.MarshalPKIXPublicKey(key) - if err != nil { - return nil, err - } - - return pem.EncodeToMemory(&pem.Block{ - Type: "PUBLIC KEY", - Bytes: der, - }), nil -} - -// ImportPrivateKey imports the private key from a byte slice. -func ImportPrivateKey(keyBytes []byte) (*ecdsa.PrivateKey, error) { - block, _ := pem.Decode(keyBytes) - if block == nil { - return nil, fmt.Errorf("failed to decode private key") - } - - privKey, err := x509.ParseECPrivateKey(block.Bytes) - if err != nil { - return nil, err - } - - return privKey, nil -} - -// ImportPublicKey imports the public key from a byte slice. -func ImportPublicKey(keyBytes []byte) (*ecdsa.PublicKey, error) { - block, _ := pem.Decode(keyBytes) - if block == nil { - return nil, fmt.Errorf("failed to decode public key") - } - - pubKeyAny, err := x509.ParsePKIXPublicKey(block.Bytes) - if err != nil { - return nil, err - } - - pubKey, ok := pubKeyAny.(*ecdsa.PublicKey) - if !ok { - return nil, fmt.Errorf("not an ECDSA public key") - } - - return pubKey, nil -} diff --git a/pkg/crypto/identity/ecdsa_test.go b/pkg/crypto/identity/ecdsa_test.go deleted file mode 100644 index 65ae29c..0000000 --- a/pkg/crypto/identity/ecdsa_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package identity_test - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "log" - "testing" - - "koti.casa/numenor-labs/dsfx/pkg/crypto/identity" -) - -func TestImportExportPrivate(t *testing.T) { - key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - t.Fatalf("failed to generate key: %v", err) - return - } - - exported, err := identity.ExportPrivateKey(key) - if err != nil { - t.Fatalf("failed to export key: %v", err) - return - } - - imported, err := identity.ImportPrivateKey(exported) - if err != nil { - t.Fatalf("failed to import key: %v", err) - return - } - - if !key.Equal(imported) { - t.Fatalf("imported key does not match original") - return - } -} - -func TestImportExportPublic(t *testing.T) { - key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - t.Fatalf("failed to generate key: %v", err) - return - } - - exported, err := identity.ExportPublicKey(&key.PublicKey) - if err != nil { - t.Fatalf("failed to export key: %v", err) - return - } - - log.Println("keylen", len(exported)) - imported, err := identity.ImportPublicKey(exported) - if err != nil { - t.Fatalf("failed to import key: %v", err) - return - } - - if !key.PublicKey.Equal(imported) { - t.Fatalf("imported key does not match original") - return - } -} diff --git a/pkg/crypto/identity/ed25519.go b/pkg/crypto/identity/ed25519.go new file mode 100644 index 0000000..5bdc6cd --- /dev/null +++ b/pkg/crypto/identity/ed25519.go @@ -0,0 +1,91 @@ +package identity + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "fmt" + "log" + "os" +) + +var ( + ExportedPublicKeySize = 32 +) + +func LoadSigningKeyFromFile(filePath string) (ed25519.PrivateKey, error) { + keyBase64, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + // Decode the base64-encoded master key file. + keyRaw, err := base64.StdEncoding.DecodeString(string(keyBase64)) + if err != nil { + return nil, fmt.Errorf("failed to decode master key file: %w", err) + } + + if len(keyRaw) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("invalid master key file size: %d", len(keyRaw)) + } + + return ed25519.PrivateKey(keyRaw), nil +} + +// Generate generates a new ED25519 private key for signing. +func Generate() (ed25519.PrivateKey, error) { + _, key, err := ed25519.GenerateKey(rand.Reader) + return key, err +} + +// Sign signs the data with the private key. +func Sign(priv ed25519.PrivateKey, data []byte) ([]byte, error) { + return ed25519.Sign(priv, data), nil +} + +// Verify verifies the signature of the data with the public key. +func Verify(pub ed25519.PublicKey, data, signature []byte) bool { + return ed25519.Verify(pub, data, signature) +} + +// ExportPrivateKey exports the private key as a byte slice. +func ExportPrivateKey(key ed25519.PrivateKey) ([]byte, error) { + return []byte(base64.StdEncoding.EncodeToString(key)), nil +} + +// ExportPublicKey exports the public key as a byte slice. +func ExportPublicKey(key ed25519.PublicKey) ([]byte, error) { + log.Println("exporting key", len(key)) + encoded := []byte(base64.StdEncoding.EncodeToString(key)) + log.Println("exported key", len(encoded)) + return encoded, nil +} + +// ImportPrivateKey imports the private key from a byte slice. +func ImportPrivateKey(keyBytes []byte) (ed25519.PrivateKey, error) { + rawKey, err := base64.StdEncoding.DecodeString(string(keyBytes)) + if err != nil { + return nil, fmt.Errorf("failed to decode private key: %w", err) + } + + if len(rawKey) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("invalid private key size: %d", len(rawKey)) + } + + return ed25519.PrivateKey(rawKey), nil +} + +// ImportPublicKey imports the public key from a byte slice. +func ImportPublicKey(keyBytes []byte) (ed25519.PublicKey, error) { + log.Println("importing key", len(keyBytes)) + decoded, err := base64.StdEncoding.DecodeString(string(keyBytes)) + if err != nil { + return nil, fmt.Errorf("failed to decode public key: %w", err) + } + log.Println("imported key", len(decoded)) + return ed25519.PublicKey(decoded), nil +} + +func ToPublicKey(key ed25519.PrivateKey) ed25519.PublicKey { + return key.Public().(ed25519.PublicKey) +} diff --git a/pkg/crypto/keyexchange/ecdh.go b/pkg/crypto/keyexchange/ecdh.go index 34375c8..742101c 100644 --- a/pkg/crypto/keyexchange/ecdh.go +++ b/pkg/crypto/keyexchange/ecdh.go @@ -9,7 +9,7 @@ import ( var ( // DefaultDHCurve is the default elliptic curve used for signing. - DefaultDHCurve = ecdh.P384 + DefaultDHCurve = ecdh.X25519 ) // GenerateDHKey generates a new ECDH private key for key exchange. diff --git a/pkg/handshake/handshake.go b/pkg/handshake/handshake.go index 9080bd5..28930ca 100644 --- a/pkg/handshake/handshake.go +++ b/pkg/handshake/handshake.go @@ -4,29 +4,24 @@ import ( "bytes" "context" "crypto/ecdh" - "crypto/ecdsa" + "crypto/ed25519" "crypto/sha256" "errors" "io" + "log" "log/slog" "koti.casa/numenor-labs/dsfx/pkg/assert" + "koti.casa/numenor-labs/dsfx/pkg/buffer" "koti.casa/numenor-labs/dsfx/pkg/crypto/encryption" "koti.casa/numenor-labs/dsfx/pkg/crypto/identity" "koti.casa/numenor-labs/dsfx/pkg/crypto/keyexchange" - "koti.casa/numenor-labs/dsfx/pkg/frame" "koti.casa/numenor-labs/dsfx/pkg/logging" ) const ( - // ECDHPublicKeySize is the size of an ECDH public key in bytes. - ECDHPublicKeySize = 97 - // ECDSAPublicKeySize is the size of an ECDSA public key in bytes. - ECDSAPublicKeySize = 222 - // BoxedClientAuthMessageSize is the size of a boxed client authentication message in bytes. - BoxedClientAuthMessageSize = 353 - // BoxedServerAuthMessageSize is the size of a boxed server authentication message in bytes. - BoxedServerAuthMessageSize = 130 + DHKeySize = 32 + IdentityKeySize = 32 ) // Initiate initiates the handshake process between the given actor @@ -34,8 +29,8 @@ const ( func Initiate( ctx context.Context, conn io.ReadWriteCloser, - lPrivKey *ecdsa.PrivateKey, - rPubKey *ecdsa.PublicKey, + lPrivKey ed25519.PrivateKey, + rPubKey ed25519.PublicKey, ) ([]byte, error) { logger := logging.FromContext(ctx).WithGroup("handshake") @@ -56,32 +51,35 @@ func Initiate( if err != nil { return nil, err } - assert.Assert(len(ourDHKeyRaw) == ECDHPublicKeySize, "invalid dh key size") + + welcomeMessage, err := buffer.NewLenPrefixed(ourDHKeyRaw) + if err != nil { + return nil, err + } // Write the actor's public key to the connection. logger.DebugContext(ctx, "sending dh key", slog.Int("key.size", len(ourDHKeyRaw))) - _, err = frame.New(ourDHKeyRaw).WriteTo(conn) + n, err := conn.Write(welcomeMessage) if err != nil { return nil, err } + if n != len(welcomeMessage) { + return nil, errors.New("failed to write dh key") + } // ------------------------------------------------------------------------ // Step 2: Ephemeral Key Exchange From Server // Read the remote actor's public key from the connection. logger.DebugContext(ctx, "waiting for server's dh key") - remoteDHKeyFrame := frame.New(nil) - _, err = remoteDHKeyFrame.ReadFrom(conn) + remoteDHKeyRaw, err := buffer.ReadLenPrefixed(buffer.MaxUint16, conn) if err != nil { return nil, err } - if len(remoteDHKeyFrame.Contents()) != ECDHPublicKeySize { - return nil, errors.New("invalid dh key size") - } // Import the remote actor's public key. logger.DebugContext(ctx, "importing server's dh key") - remoteDHKey, err := keyexchange.ImportPublicKey(remoteDHKeyFrame.Contents()) + remoteDHKey, err := keyexchange.ImportPublicKey(remoteDHKeyRaw) if err != nil { return nil, err } @@ -91,7 +89,7 @@ func Initiate( // Export the public key of the actor's signing key. logger.DebugContext(ctx, "exporting public signing key") - ourPublicKeyRaw, err := identity.ExportPublicKey(&lPrivKey.PublicKey) + ourPublicKeyRaw, err := identity.ExportPublicKey(identity.ToPublicKey(lPrivKey)) if err != nil { return nil, err } @@ -130,6 +128,9 @@ func Initiate( } assert.Assert(len(derivedKey) == 32, "invalid shared secret size") + log.Println("raw pub key", len(ourPublicKeyRaw)) + log.Println("raw sig", len(signature)) + plaintext := make([]byte, 0, len(ourPublicKeyRaw)+len(signature)) plaintext = append(plaintext, ourPublicKeyRaw...) plaintext = append(plaintext, signature...) @@ -141,20 +142,28 @@ func Initiate( return nil, err } + log.Println(string(plaintext), len(plaintext)) + // Write the boxed message to the connection. logger.DebugContext(ctx, "sending authentication message", slog.Int("message.size", len(boxedMsg))) - _, err = frame.New(boxedMsg).WriteTo(conn) + boxedMsgPrepared, err := buffer.NewLenPrefixed(boxedMsg) if err != nil { return nil, err } + n, err = conn.Write(boxedMsgPrepared) + if err != nil { + return nil, err + } + if n != len(boxedMsgPrepared) { + return nil, errors.New("failed to write authentication message") + } // ------------------------------------------------------------------------ // Step 4: Server Authentication // Read the authentication message from the connection. logger.DebugContext(ctx, "waiting for server's authentication message") - authMessageFrame := frame.New(nil) - n, err := authMessageFrame.ReadFrom(conn) + authMessageBoxed, err := buffer.ReadLenPrefixed(buffer.MaxUint16, conn) if err != nil { return nil, err } @@ -162,7 +171,7 @@ func Initiate( // Decrypt the authentication message with the derived key. logger.DebugContext(ctx, "decrypting authentication message") - plaintext, err = encryption.Decrypt(derivedKey, authMessageFrame.Contents()) + plaintext, err = encryption.Decrypt(derivedKey, authMessageBoxed) if err != nil { return nil, err } @@ -182,11 +191,17 @@ func Initiate( // Finally, we need to let the server know that the handshake is complete. logger.DebugContext(ctx, "sending handshake complete message") - handshakeCompleteMsg := []byte{0x01} - _, err = frame.New(handshakeCompleteMsg).WriteTo(conn) + handshakeCompleteMsg, err := buffer.NewLenPrefixed([]byte{0x01}) if err != nil { return nil, err } + n, err = conn.Write(handshakeCompleteMsg) + if err != nil { + return nil, err + } + if n != len(handshakeCompleteMsg) { + return nil, errors.New("failed to write handshake complete message") + } logger.DebugContext(ctx, "handshake complete") return derivedKey, nil @@ -194,7 +209,7 @@ func Initiate( // Accept accepts a handshake from the given actor and connection. It // returns the shared secret between the actor and the remote actor. -func Accept(ctx context.Context, conn io.ReadWriteCloser, lPrivKey *ecdsa.PrivateKey) (*ecdsa.PublicKey, []byte, error) { +func Accept(ctx context.Context, conn io.ReadWriteCloser, lPrivKey ed25519.PrivateKey) (ed25519.PublicKey, []byte, error) { logger := logging.FromContext(ctx).WithGroup("handshake") // ------------------------------------------------------------------------ @@ -202,15 +217,14 @@ func Accept(ctx context.Context, conn io.ReadWriteCloser, lPrivKey *ecdsa.Privat // Read the remote actor's public key from the connection. logger.DebugContext(ctx, "waiting for client's dh key") - remoteDHKeyFrame := frame.New(nil) - _, err := remoteDHKeyFrame.ReadFrom(conn) + remoteDHKeyRaw, err := buffer.ReadLenPrefixed(buffer.MaxUint16, conn) if err != nil { return nil, nil, err } // Import the remote actor's public key. logger.DebugContext(ctx, "importing client's dh key") - remoteDHKey, err := keyexchange.ImportPublicKey(remoteDHKeyFrame.Contents()) + remoteDHKey, err := keyexchange.ImportPublicKey(remoteDHKeyRaw) if err != nil { return nil, nil, err } @@ -234,22 +248,32 @@ func Accept(ctx context.Context, conn io.ReadWriteCloser, lPrivKey *ecdsa.Privat // Write the actor's public key to the connection. logger.DebugContext(ctx, "sending dh key", slog.Int("key.size", len(ourDHKeyRaw))) - _, err = frame.New(ourDHKeyRaw).WriteTo(conn) + ourPrefixedDHKey, err := buffer.NewLenPrefixed(ourDHKeyRaw) if err != nil { return nil, nil, err } + n, err := conn.Write(ourPrefixedDHKey) + if err != nil { + return nil, nil, err + } + if n != len(ourPrefixedDHKey) { + return nil, nil, errors.New("failed to write dh key") + } // ------------------------------------------------------------------------ // Step 3: Server Authentication // Read the authentication message from the connection. logger.DebugContext(ctx, "waiting for client's authentication message") - authMessageFrame := frame.New(nil) - n, err := authMessageFrame.ReadFrom(conn) + authMessageRaw, err := buffer.ReadLenPrefixed(buffer.MaxUint16, conn) if err != nil { return nil, nil, err } - logger.DebugContext(ctx, "received authentication message", slog.Int("message.size", int(n))) + logger.DebugContext( + ctx, + "received authentication message", + slog.Int("message.size", len(authMessageRaw)), + ) // Decrypt the authentication message with the derived key. logger.DebugContext(ctx, "computing shared secret") @@ -259,13 +283,14 @@ func Accept(ctx context.Context, conn io.ReadWriteCloser, lPrivKey *ecdsa.Privat } logger.DebugContext(ctx, "decrypting authentication message") - plaintext, err := encryption.Decrypt(derivedKey, authMessageFrame.Contents()) + plaintext, err := encryption.Decrypt(derivedKey, authMessageRaw) if err != nil { return nil, nil, err } + log.Println(string(plaintext), len(plaintext)) - clientPublicKeyRaw := plaintext[:identity.ExportedPublicKeySize] - signature := plaintext[identity.ExportedPublicKeySize:] + clientPublicKeyRaw := plaintext[:44] + signature := plaintext[44:] // Verify the client's public key and signature. logger.DebugContext(ctx, "importing client's public signing key") @@ -309,19 +334,25 @@ func Accept(ctx context.Context, conn io.ReadWriteCloser, lPrivKey *ecdsa.Privat // Send the server's signature back to the client. logger.DebugContext(ctx, "sending authentication message", slog.Int("message.size", len(boxedMsg))) - _, err = frame.New(boxedMsg).WriteTo(conn) + prefixedAuthMessage, err := buffer.NewLenPrefixed(boxedMsg) if err != nil { return nil, nil, err } + n, err = conn.Write(prefixedAuthMessage) + if err != nil { + return nil, nil, err + } + if n != len(prefixedAuthMessage) { + return nil, nil, errors.New("failed to write authentication message") + } logger.DebugContext(ctx, "waiting for handshake complete message") // Read the handshake complete message from the client. - handshakeCompleteFrame := frame.New(nil) - _, err = handshakeCompleteFrame.ReadFrom(conn) + handshakeCompleteMsg, err := buffer.ReadLenPrefixed(buffer.MaxUint16, conn) if err != nil { return nil, nil, err } - if !bytes.Equal(handshakeCompleteFrame.Contents(), []byte{0x01}) { + if !bytes.Equal(handshakeCompleteMsg, []byte{0x01}) { return nil, nil, errors.New("invalid handshake complete message") } diff --git a/pkg/handshake/handshake_test.go b/pkg/handshake/handshake_test.go index 7bd4348..cb4d422 100644 --- a/pkg/handshake/handshake_test.go +++ b/pkg/handshake/handshake_test.go @@ -3,7 +3,7 @@ package handshake_test import ( "bytes" "context" - "crypto/ecdsa" + "crypto/ed25519" "fmt" "net" "os" @@ -17,9 +17,9 @@ import ( func TestHandshake(t *testing.T) { ctx := context.Background() - // alice, represented by an ecdsa key pair. + // alice, represented by an ed25519 key pair. alice, _ := identity.Generate() - // bob, also represented by an ecdsa key pair. + // bob, also represented by an ed25519 key pair. bob, _ := identity.Generate() var ( @@ -28,7 +28,7 @@ func TestHandshake(t *testing.T) { // any errors produce by alice aliceErr error // alice's public key as discovered by bob - discoveredAlicePublicKey *ecdsa.PublicKey + discoveredAlicePublicKey ed25519.PublicKey // the secret that bob should arrive at on his own bobSecret []byte @@ -47,7 +47,7 @@ func TestHandshake(t *testing.T) { var wg sync.WaitGroup wg.Add(2) go func() { - aliceSecret, aliceErr = handshake.Initiate(ctx, client, alice, &bob.PublicKey) + aliceSecret, aliceErr = handshake.Initiate(ctx, client, alice, identity.ToPublicKey(bob)) wg.Done() }() go func() { @@ -76,7 +76,7 @@ func TestHandshake(t *testing.T) { return } // Bob should have discovered alice's public key. - if !alice.PublicKey.Equal(discoveredAlicePublicKey) { + if !identity.ToPublicKey(alice).Equal(discoveredAlicePublicKey) { t.Errorf("handshake failed: discovered public key is not equal to alice's public key") return } @@ -93,9 +93,9 @@ func BenchmarkHandshake(b *testing.B) { func runSimulation() error { ctx := context.Background() - // alice, represented by an ecdsa key pair. + // alice, represented by an ed25519 key pair. alice, _ := identity.Generate() - // bob, also represented by an ecdsa key pair. + // bob, also represented by an ed25519 key pair. bob, _ := identity.Generate() var ( @@ -123,7 +123,7 @@ func runSimulation() error { var wg sync.WaitGroup wg.Add(2) go func() { - _, aliceErr = handshake.Initiate(ctx, client, alice, &bob.PublicKey) + _, aliceErr = handshake.Initiate(ctx, client, alice, identity.ToPublicKey(bob)) wg.Done() }() go func() { diff --git a/pkg/network/addr.go b/pkg/network/addr.go index 970ed82..bdecce0 100644 --- a/pkg/network/addr.go +++ b/pkg/network/addr.go @@ -1,7 +1,7 @@ package network import ( - "crypto/ecdsa" + "crypto/ed25519" "encoding/base64" "errors" "fmt" @@ -22,11 +22,11 @@ type Addr struct { network string ip net.IP port int - publicKey *ecdsa.PublicKey + publicKey ed25519.PublicKey } // NewAddr creates a new Addr. -func NewAddr(ip net.IP, port int, publicKey *ecdsa.PublicKey) *Addr { +func NewAddr(ip net.IP, port int, publicKey ed25519.PublicKey) *Addr { network := "dsfx" return &Addr{network, ip, port, publicKey} } @@ -70,7 +70,7 @@ func ParseAddr(addrRaw string) (*Addr, error) { } // FromTCPAddr creates a new Addr from a net.TCPAddr. -func FromTCPAddr(addr *net.TCPAddr, publicKey *ecdsa.PublicKey) *Addr { +func FromTCPAddr(addr *net.TCPAddr, publicKey ed25519.PublicKey) *Addr { return &Addr{ network: "dsfx", ip: addr.IP, @@ -102,7 +102,7 @@ func (a *Addr) Port() int { } // PublicKey returns the public key of the Addr. -func (a *Addr) PublicKey() *ecdsa.PublicKey { +func (a *Addr) PublicKey() ed25519.PublicKey { return a.publicKey } diff --git a/pkg/network/conn.go b/pkg/network/conn.go index 8b7d986..8b12595 100644 --- a/pkg/network/conn.go +++ b/pkg/network/conn.go @@ -1,7 +1,7 @@ package network import ( - "crypto/ecdsa" + "crypto/ed25519" "net" "time" @@ -14,12 +14,12 @@ import ( type Conn struct { conn *net.TCPConn sessionKey []byte - localIdentity *ecdsa.PublicKey - remoteIdentity *ecdsa.PublicKey + localIdentity ed25519.PublicKey + remoteIdentity ed25519.PublicKey } // NewConn creates a new Conn. -func NewConn(conn *net.TCPConn, sessionKey []byte, localIdentity, remoteIdentity *ecdsa.PublicKey) *Conn { +func NewConn(conn *net.TCPConn, sessionKey []byte, localIdentity, remoteIdentity ed25519.PublicKey) *Conn { return &Conn{conn, sessionKey, localIdentity, remoteIdentity} } diff --git a/pkg/network/listener.go b/pkg/network/listener.go index 0ebd344..95beb4b 100644 --- a/pkg/network/listener.go +++ b/pkg/network/listener.go @@ -2,10 +2,11 @@ package network import ( "context" - "crypto/ecdsa" + "crypto/ed25519" "log/slog" "net" + "koti.casa/numenor-labs/dsfx/pkg/crypto/identity" "koti.casa/numenor-labs/dsfx/pkg/handshake" ) @@ -13,7 +14,7 @@ import ( type Listener struct { logger *slog.Logger tcpListener *net.TCPListener - identity *ecdsa.PrivateKey + identity ed25519.PrivateKey } // Accept implements net.Listener. @@ -30,7 +31,7 @@ func (l *Listener) Accept() (net.Conn, error) { return nil, err } - return NewConn(conn, sessionKey, &l.identity.PublicKey, clientIdentity), nil + return NewConn(conn, sessionKey, identity.ToPublicKey(l.identity), clientIdentity), nil } // Close implements net.Listener. @@ -42,5 +43,5 @@ func (l *Listener) Close() error { func (l *Listener) Addr() net.Addr { laddr := l.tcpListener.Addr().(*net.TCPAddr) - return NewAddr(laddr.IP, laddr.Port, &l.identity.PublicKey) + return NewAddr(laddr.IP, laddr.Port, identity.ToPublicKey(l.identity)) } diff --git a/pkg/network/network.go b/pkg/network/network.go index 309ab4b..c1c4bfe 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -2,7 +2,7 @@ package network import ( "context" - "crypto/ecdsa" + "crypto/ed25519" "net" "koti.casa/numenor-labs/dsfx/pkg/handshake" @@ -12,7 +12,7 @@ import ( // Dial ... func Dial( ctx context.Context, - identity *ecdsa.PrivateKey, + identity ed25519.PrivateKey, laddr *Addr, raddr *Addr, ) (*Conn, error) { @@ -32,7 +32,7 @@ func Dial( // Listen ... func Listen( ctx context.Context, - identity *ecdsa.PrivateKey, + identity ed25519.PrivateKey, laddr *Addr, ) (net.Listener, error) { tcpListener, err := net.ListenTCP("tcp", laddr.TCPAddr()) diff --git a/tool/genkey/main.go b/tool/genkey/main.go index 676cad9..6c91bd8 100644 --- a/tool/genkey/main.go +++ b/tool/genkey/main.go @@ -1,8 +1,6 @@ package main import ( - "crypto/x509" - "encoding/pem" "fmt" "os" @@ -15,17 +13,10 @@ func main() { panic(err) } - // Encode the private key to der - der, err := x509.MarshalECPrivateKey(key) + exported, err := identity.ExportPrivateKey(key) if err != nil { panic(err) } - // Encode the private key to PEM - pem := pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", - Bytes: der, - }) - - fmt.Fprint(os.Stdout, string(pem)) + fmt.Fprint(os.Stdout, string(exported)) }