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