diff options
Diffstat (limited to 'src/util/fipstools/acvp/acvptool/acvp.go')
-rw-r--r-- | src/util/fipstools/acvp/acvptool/acvp.go | 430 |
1 files changed, 430 insertions, 0 deletions
diff --git a/src/util/fipstools/acvp/acvptool/acvp.go b/src/util/fipstools/acvp/acvptool/acvp.go new file mode 100644 index 00000000..ed1a84f0 --- /dev/null +++ b/src/util/fipstools/acvp/acvptool/acvp.go @@ -0,0 +1,430 @@ +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 { + runInteractive(server, config) + 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 { + 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) + } +} |