|  | // Copyright 2019 The BoringSSL Authors | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //     https://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | package main | 
|  |  | 
|  | import ( | 
|  | "bufio" | 
|  | "bytes" | 
|  | "crypto" | 
|  | "crypto/hmac" | 
|  | "crypto/sha256" | 
|  | "crypto/x509" | 
|  | "encoding/base64" | 
|  | "encoding/binary" | 
|  | "encoding/json" | 
|  | "encoding/pem" | 
|  | "errors" | 
|  | "flag" | 
|  | "fmt" | 
|  | "io" | 
|  | "log" | 
|  | "net/http" | 
|  | neturl "net/url" | 
|  | "os" | 
|  | "path/filepath" | 
|  | "strconv" | 
|  | "strings" | 
|  | "time" | 
|  |  | 
|  | "boringssl.googlesource.com/boringssl.git/util/fipstools/acvp/acvptool/acvp" | 
|  | "boringssl.googlesource.com/boringssl.git/util/fipstools/acvp/acvptool/subprocess" | 
|  | ) | 
|  |  | 
|  | 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") | 
|  | 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") | 
|  | wrapperPath     = flag.String("wrapper", "../../../../build/modulewrapper", "Path to the wrapper binary") | 
|  | ) | 
|  |  | 
|  | type Config struct { | 
|  | CertPEMFile        string | 
|  | PrivateKeyFile     string | 
|  | PrivateKeyDERFile  string | 
|  | TOTPSecret         string | 
|  | ACVPServer         string | 
|  | SessionTokensCache string | 
|  | LogFile            string | 
|  | } | 
|  |  | 
|  | func isCommentLine(line []byte) bool { | 
|  | var foundCommentStart bool | 
|  | for _, b := range line { | 
|  | if !foundCommentStart { | 
|  | if b == ' ' || b == '\t' { | 
|  | continue | 
|  | } | 
|  | if b != '/' { | 
|  | return false | 
|  | } | 
|  | foundCommentStart = true | 
|  | } else { | 
|  | return b == '/' | 
|  | } | 
|  | } | 
|  | return false | 
|  | } | 
|  |  | 
|  | func jsonFromFile(out any, filename string) error { | 
|  | in, err := os.Open(filename) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | defer in.Close() | 
|  |  | 
|  | scanner := bufio.NewScanner(in) | 
|  | var commentsRemoved bytes.Buffer | 
|  | for scanner.Scan() { | 
|  | if isCommentLine(scanner.Bytes()) { | 
|  | continue | 
|  | } | 
|  | commentsRemoved.Write(scanner.Bytes()) | 
|  | commentsRemoved.WriteString("\n") | 
|  | } | 
|  | if err := scanner.Err(); err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | decoder := json.NewDecoder(&commentsRemoved) | 
|  | decoder.DisallowUnknownFields() | 
|  | if err := decoder.Decode(out); err != nil { | 
|  | return err | 
|  | } | 
|  | if decoder.More() { | 
|  | return errors.New("trailing garbage found") | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // TOTP implements the time-based one-time password algorithm with the suggested | 
|  | // granularity of 30 seconds. See https://tools.ietf.org/html/rfc6238 and then | 
|  | // https://tools.ietf.org/html/rfc4226#section-5.3 | 
|  | func TOTP(secret []byte) string { | 
|  | const timeStep = 30 | 
|  | now := uint64(time.Now().Unix()) / 30 | 
|  | var nowBuf [8]byte | 
|  | binary.BigEndian.PutUint64(nowBuf[:], now) | 
|  | mac := hmac.New(sha256.New, secret) | 
|  | mac.Write(nowBuf[:]) | 
|  | digest := mac.Sum(nil) | 
|  | value := binary.BigEndian.Uint32(digest[digest[31]&15:]) | 
|  | value &= 0x7fffffff | 
|  | value %= 100000000 | 
|  | return fmt.Sprintf("%08d", value) | 
|  | } | 
|  |  | 
|  | type Middle interface { | 
|  | Close() | 
|  | Config() ([]byte, error) | 
|  | Process(algorithm string, vectorSet []byte) (any, error) | 
|  | } | 
|  |  | 
|  | func loadCachedSessionTokens(server *acvp.Server, cachePath string) error { | 
|  | cacheDir, err := os.Open(cachePath) | 
|  | if err != nil { | 
|  | if os.IsNotExist(err) { | 
|  | if err := os.Mkdir(cachePath, 0700); err != nil { | 
|  | return fmt.Errorf("Failed to create session token cache directory %q: %s", cachePath, err) | 
|  | } | 
|  | return nil | 
|  | } | 
|  | return fmt.Errorf("Failed to open session token cache directory %q: %s", cachePath, err) | 
|  | } | 
|  | defer cacheDir.Close() | 
|  | names, err := cacheDir.Readdirnames(0) | 
|  | if err != nil { | 
|  | return fmt.Errorf("Failed to list session token cache directory %q: %s", cachePath, err) | 
|  | } | 
|  |  | 
|  | loaded := 0 | 
|  | for _, name := range names { | 
|  | if !strings.HasSuffix(name, ".token") { | 
|  | continue | 
|  | } | 
|  | path := filepath.Join(cachePath, name) | 
|  | contents, err := os.ReadFile(path) | 
|  | if err != nil { | 
|  | return fmt.Errorf("Failed to read session token cache entry %q: %s", path, err) | 
|  | } | 
|  | urlPath, err := neturl.PathUnescape(name[:len(name)-6]) | 
|  | if err != nil { | 
|  | return fmt.Errorf("Failed to unescape token filename %q: %s", name, err) | 
|  | } | 
|  | server.PrefixTokens[urlPath] = string(contents) | 
|  | loaded++ | 
|  | } | 
|  |  | 
|  | log.Printf("Loaded %d cached tokens", loaded) | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func trimLeadingSlash(s string) string { | 
|  | if strings.HasPrefix(s, "/") { | 
|  | return s[1:] | 
|  | } | 
|  | return s | 
|  | } | 
|  |  | 
|  | func addTrailingSlash(s string) string { | 
|  | if !strings.HasSuffix(s, "/") { | 
|  | s += "/" | 
|  | } | 
|  | return s | 
|  | } | 
|  |  | 
|  | // 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"` | 
|  | } | 
|  | if err := json.Unmarshal(element, &headerFields); err != nil { | 
|  | return false | 
|  | } | 
|  | return len(headerFields.URL) > 0 || len(headerFields.Algorithm) == 0 | 
|  | } | 
|  |  | 
|  | // 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]any, middle Middle) error { | 
|  | jsonBytes, err := os.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 one element in the file. | 
|  | if len(elements) < 1 { | 
|  | return errors.New("JSON input is empty") | 
|  | } | 
|  |  | 
|  | var header json.RawMessage | 
|  | if looksLikeVectorSetHeader(elements[0]) { | 
|  | header, elements = elements[0], elements[1:] | 
|  | if len(elements) == 0 { | 
|  | return errors.New("JSON input is empty") | 
|  | } | 
|  | } | 
|  |  | 
|  | // 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("[") | 
|  |  | 
|  | if header != nil { | 
|  | headerBytes, err := json.MarshalIndent(header, "", "    ") | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | result.Write(headerBytes) | 
|  | result.WriteString(",") | 
|  | } | 
|  |  | 
|  | for i, element := range elements { | 
|  | 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]any{ | 
|  | "vsId":       commonFields.ID, | 
|  | "testGroups": replyGroups, | 
|  | "algorithm":  algo, | 
|  | } | 
|  | replyBytes, err := json.MarshalIndent(group, "", "    ") | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | if i != 0 { | 
|  | result.WriteString(",") | 
|  | } | 
|  | result.Write(replyBytes) | 
|  | } | 
|  |  | 
|  | result.WriteString("]\n") | 
|  | os.Stdout.Write(result.Bytes()) | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | // getVectorsWithRetry fetches the given url from the server and parses it as a | 
|  | // set of vectors. Any server requested retry is handled. | 
|  | func getVectorsWithRetry(server *acvp.Server, url string) (out acvp.Vectors, vectorsBytes []byte, err error) { | 
|  | for { | 
|  | if vectorsBytes, err = server.GetBytes(url); err != nil { | 
|  | return out, nil, err | 
|  | } | 
|  |  | 
|  | var vectors acvp.Vectors | 
|  | if err := json.Unmarshal(vectorsBytes, &vectors); err != nil { | 
|  | return out, nil, err | 
|  | } | 
|  |  | 
|  | retry := vectors.Retry | 
|  | if retry == 0 { | 
|  | return vectors, vectorsBytes, nil | 
|  | } | 
|  |  | 
|  | log.Printf("Server requested %d seconds delay", retry) | 
|  | if retry > 10 { | 
|  | retry = 10 | 
|  | } | 
|  | time.Sleep(time.Duration(retry) * time.Second) | 
|  | } | 
|  | } | 
|  |  | 
|  | func uploadResult(server *acvp.Server, setURL string, resultData []byte) error { | 
|  | resultSize := uint64(len(resultData)) + 32 /* for framing overhead */ | 
|  | if server.SizeLimit == 0 || resultSize < server.SizeLimit { | 
|  | log.Printf("Result size %d bytes", resultSize) | 
|  | return server.Post(nil, trimLeadingSlash(setURL)+"/results", resultData) | 
|  | } | 
|  |  | 
|  | // The NIST ACVP server no longer requires the large-upload process, | 
|  | // suggesting that this may no longer be needed. | 
|  | log.Printf("Result is %d bytes, too much given server limit of %d bytes. Using large-upload process.", resultSize, server.SizeLimit) | 
|  | largeRequestBytes, err := json.Marshal(acvp.LargeUploadRequest{ | 
|  | Size: resultSize, | 
|  | URL:  setURL, | 
|  | }) | 
|  | if err != nil { | 
|  | return errors.New("failed to marshal large-upload request: " + err.Error()) | 
|  | } | 
|  |  | 
|  | var largeResponse acvp.LargeUploadResponse | 
|  | if err := server.Post(&largeResponse, "/large", largeRequestBytes); err != nil { | 
|  | return errors.New("failed to request large-upload endpoint: " + err.Error()) | 
|  | } | 
|  |  | 
|  | log.Printf("Directed to large-upload endpoint at %q", largeResponse.URL) | 
|  | req, err := http.NewRequest("POST", largeResponse.URL, bytes.NewBuffer(resultData)) | 
|  | if err != nil { | 
|  | return errors.New("failed to create POST request: " + err.Error()) | 
|  | } | 
|  | token := largeResponse.AccessToken | 
|  | if len(token) == 0 { | 
|  | token = server.AccessToken | 
|  | } | 
|  | req.Header.Add("Authorization", "Bearer "+token) | 
|  | req.Header.Add("Content-Type", "application/json") | 
|  |  | 
|  | client := &http.Client{} | 
|  | resp, err := client.Do(req) | 
|  | if err != nil { | 
|  | return errors.New("failed writing large upload: " + err.Error()) | 
|  | } | 
|  | resp.Body.Close() | 
|  | if resp.StatusCode != 200 { | 
|  | return fmt.Errorf("large upload resulted in status code %d", resp.StatusCode) | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func connect(config *Config, sessionTokensCacheDir string) (*acvp.Server, error) { | 
|  | if len(config.TOTPSecret) == 0 { | 
|  | return nil, errors.New("config file missing TOTPSecret") | 
|  | } | 
|  | totpSecret, err := base64.StdEncoding.DecodeString(config.TOTPSecret) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("failed to base64-decode TOTP secret from config file: %s. (Note that the secret _itself_ should be in the config, not the name of a file that contains it.)", err) | 
|  | } | 
|  |  | 
|  | if len(config.CertPEMFile) == 0 { | 
|  | return nil, errors.New("config file missing CertPEMFile") | 
|  | } | 
|  | certPEM, err := os.ReadFile(config.CertPEMFile) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("failed to read certificate from %q: %s", config.CertPEMFile, err) | 
|  | } | 
|  | block, _ := pem.Decode(certPEM) | 
|  | certDER := block.Bytes | 
|  |  | 
|  | if len(config.PrivateKeyDERFile) == 0 && len(config.PrivateKeyFile) == 0 { | 
|  | 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") | 
|  | } | 
|  | privateKeyFile := config.PrivateKeyDERFile | 
|  | if len(config.PrivateKeyFile) > 0 { | 
|  | privateKeyFile = config.PrivateKeyFile | 
|  | } | 
|  |  | 
|  | keyBytes, err := os.ReadFile(privateKeyFile) | 
|  | if err != nil { | 
|  | return nil, fmt.Errorf("failed to read private key from %q: %s", privateKeyFile, err) | 
|  | } | 
|  |  | 
|  | var keyDER []byte | 
|  | pemBlock, _ := pem.Decode(keyBytes) | 
|  | if pemBlock != nil { | 
|  | keyDER = pemBlock.Bytes | 
|  | } else { | 
|  | keyDER = keyBytes | 
|  | } | 
|  |  | 
|  | var certKey crypto.PrivateKey | 
|  | if certKey, err = x509.ParsePKCS1PrivateKey(keyDER); err != nil { | 
|  | if certKey, err = x509.ParsePKCS8PrivateKey(keyDER); err != nil { | 
|  | return nil, fmt.Errorf("failed to parse private key from %q: %s", privateKeyFile, err) | 
|  | } | 
|  | } | 
|  |  | 
|  | serverURL := "https://demo.acvts.nist.gov/" | 
|  | if len(config.ACVPServer) > 0 { | 
|  | serverURL = config.ACVPServer | 
|  | } | 
|  | server := acvp.NewServer(serverURL, config.LogFile, [][]byte{certDER}, certKey, func() string { | 
|  | return TOTP(totpSecret[:]) | 
|  | }) | 
|  |  | 
|  | if len(sessionTokensCacheDir) > 0 { | 
|  | if err := loadCachedSessionTokens(server, sessionTokensCacheDir); err != nil { | 
|  | return nil, err | 
|  | } | 
|  | } | 
|  |  | 
|  | return server, nil | 
|  | } | 
|  |  | 
|  | func getResultsWithRetry(server *acvp.Server, url string) (bool, error) { | 
|  | FetchResults: | 
|  | for { | 
|  | var results acvp.SessionResults | 
|  | if err := server.Get(&results, trimLeadingSlash(url)+"/results"); err != nil { | 
|  | return false, errors.New("failed to fetch session results: " + err.Error()) | 
|  | } | 
|  |  | 
|  | if results.Passed { | 
|  | log.Print("Test passed") | 
|  | return true, nil | 
|  | } | 
|  |  | 
|  | for _, result := range results.Results { | 
|  | if result.Status == "incomplete" { | 
|  | log.Print("Server hasn't finished processing results. Waiting 10 seconds.") | 
|  | time.Sleep(10 * time.Second) | 
|  | continue FetchResults | 
|  | } | 
|  | } | 
|  |  | 
|  | log.Printf("Server did not accept results: %#v", results) | 
|  | return false, nil | 
|  | } | 
|  | } | 
|  |  | 
|  | 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 | 
|  | } | 
|  |  | 
|  | // Processes test result and returns them in format to be uploaded. | 
|  | func processResultContent(previousResults []nistUploadResult, result []byte, sessionID string, filename string) []nistUploadResult { | 
|  | var data []vectorResult | 
|  | if err := json.Unmarshal(result, &data); err != nil { | 
|  | // Assume file is not JSON. Log and continue to next file. | 
|  | log.Printf("Failed to parse %q: %s", filename, err) | 
|  | return previousResults | 
|  | } | 
|  |  | 
|  | 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} | 
|  | return append(previousResults, newResult) | 
|  | } | 
|  |  | 
|  | // 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 := os.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()) | 
|  | content, err := os.ReadFile(filePath) | 
|  | if err != nil { | 
|  | log.Fatalf("Cannot open input: %s", err) | 
|  | } | 
|  |  | 
|  | results = processResultContent(results, content, sessionID, filePath) | 
|  | } | 
|  |  | 
|  | 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 { | 
|  | URL           string   `json:"url,omitempty"` | 
|  | VectorSetURLs []string `json:"vectorSetUrls,omitempty"` | 
|  | Time          string   `json:"time,omitempty"` | 
|  | } | 
|  |  | 
|  | func uploadFromFile(file string, config *Config, sessionTokensCacheDir string) { | 
|  | 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) | 
|  | } | 
|  |  | 
|  | // Process input and header data to nistUploadResult struct to simplify uploads. | 
|  | var results []nistUploadResult | 
|  | for i, url := range header.VectorSetURLs { | 
|  | 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) | 
|  | } | 
|  |  | 
|  | 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, -gcs) is allowed at a time") | 
|  | } else if resultFlagCount == 1 { | 
|  | if len(*jsonInputFile) > 0 { | 
|  | log.Fatalf("submit result action (-upload, -directory, -gcs) cannot be used with -json") | 
|  | } else if len(*runFlag) > 0 { | 
|  | log.Fatalf("submit result action (-upload, -directory, -gcs) cannot be used with -run") | 
|  | } else if len(*fetchFlag) > 0 { | 
|  | log.Fatalf("submit result action (-upload, -directory, -gcs) cannot be used with -fetch") | 
|  | } else if len(*expectedOutFlag) > 0 { | 
|  | log.Fatalf("submit result action (-upload, -directory, -gcs) cannot be used with -expected-out") | 
|  | } else if *dumpRegcap { | 
|  | log.Fatalf("submit result action (-upload, -directory, -gcs) cannot be used with -regcap") | 
|  | } | 
|  | } | 
|  |  | 
|  | middle, err := subprocess.New(*wrapperPath) | 
|  | if err != nil { | 
|  | log.Fatalf("failed to initialise middle: %s", err) | 
|  | } | 
|  | defer middle.Close() | 
|  |  | 
|  | configBytes, err := middle.Config() | 
|  | if err != nil { | 
|  | log.Fatalf("failed to get config from middle: %s", err) | 
|  | } | 
|  |  | 
|  | var supportedAlgos []map[string]any | 
|  | if err := json.Unmarshal(configBytes, &supportedAlgos); err != nil { | 
|  | log.Fatalf("failed to parse configuration from Middle: %s", err) | 
|  | } | 
|  |  | 
|  | if *dumpRegcap { | 
|  | nonTestAlgos := make([]map[string]any, 0, len(supportedAlgos)) | 
|  | for _, algo := range supportedAlgos { | 
|  | if value, ok := algo["acvptoolTestOnly"]; ok { | 
|  | testOnly, ok := value.(bool) | 
|  | if !ok { | 
|  | log.Fatalf("modulewrapper config contains acvptoolTestOnly field with non-boolean value %#v", value) | 
|  | } | 
|  | if testOnly { | 
|  | continue | 
|  | } | 
|  | } | 
|  | if value, ok := algo["algorithm"]; ok { | 
|  | algorithm, ok := value.(string) | 
|  | if ok && algorithm == "acvptool" { | 
|  | continue | 
|  | } | 
|  | } | 
|  | nonTestAlgos = append(nonTestAlgos, algo) | 
|  | } | 
|  |  | 
|  | regcap := []map[string]any{ | 
|  | {"acvVersion": "1.0"}, | 
|  | {"algorithms": nonTestAlgos}, | 
|  | } | 
|  | regcapBytes, err := json.MarshalIndent(regcap, "", "    ") | 
|  | if err != nil { | 
|  | log.Fatalf("failed to marshal regcap: %s", err) | 
|  | } | 
|  | os.Stdout.Write(regcapBytes) | 
|  | os.Stdout.WriteString("\n") | 
|  | return | 
|  | } | 
|  |  | 
|  | if len(*jsonInputFile) > 0 { | 
|  | if err := processFile(*jsonInputFile, supportedAlgos, middle); err != nil { | 
|  | log.Fatalf("failed to process input file: %s", err) | 
|  | } | 
|  | return | 
|  | } | 
|  |  | 
|  | var requestedAlgosFlag string | 
|  | // The output file to which expected results are written, if requested. | 
|  | var expectedOut *os.File | 
|  | // A tee that outputs to both stdout (for vectors) and the file for | 
|  | // expected results, if any. | 
|  | var fetchOutputTee io.Writer | 
|  |  | 
|  | if len(*runFlag) > 0 && len(*fetchFlag) > 0 { | 
|  | log.Fatalf("cannot specify both -run and -fetch") | 
|  | } | 
|  | if len(*expectedOutFlag) > 0 && len(*fetchFlag) == 0 { | 
|  | log.Fatalf("-expected-out can only be used with -fetch") | 
|  | } | 
|  | if len(*runFlag) > 0 { | 
|  | requestedAlgosFlag = *runFlag | 
|  | } else { | 
|  | requestedAlgosFlag = *fetchFlag | 
|  | if len(*expectedOutFlag) > 0 { | 
|  | if expectedOut, err = os.Create(*expectedOutFlag); err != nil { | 
|  | log.Fatalf("cannot open %q: %s", *expectedOutFlag, err) | 
|  | } | 
|  | fetchOutputTee = io.MultiWriter(os.Stdout, expectedOut) | 
|  | defer expectedOut.Close() | 
|  | } else { | 
|  | fetchOutputTee = os.Stdout | 
|  | } | 
|  | } | 
|  |  | 
|  | runAlgos := make(map[string]bool) | 
|  | if len(requestedAlgosFlag) > 0 { | 
|  | for _, substr := range strings.Split(requestedAlgosFlag, ",") { | 
|  | runAlgos[substr] = false | 
|  | } | 
|  | } | 
|  |  | 
|  | var algorithms []map[string]any | 
|  | for _, supportedAlgo := range supportedAlgos { | 
|  | algoInterface, ok := supportedAlgo["algorithm"] | 
|  | if !ok { | 
|  | continue | 
|  | } | 
|  |  | 
|  | algo, ok := algoInterface.(string) | 
|  | if !ok { | 
|  | continue | 
|  | } | 
|  |  | 
|  | if _, ok := runAlgos[algo]; ok { | 
|  | algorithms = append(algorithms, supportedAlgo) | 
|  | runAlgos[algo] = true | 
|  | } | 
|  | } | 
|  |  | 
|  | for algo, recognised := range runAlgos { | 
|  | if !recognised { | 
|  | log.Fatalf("requested algorithm %q was not recognised", algo) | 
|  | } | 
|  | } | 
|  |  | 
|  | var config Config | 
|  | if err := jsonFromFile(&config, *configFilename); err != nil { | 
|  | log.Fatalf("Failed to load config file: %s", err) | 
|  | } | 
|  |  | 
|  | var sessionTokensCacheDir string | 
|  | if len(config.SessionTokensCache) > 0 { | 
|  | sessionTokensCacheDir = config.SessionTokensCache | 
|  | if strings.HasPrefix(sessionTokensCacheDir, "~/") { | 
|  | home := os.Getenv("HOME") | 
|  | if len(home) == 0 { | 
|  | log.Fatal("~ used in config file but $HOME not set") | 
|  | } | 
|  | sessionTokensCacheDir = filepath.Join(home, sessionTokensCacheDir[2:]) | 
|  | } | 
|  | } | 
|  |  | 
|  | if len(*uploadInputFile) > 0 { | 
|  | uploadFromFile(*uploadInputFile, &config, sessionTokensCacheDir) | 
|  | return | 
|  | } | 
|  |  | 
|  | if len(*uploadDirectory) > 0 { | 
|  | uploadResultsDirectory(*uploadDirectory, &config, sessionTokensCacheDir) | 
|  | return | 
|  | } | 
|  | if handleGCSFlag(&config, sessionTokensCacheDir) { | 
|  | return | 
|  | } | 
|  |  | 
|  | server, err := connect(&config, sessionTokensCacheDir) | 
|  | if err != nil { | 
|  | log.Fatal(err) | 
|  | } | 
|  |  | 
|  | if err := server.Login(); err != nil { | 
|  | log.Fatalf("failed to login: %s", err) | 
|  | } | 
|  |  | 
|  | if len(requestedAlgosFlag) == 0 { | 
|  | if interactiveModeSupported { | 
|  | runInteractive(server, config) | 
|  | } else { | 
|  | log.Fatalf("no arguments given but interactive mode not supported") | 
|  | } | 
|  | return | 
|  | } | 
|  |  | 
|  | requestBytes, err := json.Marshal(acvp.TestSession{ | 
|  | IsSample:    true, | 
|  | Publishable: false, | 
|  | Algorithms:  algorithms, | 
|  | }) | 
|  | if err != nil { | 
|  | log.Fatalf("Failed to serialise JSON: %s", err) | 
|  | } | 
|  |  | 
|  | var result acvp.TestSession | 
|  | if err := server.Post(&result, "acvp/v1/testSessions", requestBytes); err != nil { | 
|  | log.Fatalf("Request to create test session failed: %s", err) | 
|  | } | 
|  |  | 
|  | url := trimLeadingSlash(result.URL) | 
|  | log.Printf("Created test session %q", url) | 
|  | if token := result.AccessToken; len(token) > 0 { | 
|  | server.PrefixTokens[url] = token | 
|  | if len(sessionTokensCacheDir) > 0 { | 
|  | os.WriteFile(filepath.Join(sessionTokensCacheDir, neturl.PathEscape(url))+".token", []byte(token), 0600) | 
|  | } | 
|  | } | 
|  |  | 
|  | log.Printf("Have vector sets %v", result.VectorSetURLs) | 
|  |  | 
|  | if len(*fetchFlag) > 0 { | 
|  | io.WriteString(fetchOutputTee, "[\n") | 
|  | json.NewEncoder(fetchOutputTee).Encode(vectorSetHeader{ | 
|  | URL:           url, | 
|  | VectorSetURLs: result.VectorSetURLs, | 
|  | Time:          time.Now().Format(time.RFC3339), | 
|  | }) | 
|  | } | 
|  |  | 
|  | for _, setURL := range result.VectorSetURLs { | 
|  | log.Printf("Fetching test vectors %q", setURL) | 
|  |  | 
|  | vectors, vectorsBytes, err := getVectorsWithRetry(server, trimLeadingSlash(setURL)) | 
|  | if err != nil { | 
|  | log.Fatalf("Failed to fetch vector set %q: %s", setURL, err) | 
|  | } | 
|  |  | 
|  | if len(*fetchFlag) > 0 { | 
|  | os.Stdout.WriteString(",\n") | 
|  | os.Stdout.Write(vectorsBytes) | 
|  | } | 
|  |  | 
|  | if expectedOut != nil { | 
|  | log.Printf("Fetching expected results") | 
|  |  | 
|  | _, expectedResultsBytes, err := getVectorsWithRetry(server, trimLeadingSlash(setURL)+"/expected") | 
|  | if err != nil { | 
|  | log.Fatalf("Failed to fetch expected results: %s", err) | 
|  | } | 
|  |  | 
|  | expectedOut.WriteString(",") | 
|  | expectedOut.Write(expectedResultsBytes) | 
|  | } | 
|  |  | 
|  | if len(*fetchFlag) > 0 { | 
|  | continue | 
|  | } | 
|  |  | 
|  | replyGroups, err := middle.Process(vectors.Algo, vectorsBytes) | 
|  | if err != nil { | 
|  | log.Printf("Failed: %s", err) | 
|  | log.Printf("Deleting test set") | 
|  | server.Delete(url) | 
|  | os.Exit(1) | 
|  | } | 
|  |  | 
|  | headerBytes, err := json.Marshal(acvp.Vectors{ | 
|  | ID:   vectors.ID, | 
|  | Algo: vectors.Algo, | 
|  | }) | 
|  | if err != nil { | 
|  | log.Printf("Failed to marshal result: %s", err) | 
|  | log.Printf("Deleting test set") | 
|  | server.Delete(url) | 
|  | os.Exit(1) | 
|  | } | 
|  |  | 
|  | var resultBuf bytes.Buffer | 
|  | resultBuf.Write(headerBytes[:len(headerBytes)-1]) | 
|  | resultBuf.WriteString(`,"testGroups":`) | 
|  | 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("}") | 
|  |  | 
|  | if err := uploadResult(server, setURL, resultBuf.Bytes()); err != nil { | 
|  | log.Printf("Deleting test set") | 
|  | server.Delete(url) | 
|  | log.Fatal(err) | 
|  | } | 
|  | } | 
|  |  | 
|  | if len(*fetchFlag) > 0 { | 
|  | io.WriteString(fetchOutputTee, "]\n") | 
|  | return | 
|  | } | 
|  |  | 
|  | if ok, err := getResultsWithRetry(server, url); err != nil { | 
|  | log.Fatal(err) | 
|  | } else if !ok { | 
|  | os.Exit(1) | 
|  | } | 
|  | } |