...

Source file src/github.com/letsencrypt/boulder/test/load-generator/acme/directory.go

Documentation: github.com/letsencrypt/boulder/test/load-generator/acme

     1  // Package acme provides ACME client functionality tailored to the needs of the
     2  // load-generator. It is not a general purpose ACME client library.
     3  package acme
     4  
     5  import (
     6  	"crypto/tls"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net"
    12  	"net/http"
    13  	"net/url"
    14  	"time"
    15  )
    16  
    17  const (
    18  	// NewNonceEndpoint is the directory key for the newNonce endpoint.
    19  	NewNonceEndpoint Endpoint = "newNonce"
    20  	// NewAccountEndpoint is the directory key for the newAccount endpoint.
    21  	NewAccountEndpoint Endpoint = "newAccount"
    22  	// NewOrderEndpoint is the directory key for the newOrder endpoint.
    23  	NewOrderEndpoint Endpoint = "newOrder"
    24  	// RevokeCertEndpoint is the directory key for the revokeCert endpoint.
    25  	RevokeCertEndpoint Endpoint = "revokeCert"
    26  	// KeyChangeEndpoint is the directory key for the keyChange endpoint.
    27  	KeyChangeEndpoint Endpoint = "keyChange"
    28  )
    29  
    30  var (
    31  	// ErrEmptyDirectory is returned if NewDirectory is provided and empty directory URL.
    32  	ErrEmptyDirectory = errors.New("directoryURL must not be empty")
    33  	// ErrInvalidDirectoryURL is returned if NewDirectory is provided an invalid directory URL.
    34  	ErrInvalidDirectoryURL = errors.New("directoryURL is not a valid URL")
    35  	// ErrInvalidDirectoryHTTPCode is returned if NewDirectory is provided a directory URL
    36  	// that returns something other than HTTP Status OK to a GET request.
    37  	ErrInvalidDirectoryHTTPCode = errors.New("GET request to directoryURL did not result in HTTP Status 200")
    38  	// ErrInvalidDirectoryJSON is returned if NewDirectory is provided a directory URL
    39  	// that returns invalid JSON.
    40  	ErrInvalidDirectoryJSON = errors.New("GET request to directoryURL returned invalid JSON")
    41  	// ErrInvalidDirectoryMeta is returned if NewDirectory is provided a directory
    42  	// URL that returns a directory resource with an invalid or  missing "meta" key.
    43  	ErrInvalidDirectoryMeta = errors.New(`server's directory resource had invalid or missing "meta" key`)
    44  	// ErrInvalidTermsOfSerivce is returned if NewDirectory is provided
    45  	// a directory URL that returns a directory resource with an invalid or
    46  	// missing "termsOfService" key in the "meta" map.
    47  	ErrInvalidTermsOfService = errors.New(`server's directory resource had invalid or missing "meta.termsOfService" key`)
    48  
    49  	// RequiredEndpoints is a slice of Endpoint keys that must be present in the
    50  	// ACME server's directory. The load-generator uses each of these endpoints
    51  	// and expects to be able to find a URL for each in the server's directory
    52  	// resource.
    53  	RequiredEndpoints = []Endpoint{
    54  		NewNonceEndpoint, NewAccountEndpoint,
    55  		NewOrderEndpoint, RevokeCertEndpoint,
    56  	}
    57  )
    58  
    59  // Endpoint represents a string key used for looking up an endpoint URL in an ACME
    60  // server directory resource.
    61  //
    62  // E.g. NewOrderEndpoint -> "newOrder" -> "https://acme.example.com/acme/v1/new-order-plz"
    63  //
    64  // See "ACME Resource Types" registry - RFC 8555 Section 9.7.5.
    65  type Endpoint string
    66  
    67  // ErrMissingEndpoint is an error returned if NewDirectory is provided an ACME
    68  // server directory URL that is missing a key for a required endpoint in the
    69  // response JSON. See also RequiredEndpoints.
    70  type ErrMissingEndpoint struct {
    71  	endpoint Endpoint
    72  }
    73  
    74  // Error returns the error message for an ErrMissingEndpoint error.
    75  func (e ErrMissingEndpoint) Error() string {
    76  	return fmt.Sprintf(
    77  		"directoryURL JSON was missing required key for %q endpoint",
    78  		e.endpoint,
    79  	)
    80  }
    81  
    82  // ErrInvalidEndpointURL is an error returned if NewDirectory is provided an
    83  // ACME server directory URL that has an invalid URL for a required endpoint.
    84  // See also RequiredEndpoints.
    85  type ErrInvalidEndpointURL struct {
    86  	endpoint Endpoint
    87  	value    string
    88  }
    89  
    90  // Error returns the error message for an ErrInvalidEndpointURL error.
    91  func (e ErrInvalidEndpointURL) Error() string {
    92  	return fmt.Sprintf(
    93  		"directoryURL JSON had invalid URL value (%q) for %q endpoint",
    94  		e.value, e.endpoint)
    95  }
    96  
    97  // Directory is a type for holding URLs extracted from the ACME server's
    98  // Directory resource.
    99  //
   100  // See RFC 8555 Section 7.1.1 "Directory".
   101  //
   102  // Its public API is read-only and therefore it is safe for concurrent access.
   103  type Directory struct {
   104  	// TermsOfService is the URL identifying the current terms of service found in
   105  	// the ACME server's directory resource's "meta" field.
   106  	TermsOfService string
   107  	// endpointURLs is a map from endpoint name to URL.
   108  	endpointURLs map[Endpoint]string
   109  }
   110  
   111  // getRawDirectory validates the provided directoryURL and makes a GET request
   112  // to fetch the raw bytes of the server's directory resource. If the URL is
   113  // invalid, if there is an error getting the directory bytes, or if the HTTP
   114  // response code is not 200 an error is returned.
   115  func getRawDirectory(directoryURL string) ([]byte, error) {
   116  	if directoryURL == "" {
   117  		return nil, ErrEmptyDirectory
   118  	}
   119  
   120  	if _, err := url.Parse(directoryURL); err != nil {
   121  		return nil, ErrInvalidDirectoryURL
   122  	}
   123  
   124  	httpClient := &http.Client{
   125  		Transport: &http.Transport{
   126  			DialContext: (&net.Dialer{
   127  				Timeout:   10 * time.Second,
   128  				KeepAlive: 30 * time.Second,
   129  			}).DialContext,
   130  			TLSHandshakeTimeout: 5 * time.Second,
   131  			TLSClientConfig: &tls.Config{
   132  				// Bypassing CDN or testing against Pebble instances can cause
   133  				// validation failures. For a **test-only** tool its acceptable to skip
   134  				// cert verification of the ACME server's HTTPs certificate.
   135  				InsecureSkipVerify: true,
   136  			},
   137  			MaxIdleConns:    1,
   138  			IdleConnTimeout: 15 * time.Second,
   139  		},
   140  		Timeout: 10 * time.Second,
   141  	}
   142  
   143  	resp, err := httpClient.Get(directoryURL)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	defer resp.Body.Close()
   148  
   149  	if resp.StatusCode != http.StatusOK {
   150  		return nil, ErrInvalidDirectoryHTTPCode
   151  	}
   152  
   153  	rawDirectory, err := io.ReadAll(resp.Body)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	return rawDirectory, nil
   159  }
   160  
   161  // termsOfService reads the termsOfService key from the meta key of the raw
   162  // directory resource.
   163  func termsOfService(rawDirectory map[string]interface{}) (string, error) {
   164  	var directoryMeta map[string]interface{}
   165  
   166  	if rawDirectoryMeta, ok := rawDirectory["meta"]; !ok {
   167  		return "", ErrInvalidDirectoryMeta
   168  	} else if directoryMetaMap, ok := rawDirectoryMeta.(map[string]interface{}); !ok {
   169  		return "", ErrInvalidDirectoryMeta
   170  	} else {
   171  		directoryMeta = directoryMetaMap
   172  	}
   173  
   174  	rawToSURL, ok := directoryMeta["termsOfService"]
   175  	if !ok {
   176  		return "", ErrInvalidTermsOfService
   177  	}
   178  
   179  	tosURL, ok := rawToSURL.(string)
   180  	if !ok {
   181  		return "", ErrInvalidTermsOfService
   182  	}
   183  	return tosURL, nil
   184  }
   185  
   186  // NewDirectory creates a Directory populated from the ACME directory resource
   187  // returned by a GET request to the provided directoryURL. It also checks that
   188  // the fetched directory contains each of the RequiredEndpoints.
   189  func NewDirectory(directoryURL string) (*Directory, error) {
   190  	// Fetch the raw directory JSON
   191  	dirContents, err := getRawDirectory(directoryURL)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	// Unmarshal the directory
   197  	var dirResource map[string]interface{}
   198  	err = json.Unmarshal(dirContents, &dirResource)
   199  	if err != nil {
   200  		return nil, ErrInvalidDirectoryJSON
   201  	}
   202  
   203  	// serverURL tries to find a valid url.URL for the provided endpoint in
   204  	// the unmarshaled directory resource.
   205  	serverURL := func(name Endpoint) (*url.URL, error) {
   206  		if rawURL, ok := dirResource[string(name)]; !ok {
   207  			return nil, ErrMissingEndpoint{endpoint: name}
   208  		} else if urlString, ok := rawURL.(string); !ok {
   209  			return nil, ErrInvalidEndpointURL{endpoint: name, value: urlString}
   210  		} else if url, err := url.Parse(urlString); err != nil {
   211  			return nil, ErrInvalidEndpointURL{endpoint: name, value: urlString}
   212  		} else {
   213  			return url, nil
   214  		}
   215  	}
   216  
   217  	// Create an empty directory to populate
   218  	directory := &Directory{
   219  		endpointURLs: make(map[Endpoint]string),
   220  	}
   221  
   222  	// Every required endpoint must have a valid URL populated from the directory
   223  	for _, endpointName := range RequiredEndpoints {
   224  		url, err := serverURL(endpointName)
   225  		if err != nil {
   226  			return nil, err
   227  		}
   228  		directory.endpointURLs[endpointName] = url.String()
   229  	}
   230  
   231  	// Populate the terms-of-service
   232  	tos, err := termsOfService(dirResource)
   233  	if err != nil {
   234  		return nil, err
   235  	}
   236  	directory.TermsOfService = tos
   237  	return directory, nil
   238  }
   239  
   240  // EndpointURL returns the string representation of the ACME server's URL for
   241  // the provided endpoint. If the Endpoint is not known an empty string is
   242  // returned.
   243  func (d *Directory) EndpointURL(ep Endpoint) string {
   244  	if url, ok := d.endpointURLs[ep]; ok {
   245  		return url
   246  	}
   247  
   248  	return ""
   249  }
   250  

View as plain text