| // Copyright (c) 2019, Google Inc. | 
 | // | 
 | // Permission to use, copy, modify, and/or distribute this software for any | 
 | // purpose with or without fee is hereby granted, provided that the above | 
 | // copyright notice and this permission notice appear in all copies. | 
 | // | 
 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | 
 | // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | 
 | // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY | 
 | // SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | 
 | // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION | 
 | // OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | 
 | // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | 
 |  | 
 | 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" | 
 | 	"strings" | 
 | 	"time" | 
 |  | 
 | 	"boringssl.googlesource.com/boringssl/util/fipstools/acvp/acvptool/acvp" | 
 | 	"boringssl.googlesource.com/boringssl/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") | 
 | 	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/util/fipstools/acvp/modulewrapper/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 interface{}, 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) (interface{}, 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 | 
 | } | 
 |  | 
 | // 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 { | 
 | 	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]interface{}, 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 looksLikeHeaderElement(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]interface{}{ | 
 | 			"vsId":       commonFields.ID, | 
 | 			"testGroups": replyGroups, | 
 | 		} | 
 | 		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 main() { | 
 | 	flag.Parse() | 
 |  | 
 | 	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:]) | 
 | 		} | 
 | 	} | 
 |  | 
 | 	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]interface{} | 
 | 	if err := json.Unmarshal(configBytes, &supportedAlgos); err != nil { | 
 | 		log.Fatalf("failed to parse configuration from Middle: %s", err) | 
 | 	} | 
 |  | 
 | 	if *dumpRegcap { | 
 | 		nonTestAlgos := make([]map[string]interface{}, 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 | 
 | 				} | 
 | 			} | 
 | 			nonTestAlgos = append(nonTestAlgos, algo) | 
 | 		} | 
 |  | 
 | 		regcap := []map[string]interface{}{ | 
 | 			map[string]interface{}{"acvVersion": "1.0"}, | 
 | 			map[string]interface{}{"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") | 
 | 		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) | 
 | 	} | 
 |  | 
 | 	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]interface{} | 
 | 	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) | 
 | 		} | 
 | 	} | 
 |  | 
 | 	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(map[string]interface{}{ | 
 | 			"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") | 
 | 		os.Exit(0) | 
 | 	} | 
 |  | 
 | 	if ok, err := getResultsWithRetry(server, url); err != nil { | 
 | 		log.Fatal(err) | 
 | 	} else if !ok { | 
 | 		os.Exit(1) | 
 | 	} | 
 | } |