acvptool: add support for uploading results.

See the update to ACVP.md for documentation but this now allows running
a test to be broken down into separate commands for each step: fetching,
processing, and uploading.

Change-Id: Id86d1cd0b07fcc9bdc6c665072b511da0832bdde
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/55608
Reviewed-by: David Benjamin <davidben@google.com>
Reviewed-by: Adam Langley <agl@google.com>
Commit-Queue: Adam Langley <agl@google.com>
diff --git a/util/fipstools/acvp/ACVP.md b/util/fipstools/acvp/ACVP.md
index d933a9d..d3578e2 100644
--- a/util/fipstools/acvp/ACVP.md
+++ b/util/fipstools/acvp/ACVP.md
@@ -251,4 +251,10 @@
 
 In online mode, a given algorithm can be run by using the `-run` option. For example, `-run SHA2-256`. This will fetch a vector set, have the module-under-test answer it, and upload the answer. If you want to just fetch the vector set for later use with the `-json` option (documented above) then you can use `-fetch` instead of `-run`. The `-fetch` option also supports passing `-expected-out <filename>` to fetch and write the expected results, if the server supports that.
 
-The tool doesn't currently support the sorts of operations that a lab would need, like uploading results from a file.
+After results have been produced with `-json`, they can be uploaded with `-upload`. So `-run` is effectively these three steps combined:
+
+```
+./acvptool -fetch SHA2-256 > request
+./acvptool -json request > result
+./acvptool -upload result
+```
diff --git a/util/fipstools/acvp/acvptool/acvp.go b/util/fipstools/acvp/acvptool/acvp.go
index ecfe088..c294eb8 100644
--- a/util/fipstools/acvp/acvptool/acvp.go
+++ b/util/fipstools/acvp/acvptool/acvp.go
@@ -45,6 +45,7 @@
 	dumpRegcap      = flag.Bool("regcap", false, "Print module capabilities JSON to stdout")
 	configFilename  = flag.String("config", "config.json", "Location of the configuration JSON file")
 	jsonInputFile   = flag.String("json", "", "Location of a vector-set input file")
+	uploadInputFile = flag.String("upload", "", "Location of a JSON results file to upload")
 	runFlag         = flag.String("run", "", "Name of primitive to run tests for")
 	fetchFlag       = flag.String("fetch", "", "Name of primitive to fetch vectors for")
 	expectedOutFlag = flag.String("expected-out", "", "Name of a file to write the expected results to")
@@ -179,12 +180,12 @@
 	return s
 }
 
-// looksLikeHeaderElement returns true iff element looks like it's a header, not
-// a test. Some ACVP files contain a header as the first element that should be
-// duplicated into the response, and some don't. If the element contains
-// a "url" field, or if it's missing an "algorithm" field, then we guess that
-// it's a header.
-func looksLikeHeaderElement(element json.RawMessage) bool {
+// looksLikeVectorSetHeader returns true iff element looks like it's a
+// vectorSetHeader, not a test. Some ACVP files contain a header as the first
+// element that should be duplicated into the response, and some don't. If the
+// element contains a "url" field, or if it's missing an "algorithm" field,
+// then we guess that it's a header.
+func looksLikeVectorSetHeader(element json.RawMessage) bool {
 	var headerFields struct {
 		URL       string `json:"url"`
 		Algorithm string `json:"algorithm"`
@@ -214,7 +215,7 @@
 	}
 
 	var header json.RawMessage
-	if looksLikeHeaderElement(elements[0]) {
+	if looksLikeVectorSetHeader(elements[0]) {
 		header, elements = elements[0], elements[1:]
 		if len(elements) == 0 {
 			return errors.New("JSON input is empty")
@@ -269,6 +270,7 @@
 		group := map[string]interface{}{
 			"vsId":       commonFields.ID,
 			"testGroups": replyGroups,
+			"algorithm":  algo,
 		}
 		replyBytes, err := json.MarshalIndent(group, "", "    ")
 		if err != nil {
@@ -454,6 +456,76 @@
 	}
 }
 
+// vectorSetHeader is the first element in the array of JSON elements that makes
+// up the on-disk format for a vector set.
+type vectorSetHeader struct {
+	URL           string   `json:"url,omitempty"`
+	VectorSetURLs []string `json:"vectorSetUrls,omitempty"`
+	Time          string   `json:"time,omitempty"`
+}
+
+func uploadFromFile(file string, config *Config, sessionTokensCacheDir string) {
+	if len(*jsonInputFile) > 0 {
+		log.Fatalf("-upload cannot be used with -json")
+	}
+	if len(*runFlag) > 0 {
+		log.Fatalf("-upload cannot be used with -run")
+	}
+	if len(*fetchFlag) > 0 {
+		log.Fatalf("-upload cannot be used with -fetch")
+	}
+	if len(*expectedOutFlag) > 0 {
+		log.Fatalf("-upload cannot be used with -expected-out")
+	}
+	if *dumpRegcap {
+		log.Fatalf("-upload cannot be used with -regcap")
+	}
+
+	in, err := os.Open(file)
+	if err != nil {
+		log.Fatalf("Cannot open input: %s", err)
+	}
+	defer in.Close()
+
+	decoder := json.NewDecoder(in)
+
+	var input []json.RawMessage
+	if err := decoder.Decode(&input); err != nil {
+		log.Fatalf("Failed to parse input: %s", err)
+	}
+
+	if len(input) < 2 {
+		log.Fatalf("Input JSON has fewer than two elements")
+	}
+
+	var header vectorSetHeader
+	if err := json.Unmarshal(input[0], &header); err != nil {
+		log.Fatalf("Failed to parse input header: %s", err)
+	}
+
+	if numGroups := len(input) - 1; numGroups != len(header.VectorSetURLs) {
+		log.Fatalf("have %d URLs from header, but only %d result groups", len(header.VectorSetURLs), numGroups)
+	}
+
+	server, err := connect(config, sessionTokensCacheDir)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	for i, url := range header.VectorSetURLs {
+		log.Printf("Uploading result for %q", url)
+		if err := uploadResult(server, url, input[i+1]); err != nil {
+			log.Fatalf("Failed to upload: %s", err)
+		}
+	}
+
+	if ok, err := getResultsWithRetry(server, header.URL); err != nil {
+		log.Fatal(err)
+	} else if !ok {
+		os.Exit(1)
+	}
+}
+
 func main() {
 	flag.Parse()
 
@@ -474,6 +546,11 @@
 		}
 	}
 
+	if len(*uploadInputFile) > 0 {
+		uploadFromFile(*uploadInputFile, &config, sessionTokensCacheDir)
+		return
+	}
+
 	middle, err := subprocess.New(*wrapperPath)
 	if err != nil {
 		log.Fatalf("failed to initialise middle: %s", err)
@@ -629,10 +706,10 @@
 
 	if len(*fetchFlag) > 0 {
 		io.WriteString(fetchOutputTee, "[\n")
-		json.NewEncoder(fetchOutputTee).Encode(map[string]interface{}{
-			"url":           url,
-			"vectorSetUrls": result.VectorSetURLs,
-			"time":          time.Now().Format(time.RFC3339),
+		json.NewEncoder(fetchOutputTee).Encode(vectorSetHeader{
+			URL:           url,
+			VectorSetURLs: result.VectorSetURLs,
+			Time:          time.Now().Format(time.RFC3339),
 		})
 	}