Mirror of BoringSSL (grpc依赖)
https://boringssl.googlesource.com/boringssl
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
655 lines
18 KiB
655 lines
18 KiB
// 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" |
|
"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 ( |
|
dumpRegcap = flag.Bool("regcap", false, "Print module capabilities JSON to stdout") |
|
configFilename = flag.String("config", "config.json", "Location of the configuration JSON file") |
|
jsonInputFile = flag.String("json", "", "Location of a vector-set input file") |
|
runFlag = flag.String("run", "", "Name of primitive to run tests for") |
|
fetchFlag = flag.String("fetch", "", "Name of primitive to fetch vectors for") |
|
wrapperPath = flag.String("wrapper", "../../../../build/util/fipstools/acvp/modulewrapper/modulewrapper", "Path to the wrapper binary") |
|
) |
|
|
|
type Config struct { |
|
CertPEMFile string |
|
PrivateKeyFile 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) (interface{}, 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 |
|
} |
|
|
|
// looksLikeHeaderElement returns true iff element looks like it's a header, not |
|
// a test. Some ACVP files contain a header as the first element that should be |
|
// duplicated into the response, and some don't. If the element contains |
|
// a "url" field, or if it's missing an "algorithm" field, then we guess that |
|
// it's a header. |
|
func looksLikeHeaderElement(element json.RawMessage) bool { |
|
var headerFields struct { |
|
URL string `json:"url"` |
|
Algorithm string `json:"algorithm"` |
|
} |
|
if err := json.Unmarshal(element, &headerFields); err != nil { |
|
return false |
|
} |
|
return len(headerFields.URL) > 0 || len(headerFields.Algorithm) == 0 |
|
} |
|
|
|
// processFile reads a file containing vector sets, at least in the format |
|
// preferred by our lab, and writes the results to stdout. |
|
func processFile(filename string, supportedAlgos []map[string]interface{}, middle Middle) error { |
|
jsonBytes, err := ioutil.ReadFile(filename) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
var elements []json.RawMessage |
|
if err := json.Unmarshal(jsonBytes, &elements); err != nil { |
|
return err |
|
} |
|
|
|
// There must be at least one element in the file. |
|
if len(elements) < 1 { |
|
return errors.New("JSON input is empty") |
|
} |
|
|
|
var header json.RawMessage |
|
if looksLikeHeaderElement(elements[0]) { |
|
header, elements = elements[0], elements[1:] |
|
if len(elements) == 0 { |
|
return errors.New("JSON input is empty") |
|
} |
|
} |
|
|
|
// Build a map of which algorithms our Middle supports. |
|
algos := make(map[string]struct{}) |
|
for _, supportedAlgo := range supportedAlgos { |
|
algoInterface, ok := supportedAlgo["algorithm"] |
|
if !ok { |
|
continue |
|
} |
|
algo, ok := algoInterface.(string) |
|
if !ok { |
|
continue |
|
} |
|
algos[algo] = struct{}{} |
|
} |
|
|
|
var result bytes.Buffer |
|
result.WriteString("[") |
|
|
|
if header != nil { |
|
headerBytes, err := json.MarshalIndent(header, "", " ") |
|
if err != nil { |
|
return err |
|
} |
|
result.Write(headerBytes) |
|
result.WriteString(",") |
|
} |
|
|
|
for i, element := range elements { |
|
var commonFields struct { |
|
Algo string `json:"algorithm"` |
|
ID uint64 `json:"vsId"` |
|
} |
|
if err := json.Unmarshal(element, &commonFields); err != nil { |
|
return fmt.Errorf("failed to extract common fields from vector set #%d", i+1) |
|
} |
|
|
|
algo := commonFields.Algo |
|
if _, ok := algos[algo]; !ok { |
|
return fmt.Errorf("vector set #%d contains unsupported algorithm %q", i+1, algo) |
|
} |
|
|
|
replyGroups, err := middle.Process(algo, element) |
|
if err != nil { |
|
return fmt.Errorf("while processing vector set #%d: %s", i+1, err) |
|
} |
|
|
|
group := map[string]interface{}{ |
|
"vsId": commonFields.ID, |
|
"testGroups": replyGroups, |
|
} |
|
replyBytes, err := json.MarshalIndent(group, "", " ") |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if i != 0 { |
|
result.WriteString(",") |
|
} |
|
result.Write(replyBytes) |
|
} |
|
|
|
result.WriteString("]\n") |
|
os.Stdout.Write(result.Bytes()) |
|
|
|
return nil |
|
} |
|
|
|
func main() { |
|
flag.Parse() |
|
|
|
var err error |
|
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) |
|
} |
|
|
|
if *dumpRegcap { |
|
nonTestAlgos := make([]map[string]interface{}, 0, len(supportedAlgos)) |
|
for _, algo := range supportedAlgos { |
|
if value, ok := algo["acvptoolTestOnly"]; ok { |
|
testOnly, ok := value.(bool) |
|
if !ok { |
|
log.Fatalf("modulewrapper config contains acvptoolTestOnly field with non-boolean value %#v", value) |
|
} |
|
if testOnly { |
|
continue |
|
} |
|
} |
|
nonTestAlgos = append(nonTestAlgos, algo) |
|
} |
|
|
|
regcap := []map[string]interface{}{ |
|
map[string]interface{}{"acvVersion": "1.0"}, |
|
map[string]interface{}{"algorithms": nonTestAlgos}, |
|
} |
|
regcapBytes, err := json.MarshalIndent(regcap, "", " ") |
|
if err != nil { |
|
log.Fatalf("failed to marshal regcap: %s", err) |
|
} |
|
os.Stdout.Write(regcapBytes) |
|
os.Stdout.WriteString("\n") |
|
os.Exit(0) |
|
} |
|
|
|
if len(*jsonInputFile) > 0 { |
|
if err := processFile(*jsonInputFile, supportedAlgos, middle); err != nil { |
|
log.Fatalf("failed to process input file: %s", err) |
|
} |
|
os.Exit(0) |
|
} |
|
|
|
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 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) |
|
} |
|
|
|
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 && len(config.PrivateKeyFile) == 0 { |
|
log.Fatal("Config file missing PrivateKeyDERFile and PrivateKeyFile") |
|
} |
|
if len(config.PrivateKeyDERFile) != 0 && len(config.PrivateKeyFile) != 0 { |
|
log.Fatal("Config file has both PrivateKeyDERFile and PrivateKeyFile. Can only have one.") |
|
} |
|
privateKeyFile := config.PrivateKeyDERFile |
|
if len(config.PrivateKeyFile) > 0 { |
|
privateKeyFile = config.PrivateKeyFile |
|
} |
|
|
|
keyBytes, err := ioutil.ReadFile(privateKeyFile) |
|
if err != nil { |
|
log.Fatalf("failed to read private key from %q: %s", privateKeyFile, err) |
|
} |
|
|
|
var keyDER []byte |
|
pemBlock, _ := pem.Decode(keyBytes) |
|
if pemBlock != nil { |
|
keyDER = pemBlock.Bytes |
|
} else { |
|
keyDER = keyBytes |
|
} |
|
|
|
var certKey crypto.PrivateKey |
|
if certKey, err = x509.ParsePKCS1PrivateKey(keyDER); err != nil { |
|
if certKey, err = x509.ParsePKCS8PrivateKey(keyDER); err != nil { |
|
log.Fatalf("failed to parse private key from %q: %s", privateKeyFile, err) |
|
} |
|
} |
|
|
|
var requestedAlgosFlag string |
|
if len(*runFlag) > 0 && len(*fetchFlag) > 0 { |
|
log.Fatalf("cannot specify both -run and -fetch") |
|
} |
|
if len(*runFlag) > 0 { |
|
requestedAlgosFlag = *runFlag |
|
} else { |
|
requestedAlgosFlag = *fetchFlag |
|
} |
|
|
|
runAlgos := make(map[string]bool) |
|
if len(requestedAlgosFlag) > 0 { |
|
for _, substr := range strings.Split(requestedAlgosFlag, ",") { |
|
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(requestedAlgosFlag) == 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) |
|
|
|
if len(*fetchFlag) > 0 { |
|
os.Stdout.WriteString("[\n") |
|
json.NewEncoder(os.Stdout).Encode(map[string]interface{}{ |
|
"url": url, |
|
"vectorSetUrls": result.VectorSetURLs, |
|
"time": time.Now().Format(time.RFC3339), |
|
}) |
|
} |
|
|
|
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 |
|
} |
|
|
|
if len(*fetchFlag) > 0 { |
|
os.Stdout.WriteString(",\n") |
|
os.Stdout.Write(vectorsBytes) |
|
break |
|
} |
|
|
|
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":`) |
|
replyBytes, err := json.Marshal(replyGroups) |
|
if err != nil { |
|
log.Printf("Failed to marshal result: %s", err) |
|
log.Printf("Deleting test set") |
|
server.Delete(url) |
|
os.Exit(1) |
|
} |
|
resultBuf.Write(replyBytes) |
|
resultBuf.WriteString("}") |
|
|
|
resultData := resultBuf.Bytes() |
|
resultSize := uint64(len(resultData)) + 32 /* for framing overhead */ |
|
if server.SizeLimit > 0 && resultSize >= server.SizeLimit { |
|
// The NIST ACVP server no longer requires the large-upload process, |
|
// suggesting that it may no longer be needed. |
|
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 |
|
} |
|
} |
|
|
|
if len(*fetchFlag) > 0 { |
|
os.Stdout.WriteString("]\n") |
|
os.Exit(0) |
|
} |
|
|
|
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) |
|
} |
|
}
|
|
|