From 9b6ae45070093e4696f3780022a2e016f23e600f Mon Sep 17 00:00:00 2001 From: Petel__ Date: Mon, 17 Nov 2025 17:21:20 -0800 Subject: [PATCH 1/3] implement secure connection in Confidential Space --- .../confidentialspace/confidentialspace.go | 89 ++++++++++++++++++- pkg/vault/confidentialspace/rpc/client.go | 54 ++++++++--- 2 files changed, 126 insertions(+), 17 deletions(-) diff --git a/pkg/vault/confidentialspace/confidentialspace.go b/pkg/vault/confidentialspace/confidentialspace.go index 3123c399..acb7c4c6 100644 --- a/pkg/vault/confidentialspace/confidentialspace.go +++ b/pkg/vault/confidentialspace/confidentialspace.go @@ -2,6 +2,9 @@ package confidentialspace import ( "context" + "crypto/ed25519" + "encoding/base64" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -9,6 +12,7 @@ import ( "net" "os" "path/filepath" + "strings" "sync" tz "github.com/ecadlabs/gotez/v2" @@ -72,11 +76,57 @@ func (e *encryptedKey) UnmarshalJSON(data []byte) error { return nil } +func parseEd25519PublicKey(keyStr string) (ed25519.PublicKey, error) { + if keyStr == "" { + return nil, errors.New("empty public key") + } + data, err := hex.DecodeString(keyStr) + if err != nil { + data, err = base64.StdEncoding.DecodeString(keyStr) + if err != nil { + return nil, fmt.Errorf("failed to decode public key as hex or base64: %w", err) + } + } + if len(data) != ed25519.PublicKeySize { + return nil, fmt.Errorf("invalid public key size: expected %d, got %d", ed25519.PublicKeySize, len(data)) + } + return ed25519.PublicKey(data), nil +} + +func loadClientPrivateKeyFromFile(path string) (ed25519.PrivateKey, error) { + if path == "" { + return nil, errors.New("empty identity file path") + } + path = os.ExpandEnv(path) + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read identity file: %w", err) + } + hexStr := string(data) + hexStr = strings.TrimSpace(hexStr) + if hexStr == "" { + return nil, errors.New("identity file is empty") + } + keyData, err := hex.DecodeString(hexStr) + if err != nil { + keyData, err = base64.StdEncoding.DecodeString(hexStr) + if err != nil { + return nil, fmt.Errorf("failed to decode private key as hex or base64: %w", err) + } + } + if len(keyData) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("invalid private key size: expected %d, got %d", ed25519.PrivateKeySize, len(keyData)) + } + return ed25519.PrivateKey(keyData), nil +} + /////////////////////////////////////////////////////////////////////////////////////////// type Config struct { ConfidentialSpaceHost string `yaml:"host"` ConfidentialSpacePort string `yaml:"port"` + ServerPublicKey string `yaml:"server_public_key"` // Ed25519 public key (hex or base64) + ClientPrivateKeyPath string `yaml:"client_private_key_path"` // Path to identity file containing Ed25519 private key WipProviderPath string `yaml:"wip_provider_path"` EncryptionKeyPath string `yaml:"encryption_key_path"` Storage *StorageConfig `yaml:"storage"` @@ -103,6 +153,8 @@ func populateConfig(c *Config) *Config { return &Config{ ConfidentialSpaceHost: resolve(c.ConfidentialSpaceHost, "CONFIDENTIAL_SPACE_HOST"), ConfidentialSpacePort: resolve(c.ConfidentialSpacePort, "CONFIDENTIAL_SPACE_PORT"), + ServerPublicKey: resolve(c.ServerPublicKey, "CONFIDENTIAL_SPACE_SERVER_PUBLIC_KEY"), + ClientPrivateKeyPath: resolve(c.ClientPrivateKeyPath, "CONFIDENTIAL_SPACE_CLIENT_PRIVATE_KEY_PATH"), WipProviderPath: resolve(c.WipProviderPath, "GCP_WIP_PROVIDER_PATH"), EncryptionKeyPath: resolve(c.EncryptionKeyPath, "GCP_KMS_ENCRYPTION_KEY_PATH"), Storage: c.Storage, @@ -337,6 +389,25 @@ func newWithStorage(ctx context.Context, config *Config, storage keyBlobStorage) return nil, errors.New("(ConfidentialSpace): missing encryption key path") } + var serverPublicKey ed25519.PublicKey + var clientPrivateKey ed25519.PrivateKey + var err error + if conf.ServerPublicKey != "" { + serverPublicKey, err = parseEd25519PublicKey(conf.ServerPublicKey) + if err != nil { + return nil, fmt.Errorf("(ConfidentialSpace): invalid server public key: %w", err) + } + + if conf.ClientPrivateKeyPath == "" { + return nil, errors.New("(ConfidentialSpace): client private key file path required when server public key is provided") + } + + clientPrivateKey, err = loadClientPrivateKeyFromFile(conf.ClientPrivateKeyPath) + if err != nil { + return nil, fmt.Errorf("(ConfidentialSpace): failed to load client private key from file: %w", err) + } + } + rpcCred := rpc.ConfidentialSpaceCredentials{ WipProviderPath: conf.WipProviderPath, EncryptionKeyPath: conf.EncryptionKeyPath, @@ -352,15 +423,27 @@ func newWithStorage(ctx context.Context, config *Config, storage keyBlobStorage) return nil, fmt.Errorf("(ConfidentialSpace): %w", err) } - v, err := newWithConn(ctx, conn, &rpcCred, storage) + v, err := newWithConn(ctx, conn, &rpcCred, storage, serverPublicKey, clientPrivateKey) if err != nil { return nil, err } return v, nil } -func newWithConn[C any](ctx context.Context, conn net.Conn, credentials *C, storage keyBlobStorage) (*ConfidentialSpaceVault[C], error) { - client := rpc.NewClient[C](conn) +func newWithConn[C any](ctx context.Context, conn net.Conn, credentials *C, storage keyBlobStorage, serverPublicKey ed25519.PublicKey, clientPrivateKey ed25519.PrivateKey) (*ConfidentialSpaceVault[C], error) { + var client *rpc.Client[C] + var err error + + if serverPublicKey != nil { + client, err = rpc.NewSecureClient[C](conn, serverPublicKey, clientPrivateKey) + if err != nil { + conn.Close() + return nil, fmt.Errorf("(ConfidentialSpace): failed to establish secure connection: %w", err) + } + } else { + client = rpc.NewClient[C](conn) + } + client.Logger = log.StandardLogger() if err := client.Initialize(ctx, credentials); err != nil { diff --git a/pkg/vault/confidentialspace/rpc/client.go b/pkg/vault/confidentialspace/rpc/client.go index 43fa38cf..593a7df0 100644 --- a/pkg/vault/confidentialspace/rpc/client.go +++ b/pkg/vault/confidentialspace/rpc/client.go @@ -2,11 +2,13 @@ package rpc import ( "context" + "crypto/ed25519" "encoding/binary" "io" "net" "time" + "github.com/ecadlabs/signatory/pkg/utils/secureconn" "github.com/ecadlabs/signatory/pkg/vault" "github.com/fxamacker/cbor/v2" "github.com/kr/pretty" @@ -21,6 +23,14 @@ func NewClient[C any](conn net.Conn) *Client[C] { return &Client[C]{conn: conn} } +func NewSecureClient[C any](conn net.Conn, serverPublicKey ed25519.PublicKey, clientPrivateKey ed25519.PrivateKey) (*Client[C], error) { + secureConn, err := secureconn.WrapConnection(conn, serverPublicKey, clientPrivateKey) + if err != nil { + return nil, err + } + return &Client[C]{conn: secureConn}, nil +} + func (c *Client[C]) Close() error { return c.conn.Close() } @@ -71,22 +81,38 @@ func RoundTripRaw[T, C any](ctx context.Context, conn net.Conn, req *Request[C], conn.SetDeadline(time.Time{}) }() - wrBuf := make([]byte, len(reqBuf)+4) - binary.BigEndian.PutUint32(wrBuf, uint32(len(reqBuf))) - copy(wrBuf[4:], reqBuf) - if _, err := conn.Write(wrBuf); err != nil { - return res, err - } + _, isSecureConn := conn.(*secureconn.SecureConn) - var lenBuf [4]byte - if _, err := io.ReadFull(conn, lenBuf[:]); err != nil { - return res, err - } - rBuf := make([]byte, int(binary.BigEndian.Uint32(lenBuf[:]))) - if _, err := io.ReadFull(conn, rBuf); err != nil { - return res, err + if isSecureConn { + if _, err := conn.Write(reqBuf); err != nil { + return res, err + } + + rBuf := make([]byte, 65536) + n, readErr := conn.Read(rBuf) + if readErr != nil { + return res, readErr + } + err = cbor.Unmarshal(rBuf[:n], &res) + } else { + wrBuf := make([]byte, len(reqBuf)+4) + binary.BigEndian.PutUint32(wrBuf, uint32(len(reqBuf))) + copy(wrBuf[4:], reqBuf) + if _, err := conn.Write(wrBuf); err != nil { + return res, err + } + + var lenBuf [4]byte + if _, err := io.ReadFull(conn, lenBuf[:]); err != nil { + return res, err + } + rBuf := make([]byte, int(binary.BigEndian.Uint32(lenBuf[:]))) + if _, err := io.ReadFull(conn, rBuf); err != nil { + return res, err + } + err = cbor.Unmarshal(rBuf, &res) } - err = cbor.Unmarshal(rBuf, &res) + if err == nil { debugLog(">>> %# v\n", pretty.Formatter(&res)) } From 7576a3f68a4b5af72247378a2d03c4b9ede5b165 Mon Sep 17 00:00:00 2001 From: Petel__ Date: Mon, 17 Nov 2025 17:42:33 -0800 Subject: [PATCH 2/3] add secure conn util functions --- pkg/utils/secureconn/secure_conn.go | 439 ++++++++++++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 pkg/utils/secureconn/secure_conn.go diff --git a/pkg/utils/secureconn/secure_conn.go b/pkg/utils/secureconn/secure_conn.go new file mode 100644 index 00000000..74e46f57 --- /dev/null +++ b/pkg/utils/secureconn/secure_conn.go @@ -0,0 +1,439 @@ +package secureconn + +import ( + "crypto/cipher" + "crypto/ecdh" + "crypto/ed25519" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + "math/big" + "net" + "time" + + "github.com/fxamacker/cbor/v2" + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/chacha20" + "golang.org/x/crypto/chacha20poly1305" +) + +const ( + tagSuite = "SIGNATORY_SECURE_CONNECTION_X25519_ED25519" + tagSecret = "DH_SECRET" + tagEphKeys = "EPHEMERAL_PUBLIC_KEYS_XOR_COMBINED" + tagAuth = "AUTHENTICATION_PUBLIC_KEYS_XOR_COMBINED" + tagLen = "SIGNATORY_SECURE_CONNECTION_LENGTH_KEY" + tagPayload = "SIGNATORY_SECURE_CONNECTION_PAYLOAD_KEY" +) + +const ( + granularity = 64 + maxMessageSize = 65536 +) + +type SecureConn struct { + conn net.Conn + readCipher, writeCipher packetCipher + remotePub ed25519.PublicKey + sessionID []byte + readBuf []byte +} + +type sessionKeys struct { + rdLength []byte + rdPayload []byte + wrLength []byte + wrPayload []byte +} + +type helloMessage struct { + _ struct{} `cbor:",toarray"` + EphemeralPublicKey []byte + AuthPublicKey *ed25519.PublicKey +} + +type authMessage struct { + _ struct{} `cbor:",toarray"` + ChallengeSignature *[]byte +} + +func curve() ecdh.Curve { return ecdh.X25519() } + +func combineKeys(a, b []byte) []byte { + if len(a) != len(b) { + panic("inconsistent key lengths") + } + out := make([]byte, len(a)) + for i := range len(a) { + out[i] = a[i] ^ b[i] + } + return out +} + +func genSingle(prk []byte, info []byte, tag string) []byte { + kdf, _ := blake2b.New256(prk) + kdf.Write([]byte(tag)) + kdf.Write(info) + return kdf.Sum(nil) +} + +func generateKeys(localEph, remoteEph *ecdh.PublicKey, secret []byte) sessionKeys { + loc := new(big.Int).SetBytes(localEph.Bytes()) + rem := new(big.Int).SetBytes(remoteEph.Bytes()) + + rDiff := new(big.Int).Sub(loc, rem) + wDiff := new(big.Int).Neg(rDiff) + + rSign := byte(0) + if rDiff.Sign() < 0 { + rSign = 1 + } + wSign := byte(0) + if wDiff.Sign() < 0 { + wSign = 1 + } + + rBytesRaw := rDiff.Bytes() + wBytesRaw := wDiff.Bytes() + + rBytes := make([]byte, 1+len(rBytesRaw)) + wBytes := make([]byte, 1+len(wBytesRaw)) + rBytes[0] = rSign + wBytes[0] = wSign + copy(rBytes[1:], rBytesRaw) + copy(wBytes[1:], wBytesRaw) + + prk := blake2b.Sum256(secret) + + keys := sessionKeys{ + rdLength: genSingle(prk[:], rBytes, tagLen), + rdPayload: genSingle(prk[:], rBytes, tagPayload), + wrLength: genSingle(prk[:], wBytes, tagLen), + wrPayload: genSingle(prk[:], wBytes, tagPayload), + } + + return keys +} + +type encodedReadWriter interface { + ReadMessage(v any) error + WriteMessage(v any) error +} + +type cborConn struct { + conn net.Conn +} + +func (c *cborConn) ReadMessage(v any) error { + var lenBuf [4]byte + if _, err := io.ReadFull(c.conn, lenBuf[:]); err != nil { + return err + } + + msgLen := binary.BigEndian.Uint32(lenBuf[:]) + if msgLen > maxMessageSize { + return fmt.Errorf("message too large: %d", msgLen) + } + + data := make([]byte, msgLen) + if _, err := io.ReadFull(c.conn, data); err != nil { + return err + } + + return cbor.Unmarshal(data, v) +} + +func (c *cborConn) WriteMessage(v any) error { + data, err := cbor.Marshal(v) + if err != nil { + return err + } + + lenBuf := make([]byte, 4) + binary.BigEndian.PutUint32(lenBuf, uint32(len(data))) + + if _, err := c.conn.Write(lenBuf); err != nil { + return err + } + if _, err := c.conn.Write(data); err != nil { + return err + } + return nil +} + +func exchange[T any, C encodedReadWriter](c C, data *T) (out *T, err error) { + out = new(T) + errCh := make(chan error, 2) + + go func() { + errCh <- c.WriteMessage(data) + }() + + go func() { + errCh <- c.ReadMessage(out) + }() + + for range 2 { + e := <-errCh + if err == nil { + err = e + } + } + return +} + +func WrapConnection(conn net.Conn, serverPublicKey ed25519.PublicKey, clientPrivateKey ed25519.PrivateKey) (net.Conn, error) { + return NewSecureConn(conn, clientPrivateKey, serverPublicKey) +} + +func NewSecureConn(transport net.Conn, localKey ed25519.PrivateKey, expectedRemoteKey ed25519.PublicKey) (*SecureConn, error) { + eph, err := curve().GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate ephemeral key: %w", err) + } + + rawConn := &cborConn{conn: transport} + + localPub := localKey.Public().(ed25519.PublicKey) + + helloResult, err := exchange(rawConn, &helloMessage{ + EphemeralPublicKey: eph.PublicKey().Bytes(), + AuthPublicKey: &localPub, + }) + if err != nil { + return nil, fmt.Errorf("hello exchange failed: %w", err) + } + + if helloResult.AuthPublicKey == nil || len(*helloResult.AuthPublicKey) == 0 || len(helloResult.EphemeralPublicKey) == 0 { + return nil, errors.New("invalid handshake message") + } + + if !helloResult.AuthPublicKey.Equal(expectedRemoteKey) { + return nil, errors.New("remote public key mismatch") + } + + remoteEphPub, err := curve().NewPublicKey(helloResult.EphemeralPublicKey) + if err != nil { + return nil, fmt.Errorf("invalid remote ephemeral key: %w", err) + } + remotePub := helloResult.AuthPublicKey + + secret, err := eph.ECDH(remoteEphPub) + if err != nil { + return nil, fmt.Errorf("ECDH failed: %w", err) + } + + combinedEphKeys := combineKeys(eph.PublicKey().Bytes(), remoteEphPub.Bytes()) + combinedAuthKeys := combineKeys(localPub[:], (*remotePub)[:]) + + ch, _ := blake2b.New256(nil) + ch.Write([]byte(tagSuite)) + ch.Write([]byte(tagEphKeys)) + ch.Write(combinedEphKeys) + ch.Write([]byte(tagAuth)) + ch.Write(combinedAuthKeys) + ch.Write([]byte(tagSecret)) + ch.Write(secret) + challenge := ch.Sum(nil) + + sig := ed25519.Sign(localKey, challenge) + + authResult, err := exchange(rawConn, &authMessage{ + ChallengeSignature: &sig, + }) + if err != nil { + return nil, fmt.Errorf("auth exchange failed: %w", err) + } + + if authResult.ChallengeSignature == nil || len(*authResult.ChallengeSignature) == 0 { + return nil, errors.New("invalid auth message") + } + + if !ed25519.Verify((*remotePub), challenge, *authResult.ChallengeSignature) { + return nil, errors.New("authentication failed: signature verification failed") + } + + keys := generateKeys(eph.PublicKey(), remoteEphPub, secret) + + sc := &SecureConn{ + conn: transport, + readCipher: newPacketCipher(keys.rdLength, keys.rdPayload), + writeCipher: newPacketCipher(keys.wrLength, keys.wrPayload), + remotePub: (*remotePub), + sessionID: challenge, + } + + return sc, nil +} + +type packetCipher struct { + lengthKey []byte + payloadCipher cipher.AEAD + buf []byte + nonce uint64 +} + +func newPacketCipher(lengthKey, payloadKey []byte) packetCipher { + plCipher, err := chacha20poly1305.New(payloadKey) + if err != nil { + panic(err) + } + + return packetCipher{ + lengthKey: lengthKey, + payloadCipher: plCipher, + } +} + +func (p *packetCipher) readPacket(r io.Reader) ([]byte, error) { + var nonce [12]byte + binary.BigEndian.PutUint64(nonce[:], p.nonce) + + var encLengthBuf [4]byte + if _, err := io.ReadFull(r, encLengthBuf[:]); err != nil { + return nil, err + } + + lc, err := chacha20.NewUnauthenticatedCipher(p.lengthKey, nonce[:]) + if err != nil { + panic(err) + } + var lengthBuf [4]byte + lc.XORKeyStream(lengthBuf[:], encLengthBuf[:]) + length := int(binary.BigEndian.Uint32(lengthBuf[:])) + + if length < chacha20poly1305.Overhead+4 { + return nil, errors.New("packet is too short") + } + + if len(p.buf) < length { + p.buf = make([]byte, length) + } + payload := p.buf[:length] + if _, err := io.ReadFull(r, payload); err != nil { + return nil, err + } + + if _, err = p.payloadCipher.Open(payload[:0], nonce[:], payload, encLengthBuf[:]); err != nil { + return nil, err + } + + p.nonce++ + + unpaddedLength := int(binary.BigEndian.Uint32(payload[:4])) + if unpaddedLength > length-4-chacha20poly1305.Overhead { + return nil, errors.New("invalid unpadded length") + } + + result := payload[4 : 4+unpaddedLength] + return result, nil +} + +func (p *packetCipher) writePacket(w io.Writer, data []byte) error { + var nonce [12]byte + binary.BigEndian.PutUint64(nonce[:], p.nonce) + + total := 4 + 4 + len(data) + chacha20poly1305.Overhead + padded := (total + granularity - 1) &^ (granularity - 1) + + if len(p.buf) < padded { + p.buf = make([]byte, padded) + } + + length := padded - 4 + dataLen := length - chacha20poly1305.Overhead + + lc, err := chacha20.NewUnauthenticatedCipher(p.lengthKey, nonce[:]) + if err != nil { + panic(err) + } + binary.BigEndian.PutUint32(p.buf[:4], uint32(length)) + lc.XORKeyStream(p.buf[:4], p.buf[:4]) + + payload := p.buf[4 : 4+dataLen] + binary.BigEndian.PutUint32(payload[:4], uint32(len(data))) + copy(payload[4:], data) + + toPad := payload[4+len(data) : dataLen] + if len(toPad) != 0 { + rand.Read(toPad) + } + + p.payloadCipher.Seal(payload[:0], nonce[:], payload, p.buf[:4]) + + packet := p.buf[:padded] + if _, err = w.Write(packet); err != nil { + return err + } + + p.nonce++ + return nil +} + +func (sc *SecureConn) ReadPacket() ([]byte, error) { + return sc.readCipher.readPacket(sc.conn) +} + +func (sc *SecureConn) WritePacket(data []byte) error { + return sc.writeCipher.writePacket(sc.conn, data) +} + +func (sc *SecureConn) Read(b []byte) (int, error) { + if len(sc.readBuf) > 0 { + n := copy(b, sc.readBuf) + sc.readBuf = sc.readBuf[n:] + return n, nil + } + + data, err := sc.ReadPacket() + if err != nil { + return 0, err + } + + n := copy(b, data) + if n < len(data) { + sc.readBuf = data[n:] + } + return n, nil +} + +func (sc *SecureConn) Write(b []byte) (int, error) { + if err := sc.WritePacket(b); err != nil { + return 0, err + } + return len(b), nil +} + +func (sc *SecureConn) RemotePublicKey() ed25519.PublicKey { + return sc.remotePub +} + +func (sc *SecureConn) SessionID() []byte { + return sc.sessionID +} + +func (sc *SecureConn) Close() error { + return sc.conn.Close() +} + +func (sc *SecureConn) LocalAddr() net.Addr { + return sc.conn.LocalAddr() +} + +func (sc *SecureConn) RemoteAddr() net.Addr { + return sc.conn.RemoteAddr() +} + +func (sc *SecureConn) SetDeadline(t time.Time) error { + return sc.conn.SetDeadline(t) +} + +func (sc *SecureConn) SetReadDeadline(t time.Time) error { + return sc.conn.SetReadDeadline(t) +} + +func (sc *SecureConn) SetWriteDeadline(t time.Time) error { + return sc.conn.SetWriteDeadline(t) +} From 983a2160ff6aa4588c7c11c36361c5b723fd2c01 Mon Sep 17 00:00:00 2001 From: Petel__ Date: Fri, 21 Nov 2025 11:47:23 -0800 Subject: [PATCH 3/3] update due to changes in the computation of derived keys --- pkg/utils/secureconn/secure_conn.go | 37 +++++++++++------------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/pkg/utils/secureconn/secure_conn.go b/pkg/utils/secureconn/secure_conn.go index 74e46f57..13d21d5f 100644 --- a/pkg/utils/secureconn/secure_conn.go +++ b/pkg/utils/secureconn/secure_conn.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "math/big" "net" "time" @@ -79,31 +78,23 @@ func genSingle(prk []byte, info []byte, tag string) []byte { return kdf.Sum(nil) } -func generateKeys(localEph, remoteEph *ecdh.PublicKey, secret []byte) sessionKeys { - loc := new(big.Int).SetBytes(localEph.Bytes()) - rem := new(big.Int).SetBytes(remoteEph.Bytes()) - - rDiff := new(big.Int).Sub(loc, rem) - wDiff := new(big.Int).Neg(rDiff) - - rSign := byte(0) - if rDiff.Sign() < 0 { - rSign = 1 +func twoComplementSub(a, b []byte) []byte { + if len(a) != len(b) { + panic("inconsistent key lengths") } - wSign := byte(0) - if wDiff.Sign() < 0 { - wSign = 1 + out := make([]byte, len(a)) + var borrow uint + for i := len(a) - 1; i >= 0; i-- { + diff := uint(a[i]) - uint(b[i]) - borrow + out[i] = byte(diff) + borrow = (diff >> 8) & 1 } + return out +} - rBytesRaw := rDiff.Bytes() - wBytesRaw := wDiff.Bytes() - - rBytes := make([]byte, 1+len(rBytesRaw)) - wBytes := make([]byte, 1+len(wBytesRaw)) - rBytes[0] = rSign - wBytes[0] = wSign - copy(rBytes[1:], rBytesRaw) - copy(wBytes[1:], wBytesRaw) +func generateKeys(localEph, remoteEph *ecdh.PublicKey, secret []byte) sessionKeys { + rBytes := twoComplementSub(localEph.Bytes(), remoteEph.Bytes()) + wBytes := twoComplementSub(remoteEph.Bytes(), localEph.Bytes()) prk := blake2b.Sum256(secret)