1// Copyright 2019 The BoringSSL Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package main
16
17import (
18	"bufio"
19	"bytes"
20	"crypto"
21	"crypto/hmac"
22	"crypto/sha256"
23	"crypto/x509"
24	"encoding/base64"
25	"encoding/binary"
26	"encoding/json"
27	"encoding/pem"
28	"errors"
29	"flag"
30	"fmt"
31	"io"
32	"log"
33	"net/http"
34	neturl "net/url"
35	"os"
36	"path/filepath"
37	"strconv"
38	"strings"
39	"time"
40
41	"boringssl.googlesource.com/boringssl.git/util/fipstools/acvp/acvptool/acvp"
42	"boringssl.googlesource.com/boringssl.git/util/fipstools/acvp/acvptool/subprocess"
43)
44
45var (
46	dumpRegcap      = flag.Bool("regcap", false, "Print module capabilities JSON to stdout")
47	configFilename  = flag.String("config", "config.json", "Location of the configuration JSON file")
48	jsonInputFile   = flag.String("json", "", "Location of a vector-set input file")
49	uploadInputFile = flag.String("upload", "", "Location of a JSON results file to upload")
50	uploadDirectory = flag.String("directory", "", "Path to folder where result files to be uploaded are")
51	runFlag         = flag.String("run", "", "Name of primitive to run tests for")
52	fetchFlag       = flag.String("fetch", "", "Name of primitive to fetch vectors for")
53	expectedOutFlag = flag.String("expected-out", "", "Name of a file to write the expected results to")
54	wrapperPath     = flag.String("wrapper", "../../../../build/modulewrapper", "Path to the wrapper binary")
55)
56
57type Config struct {
58	CertPEMFile        string
59	PrivateKeyFile     string
60	PrivateKeyDERFile  string
61	TOTPSecret         string
62	ACVPServer         string
63	SessionTokensCache string
64	LogFile            string
65}
66
67func isCommentLine(line []byte) bool {
68	var foundCommentStart bool
69	for _, b := range line {
70		if !foundCommentStart {
71			if b == ' ' || b == '\t' {
72				continue
73			}
74			if b != '/' {
75				return false
76			}
77			foundCommentStart = true
78		} else {
79			return b == '/'
80		}
81	}
82	return false
83}
84
85func jsonFromFile(out any, filename string) error {
86	in, err := os.Open(filename)
87	if err != nil {
88		return err
89	}
90	defer in.Close()
91
92	scanner := bufio.NewScanner(in)
93	var commentsRemoved bytes.Buffer
94	for scanner.Scan() {
95		if isCommentLine(scanner.Bytes()) {
96			continue
97		}
98		commentsRemoved.Write(scanner.Bytes())
99		commentsRemoved.WriteString("\n")
100	}
101	if err := scanner.Err(); err != nil {
102		return err
103	}
104
105	decoder := json.NewDecoder(&commentsRemoved)
106	decoder.DisallowUnknownFields()
107	if err := decoder.Decode(out); err != nil {
108		return err
109	}
110	if decoder.More() {
111		return errors.New("trailing garbage found")
112	}
113	return nil
114}
115
116// TOTP implements the time-based one-time password algorithm with the suggested
117// granularity of 30 seconds. See https://tools.ietf.org/html/rfc6238 and then
118// https://tools.ietf.org/html/rfc4226#section-5.3
119func TOTP(secret []byte) string {
120	const timeStep = 30
121	now := uint64(time.Now().Unix()) / 30
122	var nowBuf [8]byte
123	binary.BigEndian.PutUint64(nowBuf[:], now)
124	mac := hmac.New(sha256.New, secret)
125	mac.Write(nowBuf[:])
126	digest := mac.Sum(nil)
127	value := binary.BigEndian.Uint32(digest[digest[31]&15:])
128	value &= 0x7fffffff
129	value %= 100000000
130	return fmt.Sprintf("%08d", value)
131}
132
133type Middle interface {
134	Close()
135	Config() ([]byte, error)
136	Process(algorithm string, vectorSet []byte) (any, error)
137}
138
139func loadCachedSessionTokens(server *acvp.Server, cachePath string) error {
140	cacheDir, err := os.Open(cachePath)
141	if err != nil {
142		if os.IsNotExist(err) {
143			if err := os.Mkdir(cachePath, 0700); err != nil {
144				return fmt.Errorf("Failed to create session token cache directory %q: %s", cachePath, err)
145			}
146			return nil
147		}
148		return fmt.Errorf("Failed to open session token cache directory %q: %s", cachePath, err)
149	}
150	defer cacheDir.Close()
151	names, err := cacheDir.Readdirnames(0)
152	if err != nil {
153		return fmt.Errorf("Failed to list session token cache directory %q: %s", cachePath, err)
154	}
155
156	loaded := 0
157	for _, name := range names {
158		if !strings.HasSuffix(name, ".token") {
159			continue
160		}
161		path := filepath.Join(cachePath, name)
162		contents, err := os.ReadFile(path)
163		if err != nil {
164			return fmt.Errorf("Failed to read session token cache entry %q: %s", path, err)
165		}
166		urlPath, err := neturl.PathUnescape(name[:len(name)-6])
167		if err != nil {
168			return fmt.Errorf("Failed to unescape token filename %q: %s", name, err)
169		}
170		server.PrefixTokens[urlPath] = string(contents)
171		loaded++
172	}
173
174	log.Printf("Loaded %d cached tokens", loaded)
175	return nil
176}
177
178func trimLeadingSlash(s string) string {
179	if strings.HasPrefix(s, "/") {
180		return s[1:]
181	}
182	return s
183}
184
185func addTrailingSlash(s string) string {
186	if !strings.HasSuffix(s, "/") {
187		s += "/"
188	}
189	return s
190}
191
192// looksLikeVectorSetHeader returns true iff element looks like it's a
193// vectorSetHeader, not a test. Some ACVP files contain a header as the first
194// element that should be duplicated into the response, and some don't. If the
195// element contains a "url" field, or if it's missing an "algorithm" field,
196// then we guess that it's a header.
197func looksLikeVectorSetHeader(element json.RawMessage) bool {
198	var headerFields struct {
199		URL       string `json:"url"`
200		Algorithm string `json:"algorithm"`
201	}
202	if err := json.Unmarshal(element, &headerFields); err != nil {
203		return false
204	}
205	return len(headerFields.URL) > 0 || len(headerFields.Algorithm) == 0
206}
207
208// processFile reads a file containing vector sets, at least in the format
209// preferred by our lab, and writes the results to stdout.
210func processFile(filename string, supportedAlgos []map[string]any, middle Middle) error {
211	jsonBytes, err := os.ReadFile(filename)
212	if err != nil {
213		return err
214	}
215
216	var elements []json.RawMessage
217	if err := json.Unmarshal(jsonBytes, &elements); err != nil {
218		return err
219	}
220
221	// There must be at least one element in the file.
222	if len(elements) < 1 {
223		return errors.New("JSON input is empty")
224	}
225
226	var header json.RawMessage
227	if looksLikeVectorSetHeader(elements[0]) {
228		header, elements = elements[0], elements[1:]
229		if len(elements) == 0 {
230			return errors.New("JSON input is empty")
231		}
232	}
233
234	// Build a map of which algorithms our Middle supports.
235	algos := make(map[string]struct{})
236	for _, supportedAlgo := range supportedAlgos {
237		algoInterface, ok := supportedAlgo["algorithm"]
238		if !ok {
239			continue
240		}
241		algo, ok := algoInterface.(string)
242		if !ok {
243			continue
244		}
245		algos[algo] = struct{}{}
246	}
247
248	var result bytes.Buffer
249	result.WriteString("[")
250
251	if header != nil {
252		headerBytes, err := json.MarshalIndent(header, "", "    ")
253		if err != nil {
254			return err
255		}
256		result.Write(headerBytes)
257		result.WriteString(",")
258	}
259
260	for i, element := range elements {
261		var commonFields struct {
262			Algo string `json:"algorithm"`
263			ID   uint64 `json:"vsId"`
264		}
265		if err := json.Unmarshal(element, &commonFields); err != nil {
266			return fmt.Errorf("failed to extract common fields from vector set #%d", i+1)
267		}
268
269		algo := commonFields.Algo
270		if _, ok := algos[algo]; !ok {
271			return fmt.Errorf("vector set #%d contains unsupported algorithm %q", i+1, algo)
272		}
273
274		replyGroups, err := middle.Process(algo, element)
275		if err != nil {
276			return fmt.Errorf("while processing vector set #%d: %s", i+1, err)
277		}
278
279		group := map[string]any{
280			"vsId":       commonFields.ID,
281			"testGroups": replyGroups,
282			"algorithm":  algo,
283		}
284		replyBytes, err := json.MarshalIndent(group, "", "    ")
285		if err != nil {
286			return err
287		}
288
289		if i != 0 {
290			result.WriteString(",")
291		}
292		result.Write(replyBytes)
293	}
294
295	result.WriteString("]\n")
296	os.Stdout.Write(result.Bytes())
297
298	return nil
299}
300
301// getVectorsWithRetry fetches the given url from the server and parses it as a
302// set of vectors. Any server requested retry is handled.
303func getVectorsWithRetry(server *acvp.Server, url string) (out acvp.Vectors, vectorsBytes []byte, err error) {
304	for {
305		if vectorsBytes, err = server.GetBytes(url); err != nil {
306			return out, nil, err
307		}
308
309		var vectors acvp.Vectors
310		if err := json.Unmarshal(vectorsBytes, &vectors); err != nil {
311			return out, nil, err
312		}
313
314		retry := vectors.Retry
315		if retry == 0 {
316			return vectors, vectorsBytes, nil
317		}
318
319		log.Printf("Server requested %d seconds delay", retry)
320		if retry > 10 {
321			retry = 10
322		}
323		time.Sleep(time.Duration(retry) * time.Second)
324	}
325}
326
327func uploadResult(server *acvp.Server, setURL string, resultData []byte) error {
328	resultSize := uint64(len(resultData)) + 32 /* for framing overhead */
329	if server.SizeLimit == 0 || resultSize < server.SizeLimit {
330		log.Printf("Result size %d bytes", resultSize)
331		return server.Post(nil, trimLeadingSlash(setURL)+"/results", resultData)
332	}
333
334	// The NIST ACVP server no longer requires the large-upload process,
335	// suggesting that this may no longer be needed.
336	log.Printf("Result is %d bytes, too much given server limit of %d bytes. Using large-upload process.", resultSize, server.SizeLimit)
337	largeRequestBytes, err := json.Marshal(acvp.LargeUploadRequest{
338		Size: resultSize,
339		URL:  setURL,
340	})
341	if err != nil {
342		return errors.New("failed to marshal large-upload request: " + err.Error())
343	}
344
345	var largeResponse acvp.LargeUploadResponse
346	if err := server.Post(&largeResponse, "/large", largeRequestBytes); err != nil {
347		return errors.New("failed to request large-upload endpoint: " + err.Error())
348	}
349
350	log.Printf("Directed to large-upload endpoint at %q", largeResponse.URL)
351	req, err := http.NewRequest("POST", largeResponse.URL, bytes.NewBuffer(resultData))
352	if err != nil {
353		return errors.New("failed to create POST request: " + err.Error())
354	}
355	token := largeResponse.AccessToken
356	if len(token) == 0 {
357		token = server.AccessToken
358	}
359	req.Header.Add("Authorization", "Bearer "+token)
360	req.Header.Add("Content-Type", "application/json")
361
362	client := &http.Client{}
363	resp, err := client.Do(req)
364	if err != nil {
365		return errors.New("failed writing large upload: " + err.Error())
366	}
367	resp.Body.Close()
368	if resp.StatusCode != 200 {
369		return fmt.Errorf("large upload resulted in status code %d", resp.StatusCode)
370	}
371
372	return nil
373}
374
375func connect(config *Config, sessionTokensCacheDir string) (*acvp.Server, error) {
376	if len(config.TOTPSecret) == 0 {
377		return nil, errors.New("config file missing TOTPSecret")
378	}
379	totpSecret, err := base64.StdEncoding.DecodeString(config.TOTPSecret)
380	if err != nil {
381		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)
382	}
383
384	if len(config.CertPEMFile) == 0 {
385		return nil, errors.New("config file missing CertPEMFile")
386	}
387	certPEM, err := os.ReadFile(config.CertPEMFile)
388	if err != nil {
389		return nil, fmt.Errorf("failed to read certificate from %q: %s", config.CertPEMFile, err)
390	}
391	block, _ := pem.Decode(certPEM)
392	certDER := block.Bytes
393
394	if len(config.PrivateKeyDERFile) == 0 && len(config.PrivateKeyFile) == 0 {
395		return nil, errors.New("config file missing PrivateKeyDERFile and PrivateKeyFile")
396	}
397	if len(config.PrivateKeyDERFile) != 0 && len(config.PrivateKeyFile) != 0 {
398		return nil, errors.New("config file has both PrivateKeyDERFile and PrivateKeyFile - can only have one")
399	}
400	privateKeyFile := config.PrivateKeyDERFile
401	if len(config.PrivateKeyFile) > 0 {
402		privateKeyFile = config.PrivateKeyFile
403	}
404
405	keyBytes, err := os.ReadFile(privateKeyFile)
406	if err != nil {
407		return nil, fmt.Errorf("failed to read private key from %q: %s", privateKeyFile, err)
408	}
409
410	var keyDER []byte
411	pemBlock, _ := pem.Decode(keyBytes)
412	if pemBlock != nil {
413		keyDER = pemBlock.Bytes
414	} else {
415		keyDER = keyBytes
416	}
417
418	var certKey crypto.PrivateKey
419	if certKey, err = x509.ParsePKCS1PrivateKey(keyDER); err != nil {
420		if certKey, err = x509.ParsePKCS8PrivateKey(keyDER); err != nil {
421			return nil, fmt.Errorf("failed to parse private key from %q: %s", privateKeyFile, err)
422		}
423	}
424
425	serverURL := "https://demo.acvts.nist.gov/"
426	if len(config.ACVPServer) > 0 {
427		serverURL = config.ACVPServer
428	}
429	server := acvp.NewServer(serverURL, config.LogFile, [][]byte{certDER}, certKey, func() string {
430		return TOTP(totpSecret[:])
431	})
432
433	if len(sessionTokensCacheDir) > 0 {
434		if err := loadCachedSessionTokens(server, sessionTokensCacheDir); err != nil {
435			return nil, err
436		}
437	}
438
439	return server, nil
440}
441
442func getResultsWithRetry(server *acvp.Server, url string) (bool, error) {
443FetchResults:
444	for {
445		var results acvp.SessionResults
446		if err := server.Get(&results, trimLeadingSlash(url)+"/results"); err != nil {
447			return false, errors.New("failed to fetch session results: " + err.Error())
448		}
449
450		if results.Passed {
451			log.Print("Test passed")
452			return true, nil
453		}
454
455		for _, result := range results.Results {
456			if result.Status == "incomplete" {
457				log.Print("Server hasn't finished processing results. Waiting 10 seconds.")
458				time.Sleep(10 * time.Second)
459				continue FetchResults
460			}
461		}
462
463		log.Printf("Server did not accept results: %#v", results)
464		return false, nil
465	}
466}
467
468func getLastDigitDir(path string) (string, error) {
469	parts := strings.Split(filepath.Clean(path), string(filepath.Separator))
470
471	for i := len(parts) - 1; i >= 0; i-- {
472		part := parts[i]
473		if _, err := strconv.Atoi(part); err == nil {
474			return part, nil
475		}
476	}
477	return "", errors.New("no directory consisting of only digits found")
478}
479
480func uploadResults(results []nistUploadResult, sessionID string, config *Config, sessionTokensCacheDir string) {
481	server, err := connect(config, sessionTokensCacheDir)
482	if err != nil {
483		log.Fatal(err)
484	}
485
486	for _, result := range results {
487		url := result.URLPath
488		payload := result.JSONResult
489		log.Printf("Uploading result for %q", url)
490		if err := uploadResult(server, url, payload); err != nil {
491			log.Fatalf("Failed to upload: %s", err)
492		}
493	}
494
495	if ok, err := getResultsWithRetry(server, fmt.Sprintf("/acvp/v1/testSessions/%s", sessionID)); err != nil {
496		log.Fatal(err)
497	} else if !ok {
498		os.Exit(1)
499	}
500}
501
502// Vector Test Result files are JSON formatted with various objects and keys.
503// Define structs to read and process the files.
504type vectorResult struct {
505	Version   string `json:"acvVersion,omitempty"`
506	Algorithm string `json:"algorithm,omitempty"`
507	ID        int    `json:"vsId,omitempty"`
508	// Objects under testGroups can have various keys so use an empty interface.
509	Tests []map[string]interface{} `json:"testGroups,omitempty"`
510}
511
512func getVectorSetID(jsonData []vectorResult) (int, error) {
513	vsId := 0
514	for _, item := range jsonData {
515		if item.ID > 0 && vsId == 0 {
516			vsId = item.ID
517		} else if item.ID > 0 && vsId != 0 {
518			return 0, errors.New("found multiple vsId values")
519		}
520	}
521	if vsId != 0 {
522		return vsId, nil
523	}
524	return 0, errors.New("could not find vsId")
525}
526
527func getVectorResult(jsonData []vectorResult) ([]byte, error) {
528	for _, item := range jsonData {
529		if item.ID > 0 {
530			out, err := json.Marshal(item)
531			if err != nil {
532				return nil, fmt.Errorf("unable to marshal JSON due to %s", err)
533			}
534			return out, nil
535		}
536	}
537	return nil, errors.New("could not find vsId necessary to identify vector result")
538}
539
540// Results to be uploaded have a specific URL path to POST/PUT to, along with
541// the test results.
542// Define a struct and store this data for processing.
543type nistUploadResult struct {
544	URLPath    string
545	JSONResult []byte
546}
547
548// Processes test result and returns them in format to be uploaded.
549func processResultContent(previousResults []nistUploadResult, result []byte, sessionID string, filename string) []nistUploadResult {
550	var data []vectorResult
551	if err := json.Unmarshal(result, &data); err != nil {
552		// Assume file is not JSON. Log and continue to next file.
553		log.Printf("Failed to parse %q: %s", filename, err)
554		return previousResults
555	}
556
557	vectorSetID, err := getVectorSetID(data)
558	if err != nil {
559		log.Fatalf("failed to get VectorSetID: %s", err)
560	}
561	// uploadResult() uses acvp.Server whose write() function takes the
562	// JSON *object* payload and turns it into a JSON *array* adding
563	// {"acvVersion":"1.0"} as a top-level object. Since the result file is
564	// already in this format, the JSON provided to uploadResult() must be
565	// modified to have those aspects removed. In other words, only store only
566	// the vector test result JSON object (do not store a JSON array or
567	// acvVersion object).
568	vectorTestResult, err := getVectorResult(data)
569	if err != nil {
570		log.Fatalf("failed to get VectorResult: %s", err)
571	}
572	requestPath := fmt.Sprintf("/acvp/v1/testSessions/%s/vectorSets/%d", sessionID, vectorSetID)
573	newResult := nistUploadResult{URLPath: requestPath, JSONResult: vectorTestResult}
574	return append(previousResults, newResult)
575}
576
577// Uploads a results directory based on the directory name being the session id.
578// Non-JSON files are ignored and JSON files are assumed to be test results.
579// The vectorSetId is retrieved from the test result file.
580func uploadResultsDirectory(directory string, config *Config, sessionTokensCacheDir string) {
581	directory = filepath.Clean(directory)
582	sessionID, err := getLastDigitDir(directory)
583	if err != nil {
584		log.Fatal(err)
585	}
586
587	var results []nistUploadResult
588	// Read directory, identify, and process all files.
589	files, err := os.ReadDir(directory)
590	if err != nil {
591		log.Fatalf("Unable to read directory: %s", err)
592	}
593
594	for _, file := range files {
595		// Add contents of the result file to results.
596		filePath := filepath.Join(directory, file.Name())
597		content, err := os.ReadFile(filePath)
598		if err != nil {
599			log.Fatalf("Cannot open input: %s", err)
600		}
601
602		results = processResultContent(results, content, sessionID, filePath)
603	}
604
605	uploadResults(results, sessionID, config, sessionTokensCacheDir)
606}
607
608// vectorSetHeader is the first element in the array of JSON elements that makes
609// up the on-disk format for a vector set.
610type vectorSetHeader struct {
611	URL           string   `json:"url,omitempty"`
612	VectorSetURLs []string `json:"vectorSetUrls,omitempty"`
613	Time          string   `json:"time,omitempty"`
614}
615
616func uploadFromFile(file string, config *Config, sessionTokensCacheDir string) {
617	in, err := os.Open(file)
618	if err != nil {
619		log.Fatalf("Cannot open input: %s", err)
620	}
621	defer in.Close()
622
623	decoder := json.NewDecoder(in)
624
625	var input []json.RawMessage
626	if err := decoder.Decode(&input); err != nil {
627		log.Fatalf("Failed to parse input: %s", err)
628	}
629
630	if len(input) < 2 {
631		log.Fatalf("Input JSON has fewer than two elements")
632	}
633
634	var header vectorSetHeader
635	if err := json.Unmarshal(input[0], &header); err != nil {
636		log.Fatalf("Failed to parse input header: %s", err)
637	}
638
639	if numGroups := len(input) - 1; numGroups != len(header.VectorSetURLs) {
640		log.Fatalf("have %d URLs from header, but only %d result groups", len(header.VectorSetURLs), numGroups)
641	}
642
643	// Process input and header data to nistUploadResult struct to simplify uploads.
644	var results []nistUploadResult
645	for i, url := range header.VectorSetURLs {
646		newResult := nistUploadResult{URLPath: url, JSONResult: input[i+1]}
647		results = append(results, newResult)
648	}
649	sessionID, err := getLastDigitDir(header.URL)
650	if err != nil {
651		log.Fatalf("Cannot get session id: %s", err)
652	}
653
654	uploadResults(results, sessionID, config, sessionTokensCacheDir)
655}
656
657func main() {
658	flag.Parse()
659	// Check for various flags that are exclusive of each other.
660	// The flags that are available to upload results depend on the result format and storage.
661	// Only one result flag can be used at a time.
662	resultFlags := []bool{len(*uploadInputFile) > 0, len(*uploadDirectory) > 0}
663	resultFlagCount := 0
664	for _, f := range resultFlags {
665		if f {
666			resultFlagCount++
667		}
668	}
669	if resultFlagCount > 1 {
670		log.Fatalf("only one submit result action (-upload, -directory, -gcs) is allowed at a time")
671	} else if resultFlagCount == 1 {
672		if len(*jsonInputFile) > 0 {
673			log.Fatalf("submit result action (-upload, -directory, -gcs) cannot be used with -json")
674		} else if len(*runFlag) > 0 {
675			log.Fatalf("submit result action (-upload, -directory, -gcs) cannot be used with -run")
676		} else if len(*fetchFlag) > 0 {
677			log.Fatalf("submit result action (-upload, -directory, -gcs) cannot be used with -fetch")
678		} else if len(*expectedOutFlag) > 0 {
679			log.Fatalf("submit result action (-upload, -directory, -gcs) cannot be used with -expected-out")
680		} else if *dumpRegcap {
681			log.Fatalf("submit result action (-upload, -directory, -gcs) cannot be used with -regcap")
682		}
683	}
684
685	middle, err := subprocess.New(*wrapperPath)
686	if err != nil {
687		log.Fatalf("failed to initialise middle: %s", err)
688	}
689	defer middle.Close()
690
691	configBytes, err := middle.Config()
692	if err != nil {
693		log.Fatalf("failed to get config from middle: %s", err)
694	}
695
696	var supportedAlgos []map[string]any
697	if err := json.Unmarshal(configBytes, &supportedAlgos); err != nil {
698		log.Fatalf("failed to parse configuration from Middle: %s", err)
699	}
700
701	if *dumpRegcap {
702		nonTestAlgos := make([]map[string]any, 0, len(supportedAlgos))
703		for _, algo := range supportedAlgos {
704			if value, ok := algo["acvptoolTestOnly"]; ok {
705				testOnly, ok := value.(bool)
706				if !ok {
707					log.Fatalf("modulewrapper config contains acvptoolTestOnly field with non-boolean value %#v", value)
708				}
709				if testOnly {
710					continue
711				}
712			}
713			if value, ok := algo["algorithm"]; ok {
714				algorithm, ok := value.(string)
715				if ok && algorithm == "acvptool" {
716					continue
717				}
718			}
719			nonTestAlgos = append(nonTestAlgos, algo)
720		}
721
722		regcap := []map[string]any{
723			{"acvVersion": "1.0"},
724			{"algorithms": nonTestAlgos},
725		}
726		regcapBytes, err := json.MarshalIndent(regcap, "", "    ")
727		if err != nil {
728			log.Fatalf("failed to marshal regcap: %s", err)
729		}
730		os.Stdout.Write(regcapBytes)
731		os.Stdout.WriteString("\n")
732		return
733	}
734
735	if len(*jsonInputFile) > 0 {
736		if err := processFile(*jsonInputFile, supportedAlgos, middle); err != nil {
737			log.Fatalf("failed to process input file: %s", err)
738		}
739		return
740	}
741
742	var requestedAlgosFlag string
743	// The output file to which expected results are written, if requested.
744	var expectedOut *os.File
745	// A tee that outputs to both stdout (for vectors) and the file for
746	// expected results, if any.
747	var fetchOutputTee io.Writer
748
749	if len(*runFlag) > 0 && len(*fetchFlag) > 0 {
750		log.Fatalf("cannot specify both -run and -fetch")
751	}
752	if len(*expectedOutFlag) > 0 && len(*fetchFlag) == 0 {
753		log.Fatalf("-expected-out can only be used with -fetch")
754	}
755	if len(*runFlag) > 0 {
756		requestedAlgosFlag = *runFlag
757	} else {
758		requestedAlgosFlag = *fetchFlag
759		if len(*expectedOutFlag) > 0 {
760			if expectedOut, err = os.Create(*expectedOutFlag); err != nil {
761				log.Fatalf("cannot open %q: %s", *expectedOutFlag, err)
762			}
763			fetchOutputTee = io.MultiWriter(os.Stdout, expectedOut)
764			defer expectedOut.Close()
765		} else {
766			fetchOutputTee = os.Stdout
767		}
768	}
769
770	runAlgos := make(map[string]bool)
771	if len(requestedAlgosFlag) > 0 {
772		for _, substr := range strings.Split(requestedAlgosFlag, ",") {
773			runAlgos[substr] = false
774		}
775	}
776
777	var algorithms []map[string]any
778	for _, supportedAlgo := range supportedAlgos {
779		algoInterface, ok := supportedAlgo["algorithm"]
780		if !ok {
781			continue
782		}
783
784		algo, ok := algoInterface.(string)
785		if !ok {
786			continue
787		}
788
789		if _, ok := runAlgos[algo]; ok {
790			algorithms = append(algorithms, supportedAlgo)
791			runAlgos[algo] = true
792		}
793	}
794
795	for algo, recognised := range runAlgos {
796		if !recognised {
797			log.Fatalf("requested algorithm %q was not recognised", algo)
798		}
799	}
800
801	var config Config
802	if err := jsonFromFile(&config, *configFilename); err != nil {
803		log.Fatalf("Failed to load config file: %s", err)
804	}
805
806	var sessionTokensCacheDir string
807	if len(config.SessionTokensCache) > 0 {
808		sessionTokensCacheDir = config.SessionTokensCache
809		if strings.HasPrefix(sessionTokensCacheDir, "~/") {
810			home := os.Getenv("HOME")
811			if len(home) == 0 {
812				log.Fatal("~ used in config file but $HOME not set")
813			}
814			sessionTokensCacheDir = filepath.Join(home, sessionTokensCacheDir[2:])
815		}
816	}
817
818	if len(*uploadInputFile) > 0 {
819		uploadFromFile(*uploadInputFile, &config, sessionTokensCacheDir)
820		return
821	}
822
823	if len(*uploadDirectory) > 0 {
824		uploadResultsDirectory(*uploadDirectory, &config, sessionTokensCacheDir)
825		return
826	}
827	if handleGCSFlag(&config, sessionTokensCacheDir) {
828		return
829	}
830
831	server, err := connect(&config, sessionTokensCacheDir)
832	if err != nil {
833		log.Fatal(err)
834	}
835
836	if err := server.Login(); err != nil {
837		log.Fatalf("failed to login: %s", err)
838	}
839
840	if len(requestedAlgosFlag) == 0 {
841		if interactiveModeSupported {
842			runInteractive(server, config)
843		} else {
844			log.Fatalf("no arguments given but interactive mode not supported")
845		}
846		return
847	}
848
849	requestBytes, err := json.Marshal(acvp.TestSession{
850		IsSample:    true,
851		Publishable: false,
852		Algorithms:  algorithms,
853	})
854	if err != nil {
855		log.Fatalf("Failed to serialise JSON: %s", err)
856	}
857
858	var result acvp.TestSession
859	if err := server.Post(&result, "acvp/v1/testSessions", requestBytes); err != nil {
860		log.Fatalf("Request to create test session failed: %s", err)
861	}
862
863	url := trimLeadingSlash(result.URL)
864	log.Printf("Created test session %q", url)
865	if token := result.AccessToken; len(token) > 0 {
866		server.PrefixTokens[url] = token
867		if len(sessionTokensCacheDir) > 0 {
868			os.WriteFile(filepath.Join(sessionTokensCacheDir, neturl.PathEscape(url))+".token", []byte(token), 0600)
869		}
870	}
871
872	log.Printf("Have vector sets %v", result.VectorSetURLs)
873
874	if len(*fetchFlag) > 0 {
875		io.WriteString(fetchOutputTee, "[\n")
876		json.NewEncoder(fetchOutputTee).Encode(vectorSetHeader{
877			URL:           url,
878			VectorSetURLs: result.VectorSetURLs,
879			Time:          time.Now().Format(time.RFC3339),
880		})
881	}
882
883	for _, setURL := range result.VectorSetURLs {
884		log.Printf("Fetching test vectors %q", setURL)
885
886		vectors, vectorsBytes, err := getVectorsWithRetry(server, trimLeadingSlash(setURL))
887		if err != nil {
888			log.Fatalf("Failed to fetch vector set %q: %s", setURL, err)
889		}
890
891		if len(*fetchFlag) > 0 {
892			os.Stdout.WriteString(",\n")
893			os.Stdout.Write(vectorsBytes)
894		}
895
896		if expectedOut != nil {
897			log.Printf("Fetching expected results")
898
899			_, expectedResultsBytes, err := getVectorsWithRetry(server, trimLeadingSlash(setURL)+"/expected")
900			if err != nil {
901				log.Fatalf("Failed to fetch expected results: %s", err)
902			}
903
904			expectedOut.WriteString(",")
905			expectedOut.Write(expectedResultsBytes)
906		}
907
908		if len(*fetchFlag) > 0 {
909			continue
910		}
911
912		replyGroups, err := middle.Process(vectors.Algo, vectorsBytes)
913		if err != nil {
914			log.Printf("Failed: %s", err)
915			log.Printf("Deleting test set")
916			server.Delete(url)
917			os.Exit(1)
918		}
919
920		headerBytes, err := json.Marshal(acvp.Vectors{
921			ID:   vectors.ID,
922			Algo: vectors.Algo,
923		})
924		if err != nil {
925			log.Printf("Failed to marshal result: %s", err)
926			log.Printf("Deleting test set")
927			server.Delete(url)
928			os.Exit(1)
929		}
930
931		var resultBuf bytes.Buffer
932		resultBuf.Write(headerBytes[:len(headerBytes)-1])
933		resultBuf.WriteString(`,"testGroups":`)
934		replyBytes, err := json.Marshal(replyGroups)
935		if err != nil {
936			log.Printf("Failed to marshal result: %s", err)
937			log.Printf("Deleting test set")
938			server.Delete(url)
939			os.Exit(1)
940		}
941		resultBuf.Write(replyBytes)
942		resultBuf.WriteString("}")
943
944		if err := uploadResult(server, setURL, resultBuf.Bytes()); err != nil {
945			log.Printf("Deleting test set")
946			server.Delete(url)
947			log.Fatal(err)
948		}
949	}
950
951	if len(*fetchFlag) > 0 {
952		io.WriteString(fetchOutputTee, "]\n")
953		return
954	}
955
956	if ok, err := getResultsWithRetry(server, url); err != nil {
957		log.Fatal(err)
958	} else if !ok {
959		os.Exit(1)
960	}
961}
962