From 8236f3afa2eca0aae6c5da9560301c04d882c81b Mon Sep 17 00:00:00 2001 From: Mathias Hall-Andersen Date: Tue, 27 Jun 2017 17:33:06 +0200 Subject: [PATCH] Implemented MAC1/2 calculation --- src/config.go | 2 +- src/constants.go | 16 +++++ src/cookie.go | 39 ---------- src/device.go | 18 ++--- src/keypair.go | 11 +-- src/logger.go | 35 +++++++++ src/macs_device.go | 161 ++++++++++++++++++++++++++++++++++++++++++ src/macs_peer.go | 73 +++++++++++++++++++ src/macs_test.go | 113 +++++++++++++++++++++++++++++ src/noise_protocol.go | 22 ++++-- src/noise_test.go | 22 +++--- src/peer.go | 11 +-- src/send.go | 16 +++-- 13 files changed, 454 insertions(+), 85 deletions(-) create mode 100644 src/constants.go delete mode 100644 src/cookie.go create mode 100644 src/logger.go create mode 100644 src/macs_device.go create mode 100644 src/macs_peer.go create mode 100644 src/macs_test.go diff --git a/src/config.go b/src/config.go index 8865194..cb7e9ef 100644 --- a/src/config.go +++ b/src/config.go @@ -81,7 +81,7 @@ func ipcSetOperation(device *Device, socket *bufio.ReadWriter) *IPCError { } case "listen_port": - _, err := fmt.Sscanf(value, "%ud", &device.listenPort) + _, err := fmt.Sscanf(value, "%ud", &device.address.Port) if err != nil { return &IPCError{Code: ipcErrorInvalidPort} } diff --git a/src/constants.go b/src/constants.go new file mode 100644 index 0000000..dc95379 --- /dev/null +++ b/src/constants.go @@ -0,0 +1,16 @@ +package main + +import ( + "time" +) + +const ( + RekeyAfterMessage = (1 << 64) - (1 << 16) - 1 + RekeyAfterTime = time.Second * 120 + RekeyAttemptTime = time.Second * 90 + RekeyTimeout = time.Second * 5 + RejectAfterTime = time.Second * 180 + RejectAfterMessage = (1 << 64) - (1 << 4) - 1 + KeepaliveTimeout = time.Second * 10 + CookieRefreshTime = time.Second * 2 +) diff --git a/src/cookie.go b/src/cookie.go deleted file mode 100644 index a6987a2..0000000 --- a/src/cookie.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "errors" - "golang.org/x/crypto/blake2s" -) - -func CalculateCookie(peer *Peer, msg []byte) { - size := len(msg) - - if size < blake2s.Size128*2 { - panic(errors.New("bug: message too short")) - } - - startMac1 := size - (blake2s.Size128 * 2) - startMac2 := size - blake2s.Size128 - - mac1 := msg[startMac1 : startMac1+blake2s.Size128] - mac2 := msg[startMac2 : startMac2+blake2s.Size128] - - peer.mutex.RLock() - defer peer.mutex.RUnlock() - - // set mac1 - - func() { - mac, _ := blake2s.New128(peer.macKey[:]) - mac.Write(msg[:startMac1]) - mac.Sum(mac1[:0]) - }() - - // set mac2 - - if peer.cookie != nil { - mac, _ := blake2s.New128(peer.cookie) - mac.Write(msg[:startMac2]) - mac.Sum(mac2[:0]) - } -} diff --git a/src/device.go b/src/device.go index 4b8cda0..b3484c5 100644 --- a/src/device.go +++ b/src/device.go @@ -1,25 +1,24 @@ package main import ( - "log" "net" "sync" ) type Device struct { mtu int - source *net.UDPAddr // UDP source address + fwMark uint32 + address *net.UDPAddr // UDP source address conn *net.UDPConn // UDP "connection" mutex sync.RWMutex - peers map[NoisePublicKey]*Peer - indices IndexTable privateKey NoisePrivateKey publicKey NoisePublicKey - fwMark uint32 - listenPort uint16 routingTable RoutingTable - logger log.Logger + indices IndexTable + log *Logger queueWorkOutbound chan *OutboundWorkQueueElement + peers map[NoisePublicKey]*Peer + mac MacStateDevice } func (device *Device) SetPrivateKey(sk NoisePrivateKey) { @@ -30,8 +29,9 @@ func (device *Device) SetPrivateKey(sk NoisePrivateKey) { device.privateKey = sk device.publicKey = sk.publicKey() + device.mac.Init(device.publicKey) - // do precomputations + // do DH precomputations for _, peer := range device.peers { h := &peer.handshake @@ -45,9 +45,9 @@ func (device *Device) Init() { device.mutex.Lock() defer device.mutex.Unlock() + device.log = NewLogger() device.peers = make(map[NoisePublicKey]*Peer) device.indices.Init() - device.listenPort = 0 device.routingTable.Reset() } diff --git a/src/keypair.go b/src/keypair.go index 53e123f..0b029ce 100644 --- a/src/keypair.go +++ b/src/keypair.go @@ -3,13 +3,16 @@ package main import ( "crypto/cipher" "sync" + "time" ) type KeyPair struct { - recv cipher.AEAD - recvNonce uint64 - send cipher.AEAD - sendNonce uint64 + recv cipher.AEAD + recvNonce uint64 + send cipher.AEAD + sendNonce uint64 + isInitiator bool + created time.Time } type KeyPairs struct { diff --git a/src/logger.go b/src/logger.go new file mode 100644 index 0000000..117fe5b --- /dev/null +++ b/src/logger.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" + "os" +) + +const ( + LogLevelError = iota + LogLevelInfo + LogLevelDebug +) + +type Logger struct { + Debug *log.Logger + Info *log.Logger + Error *log.Logger +} + +func NewLogger() *Logger { + logger := new(Logger) + logger.Debug = log.New(os.Stdout, + "DEBUG: ", + log.Ldate|log.Ltime|log.Lshortfile, + ) + logger.Info = log.New(os.Stdout, + "INFO: ", + log.Ldate|log.Ltime|log.Lshortfile, + ) + logger.Error = log.New(os.Stdout, + "ERROR: ", + log.Ldate|log.Ltime|log.Lshortfile, + ) + return logger +} diff --git a/src/macs_device.go b/src/macs_device.go new file mode 100644 index 0000000..730c361 --- /dev/null +++ b/src/macs_device.go @@ -0,0 +1,161 @@ +package main + +import ( + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "github.com/aead/chacha20poly1305" // Needed for XChaCha20Poly1305, TODO: + "golang.org/x/crypto/blake2s" + "net" + "sync" + "time" +) + +type MacStateDevice struct { + mutex sync.RWMutex + refreshed time.Time + secret [blake2s.Size]byte + keyMac1 [blake2s.Size]byte + xaead cipher.AEAD +} + +func (state *MacStateDevice) Init(pk NoisePublicKey) { + state.mutex.Lock() + defer state.mutex.Unlock() + func() { + hsh, _ := blake2s.New256(nil) + hsh.Write([]byte(WGLabelMAC1)) + hsh.Write(pk[:]) + hsh.Sum(state.keyMac1[:0]) + }() + state.xaead, _ = chacha20poly1305.NewXCipher(state.keyMac1[:]) + state.refreshed = time.Time{} // never +} + +func (state *MacStateDevice) CheckMAC1(msg []byte) bool { + size := len(msg) + startMac1 := size - (blake2s.Size128 * 2) + startMac2 := size - blake2s.Size128 + + var mac1 [blake2s.Size128]byte + func() { + mac, _ := blake2s.New128(state.keyMac1[:]) + mac.Write(msg[:startMac1]) + mac.Sum(mac1[:0]) + }() + + return hmac.Equal(mac1[:], msg[startMac1:startMac2]) +} + +func (state *MacStateDevice) CheckMAC2(msg []byte, addr *net.UDPAddr) bool { + state.mutex.RLock() + defer state.mutex.RUnlock() + + if time.Now().Sub(state.refreshed) > CookieRefreshTime { + return false + } + + // derive cookie key + + var cookie [blake2s.Size128]byte + func() { + port := [2]byte{byte(addr.Port >> 8), byte(addr.Port)} + mac, _ := blake2s.New128(state.secret[:]) + mac.Write(addr.IP) + mac.Write(port[:]) + mac.Sum(cookie[:0]) + }() + + // calculate mac of packet + + start := len(msg) - blake2s.Size128 + + var mac2 [blake2s.Size128]byte + func() { + mac, _ := blake2s.New128(cookie[:]) + mac.Write(msg[:start]) + mac.Sum(mac2[:0]) + }() + + return hmac.Equal(mac2[:], msg[start:]) +} + +func (device *Device) CreateMessageCookieReply(msg []byte, receiver uint32, addr *net.UDPAddr) (*MessageCookieReply, error) { + state := &device.mac + state.mutex.RLock() + + // refresh cookie secret + + if time.Now().Sub(state.refreshed) > CookieRefreshTime { + state.mutex.RUnlock() + state.mutex.Lock() + _, err := rand.Read(state.secret[:]) + if err != nil { + state.mutex.Unlock() + return nil, err + } + state.refreshed = time.Now() + state.mutex.Unlock() + state.mutex.RLock() + } + + // derive cookie key + + var cookie [blake2s.Size128]byte + func() { + port := [2]byte{byte(addr.Port >> 8), byte(addr.Port)} + mac, _ := blake2s.New128(state.secret[:]) + mac.Write(addr.IP) + mac.Write(port[:]) + mac.Sum(cookie[:0]) + }() + + // encrypt cookie + + size := len(msg) + + startMac1 := size - (blake2s.Size128 * 2) + startMac2 := size - blake2s.Size128 + + M := msg[startMac1:startMac2] + + reply := new(MessageCookieReply) + reply.Type = MessageCookieReplyType + reply.Receiver = receiver + _, err := rand.Read(reply.Nonce[:]) + if err != nil { + state.mutex.RUnlock() + return nil, err + } + state.xaead.Seal(reply.Cookie[:0], reply.Nonce[:], cookie[:], M) + state.mutex.RUnlock() + return reply, nil +} + +func (device *Device) ConsumeMessageCookieReply(msg *MessageCookieReply) bool { + + if msg.Type != MessageCookieReplyType { + return false + } + + // lookup peer + + lookup := device.indices.Lookup(msg.Receiver) + if lookup.handshake == nil { + return false + } + + // decrypt and store cookie + + var cookie [blake2s.Size128]byte + state := &lookup.peer.mac + state.mutex.Lock() + defer state.mutex.Unlock() + _, err := state.xaead.Open(cookie[:0], msg.Nonce[:], msg.Cookie[:], state.lastMac1[:]) + if err != nil { + return false + } + state.cookieSet = time.Now() + state.cookie = cookie + return true +} diff --git a/src/macs_peer.go b/src/macs_peer.go new file mode 100644 index 0000000..d70c8f3 --- /dev/null +++ b/src/macs_peer.go @@ -0,0 +1,73 @@ +package main + +import ( + "crypto/cipher" + "errors" + "github.com/aead/chacha20poly1305" // Needed for XChaCha20Poly1305, TODO: + "golang.org/x/crypto/blake2s" + "sync" + "time" +) + +type MacStatePeer struct { + mutex sync.RWMutex + cookieSet time.Time + cookie [blake2s.Size128]byte + lastMac1 [blake2s.Size128]byte + keyMac1 [blake2s.Size]byte + xaead cipher.AEAD +} + +func (state *MacStatePeer) Init(pk NoisePublicKey) { + state.mutex.Lock() + defer state.mutex.Unlock() + func() { + hsh, _ := blake2s.New256(nil) + hsh.Write([]byte(WGLabelMAC1)) + hsh.Write(pk[:]) + hsh.Sum(state.keyMac1[:0]) + }() + state.xaead, _ = chacha20poly1305.NewXCipher(state.keyMac1[:]) + state.cookieSet = time.Time{} // never +} + +func (state *MacStatePeer) AddMacs(msg []byte) { + size := len(msg) + + if size < blake2s.Size128*2 { + panic(errors.New("bug: message too short")) + } + + startMac1 := size - (blake2s.Size128 * 2) + startMac2 := size - blake2s.Size128 + + mac1 := msg[startMac1 : startMac1+blake2s.Size128] + mac2 := msg[startMac2 : startMac2+blake2s.Size128] + + state.mutex.Lock() + defer state.mutex.Unlock() + + // set mac1 + + func() { + mac, _ := blake2s.New128(state.keyMac1[:]) + mac.Write(msg[:startMac1]) + mac.Sum(state.lastMac1[:0]) + }() + copy(mac1, state.lastMac1[:]) + + // set mac2 + + if state.cookieSet.IsZero() { + return + } + if time.Now().Sub(state.cookieSet) > CookieRefreshTime { + state.cookieSet = time.Time{} + return + } + func() { + mac, _ := blake2s.New128(state.cookie[:]) + mac.Write(msg[:startMac2]) + mac.Sum(mac2[:0]) + }() +} diff --git a/src/macs_test.go b/src/macs_test.go new file mode 100644 index 0000000..a67ccfb --- /dev/null +++ b/src/macs_test.go @@ -0,0 +1,113 @@ +package main + +import ( + "bytes" + "net" + "testing" + "testing/quick" +) + +func TestMAC1(t *testing.T) { + dev1 := newDevice(t) + dev2 := newDevice(t) + + peer1 := dev2.NewPeer(dev1.privateKey.publicKey()) + peer2 := dev1.NewPeer(dev2.privateKey.publicKey()) + + assertEqual(t, peer1.mac.keyMac1[:], dev1.mac.keyMac1[:]) + assertEqual(t, peer2.mac.keyMac1[:], dev2.mac.keyMac1[:]) + + msg1 := make([]byte, 256) + copy(msg1, []byte("some content")) + peer1.mac.AddMacs(msg1) + if dev1.mac.CheckMAC1(msg1) == false { + t.Fatal("failed to verify mac1") + } +} + +func TestMACs(t *testing.T) { + assertion := func( + addr net.UDPAddr, + addrInvalid net.UDPAddr, + sk1 NoisePrivateKey, + sk2 NoisePrivateKey, + msg []byte, + receiver uint32, + ) bool { + var device1 Device + device1.Init() + device1.SetPrivateKey(sk1) + + var device2 Device + device2.Init() + device2.SetPrivateKey(sk2) + + peer1 := device2.NewPeer(device1.privateKey.publicKey()) + peer2 := device1.NewPeer(device2.privateKey.publicKey()) + + if addr.Port < 0 { + return true + } + addr.Port &= 0xffff + + if len(msg) < 32 { + return true + } + if bytes.Compare(peer1.mac.keyMac1[:], device1.mac.keyMac1[:]) != 0 { + return false + } + if bytes.Compare(peer2.mac.keyMac1[:], device2.mac.keyMac1[:]) != 0 { + return false + } + + device2.indices.Insert(receiver, IndexTableEntry{ + peer: peer1, + handshake: &peer1.handshake, + }) + + // test just MAC1 + + peer1.mac.AddMacs(msg) + if device1.mac.CheckMAC1(msg) == false { + return false + } + + // exchange cookie reply + + cr, err := device1.CreateMessageCookieReply(msg, receiver, &addr) + if err != nil { + return false + } + + if device2.ConsumeMessageCookieReply(cr) == false { + return false + } + + // test MAC1 + MAC2 + + peer1.mac.AddMacs(msg) + if device1.mac.CheckMAC1(msg) == false { + return false + } + if device1.mac.CheckMAC2(msg, &addr) == false { + return false + } + + // test invalid + + if device1.mac.CheckMAC2(msg, &addrInvalid) { + return false + } + msg[5] ^= 1 + if device1.mac.CheckMAC1(msg) { + return false + } + + return true + } + + err := quick.Check(assertion, nil) + if err != nil { + t.Error(err) + } +} diff --git a/src/noise_protocol.go b/src/noise_protocol.go index bf1db9b..e237dbe 100644 --- a/src/noise_protocol.go +++ b/src/noise_protocol.go @@ -24,15 +24,20 @@ const ( ) const ( - MessageInitiationType = 1 - MessageResponseType = 2 - MessageCookieResponseType = 3 - MessageTransportType = 4 + MessageInitiationType = 1 + MessageResponseType = 2 + MessageCookieReplyType = 3 + MessageTransportType = 4 +) + +const ( + MessageInitiationSize = 148 + MessageResponseSize = 92 ) /* Type is an 8-bit field, followed by 3 nul bytes, * by marshalling the messages in little-endian byteorder - * we can treat these as a 32-bit int + * we can treat these as a 32-bit unsigned int (for now) * */ @@ -63,6 +68,13 @@ type MessageTransport struct { Content []byte } +type MessageCookieReply struct { + Type uint32 + Receiver uint32 + Nonce [24]byte + Cookie [blake2s.Size128 + poly1305.TagSize]byte +} + type Handshake struct { state int mutex sync.Mutex diff --git a/src/noise_test.go b/src/noise_test.go index 8450c1c..dab603b 100644 --- a/src/noise_test.go +++ b/src/noise_test.go @@ -18,6 +18,17 @@ func assertEqual(t *testing.T, a []byte, b []byte) { } } +func newDevice(t *testing.T) *Device { + var device Device + sk, err := newPrivateKey() + if err != nil { + t.Fatal(err) + } + device.Init() + device.SetPrivateKey(sk) + return &device +} + func TestCurveWrappers(t *testing.T) { sk1, err := newPrivateKey() assertNil(t, err) @@ -36,17 +47,6 @@ func TestCurveWrappers(t *testing.T) { } } -func newDevice(t *testing.T) *Device { - var device Device - sk, err := newPrivateKey() - if err != nil { - t.Fatal(err) - } - device.Init() - device.SetPrivateKey(sk) - return &device -} - func TestNoiseHandshake(t *testing.T) { dev1 := newDevice(t) diff --git a/src/peer.go b/src/peer.go index 6a879cb..e192b12 100644 --- a/src/peer.go +++ b/src/peer.go @@ -2,7 +2,6 @@ package main import ( "errors" - "golang.org/x/crypto/blake2s" "net" "sync" "time" @@ -19,12 +18,10 @@ type Peer struct { keyPairs KeyPairs handshake Handshake device *Device - macKey [blake2s.Size]byte // Hash(Label-Mac1 || publicKey) - cookie []byte // cookie - cookieExpire time.Time queueInbound chan []byte queueOutbound chan *OutboundWorkQueueElement queueOutboundRouting chan []byte + mac MacStatePeer } func (device *Device) NewPeer(pk NoisePublicKey) *Peer { @@ -35,6 +32,7 @@ func (device *Device) NewPeer(pk NoisePublicKey) *Peer { peer.mutex.Lock() peer.device = device peer.keyPairs.Init() + peer.mac.Init(pk) peer.queueOutbound = make(chan *OutboundWorkQueueElement, OutboundQueueSize) // map public key @@ -53,11 +51,6 @@ func (device *Device) NewPeer(pk NoisePublicKey) *Peer { handshake.mutex.Lock() handshake.remoteStatic = pk handshake.precomputedStaticStatic = device.privateKey.sharedSecret(handshake.remoteStatic) - - // compute mac key - - peer.macKey = blake2s.Sum256(append([]byte(WGLabelMAC1[:]), handshake.remoteStatic[:]...)) - handshake.mutex.Unlock() peer.mutex.Unlock() diff --git a/src/send.go b/src/send.go index da5905d..f58d311 100644 --- a/src/send.go +++ b/src/send.go @@ -24,6 +24,10 @@ type OutboundWorkQueueElement struct { keyPair *KeyPair } +func (peer *Peer) HandshakeWorker(handshakeQueue []byte) { + +} + func (device *Device) SendPacket(packet []byte) { // lookup peer @@ -39,7 +43,7 @@ func (device *Device) SendPacket(packet []byte) { peer = device.routingTable.LookupIPv6(dst) default: - device.logger.Println("unknown IP version") + device.log.Debug.Println("receieved packet with unknown IP version") return } @@ -146,15 +150,13 @@ func (peer *Peer) RoutineOutboundNonceWorker() { func (peer *Peer) RoutineSequential() { for work := range peer.queueOutbound { work.wg.Wait() - - // check if dropped ("ghost packet") - if work.packet == nil { continue } - - // - + if peer.endpoint == nil { + continue + } + peer.device.conn.WriteToUDP(work.packet, peer.endpoint) } }