mirror of
https://git.numenor-labs.us/dsfx.git
synced 2025-04-29 00:00:35 +00:00
commit
b78eb2b71a
14
README.md
14
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 <remote_addr>
|
||||
dsfxctl -key /path/to/clientkey test <remote_addr>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
57
pkg/buffer/lenprefixed.go
Normal file
57
pkg/buffer/lenprefixed.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
35
pkg/crypto/encryption/aead_test.go
Normal file
35
pkg/crypto/encryption/aead_test.go
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
91
pkg/crypto/identity/ed25519.go
Normal file
91
pkg/crypto/identity/ed25519.go
Normal 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)
|
||||
}
|
@ -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.
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user