• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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