acvp: support working with files.

A direct connections to the ACVP servers may not always be available. In
some cases, NVLAP labs will interact with the servers and send JSON back
and forth as files. This change supports both dumping the capabilities
JSON (which a lab will need in order to send to the server) and
processing vectors from a file on disk.

Change-Id: Iefa0c411b9a19808b5a7eb431169068d1c2ea966
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/42704
Commit-Queue: Adam Langley <agl@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/util/fipstools/acvp/acvptool/acvp.go b/util/fipstools/acvp/acvptool/acvp.go
index 2753dd3..4dec04f 100644
--- a/util/fipstools/acvp/acvptool/acvp.go
+++ b/util/fipstools/acvp/acvptool/acvp.go
@@ -41,7 +41,9 @@
 )
 
 var (
+	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")
 	runFlag        = flag.String("run", "", "Name of primitive to run tests for")
 	wrapperPath    = flag.String("wrapper", "../../../../build/util/fipstools/acvp/modulewrapper/modulewrapper", "Path to the wrapper binary")
 )
@@ -124,7 +126,7 @@
 type Middle interface {
 	Close()
 	Config() ([]byte, error)
-	Process(algorithm string, vectorSet []byte) ([]byte, error)
+	Process(algorithm string, vectorSet []byte) (interface{}, error)
 }
 
 func loadCachedSessionTokens(server *acvp.Server, cachePath string) error {
@@ -173,6 +175,85 @@
 	return s
 }
 
+// processFile reads a file containing vector sets, at least in the format
+// preferred by our lab, and writes the results to stdout.
+func processFile(filename string, supportedAlgos []map[string]interface{}, middle Middle) error {
+	jsonBytes, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return err
+	}
+
+	var elements []json.RawMessage
+	if err := json.Unmarshal(jsonBytes, &elements); err != nil {
+		return err
+	}
+
+	// There must be at least a header and one vector set in the file.
+	if len(elements) < 2 {
+		return fmt.Errorf("only %d elements in JSON array", len(elements))
+	}
+	header := elements[0]
+
+	// Build a map of which algorithms our Middle supports.
+	algos := make(map[string]struct{})
+	for _, supportedAlgo := range supportedAlgos {
+		algoInterface, ok := supportedAlgo["algorithm"]
+		if !ok {
+			continue
+		}
+		algo, ok := algoInterface.(string)
+		if !ok {
+			continue
+		}
+		algos[algo] = struct{}{}
+	}
+
+	var result bytes.Buffer
+	result.WriteString("[")
+	headerBytes, err := json.MarshalIndent(header, "", "    ")
+	if err != nil {
+		return err
+	}
+	result.Write(headerBytes)
+
+	for i, element := range elements[1:] {
+		var commonFields struct {
+			Algo string `json:"algorithm"`
+			ID   uint64 `json:"vsId"`
+		}
+		if err := json.Unmarshal(element, &commonFields); err != nil {
+			return fmt.Errorf("failed to extract common fields from vector set #%d", i+1)
+		}
+
+		algo := commonFields.Algo
+		if _, ok := algos[algo]; !ok {
+			return fmt.Errorf("vector set #%d contains unsupported algorithm %q", i+1, algo)
+		}
+
+		replyGroups, err := middle.Process(algo, element)
+		if err != nil {
+			return fmt.Errorf("while processing vector set #%d: %s", i+1, err)
+		}
+
+		group := map[string]interface{}{
+			"vsId":       commonFields.ID,
+			"testGroups": replyGroups,
+		}
+		replyBytes, err := json.MarshalIndent(group, "", "    ")
+		if err != nil {
+			return err
+		}
+
+		result.WriteString(",")
+		result.Write(replyBytes)
+	}
+
+	result.WriteString("]\n")
+	os.Stdout.Write(result.Bytes())
+
+	return nil
+}
+
 func main() {
 	flag.Parse()
 
@@ -229,6 +310,27 @@
 		log.Fatalf("failed to parse configuration from Middle: %s", err)
 	}
 
+	if *dumpRegcap {
+		regcap := []map[string]interface{}{
+			map[string]interface{}{"acvVersion": "1.0"},
+			map[string]interface{}{"algorithms": supportedAlgos},
+		}
+		regcapBytes, err := json.MarshalIndent(regcap, "", "    ")
+		if err != nil {
+			log.Fatalf("failed to marshal regcap: %s", err)
+		}
+		os.Stdout.Write(regcapBytes)
+		os.Stdout.WriteString("\n")
+		os.Exit(0)
+	}
+
+	if len(*jsonInputFile) > 0 {
+		if err := processFile(*jsonInputFile, supportedAlgos, middle); err != nil {
+			log.Fatalf("failed to process input file: %s", err)
+		}
+		os.Exit(0)
+	}
+
 	runAlgos := make(map[string]bool)
 	if len(*runFlag) > 0 {
 		for _, substr := range strings.Split(*runFlag, ",") {
@@ -370,7 +472,14 @@
 			var resultBuf bytes.Buffer
 			resultBuf.Write(headerBytes[:len(headerBytes)-1])
 			resultBuf.WriteString(`,"testGroups":`)
-			resultBuf.Write(replyGroups)
+			replyBytes, err := json.Marshal(replyGroups)
+			if err != nil {
+				log.Printf("Failed to marshal result: %s", err)
+				log.Printf("Deleting test set")
+				server.Delete(url)
+				os.Exit(1)
+			}
+			resultBuf.Write(replyBytes)
 			resultBuf.WriteString("}")
 
 			resultData := resultBuf.Bytes()
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index 990223e..431c315 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -185,7 +185,7 @@
 }
 
 // Process runs a set of test vectors and returns the result.
-func (m *Subprocess) Process(algorithm string, vectorSet []byte) ([]byte, error) {
+func (m *Subprocess) Process(algorithm string, vectorSet []byte) (interface{}, error) {
 	prim, ok := m.primitives[algorithm]
 	if !ok {
 		return nil, fmt.Errorf("unknown algorithm %q", algorithm)
@@ -194,7 +194,7 @@
 	if err != nil {
 		return nil, err
 	}
-	return json.Marshal(ret)
+	return ret, nil
 }
 
 type primitive interface {