431 lines
11 KiB
Go
431 lines
11 KiB
Go
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)
|
|
}
|
|
}
|