From f916d93e415068e0ec286249f96e6164ad822731 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Mon, 18 Nov 2024 16:19:12 +0100 Subject: [PATCH] crypto/cipher: add NewGCMWithRandomNonce Fixes #69981 Change-Id: I0cad11f5d7673304c5a6d85fc598ddc27ab93738 Reviewed-on: https://go-review.googlesource.com/c/go/+/629175 LUCI-TryBot-Result: Go LUCI Auto-Submit: Filippo Valsorda Reviewed-by: Roland Shoemaker Reviewed-by: Russ Cox --- api/next/69981.txt | 1 + .../6-stdlib/99-minor/crypto/cipher/69981.md | 3 + src/crypto/cipher/gcm.go | 117 ++++++++++++++++++ src/crypto/cipher/gcm_test.go | 9 ++ src/crypto/internal/cryptotest/aead.go | 26 +++- 5 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 api/next/69981.txt create mode 100644 doc/next/6-stdlib/99-minor/crypto/cipher/69981.md diff --git a/api/next/69981.txt b/api/next/69981.txt new file mode 100644 index 0000000000..b295c63256 --- /dev/null +++ b/api/next/69981.txt @@ -0,0 +1 @@ +pkg crypto/cipher, func NewGCMWithRandomNonce(Block) (AEAD, error) #69981 diff --git a/doc/next/6-stdlib/99-minor/crypto/cipher/69981.md b/doc/next/6-stdlib/99-minor/crypto/cipher/69981.md new file mode 100644 index 0000000000..7ef619c4d5 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/crypto/cipher/69981.md @@ -0,0 +1,3 @@ +The new [NewGCMWithRandomNonce] function returns an [AEAD] that implements +AES-GCM by generating a random nonce during Seal and prepending it to the +ciphertext. diff --git a/src/crypto/cipher/gcm.go b/src/crypto/cipher/gcm.go index d638fa94a4..c75e8eddd1 100644 --- a/src/crypto/cipher/gcm.go +++ b/src/crypto/cipher/gcm.go @@ -67,6 +67,123 @@ func newGCM(cipher Block, nonceSize, tagSize int) (AEAD, error) { return g, nil } +// NewGCMWithRandomNonce returns the given cipher wrapped in Galois Counter +// Mode, with randomly-generated nonces. The cipher must have been created by +// [aes.NewCipher]. +// +// It generates a random 96-bit nonce, which is prepended to the ciphertext by Seal, +// and is extracted from the ciphertext by Open. The NonceSize of the AEAD is zero, +// while the Overhead is 28 bytes (the combination of nonce size and tag size). +// +// A given key MUST NOT be used to encrypt more than 2^32 messages, to limit the +// risk of a random nonce collision to negligible levels. +func NewGCMWithRandomNonce(cipher Block) (AEAD, error) { + c, ok := cipher.(*aes.Block) + if !ok { + return nil, errors.New("cipher: NewGCMWithRandomNonce requires aes.Block") + } + g, err := gcm.New(c, gcmStandardNonceSize, gcmTagSize) + if err != nil { + return nil, err + } + return gcmWithRandomNonce{g}, nil +} + +type gcmWithRandomNonce struct { + *gcm.GCM +} + +func (g gcmWithRandomNonce) NonceSize() int { + return 0 +} + +func (g gcmWithRandomNonce) Overhead() int { + return gcmStandardNonceSize + gcmTagSize +} + +func (g gcmWithRandomNonce) Seal(dst, nonce, plaintext, additionalData []byte) []byte { + if len(nonce) != 0 { + panic("crypto/cipher: non-empty nonce passed to GCMWithRandomNonce") + } + + ret, out := sliceForAppend(dst, gcmStandardNonceSize+len(plaintext)+gcmTagSize) + if alias.InexactOverlap(out, plaintext) { + panic("crypto/cipher: invalid buffer overlap of output and input") + } + if alias.AnyOverlap(out, additionalData) { + panic("crypto/cipher: invalid buffer overlap of output and additional data") + } + nonce = out[:gcmStandardNonceSize] + ciphertext := out[gcmStandardNonceSize:] + + // The AEAD interface allows using plaintext[:0] or ciphertext[:0] as dst. + // + // This is kind of a problem when trying to prepend or trim a nonce, because the + // actual AES-GCTR blocks end up overlapping but not exactly. + // + // In Open, we write the output *before* the input, so unless we do something + // weird like working through a chunk of block backwards, it works out. + // + // In Seal, we could work through the input backwards or intentionally load + // ahead before writing. + // + // However, the crypto/internal/fips/aes/gcm APIs also check for exact overlap, + // so for now we just do a memmove if we detect overlap. + // + // ┌───────────────────────────┬ ─ ─ + // │PPPPPPPPPPPPPPPPPPPPPPPPPPP│ │ + // └▽─────────────────────────▲┴ ─ ─ + // ╲ Seal ╲ + // ╲ Open ╲ + // ┌───▼─────────────────────────△──┐ + // │NN|CCCCCCCCCCCCCCCCCCCCCCCCCCC|T│ + // └────────────────────────────────┘ + // + if alias.AnyOverlap(out, plaintext) { + copy(ciphertext, plaintext) + plaintext = ciphertext[:len(plaintext)] + } + + gcm.SealWithRandomNonce(g.GCM, nonce, ciphertext, plaintext, additionalData) + return ret +} + +func (g gcmWithRandomNonce) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { + if len(nonce) != 0 { + panic("crypto/cipher: non-empty nonce passed to GCMWithRandomNonce") + } + if len(ciphertext) < gcmStandardNonceSize+gcmTagSize { + return nil, errOpen + } + + ret, out := sliceForAppend(dst, len(ciphertext)-gcmStandardNonceSize-gcmTagSize) + if alias.InexactOverlap(out, ciphertext) { + panic("crypto/cipher: invalid buffer overlap of output and input") + } + if alias.AnyOverlap(out, additionalData) { + panic("crypto/cipher: invalid buffer overlap of output and additional data") + } + // See the discussion in Seal. Note that if there is any overlap at this + // point, it's because out = ciphertext, so out must have enough capacity + // even if we sliced the tag off. Also note how [AEAD] specifies that "the + // contents of dst, up to its capacity, may be overwritten". + if alias.AnyOverlap(out, ciphertext) { + nonce = make([]byte, gcmStandardNonceSize) + copy(nonce, ciphertext) + copy(out[:len(ciphertext)], ciphertext[gcmStandardNonceSize:]) + ciphertext = out[:len(ciphertext)-gcmStandardNonceSize] + } else { + nonce = ciphertext[:gcmStandardNonceSize] + ciphertext = ciphertext[gcmStandardNonceSize:] + } + + _, err := g.GCM.Open(out[:0], nonce, ciphertext, additionalData) + if err != nil { + return nil, err + } + return ret, nil +} + // gcmAble is an interface implemented by ciphers that have a specific optimized // implementation of GCM. crypto/aes doesn't use this anymore, and we'd like to // eventually remove it. diff --git a/src/crypto/cipher/gcm_test.go b/src/crypto/cipher/gcm_test.go index 14bf54c582..f6679f3d42 100644 --- a/src/crypto/cipher/gcm_test.go +++ b/src/crypto/cipher/gcm_test.go @@ -8,6 +8,7 @@ import ( "bytes" "crypto/aes" "crypto/cipher" + "crypto/internal/boring" "crypto/internal/cryptotest" "crypto/internal/fips" fipsaes "crypto/internal/fips/aes" @@ -723,6 +724,14 @@ func testGCMAEAD(t *testing.T, newCipher func(key []byte) cipher.Block) { cryptotest.TestAEAD(t, func() (cipher.AEAD, error) { return cipher.NewGCMWithNonceSize(block, nonceSize) }) }) } + + // Test NewGCMWithRandomNonce. + t.Run("GCMWithRandomNonce", func(t *testing.T) { + if _, ok := block.(*wrapper); ok || boring.Enabled { + t.Skip("NewGCMWithRandomNonce requires an AES block cipher") + } + cryptotest.TestAEAD(t, func() (cipher.AEAD, error) { return cipher.NewGCMWithRandomNonce(block) }) + }) }) } } diff --git a/src/crypto/internal/cryptotest/aead.go b/src/crypto/internal/cryptotest/aead.go index 85a9c92cf2..8988b7224f 100644 --- a/src/crypto/internal/cryptotest/aead.go +++ b/src/crypto/internal/cryptotest/aead.go @@ -208,10 +208,12 @@ func TestAEAD(t *testing.T, mAEAD MakeAEAD) { t.Errorf("Seal alters dst instead of appending; got %s, want %s", truncateHex(out[:len(prefix)]), truncateHex(prefix)) } - ciphertext := out[len(prefix):] - // Check that the appended ciphertext wasn't affected by the prefix - if expectedCT := sealMsg(t, aead, nil, nonce, plaintext, addData); !bytes.Equal(ciphertext, expectedCT) { - t.Errorf("Seal behavior affected by pre-existing data in dst; got %s, want %s", truncateHex(ciphertext), truncateHex(expectedCT)) + if isDeterministic(aead) { + ciphertext := out[len(prefix):] + // Check that the appended ciphertext wasn't affected by the prefix + if expectedCT := sealMsg(t, aead, nil, nonce, plaintext, addData); !bytes.Equal(ciphertext, expectedCT) { + t.Errorf("Seal behavior affected by pre-existing data in dst; got %s, want %s", truncateHex(ciphertext), truncateHex(expectedCT)) + } } } }) @@ -254,7 +256,9 @@ func TestAEAD(t *testing.T, mAEAD MakeAEAD) { }) t.Run("WrongNonce", func(t *testing.T) { - + if aead.NonceSize() == 0 { + t.Skip("AEAD does not use a nonce") + } // Test all combinations of plaintext and additional data lengths. for _, ptLen := range lengths { for _, adLen := range lengths { @@ -372,6 +376,18 @@ func sealMsg(t *testing.T, aead cipher.AEAD, ciphertext, nonce, plaintext, addDa return ciphertext } +func isDeterministic(aead cipher.AEAD) bool { + // Check if the AEAD is deterministic by checking if the same plaintext + // encrypted with the same nonce and additional data produces the same + // ciphertext. + nonce := make([]byte, aead.NonceSize()) + addData := []byte("additional data") + plaintext := []byte("plaintext") + ciphertext1 := aead.Seal(nil, nonce, plaintext, addData) + ciphertext2 := aead.Seal(nil, nonce, plaintext, addData) + return bytes.Equal(ciphertext1, ciphertext2) +} + // Helper function to Open and authenticate ciphertext. Checks that Open // doesn't error (assuming ciphertext was well-formed with corresponding nonce // and additional data). -- 2.48.1