...

Source file src/github.com/letsencrypt/boulder/core/objects.go

Documentation: github.com/letsencrypt/boulder/core

     1  package core
     2  
     3  import (
     4  	"crypto"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"hash/fnv"
     9  	"net"
    10  	"strings"
    11  	"time"
    12  
    13  	"golang.org/x/crypto/ocsp"
    14  	"gopkg.in/go-jose/go-jose.v2"
    15  
    16  	"github.com/letsencrypt/boulder/identifier"
    17  	"github.com/letsencrypt/boulder/probs"
    18  	"github.com/letsencrypt/boulder/revocation"
    19  )
    20  
    21  // AcmeStatus defines the state of a given authorization
    22  type AcmeStatus string
    23  
    24  // These statuses are the states of authorizations, challenges, and registrations
    25  const (
    26  	StatusUnknown     = AcmeStatus("unknown")     // Unknown status; the default
    27  	StatusPending     = AcmeStatus("pending")     // In process; client has next action
    28  	StatusProcessing  = AcmeStatus("processing")  // In process; server has next action
    29  	StatusReady       = AcmeStatus("ready")       // Order is ready for finalization
    30  	StatusValid       = AcmeStatus("valid")       // Object is valid
    31  	StatusInvalid     = AcmeStatus("invalid")     // Validation failed
    32  	StatusRevoked     = AcmeStatus("revoked")     // Object no longer valid
    33  	StatusDeactivated = AcmeStatus("deactivated") // Object has been deactivated
    34  )
    35  
    36  // AcmeResource values identify different types of ACME resources
    37  type AcmeResource string
    38  
    39  // The types of ACME resources
    40  const (
    41  	ResourceNewReg       = AcmeResource("new-reg")
    42  	ResourceNewAuthz     = AcmeResource("new-authz")
    43  	ResourceNewCert      = AcmeResource("new-cert")
    44  	ResourceRevokeCert   = AcmeResource("revoke-cert")
    45  	ResourceRegistration = AcmeResource("reg")
    46  	ResourceChallenge    = AcmeResource("challenge")
    47  	ResourceAuthz        = AcmeResource("authz")
    48  	ResourceKeyChange    = AcmeResource("key-change")
    49  )
    50  
    51  // AcmeChallenge values identify different types of ACME challenges
    52  type AcmeChallenge string
    53  
    54  // These types are the available challenges
    55  const (
    56  	ChallengeTypeHTTP01    = AcmeChallenge("http-01")
    57  	ChallengeTypeDNS01     = AcmeChallenge("dns-01")
    58  	ChallengeTypeTLSALPN01 = AcmeChallenge("tls-alpn-01")
    59  )
    60  
    61  // IsValid tests whether the challenge is a known challenge
    62  func (c AcmeChallenge) IsValid() bool {
    63  	switch c {
    64  	case ChallengeTypeHTTP01, ChallengeTypeDNS01, ChallengeTypeTLSALPN01:
    65  		return true
    66  	default:
    67  		return false
    68  	}
    69  }
    70  
    71  // OCSPStatus defines the state of OCSP for a domain
    72  type OCSPStatus string
    73  
    74  // These status are the states of OCSP
    75  const (
    76  	OCSPStatusGood    = OCSPStatus("good")
    77  	OCSPStatusRevoked = OCSPStatus("revoked")
    78  	// Not a real OCSP status. This is a placeholder we write before the
    79  	// actual precertificate is issued, to ensure we never return "good" before
    80  	// issuance succeeds, for BR compliance reasons.
    81  	OCSPStatusNotReady = OCSPStatus("wait")
    82  )
    83  
    84  var OCSPStatusToInt = map[OCSPStatus]int{
    85  	OCSPStatusGood:     ocsp.Good,
    86  	OCSPStatusRevoked:  ocsp.Revoked,
    87  	OCSPStatusNotReady: -1,
    88  }
    89  
    90  // DNSPrefix is attached to DNS names in DNS challenges
    91  const DNSPrefix = "_acme-challenge"
    92  
    93  type RawCertificateRequest struct {
    94  	CSR JSONBuffer `json:"csr"` // The encoded CSR
    95  }
    96  
    97  // Registration objects represent non-public metadata attached
    98  // to account keys.
    99  type Registration struct {
   100  	// Unique identifier
   101  	ID int64 `json:"id,omitempty" db:"id"`
   102  
   103  	// Account key to which the details are attached
   104  	Key *jose.JSONWebKey `json:"key"`
   105  
   106  	// Contact URIs
   107  	Contact *[]string `json:"contact,omitempty"`
   108  
   109  	// Agreement with terms of service
   110  	Agreement string `json:"agreement,omitempty"`
   111  
   112  	// InitialIP is the IP address from which the registration was created
   113  	InitialIP net.IP `json:"initialIp"`
   114  
   115  	// CreatedAt is the time the registration was created.
   116  	CreatedAt *time.Time `json:"createdAt,omitempty"`
   117  
   118  	Status AcmeStatus `json:"status"`
   119  }
   120  
   121  // ValidationRecord represents a validation attempt against a specific URL/hostname
   122  // and the IP addresses that were resolved and used
   123  type ValidationRecord struct {
   124  	// SimpleHTTP only
   125  	URL string `json:"url,omitempty"`
   126  
   127  	// Shared
   128  	Hostname          string   `json:"hostname,omitempty"`
   129  	Port              string   `json:"port,omitempty"`
   130  	AddressesResolved []net.IP `json:"addressesResolved,omitempty"`
   131  	AddressUsed       net.IP   `json:"addressUsed,omitempty"`
   132  	// AddressesTried contains a list of addresses tried before the `AddressUsed`.
   133  	// Presently this will only ever be one IP from `AddressesResolved` since the
   134  	// only retry is in the case of a v6 failure with one v4 fallback. E.g. if
   135  	// a record with `AddressesResolved: { 127.0.0.1, ::1 }` were processed for
   136  	// a challenge validation with the IPv6 first flag on and the ::1 address
   137  	// failed but the 127.0.0.1 retry succeeded then the record would end up
   138  	// being:
   139  	// {
   140  	//   ...
   141  	//   AddressesResolved: [ 127.0.0.1, ::1 ],
   142  	//   AddressUsed: 127.0.0.1
   143  	//   AddressesTried: [ ::1 ],
   144  	//   ...
   145  	// }
   146  	AddressesTried []net.IP `json:"addressesTried,omitempty"`
   147  }
   148  
   149  func looksLikeKeyAuthorization(str string) error {
   150  	parts := strings.Split(str, ".")
   151  	if len(parts) != 2 {
   152  		return fmt.Errorf("Invalid key authorization: does not look like a key authorization")
   153  	} else if !LooksLikeAToken(parts[0]) {
   154  		return fmt.Errorf("Invalid key authorization: malformed token")
   155  	} else if !LooksLikeAToken(parts[1]) {
   156  		// Thumbprints have the same syntax as tokens in boulder
   157  		// Both are base64-encoded and 32 octets
   158  		return fmt.Errorf("Invalid key authorization: malformed key thumbprint")
   159  	}
   160  	return nil
   161  }
   162  
   163  // Challenge is an aggregate of all data needed for any challenges.
   164  //
   165  // Rather than define individual types for different types of
   166  // challenge, we just throw all the elements into one bucket,
   167  // together with the common metadata elements.
   168  type Challenge struct {
   169  	// The type of challenge
   170  	Type AcmeChallenge `json:"type"`
   171  
   172  	// The status of this challenge
   173  	Status AcmeStatus `json:"status,omitempty"`
   174  
   175  	// Contains the error that occurred during challenge validation, if any
   176  	Error *probs.ProblemDetails `json:"error,omitempty"`
   177  
   178  	// A URI to which a response can be POSTed
   179  	URI string `json:"uri,omitempty"`
   180  
   181  	// For the V2 API the "URI" field is deprecated in favour of URL.
   182  	URL string `json:"url,omitempty"`
   183  
   184  	// Used by http-01, tls-sni-01, tls-alpn-01 and dns-01 challenges
   185  	Token string `json:"token,omitempty"`
   186  
   187  	// The expected KeyAuthorization for validation of the challenge. Populated by
   188  	// the RA prior to passing the challenge to the VA. For legacy reasons this
   189  	// field is called "ProvidedKeyAuthorization" because it was initially set by
   190  	// the content of the challenge update POST from the client. It is no longer
   191  	// set that way and should be renamed to "KeyAuthorization".
   192  	// TODO(@cpu): Rename `ProvidedKeyAuthorization` to `KeyAuthorization`.
   193  	ProvidedKeyAuthorization string `json:"keyAuthorization,omitempty"`
   194  
   195  	// Contains information about URLs used or redirected to and IPs resolved and
   196  	// used
   197  	ValidationRecord []ValidationRecord `json:"validationRecord,omitempty"`
   198  	// The time at which the server validated the challenge. Required by
   199  	// RFC8555 if status is valid.
   200  	Validated *time.Time `json:"validated,omitempty"`
   201  }
   202  
   203  // ExpectedKeyAuthorization computes the expected KeyAuthorization value for
   204  // the challenge.
   205  func (ch Challenge) ExpectedKeyAuthorization(key *jose.JSONWebKey) (string, error) {
   206  	if key == nil {
   207  		return "", fmt.Errorf("Cannot authorize a nil key")
   208  	}
   209  
   210  	thumbprint, err := key.Thumbprint(crypto.SHA256)
   211  	if err != nil {
   212  		return "", err
   213  	}
   214  
   215  	return ch.Token + "." + base64.RawURLEncoding.EncodeToString(thumbprint), nil
   216  }
   217  
   218  // RecordsSane checks the sanity of a ValidationRecord object before sending it
   219  // back to the RA to be stored.
   220  func (ch Challenge) RecordsSane() bool {
   221  	if ch.ValidationRecord == nil || len(ch.ValidationRecord) == 0 {
   222  		return false
   223  	}
   224  
   225  	switch ch.Type {
   226  	case ChallengeTypeHTTP01:
   227  		for _, rec := range ch.ValidationRecord {
   228  			if rec.URL == "" || rec.Hostname == "" || rec.Port == "" || rec.AddressUsed == nil ||
   229  				len(rec.AddressesResolved) == 0 {
   230  				return false
   231  			}
   232  		}
   233  	case ChallengeTypeTLSALPN01:
   234  		if len(ch.ValidationRecord) > 1 {
   235  			return false
   236  		}
   237  		if ch.ValidationRecord[0].URL != "" {
   238  			return false
   239  		}
   240  		if ch.ValidationRecord[0].Hostname == "" || ch.ValidationRecord[0].Port == "" ||
   241  			ch.ValidationRecord[0].AddressUsed == nil || len(ch.ValidationRecord[0].AddressesResolved) == 0 {
   242  			return false
   243  		}
   244  	case ChallengeTypeDNS01:
   245  		if len(ch.ValidationRecord) > 1 {
   246  			return false
   247  		}
   248  		if ch.ValidationRecord[0].Hostname == "" {
   249  			return false
   250  		}
   251  		return true
   252  	default: // Unsupported challenge type
   253  		return false
   254  	}
   255  
   256  	return true
   257  }
   258  
   259  // CheckConsistencyForClientOffer checks the fields of a challenge object before it is
   260  // given to the client.
   261  func (ch Challenge) CheckConsistencyForClientOffer() error {
   262  	err := ch.checkConsistency()
   263  	if err != nil {
   264  		return err
   265  	}
   266  
   267  	// Before completion, the key authorization field should be empty
   268  	if ch.ProvidedKeyAuthorization != "" {
   269  		return fmt.Errorf("A response to this challenge was already submitted.")
   270  	}
   271  	return nil
   272  }
   273  
   274  // CheckConsistencyForValidation checks the fields of a challenge object before it is
   275  // given to the VA.
   276  func (ch Challenge) CheckConsistencyForValidation() error {
   277  	err := ch.checkConsistency()
   278  	if err != nil {
   279  		return err
   280  	}
   281  
   282  	// If the challenge is completed, then there should be a key authorization
   283  	return looksLikeKeyAuthorization(ch.ProvidedKeyAuthorization)
   284  }
   285  
   286  // checkConsistency checks the sanity of a challenge object before issued to the client.
   287  func (ch Challenge) checkConsistency() error {
   288  	if ch.Status != StatusPending {
   289  		return fmt.Errorf("The challenge is not pending.")
   290  	}
   291  
   292  	// There always needs to be a token
   293  	if !LooksLikeAToken(ch.Token) {
   294  		return fmt.Errorf("The token is missing.")
   295  	}
   296  	return nil
   297  }
   298  
   299  // StringID is used to generate a ID for challenges associated with new style authorizations.
   300  // This is necessary as these challenges no longer have a unique non-sequential identifier
   301  // in the new storage scheme. This identifier is generated by constructing a fnv hash over the
   302  // challenge token and type and encoding the first 4 bytes of it using the base64 URL encoding.
   303  func (ch Challenge) StringID() string {
   304  	h := fnv.New128a()
   305  	h.Write([]byte(ch.Token))
   306  	h.Write([]byte(ch.Type))
   307  	return base64.RawURLEncoding.EncodeToString(h.Sum(nil)[0:4])
   308  }
   309  
   310  // Authorization represents the authorization of an account key holder
   311  // to act on behalf of a domain.  This struct is intended to be used both
   312  // internally and for JSON marshaling on the wire.  Any fields that should be
   313  // suppressed on the wire (e.g., ID, regID) must be made empty before marshaling.
   314  type Authorization struct {
   315  	// An identifier for this authorization, unique across
   316  	// authorizations and certificates within this instance.
   317  	ID string `json:"id,omitempty" db:"id"`
   318  
   319  	// The identifier for which authorization is being given
   320  	Identifier identifier.ACMEIdentifier `json:"identifier,omitempty" db:"identifier"`
   321  
   322  	// The registration ID associated with the authorization
   323  	RegistrationID int64 `json:"regId,omitempty" db:"registrationID"`
   324  
   325  	// The status of the validation of this authorization
   326  	Status AcmeStatus `json:"status,omitempty" db:"status"`
   327  
   328  	// The date after which this authorization will be no
   329  	// longer be considered valid. Note: a certificate may be issued even on the
   330  	// last day of an authorization's lifetime. The last day for which someone can
   331  	// hold a valid certificate based on an authorization is authorization
   332  	// lifetime + certificate lifetime.
   333  	Expires *time.Time `json:"expires,omitempty" db:"expires"`
   334  
   335  	// An array of challenges objects used to validate the
   336  	// applicant's control of the identifier.  For authorizations
   337  	// in process, these are challenges to be fulfilled; for
   338  	// final authorizations, they describe the evidence that
   339  	// the server used in support of granting the authorization.
   340  	//
   341  	// There should only ever be one challenge of each type in this
   342  	// slice and the order of these challenges may not be predictable.
   343  	Challenges []Challenge `json:"challenges,omitempty" db:"-"`
   344  
   345  	// https://datatracker.ietf.org/doc/html/rfc8555#page-29
   346  	//
   347  	// wildcard (optional, boolean):  This field MUST be present and true
   348  	//   for authorizations created as a result of a newOrder request
   349  	//   containing a DNS identifier with a value that was a wildcard
   350  	//   domain name.  For other authorizations, it MUST be absent.
   351  	//   Wildcard domain names are described in Section 7.1.3.
   352  	//
   353  	// This is not represented in the database because we calculate it from
   354  	// the identifier stored in the database. Unlike the identifier returned
   355  	// as part of the authorization, the identifier we store in the database
   356  	// can contain an asterisk.
   357  	Wildcard bool `json:"wildcard,omitempty" db:"-"`
   358  }
   359  
   360  // FindChallengeByStringID will look for a challenge matching the given ID inside
   361  // this authorization. If found, it will return the index of that challenge within
   362  // the Authorization's Challenges array. Otherwise it will return -1.
   363  func (authz *Authorization) FindChallengeByStringID(id string) int {
   364  	for i, c := range authz.Challenges {
   365  		if c.StringID() == id {
   366  			return i
   367  		}
   368  	}
   369  	return -1
   370  }
   371  
   372  // SolvedBy will look through the Authorizations challenges, returning the type
   373  // of the *first* challenge it finds with Status: valid, or an error if no
   374  // challenge is valid.
   375  func (authz *Authorization) SolvedBy() (AcmeChallenge, error) {
   376  	if len(authz.Challenges) == 0 {
   377  		return "", fmt.Errorf("Authorization has no challenges")
   378  	}
   379  	for _, chal := range authz.Challenges {
   380  		if chal.Status == StatusValid {
   381  			return chal.Type, nil
   382  		}
   383  	}
   384  	return "", fmt.Errorf("Authorization not solved by any challenge")
   385  }
   386  
   387  // JSONBuffer fields get encoded and decoded JOSE-style, in base64url encoding
   388  // with stripped padding.
   389  type JSONBuffer []byte
   390  
   391  // MarshalJSON encodes a JSONBuffer for transmission.
   392  func (jb JSONBuffer) MarshalJSON() (result []byte, err error) {
   393  	return json.Marshal(base64.RawURLEncoding.EncodeToString(jb))
   394  }
   395  
   396  // UnmarshalJSON decodes a JSONBuffer to an object.
   397  func (jb *JSONBuffer) UnmarshalJSON(data []byte) (err error) {
   398  	var str string
   399  	err = json.Unmarshal(data, &str)
   400  	if err != nil {
   401  		return err
   402  	}
   403  	*jb, err = base64.RawURLEncoding.DecodeString(strings.TrimRight(str, "="))
   404  	return
   405  }
   406  
   407  // Certificate objects are entirely internal to the server.  The only
   408  // thing exposed on the wire is the certificate itself.
   409  type Certificate struct {
   410  	ID             int64 `db:"id"`
   411  	RegistrationID int64 `db:"registrationID"`
   412  
   413  	Serial  string    `db:"serial"`
   414  	Digest  string    `db:"digest"`
   415  	DER     []byte    `db:"der"`
   416  	Issued  time.Time `db:"issued"`
   417  	Expires time.Time `db:"expires"`
   418  }
   419  
   420  // CertificateStatus structs are internal to the server. They represent the
   421  // latest data about the status of the certificate, required for generating new
   422  // OCSP responses and determining if a certificate has been revoked.
   423  type CertificateStatus struct {
   424  	ID int64 `db:"id"`
   425  
   426  	Serial string `db:"serial"`
   427  
   428  	// status: 'good' or 'revoked'. Note that good, expired certificates remain
   429  	// with status 'good' but don't necessarily get fresh OCSP responses.
   430  	Status OCSPStatus `db:"status"`
   431  
   432  	// ocspLastUpdated: The date and time of the last time we generated an OCSP
   433  	// response. If we have never generated one, this has the zero value of
   434  	// time.Time, i.e. Jan 1 1970.
   435  	OCSPLastUpdated time.Time `db:"ocspLastUpdated"`
   436  
   437  	// revokedDate: If status is 'revoked', this is the date and time it was
   438  	// revoked. Otherwise it has the zero value of time.Time, i.e. Jan 1 1970.
   439  	RevokedDate time.Time `db:"revokedDate"`
   440  
   441  	// revokedReason: If status is 'revoked', this is the reason code for the
   442  	// revocation. Otherwise it is zero (which happens to be the reason
   443  	// code for 'unspecified').
   444  	RevokedReason revocation.Reason `db:"revokedReason"`
   445  
   446  	LastExpirationNagSent time.Time `db:"lastExpirationNagSent"`
   447  
   448  	// NotAfter and IsExpired are convenience columns which allow expensive
   449  	// queries to quickly filter out certificates that we don't need to care about
   450  	// anymore. These are particularly useful for the expiration mailer and CRL
   451  	// updater. See https://github.com/letsencrypt/boulder/issues/1864.
   452  	NotAfter  time.Time `db:"notAfter"`
   453  	IsExpired bool      `db:"isExpired"`
   454  
   455  	// Note: this is not an issuance.IssuerNameID because that would create an
   456  	// import cycle between core and issuance.
   457  	// Note2: This field used to be called `issuerID`. We keep the old name in
   458  	// the DB, but update the Go field name to be clear which type of ID this
   459  	// is.
   460  	IssuerNameID int64 `db:"issuerID"`
   461  }
   462  
   463  // FQDNSet contains the SHA256 hash of the lowercased, comma joined dNSNames
   464  // contained in a certificate.
   465  type FQDNSet struct {
   466  	ID      int64
   467  	SetHash []byte
   468  	Serial  string
   469  	Issued  time.Time
   470  	Expires time.Time
   471  }
   472  
   473  // SCTDERs is a convenience type
   474  type SCTDERs [][]byte
   475  
   476  // CertDER is a convenience type that helps differentiate what the
   477  // underlying byte slice contains
   478  type CertDER []byte
   479  
   480  // SuggestedWindow is a type exposed inside the RenewalInfo resource.
   481  type SuggestedWindow struct {
   482  	Start time.Time `json:"start"`
   483  	End   time.Time `json:"end"`
   484  }
   485  
   486  // RenewalInfo is a type which is exposed to clients which query the renewalInfo
   487  // endpoint specified in draft-aaron-ari.
   488  type RenewalInfo struct {
   489  	SuggestedWindow SuggestedWindow `json:"suggestedWindow"`
   490  }
   491  
   492  // RenewalInfoSimple constructs a `RenewalInfo` object and suggested window
   493  // using a very simple renewal calculation: calculate a point 2/3rds of the way
   494  // through the validity period, then give a 2-day window around that. Both the
   495  // `issued` and `expires` timestamps are expected to be UTC.
   496  func RenewalInfoSimple(issued time.Time, expires time.Time) RenewalInfo {
   497  	validity := expires.Add(time.Second).Sub(issued)
   498  	renewalOffset := validity / time.Duration(3)
   499  	idealRenewal := expires.Add(-renewalOffset)
   500  	return RenewalInfo{
   501  		SuggestedWindow: SuggestedWindow{
   502  			Start: idealRenewal.Add(-24 * time.Hour),
   503  			End:   idealRenewal.Add(24 * time.Hour),
   504  		},
   505  	}
   506  }
   507  
   508  // RenewalInfoImmediate constructs a `RenewalInfo` object with a suggested
   509  // window in the past. Per the draft-ietf-acme-ari-01 spec, clients should
   510  // attempt to renew immediately if the suggested window is in the past. The
   511  // passed `now` is assumed to be a timestamp representing the current moment in
   512  // time.
   513  func RenewalInfoImmediate(now time.Time) RenewalInfo {
   514  	oneHourAgo := now.Add(-1 * time.Hour)
   515  	return RenewalInfo{
   516  		SuggestedWindow: SuggestedWindow{
   517  			Start: oneHourAgo,
   518  			End:   oneHourAgo.Add(time.Minute * 30),
   519  		},
   520  	}
   521  }
   522  

View as plain text