hybrid_test.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. package handshake
  2. import (
  3. "crypto/mlkem"
  4. "net/netip"
  5. "testing"
  6. "time"
  7. "github.com/DefinedNet/noise"
  8. "github.com/slackhq/nebula/cert"
  9. ct "github.com/slackhq/nebula/cert_test"
  10. "github.com/slackhq/nebula/header"
  11. "github.com/slackhq/nebula/noiseutil"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. )
  15. func hybridCipherSuite(curve cert.Curve) noise.CipherSuite {
  16. if curve == cert.Curve_P256 {
  17. return noise.NewCipherSuite(noiseutil.DHP256, noise.CipherAESGCM, noise.HashSHA256)
  18. }
  19. return noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashSHA256)
  20. }
  21. // newHybridCertState builds a peer holding a v3 certificate, with credentials for it and for its v2 form, the way
  22. // the pki does.
  23. func newHybridCertState(t *testing.T, curve cert.Curve, ca cert.Certificate, caKey []byte, name string, network string) (*testCertState, []byte) {
  24. t.Helper()
  25. c, _, rawPrivKey, _ := ct.NewTestCert(
  26. cert.Version3, curve, ca, caKey,
  27. name, ca.NotBefore(), ca.NotAfter(), []netip.Prefix{netip.MustParsePrefix(network)}, nil, nil,
  28. )
  29. priv, _, _, err := cert.UnmarshalPrivateKeyFromPEM(rawPrivKey)
  30. require.NoError(t, err)
  31. return hybridCertStateFor(t, curve, c, priv), priv
  32. }
  33. func hybridCertStateFor(t *testing.T, curve cert.Curve, c cert.Certificate, priv []byte) *testCertState {
  34. t.Helper()
  35. hs3, err := c.MarshalForHandshakes()
  36. require.NoError(t, err)
  37. v2 := cert.V2Form(c)
  38. hs2, err := v2.MarshalForHandshakes()
  39. require.NoError(t, err)
  40. ncs := hybridCipherSuite(curve)
  41. return &testCertState{
  42. version: cert.Version3,
  43. creds: map[cert.Version]*Credential{
  44. cert.Version3: NewCredential(c, hs3, priv, ncs),
  45. cert.Version2: NewCredential(v2, hs2, priv[:len(priv)-cert.MLKEM1024PrivateKeySize], ncs),
  46. },
  47. }
  48. }
  49. func newHybridMachine(t *testing.T, cs *testCertState, verifier CertVerifier, initiator bool, localIndex uint32) *Machine {
  50. t.Helper()
  51. m, err := NewMachine(
  52. cert.Version3, cs.getCredential,
  53. verifier, func() (uint32, error) { return localIndex, nil },
  54. initiator, header.HandshakeIXHybridMLKEM1024,
  55. )
  56. require.NoError(t, err)
  57. return m
  58. }
  59. type hybridPeers struct {
  60. pool *cert.CAPool
  61. ca cert.Certificate
  62. caKey []byte
  63. initCS, respCS *testCertState
  64. initPriv, respPriv []byte
  65. }
  66. func newHybridPeers(t *testing.T, curve cert.Curve) hybridPeers {
  67. t.Helper()
  68. ca, _, caKey, _ := ct.NewTestCaCert(cert.Version3, curve, time.Now().Add(-time.Hour), time.Now().Add(time.Hour), nil, nil, nil)
  69. pool := cert.NewCAPool()
  70. require.NoError(t, pool.AddCA(ca))
  71. initCS, initPriv := newHybridCertState(t, curve, ca, caKey, "initiator", "10.0.0.1/24")
  72. respCS, respPriv := newHybridCertState(t, curve, ca, caKey, "responder", "10.0.0.2/24")
  73. return hybridPeers{pool: pool, ca: ca, caKey: caKey, initCS: initCS, respCS: respCS, initPriv: initPriv, respPriv: respPriv}
  74. }
  75. func TestHybridIX(t *testing.T) {
  76. for _, curve := range []cert.Curve{cert.Curve_CURVE25519, cert.Curve_P256} {
  77. t.Run(curve.String(), func(t *testing.T) {
  78. p := newHybridPeers(t, curve)
  79. v := testVerifier(p.pool)
  80. initM := newHybridMachine(t, p.initCS, v, true, 1000)
  81. respM := newHybridMachine(t, p.respCS, v, false, 2000)
  82. msg1, err := initM.Initiate(nil)
  83. require.NoError(t, err)
  84. // The responder answers msg1 but can not complete until msg3.
  85. msg2, respResult, err := respM.ProcessPacket(nil, msg1)
  86. require.NoError(t, err)
  87. require.Nil(t, respResult)
  88. require.NotEmpty(t, msg2)
  89. // The initiator completes by writing msg3.
  90. msg3, initResult, err := initM.ProcessPacket(nil, msg2)
  91. require.NoError(t, err)
  92. require.NotNil(t, initResult)
  93. require.NotEmpty(t, msg3)
  94. out, respResult, err := respM.ProcessPacket(nil, msg3)
  95. require.NoError(t, err)
  96. require.Nil(t, out)
  97. require.NotNil(t, respResult)
  98. t.Logf("msg1 %d B, msg2 %d B, msg3 %d B", len(msg1), len(msg2), len(msg3))
  99. for i, msg := range [][]byte{msg1, msg2, msg3} {
  100. h := &header.H{}
  101. require.NoError(t, h.Parse(msg))
  102. assert.Equal(t, header.HandshakeIXHybridMLKEM1024, h.Subtype)
  103. assert.Equal(t, uint64(i+1), h.MessageCounter)
  104. }
  105. assert.Equal(t, uint32(2000), initResult.RemoteIndex)
  106. assert.Equal(t, uint32(1000), respResult.RemoteIndex)
  107. assert.Equal(t, cert.Version3, initResult.RemoteCert.Certificate.Version())
  108. assert.Equal(t, cert.Version3, respResult.RemoteCert.Certificate.Version())
  109. assert.Equal(t, "responder", initResult.RemoteCert.Certificate.Name())
  110. assert.Equal(t, "initiator", respResult.RemoteCert.Certificate.Name())
  111. assert.Equal(t, cert.Version3, initResult.MyCert.Version())
  112. assert.Equal(t, uint64(3), initResult.MessageIndex)
  113. assert.Equal(t, uint64(3), respResult.MessageIndex)
  114. // The keys pair up in both directions.
  115. for _, pair := range [][2]*Result{{initResult, respResult}, {respResult, initResult}} {
  116. ct, err := pair[0].EKey.Encrypt(nil, nil, []byte("hello"))
  117. require.NoError(t, err)
  118. pt, err := pair[1].DKey.Decrypt(nil, nil, ct)
  119. require.NoError(t, err)
  120. assert.Equal(t, []byte("hello"), pt)
  121. }
  122. })
  123. }
  124. }
  125. // A hybrid handshake needs v3 certificates, and a v3 certificate needs a hybrid handshake.
  126. func TestHybridIX_CertVersions(t *testing.T) {
  127. p := newHybridPeers(t, cert.Curve_CURVE25519)
  128. v := testVerifier(p.pool)
  129. _, err := NewMachine(cert.Version2, p.initCS.getCredential, v, func() (uint32, error) { return 1, nil }, true, header.HandshakeIXHybridMLKEM1024)
  130. require.ErrorIs(t, err, ErrCertVersionMismatch)
  131. _, err = NewMachine(cert.Version3, p.initCS.getCredential, v, func() (uint32, error) { return 1, nil }, true, header.HandshakeIXPSK0)
  132. require.ErrorIs(t, err, ErrCertVersionMismatch)
  133. // The v2 form still does a classical handshake, here with a peer that only has v2.
  134. ca2 := cert.V2Form(p.ca)
  135. pool2 := cert.NewCAPool()
  136. require.NoError(t, pool2.AddCA(ca2))
  137. classicalCAKey := p.caKey[:len(p.caKey)-cert.MLDSA87PrivateKeySize]
  138. v2Peer := newTestCertState(t, ca2, classicalCAKey, "v2peer", []netip.Prefix{netip.MustParsePrefix("10.0.0.3/24")})
  139. p.initCS.version = cert.Version2
  140. initResult, respResult := doFullHandshake(t, p.initCS, v2Peer, pool2)
  141. assert.Equal(t, cert.Version2, initResult.RemoteCert.Certificate.Version())
  142. assert.Equal(t, cert.Version2, respResult.RemoteCert.Certificate.Version())
  143. assert.Equal(t, cert.Version2, initResult.MyCert.Version())
  144. }
  145. // A responder that only trusts the v2 form of the CA can not verify a v3 certificate.
  146. func TestHybridIX_V2TrustOnly(t *testing.T) {
  147. p := newHybridPeers(t, cert.Curve_CURVE25519)
  148. pool2 := cert.NewCAPool()
  149. require.NoError(t, pool2.AddCA(cert.V2Form(p.ca)))
  150. initM := newHybridMachine(t, p.initCS, testVerifier(p.pool), true, 1000)
  151. respM := newHybridMachine(t, p.respCS, testVerifier(pool2), false, 2000)
  152. msg1, err := initM.Initiate(nil)
  153. require.NoError(t, err)
  154. _, _, err = respM.ProcessPacket(nil, msg1)
  155. require.ErrorIs(t, err, cert.ErrCaNotFound)
  156. }
  157. // A corrupted msg3 is dropped without killing the responder, and the real one still completes.
  158. func TestHybridIX_CorruptMsg3(t *testing.T) {
  159. p := newHybridPeers(t, cert.Curve_CURVE25519)
  160. v := testVerifier(p.pool)
  161. initM := newHybridMachine(t, p.initCS, v, true, 1000)
  162. respM := newHybridMachine(t, p.respCS, v, false, 2000)
  163. msg1, err := initM.Initiate(nil)
  164. require.NoError(t, err)
  165. msg2, _, err := respM.ProcessPacket(nil, msg1)
  166. require.NoError(t, err)
  167. msg3, _, err := initM.ProcessPacket(nil, msg2)
  168. require.NoError(t, err)
  169. bad := append([]byte(nil), msg3...)
  170. bad[header.Len+10] ^= 1
  171. _, res, err := respM.ProcessPacket(nil, bad)
  172. require.Error(t, err)
  173. require.Nil(t, res)
  174. require.False(t, respM.Failed())
  175. _, res, err = respM.ProcessPacket(nil, msg3)
  176. require.NoError(t, err)
  177. require.NotNil(t, res)
  178. }
  179. // withKEMKey returns a peer holding c and its classical private key, but some other ML-KEM key: someone who broke
  180. // the classical key, say with a quantum computer, and wants to be that peer.
  181. func withKEMKey(t *testing.T, curve cert.Curve, c cert.Certificate, priv []byte) *testCertState {
  182. t.Helper()
  183. dk, err := mlkem.GenerateKey1024()
  184. require.NoError(t, err)
  185. forged := append(append([]byte(nil), priv[:len(priv)-cert.MLKEM1024PrivateKeySize]...), dk.Bytes()...)
  186. return hybridCertStateFor(t, curve, c, forged)
  187. }
  188. // The classical private key alone does not get anyone through as the initiator: msg2 is unreadable without the
  189. // initiator's ML-KEM key.
  190. func TestHybridIX_ImpersonateInitiator(t *testing.T) {
  191. p := newHybridPeers(t, cert.Curve_CURVE25519)
  192. v := testVerifier(p.pool)
  193. victim := p.initCS.creds[cert.Version3].Cert
  194. attacker := withKEMKey(t, cert.Curve_CURVE25519, victim, p.initPriv)
  195. initM := newHybridMachine(t, attacker, v, true, 1000)
  196. respM := newHybridMachine(t, p.respCS, v, false, 2000)
  197. msg1, err := initM.Initiate(nil)
  198. require.NoError(t, err)
  199. msg2, _, err := respM.ProcessPacket(nil, msg1)
  200. require.NoError(t, err)
  201. _, res, err := initM.ProcessPacket(nil, msg2)
  202. require.Error(t, err)
  203. require.Nil(t, res)
  204. }
  205. // The classical private key alone does not get anyone through as the responder either: the initiator completes,
  206. // but msg3 is unreadable without the responder's ML-KEM key, so no packet under the new keys can ever come back.
  207. func TestHybridIX_ImpersonateResponder(t *testing.T) {
  208. p := newHybridPeers(t, cert.Curve_CURVE25519)
  209. v := testVerifier(p.pool)
  210. victim := p.respCS.creds[cert.Version3].Cert
  211. attacker := withKEMKey(t, cert.Curve_CURVE25519, victim, p.respPriv)
  212. initM := newHybridMachine(t, p.initCS, v, true, 1000)
  213. respM := newHybridMachine(t, attacker, v, false, 2000)
  214. msg1, err := initM.Initiate(nil)
  215. require.NoError(t, err)
  216. msg2, _, err := respM.ProcessPacket(nil, msg1)
  217. require.NoError(t, err)
  218. msg3, initResult, err := initM.ProcessPacket(nil, msg2)
  219. require.NoError(t, err)
  220. require.NotNil(t, initResult)
  221. _, res, err := respM.ProcessPacket(nil, msg3)
  222. require.Error(t, err)
  223. require.Nil(t, res)
  224. }