connection_state.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. package nebula
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log/slog"
  6. "sync"
  7. "sync/atomic"
  8. "github.com/slackhq/nebula/cert"
  9. "github.com/slackhq/nebula/handshake"
  10. "github.com/slackhq/nebula/header"
  11. "github.com/slackhq/nebula/noiseutil"
  12. )
  13. const (
  14. ReplayWindow = 8192
  15. // RehandshakeAfterMessages rolls keys inside the AES-GCM data-volume margin (~2^-36 advantage at 64KB frames).
  16. RehandshakeAfterMessages = uint64(1) << 34
  17. // RejectAfterMessages is the nonce ceiling enforced by noiseutil; a tunnel here is deleted locally, not notified.
  18. RejectAfterMessages = noiseutil.RejectAfterMessages
  19. )
  20. // RehandshakeAfterMessages must stay below RejectAfterMessages so tunnels roll before the hard send stop.
  21. const _ = RejectAfterMessages - RehandshakeAfterMessages
  22. // sessionEpoch hands out a receiver-local ordinal to every ConnectionState at creation. The RX
  23. // staging sort (overlay/batch) orders packets by (epoch, message counter). A re-handshake never
  24. // rekeys an existing tunnel; it brings up a new hostinfo and ConnectionState with a counter space
  25. // starting near zero, while the old tunnel keeps decrypting until torn down. During that cutover
  26. // one flush batch can hold packets from both tunnels, and the epoch keeps the old tunnel's
  27. // packets sorted first.
  28. var sessionEpoch atomic.Uint64
  29. type ConnectionState struct {
  30. eKey noiseutil.CipherState
  31. dKey noiseutil.CipherState
  32. myCert cert.Certificate
  33. peerCert *cert.CachedCertificate
  34. initiator bool
  35. messageCounter atomic.Uint64
  36. window *Bits
  37. decryptLock sync.Mutex
  38. writeLock sync.Mutex
  39. // epoch is this session's sessionEpoch ordinal. Immutable after creation.
  40. epoch uint64
  41. }
  42. // newConnectionStateFromResult builds a fully-populated ConnectionState from a
  43. // completed handshake.Result. It seeds messageCounter and the replay window so
  44. // that the post-handshake message indices already used on the wire don't count
  45. // as missed traffic in the data plane.
  46. func newConnectionStateFromResult(r *handshake.Result) (*ConnectionState, error) {
  47. // Refuse a MessageIndex too big for the replay window: it can only be a bug, and would spin the seed loop below.
  48. if r.MessageIndex >= ReplayWindow {
  49. return nil, fmt.Errorf("handshake message index %d exceeds replay window", r.MessageIndex)
  50. }
  51. ci := &ConnectionState{
  52. myCert: r.MyCert,
  53. initiator: r.Initiator,
  54. peerCert: r.RemoteCert,
  55. eKey: noiseutil.NewCipherState(r.EKey, r.Cipher),
  56. dKey: noiseutil.NewCipherState(r.DKey, r.Cipher),
  57. window: NewBits(ReplayWindow),
  58. epoch: sessionEpoch.Add(1),
  59. }
  60. ci.messageCounter.Add(r.MessageIndex)
  61. for i := uint64(1); i <= r.MessageIndex; i++ {
  62. ci.window.Update(nil, i)
  63. }
  64. return ci, nil
  65. }
  66. func (cs *ConnectionState) MarshalJSON() ([]byte, error) {
  67. return json.Marshal(m{
  68. "certificate": cs.peerCert,
  69. "initiator": cs.initiator,
  70. "message_counter": cs.messageCounter.Load(),
  71. })
  72. }
  73. // NextMessageCounter reserves the next 1-based counter; RejectAfterMessages is the first we refuse, pinned to not wrap.
  74. func (cs *ConnectionState) NextMessageCounter() (uint64, bool) {
  75. c := cs.messageCounter.Add(1)
  76. if c >= RejectAfterMessages {
  77. cs.messageCounter.Store(RejectAfterMessages)
  78. return c, false
  79. }
  80. return c, true
  81. }
  82. func (cs *ConnectionState) Curve() cert.Curve {
  83. return cs.myCert.Curve()
  84. }
  85. func (cs *ConnectionState) Decrypt(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) ([]byte, error) {
  86. cs.decryptLock.Lock()
  87. result := cs.window.Check(l, messageCounter)
  88. cs.decryptLock.Unlock()
  89. if !result {
  90. return nil, ErrAlreadySeen
  91. }
  92. out, err := cs.decryptPacket(messageCounter, packet, nb)
  93. return cs.acceptDecrypted(l, messageCounter, out, err)
  94. }
  95. // decryptPacket authenticates and decrypts packet in place, leaving the replay window to the caller.
  96. func (cs *ConnectionState) decryptPacket(messageCounter uint64, packet, nb []byte) ([]byte, error) {
  97. return cs.dKey.DecryptDanger(packet[header.Len:header.Len], packet[:header.Len], packet[header.Len:], messageCounter, nb)
  98. }
  99. // acceptDecrypted finishes a decryptPacket result: an authenticated packet is recorded in the replay window, and
  100. // rejected if its counter was recorded since it was checked.
  101. func (cs *ConnectionState) acceptDecrypted(l *slog.Logger, messageCounter uint64, out []byte, err error) ([]byte, error) {
  102. if err != nil {
  103. return nil, err
  104. }
  105. cs.decryptLock.Lock()
  106. result := cs.window.Update(l, messageCounter)
  107. cs.decryptLock.Unlock()
  108. if !result {
  109. return nil, ErrAlreadySeen
  110. }
  111. return out, nil
  112. }
  113. func (cs *ConnectionState) VerifyRelay(l *slog.Logger, messageCounter uint64, packet []byte, nb []byte) error {
  114. cs.decryptLock.Lock()
  115. result := cs.window.Check(l, messageCounter)
  116. cs.decryptLock.Unlock()
  117. if !result {
  118. return ErrAlreadySeen
  119. }
  120. // The entire body is sent as AD, not encrypted.
  121. // The packet consists of a 16-byte parsed Nebula header, Associated Data-protected payload, and a trailing 16-byte AEAD signature value.
  122. // 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
  123. // otherwise malformed (meaning, there is no trailing 16 byte AEAD value), then this will result in at worst a 0-length slice
  124. // which will gracefully fail in the DecryptDanger call.
  125. signedPayload := packet[:len(packet)-cs.dKey.Overhead()]
  126. signatureValue := packet[len(packet)-cs.dKey.Overhead():]
  127. _, err := cs.dKey.DecryptDanger(nil, signedPayload, signatureValue, messageCounter, nb)
  128. if err != nil {
  129. return err
  130. }
  131. cs.decryptLock.Lock()
  132. result = cs.window.Update(l, messageCounter)
  133. cs.decryptLock.Unlock()
  134. if !result {
  135. return ErrAlreadySeen
  136. }
  137. return nil
  138. }