cipher_state_test.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. package noiseutil
  2. import (
  3. "testing"
  4. "github.com/flynn/noise"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. )
  8. func TestCipherStateAESGCMRoundtrip(t *testing.T) {
  9. enc, dec := buildCipherStates(t, CipherAESGCM)
  10. roundtrip(t, NewCipherStateAESGCM(enc), NewCipherStateAESGCM(dec))
  11. }
  12. func TestCipherStateChaChaPolyRoundtrip(t *testing.T) {
  13. enc, dec := buildCipherStates(t, noise.CipherChaChaPoly)
  14. roundtrip(t, NewCipherStateChaChaPoly(enc), NewCipherStateChaChaPoly(dec))
  15. }
  16. func TestNewCipherStateDispatch(t *testing.T) {
  17. encA, _ := buildCipherStates(t, CipherAESGCM)
  18. encC, _ := buildCipherStates(t, noise.CipherChaChaPoly)
  19. assert.IsType(t, &CipherStateAESGCM{}, NewCipherState(encA, CipherAESGCM))
  20. assert.IsType(t, &CipherStateChaChaPoly{}, NewCipherState(encC, noise.CipherChaChaPoly))
  21. }
  22. func TestNewCipherStateUnsupportedPanics(t *testing.T) {
  23. enc, _ := buildCipherStates(t, CipherAESGCM)
  24. assert.Panics(t, func() {
  25. NewCipherState(enc, fakeCipher{})
  26. })
  27. }
  28. type fakeCipher struct{}
  29. func (fakeCipher) Cipher(k [32]byte) noise.Cipher { return nil }
  30. func (fakeCipher) CipherName() string { return "Fake" }
  31. // buildCipherStates runs an in-memory NN handshake with the requested cipher
  32. // to produce a pair of post-handshake CipherStates that share keys.
  33. func buildCipherStates(t *testing.T, c noise.CipherFunc) (*noise.CipherState, *noise.CipherState) {
  34. t.Helper()
  35. suite := noise.NewCipherSuite(noise.DH25519, c, noise.HashSHA256)
  36. cfg := noise.Config{CipherSuite: suite, Pattern: noise.HandshakeNN}
  37. cfg.Initiator = true
  38. hsI, err := noise.NewHandshakeState(cfg)
  39. require.NoError(t, err)
  40. cfg.Initiator = false
  41. hsR, err := noise.NewHandshakeState(cfg)
  42. require.NoError(t, err)
  43. msg, _, _, err := hsI.WriteMessage(nil, nil)
  44. require.NoError(t, err)
  45. _, _, _, err = hsR.ReadMessage(nil, msg)
  46. require.NoError(t, err)
  47. msg, dR, _, err := hsR.WriteMessage(nil, nil)
  48. require.NoError(t, err)
  49. _, eI, _, err := hsI.ReadMessage(nil, msg)
  50. require.NoError(t, err)
  51. require.NotNil(t, eI)
  52. require.NotNil(t, dR)
  53. // noise returns (cs1, cs2) where cs1 is the initiator->responder cipher.
  54. return eI, dR
  55. }
  56. func roundtrip(t *testing.T, enc, dec CipherState) {
  57. t.Helper()
  58. plaintext := []byte("nebula cipher state roundtrip")
  59. ad := []byte("aad")
  60. nb := make([]byte, 12)
  61. ct, err := enc.EncryptDanger(nil, ad, plaintext, 1, nb)
  62. require.NoError(t, err)
  63. assert.NotEqual(t, plaintext, ct)
  64. pt, err := dec.DecryptDanger(nil, ad, ct, 1, nb)
  65. require.NoError(t, err)
  66. assert.Equal(t, plaintext, pt)
  67. // Wrong nonce must fail authentication.
  68. _, err = dec.DecryptDanger(nil, ad, ct, 2, nb)
  69. require.Error(t, err)
  70. assert.Equal(t, enc.Overhead(), dec.Overhead())
  71. assert.Equal(t, 16, enc.Overhead())
  72. }
  73. func BenchmarkCipherStateEncryptAESGCM(b *testing.B) {
  74. enc, _ := buildCipherStatesB(b, CipherAESGCM)
  75. benchEncryptCipherState(b, NewCipherState(enc, CipherAESGCM))
  76. }
  77. func BenchmarkCipherStateEncryptChaChaPoly(b *testing.B) {
  78. enc, _ := buildCipherStatesB(b, noise.CipherChaChaPoly)
  79. benchEncryptCipherState(b, NewCipherState(enc, noise.CipherChaChaPoly))
  80. }
  81. func benchEncryptCipherState(b *testing.B, cs CipherState) {
  82. plaintext := make([]byte, 1280)
  83. ad := make([]byte, 16)
  84. nb := make([]byte, 12)
  85. out := make([]byte, 0, len(plaintext)+cs.Overhead())
  86. b.ResetTimer()
  87. b.ReportAllocs()
  88. for i := 0; i < b.N; i++ {
  89. var err error
  90. out, err = cs.EncryptDanger(out[:0], ad, plaintext, uint64(i+1), nb)
  91. if err != nil {
  92. b.Fatal(err)
  93. }
  94. }
  95. }
  96. func buildCipherStatesB(b *testing.B, c noise.CipherFunc) (*noise.CipherState, *noise.CipherState) {
  97. b.Helper()
  98. suite := noise.NewCipherSuite(noise.DH25519, c, noise.HashSHA256)
  99. cfg := noise.Config{CipherSuite: suite, Pattern: noise.HandshakeNN}
  100. cfg.Initiator = true
  101. hsI, err := noise.NewHandshakeState(cfg)
  102. if err != nil {
  103. b.Fatal(err)
  104. }
  105. cfg.Initiator = false
  106. hsR, err := noise.NewHandshakeState(cfg)
  107. if err != nil {
  108. b.Fatal(err)
  109. }
  110. msg, _, _, err := hsI.WriteMessage(nil, nil)
  111. if err != nil {
  112. b.Fatal(err)
  113. }
  114. if _, _, _, err := hsR.ReadMessage(nil, msg); err != nil {
  115. b.Fatal(err)
  116. }
  117. msg, dR, _, err := hsR.WriteMessage(nil, nil)
  118. if err != nil {
  119. b.Fatal(err)
  120. }
  121. _, eI, _, err := hsI.ReadMessage(nil, msg)
  122. if err != nil {
  123. b.Fatal(err)
  124. }
  125. return eI, dR
  126. }
  127. // TestDecryptDangerRelayShapeNoAlloc covers the AD-only relay path used in
  128. // outside.go's handleOutsideRelayPacket: the body is AD, the trailing 16 bytes
  129. // are the AEAD tag, the plaintext is empty, and the caller passes nil as the
  130. // destination because it only needs the auth side-effect. The call must
  131. // succeed, return an empty plaintext, and not allocate on the hot path.
  132. func TestDecryptDangerRelayShapeNoAlloc(t *testing.T) {
  133. cases := []struct {
  134. name string
  135. c noise.CipherFunc
  136. wrap func(*noise.CipherState) CipherState
  137. }{
  138. {"AESGCM", CipherAESGCM, func(cs *noise.CipherState) CipherState { return NewCipherStateAESGCM(cs) }},
  139. {"ChaChaPoly", noise.CipherChaChaPoly, func(cs *noise.CipherState) CipherState { return NewCipherStateChaChaPoly(cs) }},
  140. }
  141. for _, tc := range cases {
  142. t.Run(tc.name, func(t *testing.T) {
  143. encCS, decCS := buildCipherStates(t, tc.c)
  144. enc, dec := tc.wrap(encCS), tc.wrap(decCS)
  145. ad := make([]byte, 1200) // typical relay packet body size
  146. for i := range ad {
  147. ad[i] = byte(i)
  148. }
  149. nb := make([]byte, 12)
  150. // Build the "signature value" the way handleOutsideRelayPacket sees it:
  151. // empty plaintext encrypted with the body as AD yields just the 16-byte tag.
  152. tag, err := enc.EncryptDanger(nil, ad, nil, 1, nb)
  153. require.NoError(t, err)
  154. require.Len(t, tag, dec.Overhead())
  155. // Sanity: the relay-shaped call returns empty plaintext, no error.
  156. out, err := dec.DecryptDanger(nil, ad, tag, 1, nb)
  157. require.NoError(t, err)
  158. assert.Empty(t, out)
  159. // Tampering with the AD must fail authentication.
  160. ad[0] ^= 0xff
  161. _, err = dec.DecryptDanger(nil, ad, tag, 1, nb)
  162. require.Error(t, err)
  163. ad[0] ^= 0xff
  164. // The hot path must not allocate. AllocsPerRun does a warm-up run, so any
  165. // one-time setup is excluded. Counter has to advance so the AEAD nonce is
  166. // unique per call, but we don't care whether the auth succeeds — we only
  167. // care about whether the call path allocates.
  168. var counter uint64 = 2
  169. allocs := testing.AllocsPerRun(100, func() {
  170. _, _ = dec.DecryptDanger(nil, ad, tag, counter, nb)
  171. counter++
  172. })
  173. assert.Equal(t, 0.0, allocs, "DecryptDanger(nil, ...) must not allocate")
  174. })
  175. }
  176. }
  177. func TestCipherStateNilSafety(t *testing.T) {
  178. var aes *CipherStateAESGCM
  179. _, err := aes.EncryptDanger(nil, nil, nil, 0, make([]byte, 12))
  180. require.Error(t, err)
  181. out, err := aes.DecryptDanger(nil, nil, nil, 0, make([]byte, 12))
  182. require.NoError(t, err)
  183. assert.Empty(t, out)
  184. assert.Equal(t, 0, aes.Overhead())
  185. var cc *CipherStateChaChaPoly
  186. _, err = cc.EncryptDanger(nil, nil, nil, 0, make([]byte, 12))
  187. require.Error(t, err)
  188. out, err = cc.DecryptDanger(nil, nil, nil, 0, make([]byte, 12))
  189. require.NoError(t, err)
  190. assert.Empty(t, out)
  191. assert.Equal(t, 0, cc.Overhead())
  192. }