machine.go 15 KB

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