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