1package main 2 3import ( 4 "bufio" 5 "bytes" 6 "crypto/hmac" 7 "crypto/sha256" 8 "crypto/x509" 9 "encoding/base64" 10 "encoding/binary" 11 "encoding/json" 12 "encoding/pem" 13 "errors" 14 "flag" 15 "fmt" 16 "io/ioutil" 17 "log" 18 "net/http" 19 neturl "net/url" 20 "os" 21 "path/filepath" 22 "strings" 23 "time" 24 25 "boringssl.googlesource.com/boringssl/util/fipstools/acvp/acvptool/acvp" 26 "boringssl.googlesource.com/boringssl/util/fipstools/acvp/acvptool/subprocess" 27) 28 29var ( 30 configFilename = flag.String("config", "config.json", "Location of the configuration JSON file") 31 runFlag = flag.String("run", "", "Name of primitive to run tests for") 32 wrapperPath = flag.String("wrapper", "../../../../build/util/fipstools/acvp/modulewrapper/modulewrapper", "Path to the wrapper binary") 33) 34 35type Config struct { 36 CertPEMFile string 37 PrivateKeyDERFile string 38 TOTPSecret string 39 ACVPServer string 40 SessionTokensCache string 41 LogFile string 42} 43 44func isCommentLine(line []byte) bool { 45 var foundCommentStart bool 46 for _, b := range line { 47 if !foundCommentStart { 48 if b == ' ' || b == '\t' { 49 continue 50 } 51 if b != '/' { 52 return false 53 } 54 foundCommentStart = true 55 } else { 56 return b == '/' 57 } 58 } 59 return false 60} 61 62func jsonFromFile(out interface{}, filename string) error { 63 in, err := os.Open(filename) 64 if err != nil { 65 return err 66 } 67 defer in.Close() 68 69 scanner := bufio.NewScanner(in) 70 var commentsRemoved bytes.Buffer 71 for scanner.Scan() { 72 if isCommentLine(scanner.Bytes()) { 73 continue 74 } 75 commentsRemoved.Write(scanner.Bytes()) 76 commentsRemoved.WriteString("\n") 77 } 78 if err := scanner.Err(); err != nil { 79 return err 80 } 81 82 decoder := json.NewDecoder(&commentsRemoved) 83 decoder.DisallowUnknownFields() 84 if err := decoder.Decode(out); err != nil { 85 return err 86 } 87 if decoder.More() { 88 return errors.New("trailing garbage found") 89 } 90 return nil 91} 92 93// TOTP implements the time-based one-time password algorithm with the suggested 94// granularity of 30 seconds. See https://tools.ietf.org/html/rfc6238 and then 95// https://tools.ietf.org/html/rfc4226#section-5.3 96func TOTP(secret []byte) string { 97 const timeStep = 30 98 now := uint64(time.Now().Unix()) / 30 99 var nowBuf [8]byte 100 binary.BigEndian.PutUint64(nowBuf[:], now) 101 mac := hmac.New(sha256.New, secret) 102 mac.Write(nowBuf[:]) 103 digest := mac.Sum(nil) 104 value := binary.BigEndian.Uint32(digest[digest[31]&15:]) 105 value &= 0x7fffffff 106 value %= 100000000 107 return fmt.Sprintf("%08d", value) 108} 109 110type Middle interface { 111 Close() 112 Config() ([]byte, error) 113 Process(algorithm string, vectorSet []byte) ([]byte, error) 114} 115 116func loadCachedSessionTokens(server *acvp.Server, cachePath string) error { 117 cacheDir, err := os.Open(cachePath) 118 if err != nil { 119 if os.IsNotExist(err) { 120 if err := os.Mkdir(cachePath, 0700); err != nil { 121 return fmt.Errorf("Failed to create session token cache directory %q: %s", cachePath, err) 122 } 123 return nil 124 } 125 return fmt.Errorf("Failed to open session token cache directory %q: %s", cachePath, err) 126 } 127 defer cacheDir.Close() 128 names, err := cacheDir.Readdirnames(0) 129 if err != nil { 130 return fmt.Errorf("Failed to list session token cache directory %q: %s", cachePath, err) 131 } 132 133 loaded := 0 134 for _, name := range names { 135 if !strings.HasSuffix(name, ".token") { 136 continue 137 } 138 path := filepath.Join(cachePath, name) 139 contents, err := ioutil.ReadFile(path) 140 if err != nil { 141 return fmt.Errorf("Failed to read session token cache entry %q: %s", path, err) 142 } 143 urlPath, err := neturl.PathUnescape(name[:len(name)-6]) 144 if err != nil { 145 return fmt.Errorf("Failed to unescape token filename %q: %s", name, err) 146 } 147 server.PrefixTokens[urlPath] = string(contents) 148 loaded++ 149 } 150 151 log.Printf("Loaded %d cached tokens", loaded) 152 return nil 153} 154 155func trimLeadingSlash(s string) string { 156 if strings.HasPrefix(s, "/") { 157 return s[1:] 158 } 159 return s 160} 161 162func main() { 163 flag.Parse() 164 165 var config Config 166 if err := jsonFromFile(&config, *configFilename); err != nil { 167 log.Fatalf("Failed to load config file: %s", err) 168 } 169 170 if len(config.TOTPSecret) == 0 { 171 log.Fatal("Config file missing TOTPSecret") 172 } 173 totpSecret, err := base64.StdEncoding.DecodeString(config.TOTPSecret) 174 if err != nil { 175 log.Fatalf("Failed to decode TOTP secret from config file: %s", err) 176 } 177 178 if len(config.CertPEMFile) == 0 { 179 log.Fatal("Config file missing CertPEMFile") 180 } 181 certPEM, err := ioutil.ReadFile(config.CertPEMFile) 182 if err != nil { 183 log.Fatalf("failed to read certificate from %q: %s", config.CertPEMFile, err) 184 } 185 block, _ := pem.Decode(certPEM) 186 certDER := block.Bytes 187 188 if len(config.PrivateKeyDERFile) == 0 { 189 log.Fatal("Config file missing PrivateKeyDERFile") 190 } 191 keyDER, err := ioutil.ReadFile(config.PrivateKeyDERFile) 192 if err != nil { 193 log.Fatalf("failed to read private key from %q: %s", config.PrivateKeyDERFile, err) 194 } 195 196 certKey, err := x509.ParsePKCS1PrivateKey(keyDER) 197 if err != nil { 198 log.Fatalf("failed to parse private key from %q: %s", config.PrivateKeyDERFile, err) 199 } 200 201 var middle Middle 202 middle, err = subprocess.New(*wrapperPath) 203 if err != nil { 204 log.Fatalf("failed to initialise middle: %s", err) 205 } 206 defer middle.Close() 207 208 configBytes, err := middle.Config() 209 if err != nil { 210 log.Fatalf("failed to get config from middle: %s", err) 211 } 212 213 var supportedAlgos []map[string]interface{} 214 if err := json.Unmarshal(configBytes, &supportedAlgos); err != nil { 215 log.Fatalf("failed to parse configuration from Middle: %s", err) 216 } 217 218 runAlgos := make(map[string]bool) 219 if len(*runFlag) > 0 { 220 for _, substr := range strings.Split(*runFlag, ",") { 221 runAlgos[substr] = false 222 } 223 } 224 225 var algorithms []map[string]interface{} 226 for _, supportedAlgo := range supportedAlgos { 227 algoInterface, ok := supportedAlgo["algorithm"] 228 if !ok { 229 continue 230 } 231 232 algo, ok := algoInterface.(string) 233 if !ok { 234 continue 235 } 236 237 if _, ok := runAlgos[algo]; ok { 238 algorithms = append(algorithms, supportedAlgo) 239 runAlgos[algo] = true 240 } 241 } 242 243 for algo, recognised := range runAlgos { 244 if !recognised { 245 log.Fatalf("requested algorithm %q was not recognised", algo) 246 } 247 } 248 249 if len(config.ACVPServer) == 0 { 250 config.ACVPServer = "https://demo.acvts.nist.gov/" 251 } 252 server := acvp.NewServer(config.ACVPServer, config.LogFile, [][]byte{certDER}, certKey, func() string { 253 return TOTP(totpSecret[:]) 254 }) 255 256 var sessionTokensCacheDir string 257 if len(config.SessionTokensCache) > 0 { 258 sessionTokensCacheDir = config.SessionTokensCache 259 if strings.HasPrefix(sessionTokensCacheDir, "~/") { 260 home := os.Getenv("HOME") 261 if len(home) == 0 { 262 log.Fatal("~ used in config file but $HOME not set") 263 } 264 sessionTokensCacheDir = filepath.Join(home, sessionTokensCacheDir[2:]) 265 } 266 267 if err := loadCachedSessionTokens(server, sessionTokensCacheDir); err != nil { 268 log.Fatal(err) 269 } 270 } 271 272 if err := server.Login(); err != nil { 273 log.Fatalf("failed to login: %s", err) 274 } 275 276 if len(*runFlag) == 0 { 277 runInteractive(server, config) 278 return 279 } 280 281 requestBytes, err := json.Marshal(acvp.TestSession{ 282 IsSample: true, 283 Publishable: false, 284 Algorithms: algorithms, 285 }) 286 if err != nil { 287 log.Fatalf("Failed to serialise JSON: %s", err) 288 } 289 290 var result acvp.TestSession 291 if err := server.Post(&result, "acvp/v1/testSessions", requestBytes); err != nil { 292 log.Fatalf("Request to create test session failed: %s", err) 293 } 294 295 url := trimLeadingSlash(result.URL) 296 log.Printf("Created test session %q", url) 297 if token := result.AccessToken; len(token) > 0 { 298 server.PrefixTokens[url] = token 299 if len(sessionTokensCacheDir) > 0 { 300 ioutil.WriteFile(filepath.Join(sessionTokensCacheDir, neturl.PathEscape(url))+".token", []byte(token), 0600) 301 } 302 } 303 304 log.Printf("Have vector sets %v", result.VectorSetURLs) 305 306 for _, setURL := range result.VectorSetURLs { 307 firstTime := true 308 for { 309 if firstTime { 310 log.Printf("Fetching test vectors %q", setURL) 311 firstTime = false 312 } 313 314 vectorsBytes, err := server.GetBytes(trimLeadingSlash(setURL)) 315 if err != nil { 316 log.Fatalf("Failed to fetch vector set %q: %s", setURL, err) 317 } 318 319 var vectors acvp.Vectors 320 if err := json.Unmarshal(vectorsBytes, &vectors); err != nil { 321 log.Fatalf("Failed to parse vector set from %q: %s", setURL, err) 322 } 323 324 if retry := vectors.Retry; retry > 0 { 325 log.Printf("Server requested %d seconds delay", retry) 326 if retry > 10 { 327 retry = 10 328 } 329 time.Sleep(time.Duration(retry) * time.Second) 330 continue 331 } 332 333 replyGroups, err := middle.Process(vectors.Algo, vectorsBytes) 334 if err != nil { 335 log.Printf("Failed: %s", err) 336 log.Printf("Deleting test set") 337 server.Delete(url) 338 os.Exit(1) 339 } 340 341 headerBytes, err := json.Marshal(acvp.Vectors{ 342 ID: vectors.ID, 343 Algo: vectors.Algo, 344 }) 345 if err != nil { 346 log.Printf("Failed to marshal result: %s", err) 347 log.Printf("Deleting test set") 348 server.Delete(url) 349 os.Exit(1) 350 } 351 352 var resultBuf bytes.Buffer 353 resultBuf.Write(headerBytes[:len(headerBytes)-1]) 354 resultBuf.WriteString(`,"testGroups":`) 355 resultBuf.Write(replyGroups) 356 resultBuf.WriteString("}") 357 358 resultData := resultBuf.Bytes() 359 resultSize := uint64(len(resultData)) + 32 /* for framing overhead */ 360 if resultSize >= server.SizeLimit { 361 log.Printf("Result is %d bytes, too much given server limit of %d bytes. Using large-upload process.", resultSize, server.SizeLimit) 362 largeRequestBytes, err := json.Marshal(acvp.LargeUploadRequest{ 363 Size: resultSize, 364 URL: setURL, 365 }) 366 if err != nil { 367 log.Printf("Failed to marshal large-upload request: %s", err) 368 log.Printf("Deleting test set") 369 server.Delete(url) 370 os.Exit(1) 371 } 372 373 var largeResponse acvp.LargeUploadResponse 374 if err := server.Post(&largeResponse, "/large", largeRequestBytes); err != nil { 375 log.Fatalf("Failed to request large-upload endpoint: %s", err) 376 } 377 378 log.Printf("Directed to large-upload endpoint at %q", largeResponse.URL) 379 client := &http.Client{} 380 req, err := http.NewRequest("POST", largeResponse.URL, bytes.NewBuffer(resultData)) 381 if err != nil { 382 log.Fatalf("Failed to create POST request: %s", err) 383 } 384 token := largeResponse.AccessToken 385 if len(token) == 0 { 386 token = server.AccessToken 387 } 388 req.Header.Add("Authorization", "Bearer "+token) 389 req.Header.Add("Content-Type", "application/json") 390 resp, err := client.Do(req) 391 if err != nil { 392 log.Fatalf("Failed writing large upload: %s", err) 393 } 394 resp.Body.Close() 395 if resp.StatusCode != 200 { 396 log.Fatalf("Large upload resulted in status code %d", resp.StatusCode) 397 } 398 } else { 399 log.Printf("Result size %d bytes", resultSize) 400 if err := server.Post(nil, trimLeadingSlash(setURL)+"/results", resultData); err != nil { 401 log.Fatalf("Failed to upload results: %s\n", err) 402 } 403 } 404 405 break 406 } 407 } 408 409FetchResults: 410 for { 411 var results acvp.SessionResults 412 if err := server.Get(&results, trimLeadingSlash(url)+"/results"); err != nil { 413 log.Fatalf("Failed to fetch session results: %s", err) 414 } 415 416 if results.Passed { 417 break 418 } 419 420 for _, result := range results.Results { 421 if result.Status == "incomplete" { 422 log.Print("Server hasn't finished processing results. Waiting 10 seconds.") 423 time.Sleep(10 * time.Second) 424 continue FetchResults 425 } 426 } 427 428 log.Fatalf("Server did not accept results: %#v", results) 429 } 430} 431