fips140.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. package noiseutil
  2. import (
  3. "bytes"
  4. "crypto/cipher"
  5. "crypto/fips140"
  6. "encoding/binary"
  7. "errors"
  8. "fmt"
  9. "reflect"
  10. "runtime"
  11. "unsafe"
  12. // unsafe needed for go:linkname
  13. _ "crypto/tls"
  14. _ "unsafe"
  15. "github.com/flynn/noise"
  16. )
  17. // TODO: Use NewGCMWithCounterNonce or NewGCMForQUIC once available:
  18. // - https://github.com/golang/go/issues/73110
  19. // - https://github.com/golang/go/issues/79219
  20. // Using tls.aeadAESGCMTLS13 gives us the TLS 1.3 GCM, which also verifies
  21. // that the nonce is strictly increasing. This works for both boringcrypto
  22. // and fips140.
  23. //
  24. //go:linkname aeadAESGCMTLS13 crypto/tls.aeadAESGCMTLS13
  25. func aeadAESGCMTLS13(key, noncePrefix []byte) cipher.AEAD
  26. type cipherFn struct {
  27. fn func([32]byte) noise.Cipher
  28. name string
  29. }
  30. func (c cipherFn) Cipher(k [32]byte) noise.Cipher { return c.fn(k) }
  31. func (c cipherFn) CipherName() string { return c.name }
  32. // CipherAESGCMFIPS140 is the AES256-GCM AEAD cipher (using tls.aeadAESGCMTLS13, for both boringcrypto and fips140)
  33. var CipherAESGCMFIPS140 noise.CipherFunc = cipherFn{cipherAESGCMFIPS140, "AESGCM"}
  34. // tls.aeadAESGCMTLS13 uses a 4 byte static prefix and an 8 byte XOR mask
  35. var emptyNonce = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
  36. func cipherAESGCMFIPS140(k [32]byte) noise.Cipher {
  37. gcm := aeadAESGCMTLS13(k[:], emptyNonce)
  38. gcm = extractFIPSAEAD(gcm)
  39. return &aeadGCMFIPS140Cipher{
  40. AEAD: gcm,
  41. }
  42. }
  43. type aeadGCMFIPS140Cipher struct {
  44. cipher.AEAD
  45. ready bool
  46. }
  47. // Extract the internal FIPS GCM implementation from the tls wrapper. The TLS
  48. // wrapper is not thread safe around Open, so instead of locking around it we
  49. // can grab the internal implementation that is thread safe. This is the FIPS
  50. // module implementation: `crypto/internal/fips140/aes/gcm.GCMWithXORCounterNonce`
  51. //
  52. // - https://github.com/golang/go/blob/go1.26.4/src/crypto/internal/fips140/aes/gcm/gcm_nonces.go#L212-L287
  53. //
  54. // The wrapper is struct `crypto/tls.xorNonceAEAD` , with field `aead`:
  55. //
  56. // - https://github.com/golang/go/blob/go1.26.4/src/crypto/tls/cipher_suites.go#L482-L487
  57. //
  58. // This can be cleaned up once these FIPS implementations are exposed directly:
  59. //
  60. // - https://github.com/golang/go/issues/73110
  61. func extractFIPSAEAD(xorNonceAEAD cipher.AEAD) cipher.AEAD {
  62. r := reflect.ValueOf(xorNonceAEAD)
  63. v := r.Elem().FieldByName("aead")
  64. if !v.IsValid() {
  65. // The internal crypto/tls.xorNonceAEAD struct no longer has an `aead`
  66. // field. This can only happen on a Go version this code was not built
  67. // against; the package init() self-test guards against ever reaching
  68. // this at runtime, so this is a defensive fail-fast.
  69. panic(fmt.Sprintf("noiseutil: could not extract FIPS AEAD from %T on %s: no `aead` field (incompatible Go version)", xorNonceAEAD, runtime.Version()))
  70. }
  71. v2 := reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem()
  72. aead, ok := v2.Interface().(cipher.AEAD)
  73. if !ok {
  74. panic(fmt.Sprintf("noiseutil: extracted FIPS `aead` field is %s, not a cipher.AEAD, on %s (incompatible Go version)", v2.Type(), runtime.Version()))
  75. }
  76. return aead
  77. }
  78. func (c *aeadGCMFIPS140Cipher) init(nonce []byte) {
  79. // GCMWithXORCounterNonce expects that the first call to Seal
  80. // is with a counter of `0`, this is how it extracts the nonce mask.
  81. // We can clean this up in the future when NewGCMWithCounterNonce or
  82. // NewGCMForQUIC are available:
  83. if !bytes.Equal(emptyNonce, nonce) {
  84. c.AEAD.Seal([]byte{}, emptyNonce, []byte{}, []byte{})
  85. }
  86. c.ready = true
  87. }
  88. func (c *aeadGCMFIPS140Cipher) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
  89. if !c.ready {
  90. c.init(nonce)
  91. }
  92. return c.AEAD.Seal(dst, nonce, plaintext, additionalData)
  93. }
  94. func (c *aeadGCMFIPS140Cipher) Encrypt(out []byte, n uint64, ad, plaintext []byte) []byte {
  95. return c.Seal(out, aeadGCMFIPS140CipherNonce(n), plaintext, ad)
  96. }
  97. func (c *aeadGCMFIPS140Cipher) Decrypt(out []byte, n uint64, ad, ciphertext []byte) ([]byte, error) {
  98. return c.Open(out, aeadGCMFIPS140CipherNonce(n), ciphertext, ad)
  99. }
  100. func (c *aeadGCMFIPS140Cipher) EncryptDanger(out, ad, plaintext []byte, n uint64, nb []byte) ([]byte, error) {
  101. if c == nil {
  102. return nil, errors.New("no cipher state available to encrypt")
  103. }
  104. if n >= RejectAfterMessages {
  105. return nil, ErrMessageCounterExhausted
  106. }
  107. binary.BigEndian.PutUint64(nb[4:], n)
  108. out = c.Seal(out, nb, plaintext, ad)
  109. return out, nil
  110. }
  111. func (c *aeadGCMFIPS140Cipher) DecryptDanger(out, ad, ciphertext []byte, n uint64, nb []byte) ([]byte, error) {
  112. if c == nil {
  113. return []byte{}, nil
  114. }
  115. binary.BigEndian.PutUint64(nb[4:], n)
  116. return c.Open(out, nb, ciphertext, ad)
  117. }
  118. func (c *aeadGCMFIPS140Cipher) Overhead() int {
  119. if c == nil {
  120. return 0
  121. }
  122. return c.AEAD.Overhead()
  123. }
  124. func aeadGCMFIPS140CipherNonce(n uint64) []byte {
  125. // GCMWithXORCounterNonce uses a 4 byte static prefix and an 8 byte nonce
  126. var nonce [12]byte
  127. binary.BigEndian.PutUint64(nonce[4:], n)
  128. return nonce[:]
  129. }
  130. func init() {
  131. if boringEnabled || fips140.Enabled() {
  132. initSelfTestAESGCMFIPS140()
  133. }
  134. }
  135. // validates the go:linkname + reflection extraction and the nonce-reuse
  136. // protection at startup. cipherAESGCMFIPS140 relies on unexported
  137. // crypto/tls and crypto/internal/fips140 internals; if a future Go version changes
  138. // those, this fails fast with a clear message instead of panicking per-handshake
  139. // (or, worse, silently losing the strictly-increasing nonce check that is the whole
  140. // point of using this cipher).
  141. func initSelfTestAESGCMFIPS140() {
  142. var key [32]byte
  143. c := cipherAESGCMFIPS140(key)
  144. // Verify the extracted AEAD produces a working encrypt/decrypt roundtrip.
  145. plaintext := []byte("nebula fips140 self-test")
  146. ad := []byte("ad")
  147. ct := c.Encrypt(nil, 1, ad, plaintext)
  148. pt, err := c.Decrypt(nil, 1, ad, ct)
  149. if err != nil {
  150. panic(fmt.Sprintf("noiseutil: FIPS AES-GCM self-test roundtrip failed on %s: %v", runtime.Version(), err))
  151. }
  152. if !bytes.Equal(pt, plaintext) {
  153. panic(fmt.Sprintf("noiseutil: FIPS AES-GCM self-test roundtrip returned wrong plaintext on %s", runtime.Version()))
  154. }
  155. // Verify the nonce-reuse protection still fires: re-encrypting with the same
  156. // counter must panic. This is the defensive check that FIPS-140 requires, so
  157. // if the extraction ever silently yields an AEAD without it, refuse to start.
  158. if !reusePanics(c) {
  159. panic(fmt.Sprintf("noiseutil: FIPS AES-GCM self-test did not reject a reused nonce on %s; nonce-reuse protection is missing (incompatible Go version)", runtime.Version()))
  160. }
  161. }
  162. // reusePanics reports whether re-encrypting with an already-used counter panics,
  163. // as GCMWithXORCounterNonce is expected to.
  164. func reusePanics(c noise.Cipher) (panicked bool) {
  165. c.Encrypt(nil, 2, nil, nil)
  166. defer func() {
  167. if recover() != nil {
  168. panicked = true
  169. }
  170. }()
  171. c.Encrypt(nil, 2, nil, nil)
  172. return false
  173. }