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