blob: ed1a84f0a1794d106ccef4f90b12239d33df7bf1 [file] [log] [blame]
Pete Bentleya5c947b2019-08-09 14:24:27 +00001package 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}