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