• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1package acvp
2
3import (
4	"bytes"
5	"crypto"
6	"crypto/tls"
7	"encoding/base64"
8	"encoding/json"
9	"errors"
10	"fmt"
11	"io"
12	"io/ioutil"
13	"net"
14	"net/http"
15	"net/url"
16	"os"
17	"reflect"
18	"strings"
19	"time"
20)
21
22// Server represents an ACVP server.
23type Server struct {
24	// PrefixTokens are access tokens that apply to URLs under a certain prefix.
25	// The keys of this map are strings like "acvp/v1/testSessions/1234" and the
26	// values are JWT access tokens.
27	PrefixTokens map[string]string
28	// SizeLimit is the maximum number of bytes that the server can accept as an
29	// upload before the large endpoint support must be used.
30	SizeLimit uint64
31	// AccessToken is the top-level access token for the current session.
32	AccessToken string
33
34	client      *http.Client
35	prefix      string
36	totpFunc    func() string
37}
38
39// NewServer returns a fresh Server instance representing the ACVP server at
40// prefix (e.g. "https://acvp.example.com/"). A copy of all bytes exchanged
41// will be written to logFile, if not empty.
42func NewServer(prefix string, logFile string, derCertificates [][]byte, privateKey crypto.PrivateKey, totp func() string) *Server {
43	if !strings.HasSuffix(prefix, "/") {
44		prefix = prefix + "/"
45	}
46
47	tlsConfig := &tls.Config{
48		Certificates: []tls.Certificate{
49			tls.Certificate{
50				Certificate: derCertificates,
51				PrivateKey:  privateKey,
52			},
53		},
54		Renegotiation: tls.RenegotiateOnceAsClient,
55	}
56
57	client := &http.Client{
58		Transport: &http.Transport{
59			Dial: func(network, addr string) (net.Conn, error) {
60				panic("HTTP connection requested")
61			},
62			DialTLS: func(network, addr string) (net.Conn, error) {
63				conn, err := tls.Dial(network, addr, tlsConfig)
64				if err != nil {
65					return nil, err
66				}
67				if len(logFile) > 0 {
68					logFile, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
69					if err != nil {
70						return nil, err
71					}
72					return &logger{Conn: conn, log: logFile}, nil
73				}
74				return conn, err
75			},
76		},
77		Timeout: 10 * time.Second,
78	}
79
80	return &Server{client: client, prefix: prefix, totpFunc: totp, PrefixTokens: make(map[string]string)}
81}
82
83type logger struct {
84	*tls.Conn
85	log           *os.File
86	lastDirection int
87}
88
89var newLine = []byte{'\n'}
90
91func (l *logger) Read(buf []byte) (int, error) {
92	if l.lastDirection != 1 {
93		l.log.Write(newLine)
94	}
95	l.lastDirection = 1
96
97	n, err := l.Conn.Read(buf)
98	if err == nil {
99		l.log.Write(buf[:n])
100	}
101	return n, err
102}
103
104func (l *logger) Write(buf []byte) (int, error) {
105	if l.lastDirection != 2 {
106		l.log.Write(newLine)
107	}
108	l.lastDirection = 2
109
110	n, err := l.Conn.Write(buf)
111	if err == nil {
112		l.log.Write(buf[:n])
113	}
114	return n, err
115}
116
117const requestPrefix = `[{"acvVersion":"1.0"},`
118const requestSuffix = "]"
119
120// parseHeaderElement parses the first JSON object that's always returned by
121// ACVP servers. If successful, it returns a JSON Decoder positioned just
122// before the second element.
123func parseHeaderElement(in io.Reader) (*json.Decoder, error) {
124	decoder := json.NewDecoder(in)
125	arrayStart, err := decoder.Token()
126	if err != nil {
127		return nil, errors.New("failed to read from server reply: " + err.Error())
128	}
129	if delim, ok := arrayStart.(json.Delim); !ok || delim != '[' {
130		return nil, fmt.Errorf("found %#v when expecting initial array from server", arrayStart)
131	}
132
133	var version struct {
134		Version string `json:"acvVersion"`
135	}
136	if err := decoder.Decode(&version); err != nil {
137		return nil, errors.New("parse error while decoding version element: " + err.Error())
138	}
139	if !strings.HasPrefix(version.Version, "1.") {
140		return nil, fmt.Errorf("expected version 1.* from server but found %q", version.Version)
141	}
142
143	return decoder, nil
144}
145
146// parseReplyToBytes reads the contents of an ACVP reply after removing the
147// header element.
148func parseReplyToBytes(in io.Reader) ([]byte, error) {
149	decoder, err := parseHeaderElement(in)
150	if err != nil {
151		return nil, err
152	}
153
154	buf, err := ioutil.ReadAll(decoder.Buffered())
155	if err != nil {
156		return nil, err
157	}
158
159	rest, err := ioutil.ReadAll(in)
160	if err != nil {
161		return nil, err
162	}
163	buf = append(buf, rest...)
164
165	buf = bytes.TrimSpace(buf)
166	if len(buf) == 0 || buf[0] != ',' {
167		return nil, errors.New("didn't find initial ','")
168	}
169	buf = buf[1:]
170
171	if len(buf) == 0 || buf[len(buf)-1] != ']' {
172		return nil, errors.New("didn't find trailing ']'")
173	}
174	buf = buf[:len(buf)-1]
175
176	return buf, nil
177}
178
179// parseReply parses the contents of an ACVP reply (after removing the header
180// element) into out. See the documentation of the encoding/json package for
181// details of the parsing.
182func parseReply(out interface{}, in io.Reader) error {
183	if out == nil {
184		// No reply expected.
185		return nil
186	}
187
188	decoder, err := parseHeaderElement(in)
189	if err != nil {
190		return err
191	}
192
193	if err := decoder.Decode(out); err != nil {
194		return errors.New("error while decoding reply body: " + err.Error())
195	}
196
197	arrayEnd, err := decoder.Token()
198	if err != nil {
199		return errors.New("failed to read end of reply from server: " + err.Error())
200	}
201	if delim, ok := arrayEnd.(json.Delim); !ok || delim != ']' {
202		return fmt.Errorf("found %#v when expecting end of array from server", arrayEnd)
203	}
204	if decoder.More() {
205		return errors.New("unexpected trailing data from server")
206	}
207
208	return nil
209}
210
211// expired returns true if the given JWT token has expired.
212func expired(tokenStr string) bool {
213	parts := strings.Split(tokenStr, ".")
214	if len(parts) != 3 {
215		return false
216	}
217	jsonBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
218	if err != nil {
219		return false
220	}
221	var token struct {
222		Expiry uint64 `json:"exp"`
223	}
224	if json.Unmarshal(jsonBytes, &token) != nil {
225		return false
226	}
227	return token.Expiry > 0 && token.Expiry < uint64(time.Now().Unix())
228}
229
230func (server *Server) getToken(endPoint string) (string, error) {
231	for path, token := range server.PrefixTokens {
232		if endPoint != path && !strings.HasPrefix(endPoint, path+"/") {
233			continue
234		}
235
236		if !expired(token) {
237			return token, nil
238		}
239
240		var reply struct {
241			AccessToken string `json:"accessToken"`
242		}
243		if err := server.postMessage(&reply, "acvp/v1/login", map[string]string{
244			"password":    server.totpFunc(),
245			"accessToken": token,
246		}); err != nil {
247			return "", err
248		}
249		server.PrefixTokens[path] = reply.AccessToken
250		return reply.AccessToken, nil
251	}
252	return server.AccessToken, nil
253}
254
255// Login sends a login request and stores the returned access tokens for use
256// with future requests. The login process isn't specifically documented in
257// draft-fussell-acvp-spec and the best reference is
258// https://github.com/usnistgov/ACVP/wiki#credentials-for-accessing-the-demo-server
259func (server *Server) Login() error {
260	var reply struct {
261		AccessToken           string `json:"accessToken"`
262		LargeEndpointRequired bool   `json:"largeEndpointRequired"`
263		SizeLimit             uint64 `json:"sizeConstraint"`
264	}
265
266	if err := server.postMessage(&reply, "acvp/v1/login", map[string]string{"password": server.totpFunc()}); err != nil {
267		return err
268	}
269
270	if len(reply.AccessToken) == 0 {
271		return errors.New("login reply didn't contain access token")
272	}
273	server.AccessToken = reply.AccessToken
274
275	if reply.LargeEndpointRequired {
276		if reply.SizeLimit == 0 {
277			return errors.New("login indicated largeEndpointRequired but didn't provide a sizeConstraint")
278		}
279		server.SizeLimit = reply.SizeLimit
280	}
281
282	return nil
283}
284
285type Relation int
286
287const (
288	Equals           Relation = iota
289	NotEquals        Relation = iota
290	GreaterThan      Relation = iota
291	GreaterThanEqual Relation = iota
292	LessThan         Relation = iota
293	LessThanEqual    Relation = iota
294	Contains         Relation = iota
295	StartsWith       Relation = iota
296	EndsWith         Relation = iota
297)
298
299func (rel Relation) String() string {
300	switch rel {
301	case Equals:
302		return "eq"
303	case NotEquals:
304		return "ne"
305	case GreaterThan:
306		return "gt"
307	case GreaterThanEqual:
308		return "ge"
309	case LessThan:
310		return "lt"
311	case LessThanEqual:
312		return "le"
313	case Contains:
314		return "contains"
315	case StartsWith:
316		return "start"
317	case EndsWith:
318		return "end"
319	default:
320		panic("unknown relation")
321	}
322}
323
324type Condition struct {
325	Param    string
326	Relation Relation
327	Value    string
328}
329
330type Conjunction []Condition
331
332type Query []Conjunction
333
334func (query Query) toURLParams() string {
335	var ret string
336
337	for i, conj := range query {
338		for _, cond := range conj {
339			if len(ret) > 0 {
340				ret += "&"
341			}
342			ret += fmt.Sprintf("%s[%d]=%s:%s", url.QueryEscape(cond.Param), i, cond.Relation.String(), url.QueryEscape(cond.Value))
343		}
344	}
345
346	return ret
347}
348
349var NotFound = errors.New("acvp: HTTP code 404")
350
351func (server *Server) newRequestWithToken(method, endpoint string, body io.Reader) (*http.Request, error) {
352    token, err := server.getToken(endpoint)
353    if err != nil {
354        return nil, err
355    }
356    req, err := http.NewRequest(method, server.prefix+endpoint, body)
357    if err != nil {
358        return nil, err
359    }
360    if len(token) != 0 {
361       req.Header.Add("Authorization", "Bearer "+token)
362    }
363    return req, nil
364}
365
366func (server *Server) Get(out interface{}, endPoint string) error {
367	req, err := server.newRequestWithToken("GET", endPoint, nil)
368	if err != nil {
369		return err
370	}
371	resp, err := server.client.Do(req)
372	if err != nil {
373		return fmt.Errorf("error while fetching chunk for %q: %s", endPoint, err)
374	}
375
376	defer resp.Body.Close()
377	if resp.StatusCode == 404 {
378		return NotFound
379	} else if resp.StatusCode != 200 {
380		return fmt.Errorf("acvp: HTTP error %d", resp.StatusCode)
381	}
382	return parseReply(out, resp.Body)
383}
384
385func (server *Server) GetBytes(endPoint string) ([]byte, error) {
386	req, err := server.newRequestWithToken("GET", endPoint, nil)
387	if err != nil {
388		return nil, err
389	}
390	resp, err := server.client.Do(req)
391	if err != nil {
392		return nil, fmt.Errorf("error while fetching chunk for %q: %s", endPoint, err)
393	}
394
395	defer resp.Body.Close()
396	if resp.StatusCode == 404 {
397		return nil, NotFound
398	} else if resp.StatusCode != 200 {
399		return nil, fmt.Errorf("acvp: HTTP error %d", resp.StatusCode)
400	}
401	return parseReplyToBytes(resp.Body)
402}
403
404func (server *Server) write(method string, reply interface{}, endPoint string, contents []byte) error {
405	var buf bytes.Buffer
406	buf.WriteString(requestPrefix)
407	buf.Write(contents)
408	buf.WriteString(requestSuffix)
409
410	req, err := server.newRequestWithToken("POST", endPoint, &buf)
411	if err != nil {
412		return err
413	}
414	req.Header.Add("Content-Type", "application/json")
415	resp, err := server.client.Do(req)
416	if err != nil {
417		return fmt.Errorf("error while writing to %q: %s", endPoint, err)
418	}
419
420	defer resp.Body.Close()
421	if resp.StatusCode == 404 {
422		return NotFound
423	} else if resp.StatusCode != 200 {
424		return fmt.Errorf("acvp: HTTP error %d", resp.StatusCode)
425	}
426	return parseReply(reply, resp.Body)
427}
428
429func (server *Server) postMessage(reply interface{}, endPoint string, request interface{}) error {
430	contents, err := json.Marshal(request)
431	if err != nil {
432		return err
433	}
434	return server.write("POST", reply, endPoint, contents)
435}
436
437func (server *Server) Post(out interface{}, endPoint string, contents []byte) error {
438	return server.write("POST", out, endPoint, contents)
439}
440
441func (server *Server) Put(out interface{}, endPoint string, contents []byte) error {
442	return server.write("PUT", out, endPoint, contents)
443}
444
445func (server *Server) Delete(endPoint string) error {
446	req, err := server.newRequestWithToken("DELETE", endPoint, nil)
447	resp, err := server.client.Do(req)
448	if err != nil {
449		return fmt.Errorf("error while writing to %q: %s", endPoint, err)
450	}
451
452	defer resp.Body.Close()
453	if resp.StatusCode != 200 {
454		return fmt.Errorf("acvp: HTTP error %d", resp.StatusCode)
455	}
456	fmt.Printf("DELETE %q %d\n", server.prefix+endPoint, resp.StatusCode)
457	return nil
458}
459
460var (
461	uint64Type = reflect.TypeOf(uint64(0))
462	boolType   = reflect.TypeOf(false)
463	stringType = reflect.TypeOf("")
464)
465
466// GetPaged returns an array of records of some type using one or more requests to the server. See
467// https://usnistgov.github.io/ACVP/artifacts/draft-fussell-acvp-spec-00.html#paging_response
468func (server *Server) GetPaged(out interface{}, endPoint string, condition Query) error {
469	output := reflect.ValueOf(out)
470	if output.Kind() != reflect.Ptr {
471		panic(fmt.Sprintf("GetPaged output parameter of non-pointer type %T", out))
472	}
473
474	token, err := server.getToken(endPoint)
475	if err != nil {
476		return err
477	}
478
479	outputSlice := output.Elem()
480
481	replyType := reflect.StructOf([]reflect.StructField{
482		{Name: "TotalCount", Type: uint64Type, Tag: `json:"totalCount"`},
483		{Name: "Incomplete", Type: boolType, Tag: `json:"incomplete"`},
484		{Name: "Data", Type: output.Elem().Type(), Tag: `json:"data"`},
485		{Name: "Links", Type: reflect.StructOf([]reflect.StructField{
486			{Name: "Next", Type: stringType, Tag: `json:"next"`},
487		}), Tag: `json:"links"`},
488	})
489	nextURL := server.prefix + endPoint
490	conditionParams := condition.toURLParams()
491	if len(conditionParams) > 0 {
492		nextURL += "?" + conditionParams
493	}
494
495	isFirstRequest := true
496	for {
497		req, err := http.NewRequest("GET", nextURL, nil)
498		if err != nil {
499			return err
500		}
501		if len(token) != 0 {
502			req.Header.Add("Authorization", "Bearer "+token)
503		}
504		resp, err := server.client.Do(req)
505		if err != nil {
506			return fmt.Errorf("error while fetching chunk for %q: %s", endPoint, err)
507		}
508		if resp.StatusCode == 404 && isFirstRequest {
509			resp.Body.Close()
510			return nil
511		} else if resp.StatusCode != 200 {
512			resp.Body.Close()
513			return fmt.Errorf("acvp: HTTP error %d", resp.StatusCode)
514		}
515		isFirstRequest = false
516
517		reply := reflect.New(replyType)
518		err = parseReply(reply.Interface(), resp.Body)
519		resp.Body.Close()
520		if err != nil {
521			return err
522		}
523
524		data := reply.Elem().FieldByName("Data")
525		for i := 0; i < data.Len(); i++ {
526			outputSlice.Set(reflect.Append(outputSlice, data.Index(i)))
527		}
528
529		if uint64(outputSlice.Len()) == reply.Elem().FieldByName("TotalCount").Uint() ||
530			reply.Elem().FieldByName("Links").FieldByName("Next").String() == "" {
531			break
532		}
533
534		nextURL = server.prefix + endPoint + fmt.Sprintf("?offset=%d", outputSlice.Len())
535		if len(conditionParams) > 0 {
536			nextURL += "&" + conditionParams
537		}
538	}
539
540	return nil
541}
542
543// https://usnistgov.github.io/ACVP/artifacts/draft-fussell-acvp-spec-00.html#rfc.section.11.8.3.1
544type Vendor struct {
545	URL         string    `json:"url,omitempty"`
546	Name        string    `json:"name,omitempty"`
547	ParentURL   string    `json:"parentUrl,omitempty"`
548	Website     string    `json:"website,omitempty"`
549	Emails      []string  `json:"emails,omitempty"`
550	ContactsURL string    `json:"contactsUrl,omitempty"`
551	Addresses   []Address `json:"addresses,omitempty"`
552}
553
554// https://usnistgov.github.io/ACVP/artifacts/draft-fussell-acvp-spec-00.html#rfc.section.11.9
555type Address struct {
556	URL        string `json:"url,omitempty"`
557	Street1    string `json:"street1,omitempty"`
558	Street2    string `json:"street2,omitempty"`
559	Street3    string `json:"street3,omitempty"`
560	Locality   string `json:"locality,omitempty"`
561	Region     string `json:"region,omitempty"`
562	Country    string `json:"country,omitempty"`
563	PostalCode string `json:"postalCode,omitempty"`
564}
565
566// https://usnistgov.github.io/ACVP/artifacts/draft-fussell-acvp-spec-00.html#rfc.section.11.10
567type Person struct {
568	URL          string   `json:"url,omitempty"`
569	FullName     string   `json:"fullName,omitempty"`
570	VendorURL    string   `json:"vendorUrl,omitempty"`
571	Emails       []string `json:"emails,omitempty"`
572	PhoneNumbers []struct {
573		Number string `json:"number,omitempty"`
574		Type   string `json:"type,omitempty"`
575	} `json:"phoneNumbers,omitempty"`
576}
577
578// https://usnistgov.github.io/ACVP/artifacts/draft-fussell-acvp-spec-00.html#rfc.section.11.11
579type Module struct {
580	URL         string   `json:"url,omitempty"`
581	Name        string   `json:"name,omitempty"`
582	Version     string   `json:"version,omitempty"`
583	Type        string   `json:"type,omitempty"`
584	Website     string   `json:"website,omitempty"`
585	VendorURL   string   `json:"vendorUrl,omitempty"`
586	AddressURL  string   `json:"addressUrl,omitempty"`
587	ContactURLs []string `json:"contactUrls,omitempty"`
588	Description string   `json:"description,omitempty"`
589}
590
591type RequestStatus struct {
592	URL         string `json:"url,omitempty"`
593	Status      string `json:"status,omitempty"`
594	Message     string `json:"message,omitempty"`
595	ApprovedURL string `json:"approvedUrl,omitempty"`
596}
597
598type OperationalEnvironment struct {
599	URL            string       `json:"url,omitempty"`
600	Name           string       `json:"name,omitempty"`
601	DependencyUrls []string     `json:"dependencyUrls,omitempty"`
602	Dependencies   []Dependency `json:"dependencies,omitempty"`
603}
604
605type Dependency map[string]interface{}
606
607type Algorithm map[string]interface{}
608
609type TestSession struct {
610	URL           string                   `json:"url,omitempty"`
611	ACVPVersion   string                   `json:"acvpVersion,omitempty"`
612	Created       string                   `json:"createdOn,omitempty"`
613	Expires       string                   `json:"expiresOn,omitempty"`
614	VectorSetURLs []string                 `json:"vectorSetUrls,omitempty"`
615	AccessToken   string                   `json:"accessToken,omitempty"`
616	Algorithms    []map[string]interface{} `json:"algorithms,omitempty"`
617	EncryptAtRest bool                     `json:"encryptAtRest,omitempty"`
618	IsSample      bool                     `json:"isSample,omitempty"`
619	Publishable   bool                     `json:"publishable,omitempty"`
620	Passed        bool                     `json:"passed,omitempty"`
621}
622
623type Vectors struct {
624	Retry    uint64 `json:"retry,omitempty"`
625	ID       uint64 `json:"vsId"`
626	Algo     string `json:"algorithm,omitempty"`
627	Revision string `json:"revision,omitempty"`
628}
629
630type LargeUploadRequest struct {
631	Size uint64 `json:"submissionSize,omitempty"`
632	URL  string `json:"vectorSetUrl,omitempty"`
633}
634
635type LargeUploadResponse struct {
636	URL         string `json:"url"`
637	AccessToken string `json:"accessToken"`
638}
639
640type SessionResults struct {
641	Passed  bool `json:"passed"`
642	Results []struct {
643		URL    string `json:"vectorSetUrl,omitempty"`
644		Status string `json:"status"`
645	} `json:"results"`
646}
647