kem_test.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. package handshake
  2. import (
  3. "bytes"
  4. "crypto/mlkem"
  5. "net/netip"
  6. "testing"
  7. "time"
  8. "github.com/flynn/noise"
  9. "github.com/slackhq/nebula/cert"
  10. ct "github.com/slackhq/nebula/cert_test"
  11. "github.com/slackhq/nebula/header"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. )
  15. func newKEMMachine(t *testing.T, cs *testCertState, v CertVerifier, initiator bool, index uint32) *Machine {
  16. t.Helper()
  17. m, err := NewMachine(
  18. cs.version, cs.getCredential, v,
  19. func() (uint32, error) { return index, nil },
  20. initiator, header.HandshakeIXPSK2MLKEM1024,
  21. )
  22. require.NoError(t, err)
  23. return m
  24. }
  25. type kemFixture struct {
  26. initCS, respCS *testCertState
  27. v CertVerifier
  28. }
  29. func newKEMFixture(t *testing.T, cipher noise.CipherFunc) kemFixture {
  30. ca, _, caKey, _ := ct.NewTestCaCert(
  31. cert.Version2, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil,
  32. )
  33. return kemFixture{
  34. initCS: newTestCertStateWithCipher(t, ca, caKey, "initiator", []netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")}, cipher),
  35. respCS: newTestCertStateWithCipher(t, ca, caKey, "responder", []netip.Prefix{netip.MustParsePrefix("10.0.0.2/24")}, cipher),
  36. v: testVerifier(ct.NewTestCAPool(ca)),
  37. }
  38. }
  39. func requireKeysMatch(t *testing.T, a, b *Result) {
  40. t.Helper()
  41. c, err := a.EKey.Encrypt(nil, nil, []byte("hello"))
  42. require.NoError(t, err)
  43. p, err := b.DKey.Decrypt(nil, nil, c)
  44. require.NoError(t, err)
  45. assert.Equal(t, []byte("hello"), p)
  46. c, err = b.EKey.Encrypt(nil, nil, []byte("world"))
  47. require.NoError(t, err)
  48. p, err = a.DKey.Decrypt(nil, nil, c)
  49. require.NoError(t, err)
  50. assert.Equal(t, []byte("world"), p)
  51. }
  52. func TestKEMHandshake(t *testing.T) {
  53. for _, cipher := range []noise.CipherFunc{noise.CipherChaChaPoly, noise.CipherAESGCM} {
  54. t.Run(cipher.CipherName(), func(t *testing.T) {
  55. f := newKEMFixture(t, cipher)
  56. initM := newKEMMachine(t, f.initCS, f.v, true, 1000)
  57. respM := newKEMMachine(t, f.respCS, f.v, false, 2000)
  58. msg1, err := initM.Initiate(nil)
  59. require.NoError(t, err)
  60. assert.Equal(t, header.HandshakeIXPSK2MLKEM1024, header.MessageSubType(msg1[1]))
  61. assert.Greater(t, len(msg1), header.Len+mlkem.EncapsulationKeySize1024)
  62. msg2, respR, err := respM.ProcessPacket(nil, msg1)
  63. require.NoError(t, err)
  64. require.NotNil(t, respR)
  65. assert.Greater(t, len(msg2), header.Len+mlkem.CiphertextSize1024)
  66. _, initR, err := initM.ProcessPacket(nil, msg2)
  67. require.NoError(t, err)
  68. require.NotNil(t, initR)
  69. assert.Equal(t, "responder", initR.RemoteCert.Certificate.Name())
  70. assert.Equal(t, "initiator", respR.RemoteCert.Certificate.Name())
  71. assert.Equal(t, uint32(2000), initR.RemoteIndex)
  72. assert.Equal(t, uint32(1000), respR.RemoteIndex)
  73. requireKeysMatch(t, initR, respR)
  74. })
  75. }
  76. }
  77. // TestKEMSecretMatters checks the ML-KEM secret really reaches the transport
  78. // keys: an initiator holding a different decapsulation key cannot complete even
  79. // though every DH input is genuine.
  80. func TestKEMSecretMatters(t *testing.T) {
  81. f := newKEMFixture(t, noise.CipherChaChaPoly)
  82. initM := newKEMMachine(t, f.initCS, f.v, true, 1000)
  83. respM := newKEMMachine(t, f.respCS, f.v, false, 2000)
  84. msg1, err := initM.Initiate(nil)
  85. require.NoError(t, err)
  86. msg2, _, err := respM.ProcessPacket(nil, msg1)
  87. require.NoError(t, err)
  88. other, err := mlkem.GenerateKey1024()
  89. require.NoError(t, err)
  90. initM.kemKey = other
  91. _, res, err := initM.ProcessPacket(nil, msg2)
  92. require.Error(t, err)
  93. assert.Nil(t, res)
  94. }
  95. func TestKEMTamperedCiphertext(t *testing.T) {
  96. f := newKEMFixture(t, noise.CipherChaChaPoly)
  97. initM := newKEMMachine(t, f.initCS, f.v, true, 1000)
  98. respM := newKEMMachine(t, f.respCS, f.v, false, 2000)
  99. msg1, err := initM.Initiate(nil)
  100. require.NoError(t, err)
  101. msg2, respR, err := respM.ProcessPacket(nil, msg1)
  102. require.NoError(t, err)
  103. t.Run("truncated", func(t *testing.T) {
  104. _, res, err := initM.ProcessPacket(nil, msg2[:header.Len+mlkem.CiphertextSize1024-1])
  105. require.ErrorIs(t, err, ErrPacketTooShort)
  106. assert.Nil(t, res)
  107. assert.False(t, initM.Failed())
  108. })
  109. t.Run("flipped ciphertext bit", func(t *testing.T) {
  110. bad := bytes.Clone(msg2)
  111. bad[header.Len+10] ^= 1
  112. _, res, err := initM.ProcessPacket(nil, bad)
  113. require.Error(t, err)
  114. assert.Nil(t, res)
  115. assert.False(t, initM.Failed(), "a forged ciphertext must not kill the handshake")
  116. })
  117. t.Run("genuine response still completes", func(t *testing.T) {
  118. _, initR, err := initM.ProcessPacket(nil, msg2)
  119. require.NoError(t, err)
  120. require.NotNil(t, initR)
  121. requireKeysMatch(t, initR, respR)
  122. })
  123. }
  124. // TestKEMBadMsg1 drives processPayload by hand. With the psk modifier msg1's
  125. // payload is encrypted under a key derived from the public ephemeral, so a
  126. // forged payload would need re-encrypting; the parsing rules are what matter.
  127. func TestKEMBadMsg1(t *testing.T) {
  128. f := newKEMFixture(t, noise.CipherChaChaPoly)
  129. msg1Flags := subtypeInfos[header.HandshakeIXPSK2MLKEM1024].msgs[0]
  130. cert := f.initCS.creds[cert.Version2]
  131. payload := func(kemKey []byte) []byte {
  132. return MarshalPayload(nil, Payload{
  133. Cert: cert.Bytes,
  134. CertVersion: uint32(cert.Cert.Version()),
  135. InitiatorIndex: 1000,
  136. Time: 1,
  137. KEMKey: kemKey,
  138. })
  139. }
  140. dk, err := mlkem.GenerateKey1024()
  141. require.NoError(t, err)
  142. t.Run("missing KEM key", func(t *testing.T) {
  143. m := newKEMMachine(t, f.respCS, f.v, false, 2000)
  144. err := m.processPayload(payload(nil), msg1Flags)
  145. require.ErrorIs(t, err, ErrUnexpectedContent)
  146. assert.True(t, m.Failed())
  147. })
  148. t.Run("invalid KEM key", func(t *testing.T) {
  149. m := newKEMMachine(t, f.respCS, f.v, false, 2000)
  150. // Coefficients of 0xfff exceed the modulus, which ML-KEM must reject.
  151. bad := bytes.Repeat([]byte{0xff}, mlkem.EncapsulationKeySize1024)
  152. err := m.kemEncapsulate(bad)
  153. require.ErrorIs(t, err, ErrInvalidKEMKey)
  154. err = m.kemEncapsulate(bad[:100])
  155. require.ErrorIs(t, err, ErrInvalidKEMKey)
  156. })
  157. t.Run("valid KEM key", func(t *testing.T) {
  158. m := newKEMMachine(t, f.respCS, f.v, false, 2000)
  159. require.NoError(t, m.kemEncapsulate(dk.EncapsulationKey().Bytes()))
  160. assert.Len(t, m.kemCiphertext, mlkem.CiphertextSize1024)
  161. })
  162. t.Run("KEM key on the classical subtype", func(t *testing.T) {
  163. m := newTestMachine(t, f.respCS, f.v, false, 2000)
  164. classical := subtypeInfos[header.HandshakeIXPSK0].msgs[0]
  165. err := m.processPayload(payload(dk.EncapsulationKey().Bytes()), classical)
  166. require.ErrorIs(t, err, ErrUnexpectedContent)
  167. assert.True(t, m.Failed())
  168. })
  169. }
  170. // TestKEMDistinctFromClassical makes sure the two subtypes cannot be mixed: a
  171. // classical responder cannot answer a KEM initiator.
  172. func TestKEMDistinctFromClassical(t *testing.T) {
  173. f := newKEMFixture(t, noise.CipherChaChaPoly)
  174. initM := newKEMMachine(t, f.initCS, f.v, true, 1000)
  175. respM := newTestMachine(t, f.respCS, f.v, false, 2000)
  176. msg1, err := initM.Initiate(nil)
  177. require.NoError(t, err)
  178. _, _, err = respM.ProcessPacket(nil, msg1)
  179. require.ErrorIs(t, err, ErrSubtypeMismatch)
  180. }