switch ecdsa->ed25519

Reviewed-on: #1
This commit is contained in:
duwstiles 2025-03-10 14:35:44 +00:00
commit b78eb2b71a
17 changed files with 301 additions and 269 deletions

View File

@ -6,7 +6,7 @@ dsfx is a robust, secure, and distributed file exchange system written in Go. De
## Features ## 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. - **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. - **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. - **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 ### 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 ```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: 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. The TCP port on which the server will accept connections.
-key (required) -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. 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 ### 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: Client command usage:
```sh ```sh
dsfxctl -key /path/to/clientkey.pem test <remote_addr> dsfxctl -key /path/to/clientkey test <remote_addr>
``` ```
Where: Where:

View File

@ -2,7 +2,7 @@ package main
import ( import (
"context" "context"
"crypto/ecdsa" "crypto/ed25519"
"flag" "flag"
"fmt" "fmt"
"log/slog" "log/slog"
@ -53,7 +53,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
identity, err := identity.LoadSigningKeyFromFile(*flagKey) id, err := identity.LoadSigningKeyFromFile(*flagKey)
if err != nil { if err != nil {
logger.ErrorContext(ctx, "failed to load private key", slog.Any("error", err)) logger.ErrorContext(ctx, "failed to load private key", slog.Any("error", err))
os.Exit(1) os.Exit(1)
@ -62,7 +62,7 @@ func main() {
laddr := network.NewAddr( laddr := network.NewAddr(
net.ParseIP("0.0.0.0"), net.ParseIP("0.0.0.0"),
0, // port 0 means any available port 0, // port 0 means any available port
&identity.PublicKey, identity.ToPublicKey(id),
) )
logger.DebugContext(ctx, "using addr", slog.String("address", laddr.String())) logger.DebugContext(ctx, "using addr", slog.String("address", laddr.String()))
@ -74,7 +74,7 @@ func main() {
logger.ErrorContext(ctx, "no remote address provided") logger.ErrorContext(ctx, "no remote address provided")
os.Exit(1) os.Exit(1)
} }
testConnection(ctx, identity, laddr, raddrRaw) testConnection(ctx, id, laddr, raddrRaw)
case "": case "":
logger.InfoContext(ctx, "no command provided") logger.InfoContext(ctx, "no command provided")
os.Exit(1) 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()) logger := logging.FromContext(context.Background())
raddr, err := network.ParseAddr(raddrRaw) raddr, err := network.ParseAddr(raddrRaw)
@ -93,7 +93,7 @@ func testConnection(ctx context.Context, identity *ecdsa.PrivateKey, laddr *netw
return return
} }
conn, err := network.Dial(ctx, identity, laddr, raddr) conn, err := network.Dial(ctx, id, laddr, raddr)
if err != nil { if err != nil {
logger.ErrorContext(ctx, "failed to connect", slog.Any("error", err)) logger.ErrorContext(ctx, "failed to connect", slog.Any("error", err))
return return

View File

@ -60,7 +60,7 @@ func main() {
slog.ErrorContext(ctx, "invalid host or port") slog.ErrorContext(ctx, "invalid host or port")
os.Exit(1) os.Exit(1)
} }
addr := network.FromTCPAddr(tcpAddr, &masterKey.PublicKey) addr := network.FromTCPAddr(tcpAddr, identity.ToPublicKey(masterKey))
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Listener // Listener

57
pkg/buffer/lenprefixed.go Normal file
View File

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

View File

@ -12,6 +12,8 @@ import (
// Encrypt uses AES-GCM to encrypt the given plaintext with the given key. The // 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. // 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) { func Encrypt(key, plaintext []byte) ([]byte, error) {
switch len(key) { switch len(key) {
case 16, 24, 32: // AES-128, AES-192, AES-256 case 16, 24, 32: // AES-128, AES-192, AES-256

View File

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

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

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

@ -9,7 +9,7 @@ import (
var ( var (
// DefaultDHCurve is the default elliptic curve used for signing. // 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. // GenerateDHKey generates a new ECDH private key for key exchange.

View File

@ -4,29 +4,24 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/ecdh" "crypto/ecdh"
"crypto/ecdsa" "crypto/ed25519"
"crypto/sha256" "crypto/sha256"
"errors" "errors"
"io" "io"
"log"
"log/slog" "log/slog"
"koti.casa/numenor-labs/dsfx/pkg/assert" "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/encryption"
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity" "koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
"koti.casa/numenor-labs/dsfx/pkg/crypto/keyexchange" "koti.casa/numenor-labs/dsfx/pkg/crypto/keyexchange"
"koti.casa/numenor-labs/dsfx/pkg/frame"
"koti.casa/numenor-labs/dsfx/pkg/logging" "koti.casa/numenor-labs/dsfx/pkg/logging"
) )
const ( const (
// ECDHPublicKeySize is the size of an ECDH public key in bytes. DHKeySize = 32
ECDHPublicKeySize = 97 IdentityKeySize = 32
// 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
) )
// Initiate initiates the handshake process between the given actor // Initiate initiates the handshake process between the given actor
@ -34,8 +29,8 @@ const (
func Initiate( func Initiate(
ctx context.Context, ctx context.Context,
conn io.ReadWriteCloser, conn io.ReadWriteCloser,
lPrivKey *ecdsa.PrivateKey, lPrivKey ed25519.PrivateKey,
rPubKey *ecdsa.PublicKey, rPubKey ed25519.PublicKey,
) ([]byte, error) { ) ([]byte, error) {
logger := logging.FromContext(ctx).WithGroup("handshake") logger := logging.FromContext(ctx).WithGroup("handshake")
@ -56,32 +51,35 @@ func Initiate(
if err != nil { if err != nil {
return nil, err 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. // Write the actor's public key to the connection.
logger.DebugContext(ctx, "sending dh key", slog.Int("key.size", len(ourDHKeyRaw))) 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 { if err != nil {
return nil, err return nil, err
} }
if n != len(welcomeMessage) {
return nil, errors.New("failed to write dh key")
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Step 2: Ephemeral Key Exchange From Server // Step 2: Ephemeral Key Exchange From Server
// Read the remote actor's public key from the connection. // Read the remote actor's public key from the connection.
logger.DebugContext(ctx, "waiting for server's dh key") logger.DebugContext(ctx, "waiting for server's dh key")
remoteDHKeyFrame := frame.New(nil) remoteDHKeyRaw, err := buffer.ReadLenPrefixed(buffer.MaxUint16, conn)
_, err = remoteDHKeyFrame.ReadFrom(conn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(remoteDHKeyFrame.Contents()) != ECDHPublicKeySize {
return nil, errors.New("invalid dh key size")
}
// Import the remote actor's public key. // Import the remote actor's public key.
logger.DebugContext(ctx, "importing server's dh key") logger.DebugContext(ctx, "importing server's dh key")
remoteDHKey, err := keyexchange.ImportPublicKey(remoteDHKeyFrame.Contents()) remoteDHKey, err := keyexchange.ImportPublicKey(remoteDHKeyRaw)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -91,7 +89,7 @@ func Initiate(
// Export the public key of the actor's signing key. // Export the public key of the actor's signing key.
logger.DebugContext(ctx, "exporting public 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 { if err != nil {
return nil, err return nil, err
} }
@ -130,6 +128,9 @@ func Initiate(
} }
assert.Assert(len(derivedKey) == 32, "invalid shared secret size") 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 := make([]byte, 0, len(ourPublicKeyRaw)+len(signature))
plaintext = append(plaintext, ourPublicKeyRaw...) plaintext = append(plaintext, ourPublicKeyRaw...)
plaintext = append(plaintext, signature...) plaintext = append(plaintext, signature...)
@ -141,20 +142,28 @@ func Initiate(
return nil, err return nil, err
} }
log.Println(string(plaintext), len(plaintext))
// Write the boxed message to the connection. // Write the boxed message to the connection.
logger.DebugContext(ctx, "sending authentication message", slog.Int("message.size", len(boxedMsg))) 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 { if err != nil {
return nil, err 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 // Step 4: Server Authentication
// Read the authentication message from the connection. // Read the authentication message from the connection.
logger.DebugContext(ctx, "waiting for server's authentication message") logger.DebugContext(ctx, "waiting for server's authentication message")
authMessageFrame := frame.New(nil) authMessageBoxed, err := buffer.ReadLenPrefixed(buffer.MaxUint16, conn)
n, err := authMessageFrame.ReadFrom(conn)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -162,7 +171,7 @@ func Initiate(
// Decrypt the authentication message with the derived key. // Decrypt the authentication message with the derived key.
logger.DebugContext(ctx, "decrypting authentication message") logger.DebugContext(ctx, "decrypting authentication message")
plaintext, err = encryption.Decrypt(derivedKey, authMessageFrame.Contents()) plaintext, err = encryption.Decrypt(derivedKey, authMessageBoxed)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -182,11 +191,17 @@ func Initiate(
// Finally, we need to let the server know that the handshake is complete. // Finally, we need to let the server know that the handshake is complete.
logger.DebugContext(ctx, "sending handshake complete message") logger.DebugContext(ctx, "sending handshake complete message")
handshakeCompleteMsg := []byte{0x01} handshakeCompleteMsg, err := buffer.NewLenPrefixed([]byte{0x01})
_, err = frame.New(handshakeCompleteMsg).WriteTo(conn)
if err != nil { if err != nil {
return nil, err 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") logger.DebugContext(ctx, "handshake complete")
return derivedKey, nil return derivedKey, nil
@ -194,7 +209,7 @@ func Initiate(
// Accept accepts a handshake from the given actor and connection. It // Accept accepts a handshake from the given actor and connection. It
// returns the shared secret between the actor and the remote actor. // 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") 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. // Read the remote actor's public key from the connection.
logger.DebugContext(ctx, "waiting for client's dh key") logger.DebugContext(ctx, "waiting for client's dh key")
remoteDHKeyFrame := frame.New(nil) remoteDHKeyRaw, err := buffer.ReadLenPrefixed(buffer.MaxUint16, conn)
_, err := remoteDHKeyFrame.ReadFrom(conn)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// Import the remote actor's public key. // Import the remote actor's public key.
logger.DebugContext(ctx, "importing client's dh key") logger.DebugContext(ctx, "importing client's dh key")
remoteDHKey, err := keyexchange.ImportPublicKey(remoteDHKeyFrame.Contents()) remoteDHKey, err := keyexchange.ImportPublicKey(remoteDHKeyRaw)
if err != nil { if err != nil {
return nil, nil, err 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. // Write the actor's public key to the connection.
logger.DebugContext(ctx, "sending dh key", slog.Int("key.size", len(ourDHKeyRaw))) 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 { if err != nil {
return nil, nil, err 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 // Step 3: Server Authentication
// Read the authentication message from the connection. // Read the authentication message from the connection.
logger.DebugContext(ctx, "waiting for client's authentication message") logger.DebugContext(ctx, "waiting for client's authentication message")
authMessageFrame := frame.New(nil) authMessageRaw, err := buffer.ReadLenPrefixed(buffer.MaxUint16, conn)
n, err := authMessageFrame.ReadFrom(conn)
if err != nil { if err != nil {
return nil, nil, err 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. // Decrypt the authentication message with the derived key.
logger.DebugContext(ctx, "computing shared secret") 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") logger.DebugContext(ctx, "decrypting authentication message")
plaintext, err := encryption.Decrypt(derivedKey, authMessageFrame.Contents()) plaintext, err := encryption.Decrypt(derivedKey, authMessageRaw)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
log.Println(string(plaintext), len(plaintext))
clientPublicKeyRaw := plaintext[:identity.ExportedPublicKeySize] clientPublicKeyRaw := plaintext[:44]
signature := plaintext[identity.ExportedPublicKeySize:] signature := plaintext[44:]
// Verify the client's public key and signature. // Verify the client's public key and signature.
logger.DebugContext(ctx, "importing client's public signing key") 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. // Send the server's signature back to the client.
logger.DebugContext(ctx, "sending authentication message", slog.Int("message.size", len(boxedMsg))) 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 { if err != nil {
return nil, nil, err 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") logger.DebugContext(ctx, "waiting for handshake complete message")
// Read the handshake complete message from the client. // Read the handshake complete message from the client.
handshakeCompleteFrame := frame.New(nil) handshakeCompleteMsg, err := buffer.ReadLenPrefixed(buffer.MaxUint16, conn)
_, err = handshakeCompleteFrame.ReadFrom(conn)
if err != nil { if err != nil {
return nil, nil, err 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") return nil, nil, errors.New("invalid handshake complete message")
} }

View File

@ -3,7 +3,7 @@ package handshake_test
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/ecdsa" "crypto/ed25519"
"fmt" "fmt"
"net" "net"
"os" "os"
@ -17,9 +17,9 @@ import (
func TestHandshake(t *testing.T) { func TestHandshake(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// alice, represented by an ecdsa key pair. // alice, represented by an ed25519 key pair.
alice, _ := identity.Generate() alice, _ := identity.Generate()
// bob, also represented by an ecdsa key pair. // bob, also represented by an ed25519 key pair.
bob, _ := identity.Generate() bob, _ := identity.Generate()
var ( var (
@ -28,7 +28,7 @@ func TestHandshake(t *testing.T) {
// any errors produce by alice // any errors produce by alice
aliceErr error aliceErr error
// alice's public key as discovered by bob // alice's public key as discovered by bob
discoveredAlicePublicKey *ecdsa.PublicKey discoveredAlicePublicKey ed25519.PublicKey
// the secret that bob should arrive at on his own // the secret that bob should arrive at on his own
bobSecret []byte bobSecret []byte
@ -47,7 +47,7 @@ func TestHandshake(t *testing.T) {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(2)
go func() { go func() {
aliceSecret, aliceErr = handshake.Initiate(ctx, client, alice, &bob.PublicKey) aliceSecret, aliceErr = handshake.Initiate(ctx, client, alice, identity.ToPublicKey(bob))
wg.Done() wg.Done()
}() }()
go func() { go func() {
@ -76,7 +76,7 @@ func TestHandshake(t *testing.T) {
return return
} }
// Bob should have discovered alice's public key. // 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") t.Errorf("handshake failed: discovered public key is not equal to alice's public key")
return return
} }
@ -93,9 +93,9 @@ func BenchmarkHandshake(b *testing.B) {
func runSimulation() error { func runSimulation() error {
ctx := context.Background() ctx := context.Background()
// alice, represented by an ecdsa key pair. // alice, represented by an ed25519 key pair.
alice, _ := identity.Generate() alice, _ := identity.Generate()
// bob, also represented by an ecdsa key pair. // bob, also represented by an ed25519 key pair.
bob, _ := identity.Generate() bob, _ := identity.Generate()
var ( var (
@ -123,7 +123,7 @@ func runSimulation() error {
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(2)
go func() { go func() {
_, aliceErr = handshake.Initiate(ctx, client, alice, &bob.PublicKey) _, aliceErr = handshake.Initiate(ctx, client, alice, identity.ToPublicKey(bob))
wg.Done() wg.Done()
}() }()
go func() { go func() {

View File

@ -1,7 +1,7 @@
package network package network
import ( import (
"crypto/ecdsa" "crypto/ed25519"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@ -22,11 +22,11 @@ type Addr struct {
network string network string
ip net.IP ip net.IP
port int port int
publicKey *ecdsa.PublicKey publicKey ed25519.PublicKey
} }
// NewAddr creates a new Addr. // 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" network := "dsfx"
return &Addr{network, ip, port, publicKey} 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. // 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{ return &Addr{
network: "dsfx", network: "dsfx",
ip: addr.IP, ip: addr.IP,
@ -102,7 +102,7 @@ func (a *Addr) Port() int {
} }
// PublicKey returns the public key of the Addr. // PublicKey returns the public key of the Addr.
func (a *Addr) PublicKey() *ecdsa.PublicKey { func (a *Addr) PublicKey() ed25519.PublicKey {
return a.publicKey return a.publicKey
} }

View File

@ -1,7 +1,7 @@
package network package network
import ( import (
"crypto/ecdsa" "crypto/ed25519"
"net" "net"
"time" "time"
@ -14,12 +14,12 @@ import (
type Conn struct { type Conn struct {
conn *net.TCPConn conn *net.TCPConn
sessionKey []byte sessionKey []byte
localIdentity *ecdsa.PublicKey localIdentity ed25519.PublicKey
remoteIdentity *ecdsa.PublicKey remoteIdentity ed25519.PublicKey
} }
// NewConn creates a new Conn. // 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} return &Conn{conn, sessionKey, localIdentity, remoteIdentity}
} }

View File

@ -2,10 +2,11 @@ package network
import ( import (
"context" "context"
"crypto/ecdsa" "crypto/ed25519"
"log/slog" "log/slog"
"net" "net"
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
"koti.casa/numenor-labs/dsfx/pkg/handshake" "koti.casa/numenor-labs/dsfx/pkg/handshake"
) )
@ -13,7 +14,7 @@ import (
type Listener struct { type Listener struct {
logger *slog.Logger logger *slog.Logger
tcpListener *net.TCPListener tcpListener *net.TCPListener
identity *ecdsa.PrivateKey identity ed25519.PrivateKey
} }
// Accept implements net.Listener. // Accept implements net.Listener.
@ -30,7 +31,7 @@ func (l *Listener) Accept() (net.Conn, error) {
return nil, err 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. // Close implements net.Listener.
@ -42,5 +43,5 @@ func (l *Listener) Close() error {
func (l *Listener) Addr() net.Addr { func (l *Listener) Addr() net.Addr {
laddr := l.tcpListener.Addr().(*net.TCPAddr) 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 ( import (
"context" "context"
"crypto/ecdsa" "crypto/ed25519"
"net" "net"
"koti.casa/numenor-labs/dsfx/pkg/handshake" "koti.casa/numenor-labs/dsfx/pkg/handshake"
@ -12,7 +12,7 @@ import (
// Dial ... // Dial ...
func Dial( func Dial(
ctx context.Context, ctx context.Context,
identity *ecdsa.PrivateKey, identity ed25519.PrivateKey,
laddr *Addr, laddr *Addr,
raddr *Addr, raddr *Addr,
) (*Conn, error) { ) (*Conn, error) {
@ -32,7 +32,7 @@ func Dial(
// Listen ... // Listen ...
func Listen( func Listen(
ctx context.Context, ctx context.Context,
identity *ecdsa.PrivateKey, identity ed25519.PrivateKey,
laddr *Addr, laddr *Addr,
) (net.Listener, error) { ) (net.Listener, error) {
tcpListener, err := net.ListenTCP("tcp", laddr.TCPAddr()) tcpListener, err := net.ListenTCP("tcp", laddr.TCPAddr())

View File

@ -1,8 +1,6 @@
package main package main
import ( import (
"crypto/x509"
"encoding/pem"
"fmt" "fmt"
"os" "os"
@ -15,17 +13,10 @@ func main() {
panic(err) panic(err)
} }
// Encode the private key to der exported, err := identity.ExportPrivateKey(key)
der, err := x509.MarshalECPrivateKey(key)
if err != nil { if err != nil {
panic(err) panic(err)
} }
// Encode the private key to PEM fmt.Fprint(os.Stdout, string(exported))
pem := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: der,
})
fmt.Fprint(os.Stdout, string(pem))
} }