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