machine.go 15 KB

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