diff --git a/noiseutil/fips140.go b/noiseutil/fips140.go index 741d3b57..83a8a179 100644 --- a/noiseutil/fips140.go +++ b/noiseutil/fips140.go @@ -4,8 +4,11 @@ import ( "bytes" "crypto/cipher" "encoding/binary" + "reflect" + "unsafe" // unsafe needed for go:linkname + _ "crypto/tls" _ "unsafe" "github.com/flynn/noise" @@ -33,18 +36,18 @@ func (c cipherFn) CipherName() string { return c.name } var CipherAESGCMFIPS140 noise.CipherFunc = cipherFn{cipherAESGCMFIPS140, "AESGCM"} // tls.aeadAESGCMTLS13 uses a 4 byte static prefix and an 8 byte XOR mask -var emptyPrefix = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} -var emptyNonce = []byte{0, 0, 0, 0, 0, 0, 0, 0} +var emptyNonce = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} func cipherAESGCMFIPS140(k [32]byte) noise.Cipher { - gcm := aeadAESGCMTLS13(k[:], emptyPrefix) + gcm := aeadAESGCMTLS13(k[:], emptyNonce) + gcm = extractFIPSAEAD(gcm) return &aeadGCMFIPS140Cipher{ AEAD: gcm, ready: false, nonce: func(n uint64) []byte { // tls.aeadAESGCMTLS13 uses a 4 byte static prefix and an 8 byte nonce - var nonce [8]byte - binary.BigEndian.PutUint64(nonce[:], n) + var nonce [12]byte + binary.BigEndian.PutUint64(nonce[4:], n) return nonce[:] }, } @@ -56,8 +59,29 @@ type aeadGCMFIPS140Cipher struct { nonce func(uint64) []byte } +// Extract the internal FIPS GCM implementation from the tls wrapper. The TLS +// wrapper is not thread safe around Open, so instead of locking around it we +// can grab the internal implementation that is thread safe. This is the FIPS +// module implementation: `crypto/internal/fips140/aes/gcm.GCMWithXORCounterNonce` +// +// - https://github.com/golang/go/blob/go1.26.4/src/crypto/internal/fips140/aes/gcm/gcm_nonces.go#L212-L287 +// +// The wrapper is struct `crypto/tls.xorNonceAEAD` , with field `aead`: +// +// - https://github.com/golang/go/blob/go1.26.4/src/crypto/tls/cipher_suites.go#L482-L487 +// +// This can be cleaned up once these FIPS implementations are exposed directly: +// +// - https://github.com/golang/go/issues/73110 +func extractFIPSAEAD(xorNonceAEAD cipher.AEAD) cipher.AEAD { + r := reflect.ValueOf(xorNonceAEAD) + v := r.Elem().FieldByName("aead") + v2 := reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem() + return v2.Interface().(cipher.AEAD) +} + func (c *aeadGCMFIPS140Cipher) init(nonce []byte) { - // crypto/tls.aeadAESGCMTLS13 expects that the first call to Seal + // GCMWithXORCounterNonce expects that the first call to Seal // is with a counter of `0`, this is how it extracts the nonce mask. // We can clean this up in the future when NewGCMWithCounterNonce or // NewGCMForQUIC are available: @@ -84,11 +108,11 @@ func (c *aeadGCMFIPS140Cipher) Decrypt(out []byte, n uint64, ad, ciphertext []by func (c *aeadGCMFIPS140Cipher) EncryptDanger(out, ad, plaintext []byte, n uint64, nb []byte) ([]byte, error) { binary.BigEndian.PutUint64(nb[4:], n) - out = c.Seal(out, nb[4:], plaintext, ad) + out = c.Seal(out, nb, plaintext, ad) return out, nil } func (c *aeadGCMFIPS140Cipher) DecryptDanger(out, ad, ciphertext []byte, n uint64, nb []byte) ([]byte, error) { binary.BigEndian.PutUint64(nb[4:], n) - return c.Open(out, nb[4:], ciphertext, ad) + return c.Open(out, nb, ciphertext, ad) } diff --git a/noiseutil/fips140_test.go b/noiseutil/fips140_test.go index 012cd2ed..8f7a5eb0 100644 --- a/noiseutil/fips140_test.go +++ b/noiseutil/fips140_test.go @@ -17,7 +17,7 @@ func TestNewAESGCM(t *testing.T) { } key, _ := hex.DecodeString("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308") - iv, _ := hex.DecodeString("facedbaddecaf888") + iv, _ := hex.DecodeString("00000000facedbaddecaf888") plaintext, _ := hex.DecodeString("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39") aad, _ := hex.DecodeString("feedfacedeadbeeffeedfacedeadbeefabaddad2") expected, _ := hex.DecodeString("6a65c2edd45bd63c7e29f40e3d2ed8ba2b99f4c83135383d5676652f255059ceb24863ff10afb1089db701245da87fb88d3acd5f9dd0770cac220c3c04145caf25e190aeb775e7080401c628")