machine.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. package handshake
  2. import (
  3. "bytes"
  4. "fmt"
  5. "slices"
  6. "time"
  7. "github.com/flynn/noise"
  8. "github.com/slackhq/nebula/cert"
  9. "github.com/slackhq/nebula/header"
  10. )
  11. // IndexAllocator is called by the Machine to allocate a local index for the
  12. // handshake. It is called at most once, when the first outgoing message that
  13. // carries a payload is built.
  14. //
  15. // Implementations MUST NOT return 0. Zero is reserved as a sentinel meaning
  16. // "no index assigned" on the wire and in the payload-presence checks. If an
  17. // allocator ever returned 0, a legitimate handshake's payload could be
  18. // indistinguishable from an empty one and would be rejected.
  19. type IndexAllocator func() (uint32, error)
  20. // CertVerifier is called by the Machine after reconstructing the peer's
  21. // certificate from the handshake. The verifier performs all validation
  22. // (CA trust, expiry, policy checks, allow lists).
  23. type CertVerifier func(cert.Certificate) (*cert.CachedCertificate, error)
  24. // Result contains the results of a successful handshake.
  25. // Returned by ProcessPacket when the handshake is complete.
  26. type Result struct {
  27. EKey *noise.CipherState
  28. DKey *noise.CipherState
  29. Cipher noise.CipherFunc // identifies which post-handshake CipherState the data plane should wrap EKey/DKey in
  30. MyCert cert.Certificate
  31. RemoteCert *cert.CachedCertificate
  32. RemoteIndex uint32
  33. LocalIndex uint32
  34. HandshakeTime uint64
  35. MessageIndex uint64 // number of messages exchanged during the handshake
  36. Initiator bool
  37. }
  38. // Machine drives a Noise handshake through N messages. It handles Noise
  39. // protocol operations, certificate reconstruction, and payload encoding.
  40. // Certificate validation is delegated to the caller via CertVerifier.
  41. //
  42. // A Machine is not safe for concurrent use. The caller must ensure that
  43. // Initiate and ProcessPacket are not called concurrently.
  44. //
  45. // Error contract: when ProcessPacket or Initiate returns an error, callers
  46. // must check Failed() to decide what to do next. If Failed() is false the
  47. // underlying noise state was not advanced (the packet was rejected before
  48. // ReadMessage took effect, or the rejection is non-fatal like a stale
  49. // retransmit) and the Machine can accept another packet. If Failed() is
  50. // true the Machine is unrecoverable and the caller must abandon it.
  51. type Machine struct {
  52. hs *noise.HandshakeState
  53. getCred GetCredentialFunc
  54. allocIndex IndexAllocator
  55. verifier CertVerifier
  56. result *Result
  57. msgs []msgFlags
  58. myVersion cert.Version
  59. subtype header.MessageSubType
  60. indexAllocated bool
  61. remoteCertSet bool
  62. payloadSet bool
  63. failed bool
  64. }
  65. // NewMachine creates a handshake state machine. The subtype determines both
  66. // the noise pattern and the per-message content layout. The credential for
  67. // `version` is fetched via getCred and used to seed the noise.HandshakeState.
  68. // IndexAllocator is called lazily when the first outgoing payload is built.
  69. func NewMachine(
  70. version cert.Version,
  71. getCred GetCredentialFunc,
  72. verifier CertVerifier,
  73. allocIndex IndexAllocator,
  74. initiator bool,
  75. subtype header.MessageSubType,
  76. ) (*Machine, error) {
  77. info, err := subtypeInfoFor(subtype)
  78. if err != nil {
  79. return nil, err
  80. }
  81. cred := getCred(version)
  82. if cred == nil {
  83. return nil, fmt.Errorf("%w: %v", ErrNoCredential, version)
  84. }
  85. hs, err := cred.buildHandshakeState(initiator, info.pattern)
  86. if err != nil {
  87. return nil, fmt.Errorf("build noise state: %w", err)
  88. }
  89. return &Machine{
  90. hs: hs,
  91. subtype: subtype,
  92. msgs: info.msgs,
  93. getCred: getCred,
  94. allocIndex: allocIndex,
  95. verifier: verifier,
  96. myVersion: version,
  97. result: &Result{
  98. Initiator: initiator,
  99. Cipher: cred.cipherSuite,
  100. },
  101. }, nil
  102. }
  103. // Failed returns true if the Machine is in an unrecoverable state.
  104. func (m *Machine) Failed() bool {
  105. return m.failed
  106. }
  107. // Subtype returns the handshake subtype this Machine was built for.
  108. func (m *Machine) Subtype() header.MessageSubType {
  109. return m.subtype
  110. }
  111. // MessageIndex returns the noise handshake message index, which equals the
  112. // wire counter of the most recently sent or received message.
  113. func (m *Machine) MessageIndex() int {
  114. return m.hs.MessageIndex()
  115. }
  116. // requireComplete checks that both a peer cert and payload have been received.
  117. // Marks the machine as failed if not.
  118. func (m *Machine) requireComplete() error {
  119. if !m.payloadSet || !m.remoteCertSet {
  120. m.failed = true
  121. return ErrIncompleteHandshake
  122. }
  123. return nil
  124. }
  125. // myMsgFlags returns the flags for the current outgoing message.
  126. func (m *Machine) myMsgFlags() msgFlags {
  127. idx := m.hs.MessageIndex()
  128. if idx < len(m.msgs) {
  129. return m.msgs[idx]
  130. }
  131. return msgFlags{}
  132. }
  133. // peerMsgFlags returns the flags for the message we just read.
  134. func (m *Machine) peerMsgFlags() msgFlags {
  135. idx := m.hs.MessageIndex() - 1
  136. if idx >= 0 && idx < len(m.msgs) {
  137. return m.msgs[idx]
  138. }
  139. return msgFlags{}
  140. }
  141. // Initiate produces the first handshake message. Only valid for initiators,
  142. // and must be called exactly once before ProcessPacket.
  143. //
  144. // out is a destination buffer the message is appended to and returned. Pass
  145. // nil to allocate fresh, or pass a re-used buffer sliced to length 0 (e.g.
  146. // buf[:0]) with sufficient capacity to avoid allocation.
  147. //
  148. // An error return may not indicate a fatal condition, check Failed() to
  149. // determine if the Machine can still be used.
  150. func (m *Machine) Initiate(out []byte) ([]byte, error) {
  151. if m.failed {
  152. return nil, ErrMachineFailed
  153. }
  154. if !m.result.Initiator {
  155. m.failed = true
  156. return nil, ErrInitiateOnResponder
  157. }
  158. if m.hs.MessageIndex() != 0 {
  159. m.failed = true
  160. return nil, ErrInitiateAlreadyCalled
  161. }
  162. // At MessageIndex=0 with RemoteIndex still zero, buildResponse produces
  163. // header counter 1 and remote index 0, which is what the initial message needs.
  164. out, _, _, err := m.buildResponse(out)
  165. if err != nil {
  166. m.failed = true
  167. return nil, err
  168. }
  169. return out, nil
  170. }
  171. // ProcessPacket handles an incoming handshake message. It advances the Noise
  172. // state, validates the peer certificate via the verifier, and optionally
  173. // produces a response.
  174. //
  175. // out is a destination buffer the response is appended to and returned. Pass
  176. // nil to allocate fresh, or pass a re-used buffer sliced to length 0 (e.g.
  177. // buf[:0]) with sufficient capacity to avoid allocation. The returned slice
  178. // is nil when no outgoing message is produced (handshake complete on this
  179. // side, or final message of a multi-message pattern).
  180. //
  181. // Returns a non-nil Result when the handshake is complete.
  182. // An error return may not indicate a fatal condition, check Failed() to
  183. // determine if the Machine can still be used.
  184. func (m *Machine) ProcessPacket(out, packet []byte) ([]byte, *Result, error) {
  185. if m.failed {
  186. return nil, nil, ErrMachineFailed
  187. }
  188. if len(packet) < header.Len {
  189. return nil, nil, ErrPacketTooShort
  190. }
  191. // Reject packets whose subtype doesn't match the one this Machine was
  192. // built for. A pending handshake that suddenly receives a different
  193. // subtype on its index is either a stray packet that matched by chance
  194. // or a peer protocol violation; drop it without failing the Machine so
  195. // the legitimate retransmit can still complete.
  196. if header.MessageSubType(packet[1]) != m.subtype {
  197. return nil, nil, ErrSubtypeMismatch
  198. }
  199. if m.result.Initiator && m.hs.MessageIndex() == 0 {
  200. m.failed = true
  201. return nil, nil, ErrInitiateNotCalled
  202. }
  203. // The (eKey, dKey) ordering here is correct for IX, where the initiator
  204. // completes the handshake by reading the responder's stage-2 message.
  205. // noise returns (cs1, cs2) where cs1 is the initiator->responder cipher.
  206. // For 3-message patterns where a responder finishes by reading the final
  207. // message, this ordering would be wrong; revisit when XX/pqIX lands.
  208. msg, eKey, dKey, err := m.hs.ReadMessage(nil, packet[header.Len:])
  209. if err != nil {
  210. // Noise ReadMessage failed. The noise library checkpoints and rolls back
  211. // on failure, so the Machine is still alive. The caller can retry with
  212. // a different packet.
  213. return nil, nil, fmt.Errorf("noise ReadMessage: %w", err)
  214. }
  215. // From here on, noise state has advanced. Any error is fatal.
  216. flags := m.peerMsgFlags()
  217. if err := m.processPayload(msg, flags); err != nil {
  218. return nil, nil, err
  219. }
  220. // If ReadMessage derived keys, the handshake is complete. Noise should
  221. // always produce both keys together; asymmetry is a protocol invariant
  222. // violation.
  223. if eKey != nil || dKey != nil {
  224. if eKey == nil || dKey == nil {
  225. m.failed = true
  226. return nil, nil, ErrAsymmetricCipherKeys
  227. }
  228. if err := m.requireComplete(); err != nil {
  229. return nil, nil, err
  230. }
  231. return nil, m.completed(eKey, dKey), nil
  232. }
  233. // ReadMessage didn't complete, produce the next outgoing message
  234. out, dk, ek, err := m.buildResponse(out)
  235. if err != nil {
  236. m.failed = true
  237. return nil, nil, err
  238. }
  239. if ek != nil || dk != nil {
  240. if ek == nil || dk == nil {
  241. m.failed = true
  242. return nil, nil, ErrAsymmetricCipherKeys
  243. }
  244. if err := m.requireComplete(); err != nil {
  245. return nil, nil, err
  246. }
  247. return out, m.completed(ek, dk), nil
  248. }
  249. return out, nil, nil
  250. }
  251. func (m *Machine) completed(eKey, dKey *noise.CipherState) *Result {
  252. m.result.EKey = eKey
  253. m.result.DKey = dKey
  254. m.result.MessageIndex = uint64(m.hs.MessageIndex())
  255. return m.result
  256. }
  257. func (m *Machine) processPayload(msg []byte, flags msgFlags) error {
  258. if len(msg) == 0 {
  259. if flags.expectsPayload || flags.expectsCert {
  260. m.failed = true
  261. return ErrMissingContent
  262. }
  263. return nil
  264. }
  265. payload, err := UnmarshalPayload(msg)
  266. if err != nil {
  267. m.failed = true
  268. return fmt.Errorf("unmarshal handshake: %w", err)
  269. }
  270. // Assert the payload contains exactly what we expect
  271. hasPayloadData := payload.InitiatorIndex != 0 || payload.ResponderIndex != 0 || payload.Time != 0
  272. if hasPayloadData != flags.expectsPayload {
  273. m.failed = true
  274. return ErrUnexpectedContent
  275. }
  276. hasCertData := len(payload.Cert) > 0
  277. if hasCertData != flags.expectsCert {
  278. m.failed = true
  279. return ErrUnexpectedContent
  280. }
  281. // Process payload
  282. if flags.expectsPayload {
  283. if m.result.Initiator {
  284. m.result.RemoteIndex = payload.ResponderIndex
  285. } else {
  286. m.result.RemoteIndex = payload.InitiatorIndex
  287. }
  288. m.result.HandshakeTime = payload.Time
  289. m.payloadSet = true
  290. }
  291. // Process certificate
  292. if flags.expectsCert {
  293. if err := m.validateCert(payload); err != nil {
  294. return err
  295. }
  296. }
  297. return nil
  298. }
  299. func (m *Machine) validateCert(payload Payload) error {
  300. cred := m.getCred(m.myVersion)
  301. if cred == nil {
  302. m.failed = true
  303. return fmt.Errorf("%w: %v", ErrNoCredential, m.myVersion)
  304. }
  305. rc, err := cert.Recombine(
  306. cert.Version(payload.CertVersion),
  307. payload.Cert,
  308. m.hs.PeerStatic(),
  309. cred.Cert.Curve(),
  310. )
  311. if err != nil {
  312. m.failed = true
  313. return fmt.Errorf("recombine cert: %w", err)
  314. }
  315. if !bytes.Equal(rc.PublicKey(), m.hs.PeerStatic()) {
  316. m.failed = true
  317. return ErrPublicKeyMismatch
  318. }
  319. // Version negotiation, if the peer sent a different version and we have it, switch
  320. if rc.Version() != m.myVersion {
  321. if m.getCred(rc.Version()) != nil {
  322. m.myVersion = rc.Version()
  323. }
  324. }
  325. verified, err := m.verifier(rc)
  326. if err != nil {
  327. m.failed = true
  328. return fmt.Errorf("verify cert: %w", err)
  329. }
  330. m.result.RemoteCert = verified
  331. m.remoteCertSet = true
  332. return nil
  333. }
  334. func (m *Machine) marshalOutgoing(flags msgFlags) ([]byte, error) {
  335. if !flags.expectsPayload && !flags.expectsCert {
  336. return nil, nil
  337. }
  338. var p Payload
  339. if flags.expectsPayload {
  340. if !m.indexAllocated {
  341. index, err := m.allocIndex()
  342. if err != nil {
  343. return nil, fmt.Errorf("%w: %w", ErrIndexAllocation, err)
  344. }
  345. m.result.LocalIndex = index
  346. m.indexAllocated = true
  347. }
  348. if m.result.Initiator {
  349. p.InitiatorIndex = m.result.LocalIndex
  350. } else {
  351. p.ResponderIndex = m.result.LocalIndex
  352. p.InitiatorIndex = m.result.RemoteIndex
  353. }
  354. p.Time = uint64(time.Now().UnixNano())
  355. }
  356. if flags.expectsCert {
  357. cred := m.getCred(m.myVersion)
  358. if cred == nil {
  359. return nil, fmt.Errorf("%w: %v", ErrNoCredential, m.myVersion)
  360. }
  361. p.Cert = cred.Bytes
  362. p.CertVersion = uint32(cred.Cert.Version())
  363. m.result.MyCert = cred.Cert
  364. }
  365. return MarshalPayload(nil, p), nil
  366. }
  367. func (m *Machine) buildResponse(out []byte) ([]byte, *noise.CipherState, *noise.CipherState, error) {
  368. flags := m.myMsgFlags()
  369. hsBytes, err := m.marshalOutgoing(flags)
  370. if err != nil {
  371. return nil, nil, nil, err
  372. }
  373. // Extend out by header.Len to make room for the header. slices.Grow is a
  374. // no-op when the cap is already sufficient (the zero-copy case where the
  375. // caller passed a pre-sized buffer). header.Encode overwrites the new
  376. // bytes, so they don't need to be zeroed.
  377. start := len(out)
  378. out = slices.Grow(out, header.Len)[:start+header.Len]
  379. header.Encode(
  380. out[start:],
  381. header.Version, header.Handshake, m.subtype,
  382. m.result.RemoteIndex,
  383. uint64(m.hs.MessageIndex()+1),
  384. )
  385. // noise.WriteMessage appends the encrypted handshake message to out,
  386. // reusing capacity when present.
  387. //
  388. // The (dKey, eKey) ordering here is correct for IX, where the responder
  389. // completes the handshake by writing the stage-2 message. noise returns
  390. // (cs1, cs2) where cs1 is the initiator->responder cipher (which is the
  391. // responder's decrypt key). For 3-message patterns where an initiator
  392. // finishes by writing the final message, this ordering would be wrong;
  393. // revisit when XX/pqIX lands.
  394. out, dKey, eKey, err := m.hs.WriteMessage(out, hsBytes)
  395. if err != nil {
  396. return nil, nil, nil, fmt.Errorf("noise WriteMessage: %w", err)
  397. }
  398. return out, dKey, eKey, nil
  399. }