crypto/internal/fips140/aes/gcm: add more GCM nonce modes

First, this adds a GCM mode for QUIC, and a generic TLS 1.3/QUIC-like
XOR'd counter mode. QUIC constructs nonces exactly like TLS 1.3, but the
counter does not reset to zero on a key update, so the mask must be
provided explicitly (or we will panic well before running out of
counters because the wrong value is XOR'd out). See the analysis at
https://github.com/quic-go/quic-go/issues/5077#issuecomment-3570352683
for the details of QUIC and FIPS 140-3 compliance (including a
workaround for the key update issue).

Second, this coalesces all the compliance modes around two schemes:
fixed || counter, and fixed XOR counter. The former is used in TLS 1.2
and SSH, and the latter is used in TLS 1.3 and QUIC.

This would not be a backwards compatible change if these were public
APIs, but we only need different versions of the FIPS 140-3 module to be
source-compatible with the standard library, and the callers of these
NewGCMFor* functions only care that the return value implements
cipher.AEAD, at least for now.

This exposes no new public APIs, but lets us get started validating
these functions in v2.0.0 of the FIPS 140-3 module.

Updates #73110

Change-Id: I3d86cf8a3c4a96caf361c29f0db5f9706a6a6964
Reviewed-on: https://go-review.googlesource.com/c/go/+/723760
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Filippo Valsorda 2025-11-24 13:35:15 +01:00 committed by Gopher Robot
parent 1768cb40b8
commit 272df5f6ba
3 changed files with 323 additions and 226 deletions

View file

@ -0,0 +1,91 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !fips140v1.0
package cipher_test
import (
"crypto/cipher"
"crypto/internal/fips140"
fipsaes "crypto/internal/fips140/aes"
"crypto/internal/fips140/aes/gcm"
"encoding/binary"
"math"
"testing"
)
func TestGCMNoncesFIPSV2(t *testing.T) {
tryNonce := func(aead cipher.AEAD, nonce []byte) bool {
fips140.ResetServiceIndicator()
aead.Seal(nil, nonce, []byte("x"), nil)
return fips140.ServiceIndicator()
}
expectOK := func(t *testing.T, aead cipher.AEAD, nonce []byte) {
t.Helper()
if !tryNonce(aead, nonce) {
t.Errorf("expected service indicator true for %x", nonce)
}
}
expectPanic := func(t *testing.T, aead cipher.AEAD, nonce []byte) {
t.Helper()
defer func() {
t.Helper()
if recover() == nil {
t.Errorf("expected panic for %x", nonce)
}
}()
tryNonce(aead, nonce)
}
t.Run("NewGCMWithXORCounterNonce", func(t *testing.T) {
newGCM := func() *gcm.GCMWithXORCounterNonce {
key := make([]byte, 16)
block, _ := fipsaes.New(key)
aead, _ := gcm.NewGCMWithXORCounterNonce(block)
return aead
}
nonce := func(mask []byte, counter uint64) []byte {
nonce := make([]byte, 12)
copy(nonce, mask)
n := binary.BigEndian.AppendUint64(nil, counter)
for i, b := range n {
nonce[4+i] ^= b
}
return nonce
}
for _, mask := range [][]byte{
decodeHex(t, "ffffffffffffffffffffffff"),
decodeHex(t, "aabbccddeeff001122334455"),
decodeHex(t, "000000000000000000000000"),
} {
g := newGCM()
// Mask is derived from first invocation with zero nonce.
expectOK(t, g, nonce(mask, 0))
expectOK(t, g, nonce(mask, 1))
expectOK(t, g, nonce(mask, 100))
expectPanic(t, g, nonce(mask, 100))
expectPanic(t, g, nonce(mask, 99))
expectOK(t, g, nonce(mask, math.MaxUint64-2))
expectOK(t, g, nonce(mask, math.MaxUint64-1))
expectPanic(t, g, nonce(mask, math.MaxUint64))
expectPanic(t, g, nonce(mask, 0))
g = newGCM()
g.SetNoncePrefixAndMask(mask)
expectOK(t, g, nonce(mask, 0xFFFFFFFF))
expectOK(t, g, nonce(mask, math.MaxUint64-2))
expectOK(t, g, nonce(mask, math.MaxUint64-1))
expectPanic(t, g, nonce(mask, math.MaxUint64))
expectPanic(t, g, nonce(mask, 0))
g = newGCM()
g.SetNoncePrefixAndMask(mask)
expectOK(t, g, nonce(mask, math.MaxUint64-1))
expectPanic(t, g, nonce(mask, math.MaxUint64))
expectPanic(t, g, nonce(mask, 0))
}
})
}

View file

@ -761,19 +761,13 @@ func TestGCMExtraMethods(t *testing.T) {
}) })
} }
func TestFIPSServiceIndicator(t *testing.T) { func TestGCMNonces(t *testing.T) {
newGCM := func() cipher.AEAD {
key := make([]byte, 16)
block, _ := fipsaes.New(key)
aead, _ := gcm.NewGCMWithCounterNonce(block)
return aead
}
tryNonce := func(aead cipher.AEAD, nonce []byte) bool { tryNonce := func(aead cipher.AEAD, nonce []byte) bool {
fips140.ResetServiceIndicator() fips140.ResetServiceIndicator()
aead.Seal(nil, nonce, []byte("x"), nil) aead.Seal(nil, nonce, []byte("x"), nil)
return fips140.ServiceIndicator() return fips140.ServiceIndicator()
} }
expectTrue := func(t *testing.T, aead cipher.AEAD, nonce []byte) { expectOK := func(t *testing.T, aead cipher.AEAD, nonce []byte) {
t.Helper() t.Helper()
if !tryNonce(aead, nonce) { if !tryNonce(aead, nonce) {
t.Errorf("expected service indicator true for %x", nonce) t.Errorf("expected service indicator true for %x", nonce)
@ -790,108 +784,106 @@ func TestFIPSServiceIndicator(t *testing.T) {
tryNonce(aead, nonce) tryNonce(aead, nonce)
} }
g := newGCM() t.Run("NewGCMWithCounterNonce", func(t *testing.T) {
expectTrue(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) newGCM := func() cipher.AEAD {
expectTrue(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) key := make([]byte, 16)
expectTrue(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100}) block, _ := fipsaes.New(key)
expectTrue(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0}) aead, _ := gcm.NewGCMWithCounterNonce(block)
expectTrue(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0}) return aead
expectTrue(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0}) }
expectTrue(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0})
expectTrue(t, g, []byte{0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0})
expectTrue(t, g, []byte{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0})
expectTrue(t, g, []byte{0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0})
// Changed name.
expectPanic(t, g, []byte{0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0})
g = newGCM() g := newGCM()
expectTrue(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) expectOK(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
// Went down. expectOK(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
expectPanic(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) expectOK(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100})
expectOK(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0})
expectOK(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0})
expectOK(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0})
expectOK(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0})
expectOK(t, g, []byte{0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0})
expectOK(t, g, []byte{0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0})
expectOK(t, g, []byte{0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0})
// Changed name.
expectPanic(t, g, []byte{0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0})
g = newGCM() g = newGCM()
expectTrue(t, g, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) expectOK(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
expectTrue(t, g, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13}) // Went down.
// Did not increment. expectPanic(t, g, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
expectPanic(t, g, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13})
g = newGCM() g = newGCM()
expectTrue(t, g, []byte{1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00}) expectOK(t, g, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12})
expectTrue(t, g, []byte{1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) expectOK(t, g, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13})
// Wrap is ok as long as we don't run out of values. // Did not increment.
expectTrue(t, g, []byte{1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0}) expectPanic(t, g, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13})
expectTrue(t, g, []byte{1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xfe})
// Run out of counters.
expectPanic(t, g, []byte{1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff})
g = newGCM() g = newGCM()
expectTrue(t, g, []byte{1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) expectOK(t, g, []byte{1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00})
// Wrap with overflow. expectOK(t, g, []byte{1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff})
expectPanic(t, g, []byte{1, 2, 3, 5, 0, 0, 0, 0, 0, 0, 0, 0}) // Wrap is ok as long as we don't run out of values.
} expectOK(t, g, []byte{1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0})
expectOK(t, g, []byte{1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xfe})
// Run out of counters.
expectPanic(t, g, []byte{1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff})
func TestGCMForSSH(t *testing.T) { g = newGCM()
// incIV from x/crypto/ssh/cipher.go. expectOK(t, g, []byte{1, 2, 3, 4, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff})
incIV := func(iv []byte) { // Wrap with overflow.
for i := 4 + 7; i >= 4; i-- { expectPanic(t, g, []byte{1, 2, 3, 5, 0, 0, 0, 0, 0, 0, 0, 0})
iv[i]++ })
if iv[i] != 0 {
break t.Run("NewGCMForSSH", func(t *testing.T) {
newGCM := func() cipher.AEAD {
key := make([]byte, 16)
block, _ := fipsaes.New(key)
aead, _ := gcm.NewGCMForSSH(block)
return aead
}
// incIV from x/crypto/ssh/cipher.go.
incIV := func(iv []byte) {
for i := 4 + 7; i >= 4; i-- {
iv[i]++
if iv[i] != 0 {
break
}
} }
} }
}
expectOK := func(aead cipher.AEAD, iv []byte) { aead := newGCM()
aead.Seal(nil, iv, []byte("hello, world"), nil) iv := decodeHex(t, "11223344"+"0000000000000000")
} expectOK(t, aead, iv)
incIV(iv)
expectOK(t, aead, iv)
iv = decodeHex(t, "11223344"+"fffffffffffffffe")
expectOK(t, aead, iv)
incIV(iv)
expectPanic(t, aead, iv)
expectPanic := func(aead cipher.AEAD, iv []byte) { // Wrapping is ok as long as we don't run out of values.
defer func() { aead = newGCM()
if recover() == nil { iv = decodeHex(t, "11223344"+"fffffffffffffffe")
t.Errorf("expected panic") expectOK(t, aead, iv)
} incIV(iv)
}() expectOK(t, aead, iv)
aead.Seal(nil, iv, []byte("hello, world"), nil) incIV(iv)
} expectOK(t, aead, iv)
incIV(iv)
expectOK(t, aead, iv)
key := make([]byte, 16) aead = newGCM()
block, _ := fipsaes.New(key) iv = decodeHex(t, "11223344"+"aaaaaaaaaaaaaaaa")
aead, err := gcm.NewGCMForSSH(block) expectOK(t, aead, iv)
if err != nil { iv = decodeHex(t, "11223344"+"ffffffffffffffff")
t.Fatal(err) expectOK(t, aead, iv)
} incIV(iv)
iv := decodeHex(t, "11223344"+"0000000000000000") expectOK(t, aead, iv)
expectOK(aead, iv) iv = decodeHex(t, "11223344"+"aaaaaaaaaaaaaaa8")
incIV(iv) expectOK(t, aead, iv)
expectOK(aead, iv) incIV(iv)
iv = decodeHex(t, "11223344"+"fffffffffffffffe") expectPanic(t, aead, iv)
expectOK(aead, iv) iv = decodeHex(t, "11223344"+"bbbbbbbbbbbbbbbb")
incIV(iv) expectPanic(t, aead, iv)
expectPanic(aead, iv) })
aead, _ = gcm.NewGCMForSSH(block)
iv = decodeHex(t, "11223344"+"fffffffffffffffe")
expectOK(aead, iv)
incIV(iv)
expectOK(aead, iv)
incIV(iv)
expectOK(aead, iv)
incIV(iv)
expectOK(aead, iv)
aead, _ = gcm.NewGCMForSSH(block)
iv = decodeHex(t, "11223344"+"aaaaaaaaaaaaaaaa")
expectOK(aead, iv)
iv = decodeHex(t, "11223344"+"ffffffffffffffff")
expectOK(aead, iv)
incIV(iv)
expectOK(aead, iv)
iv = decodeHex(t, "11223344"+"aaaaaaaaaaaaaaa8")
expectOK(aead, iv)
incIV(iv)
expectPanic(aead, iv)
iv = decodeHex(t, "11223344"+"bbbbbbbbbbbbbbbb")
expectPanic(aead, iv)
} }
func decodeHex(t *testing.T, s string) []byte { func decodeHex(t *testing.T, s string) []byte {

View file

@ -10,6 +10,7 @@ import (
"crypto/internal/fips140/alias" "crypto/internal/fips140/alias"
"crypto/internal/fips140/drbg" "crypto/internal/fips140/drbg"
"crypto/internal/fips140deps/byteorder" "crypto/internal/fips140deps/byteorder"
"errors"
"math" "math"
) )
@ -45,7 +46,9 @@ func SealWithRandomNonce(g *GCM, nonce, out, plaintext, additionalData []byte) {
// NewGCMWithCounterNonce returns a new AEAD that works like GCM, but enforces // NewGCMWithCounterNonce returns a new AEAD that works like GCM, but enforces
// the construction of deterministic nonces. The nonce must be 96 bits, the // the construction of deterministic nonces. The nonce must be 96 bits, the
// first 32 bits must be an encoding of the module name, and the last 64 bits // first 32 bits must be an encoding of the module name, and the last 64 bits
// must be a counter. // must be a counter. The starting value of the counter is set on the first call
// to Seal, and each subsequent call must increment it as a big-endian uint64.
// If the counter reaches the starting value minus one, Seal will panic.
// //
// This complies with FIPS 140-3 IG C.H Scenario 3. // This complies with FIPS 140-3 IG C.H Scenario 3.
func NewGCMWithCounterNonce(cipher *aes.Block) (*GCMWithCounterNonce, error) { func NewGCMWithCounterNonce(cipher *aes.Block) (*GCMWithCounterNonce, error) {
@ -56,41 +59,77 @@ func NewGCMWithCounterNonce(cipher *aes.Block) (*GCMWithCounterNonce, error) {
return &GCMWithCounterNonce{g: *g}, nil return &GCMWithCounterNonce{g: *g}, nil
} }
// NewGCMForTLS12 returns a new AEAD that works like GCM, but enforces the
// construction of nonces as specified in RFC 5288, Section 3 and RFC 9325,
// Section 7.2.1.
//
// This complies with FIPS 140-3 IG C.H Scenario 1.a.
func NewGCMForTLS12(cipher *aes.Block) (*GCMWithCounterNonce, error) {
g, err := newGCM(&GCM{}, cipher, gcmStandardNonceSize, gcmTagSize)
if err != nil {
return nil, err
}
// TLS 1.2 counters always start at zero.
return &GCMWithCounterNonce{g: *g, startReady: true}, nil
}
// NewGCMForSSH returns a new AEAD that works like GCM, but enforces the
// construction of nonces as specified in RFC 5647.
//
// This complies with FIPS 140-3 IG C.H Scenario 1.d.
func NewGCMForSSH(cipher *aes.Block) (*GCMWithCounterNonce, error) {
g, err := newGCM(&GCM{}, cipher, gcmStandardNonceSize, gcmTagSize)
if err != nil {
return nil, err
}
return &GCMWithCounterNonce{g: *g}, nil
}
type GCMWithCounterNonce struct { type GCMWithCounterNonce struct {
g GCM g GCM
ready bool prefixReady bool
fixedName uint32 prefix uint32
start uint64 startReady bool
next uint64 start uint64
next uint64
} }
func (g *GCMWithCounterNonce) NonceSize() int { return gcmStandardNonceSize } func (g *GCMWithCounterNonce) NonceSize() int { return gcmStandardNonceSize }
func (g *GCMWithCounterNonce) Overhead() int { return gcmTagSize } func (g *GCMWithCounterNonce) Overhead() int { return gcmTagSize }
// Seal implements the [cipher.AEAD] interface, checking that the nonce prefix
// is stable and that the counter is strictly increasing.
//
// It is not safe for concurrent use.
func (g *GCMWithCounterNonce) Seal(dst, nonce, plaintext, data []byte) []byte { func (g *GCMWithCounterNonce) Seal(dst, nonce, plaintext, data []byte) []byte {
if len(nonce) != gcmStandardNonceSize { if len(nonce) != gcmStandardNonceSize {
panic("crypto/cipher: incorrect nonce length given to GCM") panic("crypto/cipher: incorrect nonce length given to GCM")
} }
counter := byteorder.BEUint64(nonce[len(nonce)-8:]) if !g.prefixReady {
if !g.ready { // The first invocation sets the fixed prefix.
// The first invocation sets the fixed name encoding and start counter. g.prefixReady = true
g.ready = true g.prefix = byteorder.BEUint32(nonce[:4])
g.start = counter
g.fixedName = byteorder.BEUint32(nonce[:4])
} }
if g.fixedName != byteorder.BEUint32(nonce[:4]) { if g.prefix != byteorder.BEUint32(nonce[:4]) {
panic("crypto/cipher: incorrect module name given to GCMWithCounterNonce") panic("crypto/cipher: GCM nonce prefix changed")
}
counter := byteorder.BEUint64(nonce[len(nonce)-8:])
if !g.startReady {
// The first invocation sets the starting counter, if not fixed.
g.startReady = true
g.start = counter
} }
counter -= g.start counter -= g.start
// Ensure the counter is monotonically increasing. // Ensure the counter is strictly increasing.
if counter == math.MaxUint64 { if counter == math.MaxUint64 {
panic("crypto/cipher: counter wrapped") panic("crypto/cipher: counter exhausted")
} }
if counter < g.next { if counter < g.next {
panic("crypto/cipher: counter decreased") panic("crypto/cipher: counter decreased or remained the same")
} }
g.next = counter + 1 g.next = counter + 1
@ -103,93 +142,122 @@ func (g *GCMWithCounterNonce) Open(dst, nonce, ciphertext, data []byte) ([]byte,
return g.g.Open(dst, nonce, ciphertext, data) return g.g.Open(dst, nonce, ciphertext, data)
} }
// NewGCMForTLS12 returns a new AEAD that works like GCM, but enforces the // NewGCMWithXORCounterNonce returns a new AEAD that works like GCM, but
// construction of nonces as specified in RFC 5288, Section 3 and RFC 9325, // enforces the construction of deterministic nonces. The nonce must be 96 bits,
// Section 7.2.1. // the first 32 bits must be an encoding of the module name, and the last 64
// bits must be a counter XOR'd with a fixed value. The module name and XOR mask
// can be set with [GCMWithCounterNonce.SetNoncePrefixAndMask], or they are set
// on the first call to Seal, assuming the counter starts at zero. Each
// subsequent call must increment the counter as a big-endian uint64. If the
// counter reaches 2⁶⁴ minus one, Seal will panic.
// //
// This complies with FIPS 140-3 IG C.H Scenario 1.a. // This complies with FIPS 140-3 IG C.H Scenario 3.
func NewGCMForTLS12(cipher *aes.Block) (*GCMForTLS12, error) { func NewGCMWithXORCounterNonce(cipher *aes.Block) (*GCMWithXORCounterNonce, error) {
g, err := newGCM(&GCM{}, cipher, gcmStandardNonceSize, gcmTagSize) g, err := newGCM(&GCM{}, cipher, gcmStandardNonceSize, gcmTagSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &GCMForTLS12{g: *g}, nil return &GCMWithXORCounterNonce{g: *g}, nil
}
type GCMForTLS12 struct {
g GCM
next uint64
}
func (g *GCMForTLS12) NonceSize() int { return gcmStandardNonceSize }
func (g *GCMForTLS12) Overhead() int { return gcmTagSize }
func (g *GCMForTLS12) Seal(dst, nonce, plaintext, data []byte) []byte {
if len(nonce) != gcmStandardNonceSize {
panic("crypto/cipher: incorrect nonce length given to GCM")
}
counter := byteorder.BEUint64(nonce[len(nonce)-8:])
// Ensure the counter is monotonically increasing.
if counter == math.MaxUint64 {
panic("crypto/cipher: counter wrapped")
}
if counter < g.next {
panic("crypto/cipher: counter decreased")
}
g.next = counter + 1
fips140.RecordApproved()
return g.g.sealAfterIndicator(dst, nonce, plaintext, data)
}
func (g *GCMForTLS12) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) {
fips140.RecordApproved()
return g.g.Open(dst, nonce, ciphertext, data)
} }
// NewGCMForTLS13 returns a new AEAD that works like GCM, but enforces the // NewGCMForTLS13 returns a new AEAD that works like GCM, but enforces the
// construction of nonces as specified in RFC 8446, Section 5.3. // construction of nonces as specified in RFC 8446, Section 5.3.
func NewGCMForTLS13(cipher *aes.Block) (*GCMForTLS13, error) { //
// This complies with FIPS 140-3 IG C.H Scenario 1.a.
func NewGCMForTLS13(cipher *aes.Block) (*GCMWithXORCounterNonce, error) {
g, err := newGCM(&GCM{}, cipher, gcmStandardNonceSize, gcmTagSize) g, err := newGCM(&GCM{}, cipher, gcmStandardNonceSize, gcmTagSize)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &GCMForTLS13{g: *g}, nil return &GCMWithXORCounterNonce{g: *g}, nil
} }
type GCMForTLS13 struct { // NewGCMForQUIC returns a new AEAD that works like GCM, but enforces the
g GCM // construction of nonces as specified in RFC 9001, Section 5.3.
ready bool //
mask uint64 // Unlike in TLS 1.3, the QUIC nonce counter does not always start at zero, as
next uint64 // the packet number does not reset on key updates, so the XOR mask must be
// provided explicitly instead of being learned on the first Seal call. Note
// that the nonce passed to Seal must already be XOR'd with the IV, the IV is
// provided here only to allow Seal to enforce that the counter is strictly
// increasing.
//
// This complies with FIPS 140-3 IG C.H Scenario 5.
func NewGCMForQUIC(cipher *aes.Block, iv []byte) (*GCMWithXORCounterNonce, error) {
g, err := newGCM(&GCM{}, cipher, gcmStandardNonceSize, gcmTagSize)
if err != nil {
return nil, err
}
gcm := &GCMWithXORCounterNonce{g: *g}
if err := gcm.SetNoncePrefixAndMask(iv); err != nil {
return nil, err
}
return gcm, nil
} }
func (g *GCMForTLS13) NonceSize() int { return gcmStandardNonceSize } type GCMWithXORCounterNonce struct {
g GCM
ready bool
prefix uint32
mask uint64
next uint64
}
func (g *GCMForTLS13) Overhead() int { return gcmTagSize } // SetNoncePrefixAndMask sets the fixed prefix and XOR mask for the nonces used
// in Seal. It must be called before the first call to Seal.
//
// The first 32 bits of nonce are used as the fixed prefix, and the last 64 bits
// are used as the XOR mask.
//
// Note that Seal expects the nonce to be already XOR'd with the mask. The mask
// is provided here only to allow Seal to enforce that the counter is strictly
// increasing.
func (g *GCMWithXORCounterNonce) SetNoncePrefixAndMask(nonce []byte) error {
if len(nonce) != gcmStandardNonceSize {
return errors.New("crypto/cipher: incorrect nonce length given to SetNoncePrefixAndMask")
}
if g.ready {
return errors.New("crypto/cipher: SetNoncePrefixAndMask called twice or after first Seal")
}
g.prefix = byteorder.BEUint32(nonce[:4])
g.mask = byteorder.BEUint64(nonce[4:])
g.ready = true
return nil
}
func (g *GCMForTLS13) Seal(dst, nonce, plaintext, data []byte) []byte { func (g *GCMWithXORCounterNonce) NonceSize() int { return gcmStandardNonceSize }
func (g *GCMWithXORCounterNonce) Overhead() int { return gcmTagSize }
// Seal implements the [cipher.AEAD] interface, checking that the nonce prefix
// is stable and that the counter is strictly increasing.
//
// It is not safe for concurrent use.
func (g *GCMWithXORCounterNonce) Seal(dst, nonce, plaintext, data []byte) []byte {
if len(nonce) != gcmStandardNonceSize { if len(nonce) != gcmStandardNonceSize {
panic("crypto/cipher: incorrect nonce length given to GCM") panic("crypto/cipher: incorrect nonce length given to GCM")
} }
counter := byteorder.BEUint64(nonce[len(nonce)-8:]) counter := byteorder.BEUint64(nonce[len(nonce)-8:])
if !g.ready { if !g.ready {
// In the first call, the counter is zero, so we learn the XOR mask. // In the first call, if [GCMWithXORCounterNonce.SetNoncePrefixAndMask]
// wasn't used, we assume the counter is zero to learn the XOR mask and
// fixed prefix.
g.ready = true g.ready = true
g.mask = counter g.mask = counter
g.prefix = byteorder.BEUint32(nonce[:4])
}
if g.prefix != byteorder.BEUint32(nonce[:4]) {
panic("crypto/cipher: GCM nonce prefix changed")
} }
counter ^= g.mask counter ^= g.mask
// Ensure the counter is monotonically increasing. // Ensure the counter is strictly increasing.
if counter == math.MaxUint64 { if counter == math.MaxUint64 {
panic("crypto/cipher: counter wrapped") panic("crypto/cipher: counter exhausted")
} }
if counter < g.next { if counter < g.next {
panic("crypto/cipher: counter decreased") panic("crypto/cipher: counter decreased or remained the same")
} }
g.next = counter + 1 g.next = counter + 1
@ -197,61 +265,7 @@ func (g *GCMForTLS13) Seal(dst, nonce, plaintext, data []byte) []byte {
return g.g.sealAfterIndicator(dst, nonce, plaintext, data) return g.g.sealAfterIndicator(dst, nonce, plaintext, data)
} }
func (g *GCMForTLS13) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { func (g *GCMWithXORCounterNonce) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) {
fips140.RecordApproved()
return g.g.Open(dst, nonce, ciphertext, data)
}
// NewGCMForSSH returns a new AEAD that works like GCM, but enforces the
// construction of nonces as specified in RFC 5647.
//
// This complies with FIPS 140-3 IG C.H Scenario 1.d.
func NewGCMForSSH(cipher *aes.Block) (*GCMForSSH, error) {
g, err := newGCM(&GCM{}, cipher, gcmStandardNonceSize, gcmTagSize)
if err != nil {
return nil, err
}
return &GCMForSSH{g: *g}, nil
}
type GCMForSSH struct {
g GCM
ready bool
start uint64
next uint64
}
func (g *GCMForSSH) NonceSize() int { return gcmStandardNonceSize }
func (g *GCMForSSH) Overhead() int { return gcmTagSize }
func (g *GCMForSSH) Seal(dst, nonce, plaintext, data []byte) []byte {
if len(nonce) != gcmStandardNonceSize {
panic("crypto/cipher: incorrect nonce length given to GCM")
}
counter := byteorder.BEUint64(nonce[len(nonce)-8:])
if !g.ready {
// In the first call we learn the start value.
g.ready = true
g.start = counter
}
counter -= g.start
// Ensure the counter is monotonically increasing.
if counter == math.MaxUint64 {
panic("crypto/cipher: counter wrapped")
}
if counter < g.next {
panic("crypto/cipher: counter decreased")
}
g.next = counter + 1
fips140.RecordApproved()
return g.g.sealAfterIndicator(dst, nonce, plaintext, data)
}
func (g *GCMForSSH) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) {
fips140.RecordApproved() fips140.RecordApproved()
return g.g.Open(dst, nonce, ciphertext, data) return g.g.Open(dst, nonce, ciphertext, data)
} }