connection_state.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. package nebula
  2. import (
  3. "crypto/hkdf"
  4. "crypto/sha256"
  5. "encoding/json"
  6. "fmt"
  7. "log/slog"
  8. "strconv"
  9. "sync"
  10. "sync/atomic"
  11. "github.com/slackhq/nebula/cert"
  12. "github.com/slackhq/nebula/handshake"
  13. "github.com/slackhq/nebula/header"
  14. "github.com/slackhq/nebula/noiseutil"
  15. )
  16. const (
  17. ReplayWindow = 8192
  18. // RehandshakeAfterMessages rolls keys inside the AES-GCM data-volume margin (~2^-36 advantage at 64KB frames).
  19. RehandshakeAfterMessages = uint64(1) << 34
  20. // RejectAfterMessages is the nonce ceiling enforced by noiseutil; a tunnel here is deleted locally, not notified.
  21. RejectAfterMessages = noiseutil.RejectAfterMessages
  22. )
  23. // RehandshakeAfterMessages must stay below RejectAfterMessages so tunnels roll before the hard send stop.
  24. const _ = RejectAfterMessages - RehandshakeAfterMessages
  25. // sessionEpoch hands out a receiver-local ordinal to every ConnectionState at creation. The RX
  26. // staging sort (overlay/batch) orders packets by (epoch, message counter). A re-handshake never
  27. // rekeys an existing tunnel; it brings up a new hostinfo and ConnectionState with a counter space
  28. // starting near zero, while the old tunnel keeps decrypting until torn down. During that cutover
  29. // one flush batch can hold packets from both tunnels, and the epoch keeps the old tunnel's
  30. // packets sorted first.
  31. var sessionEpoch atomic.Uint64
  32. type ConnectionState struct {
  33. eKey noiseutil.CipherState
  34. dKey noiseutil.CipherState
  35. myCert cert.Certificate
  36. peerCert *cert.CachedCertificate
  37. initiator bool
  38. messageCounter atomic.Uint64
  39. window *Bits
  40. decryptLock sync.Mutex
  41. writeLock sync.Mutex
  42. // epoch is this session's sessionEpoch ordinal. Immutable after creation.
  43. epoch uint64
  44. }
  45. // newConnectionStateFromResult builds a fully-populated ConnectionState from a
  46. // completed handshake.Result. It seeds messageCounter and the replay window so
  47. // that the post-handshake message indices already used on the wire don't count
  48. // as missed traffic in the data plane.
  49. func newConnectionStateFromResult(r *handshake.Result) (*ConnectionState, error) {
  50. // Refuse a MessageIndex too big for the replay window: it can only be a bug, and would spin the seed loop below.
  51. if r.MessageIndex >= ReplayWindow {
  52. return nil, fmt.Errorf("handshake message index %d exceeds replay window", r.MessageIndex)
  53. }
  54. ci := &ConnectionState{
  55. myCert: r.MyCert,
  56. initiator: r.Initiator,
  57. peerCert: r.RemoteCert,
  58. eKey: noiseutil.NewCipherState(r.EKey, r.Cipher),
  59. dKey: noiseutil.NewCipherState(r.DKey, r.Cipher),
  60. window: NewBits(ReplayWindow),
  61. epoch: sessionEpoch.Add(1),
  62. }
  63. ci.messageCounter.Add(r.MessageIndex)
  64. for i := uint64(1); i <= r.MessageIndex; i++ {
  65. ci.window.Update(nil, i)
  66. }
  67. return ci, nil
  68. }
  69. // newLaneConnectionState derives multiport lane s's session from the base
  70. // tunnel's material. Each key is an HKDF expansion of the base tunnel's matching
  71. // key, labelled with the lane index, so the pair stays matched with no extra
  72. // negotiation: Noise leaves our send key equal to the peer's receive key, and
  73. // expanding both with the same label preserves that.
  74. //
  75. // The lane gets its own counter and replay window starting from zero. No
  76. // handshake messages were spent on it, so unlike the base session there is
  77. // nothing to seed.
  78. func newLaneConnectionState(m *laneMaterial, lane uint8) (*ConnectionState, error) {
  79. if lane == 0 {
  80. return nil, fmt.Errorf("lane 0 is the base session")
  81. }
  82. eKey, err := deriveLaneKey(m.eKey, lane)
  83. if err != nil {
  84. return nil, err
  85. }
  86. dKey, err := deriveLaneKey(m.dKey, lane)
  87. if err != nil {
  88. return nil, err
  89. }
  90. return &ConnectionState{
  91. myCert: m.myCert,
  92. initiator: m.initiator,
  93. peerCert: m.peerCert,
  94. eKey: noiseutil.NewCipherStateFromKey(eKey, m.cipher),
  95. dKey: noiseutil.NewCipherStateFromKey(dKey, m.cipher),
  96. window: NewBits(ReplayWindow),
  97. epoch: sessionEpoch.Add(1),
  98. }, nil
  99. }
  100. // deriveLaneKey expands a base tunnel key into the key for one lane.
  101. func deriveLaneKey(base [32]byte, lane uint8) ([32]byte, error) {
  102. var out [32]byte
  103. // The base key is already unique to this tunnel and direction, so the lane
  104. // index is the only thing that needs to vary; no salt is required.
  105. k, err := hkdf.Key(sha256.New, base[:], nil, laneKeyInfo+" "+strconv.Itoa(int(lane)), len(out))
  106. if err != nil {
  107. return out, err
  108. }
  109. copy(out[:], k)
  110. return out, nil
  111. }
  112. func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
  113. return json.Marshal(m{
  114. "certificate": cs.peerCert,
  115. "initiator": cs.initiator,
  116. "message_counter": cs.messageCounter.Load(),
  117. })
  118. }
  119. // NextMessageCounter reserves the next 1-based counter; RejectAfterMessages is the first we refuse, pinned to not wrap.
  120. func (cs *ConnectionState) NextMessageCounter() (uint64, bool) {
  121. c := cs.messageCounter.Add(1)
  122. if c >= RejectAfterMessages {
  123. cs.messageCounter.Store(RejectAfterMessages)
  124. return c, false
  125. }
  126. return c, true
  127. }
  128. func (cs *ConnectionState) Curve() cert.Curve {
  129. return cs.myCert.Curve()
  130. }
  131. func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) ([]byte, error) {
  132. cs.decryptLock.Lock()
  133. result := cs.window.Check(l, messageCounter)
  134. cs.decryptLock.Unlock()
  135. if !result {
  136. return nil, ErrAlreadySeen
  137. }
  138. out, err := cs.dKey.DecryptDanger(packet[header.Len:header.Len], packet[:header.Len], packet[header.Len:], messageCounter, nb)
  139. if err != nil {
  140. return nil, err
  141. }
  142. cs.decryptLock.Lock()
  143. result = cs.window.Update(l, messageCounter)
  144. cs.decryptLock.Unlock()
  145. if !result {
  146. return nil, ErrAlreadySeen
  147. }
  148. return out, nil
  149. }
  150. // noteSeen records a counter that some other session for the same keys already
  151. // accepted, so a packet doesn't become replayable just because the session that
  152. // decrypted it was thrown away. See laneSet.installSession, its only caller.
  153. func (cs *ConnectionState) noteSeen(l *slog.Logger, messageCounter uint64) {
  154. cs.decryptLock.Lock()
  155. cs.window.Update(l, messageCounter)
  156. cs.decryptLock.Unlock()
  157. }
  158. func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) error {
  159. cs.decryptLock.Lock()
  160. result := cs.window.Check(l, messageCounter)
  161. cs.decryptLock.Unlock()
  162. if !result {
  163. return ErrAlreadySeen
  164. }
  165. // The entire body is sent as AD, not encrypted.
  166. // The packet consists of a 16-byte parsed Nebula header, Associated Data-protected payload, and a trailing 16-byte AEAD signature value.
  167. // The packet is guaranteed to be at least 16 bytes at this point, b/c it got past the h.Parse() call above. If it's
  168. // otherwise malformed (meaning, there is no trailing 16 byte AEAD value), then this will result in at worst a 0-length slice
  169. // which will gracefully fail in the DecryptDanger call.
  170. signedPayload := packet[:len(packet)-cs.dKey.Overhead()]
  171. signatureValue := packet[len(packet)-cs.dKey.Overhead():]
  172. _, err := cs.dKey.DecryptDanger(nil, signedPayload, signatureValue, messageCounter, nb)
  173. if err != nil {
  174. return err
  175. }
  176. cs.decryptLock.Lock()
  177. result = cs.window.Update(l, messageCounter)
  178. cs.decryptLock.Unlock()
  179. if !result {
  180. return ErrAlreadySeen
  181. }
  182. return nil
  183. }