xref: /aosp_15_r20/external/boringssl/src/util/fipstools/acvp/acvptool/acvp.go (revision 8fb009dc861624b67b6cdb62ea21f0f22d0c584b)
1// Copyright (c) 2019, Google Inc.
2//
3// Permission to use, copy, modify, and/or distribute this software for any
4// purpose with or without fee is hereby granted, provided that the above
5// copyright notice and this permission notice appear in all copies.
6//
7// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
10// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
12// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
13// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
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	"strings"
38	"time"
39
40	"boringssl.googlesource.com/boringssl/util/fipstools/acvp/acvptool/acvp"
41	"boringssl.googlesource.com/boringssl/util/fipstools/acvp/acvptool/subprocess"
42)
43
44var (
45	dumpRegcap      = flag.Bool("regcap", false, "Print module capabilities JSON to stdout")
46	configFilename  = flag.String("config", "config.json", "Location of the configuration JSON file")
47	jsonInputFile   = flag.String("json", "", "Location of a vector-set input file")
48	uploadInputFile = flag.String("upload", "", "Location of a JSON results file to upload")
49	runFlag         = flag.String("run", "", "Name of primitive to run tests for")
50	fetchFlag       = flag.String("fetch", "", "Name of primitive to fetch vectors for")
51	expectedOutFlag = flag.String("expected-out", "", "Name of a file to write the expected results to")
52	wrapperPath     = flag.String("wrapper", "../../../../build/util/fipstools/acvp/modulewrapper/modulewrapper", "Path to the wrapper binary")
53)
54
55type Config struct {
56	CertPEMFile        string
57	PrivateKeyFile     string
58	PrivateKeyDERFile  string
59	TOTPSecret         string
60	ACVPServer         string
61	SessionTokensCache string
62	LogFile            string
63}
64
65func isCommentLine(line []byte) bool {
66	var foundCommentStart bool
67	for _, b := range line {
68		if !foundCommentStart {
69			if b == ' ' || b == '\t' {
70				continue
71			}
72			if b != '/' {
73				return false
74			}
75			foundCommentStart = true
76		} else {
77			return b == '/'
78		}
79	}
80	return false
81}
82
83func jsonFromFile(out any, filename string) error {
84	in, err := os.Open(filename)
85	if err != nil {
86		return err
87	}
88	defer in.Close()
89
90	scanner := bufio.NewScanner(in)
91	var commentsRemoved bytes.Buffer
92	for scanner.Scan() {
93		if isCommentLine(scanner.Bytes()) {
94			continue
95		}
96		commentsRemoved.Write(scanner.Bytes())
97		commentsRemoved.WriteString("\n")
98	}
99	if err := scanner.Err(); err != nil {
100		return err
101	}
102
103	decoder := json.NewDecoder(&commentsRemoved)
104	decoder.DisallowUnknownFields()
105	if err := decoder.Decode(out); err != nil {
106		return err
107	}
108	if decoder.More() {
109		return errors.New("trailing garbage found")
110	}
111	return nil
112}
113
114// TOTP implements the time-based one-time password algorithm with the suggested
115// granularity of 30 seconds. See https://tools.ietf.org/html/rfc6238 and then
116// https://tools.ietf.org/html/rfc4226#section-5.3
117func TOTP(secret []byte) string {
118	const timeStep = 30
119	now := uint64(time.Now().Unix()) / 30
120	var nowBuf [8]byte
121	binary.BigEndian.PutUint64(nowBuf[:], now)
122	mac := hmac.New(sha256.New, secret)
123	mac.Write(nowBuf[:])
124	digest := mac.Sum(nil)
125	value := binary.BigEndian.Uint32(digest[digest[31]&15:])
126	value &= 0x7fffffff
127	value %= 100000000
128	return fmt.Sprintf("%08d", value)
129}
130
131type Middle interface {
132	Close()
133	Config() ([]byte, error)
134	Process(algorithm string, vectorSet []byte) (any, error)
135}
136
137func loadCachedSessionTokens(server *acvp.Server, cachePath string) error {
138	cacheDir, err := os.Open(cachePath)
139	if err != nil {
140		if os.IsNotExist(err) {
141			if err := os.Mkdir(cachePath, 0700); err != nil {
142				return fmt.Errorf("Failed to create session token cache directory %q: %s", cachePath, err)
143			}
144			return nil
145		}
146		return fmt.Errorf("Failed to open session token cache directory %q: %s", cachePath, err)
147	}
148	defer cacheDir.Close()
149	names, err := cacheDir.Readdirnames(0)
150	if err != nil {
151		return fmt.Errorf("Failed to list session token cache directory %q: %s", cachePath, err)
152	}
153
154	loaded := 0
155	for _, name := range names {
156		if !strings.HasSuffix(name, ".token") {
157			continue
158		}
159		path := filepath.Join(cachePath, name)
160		contents, err := os.ReadFile(path)
161		if err != nil {
162			return fmt.Errorf("Failed to read session token cache entry %q: %s", path, err)
163		}
164		urlPath, err := neturl.PathUnescape(name[:len(name)-6])
165		if err != nil {
166			return fmt.Errorf("Failed to unescape token filename %q: %s", name, err)
167		}
168		server.PrefixTokens[urlPath] = string(contents)
169		loaded++
170	}
171
172	log.Printf("Loaded %d cached tokens", loaded)
173	return nil
174}
175
176func trimLeadingSlash(s string) string {
177	if strings.HasPrefix(s, "/") {
178		return s[1:]
179	}
180	return s
181}
182
183// looksLikeVectorSetHeader returns true iff element looks like it's a
184// vectorSetHeader, not a test. Some ACVP files contain a header as the first
185// element that should be duplicated into the response, and some don't. If the
186// element contains a "url" field, or if it's missing an "algorithm" field,
187// then we guess that it's a header.
188func looksLikeVectorSetHeader(element json.RawMessage) bool {
189	var headerFields struct {
190		URL       string `json:"url"`
191		Algorithm string `json:"algorithm"`
192	}
193	if err := json.Unmarshal(element, &headerFields); err != nil {
194		return false
195	}
196	return len(headerFields.URL) > 0 || len(headerFields.Algorithm) == 0
197}
198
199// processFile reads a file containing vector sets, at least in the format
200// preferred by our lab, and writes the results to stdout.
201func processFile(filename string, supportedAlgos []map[string]any, middle Middle) error {
202	jsonBytes, err := os.ReadFile(filename)
203	if err != nil {
204		return err
205	}
206
207	var elements []json.RawMessage
208	if err := json.Unmarshal(jsonBytes, &elements); err != nil {
209		return err
210	}
211
212	// There must be at least one element in the file.
213	if len(elements) < 1 {
214		return errors.New("JSON input is empty")
215	}
216
217	var header json.RawMessage
218	if looksLikeVectorSetHeader(elements[0]) {
219		header, elements = elements[0], elements[1:]
220		if len(elements) == 0 {
221			return errors.New("JSON input is empty")
222		}
223	}
224
225	// Build a map of which algorithms our Middle supports.
226	algos := make(map[string]struct{})
227	for _, supportedAlgo := range supportedAlgos {
228		algoInterface, ok := supportedAlgo["algorithm"]
229		if !ok {
230			continue
231		}
232		algo, ok := algoInterface.(string)
233		if !ok {
234			continue
235		}
236		algos[algo] = struct{}{}
237	}
238
239	var result bytes.Buffer
240	result.WriteString("[")
241
242	if header != nil {
243		headerBytes, err := json.MarshalIndent(header, "", "    ")
244		if err != nil {
245			return err
246		}
247		result.Write(headerBytes)
248		result.WriteString(",")
249	}
250
251	for i, element := range elements {
252		var commonFields struct {
253			Algo string `json:"algorithm"`
254			ID   uint64 `json:"vsId"`
255		}
256		if err := json.Unmarshal(element, &commonFields); err != nil {
257			return fmt.Errorf("failed to extract common fields from vector set #%d", i+1)
258		}
259
260		algo := commonFields.Algo
261		if _, ok := algos[algo]; !ok {
262			return fmt.Errorf("vector set #%d contains unsupported algorithm %q", i+1, algo)
263		}
264
265		replyGroups, err := middle.Process(algo, element)
266		if err != nil {
267			return fmt.Errorf("while processing vector set #%d: %s", i+1, err)
268		}
269
270		group := map[string]any{
271			"vsId":       commonFields.ID,
272			"testGroups": replyGroups,
273			"algorithm":  algo,
274		}
275		replyBytes, err := json.MarshalIndent(group, "", "    ")
276		if err != nil {
277			return err
278		}
279
280		if i != 0 {
281			result.WriteString(",")
282		}
283		result.Write(replyBytes)
284	}
285
286	result.WriteString("]\n")
287	os.Stdout.Write(result.Bytes())
288
289	return nil
290}
291
292// getVectorsWithRetry fetches the given url from the server and parses it as a
293// set of vectors. Any server requested retry is handled.
294func getVectorsWithRetry(server *acvp.Server, url string) (out acvp.Vectors, vectorsBytes []byte, err error) {
295	for {
296		if vectorsBytes, err = server.GetBytes(url); err != nil {
297			return out, nil, err
298		}
299
300		var vectors acvp.Vectors
301		if err := json.Unmarshal(vectorsBytes, &vectors); err != nil {
302			return out, nil, err
303		}
304
305		retry := vectors.Retry
306		if retry == 0 {
307			return vectors, vectorsBytes, nil
308		}
309
310		log.Printf("Server requested %d seconds delay", retry)
311		if retry > 10 {
312			retry = 10
313		}
314		time.Sleep(time.Duration(retry) * time.Second)
315	}
316}
317
318func uploadResult(server *acvp.Server, setURL string, resultData []byte) error {
319	resultSize := uint64(len(resultData)) + 32 /* for framing overhead */
320	if server.SizeLimit == 0 || resultSize < server.SizeLimit {
321		log.Printf("Result size %d bytes", resultSize)
322		return server.Post(nil, trimLeadingSlash(setURL)+"/results", resultData)
323	}
324
325	// The NIST ACVP server no longer requires the large-upload process,
326	// suggesting that this may no longer be needed.
327	log.Printf("Result is %d bytes, too much given server limit of %d bytes. Using large-upload process.", resultSize, server.SizeLimit)
328	largeRequestBytes, err := json.Marshal(acvp.LargeUploadRequest{
329		Size: resultSize,
330		URL:  setURL,
331	})
332	if err != nil {
333		return errors.New("failed to marshal large-upload request: " + err.Error())
334	}
335
336	var largeResponse acvp.LargeUploadResponse
337	if err := server.Post(&largeResponse, "/large", largeRequestBytes); err != nil {
338		return errors.New("failed to request large-upload endpoint: " + err.Error())
339	}
340
341	log.Printf("Directed to large-upload endpoint at %q", largeResponse.URL)
342	req, err := http.NewRequest("POST", largeResponse.URL, bytes.NewBuffer(resultData))
343	if err != nil {
344		return errors.New("failed to create POST request: " + err.Error())
345	}
346	token := largeResponse.AccessToken
347	if len(token) == 0 {
348		token = server.AccessToken
349	}
350	req.Header.Add("Authorization", "Bearer "+token)
351	req.Header.Add("Content-Type", "application/json")
352
353	client := &http.Client{}
354	resp, err := client.Do(req)
355	if err != nil {
356		return errors.New("failed writing large upload: " + err.Error())
357	}
358	resp.Body.Close()
359	if resp.StatusCode != 200 {
360		return fmt.Errorf("large upload resulted in status code %d", resp.StatusCode)
361	}
362
363	return nil
364}
365
366func connect(config *Config, sessionTokensCacheDir string) (*acvp.Server, error) {
367	if len(config.TOTPSecret) == 0 {
368		return nil, errors.New("config file missing TOTPSecret")
369	}
370	totpSecret, err := base64.StdEncoding.DecodeString(config.TOTPSecret)
371	if err != nil {
372		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)
373	}
374
375	if len(config.CertPEMFile) == 0 {
376		return nil, errors.New("config file missing CertPEMFile")
377	}
378	certPEM, err := os.ReadFile(config.CertPEMFile)
379	if err != nil {
380		return nil, fmt.Errorf("failed to read certificate from %q: %s", config.CertPEMFile, err)
381	}
382	block, _ := pem.Decode(certPEM)
383	certDER := block.Bytes
384
385	if len(config.PrivateKeyDERFile) == 0 && len(config.PrivateKeyFile) == 0 {
386		return nil, errors.New("config file missing PrivateKeyDERFile and PrivateKeyFile")
387	}
388	if len(config.PrivateKeyDERFile) != 0 && len(config.PrivateKeyFile) != 0 {
389		return nil, errors.New("config file has both PrivateKeyDERFile and PrivateKeyFile. Can only have one.")
390	}
391	privateKeyFile := config.PrivateKeyDERFile
392	if len(config.PrivateKeyFile) > 0 {
393		privateKeyFile = config.PrivateKeyFile
394	}
395
396	keyBytes, err := os.ReadFile(privateKeyFile)
397	if err != nil {
398		return nil, fmt.Errorf("failed to read private key from %q: %s", privateKeyFile, err)
399	}
400
401	var keyDER []byte
402	pemBlock, _ := pem.Decode(keyBytes)
403	if pemBlock != nil {
404		keyDER = pemBlock.Bytes
405	} else {
406		keyDER = keyBytes
407	}
408
409	var certKey crypto.PrivateKey
410	if certKey, err = x509.ParsePKCS1PrivateKey(keyDER); err != nil {
411		if certKey, err = x509.ParsePKCS8PrivateKey(keyDER); err != nil {
412			return nil, fmt.Errorf("failed to parse private key from %q: %s", privateKeyFile, err)
413		}
414	}
415
416	serverURL := "https://demo.acvts.nist.gov/"
417	if len(config.ACVPServer) > 0 {
418		serverURL = config.ACVPServer
419	}
420	server := acvp.NewServer(serverURL, config.LogFile, [][]byte{certDER}, certKey, func() string {
421		return TOTP(totpSecret[:])
422	})
423
424	if len(sessionTokensCacheDir) > 0 {
425		if err := loadCachedSessionTokens(server, sessionTokensCacheDir); err != nil {
426			return nil, err
427		}
428	}
429
430	return server, nil
431}
432
433func getResultsWithRetry(server *acvp.Server, url string) (bool, error) {
434FetchResults:
435	for {
436		var results acvp.SessionResults
437		if err := server.Get(&results, trimLeadingSlash(url)+"/results"); err != nil {
438			return false, errors.New("failed to fetch session results: " + err.Error())
439		}
440
441		if results.Passed {
442			log.Print("Test passed")
443			return true, nil
444		}
445
446		for _, result := range results.Results {
447			if result.Status == "incomplete" {
448				log.Print("Server hasn't finished processing results. Waiting 10 seconds.")
449				time.Sleep(10 * time.Second)
450				continue FetchResults
451			}
452		}
453
454		log.Printf("Server did not accept results: %#v", results)
455		return false, nil
456	}
457}
458
459// vectorSetHeader is the first element in the array of JSON elements that makes
460// up the on-disk format for a vector set.
461type vectorSetHeader struct {
462	URL           string   `json:"url,omitempty"`
463	VectorSetURLs []string `json:"vectorSetUrls,omitempty"`
464	Time          string   `json:"time,omitempty"`
465}
466
467func uploadFromFile(file string, config *Config, sessionTokensCacheDir string) {
468	if len(*jsonInputFile) > 0 {
469		log.Fatalf("-upload cannot be used with -json")
470	}
471	if len(*runFlag) > 0 {
472		log.Fatalf("-upload cannot be used with -run")
473	}
474	if len(*fetchFlag) > 0 {
475		log.Fatalf("-upload cannot be used with -fetch")
476	}
477	if len(*expectedOutFlag) > 0 {
478		log.Fatalf("-upload cannot be used with -expected-out")
479	}
480	if *dumpRegcap {
481		log.Fatalf("-upload cannot be used with -regcap")
482	}
483
484	in, err := os.Open(file)
485	if err != nil {
486		log.Fatalf("Cannot open input: %s", err)
487	}
488	defer in.Close()
489
490	decoder := json.NewDecoder(in)
491
492	var input []json.RawMessage
493	if err := decoder.Decode(&input); err != nil {
494		log.Fatalf("Failed to parse input: %s", err)
495	}
496
497	if len(input) < 2 {
498		log.Fatalf("Input JSON has fewer than two elements")
499	}
500
501	var header vectorSetHeader
502	if err := json.Unmarshal(input[0], &header); err != nil {
503		log.Fatalf("Failed to parse input header: %s", err)
504	}
505
506	if numGroups := len(input) - 1; numGroups != len(header.VectorSetURLs) {
507		log.Fatalf("have %d URLs from header, but only %d result groups", len(header.VectorSetURLs), numGroups)
508	}
509
510	server, err := connect(config, sessionTokensCacheDir)
511	if err != nil {
512		log.Fatal(err)
513	}
514
515	for i, url := range header.VectorSetURLs {
516		log.Printf("Uploading result for %q", url)
517		if err := uploadResult(server, url, input[i+1]); err != nil {
518			log.Fatalf("Failed to upload: %s", err)
519		}
520	}
521
522	if ok, err := getResultsWithRetry(server, header.URL); err != nil {
523		log.Fatal(err)
524	} else if !ok {
525		os.Exit(1)
526	}
527}
528
529func main() {
530	flag.Parse()
531
532	middle, err := subprocess.New(*wrapperPath)
533	if err != nil {
534		log.Fatalf("failed to initialise middle: %s", err)
535	}
536	defer middle.Close()
537
538	configBytes, err := middle.Config()
539	if err != nil {
540		log.Fatalf("failed to get config from middle: %s", err)
541	}
542
543	var supportedAlgos []map[string]any
544	if err := json.Unmarshal(configBytes, &supportedAlgos); err != nil {
545		log.Fatalf("failed to parse configuration from Middle: %s", err)
546	}
547
548	if *dumpRegcap {
549		nonTestAlgos := make([]map[string]any, 0, len(supportedAlgos))
550		for _, algo := range supportedAlgos {
551			if value, ok := algo["acvptoolTestOnly"]; ok {
552				testOnly, ok := value.(bool)
553				if !ok {
554					log.Fatalf("modulewrapper config contains acvptoolTestOnly field with non-boolean value %#v", value)
555				}
556				if testOnly {
557					continue
558				}
559			}
560			if value, ok := algo["algorithm"]; ok {
561				algorithm, ok := value.(string)
562				if ok && algorithm == "acvptool" {
563					continue
564				}
565			}
566			nonTestAlgos = append(nonTestAlgos, algo)
567		}
568
569		regcap := []map[string]any{
570			{"acvVersion": "1.0"},
571			{"algorithms": nonTestAlgos},
572		}
573		regcapBytes, err := json.MarshalIndent(regcap, "", "    ")
574		if err != nil {
575			log.Fatalf("failed to marshal regcap: %s", err)
576		}
577		os.Stdout.Write(regcapBytes)
578		os.Stdout.WriteString("\n")
579		return
580	}
581
582	if len(*jsonInputFile) > 0 {
583		if err := processFile(*jsonInputFile, supportedAlgos, middle); err != nil {
584			log.Fatalf("failed to process input file: %s", err)
585		}
586		return
587	}
588
589	var requestedAlgosFlag string
590	// The output file to which expected results are written, if requested.
591	var expectedOut *os.File
592	// A tee that outputs to both stdout (for vectors) and the file for
593	// expected results, if any.
594	var fetchOutputTee io.Writer
595
596	if len(*runFlag) > 0 && len(*fetchFlag) > 0 {
597		log.Fatalf("cannot specify both -run and -fetch")
598	}
599	if len(*expectedOutFlag) > 0 && len(*fetchFlag) == 0 {
600		log.Fatalf("-expected-out can only be used with -fetch")
601	}
602	if len(*runFlag) > 0 {
603		requestedAlgosFlag = *runFlag
604	} else {
605		requestedAlgosFlag = *fetchFlag
606		if len(*expectedOutFlag) > 0 {
607			if expectedOut, err = os.Create(*expectedOutFlag); err != nil {
608				log.Fatalf("cannot open %q: %s", *expectedOutFlag, err)
609			}
610			fetchOutputTee = io.MultiWriter(os.Stdout, expectedOut)
611			defer expectedOut.Close()
612		} else {
613			fetchOutputTee = os.Stdout
614		}
615	}
616
617	runAlgos := make(map[string]bool)
618	if len(requestedAlgosFlag) > 0 {
619		for _, substr := range strings.Split(requestedAlgosFlag, ",") {
620			runAlgos[substr] = false
621		}
622	}
623
624	var algorithms []map[string]any
625	for _, supportedAlgo := range supportedAlgos {
626		algoInterface, ok := supportedAlgo["algorithm"]
627		if !ok {
628			continue
629		}
630
631		algo, ok := algoInterface.(string)
632		if !ok {
633			continue
634		}
635
636		if _, ok := runAlgos[algo]; ok {
637			algorithms = append(algorithms, supportedAlgo)
638			runAlgos[algo] = true
639		}
640	}
641
642	for algo, recognised := range runAlgos {
643		if !recognised {
644			log.Fatalf("requested algorithm %q was not recognised", algo)
645		}
646	}
647
648	var config Config
649	if err := jsonFromFile(&config, *configFilename); err != nil {
650		log.Fatalf("Failed to load config file: %s", err)
651	}
652
653	var sessionTokensCacheDir string
654	if len(config.SessionTokensCache) > 0 {
655		sessionTokensCacheDir = config.SessionTokensCache
656		if strings.HasPrefix(sessionTokensCacheDir, "~/") {
657			home := os.Getenv("HOME")
658			if len(home) == 0 {
659				log.Fatal("~ used in config file but $HOME not set")
660			}
661			sessionTokensCacheDir = filepath.Join(home, sessionTokensCacheDir[2:])
662		}
663	}
664
665	if len(*uploadInputFile) > 0 {
666		uploadFromFile(*uploadInputFile, &config, sessionTokensCacheDir)
667		return
668	}
669
670	server, err := connect(&config, sessionTokensCacheDir)
671	if err != nil {
672		log.Fatal(err)
673	}
674
675	if err := server.Login(); err != nil {
676		log.Fatalf("failed to login: %s", err)
677	}
678
679	if len(requestedAlgosFlag) == 0 {
680		if interactiveModeSupported {
681			runInteractive(server, config)
682		} else {
683			log.Fatalf("no arguments given but interactive mode not supported")
684		}
685		return
686	}
687
688	requestBytes, err := json.Marshal(acvp.TestSession{
689		IsSample:    true,
690		Publishable: false,
691		Algorithms:  algorithms,
692	})
693	if err != nil {
694		log.Fatalf("Failed to serialise JSON: %s", err)
695	}
696
697	var result acvp.TestSession
698	if err := server.Post(&result, "acvp/v1/testSessions", requestBytes); err != nil {
699		log.Fatalf("Request to create test session failed: %s", err)
700	}
701
702	url := trimLeadingSlash(result.URL)
703	log.Printf("Created test session %q", url)
704	if token := result.AccessToken; len(token) > 0 {
705		server.PrefixTokens[url] = token
706		if len(sessionTokensCacheDir) > 0 {
707			os.WriteFile(filepath.Join(sessionTokensCacheDir, neturl.PathEscape(url))+".token", []byte(token), 0600)
708		}
709	}
710
711	log.Printf("Have vector sets %v", result.VectorSetURLs)
712
713	if len(*fetchFlag) > 0 {
714		io.WriteString(fetchOutputTee, "[\n")
715		json.NewEncoder(fetchOutputTee).Encode(vectorSetHeader{
716			URL:           url,
717			VectorSetURLs: result.VectorSetURLs,
718			Time:          time.Now().Format(time.RFC3339),
719		})
720	}
721
722	for _, setURL := range result.VectorSetURLs {
723		log.Printf("Fetching test vectors %q", setURL)
724
725		vectors, vectorsBytes, err := getVectorsWithRetry(server, trimLeadingSlash(setURL))
726		if err != nil {
727			log.Fatalf("Failed to fetch vector set %q: %s", setURL, err)
728		}
729
730		if len(*fetchFlag) > 0 {
731			os.Stdout.WriteString(",\n")
732			os.Stdout.Write(vectorsBytes)
733		}
734
735		if expectedOut != nil {
736			log.Printf("Fetching expected results")
737
738			_, expectedResultsBytes, err := getVectorsWithRetry(server, trimLeadingSlash(setURL)+"/expected")
739			if err != nil {
740				log.Fatalf("Failed to fetch expected results: %s", err)
741			}
742
743			expectedOut.WriteString(",")
744			expectedOut.Write(expectedResultsBytes)
745		}
746
747		if len(*fetchFlag) > 0 {
748			continue
749		}
750
751		replyGroups, err := middle.Process(vectors.Algo, vectorsBytes)
752		if err != nil {
753			log.Printf("Failed: %s", err)
754			log.Printf("Deleting test set")
755			server.Delete(url)
756			os.Exit(1)
757		}
758
759		headerBytes, err := json.Marshal(acvp.Vectors{
760			ID:   vectors.ID,
761			Algo: vectors.Algo,
762		})
763		if err != nil {
764			log.Printf("Failed to marshal result: %s", err)
765			log.Printf("Deleting test set")
766			server.Delete(url)
767			os.Exit(1)
768		}
769
770		var resultBuf bytes.Buffer
771		resultBuf.Write(headerBytes[:len(headerBytes)-1])
772		resultBuf.WriteString(`,"testGroups":`)
773		replyBytes, err := json.Marshal(replyGroups)
774		if err != nil {
775			log.Printf("Failed to marshal result: %s", err)
776			log.Printf("Deleting test set")
777			server.Delete(url)
778			os.Exit(1)
779		}
780		resultBuf.Write(replyBytes)
781		resultBuf.WriteString("}")
782
783		if err := uploadResult(server, setURL, resultBuf.Bytes()); err != nil {
784			log.Printf("Deleting test set")
785			server.Delete(url)
786			log.Fatal(err)
787		}
788	}
789
790	if len(*fetchFlag) > 0 {
791		io.WriteString(fetchOutputTee, "]\n")
792		return
793	}
794
795	if ok, err := getResultsWithRetry(server, url); err != nil {
796		log.Fatal(err)
797	} else if !ok {
798		os.Exit(1)
799	}
800}
801