Add ability to upload a directory of vector test results.
Change-Id: I11469dc8b987d12f737e51f092ff36f30ee74cd8
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/73847
Commit-Queue: Adam Langley <agl@google.com>
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/util/fipstools/acvp/acvptool/acvp.go b/util/fipstools/acvp/acvptool/acvp.go
index fec4b4d..2be9ba8 100644
--- a/util/fipstools/acvp/acvptool/acvp.go
+++ b/util/fipstools/acvp/acvptool/acvp.go
@@ -29,11 +29,13 @@
"flag"
"fmt"
"io"
+ "io/ioutil"
"log"
"net/http"
neturl "net/url"
"os"
"path/filepath"
+ "strconv"
"strings"
"time"
@@ -46,6 +48,7 @@
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")
+ uploadDirectory = flag.String("directory", "", "Path to folder where result files to be uploaded are")
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")
@@ -386,7 +389,7 @@
return nil, errors.New("config file missing PrivateKeyDERFile and PrivateKeyFile")
}
if len(config.PrivateKeyDERFile) != 0 && len(config.PrivateKeyFile) != 0 {
- return nil, errors.New("config file has both PrivateKeyDERFile and PrivateKeyFile. Can only have one.")
+ return nil, errors.New("config file has both PrivateKeyDERFile and PrivateKeyFile - can only have one")
}
privateKeyFile := config.PrivateKeyDERFile
if len(config.PrivateKeyFile) > 0 {
@@ -456,6 +459,141 @@
}
}
+func getLastDigitDir(path string) (string, error) {
+ parts := strings.Split(filepath.Clean(path), string(filepath.Separator))
+
+ for i := len(parts) - 1; i >= 0; i-- {
+ part := parts[i]
+ if _, err := strconv.Atoi(part); err == nil {
+ return part, nil
+ }
+ }
+ return "", errors.New("no directory consisting of only digits found")
+}
+
+func uploadResults(results []nistUploadResult, sessionID string, config *Config, sessionTokensCacheDir string) {
+ server, err := connect(config, sessionTokensCacheDir)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for _, result := range results {
+ url := result.URLPath
+ payload := result.JSONResult
+ log.Printf("Uploading result for %q", url)
+ if err := uploadResult(server, url, payload); err != nil {
+ log.Fatalf("Failed to upload: %s", err)
+ }
+ }
+
+ if ok, err := getResultsWithRetry(server, fmt.Sprintf("/acvp/v1/testSessions/%s", sessionID)); err != nil {
+ log.Fatal(err)
+ } else if !ok {
+ os.Exit(1)
+ }
+}
+
+// Vector Test Result files are JSON formatted with various objects and keys.
+// Define structs to read and process the files.
+type vectorResult struct {
+ Version string `json:"acvVersion,omitempty"`
+ Algorithm string `json:"algorithm,omitempty"`
+ ID int `json:"vsId,omitempty"`
+ // Objects under testGroups can have various keys so use an empty interface.
+ Tests []map[string]interface{} `json:"testGroups,omitempty"`
+}
+
+func getVectorSetID(jsonData []vectorResult) (int, error) {
+ vsId := 0
+ for _, item := range jsonData {
+ if item.ID > 0 && vsId == 0 {
+ vsId = item.ID
+ } else if item.ID > 0 && vsId != 0 {
+ return 0, errors.New("found multiple vsId values")
+ }
+ }
+ if vsId != 0 {
+ return vsId, nil
+ }
+ return 0, errors.New("could not find vsId")
+}
+
+func getVectorResult(jsonData []vectorResult) ([]byte, error) {
+ for _, item := range jsonData {
+ if item.ID > 0 {
+ out, err := json.Marshal(item)
+ if err != nil {
+ return nil, fmt.Errorf("unable to marshal JSON due to %s", err)
+ }
+ return out, nil
+ }
+ }
+ return nil, errors.New("could not find vsId necessary to identify vector result")
+}
+
+// Results to be uploaded have a specific URL path to POST/PUT to, along with
+// the test results.
+// Define a struct and store this data for processing.
+type nistUploadResult struct {
+ URLPath string
+ JSONResult []byte
+}
+
+// Uploads a results directory based on the directory name being the session id.
+// Non-JSON files are ignored and JSON files are assumed to be test results.
+// The vectorSetId is retrieved from the test result file.
+func uploadResultsDirectory(directory string, config *Config, sessionTokensCacheDir string) {
+ directory = filepath.Clean(directory)
+ sessionID, err := getLastDigitDir(directory)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var results []nistUploadResult
+ // Read directory, identify, and process all files.
+ files, err := ioutil.ReadDir(directory)
+ if err != nil {
+ log.Fatalf("Unable to read directory: %s", err)
+ }
+
+ for _, file := range files {
+ // Add contents of the result file to results.
+ filePath := filepath.Join(directory, file.Name())
+ in, err := ioutil.ReadFile(filePath)
+ if err != nil {
+ log.Fatalf("Cannot open input: %s", err)
+ }
+
+ var data []vectorResult
+ if err := json.Unmarshal(in, &data); err != nil {
+ // Assume file is not JSON. Log and continue to next file.
+ log.Printf("Failed to parse %q: %s", filePath, err)
+ continue
+ }
+
+ vectorSetID, err := getVectorSetID(data)
+ if err != nil {
+ log.Fatalf("Failed to get VectorSetId: %s", err)
+ }
+ // uploadResult() uses acvp.Server whose write() function takes the
+ // JSON *object* payload and turns it into a JSON *array* adding
+ // {"acvVersion":"1.0"} as a top-level object. Since the result file is
+ // already in this format, the JSON provided to uploadResult() must be
+ // modified to have those aspects removed. In other words, only store only
+ // the vector test result JSON object (do not store a JSON array or
+ // acvVersion object).
+ vectorTestResult, err := getVectorResult(data)
+ if err != nil {
+ log.Fatalf("Failed to get VectorResult: %s", err)
+ }
+ requestPath := fmt.Sprintf("/acvp/v1/testSessions/%s/vectorSets/%d", sessionID, vectorSetID)
+ newResult := nistUploadResult{URLPath: requestPath, JSONResult: vectorTestResult}
+ results = append(results, newResult)
+ }
+
+ uploadResults(results, sessionID, config, sessionTokensCacheDir)
+}
+
// 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 {
@@ -465,22 +603,6 @@
}
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)
@@ -507,27 +629,47 @@
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)
- }
-
+ // Process input and header data to nistUploadResult struct to simplify uploads.
+ var results []nistUploadResult
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)
- }
+ newResult := nistUploadResult{URLPath: url, JSONResult: input[i+1]}
+ results = append(results, newResult)
+ }
+ sessionID, err := getLastDigitDir(header.URL)
+ if err != nil {
+ log.Fatalf("Cannot get session id: %s", err)
}
- if ok, err := getResultsWithRetry(server, header.URL); err != nil {
- log.Fatal(err)
- } else if !ok {
- os.Exit(1)
- }
+ uploadResults(results, sessionID, config, sessionTokensCacheDir)
}
func main() {
flag.Parse()
+ // Check for various flags that are exclusive of each other.
+ // The flags that are available to upload results depend on the result format and storage.
+ // Only one result flag can be used at a time.
+ resultFlags := []bool{len(*uploadInputFile) > 0, len(*uploadDirectory) > 0}
+ resultFlagCount := 0
+ for _, f := range resultFlags {
+ if f {
+ resultFlagCount++
+ }
+ }
+ if resultFlagCount > 1 {
+ log.Fatalf("only one submit result action (-upload, -directory) is allowed at a time")
+ } else if resultFlagCount == 1 {
+ if len(*jsonInputFile) > 0 {
+ log.Fatalf("submit result action (-upload, -directory) cannot be used with -json")
+ } else if len(*runFlag) > 0 {
+ log.Fatalf("submit result action (-upload, -directory) cannot be used with -run")
+ } else if len(*fetchFlag) > 0 {
+ log.Fatalf("submit result action (-upload, -directory) cannot be used with -fetch")
+ } else if len(*expectedOutFlag) > 0 {
+ log.Fatalf("submit result action (-upload, -directory) cannot be used with -expected-out")
+ } else if *dumpRegcap {
+ log.Fatalf("submit result action (-upload, -directory) cannot be used with -regcap")
+ }
+ }
middle, err := subprocess.New(*wrapperPath)
if err != nil {
@@ -667,6 +809,11 @@
return
}
+ if len(*uploadDirectory) > 0 {
+ uploadResultsDirectory(*uploadDirectory, &config, sessionTokensCacheDir)
+ return
+ }
+
server, err := connect(&config, sessionTokensCacheDir)
if err != nil {
log.Fatal(err)