mirror of
https://git.numenor-labs.us/dsfx.git
synced 2025-04-29 16:20:34 +00:00
initial commit
This commit is contained in:
commit
dc892f6ac4
9
main.go
Normal file
9
main.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("Hello, World!")
|
||||||
|
}
|
58
pkg/dcrypto/aead.go
Normal file
58
pkg/dcrypto/aead.go
Normal 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
31
pkg/dcrypto/ecdh.go
Normal 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
62
pkg/dcrypto/ecdsa.go
Normal 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
11
pkg/dcrypto/hkdf.go
Normal 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
12
pkg/dcrypto/rand.go
Normal 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
55
pkg/encoding/vwb/vwb.go
Normal 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
347
pkg/handshake/handshake.go
Normal 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
|
||||||
|
}
|
90
pkg/handshake/handshake_test.go
Normal file
90
pkg/handshake/handshake_test.go
Normal 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
41
revive.toml
Normal 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]
|
Loading…
x
Reference in New Issue
Block a user