2025-03-08 15:07:27 -05:00
|
|
|
package dnet
|
2025-03-07 21:05:37 -05:00
|
|
|
|
|
|
|
import (
|
2025-03-08 15:07:27 -05:00
|
|
|
"bytes"
|
|
|
|
"context"
|
2025-03-07 21:05:37 -05:00
|
|
|
"crypto/ecdh"
|
|
|
|
"crypto/ecdsa"
|
|
|
|
"crypto/sha256"
|
|
|
|
"errors"
|
|
|
|
"io"
|
2025-03-08 15:07:27 -05:00
|
|
|
"log/slog"
|
2025-03-07 21:05:37 -05:00
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
"koti.casa/numenor-labs/dsfx/shared/dcrypto"
|
|
|
|
"koti.casa/numenor-labs/dsfx/shared/dlog"
|
2025-03-07 21:05:37 -05:00
|
|
|
)
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
type Hand struct {
|
|
|
|
State byte
|
|
|
|
Identity *ecdsa.PrivateKey
|
|
|
|
AuthMessage []byte
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
func NewHandshake(identity *ecdsa.PrivateKey) *Hand {
|
|
|
|
return &Hand{
|
|
|
|
State: 0,
|
|
|
|
Identity: identity,
|
|
|
|
}
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
// Handshake initiates the handshake process between the given actor
|
2025-03-07 21:05:37 -05:00
|
|
|
// and the remote actor.
|
2025-03-08 15:07:27 -05:00
|
|
|
func Handshake(
|
|
|
|
ctx context.Context,
|
|
|
|
conn io.ReadWriteCloser,
|
|
|
|
lPrivKey *ecdsa.PrivateKey,
|
|
|
|
rPubKey *ecdsa.PublicKey,
|
|
|
|
) ([]byte, error) {
|
|
|
|
logger := dlog.FromContext(ctx).WithGroup("handshake")
|
|
|
|
|
2025-03-07 21:05:37 -05:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Step 1: Ephemeral Key Exchange To Server
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "creating dh key")
|
2025-03-07 21:05:37 -05:00
|
|
|
// Create a new ECDH private key for the actor.
|
|
|
|
ourDHKey, err := dcrypto.GenerateDHKey()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "exporting dh key")
|
2025-03-07 21:05:37 -05:00
|
|
|
// Export the public key of the actor's ECDH private key.
|
|
|
|
ourDHKeyRaw, err := dcrypto.ExportDHPublicKey(ourDHKey.PublicKey())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the actor's public key to the connection.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "sending dh key", slog.Int("key.size", len(ourDHKeyRaw)))
|
|
|
|
_, err = NewFrame(ourDHKeyRaw).WriteTo(conn)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Step 2: Ephemeral Key Exchange From Server
|
|
|
|
|
|
|
|
// Read the remote actor's public key from the connection.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "waiting for server's dh key")
|
|
|
|
remoteDHKeyFrame := NewFrame(nil)
|
|
|
|
_, err = remoteDHKeyFrame.ReadFrom(conn)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Import the remote actor's public key.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "importing server's dh key")
|
|
|
|
remoteDHKey, err := dcrypto.ImportDHPublicKey(remoteDHKeyFrame.Contents())
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Step 3: Client Authentication
|
|
|
|
|
|
|
|
// Export the public key of the actor's signing key.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "exporting public signing key")
|
|
|
|
ourPublicKeyRaw, err := dcrypto.ExportPublicSigningKey(&lPrivKey.PublicKey)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "exporting remote public signing key")
|
|
|
|
remotePublicKeyRaw, err := dcrypto.ExportPublicSigningKey(rPubKey)
|
2025-03-07 21:05:37 -05:00
|
|
|
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.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "building authentication message")
|
2025-03-07 21:05:37 -05:00
|
|
|
authMessage, err := buildMessage(ourDHKey.PublicKey(), remoteDHKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sign the message with the actor's private key.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "signing authentication message")
|
|
|
|
signature, err := dcrypto.Sign(lPrivKey, authMessage)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compute the shared secret between the actor and the remote actor.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "computing shared secret")
|
2025-03-07 21:05:37 -05:00
|
|
|
sharedSecret, err := dcrypto.ComputeDHSecret(ourDHKey, remoteDHKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Derive a key from the shared secret using HKDF.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "deriving key from shared secret")
|
2025-03-07 21:05:37 -05:00
|
|
|
derivedKey, err := dcrypto.Key(sharedSecret, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
plaintext := make([]byte, 0, len(ourPublicKeyRaw)+len(signature))
|
|
|
|
plaintext = append(plaintext, ourPublicKeyRaw...)
|
|
|
|
plaintext = append(plaintext, signature...)
|
|
|
|
|
2025-03-07 21:05:37 -05:00
|
|
|
// Encrypt the message with the derived key.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "encrypting authentication message")
|
2025-03-07 21:05:37 -05:00
|
|
|
boxedMsg, err := dcrypto.Encrypt(derivedKey, plaintext)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the boxed message to the connection.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "sending authentication message", slog.Int("message.size", len(boxedMsg)))
|
|
|
|
_, err = NewFrame(boxedMsg).WriteTo(conn)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Step 4: Server Authentication
|
|
|
|
|
|
|
|
// Read the authentication message from the connection.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "waiting for server's authentication message")
|
|
|
|
authMessageFrame := NewFrame(nil)
|
|
|
|
n, err := authMessageFrame.ReadFrom(conn)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "received authentication message", slog.Int("message.size", int(n)))
|
2025-03-07 21:05:37 -05:00
|
|
|
|
|
|
|
// Decrypt the authentication message with the derived key.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "decrypting authentication message")
|
|
|
|
plaintext, err = dcrypto.Decrypt(derivedKey, authMessageFrame.Contents())
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// The server authentication is just verifying the signature it created of
|
|
|
|
// the client authentication message.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "importing server's public signing key")
|
2025-03-07 21:05:37 -05:00
|
|
|
remotePublicKey, err := dcrypto.ImportPublicSigningKey(remotePublicKeyRaw)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "verifying server's signature")
|
2025-03-07 21:05:37 -05:00
|
|
|
if !dcrypto.Verify(remotePublicKey, authMessage, plaintext) {
|
|
|
|
return nil, errors.New("failed to verify server's signature")
|
|
|
|
}
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
// Finally, we need to let the server know that the handshake is complete.
|
|
|
|
logger.DebugContext(ctx, "sending handshake complete message")
|
|
|
|
handshakeCompleteMsg := []byte{0x01}
|
|
|
|
_, err = NewFrame(handshakeCompleteMsg).WriteTo(conn)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.DebugContext(ctx, "handshake complete")
|
|
|
|
return derivedKey, nil
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// AcceptHandshake accepts a handshake from the given actor and connection. It
|
|
|
|
// returns the shared secret between the actor and the remote actor.
|
2025-03-08 15:07:27 -05:00
|
|
|
func AcceptHandshake(ctx context.Context, conn io.ReadWriteCloser, lPrivKey *ecdsa.PrivateKey) (*ecdsa.PublicKey, []byte, error) {
|
|
|
|
logger := dlog.FromContext(ctx).WithGroup("handshake")
|
|
|
|
|
2025-03-07 21:05:37 -05:00
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Step 1: Ephemeral Key Exchange From Client
|
|
|
|
|
|
|
|
// Read the remote actor's public key from the connection.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "waiting for client's dh key")
|
|
|
|
remoteDHKeyFrame := NewFrame(nil)
|
|
|
|
_, err := remoteDHKeyFrame.ReadFrom(conn)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Import the remote actor's public key.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "importing client's dh key")
|
|
|
|
remoteDHKey, err := dcrypto.ImportDHPublicKey(remoteDHKeyFrame.Contents())
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Step 2: Ephemeral Key Exchange To Client
|
|
|
|
|
|
|
|
// Create a new ECDH private key for the actor.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "creating dh key")
|
2025-03-07 21:05:37 -05:00
|
|
|
ourDHKey, err := dcrypto.GenerateDHKey()
|
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Export the public key of the actor's ECDH private key.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "exporting dh key")
|
2025-03-07 21:05:37 -05:00
|
|
|
ourDHKeyRaw, err := dcrypto.ExportDHPublicKey(ourDHKey.PublicKey())
|
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Write the actor's public key to the connection.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "sending dh key", slog.Int("key.size", len(ourDHKeyRaw)))
|
|
|
|
_, err = NewFrame(ourDHKeyRaw).WriteTo(conn)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Step 3: Server Authentication
|
|
|
|
|
|
|
|
// Read the authentication message from the connection.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "waiting for client's authentication message")
|
|
|
|
authMessageFrame := NewFrame(nil)
|
|
|
|
n, err := authMessageFrame.ReadFrom(conn)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "received authentication message", slog.Int("message.size", int(n)))
|
2025-03-07 21:05:37 -05:00
|
|
|
|
|
|
|
// Decrypt the authentication message with the derived key.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "computing shared secret")
|
2025-03-07 21:05:37 -05:00
|
|
|
sharedSecret, err := dcrypto.ComputeDHSecret(ourDHKey, remoteDHKey)
|
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Derive a key from the shared secret using HKDF.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "deriving key from shared secret")
|
2025-03-07 21:05:37 -05:00
|
|
|
derivedKey, err := dcrypto.Key(sharedSecret, nil)
|
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "decrypting authentication message")
|
|
|
|
plaintext, err := dcrypto.Decrypt(derivedKey, authMessageFrame.Contents())
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
clientPublicKeyRaw := plaintext[:222]
|
|
|
|
signature := plaintext[222:]
|
|
|
|
|
|
|
|
// Verify the client's public key and signature.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "importing client's public signing key")
|
2025-03-07 21:05:37 -05:00
|
|
|
clientPublicKey, err := dcrypto.ImportPublicSigningKey(clientPublicKeyRaw)
|
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "building authentication message")
|
2025-03-07 21:05:37 -05:00
|
|
|
authMessage, err := buildMessage(remoteDHKey, ourDHKey.PublicKey())
|
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "verifying client's signature")
|
2025-03-07 21:05:37 -05:00
|
|
|
if !dcrypto.Verify(clientPublicKey, authMessage, signature) {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, errors.New("failed to verify client's signature")
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "signing authentication message")
|
|
|
|
serverSignature, err := dcrypto.Sign(lPrivKey, authMessage)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "encrypting server's signature")
|
2025-03-07 21:05:37 -05:00
|
|
|
boxedMsg, err := dcrypto.Encrypt(derivedKey, serverSignature)
|
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Send the server's signature back to the client.
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "sending authentication message", slog.Int("message.size", len(boxedMsg)))
|
|
|
|
_, err = NewFrame(boxedMsg).WriteTo(conn)
|
2025-03-07 21:05:37 -05:00
|
|
|
if err != nil {
|
2025-03-08 15:07:27 -05:00
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.DebugContext(ctx, "waiting for handshake complete message")
|
|
|
|
// Read the handshake complete message from the client.
|
|
|
|
handshakeCompleteFrame := NewFrame(nil)
|
|
|
|
_, err = handshakeCompleteFrame.ReadFrom(conn)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
if !bytes.Equal(handshakeCompleteFrame.Contents(), []byte{0x01}) {
|
|
|
|
return nil, nil, errors.New("invalid handshake complete message")
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------------
|
|
|
|
// Step 4: Client Authentication
|
|
|
|
|
2025-03-08 15:07:27 -05:00
|
|
|
logger.DebugContext(ctx, "handshake complete")
|
|
|
|
return clientPublicKey, derivedKey, nil
|
2025-03-07 21:05:37 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|