initial commit

This commit is contained in:
Dustin Stiles 2025-03-07 21:05:37 -05:00
commit dc892f6ac4
Signed by: duwstiles
GPG Key ID: BCD9912EC231FC87
12 changed files with 720 additions and 0 deletions

1
README.md Normal file
View File

@ -0,0 +1 @@
# DSFX

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module koti.casa/numenor-labs/dsfx
go 1.24.1

9
main.go Normal file
View File

@ -0,0 +1,9 @@
package main
import (
"log"
)
func main() {
log.Println("Hello, World!")
}

58
pkg/dcrypto/aead.go Normal file
View File

@ -0,0 +1,58 @@
package dcrypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
)
// Encrypt uses AES-GCM to encrypt the given plaintext with the given key.
func Encrypt(key, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
// Decrypt uses AES-GCM to decrypt the given ciphertext with the given key.
func Decrypt(key, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}

31
pkg/dcrypto/ecdh.go Normal file
View File

@ -0,0 +1,31 @@
package dcrypto
import (
"crypto/ecdh"
"crypto/rand"
)
var (
// DefaultDHCurve is the default elliptic curve used for signing.
DefaultDHCurve = ecdh.P384
)
// GenerateDHKey generates a new ECDH private key for key exchange.
func GenerateDHKey() (*ecdh.PrivateKey, error) {
return DefaultDHCurve().GenerateKey(rand.Reader)
}
// ComputeDHSecret computes the shared secret from the private key and the public key.
func ComputeDHSecret(priv *ecdh.PrivateKey, pub *ecdh.PublicKey) ([]byte, error) {
return priv.ECDH(pub)
}
// ExportDHPublicKey exports the public key as a byte slice.
func ExportDHPublicKey(pub *ecdh.PublicKey) ([]byte, error) {
return pub.Bytes(), nil
}
// ImportDHPublicKey imports the public key from a byte slice.
func ImportDHPublicKey(data []byte) (*ecdh.PublicKey, error) {
return DefaultDHCurve().NewPublicKey(data)
}

62
pkg/dcrypto/ecdsa.go Normal file
View File

@ -0,0 +1,62 @@
package dcrypto
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"math/big"
)
var (
// DefaultSigningCurve is the default elliptic curve used for signing.
DefaultSigningCurve = elliptic.P384
)
// GenerateSigningKey generates a new ECDSA private key for signing.
func GenerateSigningKey() (*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)
}
// ExportPublicSigningKey exports the public key as a byte slice.
func ExportPublicSigningKey(pub *ecdsa.PublicKey) ([]byte, error) {
data := struct {
N []byte `json:"n"`
PubX []byte `json:"pub_x"`
PubY []byte `json:"pub_y"`
}{
N: pub.Curve.Params().N.Bytes(),
PubX: pub.X.Bytes(),
PubY: pub.Y.Bytes(),
}
return json.Marshal(data)
}
// ImportPublicSigningKey imports the public key from a byte slice.
func ImportPublicSigningKey(pubBytes []byte) (*ecdsa.PublicKey, error) {
var data struct {
N []byte `json:"n"`
PubX []byte `json:"pub_x"`
PubY []byte `json:"pub_y"`
}
if err := json.Unmarshal(pubBytes, &data); err != nil {
return nil, err
}
params := new(ecdsa.PublicKey)
params.Curve = DefaultSigningCurve()
params.X = new(big.Int).SetBytes(data.PubX)
params.Y = new(big.Int).SetBytes(data.PubY)
return params, nil
}

11
pkg/dcrypto/hkdf.go Normal file
View File

@ -0,0 +1,11 @@
package dcrypto
import (
"crypto/hkdf"
"crypto/sha256"
)
// Key creates a key from the given secret and salt using HKDF.
func Key(secret, salt []byte) ([]byte, error) {
return hkdf.Key(sha256.New, secret, salt, "", 32)
}

12
pkg/dcrypto/rand.go Normal file
View File

@ -0,0 +1,12 @@
package dcrypto
import "crypto/rand"
// Challenge generates a random challenge of n bytes.
func Challenge(n int) []byte {
challenge := make([]byte, n)
if _, err := rand.Read(challenge); err != nil || len(challenge) != n {
return nil
}
return challenge
}

55
pkg/encoding/vwb/vwb.go Normal file
View File

@ -0,0 +1,55 @@
// Package vwb provides utilities for working with variable-width byte slices,
// usually in the context of network requests.
package vwb
import (
"fmt"
"io"
)
func debugf(msg string, args ...any) {
fmt.Printf("(encoding/vwb): "+msg, args...)
}
// Encode ...
func Encode(w io.Writer, input []byte) error {
debugf("encoding %d bytes\n", len(input))
buf := make([]byte, 0, len(input)+2)
// Write the length of the input
buf = append(buf, byte(len(input)>>8), byte(len(input)))
// Write the input
buf = append(buf, input...)
debugf("writing %d bytes\n", len(buf))
// Write the buffer to the writer
if _, err := w.Write(buf); err != nil {
return err
}
return nil
}
// Decode ...
func Decode(r io.Reader) ([]byte, error) {
debugf("beginning decode\n")
var lenbuf [2]byte
// Read the length of the input
if _, err := io.ReadFull(r, lenbuf[:]); err != nil {
return nil, err
}
debugf("read %d bytes for length\n", lenbuf)
// Calculate the length of the input
length := int(lenbuf[0])<<8 | int(lenbuf[1])
debugf("reading %d byte sized message\n", length)
// Read the input
data := make([]byte, length)
if _, err := io.ReadFull(r, data); err != nil {
return nil, err
}
debugf("done\n")
return data, nil
}

347
pkg/handshake/handshake.go Normal file
View File

@ -0,0 +1,347 @@
package handshake
import (
"crypto/ecdh"
"crypto/ecdsa"
"crypto/sha256"
"errors"
"fmt"
"io"
"net"
"koti.casa/numenor-labs/dsfx/pkg/dcrypto"
"koti.casa/numenor-labs/dsfx/pkg/encoding/vwb"
)
func debugf(msg string, args ...any) {
fmt.Printf("(debug): "+msg, args...)
}
// An Actor represents an entity that can participate in the handshake process.
// In this model, there are two actors: Alice and Bob. Alice represents the
// client role and Bob represents the server role. Each actor has a ecdsa private
// key used for identification, and a tcp address that they are reachable at.
type Actor interface {
// Identity represents the identity of the actor. It is used to sign messages
// on behalf of the actor during the handshake process. The actor may choose
// to use a well known key here in order to be recognized by other actors.
// It is also valid to return an ephemeral key here if the actor wishes to
// remain anonymous with the remote actor.
IdentityKey() *ecdsa.PrivateKey
// Address returns the address of the actor. This is used when listening for
// incoming connections, and when dialing out to other actors.
Address() *net.TCPAddr
}
// State represents the state of the handshake process. It should be passed to
// and from the various functions in the handshake process.
type State struct {
EphemeralKey *ecdh.PrivateKey
}
// InitiateHandshake initiates the handshake process between the given actor
// and the remote actor.
func InitiateHandshake(actor Actor, conn io.ReadWriter, rpub *ecdsa.PublicKey) ([]byte, error) {
// ------------------------------------------------------------------------
// Step 1: Ephemeral Key Exchange To Server
debugf("client: creating dh key\n")
// Create a new ECDH private key for the actor.
ourDHKey, err := dcrypto.GenerateDHKey()
if err != nil {
return nil, err
}
debugf("client: exporting dh key\n")
// Export the public key of the actor's ECDH private key.
ourDHKeyRaw, err := dcrypto.ExportDHPublicKey(ourDHKey.PublicKey())
if err != nil {
return nil, err
}
debugf("client: sending dh key\n")
// Write the actor's public key to the connection.
err = vwb.Encode(conn, ourDHKeyRaw)
if err != nil {
return nil, err
}
// ------------------------------------------------------------------------
// Step 2: Ephemeral Key Exchange From Server
debugf("client: waiting for server's dh key\n")
// Read the remote actor's public key from the connection.
remoteDHKeyRaw, err := vwb.Decode(conn)
if err != nil {
return nil, err
}
debugf("client: importing server's dh key\n")
// Import the remote actor's public key.
remoteDHKey, err := dcrypto.ImportDHPublicKey(remoteDHKeyRaw)
if err != nil {
return nil, err
}
// ------------------------------------------------------------------------
// Step 3: Client Authentication
debugf("client: exporting public signing key\n")
// Export the public key of the actor's signing key.
ourPublicKeyRaw, err := dcrypto.ExportPublicSigningKey(&actor.IdentityKey().PublicKey)
if err != nil {
return nil, err
}
debugf("client: exporting remote public signing key\n")
remotePublicKeyRaw, err := dcrypto.ExportPublicSigningKey(rpub)
if err != nil {
return nil, err
}
debugf("client: creating authentication message\n")
// Construct the message that will be signed by the client.
// This message is formatted as follows:
// rlt + sha256(ae + be)
// This binds both the client and server's long term public keys to the
// ephemeral keys that were exchanged in the previous step. This creates a
// verifiable link between both parties and the ephemeral keys used to
// establish the shared secret.
authMessage, err := buildMessage(ourDHKey.PublicKey(), remoteDHKey)
if err != nil {
return nil, err
}
debugf("client: signing authentication message\n")
// Sign the message with the actor's private key.
signature, err := dcrypto.Sign(actor.IdentityKey(), authMessage)
if err != nil {
return nil, err
}
debugf("client: encoding %d bytes of public key\n", len(ourPublicKeyRaw))
debugf("client: encoding %d bytes of signature\n", len(signature))
plaintext := make([]byte, 0, len(ourPublicKeyRaw)+len(signature))
plaintext = append(plaintext, ourPublicKeyRaw...)
plaintext = append(plaintext, signature...)
debugf("client: computing shared secret\n")
// Compute the shared secret between the actor and the remote actor.
sharedSecret, err := dcrypto.ComputeDHSecret(ourDHKey, remoteDHKey)
if err != nil {
return nil, err
}
debugf("client: deriving key from shared secret\n")
// Derive a key from the shared secret using HKDF.
derivedKey, err := dcrypto.Key(sharedSecret, nil)
if err != nil {
return nil, err
}
debugf("client: encrypting authentication message\n")
// Encrypt the message with the derived key.
boxedMsg, err := dcrypto.Encrypt(derivedKey, plaintext)
if err != nil {
return nil, err
}
debugf("client: sending authentication message\n")
// Write the boxed message to the connection.
err = vwb.Encode(conn, boxedMsg)
if err != nil {
return nil, err
}
// ------------------------------------------------------------------------
// Step 4: Server Authentication
debugf("client: waiting for server's authentication message\n")
// Read the authentication message from the connection.
authMessageRaw, err := vwb.Decode(conn)
if err != nil {
return nil, err
}
debugf("client: decrypting authentication message\n")
// Decrypt the authentication message with the derived key.
plaintext, err = dcrypto.Decrypt(derivedKey, authMessageRaw)
if err != nil {
return nil, err
}
debugf("client: importing server's signing key\n")
// The server authentication is just verifying the signature it created of
// the client authentication message.
remotePublicKey, err := dcrypto.ImportPublicSigningKey(remotePublicKeyRaw)
if err != nil {
return nil, err
}
debugf("client: verifying server's signature\n")
if !dcrypto.Verify(remotePublicKey, authMessage, plaintext) {
return nil, errors.New("failed to verify server's signature")
}
debugf("client: handshake complete\n")
return sharedSecret, nil
}
// AcceptHandshake accepts a handshake from the given actor and connection. It
// returns the shared secret between the actor and the remote actor.
func AcceptHandshake(actor Actor, conn io.ReadWriter) ([]byte, error) {
// ------------------------------------------------------------------------
// Step 1: Ephemeral Key Exchange From Client
debugf("server: starting handshake process\n")
// Read the remote actor's public key from the connection.
remoteDHKeyRaw, err := vwb.Decode(conn)
if err != nil {
return nil, err
}
debugf("server: importing client's dh key\n")
// Import the remote actor's public key.
remoteDHKey, err := dcrypto.ImportDHPublicKey(remoteDHKeyRaw)
if err != nil {
return nil, err
}
// ------------------------------------------------------------------------
// Step 2: Ephemeral Key Exchange To Client
debugf("server: creating dh key\n")
// Create a new ECDH private key for the actor.
ourDHKey, err := dcrypto.GenerateDHKey()
if err != nil {
return nil, err
}
debugf("server: exporting dh key\n")
// Export the public key of the actor's ECDH private key.
ourDHKeyRaw, err := dcrypto.ExportDHPublicKey(ourDHKey.PublicKey())
if err != nil {
return nil, err
}
debugf("server: sending dh key\n")
// Write the actor's public key to the connection.
err = vwb.Encode(conn, ourDHKeyRaw)
if err != nil {
return nil, err
}
// ------------------------------------------------------------------------
// Step 3: Server Authentication
debugf("server: waiting for client's authentication message\n")
// Read the authentication message from the connection.
authMessageRaw, err := vwb.Decode(conn)
if err != nil {
return nil, err
}
debugf("server: computing shared secret\n")
// Decrypt the authentication message with the derived key.
sharedSecret, err := dcrypto.ComputeDHSecret(ourDHKey, remoteDHKey)
if err != nil {
return nil, err
}
debugf("server: deriving key from shared secret\n")
// Derive a key from the shared secret using HKDF.
derivedKey, err := dcrypto.Key(sharedSecret, nil)
if err != nil {
return nil, err
}
debugf("server: decrypting authentication message\n")
plaintext, err := dcrypto.Decrypt(derivedKey, authMessageRaw)
if err != nil {
return nil, err
}
clientPublicKeyRaw := plaintext[:222]
signature := plaintext[222:]
debugf("server: importing client's public signing key\n")
// Verify the client's public key and signature.
clientPublicKey, err := dcrypto.ImportPublicSigningKey(clientPublicKeyRaw)
if err != nil {
return nil, err
}
debugf("server: verifying client's signature\n")
// Construct the message that was signed by the client.
// This message is formatted as follows:
// rlt + sha256(ae + be)
// This binds both the client and server's long term public keys to the
// ephemeral keys that were exchanged in the previous step. This creates a
// verifiable link between both parties and the ephemeral keys used to
// establish the shared secret.
authMessage, err := buildMessage(remoteDHKey, ourDHKey.PublicKey())
if err != nil {
return nil, err
}
if !dcrypto.Verify(clientPublicKey, authMessage, signature) {
return nil, errors.New("failed to verify client's signature")
}
debugf("server: creating authentication message\n")
// Now we need to sign the authentication message with the server's private
// key. This will be sent back to the client in the next step to authenticate
// the server to the client.
serverSignature, err := dcrypto.Sign(actor.IdentityKey(), authMessage)
if err != nil {
return nil, err
}
boxedMsg, err := dcrypto.Encrypt(derivedKey, serverSignature)
if err != nil {
return nil, err
}
debugf("server: sending authentication message\n")
// Send the server's signature back to the client.
err = vwb.Encode(conn, boxedMsg)
if err != nil {
return nil, err
}
// ------------------------------------------------------------------------
// Step 4: Client Authentication
debugf("server: handshake complete\n")
return sharedSecret, nil
}
func buildMessage(clientPubKey *ecdh.PublicKey, serverPubKey *ecdh.PublicKey) ([]byte, error) {
clientPubKeyRaw, err := dcrypto.ExportDHPublicKey(clientPubKey)
if err != nil {
return nil, err
}
serverPubKeyRaw, err := dcrypto.ExportDHPublicKey(serverPubKey)
if err != nil {
return nil, err
}
// Construct the message that will be signed by the client.
// This message is formatted as follows:
// rlt + sha256(ae + be)
// This binds both the client and server's long term public keys to the
// ephemeral keys that were exchanged in the previous step. This creates a
// verifiable link between both parties and the ephemeral keys used to
// establish the shared secret.
message := make([]byte, 0, len(clientPubKeyRaw)+len(serverPubKeyRaw))
message = append(message, clientPubKeyRaw...)
message = append(message, serverPubKeyRaw...)
messageChecksum := sha256.Sum256(message)
authMessage := make([]byte, 0, len(serverPubKeyRaw)+sha256.Size)
authMessage = append(authMessage, serverPubKeyRaw...)
authMessage = append(authMessage, messageChecksum[:]...)
return authMessage, nil
}

View File

@ -0,0 +1,90 @@
package handshake_test
import (
"bytes"
"crypto/ecdsa"
"log"
"net"
"sync"
"testing"
"koti.casa/numenor-labs/dsfx/pkg/dcrypto"
"koti.casa/numenor-labs/dsfx/pkg/handshake"
)
func TestHandshake(t *testing.T) {
var wg sync.WaitGroup
client, server := net.Pipe()
defer client.Close()
defer server.Close()
alice := newActor()
bob := newActor()
var aliceSharedSecret []byte
var aliceErr error
var bobSharedSecret []byte
var bobErr error
wg.Add(2)
go func() {
aliceSharedSecret, aliceErr = handshake.InitiateHandshake(alice, client, &bob.IdentityKey().PublicKey)
wg.Done()
}()
go func() {
bobSharedSecret, bobErr = handshake.AcceptHandshake(bob, server)
wg.Done()
}()
wg.Wait()
if aliceErr != nil || bobErr != nil {
if aliceErr != nil {
t.Errorf("alice error: %v", aliceErr)
return
}
t.Errorf("bob error: %v", bobErr)
return
}
if aliceSharedSecret == nil || bobSharedSecret == nil {
t.Errorf("handshake failed: shared secret is nil")
return
}
if len(aliceSharedSecret) == 0 || len(bobSharedSecret) == 0 {
t.Errorf("handshake failed: shared secret is empty")
}
if !bytes.Equal(aliceSharedSecret, bobSharedSecret) {
t.Errorf("handshake failed: shared secrets do not match")
return
}
}
// An actor represents an entity that can participate in the handshake process.
type actor struct {
identity *ecdsa.PrivateKey
}
// newActor creates a new actor with a random identity key.
func newActor() *actor {
key, err := dcrypto.GenerateSigningKey()
if err != nil {
log.Fatal(err)
}
return &actor{
identity: key,
}
}
// IdentityKey ...
func (a *actor) IdentityKey() *ecdsa.PrivateKey {
return a.identity
}
// Address returns the address of the actor. This is used when listening for
// incoming connections, and when dialing out to other actors.
func (a *actor) Address() *net.TCPAddr {
return nil
}

41
revive.toml Normal file
View File

@ -0,0 +1,41 @@
ignoreGeneratedHeader = false
severity = "warning"
confidence = 0.8
errorCode = 1
warningCode = 1
[rule.bare-return]
[rule.blank-imports]
[rule.context-as-argument]
[rule.context-keys-type]
[rule.dot-imports]
[rule.empty-block]
[rule.empty-lines]
[rule.enforce-map-style]
[rule.enforce-slice-style]
[rule.error-naming]
[rule.error-return]
[rule.error-strings]
[rule.errorf]
[rule.exported]
[rule.filename-format]
# Override the default pattern to forbid .go files with uppercase letters and dashes.
arguments=["^[_a-z][_a-z0-9]*\\.go$"]
[rule.increment-decrement]
[rule.indent-error-flow]
[rule.line-length-limit]
arguments = [200]
# [rule.package-comments]
[rule.range]
[rule.receiver-naming]
[rule.redefines-builtin-id]
[rule.superfluous-else]
[rule.time-naming]
[rule.unexported-naming]
[rule.unexported-return]
[rule.unreachable-code]
[rule.unused-parameter]
[rule.useless-break]
[rule.use-any]
[rule.var-declaration]
[rule.var-naming]