acvp: add 3DES-ECB support

Change-Id: I4ffa2572acce1fdccdf4d3c33680e6d0114bd42b
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/43405
Commit-Queue: Adam Langley <agl@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/util/fipstools/acvp/acvptool/subprocess/block.go b/util/fipstools/acvp/acvptool/subprocess/block.go
index 365399e..35c2133 100644
--- a/util/fipstools/acvp/acvptool/subprocess/block.go
+++ b/util/fipstools/acvp/acvptool/subprocess/block.go
@@ -18,6 +18,7 @@
 	"encoding/hex"
 	"encoding/json"
 	"fmt"
+	"math/bits"
 )
 
 // aesKeyShuffle is the "AES Monte Carlo Key Shuffle" from the ACVP
@@ -140,6 +141,68 @@
 	return mctResults
 }
 
+// xorKeyWithOddParityLSB XORs value into key while setting the LSB of each bit
+// to establish odd parity. This embedding of a parity check in a DES key is an
+// old tradition and something that NIST's tests require (despite being
+// undocumented).
+func xorKeyWithOddParityLSB(key, value []byte) {
+	for i := range key {
+		v := key[i] ^ value[i]
+		// Use LSB to establish odd parity.
+		v ^= byte((bits.OnesCount8(v) & 1)) ^ 1
+		key[i] = v
+	}
+}
+
+// desKeyShuffle implements the manipulation of the Key arrays in the "TDES
+// Monte Carlo Test - ECB mode" algorithm from the ACVP specification.
+func keyShuffle3DES(key, result, prevResult, prevPrevResult []byte) {
+	xorKeyWithOddParityLSB(key[:8], result)
+	xorKeyWithOddParityLSB(key[8:16], prevResult)
+	xorKeyWithOddParityLSB(key[16:], prevPrevResult)
+}
+
+// iterate3DES implements "TDES Monte Carlo Test - ECB mode" from the ACVP
+// specification.
+func iterate3DES(transact func(n int, args ...[]byte) ([][]byte, error), encrypt bool, key, input, iv []byte) (mctResults []blockCipherMCTResult) {
+	for i := 0; i < 400; i++ {
+		var iteration blockCipherMCTResult
+		keyHex := hex.EncodeToString(key)
+		iteration.Key1Hex = keyHex[:16]
+		iteration.Key2Hex = keyHex[16:32]
+		iteration.Key3Hex = keyHex[32:]
+
+		if encrypt {
+			iteration.PlaintextHex = hex.EncodeToString(input)
+		} else {
+			iteration.CiphertextHex = hex.EncodeToString(input)
+		}
+
+		var result, prevResult, prevPrevResult []byte
+		for j := 0; j < 10000; j++ {
+			prevPrevResult = prevResult
+			prevResult = input
+			result, err := transact(1, key, input)
+			if err != nil {
+				panic("block operation failed")
+			}
+			input = result[0]
+		}
+		result = input
+
+		if encrypt {
+			iteration.CiphertextHex = hex.EncodeToString(result)
+		} else {
+			iteration.PlaintextHex = hex.EncodeToString(result)
+		}
+
+		keyShuffle3DES(key, result, prevResult, prevPrevResult)
+		mctResults = append(mctResults, iteration)
+	}
+
+	return mctResults
+}
+
 // blockCipher implements an ACVP algorithm by making requests to the subprocess
 // to encrypt and decrypt with a block cipher.
 type blockCipher struct {
@@ -165,6 +228,11 @@
 		CiphertextHex string `json:"ct"`
 		IVHex         string `json:"iv"`
 		KeyHex        string `json:"key"`
+
+		// 3DES tests serialise the key differently.
+		Key1Hex string `json:"key1"`
+		Key2Hex string `json:"key2"`
+		Key3Hex string `json:"key3"`
 	} `json:"tests"`
 }
 
@@ -181,10 +249,15 @@
 }
 
 type blockCipherMCTResult struct {
-	KeyHex        string `json:"key"`
+	KeyHex        string `json:"key,omitempty"`
 	PlaintextHex  string `json:"pt"`
 	CiphertextHex string `json:"ct"`
 	IVHex         string `json:"iv,omitempty"`
+
+	// 3DES tests serialise the key differently.
+	Key1Hex string `json:"key1"`
+	Key2Hex string `json:"key2"`
+	Key3Hex string `json:"key3"`
 }
 
 func (b *blockCipher) Process(vectorSet []byte, m Transactable) (interface{}, error) {
@@ -230,6 +303,11 @@
 			return nil, fmt.Errorf("test group %d has unknown type %q", group.ID, group.Type)
 		}
 
+		if group.KeyBits == 0 {
+			// 3DES tests fail to set this parameter.
+			group.KeyBits = 192
+		}
+
 		if group.KeyBits%8 != 0 {
 			return nil, fmt.Errorf("test group %d contains non-byte-multiple key length %d", group.ID, group.KeyBits)
 		}
@@ -240,6 +318,11 @@
 		}
 
 		for _, test := range group.Tests {
+			if len(test.KeyHex) == 0 && len(test.Key1Hex) > 0 {
+				// 3DES encodes the key differently.
+				test.KeyHex = test.Key1Hex + test.Key2Hex + test.Key3Hex
+			}
+
 			if len(test.KeyHex) != keyBytes*2 {
 				return nil, fmt.Errorf("test case %d/%d contains key %q of length %d, but expected %d-bit key", group.ID, test.ID, test.KeyHex, len(test.KeyHex), group.KeyBits)
 			}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index 05d7fb5..3c04981 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -79,6 +79,7 @@
 		"ACVP-AES-ECB":  &blockCipher{"AES", 16, true, false, iterateAES},
 		"ACVP-AES-CBC":  &blockCipher{"AES-CBC", 16, true, true, iterateAESCBC},
 		"ACVP-AES-CTR":  &blockCipher{"AES-CTR", 16, false, true, nil},
+		"ACVP-TDES-ECB": &blockCipher{"3DES-ECB", 8, true, false, iterate3DES},
 		"ACVP-AES-GCM":  &aead{"AES-GCM", false},
 		"ACVP-AES-CCM":  &aead{"AES-CCM", true},
 		"ACVP-AES-KW":   &aead{"AES-KW", false},
diff --git a/util/fipstools/acvp/modulewrapper/modulewrapper.cc b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
index 1ffb432..d97ea91 100644
--- a/util/fipstools/acvp/modulewrapper/modulewrapper.cc
+++ b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
@@ -26,6 +26,7 @@
 #include <openssl/aead.h>
 #include <openssl/aes.h>
 #include <openssl/bn.h>
+#include <openssl/cipher.h>
 #include <openssl/cmac.h>
 #include <openssl/digest.h>
 #include <openssl/ec.h>
@@ -251,6 +252,13 @@
         "aadLen": [{"min": 0, "max": 1024, "increment": 8}]
       },
       {
+        "algorithm": "ACVP-TDES-ECB",
+        "revision": "1.0",
+        "direction": ["encrypt", "decrypt"],
+        "keyLen": [192],
+        "keyingOption": [1]
+      },
+      {
         "algorithm": "HMAC-SHA-1",
         "revision": "1.0",
         "keyLen": [{
@@ -720,6 +728,39 @@
                     Span<const uint8_t>(out));
 }
 
+template<bool Encrypt>
+static bool TDES(const Span<const uint8_t> args[]) {
+  const EVP_CIPHER *cipher = EVP_des_ede3();
+
+  if (args[0].size() != 24) {
+    fprintf(stderr, "Bad key length %u for 3DES.\n",
+            static_cast<unsigned>(args[0].size()));
+    return false;
+  }
+  if (args[1].size() % 8) {
+    fprintf(stderr, "Bad input length %u for 3DES.\n",
+            static_cast<unsigned>(args[1].size()));
+    return false;
+  }
+
+  std::vector<uint8_t> out;
+  out.resize(args[1].size());
+
+  bssl::ScopedEVP_CIPHER_CTX ctx;
+  int out_len, out_len2;
+  if (!EVP_CipherInit_ex(ctx.get(), cipher, nullptr, args[0].data(), nullptr,
+                         Encrypt ? 1 : 0) ||
+      !EVP_CIPHER_CTX_set_padding(ctx.get(), 0) ||
+      !EVP_CipherUpdate(ctx.get(), out.data(), &out_len, args[1].data(),
+                        args[1].size()) ||
+      !EVP_CipherFinal_ex(ctx.get(), out.data() + out_len, &out_len2) ||
+      (out_len + out_len2) != static_cast<int>(out.size())) {
+    return false;
+  }
+
+  return WriteReply(STDOUT_FILENO, Span<const uint8_t>(out));
+}
+
 template <const EVP_MD *HashFunc()>
 static bool HMAC(const Span<const uint8_t> args[]) {
   const EVP_MD *const md = HashFunc();
@@ -975,6 +1016,8 @@
     {"AES-KWP/open", 5, AESPaddedKeyWrapOpen},
     {"AES-CCM/seal", 5, AEADSeal<AESCCMSetup>},
     {"AES-CCM/open", 5, AEADOpen<AESCCMSetup>},
+    {"3DES-ECB/encrypt", 2, TDES<true>},
+    {"3DES-ECB/decrypt", 2, TDES<false>},
     {"HMAC-SHA-1", 2, HMAC<EVP_sha1>},
     {"HMAC-SHA2-224", 2, HMAC<EVP_sha224>},
     {"HMAC-SHA2-256", 2, HMAC<EVP_sha256>},