]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/cipher: add NewGCMWithRandomNonce
authorFilippo Valsorda <filippo@golang.org>
Mon, 18 Nov 2024 15:19:12 +0000 (16:19 +0100)
committerGopher Robot <gobot@golang.org>
Tue, 19 Nov 2024 16:26:40 +0000 (16:26 +0000)
Fixes #69981

Change-Id: I0cad11f5d7673304c5a6d85fc598ddc27ab93738
Reviewed-on: https://go-review.googlesource.com/c/go/+/629175
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
api/next/69981.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/crypto/cipher/69981.md [new file with mode: 0644]
src/crypto/cipher/gcm.go
src/crypto/cipher/gcm_test.go
src/crypto/internal/cryptotest/aead.go

diff --git a/api/next/69981.txt b/api/next/69981.txt
new file mode 100644 (file)
index 0000000..b295c63
--- /dev/null
@@ -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 (file)
index 0000000..7ef619c
--- /dev/null
@@ -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.
index d638fa94a4f543d08207fa4061872e6be5874c49..c75e8eddd146f5009e7b911d6c897733439dc9a0 100644 (file)
@@ -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.
index 14bf54c582712d9c6856214f82def582064a2a63..f6679f3d422772e2d361012407f286eb2dc01136 100644 (file)
@@ -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) })
+                       })
                })
        }
 }
index 85a9c92cf228ba33058173f8dc32b2ecfcda838b..8988b7224f3bbfe04b09987ebb29a7a859aa8107 100644 (file)
@@ -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).