acvptool: add CS3 support.
CS3 is ciphertext-stealing variant three from SP 800-38A.
Change-Id: I992dc22778c91efad361f25ff65ae5966fc447c6
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/49505
Commit-Queue: Adam Langley <agl@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/util/fipstools/acvp/ACVP.md b/util/fipstools/acvp/ACVP.md
index d1255ca..ba5bdbb 100644
--- a/util/fipstools/acvp/ACVP.md
+++ b/util/fipstools/acvp/ACVP.md
@@ -49,6 +49,8 @@
| 3DES/encrypt | Key, input block, num iterations¹ | Result, Previous result |
| AES-CBC/decrypt | Key, ciphertext, IV, num iterations¹ | Result, Previous result |
| AES-CBC/encrypt | Key, plaintext, IV, num iterations¹ | Result, Previous result |
+| AES-CBC-CS3/decrypt | Key, ciphertext, IV, num iterations² | Result |
+| AES-CBC-CS3/encrypt | Key, plaintext, IV, num iterations² | Result |
| AES-CCM/open | Tag length, key, ciphertext, nonce, ad | One-byte success flag, plaintext or empty |
| AES-CCM/seal | Tag length, key, plaintext, nonce, ad | Ciphertext |
| AES-CTR/decrypt | Key, ciphertext, initial counter, constant 1 | Plaintext |
@@ -106,6 +108,8 @@
¹ The iterated tests would result in excessive numbers of round trips if the module wrapper handled only basic operations. Thus some ACVP logic is pushed down for these tests so that the inner loop can be handled locally. Either read the NIST documentation ([block-ciphers](https://pages.nist.gov/ACVP/draft-celi-acvp-symmetric.html#name-monte-carlo-tests-for-block) [hashes](https://pages.nist.gov/ACVP/draft-celi-acvp-sha.html#name-monte-carlo-tests-for-sha-1)) to understand the iteration count and return values or, probably more fruitfully, see how these functions are handled in the `modulewrapper` directory.
+² Will always be one because MCT tests are not supported for CS3.
+
## Online operation
If you have credentials to speak to either of the NIST ACVP servers then you can run the tool in online mode.
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index fe74993..844c9c4 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -71,37 +71,38 @@
}
m.primitives = map[string]primitive{
- "SHA-1": &hashPrimitive{"SHA-1", 20},
- "SHA2-224": &hashPrimitive{"SHA2-224", 28},
- "SHA2-256": &hashPrimitive{"SHA2-256", 32},
- "SHA2-384": &hashPrimitive{"SHA2-384", 48},
- "SHA2-512": &hashPrimitive{"SHA2-512", 64},
- "SHA2-512/256": &hashPrimitive{"SHA2-512/256", 32},
- "ACVP-AES-ECB": &blockCipher{"AES", 16, 2, true, false, iterateAES},
- "ACVP-AES-CBC": &blockCipher{"AES-CBC", 16, 2, true, true, iterateAESCBC},
- "ACVP-AES-CTR": &blockCipher{"AES-CTR", 16, 1, false, true, nil},
- "ACVP-AES-XTS": &xts{},
- "ACVP-TDES-ECB": &blockCipher{"3DES-ECB", 8, 3, true, false, iterate3DES},
- "ACVP-TDES-CBC": &blockCipher{"3DES-CBC", 8, 3, true, true, iterate3DESCBC},
- "ACVP-AES-GCM": &aead{"AES-GCM", false},
- "ACVP-AES-GMAC": &aead{"AES-GCM", false},
- "ACVP-AES-CCM": &aead{"AES-CCM", true},
- "ACVP-AES-KW": &aead{"AES-KW", false},
- "ACVP-AES-KWP": &aead{"AES-KWP", false},
- "HMAC-SHA-1": &hmacPrimitive{"HMAC-SHA-1", 20},
- "HMAC-SHA2-224": &hmacPrimitive{"HMAC-SHA2-224", 28},
- "HMAC-SHA2-256": &hmacPrimitive{"HMAC-SHA2-256", 32},
- "HMAC-SHA2-384": &hmacPrimitive{"HMAC-SHA2-384", 48},
- "HMAC-SHA2-512": &hmacPrimitive{"HMAC-SHA2-512", 64},
- "ctrDRBG": &drbg{"ctrDRBG", map[string]bool{"AES-128": true, "AES-192": true, "AES-256": true}},
- "hmacDRBG": &drbg{"hmacDRBG", map[string]bool{"SHA-1": true, "SHA2-224": true, "SHA2-256": true, "SHA2-384": true, "SHA2-512": true}},
- "KDF": &kdfPrimitive{},
- "KAS-KDF": &hkdf{},
- "CMAC-AES": &keyedMACPrimitive{"CMAC-AES"},
- "RSA": &rsa{},
- "kdf-components": &tlsKDF{},
- "KAS-ECC-SSC": &kas{},
- "KAS-FFC-SSC": &kasDH{},
+ "SHA-1": &hashPrimitive{"SHA-1", 20},
+ "SHA2-224": &hashPrimitive{"SHA2-224", 28},
+ "SHA2-256": &hashPrimitive{"SHA2-256", 32},
+ "SHA2-384": &hashPrimitive{"SHA2-384", 48},
+ "SHA2-512": &hashPrimitive{"SHA2-512", 64},
+ "SHA2-512/256": &hashPrimitive{"SHA2-512/256", 32},
+ "ACVP-AES-ECB": &blockCipher{"AES", 16, 2, true, false, iterateAES},
+ "ACVP-AES-CBC": &blockCipher{"AES-CBC", 16, 2, true, true, iterateAESCBC},
+ "ACVP-AES-CBC-CS3": &blockCipher{"AES-CBC-CS3", 16, 1, false, true, iterateAESCBC},
+ "ACVP-AES-CTR": &blockCipher{"AES-CTR", 16, 1, false, true, nil},
+ "ACVP-AES-XTS": &xts{},
+ "ACVP-TDES-ECB": &blockCipher{"3DES-ECB", 8, 3, true, false, iterate3DES},
+ "ACVP-TDES-CBC": &blockCipher{"3DES-CBC", 8, 3, true, true, iterate3DESCBC},
+ "ACVP-AES-GCM": &aead{"AES-GCM", false},
+ "ACVP-AES-GMAC": &aead{"AES-GCM", false},
+ "ACVP-AES-CCM": &aead{"AES-CCM", true},
+ "ACVP-AES-KW": &aead{"AES-KW", false},
+ "ACVP-AES-KWP": &aead{"AES-KWP", false},
+ "HMAC-SHA-1": &hmacPrimitive{"HMAC-SHA-1", 20},
+ "HMAC-SHA2-224": &hmacPrimitive{"HMAC-SHA2-224", 28},
+ "HMAC-SHA2-256": &hmacPrimitive{"HMAC-SHA2-256", 32},
+ "HMAC-SHA2-384": &hmacPrimitive{"HMAC-SHA2-384", 48},
+ "HMAC-SHA2-512": &hmacPrimitive{"HMAC-SHA2-512", 64},
+ "ctrDRBG": &drbg{"ctrDRBG", map[string]bool{"AES-128": true, "AES-192": true, "AES-256": true}},
+ "hmacDRBG": &drbg{"hmacDRBG", map[string]bool{"SHA-1": true, "SHA2-224": true, "SHA2-256": true, "SHA2-384": true, "SHA2-512": true}},
+ "KDF": &kdfPrimitive{},
+ "KAS-KDF": &hkdf{},
+ "CMAC-AES": &keyedMACPrimitive{"CMAC-AES"},
+ "RSA": &rsa{},
+ "kdf-components": &tlsKDF{},
+ "KAS-ECC-SSC": &kas{},
+ "KAS-FFC-SSC": &kasDH{},
}
m.primitives["ECDSA"] = &ecdsa{"ECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives}
diff --git a/util/fipstools/acvp/acvptool/test/expected/ACVP-AES-CBC-CS3.bz2 b/util/fipstools/acvp/acvptool/test/expected/ACVP-AES-CBC-CS3.bz2
new file mode 100644
index 0000000..9a6c4c0
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/expected/ACVP-AES-CBC-CS3.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/test/tests.json b/util/fipstools/acvp/acvptool/test/tests.json
index afd6b8d..f613917 100644
--- a/util/fipstools/acvp/acvptool/test/tests.json
+++ b/util/fipstools/acvp/acvptool/test/tests.json
@@ -1,5 +1,6 @@
[
{"Wrapper": "modulewrapper", "In": "vectors/ACVP-AES-CBC.bz2", "Out": "expected/ACVP-AES-CBC.bz2"},
+{"Wrapper": "testmodulewrapper", "In": "vectors/ACVP-AES-CBC-CS3.bz2", "Out": "expected/ACVP-AES-CBC-CS3.bz2"},
{"Wrapper": "modulewrapper", "In": "vectors/ACVP-AES-CCM.bz2", "Out": "expected/ACVP-AES-CCM.bz2"},
{"Wrapper": "modulewrapper", "In": "vectors/ACVP-AES-CTR.bz2", "Out": "expected/ACVP-AES-CTR.bz2"},
{"Wrapper": "modulewrapper", "In": "vectors/ACVP-AES-ECB.bz2", "Out": "expected/ACVP-AES-ECB.bz2"},
diff --git a/util/fipstools/acvp/acvptool/test/vectors/ACVP-AES-CBC-CS3.bz2 b/util/fipstools/acvp/acvptool/test/vectors/ACVP-AES-CBC-CS3.bz2
new file mode 100644
index 0000000..a46186d
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/test/vectors/ACVP-AES-CBC-CS3.bz2
Binary files differ
diff --git a/util/fipstools/acvp/acvptool/testmodulewrapper/cts_test.go b/util/fipstools/acvp/acvptool/testmodulewrapper/cts_test.go
new file mode 100644
index 0000000..5e7a597
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/testmodulewrapper/cts_test.go
@@ -0,0 +1,95 @@
+// Copyright (c) 2021, Google Inc.
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+package main
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/rand"
+ "encoding/hex"
+ "testing"
+)
+
+func TestCTSRoundTrip(t *testing.T) {
+ var buf [aes.BlockSize * 8]byte
+ var key, iv [16]byte
+ rand.Reader.Read(buf[:])
+ rand.Reader.Read(key[:])
+ rand.Reader.Read(iv[:])
+
+ for i := aes.BlockSize; i < len(buf); i++ {
+ in := buf[:i]
+ ciphertext := doCTSEncrypt(key[:], in[:], iv[:])
+ if len(ciphertext) != len(in) {
+ t.Errorf("incorrect ciphertext length for input length %d", len(in))
+ continue
+ }
+ out := doCTSDecrypt(key[:], ciphertext, iv[:])
+
+ if !bytes.Equal(in[:], out) {
+ t.Errorf("did not round trip for length %d", len(in))
+ }
+ }
+}
+
+func TestCTSVectors(t *testing.T) {
+ tests := []struct {
+ plaintextHex string
+ ciphertextHex string
+ ivHex string
+ }{
+ // Test vectors from OpenSSL.
+ {
+ "4920776f756c64206c696b652074686520",
+ "c6353568f2bf8cb4d8a580362da7ff7f97",
+ "00000000000000000000000000000000",
+ },
+ {
+ "4920776f756c64206c696b65207468652047656e6572616c20476175277320",
+ "fc00783e0efdb2c1d445d4c8eff7ed2297687268d6ecccc0c07b25e25ecfe5",
+ "00000000000000000000000000000000",
+ },
+ {
+ "4920776f756c64206c696b65207468652047656e6572616c2047617527732043",
+ "39312523a78662d5be7fcbcc98ebf5a897687268d6ecccc0c07b25e25ecfe584",
+ "00000000000000000000000000000000",
+ },
+ {
+ "4920776f756c64206c696b65207468652047656e6572616c20476175277320436869636b656e2c20706c656173652c",
+ "97687268d6ecccc0c07b25e25ecfe584b3fffd940c16a18c1b5549d2f838029e39312523a78662d5be7fcbcc98ebf5",
+ "00000000000000000000000000000000",
+ },
+ {
+ "4920776f756c64206c696b65207468652047656e6572616c20476175277320436869636b656e2c20706c656173652c",
+ "5432a630742dee7beb70f9f1400ee6a0426da5c54a9990f5ae0b7825f51f0060b557cfb581949a4bdf3bb67dedd472",
+ "000102030405060708090a0b0c0d0e0f",
+ },
+ }
+
+ key := fromHex("636869636b656e207465726979616b69")
+
+ for i, test := range tests {
+ plaintext := fromHex(test.plaintextHex)
+ iv := fromHex(test.ivHex)
+ ciphertext := doCTSEncrypt(key, plaintext, iv)
+ if got := hex.EncodeToString(ciphertext); got != test.ciphertextHex {
+ t.Errorf("#%d: unexpected ciphertext %s, want %s", i, got, test.ciphertextHex)
+ }
+ plaintextAgain := doCTSDecrypt(key, ciphertext, iv)
+ if !bytes.Equal(plaintext, plaintextAgain) {
+ t.Errorf("#%d: did not round trip", i)
+ }
+ }
+}
diff --git a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
index 08a0fd8..afb1804 100644
--- a/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
+++ b/util/fipstools/acvp/acvptool/testmodulewrapper/testmodulewrapper.go
@@ -21,6 +21,7 @@
import (
"bytes"
"crypto/aes"
+ "crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
@@ -42,6 +43,8 @@
"HKDF/SHA2-256": hkdfMAC,
"hmacDRBG-reseed/SHA2-256": hmacDRBGReseed,
"hmacDRBG-pr/SHA2-256": hmacDRBGPredictionResistance,
+ "AES-CBC-CS3/encrypt": ctsEncrypt,
+ "AES-CBC-CS3/decrypt": ctsDecrypt,
}
func getConfig(args [][]byte) error {
@@ -142,6 +145,22 @@
],
"returnedBitsLen": 256
}]
+ }, {
+ "algorithm": "ACVP-AES-CBC-CS3",
+ "revision": "1.0",
+ "payloadLen": [{
+ "min": 128,
+ "max": 2048,
+ "increment": 8
+ }],
+ "direction": [
+ "encrypt",
+ "decrypt"
+ ],
+ "keyLen": [
+ 128,
+ 256
+ ]
}
]`))
}
@@ -327,6 +346,122 @@
return reply(out)
}
+func swapFinalTwoAESBlocks(d []byte) {
+ var blockNMinus1 [aes.BlockSize]byte
+ copy(blockNMinus1[:], d[len(d)-2*aes.BlockSize:])
+ copy(d[len(d)-2*aes.BlockSize:], d[len(d)-aes.BlockSize:])
+ copy(d[len(d)-aes.BlockSize:], blockNMinus1[:])
+}
+
+func roundUp(n, m int) int {
+ return n + (m-(n%m))%m
+}
+
+func doCTSEncrypt(key, origPlaintext, iv []byte) []byte {
+ // https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-38a-add.pdf
+ if len(origPlaintext) < aes.BlockSize {
+ panic("input too small")
+ }
+
+ plaintext := make([]byte, roundUp(len(origPlaintext), aes.BlockSize))
+ copy(plaintext, origPlaintext)
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ panic(err)
+ }
+ cbcEncryptor := cipher.NewCBCEncrypter(block, iv)
+ cbcEncryptor.CryptBlocks(plaintext, plaintext)
+ ciphertext := plaintext
+
+ if len(origPlaintext) > aes.BlockSize {
+ swapFinalTwoAESBlocks(ciphertext)
+
+ if len(origPlaintext)%16 != 0 {
+ // Truncate the ciphertext
+ ciphertext = ciphertext[:len(ciphertext)-aes.BlockSize+(len(origPlaintext)%aes.BlockSize)]
+ }
+ }
+
+ if len(ciphertext) != len(origPlaintext) {
+ panic("internal error")
+ }
+
+ return ciphertext
+}
+
+func doCTSDecrypt(key, origCiphertext, iv []byte) []byte {
+ if len(origCiphertext) < aes.BlockSize {
+ panic("input too small")
+ }
+
+ ciphertext := make([]byte, roundUp(len(origCiphertext), aes.BlockSize))
+ copy(ciphertext, origCiphertext)
+
+ if len(ciphertext) > aes.BlockSize {
+ swapFinalTwoAESBlocks(ciphertext)
+ }
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ panic(err)
+ }
+ cbcDecrypter := cipher.NewCBCDecrypter(block, iv)
+
+ var plaintext []byte
+ if overhang := len(origCiphertext) % aes.BlockSize; overhang == 0 {
+ cbcDecrypter.CryptBlocks(ciphertext, ciphertext)
+ plaintext = ciphertext
+ } else {
+ ciphertext, finalBlock := ciphertext[:len(ciphertext)-aes.BlockSize], ciphertext[len(ciphertext)-aes.BlockSize:]
+ var plaintextFinalBlock [aes.BlockSize]byte
+ block.Decrypt(plaintextFinalBlock[:], finalBlock)
+ copy(ciphertext[len(ciphertext)-aes.BlockSize+overhang:], plaintextFinalBlock[overhang:])
+ plaintext = make([]byte, len(origCiphertext))
+ cbcDecrypter.CryptBlocks(plaintext, ciphertext)
+ for i := 0; i < overhang; i++ {
+ plaintextFinalBlock[i] ^= ciphertext[len(ciphertext)-aes.BlockSize+i]
+ }
+ copy(plaintext[len(ciphertext):], plaintextFinalBlock[:overhang])
+ }
+
+ return plaintext
+}
+
+func ctsEncrypt(args [][]byte) error {
+ if len(args) != 4 {
+ return fmt.Errorf("ctsEncrypt received %d args, wanted 4", len(args))
+ }
+
+ key, plaintext, iv, numIterations32 := args[0], args[1], args[2], args[3]
+ if len(numIterations32) != 4 || binary.LittleEndian.Uint32(numIterations32) != 1 {
+ return errors.New("only a single iteration supported for ctsEncrypt")
+ }
+
+ if len(plaintext) < aes.BlockSize {
+ return fmt.Errorf("ctsEncrypt plaintext too short: %d bytes", len(plaintext))
+ }
+
+ return reply(doCTSEncrypt(key, plaintext, iv))
+}
+
+func ctsDecrypt(args [][]byte) error {
+ if len(args) != 4 {
+ return fmt.Errorf("ctsDecrypt received %d args, wanted 4", len(args))
+ }
+
+ key, ciphertext, iv, numIterations32 := args[0], args[1], args[2], args[3]
+ if len(numIterations32) != 4 || binary.LittleEndian.Uint32(numIterations32) != 1 {
+ return errors.New("only a single iteration supported for ctsDecrypt")
+ }
+
+ if len(ciphertext) < aes.BlockSize {
+ return errors.New("ctsDecrypt ciphertext too short")
+ }
+
+ return reply(doCTSDecrypt(key, ciphertext, iv))
+}
+
const (
maxArgs = 9
maxArgLength = 1 << 20