diff --git a/.gitignore b/.gitignore index ba077a4..711f9b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ bin +.test-keys diff --git a/README.md b/README.md index 4b60f93..fd2d912 100644 --- a/README.md +++ b/README.md @@ -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 +```sh +/dsfx-client -key /path/to/clientkey.pem test +``` Where: @@ -87,7 +95,9 @@ For example, “dsfx://127.0.0.1:8000#” 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. diff --git a/cmd/genid/main.go b/cmd/genkey/main.go similarity index 100% rename from cmd/genid/main.go rename to cmd/genkey/main.go diff --git a/dsfx-client/config.go b/dsfx-client/config.go deleted file mode 100644 index bae73af..0000000 --- a/dsfx-client/config.go +++ /dev/null @@ -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 -} diff --git a/dsfx-client/main.go b/dsfx-client/main.go index 6fe0199..a9953d6 100644 --- a/dsfx-client/main.go +++ b/dsfx-client/main.go @@ -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 diff --git a/dsfx-client/masterkey b/dsfx-client/masterkey deleted file mode 100644 index 878a152..0000000 --- a/dsfx-client/masterkey +++ /dev/null @@ -1,6 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGkAgEBBDAyJe83G1ZRqJ+kdngI9bwCnTJLrby+1sHIC+aB7OLuuIZXBHkA4fPs -1owMaoDSZ66gBwYFK4EEACKhZANiAAR0D64K3NuL8+pLnfKJex9aBd9xWlzCdpCI -C3IyrunWIIeXzcIPCfD4OiMtIkBD6jjOmJUKHMIzVQYr4isUa3z5j5va0n0if0+I -1P1X2FU27eVK1AvUxR7OUI1OaJX23GQ= ------END PRIVATE KEY----- diff --git a/dsfx-server/main.go b/dsfx-server/main.go index abb2584..667724c 100644 --- a/dsfx-server/main.go +++ b/dsfx-server/main.go @@ -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 -} diff --git a/shared/dcrypto/ecdsa.go b/shared/dcrypto/ecdsa.go index e1996b6..c5d5026 100644 --- a/shared/dcrypto/ecdsa.go +++ b/shared/dcrypto/ecdsa.go @@ -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) diff --git a/shared/dnet/addr.go b/shared/dnet/addr.go index 7bd4b0c..2abb561 100644 --- a/shared/dnet/addr.go +++ b/shared/dnet/addr.go @@ -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, + } +} diff --git a/shared/dnet/dnet.go b/shared/dnet/dnet.go index 83cfa8c..961ee26 100644 --- a/shared/dnet/dnet.go +++ b/shared/dnet/dnet.go @@ -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 } diff --git a/shared/dnet/handshake.go b/shared/dnet/handshake.go index cbe2a08..f2c2843 100644 --- a/shared/dnet/handshake.go +++ b/shared/dnet/handshake.go @@ -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...)