305 lines
7.2 KiB
Go
305 lines
7.2 KiB
Go
package lafsession
|
|
|
|
import (
|
|
"crypto/ecdh"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"hash"
|
|
)
|
|
|
|
const (
|
|
ProtocolVersion uint16 = 1
|
|
CipherSuiteX25519ChaCha20Poly1305 uint16 = 1
|
|
|
|
msgClientHello byte = 1
|
|
msgServerHello byte = 2
|
|
msgClientFinished byte = 3
|
|
msgServerFinished byte = 4
|
|
|
|
ephPubLen = 32
|
|
idPubLen = 32
|
|
sigLen = ed25519.SignatureSize // 64
|
|
|
|
// type|ver|suite|eph|id|sig
|
|
helloMsgLen = 1 + 2 + 2 + ephPubLen + idPubLen + sigLen // 133
|
|
// type|sig
|
|
finishedMsgLen = 1 + sigLen // 65
|
|
|
|
// CH+SH+CF+SF upper bound
|
|
transcriptCap = helloMsgLen*2 + finishedMsgLen*2 // 396
|
|
)
|
|
|
|
// HandshakeRole is which side we act as.
|
|
type HandshakeRole int
|
|
|
|
const (
|
|
RoleClient HandshakeRole = iota
|
|
RoleServer
|
|
)
|
|
|
|
type peerAcceptFunc func(peerPub []byte) bool
|
|
|
|
type handshakeState struct {
|
|
role HandshakeRole
|
|
identity *Identity
|
|
acceptPeer peerAcceptFunc
|
|
|
|
ephPriv *ecdh.PrivateKey
|
|
ephPub [ephPubLen]byte
|
|
|
|
peerEphPub [ephPubLen]byte
|
|
peerIDPub [idPubLen]byte
|
|
hasPeer bool
|
|
|
|
transcript []byte
|
|
hasher hash.Hash // running SHA-256 over transcript; kept in sync with transcript
|
|
hashBuf [32]byte
|
|
labelBuf [1 + 32]byte // finished label: type || transcriptHash
|
|
msgBuf []byte // reusable outbound message buffer
|
|
|
|
version uint16
|
|
suite uint16
|
|
}
|
|
|
|
func newHandshake(role HandshakeRole, id *Identity, accept peerAcceptFunc) (*handshakeState, error) {
|
|
curve := ecdh.X25519()
|
|
eph, err := curve.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
h := &handshakeState{
|
|
role: role,
|
|
identity: id,
|
|
acceptPeer: accept,
|
|
ephPriv: eph,
|
|
transcript: make([]byte, 0, transcriptCap),
|
|
hasher: sha256.New(),
|
|
msgBuf: make([]byte, 0, helloMsgLen),
|
|
version: ProtocolVersion,
|
|
suite: CipherSuiteX25519ChaCha20Poly1305,
|
|
}
|
|
copy(h.ephPub[:], eph.PublicKey().Bytes())
|
|
return h, nil
|
|
}
|
|
|
|
func (h *handshakeState) resetHasherFromTranscript() {
|
|
h.hasher.Reset()
|
|
_, _ = h.hasher.Write(h.transcript)
|
|
}
|
|
|
|
func (h *handshakeState) transcriptSum(dst []byte) []byte {
|
|
// Sum into hashBuf then copy to dst if needed; Sum allocates if dst too small
|
|
return h.hasher.Sum(h.hashBuf[:0])
|
|
}
|
|
|
|
func (h *handshakeState) writeTranscript(p []byte) {
|
|
h.transcript = append(h.transcript, p...)
|
|
_, _ = h.hasher.Write(p)
|
|
}
|
|
|
|
// buildClientHello: type | ver | suite | ephPub | idPub | sig
|
|
func (h *handshakeState) buildClientHello() ([]byte, error) {
|
|
if h.role != RoleClient {
|
|
return nil, ErrHandshakeState
|
|
}
|
|
msg := h.msgBuf[:0]
|
|
msg = append(msg, msgClientHello)
|
|
msg = binary.BigEndian.AppendUint16(msg, h.version)
|
|
msg = binary.BigEndian.AppendUint16(msg, h.suite)
|
|
msg = append(msg, h.ephPub[:]...)
|
|
msg = append(msg, h.identity.Public...)
|
|
|
|
baseLen := len(msg)
|
|
h.writeTranscript(msg)
|
|
sum := h.transcriptSum(nil)
|
|
sig := h.identity.Sign(sum)
|
|
msg = append(msg, sig...)
|
|
h.writeTranscript(sig)
|
|
h.msgBuf = msg
|
|
// return a copy so caller's network write is independent of msgBuf reuse
|
|
out := make([]byte, len(msg))
|
|
copy(out, msg)
|
|
_ = baseLen
|
|
return out, nil
|
|
}
|
|
|
|
func (h *handshakeState) processClientHello(msg []byte) error {
|
|
if h.role != RoleServer {
|
|
return ErrHandshakeState
|
|
}
|
|
if len(msg) < helloMsgLen {
|
|
return ErrShortMessage
|
|
}
|
|
if msg[0] != msgClientHello {
|
|
return ErrHandshakeState
|
|
}
|
|
ver := binary.BigEndian.Uint16(msg[1:3])
|
|
suite := binary.BigEndian.Uint16(msg[3:5])
|
|
if ver != ProtocolVersion {
|
|
return ErrBadVersion
|
|
}
|
|
if suite != CipherSuiteX25519ChaCha20Poly1305 {
|
|
return ErrBadCipher
|
|
}
|
|
off := 5
|
|
peerEph := msg[off : off+ephPubLen]
|
|
off += ephPubLen
|
|
peerID := msg[off : off+idPubLen]
|
|
off += idPubLen
|
|
sig := msg[off : off+sigLen]
|
|
|
|
if h.acceptPeer != nil && !h.acceptPeer(peerID) {
|
|
return ErrIdentityRejected
|
|
}
|
|
|
|
base := msg[:5+ephPubLen+idPubLen]
|
|
// Verify against hash(base) without permanently writing sig yet
|
|
h.hasher.Reset()
|
|
_, _ = h.hasher.Write(base)
|
|
sum := h.transcriptSum(nil)
|
|
if !VerifyPeer(peerID, sum, sig) {
|
|
h.hasher.Reset()
|
|
return ErrBadSignature
|
|
}
|
|
|
|
h.version = ver
|
|
h.suite = suite
|
|
copy(h.peerEphPub[:], peerEph)
|
|
copy(h.peerIDPub[:], peerID)
|
|
h.hasPeer = true
|
|
|
|
// hasher already holds base from verify; only append sig
|
|
h.transcript = append(h.transcript[:0], base...)
|
|
h.transcript = append(h.transcript, sig...)
|
|
_, _ = h.hasher.Write(sig)
|
|
return nil
|
|
}
|
|
|
|
func (h *handshakeState) buildServerHello() ([]byte, error) {
|
|
if h.role != RoleServer {
|
|
return nil, ErrHandshakeState
|
|
}
|
|
msg := h.msgBuf[:0]
|
|
msg = append(msg, msgServerHello)
|
|
msg = binary.BigEndian.AppendUint16(msg, h.version)
|
|
msg = binary.BigEndian.AppendUint16(msg, h.suite)
|
|
msg = append(msg, h.ephPub[:]...)
|
|
msg = append(msg, h.identity.Public...)
|
|
|
|
h.writeTranscript(msg)
|
|
sum := h.transcriptSum(nil)
|
|
sig := h.identity.Sign(sum)
|
|
msg = append(msg, sig...)
|
|
h.writeTranscript(sig)
|
|
h.msgBuf = msg
|
|
|
|
out := make([]byte, len(msg))
|
|
copy(out, msg)
|
|
return out, nil
|
|
}
|
|
|
|
func (h *handshakeState) processServerHello(msg []byte) error {
|
|
if h.role != RoleClient {
|
|
return ErrHandshakeState
|
|
}
|
|
if len(msg) < helloMsgLen {
|
|
return ErrShortMessage
|
|
}
|
|
if msg[0] != msgServerHello {
|
|
return ErrHandshakeState
|
|
}
|
|
ver := binary.BigEndian.Uint16(msg[1:3])
|
|
suite := binary.BigEndian.Uint16(msg[3:5])
|
|
if ver != ProtocolVersion {
|
|
return ErrBadVersion
|
|
}
|
|
if suite != CipherSuiteX25519ChaCha20Poly1305 {
|
|
return ErrBadCipher
|
|
}
|
|
off := 5
|
|
peerEph := msg[off : off+ephPubLen]
|
|
off += ephPubLen
|
|
peerID := msg[off : off+idPubLen]
|
|
off += idPubLen
|
|
sig := msg[off : off+sigLen]
|
|
|
|
if h.acceptPeer != nil && !h.acceptPeer(peerID) {
|
|
return ErrIdentityRejected
|
|
}
|
|
|
|
base := msg[:5+ephPubLen+idPubLen]
|
|
// Temporarily extend transcript for verify; roll back on failure
|
|
tLen := len(h.transcript)
|
|
h.writeTranscript(base)
|
|
sum := h.transcriptSum(nil)
|
|
if !VerifyPeer(peerID, sum, sig) {
|
|
h.transcript = h.transcript[:tLen]
|
|
h.resetHasherFromTranscript()
|
|
return ErrBadSignature
|
|
}
|
|
|
|
copy(h.peerEphPub[:], peerEph)
|
|
copy(h.peerIDPub[:], peerID)
|
|
h.hasPeer = true
|
|
h.writeTranscript(sig)
|
|
return nil
|
|
}
|
|
|
|
func (h *handshakeState) buildFinished(msgType byte) ([]byte, error) {
|
|
sum := h.transcriptSum(nil)
|
|
h.labelBuf[0] = msgType
|
|
copy(h.labelBuf[1:], sum)
|
|
sig := h.identity.Sign(h.labelBuf[:])
|
|
msg := h.msgBuf[:0]
|
|
msg = append(msg, msgType)
|
|
msg = append(msg, sig...)
|
|
h.writeTranscript(msg)
|
|
h.msgBuf = msg
|
|
|
|
out := make([]byte, len(msg))
|
|
copy(out, msg)
|
|
return out, nil
|
|
}
|
|
|
|
func (h *handshakeState) processFinished(msg []byte, expectedType byte, peerID []byte) error {
|
|
if len(msg) < finishedMsgLen {
|
|
return ErrShortMessage
|
|
}
|
|
if msg[0] != expectedType {
|
|
return ErrHandshakeState
|
|
}
|
|
sig := msg[1 : 1+sigLen]
|
|
sum := h.transcriptSum(nil)
|
|
h.labelBuf[0] = expectedType
|
|
copy(h.labelBuf[1:], sum)
|
|
if !VerifyPeer(peerID, h.labelBuf[:], sig) {
|
|
return ErrBadSignature
|
|
}
|
|
h.writeTranscript(msg[:finishedMsgLen])
|
|
return nil
|
|
}
|
|
|
|
func (h *handshakeState) deriveKeys() (*trafficKeys, error) {
|
|
curve := ecdh.X25519()
|
|
peerPub, err := curve.NewPublicKey(h.peerEphPub[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
shared, err := h.ephPriv.ECDH(peerPub)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer zero(shared)
|
|
sum := h.transcriptSum(nil)
|
|
return deriveTrafficKeys(shared, sum, h.version, h.suite)
|
|
}
|
|
|
|
func (h *handshakeState) peerIDSlice() []byte {
|
|
if !h.hasPeer {
|
|
return nil
|
|
}
|
|
return h.peerIDPub[:]
|
|
}
|