| // 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/hmac" |
| "crypto/sha256" |
| "crypto/x509" |
| "encoding/base64" |
| "encoding/binary" |
| "encoding/json" |
| "encoding/pem" |
| "errors" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "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 ( |
| configFilename = flag.String("config", "config.json", "Location of the configuration JSON file") |
| runFlag = flag.String("run", "", "Name of primitive to run tests for") |
| wrapperPath = flag.String("wrapper", "../../../../build/util/fipstools/acvp/modulewrapper/modulewrapper", "Path to the wrapper binary") |
| ) |
| |
| type Config struct { |
| CertPEMFile 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) ([]byte, 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 := ioutil.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 |
| } |
| |
| func main() { |
| flag.Parse() |
| |
| var config Config |
| if err := jsonFromFile(&config, *configFilename); err != nil { |
| log.Fatalf("Failed to load config file: %s", err) |
| } |
| |
| if len(config.TOTPSecret) == 0 { |
| log.Fatal("Config file missing TOTPSecret") |
| } |
| totpSecret, err := base64.StdEncoding.DecodeString(config.TOTPSecret) |
| if err != nil { |
| log.Fatalf("Failed to decode TOTP secret from config file: %s", err) |
| } |
| |
| if len(config.CertPEMFile) == 0 { |
| log.Fatal("Config file missing CertPEMFile") |
| } |
| certPEM, err := ioutil.ReadFile(config.CertPEMFile) |
| if err != nil { |
| log.Fatalf("failed to read certificate from %q: %s", config.CertPEMFile, err) |
| } |
| block, _ := pem.Decode(certPEM) |
| certDER := block.Bytes |
| |
| if len(config.PrivateKeyDERFile) == 0 { |
| log.Fatal("Config file missing PrivateKeyDERFile") |
| } |
| keyDER, err := ioutil.ReadFile(config.PrivateKeyDERFile) |
| if err != nil { |
| log.Fatalf("failed to read private key from %q: %s", config.PrivateKeyDERFile, err) |
| } |
| |
| certKey, err := x509.ParsePKCS1PrivateKey(keyDER) |
| if err != nil { |
| log.Fatalf("failed to parse private key from %q: %s", config.PrivateKeyDERFile, err) |
| } |
| |
| var middle Middle |
| 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) |
| } |
| |
| runAlgos := make(map[string]bool) |
| if len(*runFlag) > 0 { |
| for _, substr := range strings.Split(*runFlag, ",") { |
| 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) |
| } |
| } |
| |
| if len(config.ACVPServer) == 0 { |
| config.ACVPServer = "https://demo.acvts.nist.gov/" |
| } |
| server := acvp.NewServer(config.ACVPServer, config.LogFile, [][]byte{certDER}, certKey, func() string { |
| return TOTP(totpSecret[:]) |
| }) |
| |
| 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:]) |
| } |
| |
| if err := loadCachedSessionTokens(server, sessionTokensCacheDir); err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| if err := server.Login(); err != nil { |
| log.Fatalf("failed to login: %s", err) |
| } |
| |
| if len(*runFlag) == 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 { |
| ioutil.WriteFile(filepath.Join(sessionTokensCacheDir, neturl.PathEscape(url))+".token", []byte(token), 0600) |
| } |
| } |
| |
| log.Printf("Have vector sets %v", result.VectorSetURLs) |
| |
| for _, setURL := range result.VectorSetURLs { |
| firstTime := true |
| for { |
| if firstTime { |
| log.Printf("Fetching test vectors %q", setURL) |
| firstTime = false |
| } |
| |
| vectorsBytes, err := server.GetBytes(trimLeadingSlash(setURL)) |
| if err != nil { |
| log.Fatalf("Failed to fetch vector set %q: %s", setURL, err) |
| } |
| |
| var vectors acvp.Vectors |
| if err := json.Unmarshal(vectorsBytes, &vectors); err != nil { |
| log.Fatalf("Failed to parse vector set from %q: %s", setURL, err) |
| } |
| |
| if retry := vectors.Retry; retry > 0 { |
| log.Printf("Server requested %d seconds delay", retry) |
| if retry > 10 { |
| retry = 10 |
| } |
| time.Sleep(time.Duration(retry) * time.Second) |
| 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":`) |
| resultBuf.Write(replyGroups) |
| resultBuf.WriteString("}") |
| |
| resultData := resultBuf.Bytes() |
| resultSize := uint64(len(resultData)) + 32 /* for framing overhead */ |
| if resultSize >= server.SizeLimit { |
| 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 { |
| log.Printf("Failed to marshal large-upload request: %s", err) |
| log.Printf("Deleting test set") |
| server.Delete(url) |
| os.Exit(1) |
| } |
| |
| var largeResponse acvp.LargeUploadResponse |
| if err := server.Post(&largeResponse, "/large", largeRequestBytes); err != nil { |
| log.Fatalf("Failed to request large-upload endpoint: %s", err) |
| } |
| |
| log.Printf("Directed to large-upload endpoint at %q", largeResponse.URL) |
| client := &http.Client{} |
| req, err := http.NewRequest("POST", largeResponse.URL, bytes.NewBuffer(resultData)) |
| if err != nil { |
| log.Fatalf("Failed to create POST request: %s", err) |
| } |
| token := largeResponse.AccessToken |
| if len(token) == 0 { |
| token = server.AccessToken |
| } |
| req.Header.Add("Authorization", "Bearer "+token) |
| req.Header.Add("Content-Type", "application/json") |
| resp, err := client.Do(req) |
| if err != nil { |
| log.Fatalf("Failed writing large upload: %s", err) |
| } |
| resp.Body.Close() |
| if resp.StatusCode != 200 { |
| log.Fatalf("Large upload resulted in status code %d", resp.StatusCode) |
| } |
| } else { |
| log.Printf("Result size %d bytes", resultSize) |
| if err := server.Post(nil, trimLeadingSlash(setURL)+"/results", resultData); err != nil { |
| log.Fatalf("Failed to upload results: %s\n", err) |
| } |
| } |
| |
| break |
| } |
| } |
| |
| FetchResults: |
| for { |
| var results acvp.SessionResults |
| if err := server.Get(&results, trimLeadingSlash(url)+"/results"); err != nil { |
| log.Fatalf("Failed to fetch session results: %s", err) |
| } |
| |
| if results.Passed { |
| log.Print("Test passed") |
| break |
| } |
| |
| 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.Fatalf("Server did not accept results: %#v", results) |
| } |
| } |