mirror of
https://git.numenor-labs.us/dsfx.git
synced 2025-04-29 08:10:34 +00:00
minor improvements
This commit is contained in:
parent
6ac855b0d1
commit
a193a5e398
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
bin
|
||||
.test-keys
|
||||
|
32
README.md
32
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 <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.
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -1,6 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGkAgEBBDAyJe83G1ZRqJ+kdngI9bwCnTJLrby+1sHIC+aB7OLuuIZXBHkA4fPs
|
||||
1owMaoDSZ66gBwYFK4EEACKhZANiAAR0D64K3NuL8+pLnfKJex9aBd9xWlzCdpCI
|
||||
C3IyrunWIIeXzcIPCfD4OiMtIkBD6jjOmJUKHMIzVQYr4isUa3z5j5va0n0if0+I
|
||||
1P1X2FU27eVK1AvUxR7OUI1OaJX23GQ=
|
||||
-----END PRIVATE KEY-----
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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...)
|
||||
|
Loading…
x
Reference in New Issue
Block a user