connection_state.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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 tunnel 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. //
  32. // It is drawn once per tunnel, by the base session: a tunnel's multiport lane sessions copy it
  33. // rather than draw their own.
  34. var sessionEpoch atomic.Uint64
  35. type ConnectionState struct {
  36. eKey noiseutil.CipherState
  37. dKey noiseutil.CipherState
  38. myCert cert.Certificate
  39. peerCert *cert.CachedCertificate
  40. initiator bool
  41. messageCounter atomic.Uint64
  42. window *Bits
  43. decryptLock sync.Mutex
  44. writeLock sync.Mutex
  45. // epoch is this session's sessionEpoch ordinal. Immutable after creation.
  46. epoch uint64
  47. }
  48. // newConnectionStateFromResult builds a fully-populated ConnectionState from a
  49. // completed handshake.Result. It seeds messageCounter and the replay window so
  50. // that the post-handshake message indices already used on the wire don't count
  51. // as missed traffic in the data plane.
  52. func newConnectionStateFromResult(r *handshake.Result) (*ConnectionState, error) {
  53. // Refuse a MessageIndex too big for the replay window: it can only be a bug, and would spin the seed loop below.
  54. if r.MessageIndex >= ReplayWindow {
  55. return nil, fmt.Errorf("handshake message index %d exceeds replay window", r.MessageIndex)
  56. }
  57. ci := &ConnectionState{
  58. myCert: r.MyCert,
  59. initiator: r.Initiator,
  60. peerCert: r.RemoteCert,
  61. eKey: noiseutil.NewCipherState(r.EKey, r.Cipher),
  62. dKey: noiseutil.NewCipherState(r.DKey, r.Cipher),
  63. window: NewBits(ReplayWindow),
  64. epoch: sessionEpoch.Add(1),
  65. }
  66. ci.messageCounter.Add(r.MessageIndex)
  67. for i := uint64(1); i <= r.MessageIndex; i++ {
  68. ci.window.Update(nil, i)
  69. }
  70. return ci, nil
  71. }
  72. // newLaneConnectionState derives multiport lane s's session from the base
  73. // tunnel's material. Each key is an HKDF expansion of the base tunnel's matching
  74. // key, labelled with the lane index, so the pair stays matched with no extra
  75. // negotiation: Noise leaves our send key equal to the peer's receive key, and
  76. // expanding both with the same label preserves that.
  77. //
  78. // The lane gets its own counter and replay window starting from zero. No
  79. // handshake messages were spent on it, so unlike the base session there is
  80. // nothing to seed. The epoch is the tunnel's, not the lane's; see sessionEpoch.
  81. func newLaneConnectionState(m *laneMaterial, lane uint8) (*ConnectionState, error) {
  82. if lane == 0 {
  83. return nil, fmt.Errorf("lane 0 is the base session")
  84. }
  85. eKey, err := deriveLaneKey(m.eKey, lane)
  86. if err != nil {
  87. return nil, err
  88. }
  89. dKey, err := deriveLaneKey(m.dKey, lane)
  90. if err != nil {
  91. return nil, err
  92. }
  93. return &ConnectionState{
  94. myCert: m.myCert,
  95. initiator: m.initiator,
  96. peerCert: m.peerCert,
  97. eKey: noiseutil.NewCipherStateFromKey(eKey, m.cipher),
  98. dKey: noiseutil.NewCipherStateFromKey(dKey, m.cipher),
  99. window: NewBits(ReplayWindow),
  100. epoch: m.epoch,
  101. }, nil
  102. }
  103. // deriveLaneKey expands a base tunnel key into the key for one lane.
  104. func deriveLaneKey(base [32]byte, lane uint8) ([32]byte, error) {
  105. var out [32]byte
  106. // The base key is already unique to this tunnel and direction, so the lane
  107. // index is the only thing that needs to vary; no salt is required.
  108. k, err := hkdf.Key(sha256.New, base[:], nil, laneKeyInfo+" "+strconv.Itoa(int(lane)), len(out))
  109. if err != nil {
  110. return out, err
  111. }
  112. copy(out[:], k)
  113. return out, nil
  114. }
  115. func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
  116. return json.Marshal(m{
  117. "certificate": cs.peerCert,
  118. "initiator": cs.initiator,
  119. "message_counter": cs.messageCounter.Load(),
  120. })
  121. }
  122. // NextMessageCounter reserves the next 1-based counter; RejectAfterMessages is the first we refuse, pinned to not wrap.
  123. func (cs *ConnectionState) NextMessageCounter() (uint64, bool) {
  124. c := cs.messageCounter.Add(1)
  125. if c >= RejectAfterMessages {
  126. cs.messageCounter.Store(RejectAfterMessages)
  127. return c, false
  128. }
  129. return c, true
  130. }
  131. func (cs *ConnectionState) Curve() cert.Curve {
  132. return cs.myCert.Curve()
  133. }
  134. func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) ([]byte, error) {
  135. cs.decryptLock.Lock()
  136. result := cs.window.Check(l, messageCounter)
  137. cs.decryptLock.Unlock()
  138. if !result {
  139. return nil, ErrAlreadySeen
  140. }
  141. out, err := cs.dKey.DecryptDanger(packet[header.Len:header.Len], packet[:header.Len], packet[header.Len:], messageCounter, nb)
  142. if err != nil {
  143. return nil, err
  144. }
  145. cs.decryptLock.Lock()
  146. result = cs.window.Update(l, messageCounter)
  147. cs.decryptLock.Unlock()
  148. if !result {
  149. return nil, ErrAlreadySeen
  150. }
  151. return out, nil
  152. }
  153. // noteSeen records a counter that some other session for the same keys already
  154. // accepted, so a packet doesn't become replayable just because the session that
  155. // decrypted it was thrown away. See laneSet.installSession, its only caller.
  156. func (cs *ConnectionState) noteSeen(l *slog.Logger, messageCounter uint64) {
  157. cs.decryptLock.Lock()
  158. cs.window.Update(l, messageCounter)
  159. cs.decryptLock.Unlock()
  160. }
  161. func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) error {
  162. cs.decryptLock.Lock()
  163. result := cs.window.Check(l, messageCounter)
  164. cs.decryptLock.Unlock()
  165. if !result {
  166. return ErrAlreadySeen
  167. }
  168. // The entire body is sent as AD, not encrypted.
  169. // The packet consists of a 16-byte parsed Nebula header, Associated Data-protected payload, and a trailing 16-byte AEAD signature value.
  170. // 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
  171. // otherwise malformed (meaning, there is no trailing 16 byte AEAD value), then this will result in at worst a 0-length slice
  172. // which will gracefully fail in the DecryptDanger call.
  173. signedPayload := packet[:len(packet)-cs.dKey.Overhead()]
  174. signatureValue := packet[len(packet)-cs.dKey.Overhead():]
  175. _, err := cs.dKey.DecryptDanger(nil, signedPayload, signatureValue, messageCounter, nb)
  176. if err != nil {
  177. return err
  178. }
  179. cs.decryptLock.Lock()
  180. result = cs.window.Update(l, messageCounter)
  181. cs.decryptLock.Unlock()
  182. if !result {
  183. return ErrAlreadySeen
  184. }
  185. return nil
  186. }