summaryrefslogtreecommitdiff
path: root/src/util/fipstools/acvp/acvptool/acvp.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/fipstools/acvp/acvptool/acvp.go')
-rw-r--r--src/util/fipstools/acvp/acvptool/acvp.go430
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)
+ }
+}