github.com/letsencrypt/boulder@v0.20251208.0/core/objects.go (about)

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