minor improvements

This commit is contained in:
Dustin Stiles 2025-03-09 12:33:27 -04:00
parent 6ac855b0d1
commit a193a5e398
Signed by: duwstiles
GPG Key ID: BCD9912EC231FC87
11 changed files with 119 additions and 98 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
bin bin
.test-keys

View File

@ -25,18 +25,22 @@ dsfx is a robust, secure, and distributed file exchange system written in Go. De
Clone the repository: Clone the repository:
```sh
git clone https://koti.casa/numenor-labs/dsfx.git git clone https://koti.casa/numenor-labs/dsfx.git
cd dsfx cd dsfx
```
Build the project: Build the project:
go build -o dsfx-server ./dsfx-server ```sh
go build -o dsfx-client ./dsfx-client go build -o dist/ ./...
```
You can also install the executables to your $GOPATH/bin: You can also install the executables to your $GOPATH/bin:
go install ./dsfx-server ```sh
go install ./dsfx-client go install ./...
```
--- ---
@ -46,9 +50,11 @@ go install ./dsfx-client
The dsfx-server requires a listening host, port, and a master key (ECDSA private key in PEM format). For example: The dsfx-server requires a listening host, port, and a master key (ECDSA private key in PEM format). For example:
./dsfx-server -host localhost -port 8000 -masterKey /path/to/masterkey.pem ```sh
dsfx-server -host localhost -port 8000 -masterKey /path/to/masterkey.pem
```
> Note, if you need to generate a new ECDSA key, you can use the following command: `go run ./cmd/genid > path/to/masterkey.pem` > Note, if you need to generate a new ECDSA key, you can use the following command: `go run ./cmd/genkey > path/to/masterkey.pem`
Command-line flags for dsfx-server: Command-line flags for dsfx-server:
@ -69,7 +75,9 @@ The dsfx-client uses a private key for the client (also an ECDSA key in PEM form
Client command usage: Client command usage:
./dsfx-client -key /path/to/clientkey.pem test <remote_addr> ```sh
/dsfx-client -key /path/to/clientkey.pem test <remote_addr>
```
Where: Where:
@ -87,7 +95,9 @@ For example, “dsfx://127.0.0.1:8000#<base64 pubkey>” or “dsfx://127.0.0.1:
Example: Example:
./dsfx-client -key ./dsfx-client/masterkey test dsfx://127.0.0.1:8000#eyJuIjoiLy8v.. ```sh
dsfx-client -key ./dsfx-client/masterkey test dsfx://127.0.0.1:8000#eyJuIjoiLy8v..
```
If no command or an unrecognized command is provided, the client will print a brief usage message and exit. If no command or an unrecognized command is provided, the client will print a brief usage message and exit.
@ -95,8 +105,10 @@ If no command or an unrecognized command is provided, the client will print a br
For quick help, simply pass the -h flag: For quick help, simply pass the -h flag:
./dsfx-server -h ```sh
./dsfx-client -h dsfx-server -h
dsfx-client -h
```
This will display the usage information along with available flags. This will display the usage information along with available flags.

View File

@ -1,29 +0,0 @@
package main
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
)
func LoadMasterKey(path string) (*ecdsa.PrivateKey, error) {
masterKeyFile, err := os.ReadFile(path)
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
}

View File

@ -9,6 +9,7 @@ import (
"net" "net"
"os" "os"
"koti.casa/numenor-labs/dsfx/shared/dcrypto"
"koti.casa/numenor-labs/dsfx/shared/dlog" "koti.casa/numenor-labs/dsfx/shared/dlog"
"koti.casa/numenor-labs/dsfx/shared/dnet" "koti.casa/numenor-labs/dsfx/shared/dnet"
) )
@ -52,20 +53,28 @@ func main() {
os.Exit(1) os.Exit(1)
} }
masterKey, err := LoadMasterKey(*flagKey) identity, err := dcrypto.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)
} }
laddr := dnet.NewAddr(
net.ParseIP("0.0.0.0"),
0, // port 0 means any available port
&identity.PublicKey,
)
logger.DebugContext(ctx, "using addr", slog.String("address", laddr.String()))
switch flag.Arg(0) { switch flag.Arg(0) {
case "test": case "test":
raddr := flag.Arg(1) raddrRaw := flag.Arg(1)
if raddr == "" { if raddrRaw == "" {
logger.ErrorContext(ctx, "no remote address provided") logger.ErrorContext(ctx, "no remote address provided")
os.Exit(1) os.Exit(1)
} }
testConnection(ctx, raddr, masterKey) testConnection(ctx, identity, laddr, raddrRaw)
case "": case "":
logger.InfoContext(ctx, "no command provided") logger.InfoContext(ctx, "no command provided")
os.Exit(1) os.Exit(1)
@ -75,21 +84,16 @@ func main() {
} }
} }
func testConnection(ctx context.Context, raddr string, clientPrivateKey *ecdsa.PrivateKey) { func testConnection(ctx context.Context, identity *ecdsa.PrivateKey, laddr *dnet.Addr, raddrRaw string) {
logger := dlog.FromContext(context.Background()) logger := dlog.FromContext(context.Background())
serverAddr, err := dnet.ParseAddr(raddr) raddr, err := dnet.ParseAddr(raddrRaw)
if err != nil { if err != nil {
logger.ErrorContext(ctx, "failed to parse server address", slog.Any("error", err)) logger.ErrorContext(ctx, "failed to parse server address", slog.Any("error", err))
return return
} }
tcpAddr := &net.TCPAddr{ conn, err := dnet.Dial(ctx, identity, laddr, raddr)
IP: serverAddr.IP(),
Port: serverAddr.Port(),
}
conn, err := dnet.Dial(ctx, nil, tcpAddr, clientPrivateKey, serverAddr.PublicKey())
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

@ -1,6 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIGkAgEBBDAyJe83G1ZRqJ+kdngI9bwCnTJLrby+1sHIC+aB7OLuuIZXBHkA4fPs
1owMaoDSZ66gBwYFK4EEACKhZANiAAR0D64K3NuL8+pLnfKJex9aBd9xWlzCdpCI
C3IyrunWIIeXzcIPCfD4OiMtIkBD6jjOmJUKHMIzVQYr4isUa3z5j5va0n0if0+I
1P1X2FU27eVK1AvUxR7OUI1OaJX23GQ=
-----END PRIVATE KEY-----

View File

@ -2,15 +2,13 @@ package main
import ( import (
"context" "context"
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"flag" "flag"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
"os" "os"
"koti.casa/numenor-labs/dsfx/shared/dcrypto"
"koti.casa/numenor-labs/dsfx/shared/dlog" "koti.casa/numenor-labs/dsfx/shared/dlog"
"koti.casa/numenor-labs/dsfx/shared/dnet" "koti.casa/numenor-labs/dsfx/shared/dnet"
) )
@ -45,28 +43,29 @@ func main() {
flag.Parse() flag.Parse()
addrRaw := net.JoinHostPort(*flagHost, fmt.Sprint(*flagPort))
addr, err := net.ResolveTCPAddr("tcp", addrRaw)
if err != nil {
slog.ErrorContext(ctx, "invalid host or port")
os.Exit(1)
}
if *flagMasterKey == "" { if *flagMasterKey == "" {
slog.ErrorContext(ctx, "master key path is required") slog.ErrorContext(ctx, "master key path is required")
os.Exit(1) os.Exit(1)
} }
masterKey, err := LoadMasterKey(*flagMasterKey) masterKey, err := dcrypto.LoadSigningKeyFromFile(*flagMasterKey)
if err != nil { if err != nil {
logger.ErrorContext(ctx, "failed to load master key", slog.Any("error", err)) logger.ErrorContext(ctx, "failed to load master key", slog.Any("error", err))
os.Exit(1) os.Exit(1)
} }
tcpAddrRaw := net.JoinHostPort(*flagHost, fmt.Sprint(*flagPort))
tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw)
if err != nil {
slog.ErrorContext(ctx, "invalid host or port")
os.Exit(1)
}
addr := dnet.FromTCPAddr(tcpAddr, &masterKey.PublicKey)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Listener // Listener
listener, err := dnet.Listen(ctx, addr, masterKey) listener, err := dnet.Listen(ctx, masterKey, addr)
if err != nil { if err != nil {
logger.ErrorContext(ctx, "listener error", slog.Any("error", err)) logger.ErrorContext(ctx, "listener error", slog.Any("error", err))
os.Exit(1) os.Exit(1)
@ -101,23 +100,3 @@ func handleConnection(ctx context.Context, conn net.Conn) error {
return nil return nil
} }
func LoadMasterKey(path string) (*ecdsa.PrivateKey, error) {
masterKeyFile, err := os.ReadFile(path)
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
}

View File

@ -4,8 +4,12 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem"
"fmt"
"math/big" "math/big"
"os"
) )
var ( var (
@ -13,6 +17,26 @@ var (
DefaultSigningCurve = elliptic.P384 DefaultSigningCurve = elliptic.P384
) )
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
}
// GenerateSigningKey generates a new ECDSA private key for signing. // GenerateSigningKey generates a new ECDSA private key for signing.
func GenerateSigningKey() (*ecdsa.PrivateKey, error) { func GenerateSigningKey() (*ecdsa.PrivateKey, error) {
return ecdsa.GenerateKey(DefaultSigningCurve(), rand.Reader) return ecdsa.GenerateKey(DefaultSigningCurve(), rand.Reader)

View File

@ -69,6 +69,16 @@ func ParseAddr(addrRaw string) (*Addr, error) {
return &Addr{network, ip, port, publicKey}, nil return &Addr{network, ip, port, publicKey}, nil
} }
// FromTCPAddr creates a new Addr from a net.TCPAddr.
func FromTCPAddr(addr *net.TCPAddr, publicKey *ecdsa.PublicKey) *Addr {
return &Addr{
network: "dsfx",
ip: addr.IP,
port: addr.Port,
publicKey: publicKey,
}
}
// Network implements net.Addr. // Network implements net.Addr.
func (a *Addr) Network() string { func (a *Addr) Network() string {
return a.network return a.network
@ -95,3 +105,11 @@ func (a *Addr) Port() int {
func (a *Addr) PublicKey() *ecdsa.PublicKey { func (a *Addr) PublicKey() *ecdsa.PublicKey {
return a.publicKey return a.publicKey
} }
// TCPAddr returns a net.TCPAddr for the Addr.
func (a *Addr) TCPAddr() *net.TCPAddr {
return &net.TCPAddr{
IP: a.ip,
Port: a.port,
}
}

View File

@ -11,31 +11,30 @@ import (
// Dial ... // Dial ...
func Dial( func Dial(
ctx context.Context, ctx context.Context,
laddr *net.TCPAddr, identity *ecdsa.PrivateKey,
raddr *net.TCPAddr, laddr *Addr,
clientPrivateKey *ecdsa.PrivateKey, raddr *Addr,
serverPublicKey *ecdsa.PublicKey,
) (*Conn, error) { ) (*Conn, error) {
conn, err := net.DialTCP("tcp", laddr, raddr) conn, err := net.DialTCP("tcp", laddr.TCPAddr(), raddr.TCPAddr())
if err != nil { if err != nil {
return nil, err return nil, err
} }
sessionKey, err := Handshake(ctx, conn, clientPrivateKey, serverPublicKey) sessionKey, err := Handshake(ctx, conn, identity, raddr.PublicKey())
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(conn, sessionKey, &clientPrivateKey.PublicKey, serverPublicKey), nil return NewConn(conn, sessionKey, laddr.PublicKey(), raddr.PublicKey()), nil
} }
// Listen ... // Listen ...
func Listen( func Listen(
ctx context.Context, ctx context.Context,
laddr *net.TCPAddr,
identity *ecdsa.PrivateKey, identity *ecdsa.PrivateKey,
laddr *Addr,
) (net.Listener, error) { ) (net.Listener, error) {
tcpListener, err := net.ListenTCP("tcp", laddr) tcpListener, err := net.ListenTCP("tcp", laddr.TCPAddr())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -10,10 +10,22 @@ import (
"io" "io"
"log/slog" "log/slog"
"koti.casa/numenor-labs/dsfx/pkg/assert"
"koti.casa/numenor-labs/dsfx/shared/dcrypto" "koti.casa/numenor-labs/dsfx/shared/dcrypto"
"koti.casa/numenor-labs/dsfx/shared/dlog" "koti.casa/numenor-labs/dsfx/shared/dlog"
) )
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
)
// Handshake initiates the handshake process between the given actor // Handshake initiates the handshake process between the given actor
// and the remote actor. // and the remote actor.
func Handshake( func Handshake(
@ -33,6 +45,7 @@ func Handshake(
if err != nil { if err != nil {
return nil, err return nil, err
} }
assert.Assert(ourDHKey != nil, "failed to generate dh key")
logger.DebugContext(ctx, "exporting dh key") logger.DebugContext(ctx, "exporting dh key")
// Export the public key of the actor's ECDH private key. // Export the public key of the actor's ECDH private key.
@ -40,6 +53,7 @@ func Handshake(
if err != nil { if err != nil {
return nil, err return nil, err
} }
assert.Assert(len(ourDHKeyRaw) == ECDHPublicKeySize, "invalid dh key size")
// 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)))
@ -58,6 +72,9 @@ func Handshake(
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")
@ -108,6 +125,7 @@ func Handshake(
if err != nil { if err != nil {
return nil, err return nil, err
} }
assert.Assert(len(sharedSecret) == 48, "invalid shared secret size")
// Derive a key from the shared secret using HKDF. // Derive a key from the shared secret using HKDF.
logger.DebugContext(ctx, "deriving key from shared secret") logger.DebugContext(ctx, "deriving key from shared secret")
@ -115,6 +133,7 @@ func Handshake(
if err != nil { if err != nil {
return nil, err return nil, err
} }
assert.Assert(len(derivedKey) == 32, "invalid derived key size")
plaintext := make([]byte, 0, len(ourPublicKeyRaw)+len(signature)) plaintext := make([]byte, 0, len(ourPublicKeyRaw)+len(signature))
plaintext = append(plaintext, ourPublicKeyRaw...) plaintext = append(plaintext, ourPublicKeyRaw...)