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/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 c3c5648..0000000 --- a/pkg/crypto/identity/ecdsa_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package identity_test - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "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 - } - - 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/handshake/handshake.go b/pkg/handshake/handshake.go index 74c86d2..28930ca 100644 --- a/pkg/handshake/handshake.go +++ b/pkg/handshake/handshake.go @@ -4,10 +4,11 @@ import ( "bytes" "context" "crypto/ecdh" - "crypto/ecdsa" + "crypto/ed25519" "crypto/sha256" "errors" "io" + "log" "log/slog" "koti.casa/numenor-labs/dsfx/pkg/assert" @@ -28,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") @@ -88,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 } @@ -127,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...) @@ -138,6 +142,8 @@ 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))) boxedMsgPrepared, err := buffer.NewLenPrefixed(boxedMsg) @@ -203,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") // ------------------------------------------------------------------------ @@ -281,9 +287,10 @@ func Accept(ctx context.Context, conn io.ReadWriteCloser, lPrivKey *ecdsa.Privat 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") 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)) }