fipsoracle: Add cavp_tdes_test (KAT mode only).

Change-Id: I560051d554760feab8b432de429ccbef7bc19c0a
Reviewed-on: https://boringssl-review.googlesource.com/15684
Reviewed-by: Adam Langley <agl@google.com>
Commit-Queue: Adam Langley <agl@google.com>
CQ-Verified: CQ bot account: commit-bot@chromium.org <commit-bot@chromium.org>
diff --git a/crypto/test/file_test.cc b/crypto/test/file_test.cc
index a730a66..b48100e 100644
--- a/crypto/test/file_test.cc
+++ b/crypto/test/file_test.cc
@@ -100,6 +100,7 @@
   std::unique_ptr<char[]> buf(new char[kBufLen]);
 
   bool in_instruction_block = false;
+  is_at_new_instruction_block_ = false;
 
   while (true) {
     // Read the next line.
@@ -137,6 +138,7 @@
       // comment because the FIPS lab's request files are hopelessly
       // inconsistent.
     } else if (buf[0] == '[') {  // Inside an instruction block.
+      is_at_new_instruction_block_ = true;
       if (start_line_ != 0) {
         // Instructions should be separate blocks.
         fprintf(stderr, "Line %u is an instruction in a test case.\n", line_);
@@ -170,11 +172,12 @@
         kv = kv.substr(idx + 1);
       }
     } else {
+      // Parsing a test case.
       if (in_instruction_block) {
-        // Test cases should be separate blocks.
-        fprintf(stderr, "Line %u is a test case attribute in an instruction block.\n",
-                line_);
-        return kReadError;
+        // Some NIST CAVP test files (TDES) have a test case immediately
+        // following an instruction block, without a separate blank line, some
+        // of the time.
+        in_instruction_block = false;
       }
 
       current_test_ += std::string(buf.get(), len);
@@ -360,6 +363,10 @@
   unused_instructions_.erase(key);
 }
 
+bool FileTest::IsAtNewInstructionBlock() const {
+  return is_at_new_instruction_block_;
+}
+
 void FileTest::SetIgnoreUnusedAttributes(bool ignore) {
   ignore_unused_attributes_ = ignore;
 }
diff --git a/crypto/test/file_test.h b/crypto/test/file_test.h
index aac9289..8a3108e 100644
--- a/crypto/test/file_test.h
+++ b/crypto/test/file_test.h
@@ -139,6 +139,10 @@
   bool ExpectBytesEqual(const uint8_t *expected, size_t expected_len,
                         const uint8_t *actual, size_t actual_len);
 
+  // AtNewInstructionBlock returns true if the current test was immediately
+  // preceded by an instruction block.
+  bool IsAtNewInstructionBlock() const;
+
   // HasInstruction returns true if the current test has an instruction.
   bool HasInstruction(const std::string &key);
 
@@ -185,6 +189,8 @@
 
   std::string current_test_;
 
+  bool is_at_new_instruction_block_ = false;
+
   bool ignore_unused_attributes_ = false;
 
   FileTest(const FileTest &) = delete;
diff --git a/fipsoracle/CMakeLists.txt b/fipsoracle/CMakeLists.txt
index 732ba86..683bb99 100644
--- a/fipsoracle/CMakeLists.txt
+++ b/fipsoracle/CMakeLists.txt
@@ -53,7 +53,14 @@
     cavp_hmac_test
 
     cavp_hmac_test.cc
-    cavp_test_util.h
+    cavp_test_util.cc
+    $<TARGET_OBJECTS:test_support>
+  )
+
+  add_executable(
+    cavp_tdes_test
+
+    cavp_tdes_test.cc
     cavp_test_util.cc
     $<TARGET_OBJECTS:test_support>
   )
@@ -62,7 +69,6 @@
     cavp_sha_test
 
     cavp_sha_test.cc
-    cavp_test_util.h
     cavp_test_util.cc
     $<TARGET_OBJECTS:test_support>
   )
@@ -71,7 +77,6 @@
     cavp_sha_monte_test
 
     cavp_sha_monte_test.cc
-    cavp_test_util.h
     cavp_test_util.cc
     $<TARGET_OBJECTS:test_support>
   )
@@ -87,16 +92,13 @@
 
   target_link_libraries(cavp_aes_test crypto)
   target_link_libraries(cavp_aes_gcm_test crypto)
-
+  target_link_libraries(cavp_ctr_drbg_test crypto)
   target_link_libraries(cavp_ecdsa2_keypair_test crypto)
   target_link_libraries(cavp_ecdsa2_pkv_test crypto)
   target_link_libraries(cavp_ecdsa2_siggen_test crypto)
   target_link_libraries(cavp_ecdsa2_sigver_test crypto)
-
   target_link_libraries(cavp_hmac_test crypto)
-
   target_link_libraries(cavp_sha_test crypto)
   target_link_libraries(cavp_sha_monte_test crypto)
-
-  target_link_libraries(cavp_ctr_drbg_test crypto)
+  target_link_libraries(cavp_tdes_test crypto)
 endif()
diff --git a/fipsoracle/cavp_tdes_test.cc b/fipsoracle/cavp_tdes_test.cc
new file mode 100644
index 0000000..31e850c
--- /dev/null
+++ b/fipsoracle/cavp_tdes_test.cc
@@ -0,0 +1,164 @@
+/* Copyright (c) 2017, 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. */
+
+// cavp_tdes_test processes a NIST TMOVS test vector request file and emits the
+// corresponding response. An optional sample vector file can be passed to
+// verify the result.
+
+#include <stdlib.h>
+
+#include <openssl/cipher.h>
+#include <openssl/crypto.h>
+#include <openssl/err.h>
+
+#include "../crypto/test/file_test.h"
+#include "cavp_test_util.h"
+
+
+struct TestCtx {
+  const EVP_CIPHER *cipher;
+  std::unique_ptr<FileTest> response_sample;
+  enum Mode {
+    kKAT,  // Known Answer Test
+    kMCT,  // Monte Carlo Test
+    kMMT,  // Multi Message Test
+  };
+  bool has_iv;
+  Mode mode;
+};
+
+static bool TestKAT(FileTest *t, void *arg) {
+  TestCtx *ctx = reinterpret_cast<TestCtx *>(arg);
+
+  if (t->HasInstruction("ENCRYPT") == t->HasInstruction("DECRYPT")) {
+    t->PrintLine("Want either ENCRYPT or DECRYPT");
+    return false;
+  }
+  enum {
+    kEncrypt,
+    kDecrypt,
+  } operation = t->HasInstruction("ENCRYPT") ? kEncrypt : kDecrypt;
+
+  std::string count;
+  std::vector<uint8_t> key, iv, in, result;
+  const std::string op_label = operation == kEncrypt ? "PLAINTEXT" : "CIPHERTEXT";
+  if (!t->GetAttribute(&count, "COUNT") ||
+      !t->GetBytes(&key, "KEYs") ||
+      (ctx->has_iv && !t->GetBytes(&iv, "IV")) ||
+      !t->GetBytes(&in, op_label)) {
+    return false;
+  }
+  std::vector<uint8_t> triple_key(key);
+  triple_key.insert(triple_key.end(), key.begin(), key.end());
+  triple_key.insert(triple_key.end(), key.begin(), key.end());
+
+  const EVP_CIPHER *cipher = ctx->cipher;
+
+  if (!CipherOperation(cipher, &result, operation == kEncrypt, triple_key, iv,
+                       in)) {
+    return false;
+  }
+  const std::string result_label =
+      operation == kEncrypt ? "CIPHERTEXT" : "PLAINTEXT";
+
+  // TDES fax files output format differs from its input format, so we
+  // construct it manually rather than printing CurrentTestToString().
+  if (t->IsAtNewInstructionBlock()) {
+    std::string header = operation == kEncrypt ? "[ENCRYPT]" : "[DECRYPT]";
+    printf("%s\r\n", header.c_str());
+  }
+  printf("COUNT = %s\r\nKEYs = %s\r\n", count.c_str(),
+         EncodeHex(key.data(), key.size()).c_str());
+  if (ctx->has_iv) {
+    printf("IV = %s\r\n", EncodeHex(iv.data(), iv.size()).c_str());
+  }
+  printf("%s = %s\r\n%s = %s\r\n\r\n", op_label.c_str(),
+         EncodeHex(in.data(), in.size()).c_str(), result_label.c_str(),
+         EncodeHex(result.data(), result.size()).c_str());
+
+  // Check if sample response file matches.
+  if (ctx->response_sample) {
+    if (ctx->response_sample->ReadNext() != FileTest::kReadSuccess) {
+      t->PrintLine("invalid sample file");
+      return false;
+    }
+    std::string expected_count;
+    std::vector<uint8_t> expected_result;
+    if (!ctx->response_sample->GetAttribute(&expected_count, "COUNT") ||
+        count != expected_count ||
+        (!ctx->response_sample->GetBytes(&expected_result, result_label)) ||
+        !t->ExpectBytesEqual(expected_result.data(), expected_result.size(),
+                             result.data(), result.size())) {
+      t->PrintLine("result doesn't match");
+      return false;
+    }
+  }
+
+  return true;
+}
+
+static int usage(char *arg) {
+  fprintf(
+      stderr,
+      "usage: %s (kat|mct|mmt) <cipher> <test file> [<sample response file>]\n",
+      arg);
+  return 1;
+}
+
+int main(int argc, char **argv) {
+  CRYPTO_library_init();
+
+  if (argc < 4 || argc > 5) {
+    return usage(argv[0]);
+  }
+
+  const std::string tm(argv[1]);
+  enum TestCtx::Mode test_mode;
+  if (tm == "kat") {
+    test_mode = TestCtx::kKAT;
+  } else if (tm == "mmt") {
+    test_mode = TestCtx::kMMT;
+  } else if (tm == "mct") {
+    test_mode = TestCtx::kMCT;
+  } else {
+    fprintf(stderr, "invalid test_mode: %s\n", tm.c_str());
+    return usage(argv[0]);
+  }
+
+  const std::string cipher_name(argv[2]);
+  const EVP_CIPHER *cipher = GetCipher(argv[2]);
+  if (cipher == nullptr) {
+    fprintf(stderr, "invalid cipher: %s\n", argv[2]);
+    return 1;
+  }
+  bool has_iv = cipher_name != "des-ede3";
+  TestCtx ctx = {cipher, nullptr, has_iv, test_mode};
+
+  if (argc == 5) {
+    ctx.response_sample.reset(new FileTest(argv[4]));
+    if (!ctx.response_sample->is_open()) {
+      return 1;
+    }
+    ctx.response_sample->SetIgnoreUnusedAttributes(true);
+  }
+
+  printf("# Generated by");
+  for (int i = 0; i < argc; i++) {
+    printf(" %s", argv[i]);
+  }
+  printf("\r\n\r\n");
+
+  // TODO(martinkr): Add MMT, MCT.
+  return FileTestMainSilent(TestKAT, &ctx, argv[3]);
+}
diff --git a/fipsoracle/run_cavp.go b/fipsoracle/run_cavp.go
index 9ff81e7..6ef4dab 100644
--- a/fipsoracle/run_cavp.go
+++ b/fipsoracle/run_cavp.go
@@ -167,6 +167,31 @@
 	[]test{{"CTR_DRBG", nil, false}},
 }
 
+var tdesTests = testSuite{
+	"TDES",
+	"cavp_tdes_test",
+	[]test{
+		// {"TCBCMMT2", []string{"mmt"}, false},
+		// {"TCBCMMT3", []string{"mmt"}, false},
+		// {"TCBCMonte2", []string{"mct"}, false},
+		// {"TCBCMonte3", []string{"mct"}, false},
+		{"TCBCinvperm", []string{"kat", "des-ede3-cbc"}, false},
+		{"TCBCpermop", []string{"kat", "des-ede3-cbc"}, false},
+		{"TCBCsubtab", []string{"kat", "des-ede3-cbc"}, false},
+		{"TCBCvarkey", []string{"kat", "des-ede3-cbc"}, false},
+		{"TCBCvartext", []string{"kat", "des-ede3-cbc"}, false},
+		// {"TECBMMT2", []string{"mmt"}, false},
+		// {"TECBMMT3", []string{"mmt"}, false},
+		// {"TECBMonte2", []string{"mct"}, false},
+		// {"TECBMonte3", []string{"mct"}, false},
+		{"TECBinvperm", []string{"kat", "des-ede3"}, false},
+		{"TECBpermop", []string{"kat", "des-ede3"}, false},
+		{"TECBsubtab", []string{"kat", "des-ede3"}, false},
+		{"TECBvarkey", []string{"kat", "des-ede3"}, false},
+		{"TECBvartext", []string{"kat", "des-ede3"}, false},
+	},
+}
+
 var allTestSuites = []*testSuite{
 	&aesGCMTests,
 	&aesTests,
@@ -178,6 +203,7 @@
 	&hmacTests,
 	&shaTests,
 	&shaMonteTests,
+	&tdesTests,
 }
 
 func main() {
@@ -224,7 +250,7 @@
 	cmd.Stderr = os.Stderr
 
 	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("cannot run command for %q %q: %s", suite.getDirectory(), test.inFile, err)
+		return fmt.Errorf("cannot run command for %q %q (%s): %s", suite.getDirectory(), test.inFile, strings.Join(append([]string{binary}, args...), " "), err)
 	}
 
 	return nil