From 4b2716348cfcc336469d38ce9a8e23f0b909ab05 Mon Sep 17 00:00:00 2001 From: Manuel Sabin Date: Mon, 24 Jun 2024 13:58:35 -0400 Subject: [PATCH] crypto/internal/cryptotest: add tests for the cipher.BlockMode interface This CL creates tests for the cipher.BlockMode interface in the new cryptotest package. This set of tests is called from the tests of implementations of the BlockMode interface e.g. cbc_test.go Updates #25309 Change-Id: I3685bbee24d08d66f5bb4b7f001cbf520c844881 Reviewed-on: https://go-review.googlesource.com/c/go/+/595120 Reviewed-by: Filippo Valsorda Auto-Submit: Filippo Valsorda Reviewed-by: Russell Webb LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Knyszek Reviewed-by: Dmitri Shuralyov --- src/crypto/cipher/cbc_test.go | 57 +++++ src/crypto/internal/cryptotest/blockmode.go | 224 ++++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 src/crypto/cipher/cbc_test.go create mode 100644 src/crypto/internal/cryptotest/blockmode.go diff --git a/src/crypto/cipher/cbc_test.go b/src/crypto/cipher/cbc_test.go new file mode 100644 index 0000000000..e6666d2cff --- /dev/null +++ b/src/crypto/cipher/cbc_test.go @@ -0,0 +1,57 @@ +// Copyright 2024 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. + +package cipher_test + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/des" + "crypto/internal/cryptotest" + "fmt" + "io" + "math/rand" + "testing" + "time" +) + +// Test CBC Blockmode against the general cipher.BlockMode interface tester +func TestCBCBlockMode(t *testing.T) { + for _, keylen := range []int{128, 192, 256} { + + t.Run(fmt.Sprintf("AES-%d", keylen), func(t *testing.T) { + rng := newRandReader(t) + + key := make([]byte, keylen/8) + rng.Read(key) + + block, err := aes.NewCipher(key) + if err != nil { + panic(err) + } + + cryptotest.TestBlockMode(t, block, cipher.NewCBCEncrypter, cipher.NewCBCDecrypter) + }) + } + + t.Run("DES", func(t *testing.T) { + rng := newRandReader(t) + + key := make([]byte, 8) + rng.Read(key) + + block, err := des.NewCipher(key) + if err != nil { + panic(err) + } + + cryptotest.TestBlockMode(t, block, cipher.NewCBCEncrypter, cipher.NewCBCDecrypter) + }) +} + +func newRandReader(t *testing.T) io.Reader { + seed := time.Now().UnixNano() + t.Logf("Deterministic RNG seed: 0x%x", seed) + return rand.New(rand.NewSource(seed)) +} diff --git a/src/crypto/internal/cryptotest/blockmode.go b/src/crypto/internal/cryptotest/blockmode.go new file mode 100644 index 0000000000..d3271e583f --- /dev/null +++ b/src/crypto/internal/cryptotest/blockmode.go @@ -0,0 +1,224 @@ +// Copyright 2024 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. + +package cryptotest + +import ( + "bytes" + "crypto/cipher" + "testing" +) + +// MakeBlockMode returns a cipher.BlockMode instance. +// It expects len(iv) == b.BlockSize(). +type MakeBlockMode func(b cipher.Block, iv []byte) cipher.BlockMode + +// TestBlockMode performs a set of tests on cipher.BlockMode implementations, +// checking the documented requirements of CryptBlocks. +func TestBlockMode(t *testing.T, block cipher.Block, makeEncrypter, makeDecrypter MakeBlockMode) { + rng := newRandReader(t) + iv := make([]byte, block.BlockSize()) + rng.Read(iv) + + testBlockModePair(t, block, makeEncrypter, makeDecrypter, iv) +} + +func testBlockModePair(t *testing.T, b cipher.Block, enc, dec MakeBlockMode, iv []byte) { + t.Run("Encryption", func(t *testing.T) { + testBlockMode(t, enc, b, iv) + }) + + t.Run("Decryption", func(t *testing.T) { + testBlockMode(t, dec, b, iv) + }) + + t.Run("Roundtrip", func(t *testing.T) { + rng := newRandReader(t) + + blockSize := enc(b, iv).BlockSize() + if decBlockSize := dec(b, iv).BlockSize(); decBlockSize != blockSize { + t.Errorf("decryption blocksize different than encryption's; got %d, want %d", decBlockSize, blockSize) + } + + before, dst, after := make([]byte, blockSize*2), make([]byte, blockSize*2), make([]byte, blockSize*2) + rng.Read(before) + + enc(b, iv).CryptBlocks(dst, before) + dec(b, iv).CryptBlocks(after, dst) + if !bytes.Equal(after, before) { + t.Errorf("plaintext is different after an encrypt/decrypt cycle; got %x, want %x", after, before) + } + }) +} + +func testBlockMode(t *testing.T, bm MakeBlockMode, b cipher.Block, iv []byte) { + blockSize := bm(b, iv).BlockSize() + + t.Run("WrongIVLen", func(t *testing.T) { + iv := make([]byte, b.BlockSize()+1) + mustPanic(t, "IV length must equal block size", func() { bm(b, iv) }) + }) + + t.Run("AlterInput", func(t *testing.T) { + rng := newRandReader(t) + + src, dst, before := make([]byte, blockSize*2), make([]byte, blockSize*2), make([]byte, blockSize*2) + + for _, length := range []int{0, blockSize, blockSize * 2} { + rng.Read(src) + copy(before, src) + + bm(b, iv).CryptBlocks(dst[:length], src[:length]) + if !bytes.Equal(src, before) { + t.Errorf("CryptBlocks modified src; got %x, want %x", src, before) + } + } + }) + + t.Run("Aliasing", func(t *testing.T) { + rng := newRandReader(t) + + buff, expectedOutput := make([]byte, blockSize*2), make([]byte, blockSize*2) + + for _, length := range []int{0, blockSize, blockSize * 2} { + // Record what output is when src and dst are different + rng.Read(buff) + bm(b, iv).CryptBlocks(expectedOutput[:length], buff[:length]) + + // Check that the same output is generated when src=dst alias to the same + // memory + bm(b, iv).CryptBlocks(buff[:length], buff[:length]) + if !bytes.Equal(buff[:length], expectedOutput[:length]) { + t.Errorf("block cipher produced different output when dst = src; got %x, want %x", buff[:length], expectedOutput[:length]) + } + } + }) + + t.Run("OutOfBoundsWrite", func(t *testing.T) { // Issue 21104 + rng := newRandReader(t) + + src := make([]byte, blockSize) + rng.Read(src) + + // Make a buffer with dst in the middle and data on either end + buff := make([]byte, blockSize*3) + endOfPrefix, startOfSuffix := blockSize, blockSize*2 + rng.Read(buff[:endOfPrefix]) + rng.Read(buff[startOfSuffix:]) + dst := buff[endOfPrefix:startOfSuffix] + + // Record the prefix and suffix data to make sure they aren't written to + initPrefix, initSuffix := make([]byte, blockSize), make([]byte, blockSize) + copy(initPrefix, buff[:endOfPrefix]) + copy(initSuffix, buff[startOfSuffix:]) + + // Write to dst (the middle of the buffer) and make sure it doesn't write + // beyond the dst slice on a valid CryptBlocks call + bm(b, iv).CryptBlocks(dst, src) + if !bytes.Equal(buff[startOfSuffix:], initSuffix) { + t.Errorf("block cipher did out of bounds write after end of dst slice; got %x, want %x", buff[startOfSuffix:], initSuffix) + } + if !bytes.Equal(buff[:endOfPrefix], initPrefix) { + t.Errorf("block cipher did out of bounds write before beginning of dst slice; got %x, want %x", buff[:endOfPrefix], initPrefix) + } + + // Check that dst isn't written to beyond len(src) even if there is room in + // the slice + dst = buff[endOfPrefix:] // Extend dst to include suffix + bm(b, iv).CryptBlocks(dst, src) + if !bytes.Equal(buff[startOfSuffix:], initSuffix) { + t.Errorf("CryptBlocks modified dst past len(src); got %x, want %x", buff[startOfSuffix:], initSuffix) + } + + // Issue 21104: Shouldn't write to anything outside of dst even if src is bigger + src = make([]byte, blockSize*3) + rng.Read(src) + + mustPanic(t, "output smaller than input", func() { + bm(b, iv).CryptBlocks(dst, src) + }) + + if !bytes.Equal(buff[startOfSuffix:], initSuffix) { + t.Errorf("block cipher did out of bounds write after end of dst slice; got %x, want %x", buff[startOfSuffix:], initSuffix) + } + if !bytes.Equal(buff[:endOfPrefix], initPrefix) { + t.Errorf("block cipher did out of bounds write before beginning of dst slice; got %x, want %x", buff[:endOfPrefix], initPrefix) + } + }) + + // Check that output of cipher isn't affected by adjacent data beyond input + // slice scope + t.Run("OutOfBoundsRead", func(t *testing.T) { + rng := newRandReader(t) + + src := make([]byte, blockSize) + rng.Read(src) + expectedDst := make([]byte, blockSize) + bm(b, iv).CryptBlocks(expectedDst, src) + + // Make a buffer with src in the middle and data on either end + buff := make([]byte, blockSize*3) + endOfPrefix, startOfSuffix := blockSize, blockSize*2 + + copy(buff[endOfPrefix:startOfSuffix], src) + rng.Read(buff[:endOfPrefix]) + rng.Read(buff[startOfSuffix:]) + + testDst := make([]byte, blockSize) + bm(b, iv).CryptBlocks(testDst, buff[endOfPrefix:startOfSuffix]) + + if !bytes.Equal(testDst, expectedDst) { + t.Errorf("CryptBlocks affected by data outside of src slice bounds; got %x, want %x", testDst, expectedDst) + } + }) + + t.Run("BufferOverlap", func(t *testing.T) { + rng := newRandReader(t) + + buff := make([]byte, blockSize*2) + rng.Read(buff) + + // Make src and dst slices point to same array with inexact overlap + src := buff[:blockSize] + dst := buff[1 : blockSize+1] + mustPanic(t, "invalid buffer overlap", func() { bm(b, iv).CryptBlocks(dst, src) }) + + // Only overlap on one byte + src = buff[:blockSize] + dst = buff[blockSize-1 : 2*blockSize-1] + mustPanic(t, "invalid buffer overlap", func() { bm(b, iv).CryptBlocks(dst, src) }) + + // src comes after dst with one byte overlap + src = buff[blockSize-1 : 2*blockSize-1] + dst = buff[:blockSize] + mustPanic(t, "invalid buffer overlap", func() { bm(b, iv).CryptBlocks(dst, src) }) + }) + + // Input to CryptBlocks should be a multiple of BlockSize + t.Run("PartialBlocks", func(t *testing.T) { + // Check a few cases of not being a multiple of BlockSize + for _, srcSize := range []int{blockSize - 1, blockSize + 1, 2*blockSize - 1, 2*blockSize + 1} { + src := make([]byte, srcSize) + dst := make([]byte, 3*blockSize) // Make a dst large enough for all src + mustPanic(t, "input not full blocks", func() { bm(b, iv).CryptBlocks(dst, src) }) + } + }) + + t.Run("KeepState", func(t *testing.T) { + rng := newRandReader(t) + + src, serialDst, compositeDst := make([]byte, blockSize*4), make([]byte, blockSize*4), make([]byte, blockSize*4) + rng.Read(src) + + length, block := 2*blockSize, bm(b, iv) + block.CryptBlocks(serialDst, src[:length]) + block.CryptBlocks(serialDst[length:], src[length:]) + + bm(b, iv).CryptBlocks(compositeDst, src) + + if !bytes.Equal(serialDst, compositeDst) { + t.Errorf("two successive CryptBlocks calls returned a different result than a single one; got %x, want %x", serialDst, compositeDst) + } + }) +} -- 2.48.1