acvptool: Add support for HMAC

Change-Id: Ie3e3748cc1eb0e2f66ef052847179deaf0de239b
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/38544
Commit-Queue: Adam Langley <agl@google.com>
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/util/fipstools/acvp/acvptool/subprocess/hmac.go b/util/fipstools/acvp/acvptool/subprocess/hmac.go
new file mode 100644
index 0000000..a99da9e
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/subprocess/hmac.go
@@ -0,0 +1,112 @@
+package subprocess
+
+import (
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"strconv"
+)
+
+// The following structures reflect the JSON of ACVP HMAC tests. See
+// https://usnistgov.github.io/ACVP/artifacts/acvp_sub_mac.html#hmac_test_vectors
+
+type hmacTestVectorSet struct {
+	Groups []hmacTestGroup `json:"testGroups"`
+}
+
+type hmacTestGroup struct {
+	ID      uint64 `json:"tgId"`
+	Type    string `json:"testType"`
+	MsgBits int    `json:"msgLen"`
+	KeyBits int    `json:"keyLen"` // maximum possible value is 524288
+	MACBits int    `json:"macLen"` // maximum possible value is 512
+	Tests   []struct {
+		ID     uint64 `json:"tcId"`
+		KeyHex string `json:"key"`
+		MsgHex string `json:"msg"`
+	} `json:"tests"`
+}
+
+type hmacTestGroupResponse struct {
+	ID    uint64             `json:"tgId"`
+	Tests []hmacTestResponse `json:"tests"`
+}
+
+type hmacTestResponse struct {
+	ID     uint64 `json:"tcId"`
+	MACHex string `json:"mac,omitempty"`
+}
+
+// hmacPrimitive implements an ACVP algorithm by making requests to the
+// subprocess to HMAC strings with the given key.
+type hmacPrimitive struct {
+	// algo is the ACVP name for this algorithm and also the command name
+	// given to the subprocess to HMAC with this hash function.
+	algo  string
+	mdLen int // mdLen is the number of bytes of output that the underlying hash produces.
+	m     *Subprocess
+}
+
+// hmac uses the subprocess to compute HMAC and returns the result.
+func (h *hmacPrimitive) hmac(msg []byte, key []byte, outBits int) []byte {
+	if outBits%8 != 0 {
+		panic("fractional-byte output length requested: " + strconv.Itoa(outBits))
+	}
+	outBytes := outBits / 8
+	result, err := h.m.transact(h.algo, 1, msg, key)
+	if err != nil {
+		panic("HMAC operation failed: " + err.Error())
+	}
+	if l := len(result[0]); l < outBytes {
+		panic(fmt.Sprintf("HMAC result too short: %d bytes but wanted %d", l, outBytes))
+	}
+	return result[0][:outBytes]
+}
+
+func (h *hmacPrimitive) Process(vectorSet []byte) (interface{}, error) {
+	var parsed hmacTestVectorSet
+	if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+		return nil, err
+	}
+
+	var ret []hmacTestGroupResponse
+	// See
+	// https://usnistgov.github.io/ACVP/artifacts/acvp_sub_mac.html#hmac_test_vectors
+	// for details about the tests.
+	for _, group := range parsed.Groups {
+		response := hmacTestGroupResponse{
+			ID: group.ID,
+		}
+		if group.MACBits > h.mdLen*8 {
+			return nil, fmt.Errorf("test group %d specifies MAC length should be %d, but maximum possible length is %d", group.ID, group.MACBits, h.mdLen*8)
+		}
+
+		for _, test := range group.Tests {
+			if len(test.MsgHex)*4 != group.MsgBits {
+				return nil, fmt.Errorf("test case %d/%d contains hex message of length %d but specifies a bit length of %d", group.ID, test.ID, len(test.MsgHex), group.MsgBits)
+			}
+			msg, err := hex.DecodeString(test.MsgHex)
+			if err != nil {
+				return nil, fmt.Errorf("failed to decode hex in test case %d/%d: %s", group.ID, test.ID, err)
+			}
+
+			if len(test.KeyHex)*4 != group.KeyBits {
+				return nil, fmt.Errorf("test case %d/%d contains hex key of length %d but specifies a bit length of %d", group.ID, test.ID, len(test.KeyHex), group.KeyBits)
+			}
+			key, err := hex.DecodeString(test.KeyHex)
+			if err != nil {
+				return nil, fmt.Errorf("failed to decode key in test case %d/%d: %s", group.ID, test.ID, err)
+			}
+
+			// https://usnistgov.github.io/ACVP/artifacts/acvp_sub_mac.html#hmac_vector_responses
+			response.Tests = append(response.Tests, hmacTestResponse{
+				ID:     test.ID,
+				MACHex: hex.EncodeToString(h.hmac(msg, key, group.MACBits)),
+			})
+		}
+
+		ret = append(ret, response)
+	}
+
+	return ret, nil
+}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index 3550b4d..1f3ff8e 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -43,19 +43,24 @@
 // WriteCloser. The returned Subprocess will call Wait on the Cmd when closed.
 func NewWithIO(cmd *exec.Cmd, in io.WriteCloser, out io.ReadCloser) *Subprocess {
 	m := &Subprocess{
-		cmd:	cmd,
+		cmd:    cmd,
 		stdin:  in,
 		stdout: out,
 	}
 
 	m.primitives = map[string]primitive{
-		"SHA-1":        &hashPrimitive{"SHA-1", 20, m},
-		"SHA2-224":     &hashPrimitive{"SHA2-224", 28, m},
-		"SHA2-256":     &hashPrimitive{"SHA2-256", 32, m},
-		"SHA2-384":     &hashPrimitive{"SHA2-384", 48, m},
-		"SHA2-512":     &hashPrimitive{"SHA2-512", 64, m},
-		"ACVP-AES-ECB": &blockCipher{"AES", 16, false, m},
-		"ACVP-AES-CBC": &blockCipher{"AES-CBC", 16, true, m},
+		"SHA-1":         &hashPrimitive{"SHA-1", 20, m},
+		"SHA2-224":      &hashPrimitive{"SHA2-224", 28, m},
+		"SHA2-256":      &hashPrimitive{"SHA2-256", 32, m},
+		"SHA2-384":      &hashPrimitive{"SHA2-384", 48, m},
+		"SHA2-512":      &hashPrimitive{"SHA2-512", 64, m},
+		"ACVP-AES-ECB":  &blockCipher{"AES", 16, false, m},
+		"ACVP-AES-CBC":  &blockCipher{"AES-CBC", 16, true, m},
+		"HMAC-SHA-1":    &hmacPrimitive{"HMAC-SHA-1", 20, m},
+		"HMAC-SHA2-224": &hmacPrimitive{"HMAC-SHA2-224", 28, m},
+		"HMAC-SHA2-256": &hmacPrimitive{"HMAC-SHA2-256", 32, m},
+		"HMAC-SHA2-384": &hmacPrimitive{"HMAC-SHA2-384", 48, m},
+		"HMAC-SHA2-512": &hmacPrimitive{"HMAC-SHA2-512", 64, m},
 	}
 
 	return m
diff --git a/util/fipstools/acvp/modulewrapper/modulewrapper.cc b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
index fd7639a..e6d8b21 100644
--- a/util/fipstools/acvp/modulewrapper/modulewrapper.cc
+++ b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
@@ -23,6 +23,8 @@
 #include <cstdarg>
 
 #include <openssl/aes.h>
+#include <openssl/digest.h>
+#include <openssl/hmac.h>
 #include <openssl/sha.h>
 #include <openssl/span.h>
 
@@ -160,6 +162,56 @@
       "  \"revision\": \"1.0\","
       "  \"direction\": [\"encrypt\", \"decrypt\"],"
       "  \"keyLen\": [128, 192, 256]"
+      "},"
+      "{"
+      "  \"algorithm\": \"HMAC-SHA-1\","
+      "  \"revision\": \"1.0\","
+      "  \"keyLen\": [{"
+      "    \"min\": 8, \"max\": 2048, \"increment\": 8"
+      "  }],"
+      "  \"macLen\": [{"
+      "    \"min\": 32, \"max\": 160, \"increment\": 8"
+      "  }]"
+      "},"
+      "{"
+      "  \"algorithm\": \"HMAC-SHA2-224\","
+      "  \"revision\": \"1.0\","
+      "  \"keyLen\": [{"
+      "    \"min\": 8, \"max\": 2048, \"increment\": 8"
+      "  }],"
+      "  \"macLen\": [{"
+      "    \"min\": 32, \"max\": 224, \"increment\": 8"
+      "  }]"
+      "},"
+      "{"
+      "  \"algorithm\": \"HMAC-SHA2-256\","
+      "  \"revision\": \"1.0\","
+      "  \"keyLen\": [{"
+      "    \"min\": 8, \"max\": 2048, \"increment\": 8"
+      "  }],"
+      "  \"macLen\": [{"
+      "    \"min\": 32, \"max\": 256, \"increment\": 8"
+      "  }]"
+      "},"
+      "{"
+      "  \"algorithm\": \"HMAC-SHA2-384\","
+      "  \"revision\": \"1.0\","
+      "  \"keyLen\": [{"
+      "    \"min\": 8, \"max\": 2048, \"increment\": 8"
+      "  }],"
+      "  \"macLen\": [{"
+      "    \"min\": 32, \"max\": 384, \"increment\": 8"
+      "  }]"
+      "},"
+      "{"
+      "  \"algorithm\": \"HMAC-SHA2-512\","
+      "  \"revision\": \"1.0\","
+      "  \"keyLen\": [{"
+      "    \"min\": 8, \"max\": 2048, \"increment\": 8"
+      "  }],"
+      "  \"macLen\": [{"
+      "    \"min\": 32, \"max\": 512, \"increment\": 8"
+      "  }]"
       "}"
       "]";
   return WriteReply(
@@ -216,6 +268,18 @@
   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();
+  uint8_t digest[EVP_MAX_MD_SIZE];
+  unsigned digest_len;
+  if (::HMAC(md, args[1].data(), args[1].size(), args[0].data(), args[0].size(),
+             digest, &digest_len) == nullptr) {
+    return false;
+  }
+  return WriteReply(STDOUT_FILENO, Span<const uint8_t>(digest, digest_len));
+}
+
 static constexpr struct {
   const char name[kMaxNameLength + 1];
   uint8_t expected_args;
@@ -231,6 +295,11 @@
     {"AES/decrypt", 2, AES<AES_set_decrypt_key, AES_decrypt>},
     {"AES-CBC/encrypt", 3, AES_CBC<AES_set_encrypt_key, AES_ENCRYPT>},
     {"AES-CBC/decrypt", 3, AES_CBC<AES_set_decrypt_key, AES_DECRYPT>},
+    {"HMAC-SHA-1", 2, HMAC<EVP_sha1>},
+    {"HMAC-SHA2-224", 2, HMAC<EVP_sha224>},
+    {"HMAC-SHA2-256", 2, HMAC<EVP_sha256>},
+    {"HMAC-SHA2-384", 2, HMAC<EVP_sha384>},
+    {"HMAC-SHA2-512", 2, HMAC<EVP_sha512>},
 };
 
 int main() {