switch ecdsa to ed25519

This commit is contained in:
Dustin Stiles 2025-03-10 10:29:54 -04:00
parent 9eaf838069
commit ff81b1bc3e
Signed by: duwstiles
GPG Key ID: BCD9912EC231FC87
13 changed files with 147 additions and 231 deletions

View File

@ -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 servers master key.
File path to the Base64-encoded ED25519 private key that serves as the servers 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 <remote_addr>
dsfxctl -key /path/to/clientkey test <remote_addr>
```
Where:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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