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
.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:
```sh
git clone https://koti.casa/numenor-labs/dsfx.git
cd dsfx
```
Build the project:
go build -o dsfx-server ./dsfx-server
go build -o dsfx-client ./dsfx-client
```sh
go build -o dist/ ./...
```
You can also install the executables to your $GOPATH/bin:
go install ./dsfx-server
go install ./dsfx-client
```sh
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:
./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:
@ -69,7 +75,9 @@ The dsfx-client uses a private key for the client (also an ECDSA key in PEM form
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:
@ -87,7 +95,9 @@ For example, “dsfx://127.0.0.1:8000#<base64 pubkey>” or “dsfx://127.0.0.1:
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.
@ -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:
./dsfx-server -h
./dsfx-client -h
```sh
dsfx-server -h
dsfx-client -h
```
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"
"os"
"koti.casa/numenor-labs/dsfx/shared/dcrypto"
"koti.casa/numenor-labs/dsfx/shared/dlog"
"koti.casa/numenor-labs/dsfx/shared/dnet"
)
@ -52,20 +53,28 @@ func main() {
os.Exit(1)
}
masterKey, err := LoadMasterKey(*flagKey)
identity, err := dcrypto.LoadSigningKeyFromFile(*flagKey)
if err != nil {
logger.ErrorContext(ctx, "failed to load private key", slog.Any("error", err))
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) {
case "test":
raddr := flag.Arg(1)
if raddr == "" {
raddrRaw := flag.Arg(1)
if raddrRaw == "" {
logger.ErrorContext(ctx, "no remote address provided")
os.Exit(1)
}
testConnection(ctx, raddr, masterKey)
testConnection(ctx, identity, laddr, raddrRaw)
case "":
logger.InfoContext(ctx, "no command provided")
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())
serverAddr, err := dnet.ParseAddr(raddr)
raddr, err := dnet.ParseAddr(raddrRaw)
if err != nil {
logger.ErrorContext(ctx, "failed to parse server address", slog.Any("error", err))
return
}
tcpAddr := &net.TCPAddr{
IP: serverAddr.IP(),
Port: serverAddr.Port(),
}
conn, err := dnet.Dial(ctx, nil, tcpAddr, clientPrivateKey, serverAddr.PublicKey())
conn, err := dnet.Dial(ctx, identity, laddr, raddr)
if err != nil {
logger.ErrorContext(ctx, "failed to connect", slog.Any("error", err))
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 (
"context"
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"log/slog"
"net"
"os"
"koti.casa/numenor-labs/dsfx/shared/dcrypto"
"koti.casa/numenor-labs/dsfx/shared/dlog"
"koti.casa/numenor-labs/dsfx/shared/dnet"
)
@ -45,28 +43,29 @@ func main() {
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 == "" {
slog.ErrorContext(ctx, "master key path is required")
os.Exit(1)
}
masterKey, err := LoadMasterKey(*flagMasterKey)
masterKey, err := dcrypto.LoadSigningKeyFromFile(*flagMasterKey)
if err != nil {
logger.ErrorContext(ctx, "failed to load master key", slog.Any("error", err))
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, err := dnet.Listen(ctx, addr, masterKey)
listener, err := dnet.Listen(ctx, masterKey, addr)
if err != nil {
logger.ErrorContext(ctx, "listener error", slog.Any("error", err))
os.Exit(1)
@ -101,23 +100,3 @@ func handleConnection(ctx context.Context, conn net.Conn) error {
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/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"math/big"
"os"
)
var (
@ -13,6 +17,26 @@ var (
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.
func GenerateSigningKey() (*ecdsa.PrivateKey, error) {
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
}
// 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.
func (a *Addr) Network() string {
return a.network
@ -95,3 +105,11 @@ func (a *Addr) Port() int {
func (a *Addr) PublicKey() *ecdsa.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 ...
func Dial(
ctx context.Context,
laddr *net.TCPAddr,
raddr *net.TCPAddr,
clientPrivateKey *ecdsa.PrivateKey,
serverPublicKey *ecdsa.PublicKey,
identity *ecdsa.PrivateKey,
laddr *Addr,
raddr *Addr,
) (*Conn, error) {
conn, err := net.DialTCP("tcp", laddr, raddr)
conn, err := net.DialTCP("tcp", laddr.TCPAddr(), raddr.TCPAddr())
if err != nil {
return nil, err
}
sessionKey, err := Handshake(ctx, conn, clientPrivateKey, serverPublicKey)
sessionKey, err := Handshake(ctx, conn, identity, raddr.PublicKey())
if err != nil {
return nil, err
}
return NewConn(conn, sessionKey, &clientPrivateKey.PublicKey, serverPublicKey), nil
return NewConn(conn, sessionKey, laddr.PublicKey(), raddr.PublicKey()), nil
}
// Listen ...
func Listen(
ctx context.Context,
laddr *net.TCPAddr,
identity *ecdsa.PrivateKey,
laddr *Addr,
) (net.Listener, error) {
tcpListener, err := net.ListenTCP("tcp", laddr)
tcpListener, err := net.ListenTCP("tcp", laddr.TCPAddr())
if err != nil {
return nil, err
}

View File

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