package ra import ( "context" "crypto" "crypto/x509" "encoding/hex" "encoding/json" "errors" "fmt" "math/big" "net" "net/url" "os" "slices" "sort" "strconv" "strings" "sync" "time" "github.com/go-jose/go-jose/v4" "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/ocsp" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" "github.com/letsencrypt/boulder/akamai" akamaipb "github.com/letsencrypt/boulder/akamai/proto" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" csrlib "github.com/letsencrypt/boulder/csr" "github.com/letsencrypt/boulder/ctpolicy" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/goodkey" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/issuance" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/policy" "github.com/letsencrypt/boulder/probs" pubpb "github.com/letsencrypt/boulder/publisher/proto" rapb "github.com/letsencrypt/boulder/ra/proto" "github.com/letsencrypt/boulder/ratelimit" "github.com/letsencrypt/boulder/ratelimits" "github.com/letsencrypt/boulder/revocation" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/va" vapb "github.com/letsencrypt/boulder/va/proto" "github.com/letsencrypt/boulder/web" ) var ( errIncompleteGRPCRequest = errors.New("incomplete gRPC request message") errIncompleteGRPCResponse = errors.New("incomplete gRPC response message") // caaRecheckDuration is the amount of time after a CAA check that we will // recheck the CAA records for a domain. Per Baseline Requirements, we must // recheck CAA records within 8 hours of issuance. We set this to 7 hours to // stay on the safe side. caaRecheckDuration = -7 * time.Hour ) // RegistrationAuthorityImpl defines an RA. // // NOTE: All of the fields in RegistrationAuthorityImpl need to be // populated, or there is a risk of panic. type RegistrationAuthorityImpl struct { rapb.UnsafeRegistrationAuthorityServer CA capb.CertificateAuthorityClient OCSP capb.OCSPGeneratorClient VA va.RemoteClients SA sapb.StorageAuthorityClient PA core.PolicyAuthority publisher pubpb.PublisherClient clk clock.Clock log blog.Logger keyPolicy goodkey.KeyPolicy // How long before a newly created authorization expires. authorizationLifetime time.Duration pendingAuthorizationLifetime time.Duration rlPolicies ratelimit.Limits maxContactsPerReg int limiter *ratelimits.Limiter txnBuilder *ratelimits.TransactionBuilder maxNames int orderLifetime time.Duration finalizeTimeout time.Duration drainWG sync.WaitGroup issuersByNameID map[issuance.NameID]*issuance.Certificate purger akamaipb.AkamaiPurgerClient ctpolicy *ctpolicy.CTPolicy ctpolicyResults *prometheus.HistogramVec revocationReasonCounter *prometheus.CounterVec namesPerCert *prometheus.HistogramVec rlCheckLatency *prometheus.HistogramVec rlOverrideUsageGauge *prometheus.GaugeVec newRegCounter prometheus.Counter recheckCAACounter prometheus.Counter newCertCounter *prometheus.CounterVec authzAges *prometheus.HistogramVec orderAges *prometheus.HistogramVec inflightFinalizes prometheus.Gauge certCSRMismatch prometheus.Counter pauseCounter *prometheus.CounterVec } var _ rapb.RegistrationAuthorityServer = (*RegistrationAuthorityImpl)(nil) // NewRegistrationAuthorityImpl constructs a new RA object. func NewRegistrationAuthorityImpl( clk clock.Clock, logger blog.Logger, stats prometheus.Registerer, maxContactsPerReg int, keyPolicy goodkey.KeyPolicy, limiter *ratelimits.Limiter, txnBuilder *ratelimits.TransactionBuilder, maxNames int, authorizationLifetime time.Duration, pendingAuthorizationLifetime time.Duration, pubc pubpb.PublisherClient, orderLifetime time.Duration, finalizeTimeout time.Duration, ctp *ctpolicy.CTPolicy, purger akamaipb.AkamaiPurgerClient, issuers []*issuance.Certificate, ) *RegistrationAuthorityImpl { ctpolicyResults := prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "ctpolicy_results", Help: "Histogram of latencies of ctpolicy.GetSCTs calls with success/failure/deadlineExceeded labels", Buckets: metrics.InternetFacingBuckets, }, []string{"result"}, ) stats.MustRegister(ctpolicyResults) namesPerCert := prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "names_per_cert", Help: "Histogram of the number of SANs in requested and issued certificates", // The namesPerCert buckets are chosen based on the current Let's Encrypt // limit of 100 SANs per certificate. Buckets: []float64{1, 5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100}, }, // Type label value is either "requested" or "issued". []string{"type"}, ) stats.MustRegister(namesPerCert) rlCheckLatency := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "ratelimitsv1_check_latency_seconds", Help: fmt.Sprintf("Latency of ratelimit checks labeled by limit=[name] and decision=[%s|%s], in seconds", ratelimits.Allowed, ratelimits.Denied), }, []string{"limit", "decision"}) stats.MustRegister(rlCheckLatency) overrideUsageGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "ratelimitsv1_override_usage", Help: "Proportion of override limit used, by limit name and client identifier.", }, []string{"limit", "override_key"}) stats.MustRegister(overrideUsageGauge) newRegCounter := prometheus.NewCounter(prometheus.CounterOpts{ Name: "new_registrations", Help: "A counter of new registrations", }) stats.MustRegister(newRegCounter) recheckCAACounter := prometheus.NewCounter(prometheus.CounterOpts{ Name: "recheck_caa", Help: "A counter of CAA rechecks", }) stats.MustRegister(recheckCAACounter) newCertCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "new_certificates", Help: "A counter of new certificates including the certificate profile name and hexadecimal certificate profile hash", }, []string{"profileName", "profileHash"}) stats.MustRegister(newCertCounter) revocationReasonCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "revocation_reason", Help: "A counter of certificate revocation reasons", }, []string{"reason"}) stats.MustRegister(revocationReasonCounter) authzAges := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "authz_ages", Help: "Histogram of ages, in seconds, of Authorization objects, labelled by method and type", // authzAges keeps track of how old, in seconds, authorizations are when // we attach them to a new order and again when we finalize that order. // We give it a non-standard bucket distribution so that the leftmost // (closest to zero) bucket can be used exclusively for brand-new (i.e. // not reused) authzs. Our buckets are: one nanosecond, one second, one // minute, one hour, 7 hours (our CAA reuse time), 1 day, 2 days, 7 // days, 30 days, +inf (should be empty). Buckets: []float64{0.000000001, 1, 60, 3600, 25200, 86400, 172800, 604800, 2592000, 7776000}, }, []string{"method", "type"}) stats.MustRegister(authzAges) orderAges := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "order_ages", Help: "Histogram of ages, in seconds, of Order objects when they're reused and finalized, labelled by method", // Orders currently have a max age of 7 days (168hrs), so our buckets // are: one nanosecond (new), 1 second, 10 seconds, 1 minute, 10 // minutes, 1 hour, 7 hours (our CAA reuse time), 1 day, 2 days, 7 days, +inf. Buckets: []float64{0.000000001, 1, 10, 60, 600, 3600, 25200, 86400, 172800, 604800}, }, []string{"method"}) stats.MustRegister(orderAges) inflightFinalizes := prometheus.NewGauge(prometheus.GaugeOpts{ Name: "inflight_finalizes", Help: "Gauge of the number of current asynchronous finalize goroutines", }) stats.MustRegister(inflightFinalizes) certCSRMismatch := prometheus.NewCounter(prometheus.CounterOpts{ Name: "cert_csr_mismatch", Help: "Number of issued certificates that have failed ra.matchesCSR for any reason. This is _real bad_ and should be alerted upon.", }) stats.MustRegister(certCSRMismatch) pauseCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "paused_pairs", Help: "Number of times a pause operation is performed, labeled by paused=[bool], repaused=[bool], grace=[bool]", }, []string{"paused", "repaused", "grace"}) stats.MustRegister(pauseCounter) issuersByNameID := make(map[issuance.NameID]*issuance.Certificate) for _, issuer := range issuers { issuersByNameID[issuer.NameID()] = issuer } ra := &RegistrationAuthorityImpl{ clk: clk, log: logger, authorizationLifetime: authorizationLifetime, pendingAuthorizationLifetime: pendingAuthorizationLifetime, rlPolicies: ratelimit.New(), maxContactsPerReg: maxContactsPerReg, keyPolicy: keyPolicy, limiter: limiter, txnBuilder: txnBuilder, maxNames: maxNames, publisher: pubc, orderLifetime: orderLifetime, finalizeTimeout: finalizeTimeout, ctpolicy: ctp, ctpolicyResults: ctpolicyResults, purger: purger, issuersByNameID: issuersByNameID, namesPerCert: namesPerCert, rlCheckLatency: rlCheckLatency, rlOverrideUsageGauge: overrideUsageGauge, newRegCounter: newRegCounter, recheckCAACounter: recheckCAACounter, newCertCounter: newCertCounter, revocationReasonCounter: revocationReasonCounter, authzAges: authzAges, orderAges: orderAges, inflightFinalizes: inflightFinalizes, certCSRMismatch: certCSRMismatch, pauseCounter: pauseCounter, } return ra } func (ra *RegistrationAuthorityImpl) LoadRateLimitPoliciesFile(filename string) error { configBytes, err := os.ReadFile(filename) if err != nil { return err } err = ra.rlPolicies.LoadPolicies(configBytes) if err != nil { return err } return nil } // certificateRequestAuthz is a struct for holding information about a valid // authz referenced during a certificateRequestEvent. It holds both the // authorization ID and the challenge type that made the authorization valid. We // specifically include the challenge type that solved the authorization to make // some common analysis easier. type certificateRequestAuthz struct { ID string ChallengeType core.AcmeChallenge } // certificateRequestEvent is a struct for holding information that is logged as // JSON to the audit log as the result of an issuance event. type certificateRequestEvent struct { ID string `json:",omitempty"` // Requester is the associated account ID Requester int64 `json:",omitempty"` // OrderID is the associated order ID (may be empty for an ACME v1 issuance) OrderID int64 `json:",omitempty"` // SerialNumber is the string representation of the issued certificate's // serial number SerialNumber string `json:",omitempty"` // VerifiedFields are required by the baseline requirements and are always // a static value for Boulder. VerifiedFields []string `json:",omitempty"` // CommonName is the subject common name from the issued cert CommonName string `json:",omitempty"` // Names are the DNS SAN entries from the issued cert Names []string `json:",omitempty"` // NotBefore is the starting timestamp of the issued cert's validity period NotBefore time.Time `json:",omitempty"` // NotAfter is the ending timestamp of the issued cert's validity period NotAfter time.Time `json:",omitempty"` // RequestTime and ResponseTime are for tracking elapsed time during issuance RequestTime time.Time `json:",omitempty"` ResponseTime time.Time `json:",omitempty"` // Error contains any encountered errors Error string `json:",omitempty"` // Authorizations is a map of identifier names to certificateRequestAuthz // objects. It can be used to understand how the names in a certificate // request were authorized. Authorizations map[string]certificateRequestAuthz // CertProfileName is a human readable name used to refer to the certificate // profile. CertProfileName string `json:",omitempty"` // CertProfileHash is SHA256 sum over every exported field of an // issuance.ProfileConfig, represented here as a hexadecimal string. CertProfileHash string `json:",omitempty"` } // certificateRevocationEvent is a struct for holding information that is logged // as JSON to the audit log as the result of a revocation event. type certificateRevocationEvent struct { ID string `json:",omitempty"` // SerialNumber is the string representation of the revoked certificate's // serial number. SerialNumber string `json:",omitempty"` // Reason is the integer representing the revocation reason used. Reason int64 `json:"reason"` // Method is the way in which revocation was requested. // It will be one of the strings: "applicant", "subscriber", "control", "key", or "admin". Method string `json:",omitempty"` // RequesterID is the account ID of the requester. // Will be zero for admin revocations. RequesterID int64 `json:",omitempty"` // AdminName is the name of the admin requester. // Will be zero for subscriber revocations. AdminName string `json:",omitempty"` // Error contains any error encountered during revocation. Error string `json:",omitempty"` } // finalizationCAACheckEvent is a struct for holding information logged as JSON // to the info log as the result of an issuance event. It is logged when the RA // performs the final CAA check of a certificate finalization request. type finalizationCAACheckEvent struct { // Requester is the associated account ID. Requester int64 `json:",omitempty"` // Reused is a count of Authz where the original CAA check was performed in // the last 7 hours. Reused int `json:",omitempty"` // Rechecked is a count of Authz where a new CAA check was performed because // the original check was older than 7 hours. Rechecked int `json:",omitempty"` } // NewRegistration constructs a new Registration from a request. func (ra *RegistrationAuthorityImpl) NewRegistration(ctx context.Context, request *corepb.Registration) (*corepb.Registration, error) { // Error if the request is nil, there is no account key or IP address if request == nil || len(request.Key) == 0 { return nil, errIncompleteGRPCRequest } // Check if account key is acceptable for use. var key jose.JSONWebKey err := key.UnmarshalJSON(request.Key) if err != nil { return nil, berrors.InternalServerError("failed to unmarshal account key: %s", err.Error()) } err = ra.keyPolicy.GoodKey(ctx, key.Key) if err != nil { return nil, berrors.MalformedError("invalid public key: %s", err.Error()) } // Check that contacts conform to our expectations. err = validateContactsPresent(request.Contact, request.ContactsPresent) if err != nil { return nil, err } err = ra.validateContacts(request.Contact) if err != nil { return nil, err } // Don't populate ID or CreatedAt because those will be set by the SA. req := &corepb.Registration{ Key: request.Key, Contact: request.Contact, ContactsPresent: request.ContactsPresent, Agreement: request.Agreement, Status: string(core.StatusValid), } // Store the registration object, then return the version that got stored. res, err := ra.SA.NewRegistration(ctx, req) if err != nil { return nil, err } ra.newRegCounter.Inc() return res, nil } // validateContacts checks the provided list of contacts, returning an error if // any are not acceptable. Unacceptable contacts lists include: // * An empty list // * A list has more than maxContactsPerReg contacts // * A list containing an empty contact // * A list containing a contact that does not parse as a URL // * A list containing a contact that has a URL scheme other than mailto // * A list containing a mailto contact that contains hfields // * A list containing a contact that has non-ascii characters // * A list containing a contact that doesn't pass `policy.ValidEmail` func (ra *RegistrationAuthorityImpl) validateContacts(contacts []string) error { if len(contacts) == 0 { return nil // Nothing to validate } if ra.maxContactsPerReg > 0 && len(contacts) > ra.maxContactsPerReg { return berrors.MalformedError( "too many contacts provided: %d > %d", len(contacts), ra.maxContactsPerReg, ) } for _, contact := range contacts { if contact == "" { return berrors.InvalidEmailError("empty contact") } parsed, err := url.Parse(contact) if err != nil { return berrors.InvalidEmailError("invalid contact") } if parsed.Scheme != "mailto" { return berrors.UnsupportedContactError("only contact scheme 'mailto:' is supported") } if parsed.RawQuery != "" || contact[len(contact)-1] == '?' { return berrors.InvalidEmailError("contact email contains a question mark") } if parsed.Fragment != "" || contact[len(contact)-1] == '#' { return berrors.InvalidEmailError("contact email contains a '#'") } if !core.IsASCII(contact) { return berrors.InvalidEmailError("contact email contains non-ASCII characters") } err = policy.ValidEmail(parsed.Opaque) if err != nil { return err } } // NOTE(@cpu): For historical reasons (= maxContactBytes { return berrors.InvalidEmailError( "too many/too long contact(s). Please use shorter or fewer email addresses") } return nil } func (ra *RegistrationAuthorityImpl) checkPendingAuthorizationLimit(ctx context.Context, regID int64, limit ratelimit.RateLimitPolicy) error { // This rate limit's threshold can only be overridden on a per-regID basis, // not based on any other key. threshold, overrideKey := limit.GetThreshold("", regID) if threshold == -1 { return nil } countPB, err := ra.SA.CountPendingAuthorizations2(ctx, &sapb.RegistrationID{ Id: regID, }) if err != nil { return err } if countPB.Count >= threshold { ra.log.Infof("Rate limit exceeded, PendingAuthorizationsByRegID, regID: %d", regID) return berrors.RateLimitError(0, "too many currently pending authorizations: %d", countPB.Count) } if overrideKey != "" { utilization := float64(countPB.Count) / float64(threshold) ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.PendingAuthorizationsPerAccount, overrideKey).Set(utilization) } return nil } // checkInvalidAuthorizationLimits checks the failed validation limit for each // of the provided hostnames. It returns the first error. func (ra *RegistrationAuthorityImpl) checkInvalidAuthorizationLimits(ctx context.Context, regID int64, hostnames []string, limits ratelimit.RateLimitPolicy) error { results := make(chan error, len(hostnames)) for _, hostname := range hostnames { go func(hostname string) { results <- ra.checkInvalidAuthorizationLimit(ctx, regID, hostname, limits) }(hostname) } // We don't have to wait for all of the goroutines to finish because there's // enough capacity in the chan for them all to write their result even if // nothing is reading off the chan anymore. for range len(hostnames) { err := <-results if err != nil { return err } } return nil } func (ra *RegistrationAuthorityImpl) checkInvalidAuthorizationLimit(ctx context.Context, regID int64, hostname string, limit ratelimit.RateLimitPolicy) error { latest := ra.clk.Now().Add(ra.pendingAuthorizationLifetime) earliest := latest.Add(-limit.Window.Duration) req := &sapb.CountInvalidAuthorizationsRequest{ RegistrationID: regID, DnsName: hostname, Range: &sapb.Range{ Earliest: timestamppb.New(earliest), Latest: timestamppb.New(latest), }, } count, err := ra.SA.CountInvalidAuthorizations2(ctx, req) if err != nil { return err } // Most rate limits have a key for overrides, but there is no meaningful key // here. noKey := "" threshold, overrideKey := limit.GetThreshold(noKey, regID) if count.Count >= threshold { ra.log.Infof("Rate limit exceeded, InvalidAuthorizationsByRegID, regID: %d", regID) return berrors.FailedAuthorizationsPerDomainPerAccountError(0, "too many failed authorizations recently") } if overrideKey != "" { utilization := float64(count.Count) / float64(threshold) ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.InvalidAuthorizationsPerAccount, overrideKey).Set(utilization) } return nil } // checkNewOrdersPerAccountLimit enforces the rlPolicies `NewOrdersPerAccount` // rate limit. This rate limit ensures a client can not create more than the // specified threshold of new orders within the specified time window. func (ra *RegistrationAuthorityImpl) checkNewOrdersPerAccountLimit(ctx context.Context, acctID int64, limit ratelimit.RateLimitPolicy) error { now := ra.clk.Now() count, err := ra.SA.CountOrders(ctx, &sapb.CountOrdersRequest{ AccountID: acctID, Range: &sapb.Range{ Earliest: timestamppb.New(now.Add(-limit.Window.Duration)), Latest: timestamppb.New(now), }, }) if err != nil { return err } // There is no meaningful override key to use for this rate limit noKey := "" threshold, overrideKey := limit.GetThreshold(noKey, acctID) if count.Count >= threshold { return berrors.NewOrdersPerAccountError(0, "too many new orders recently") } if overrideKey != "" { utilization := float64(count.Count+1) / float64(threshold) ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.NewOrdersPerAccount, overrideKey).Set(utilization) } return nil } // matchesCSR tests the contents of a generated certificate to make sure // that the PublicKey, CommonName, and DNSNames match those provided in // the CSR that was used to generate the certificate. It also checks the // following fields for: // - notBefore is not more than 24 hours ago // - BasicConstraintsValid is true // - IsCA is false // - ExtKeyUsage only contains ExtKeyUsageServerAuth & ExtKeyUsageClientAuth // - Subject only contains CommonName & Names func (ra *RegistrationAuthorityImpl) matchesCSR(parsedCertificate *x509.Certificate, csr *x509.CertificateRequest) error { if !core.KeyDigestEquals(parsedCertificate.PublicKey, csr.PublicKey) { return berrors.InternalServerError("generated certificate public key doesn't match CSR public key") } csrNames := csrlib.NamesFromCSR(csr) if parsedCertificate.Subject.CommonName != "" { // Only check that the issued common name matches one of the SANs if there // is an issued CN at all: this allows flexibility on whether we include // the CN. if !slices.Contains(csrNames.SANs, parsedCertificate.Subject.CommonName) { return berrors.InternalServerError("generated certificate CommonName doesn't match any CSR name") } } parsedNames := parsedCertificate.DNSNames sort.Strings(parsedNames) if !slices.Equal(parsedNames, csrNames.SANs) { return berrors.InternalServerError("generated certificate DNSNames don't match CSR DNSNames") } if !slices.EqualFunc(parsedCertificate.IPAddresses, csr.IPAddresses, func(l, r net.IP) bool { return l.Equal(r) }) { return berrors.InternalServerError("generated certificate IPAddresses don't match CSR IPAddresses") } if !slices.Equal(parsedCertificate.EmailAddresses, csr.EmailAddresses) { return berrors.InternalServerError("generated certificate EmailAddresses don't match CSR EmailAddresses") } if len(parsedCertificate.Subject.Country) > 0 || len(parsedCertificate.Subject.Organization) > 0 || len(parsedCertificate.Subject.OrganizationalUnit) > 0 || len(parsedCertificate.Subject.Locality) > 0 || len(parsedCertificate.Subject.Province) > 0 || len(parsedCertificate.Subject.StreetAddress) > 0 || len(parsedCertificate.Subject.PostalCode) > 0 { return berrors.InternalServerError("generated certificate Subject contains fields other than CommonName, or SerialNumber") } now := ra.clk.Now() if now.Sub(parsedCertificate.NotBefore) > time.Hour*24 { return berrors.InternalServerError("generated certificate is back dated %s", now.Sub(parsedCertificate.NotBefore)) } if !parsedCertificate.BasicConstraintsValid { return berrors.InternalServerError("generated certificate doesn't have basic constraints set") } if parsedCertificate.IsCA { return berrors.InternalServerError("generated certificate can sign other certificates") } for _, eku := range parsedCertificate.ExtKeyUsage { if eku != x509.ExtKeyUsageServerAuth && eku != x509.ExtKeyUsageClientAuth { return berrors.InternalServerError("generated certificate has unacceptable EKU") } } if !slices.Contains(parsedCertificate.ExtKeyUsage, x509.ExtKeyUsageServerAuth) { return berrors.InternalServerError("generated certificate doesn't have serverAuth EKU") } return nil } // checkOrderAuthorizations verifies that a provided set of names associated // with a specific order and account has all of the required valid, unexpired // authorizations to proceed with issuance. It returns the authorizations that // satisfied the set of names or it returns an error. If it returns an error, it // will be of type BoulderError. func (ra *RegistrationAuthorityImpl) checkOrderAuthorizations( ctx context.Context, orderID orderID, acctID accountID, names []string, now time.Time) (map[identifier.ACMEIdentifier]*core.Authorization, error) { // Get all of the valid authorizations for this account/order req := &sapb.GetValidOrderAuthorizationsRequest{ Id: int64(orderID), AcctID: int64(acctID), } authzMapPB, err := ra.SA.GetValidOrderAuthorizations2(ctx, req) if err != nil { return nil, berrors.InternalServerError("error in GetValidOrderAuthorizations: %s", err) } authzs, err := bgrpc.PBToAuthzMap(authzMapPB) if err != nil { return nil, err } // Ensure that every identifier has a matching authz, and vice-versa. var missing []string var invalid []string var expired []string for _, name := range names { // TODO(#7647): Iterate directly over identifiers here, once the rest of the // finalization flow supports non-dnsName identifiers. ident := identifier.NewDNS(name) authz, ok := authzs[ident] if !ok || authz == nil { missing = append(missing, ident.Value) continue } if authz.Status != core.StatusValid { invalid = append(invalid, ident.Value) continue } if authz.Expires.Before(now) { expired = append(expired, ident.Value) continue } err = ra.PA.CheckAuthzChallenges(authz) if err != nil { invalid = append(invalid, ident.Value) continue } } if len(missing) > 0 { return nil, berrors.UnauthorizedError( "authorizations for these identifiers not found: %s", strings.Join(missing, ", "), ) } if len(invalid) > 0 { return nil, berrors.UnauthorizedError( "authorizations for these identifiers not valid: %s", strings.Join(invalid, ", "), ) } if len(expired) > 0 { return nil, berrors.UnauthorizedError( "authorizations for these identifiers expired: %s", strings.Join(expired, ", "), ) } // Even though this check is cheap, we do it after the more specific checks // so that we can return more specific error messages. if len(names) != len(authzs) { return nil, berrors.UnauthorizedError("incorrect number of names requested for finalization") } // Check that the authzs either don't need CAA rechecking, or do the // necessary CAA rechecks right now. err = ra.checkAuthorizationsCAA(ctx, int64(acctID), authzs, now) if err != nil { return nil, err } return authzs, nil } // validatedBefore checks if a given authorization's challenge was // validated before a given time. Returns a bool. func validatedBefore(authz *core.Authorization, caaRecheckTime time.Time) (bool, error) { numChallenges := len(authz.Challenges) if numChallenges != 1 { return false, berrors.InternalServerError("authorization has incorrect number of challenges. 1 expected, %d found for: id %s", numChallenges, authz.ID) } if authz.Challenges[0].Validated == nil { return false, berrors.InternalServerError("authorization's challenge has no validated timestamp for: id %s", authz.ID) } return authz.Challenges[0].Validated.Before(caaRecheckTime), nil } // checkAuthorizationsCAA ensures that we have sufficiently-recent CAA checks // for every input identifier/authz. If any authz was validated too long ago, it // kicks off a CAA recheck for that identifier If it returns an error, it will // be of type BoulderError. func (ra *RegistrationAuthorityImpl) checkAuthorizationsCAA( ctx context.Context, acctID int64, authzs map[identifier.ACMEIdentifier]*core.Authorization, now time.Time) error { // recheckAuthzs is a list of authorizations that must have their CAA records rechecked var recheckAuthzs []*core.Authorization // Per Baseline Requirements, CAA must be checked within 8 hours of // issuance. CAA is checked when an authorization is validated, so as // long as that was less than 8 hours ago, we're fine. We recheck if // that was more than 7 hours ago, to be on the safe side. We can // check to see if the authorized challenge `AttemptedAt` // (`Validated`) value from the database is before our caaRecheckTime. // Set the recheck time to 7 hours ago. caaRecheckAfter := now.Add(caaRecheckDuration) for _, authz := range authzs { if staleCAA, err := validatedBefore(authz, caaRecheckAfter); err != nil { return err } else if staleCAA { // Ensure that CAA is rechecked for this name recheckAuthzs = append(recheckAuthzs, authz) } } if len(recheckAuthzs) > 0 { err := ra.recheckCAA(ctx, recheckAuthzs) if err != nil { return err } } caaEvent := &finalizationCAACheckEvent{ Requester: acctID, Reused: len(authzs) - len(recheckAuthzs), Rechecked: len(recheckAuthzs), } ra.log.InfoObject("FinalizationCaaCheck", caaEvent) return nil } // recheckCAA accepts a list of names that need to have their CAA records // rechecked because their associated authorizations are sufficiently old and // performs the CAA checks required for each. If any of the rechecks fail an // error is returned. func (ra *RegistrationAuthorityImpl) recheckCAA(ctx context.Context, authzs []*core.Authorization) error { ra.recheckCAACounter.Add(float64(len(authzs))) type authzCAAResult struct { authz *core.Authorization err error } ch := make(chan authzCAAResult, len(authzs)) for _, authz := range authzs { go func(authz *core.Authorization) { name := authz.Identifier.Value // If an authorization has multiple valid challenges, // the type of the first valid challenge is used for // the purposes of CAA rechecking. var method string for _, challenge := range authz.Challenges { if challenge.Status == core.StatusValid { method = string(challenge.Type) break } } if method == "" { ch <- authzCAAResult{ authz: authz, err: berrors.InternalServerError( "Internal error determining validation method for authorization ID %v (%v)", authz.ID, name), } return } var resp *vapb.IsCAAValidResponse var err error if !features.Get().EnforceMPIC { resp, err = ra.VA.IsCAAValid(ctx, &vapb.IsCAAValidRequest{ Domain: name, ValidationMethod: method, AccountURIID: authz.RegistrationID, }) } else { resp, err = ra.VA.DoCAA(ctx, &vapb.IsCAAValidRequest{ Domain: name, ValidationMethod: method, AccountURIID: authz.RegistrationID, }) } if err != nil { ra.log.AuditErrf("Rechecking CAA: %s", err) err = berrors.InternalServerError( "Internal error rechecking CAA for authorization ID %v (%v)", authz.ID, name, ) } else if resp.Problem != nil { err = berrors.CAAError("rechecking caa: %s", resp.Problem.Detail) } ch <- authzCAAResult{ authz: authz, err: err, } }(authz) } var subErrors []berrors.SubBoulderError // Read a recheckResult for each authz from the results channel for range len(authzs) { recheckResult := <-ch // If the result had a CAA boulder error, construct a suberror with the // identifier from the authorization that was checked. err := recheckResult.err if err != nil { var bErr *berrors.BoulderError if errors.As(err, &bErr) && bErr.Type == berrors.CAA { subErrors = append(subErrors, berrors.SubBoulderError{ Identifier: recheckResult.authz.Identifier, BoulderError: bErr}) } else { return err } } } if len(subErrors) > 0 { var detail string // If there was only one error, then use it as the top level error that is // returned. if len(subErrors) == 1 { return subErrors[0].BoulderError } detail = fmt.Sprintf( "Rechecking CAA for %q and %d more identifiers failed. "+ "Refer to sub-problems for more information", subErrors[0].Identifier.Value, len(subErrors)-1) return (&berrors.BoulderError{ Type: berrors.CAA, Detail: detail, }).WithSubErrors(subErrors) } return nil } // failOrder marks an order as failed by setting the problem details field of // the order & persisting it through the SA. If an error occurs doing this we // log it and don't modify the input order. There aren't any alternatives if we // can't add the error to the order. This function MUST only be called when we // are already returning an error for another reason. func (ra *RegistrationAuthorityImpl) failOrder( ctx context.Context, order *corepb.Order, prob *probs.ProblemDetails) { // Use a separate context with its own timeout, since the error we encountered // may have been a context cancellation or timeout, and these operations still // need to succeed. ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 1*time.Second) defer cancel() // Convert the problem to a protobuf problem for the *corepb.Order field pbProb, err := bgrpc.ProblemDetailsToPB(prob) if err != nil { ra.log.AuditErrf("Could not convert order error problem to PB: %q", err) return } // Assign the protobuf problem to the field and save it via the SA order.Error = pbProb _, err = ra.SA.SetOrderError(ctx, &sapb.SetOrderErrorRequest{ Id: order.Id, Error: order.Error, }) if err != nil { ra.log.AuditErrf("Could not persist order error: %q", err) } } // To help minimize the chance that an accountID would be used as an order ID // (or vice versa) when calling functions that use both we define internal // `accountID` and `orderID` types so that callers must explicitly cast. type accountID int64 type orderID int64 // FinalizeOrder accepts a request to finalize an order object and, if possible, // issues a certificate to satisfy the order. If an order does not have valid, // unexpired authorizations for all of its associated names an error is // returned. Similarly we vet that all of the names in the order are acceptable // based on current policy and return an error if the order can't be fulfilled. // If successful the order will be returned in processing status for the client // to poll while awaiting finalization to occur. func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rapb.FinalizeOrderRequest) (*corepb.Order, error) { // Step 1: Set up logging/tracing and validate the Order if req == nil || req.Order == nil || len(req.Csr) == 0 { return nil, errIncompleteGRPCRequest } logEvent := certificateRequestEvent{ ID: core.NewToken(), OrderID: req.Order.Id, Requester: req.Order.RegistrationID, RequestTime: ra.clk.Now(), } csr, err := ra.validateFinalizeRequest(ctx, req, &logEvent) if err != nil { return nil, err } // Observe the age of this order, so we know how quickly most clients complete // issuance flows. ra.orderAges.WithLabelValues("FinalizeOrder").Observe(ra.clk.Since(req.Order.Created.AsTime()).Seconds()) // Step 2: Set the Order to Processing status // // We do this separately from the issuance process itself so that, when we // switch to doing issuance asynchronously, we aren't lying to the client // when we say that their order is already Processing. // // NOTE(@cpu): After this point any errors that are encountered must update // the state of the order to invalid by setting the order's error field. // Otherwise the order will be "stuck" in processing state. It can not be // finalized because it isn't pending, but we aren't going to process it // further because we already did and encountered an error. _, err = ra.SA.SetOrderProcessing(ctx, &sapb.OrderRequest{Id: req.Order.Id}) if err != nil { // Fail the order with a server internal error - we weren't able to set the // status to processing and that's unexpected & weird. ra.failOrder(ctx, req.Order, probs.ServerInternal("Error setting order processing")) return nil, err } // Update the order status locally since the SA doesn't return the updated // order itself after setting the status order := req.Order order.Status = string(core.StatusProcessing) // Steps 3 (issuance) and 4 (cleanup) are done inside a helper function so // that we can control whether or not that work happens asynchronously. if features.Get().AsyncFinalize { // We do this work in a goroutine so that we can better handle latency from // getting SCTs and writing the (pre)certificate to the database. This lets // us return the order in the Processing state to the client immediately, // prompting them to poll the Order object and wait for it to be put into // its final state. // // We track this goroutine's lifetime in a waitgroup global to this RA, so // that it can wait for all goroutines to drain during shutdown. ra.drainWG.Add(1) go func() { _, err := ra.issueCertificateOuter(ctx, proto.Clone(order).(*corepb.Order), csr, logEvent) if err != nil { // We only log here, because this is in a background goroutine with // no parent goroutine waiting for it to receive the error. ra.log.AuditErrf("Asynchronous finalization failed: %s", err.Error()) } ra.drainWG.Done() }() return order, nil } else { return ra.issueCertificateOuter(ctx, order, csr, logEvent) } } // validateFinalizeRequest checks that a FinalizeOrder request is fully correct // and ready for issuance. func (ra *RegistrationAuthorityImpl) validateFinalizeRequest( ctx context.Context, req *rapb.FinalizeOrderRequest, logEvent *certificateRequestEvent) (*x509.CertificateRequest, error) { if req.Order.Id <= 0 { return nil, berrors.MalformedError("invalid order ID: %d", req.Order.Id) } if req.Order.RegistrationID <= 0 { return nil, berrors.MalformedError("invalid account ID: %d", req.Order.RegistrationID) } if core.AcmeStatus(req.Order.Status) != core.StatusReady { return nil, berrors.OrderNotReadyError( "Order's status (%q) is not acceptable for finalization", req.Order.Status) } // There should never be an order with 0 names at the stage, but we check to // be on the safe side, throwing an internal server error if this assumption // is ever violated. if len(req.Order.DnsNames) == 0 { return nil, berrors.InternalServerError("Order has no associated names") } // Parse the CSR from the request csr, err := x509.ParseCertificateRequest(req.Csr) if err != nil { return nil, berrors.BadCSRError("unable to parse CSR: %s", err.Error()) } err = csrlib.VerifyCSR(ctx, csr, ra.maxNames, &ra.keyPolicy, ra.PA) if err != nil { // VerifyCSR returns berror instances that can be passed through as-is // without wrapping. return nil, err } // Dedupe, lowercase and sort both the names from the CSR and the names in the // order. csrNames := csrlib.NamesFromCSR(csr).SANs orderNames := core.UniqueLowerNames(req.Order.DnsNames) // Check that the order names and the CSR names are an exact match if !slices.Equal(csrNames, orderNames) { return nil, berrors.UnauthorizedError(("CSR does not specify same identifiers as Order")) } // Get the originating account for use in the next check. regPB, err := ra.SA.GetRegistration(ctx, &sapb.RegistrationID{Id: req.Order.RegistrationID}) if err != nil { return nil, err } account, err := bgrpc.PbToRegistration(regPB) if err != nil { return nil, err } // Make sure they're not using their account key as the certificate key too. if core.KeyDigestEquals(csr.PublicKey, account.Key) { return nil, berrors.MalformedError("certificate public key must be different than account key") } // Double-check that all authorizations on this order are valid, are also // associated with the same account as the order itself, and have recent CAA. authzs, err := ra.checkOrderAuthorizations( ctx, orderID(req.Order.Id), accountID(req.Order.RegistrationID), csrNames, ra.clk.Now()) if err != nil { // Pass through the error without wrapping it because the called functions // return BoulderError and we don't want to lose the type. return nil, err } // Collect up a certificateRequestAuthz that stores the ID and challenge type // of each of the valid authorizations we used for this issuance. logEventAuthzs := make(map[string]certificateRequestAuthz, len(csrNames)) for _, authz := range authzs { // No need to check for error here because we know this same call just // succeeded inside ra.checkOrderAuthorizations solvedByChallengeType, _ := authz.SolvedBy() logEventAuthzs[authz.Identifier.Value] = certificateRequestAuthz{ ID: authz.ID, ChallengeType: solvedByChallengeType, } authzAge := (ra.authorizationLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds() ra.authzAges.WithLabelValues("FinalizeOrder", string(authz.Status)).Observe(authzAge) } logEvent.Authorizations = logEventAuthzs // Mark that we verified the CN and SANs logEvent.VerifiedFields = []string{"subject.commonName", "subjectAltName"} return csr, nil } // issueCertificateOuter exists solely to ensure that all calls to // issueCertificateInner have their result handled uniformly, no matter what // return path that inner function takes. It takes ownership of the logEvent, // mutates it, and is responsible for outputting its final state. func (ra *RegistrationAuthorityImpl) issueCertificateOuter( ctx context.Context, order *corepb.Order, csr *x509.CertificateRequest, logEvent certificateRequestEvent, ) (*corepb.Order, error) { ra.inflightFinalizes.Inc() defer ra.inflightFinalizes.Dec() // Step 3: Issue the Certificate cert, cpId, err := ra.issueCertificateInner( ctx, csr, order.CertificateProfileName, accountID(order.RegistrationID), orderID(order.Id)) // Step 4: Fail the order if necessary, and update metrics and log fields var result string if err != nil { // The problem is computed using `web.ProblemDetailsForError`, the same // function the WFE uses to convert between `berrors` and problems. This // will turn normal expected berrors like berrors.UnauthorizedError into the // correct `urn:ietf:params:acme:error:unauthorized` problem while not // letting anything like a server internal error through with sensitive // info. ra.failOrder(ctx, order, web.ProblemDetailsForError(err, "Error finalizing order")) order.Status = string(core.StatusInvalid) logEvent.Error = err.Error() result = "error" } else { order.CertificateSerial = core.SerialToString(cert.SerialNumber) order.Status = string(core.StatusValid) ra.namesPerCert.With( prometheus.Labels{"type": "issued"}, ).Observe(float64(len(order.DnsNames))) ra.newCertCounter.With( prometheus.Labels{ "profileName": cpId.name, "profileHash": hex.EncodeToString(cpId.hash), }).Inc() logEvent.SerialNumber = core.SerialToString(cert.SerialNumber) logEvent.CommonName = cert.Subject.CommonName logEvent.Names = cert.DNSNames logEvent.NotBefore = cert.NotBefore logEvent.NotAfter = cert.NotAfter logEvent.CertProfileName = cpId.name logEvent.CertProfileHash = hex.EncodeToString(cpId.hash) result = "successful" } logEvent.ResponseTime = ra.clk.Now() ra.log.AuditObject(fmt.Sprintf("Certificate request - %s", result), logEvent) return order, err } // countCertificateIssued increments the certificates (per domain and per // account) and duplicate certificate rate limits. There is no reason to surface // errors from this function to the Subscriber, spends against these limit are // best effort. func (ra *RegistrationAuthorityImpl) countCertificateIssued(ctx context.Context, regId int64, orderDomains []string, isRenewal bool) { if ra.limiter == nil || ra.txnBuilder == nil { // Limiter is disabled. return } var transactions []ratelimits.Transaction if !isRenewal { txns, err := ra.txnBuilder.CertificatesPerDomainSpendOnlyTransactions(regId, orderDomains) if err != nil { ra.log.Warningf("building rate limit transactions at finalize: %s", err) } transactions = append(transactions, txns...) } txn, err := ra.txnBuilder.CertificatesPerFQDNSetSpendOnlyTransaction(orderDomains) if err != nil { ra.log.Warningf("building rate limit transaction at finalize: %s", err) } transactions = append(transactions, txn) _, err = ra.limiter.BatchSpend(ctx, transactions) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return } ra.log.Warningf("spending against rate limits at finalize: %s", err) } } // certProfileID contains the name and hash of a certificate profile returned by // a CA. type certProfileID struct { name string hash []byte } // issueCertificateInner is part of the [issuance cycle]. // // It gets a precertificate from the CA, submits it to CT logs to get SCTs, // then sends the precertificate and the SCTs to the CA to get a final certificate. // // This function is responsible for ensuring that we never try to issue a final // certificate twice for the same precertificate, because that has the potential // to create certificates with duplicate serials. For instance, this could // happen if final certificates were created with different sets of SCTs. This // function accomplishes that by bailing on issuance if there is any error in // IssueCertificateForPrecertificate; there are no retries, and serials are // generated in IssuePrecertificate, so serials with errors are dropped and // never have final certificates issued for them (because there is a possibility // that the certificate was actually issued but there was an error returning // it). // // [issuance cycle]: https://github.com/letsencrypt/boulder/blob/main/docs/ISSUANCE-CYCLE.md func (ra *RegistrationAuthorityImpl) issueCertificateInner( ctx context.Context, csr *x509.CertificateRequest, profileName string, acctID accountID, oID orderID) (*x509.Certificate, *certProfileID, error) { if features.Get().AsyncFinalize { // If we're in async mode, use a context with a much longer timeout. var cancel func() ctx, cancel = context.WithTimeout(context.WithoutCancel(ctx), ra.finalizeTimeout) defer cancel() } // wrapError adds a prefix to an error. If the error is a boulder error then // the problem detail is updated with the prefix. Otherwise a new error is // returned with the message prefixed using `fmt.Errorf` wrapError := func(e error, prefix string) error { if berr, ok := e.(*berrors.BoulderError); ok { berr.Detail = fmt.Sprintf("%s: %s", prefix, berr.Detail) return berr } return fmt.Errorf("%s: %s", prefix, e) } issueReq := &capb.IssueCertificateRequest{ Csr: csr.Raw, RegistrationID: int64(acctID), OrderID: int64(oID), CertProfileName: profileName, } // Once we get a precert from IssuePrecertificate, we must attempt issuing // a final certificate at most once. We achieve that by bailing on any error // between here and IssueCertificateForPrecertificate. precert, err := ra.CA.IssuePrecertificate(ctx, issueReq) if err != nil { return nil, nil, wrapError(err, "issuing precertificate") } parsedPrecert, err := x509.ParseCertificate(precert.DER) if err != nil { return nil, nil, wrapError(err, "parsing precertificate") } scts, err := ra.getSCTs(ctx, precert.DER, parsedPrecert.NotAfter) if err != nil { return nil, nil, wrapError(err, "getting SCTs") } exists, err := ra.SA.FQDNSetExists(ctx, &sapb.FQDNSetExistsRequest{DnsNames: parsedPrecert.DNSNames}) if err != nil { return nil, nil, wrapError(err, "checking if certificate is a renewal") } isRenewal := exists.Exists cert, err := ra.CA.IssueCertificateForPrecertificate(ctx, &capb.IssueCertificateForPrecertificateRequest{ DER: precert.DER, SCTs: scts, RegistrationID: int64(acctID), OrderID: int64(oID), CertProfileHash: precert.CertProfileHash, }) if err != nil { return nil, nil, wrapError(err, "issuing certificate for precertificate") } parsedCertificate, err := x509.ParseCertificate(cert.Der) if err != nil { return nil, nil, wrapError(err, "parsing final certificate") } ra.countCertificateIssued(ctx, int64(acctID), slices.Clone(parsedCertificate.DNSNames), isRenewal) // Asynchronously submit the final certificate to any configured logs go ra.ctpolicy.SubmitFinalCert(cert.Der, parsedCertificate.NotAfter) err = ra.matchesCSR(parsedCertificate, csr) if err != nil { ra.certCSRMismatch.Inc() return nil, nil, err } _, err = ra.SA.FinalizeOrder(ctx, &sapb.FinalizeOrderRequest{ Id: int64(oID), CertificateSerial: core.SerialToString(parsedCertificate.SerialNumber), }) if err != nil { return nil, nil, wrapError(err, "persisting finalized order") } return parsedCertificate, &certProfileID{name: precert.CertProfileName, hash: precert.CertProfileHash}, nil } func (ra *RegistrationAuthorityImpl) getSCTs(ctx context.Context, cert []byte, expiration time.Time) (core.SCTDERs, error) { started := ra.clk.Now() scts, err := ra.ctpolicy.GetSCTs(ctx, cert, expiration) took := ra.clk.Since(started) if err != nil { state := "failure" if err == context.DeadlineExceeded { state = "deadlineExceeded" // Convert the error to a missingSCTsError to communicate the timeout, // otherwise it will be a generic serverInternalError err = berrors.MissingSCTsError("failed to get SCTs: %s", err.Error()) } ra.log.Warningf("ctpolicy.GetSCTs failed: %s", err) ra.ctpolicyResults.With(prometheus.Labels{"result": state}).Observe(took.Seconds()) return nil, err } ra.ctpolicyResults.With(prometheus.Labels{"result": "success"}).Observe(took.Seconds()) return scts, nil } // enforceNameCounts uses the provided count RPC to find a count of certificates // for each of the names. If the count for any of the names exceeds the limit // for the given registration then the names out of policy are returned to be // used for a rate limit error. func (ra *RegistrationAuthorityImpl) enforceNameCounts(ctx context.Context, names []string, limit ratelimit.RateLimitPolicy, regID int64) ([]string, time.Time, error) { now := ra.clk.Now() req := &sapb.CountCertificatesByNamesRequest{ DnsNames: names, Range: &sapb.Range{ Earliest: timestamppb.New(limit.WindowBegin(now)), Latest: timestamppb.New(now), }, } response, err := ra.SA.CountCertificatesByNames(ctx, req) if err != nil { return nil, time.Time{}, err } if len(response.Counts) == 0 { return nil, time.Time{}, errIncompleteGRPCResponse } var badNames []string var metricsData []struct { overrideKey string utilization float64 } // Find the names that have counts at or over the threshold. Range // over the names slice input to ensure the order of badNames will // return the badNames in the same order they were input. for _, name := range names { threshold, overrideKey := limit.GetThreshold(name, regID) if response.Counts[name] >= threshold { badNames = append(badNames, name) } if overrideKey != "" { // Name is under threshold due to an override. utilization := float64(response.Counts[name]+1) / float64(threshold) metricsData = append(metricsData, struct { overrideKey string utilization float64 }{overrideKey, utilization}) } } if len(badNames) == 0 { // All names were under the threshold, emit override utilization metrics. for _, data := range metricsData { ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerName, data.overrideKey).Set(data.utilization) } } return badNames, response.Earliest.AsTime(), nil } func (ra *RegistrationAuthorityImpl) checkCertificatesPerNameLimit(ctx context.Context, names []string, limit ratelimit.RateLimitPolicy, regID int64) error { tldNames := ratelimits.FQDNsToETLDsPlusOne(names) namesOutOfLimit, earliest, err := ra.enforceNameCounts(ctx, tldNames, limit, regID) if err != nil { return fmt.Errorf("checking certificates per name limit for %q: %s", names, err) } if len(namesOutOfLimit) > 0 { // Determine the amount of time until the earliest event would fall out // of the window. retryAfter := earliest.Add(limit.Window.Duration).Sub(ra.clk.Now()) retryString := earliest.Add(limit.Window.Duration).Format(time.RFC3339) ra.log.Infof("Rate limit exceeded, CertificatesForDomain, regID: %d, domains: %s", regID, strings.Join(namesOutOfLimit, ", ")) if len(namesOutOfLimit) > 1 { var subErrors []berrors.SubBoulderError for _, name := range namesOutOfLimit { subErrors = append(subErrors, berrors.SubBoulderError{ Identifier: identifier.NewDNS(name), BoulderError: berrors.NewOrdersPerAccountError(retryAfter, "too many certificates already issued. Retry after %s", retryString).(*berrors.BoulderError), }) } return berrors.NewOrdersPerAccountError(retryAfter, "too many certificates already issued for multiple names (%q and %d others). Retry after %s", namesOutOfLimit[0], len(namesOutOfLimit), retryString).(*berrors.BoulderError).WithSubErrors(subErrors) } return berrors.NewOrdersPerAccountError(retryAfter, "too many certificates already issued for %q. Retry after %s", namesOutOfLimit[0], retryString) } return nil } func (ra *RegistrationAuthorityImpl) checkCertificatesPerFQDNSetLimit(ctx context.Context, names []string, limit ratelimit.RateLimitPolicy, regID int64) error { names = core.UniqueLowerNames(names) threshold, overrideKey := limit.GetThreshold(strings.Join(names, ","), regID) if threshold <= 0 { // No limit configured. return nil } prevIssuances, err := ra.SA.FQDNSetTimestampsForWindow(ctx, &sapb.CountFQDNSetsRequest{ DnsNames: names, Window: durationpb.New(limit.Window.Duration), }) if err != nil { return fmt.Errorf("checking duplicate certificate limit for %q: %s", names, err) } if overrideKey != "" { utilization := float64(len(prevIssuances.Timestamps)) / float64(threshold) ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerFQDNSet, overrideKey).Set(utilization) } issuanceCount := int64(len(prevIssuances.Timestamps)) if issuanceCount < threshold { // Issuance in window is below the threshold, no need to limit. if overrideKey != "" { utilization := float64(issuanceCount+1) / float64(threshold) ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerFQDNSet, overrideKey).Set(utilization) } return nil } else { // Evaluate the rate limit using a token bucket algorithm. The bucket // has a capacity of threshold and is refilled at a rate of 1 token per // limit.Window/threshold from the time of each issuance timestamp. The // timestamps start from the most recent issuance and go back in time. now := ra.clk.Now() nsPerToken := limit.Window.Nanoseconds() / threshold for i, timestamp := range prevIssuances.Timestamps { tokensGeneratedSince := now.Add(-time.Duration(int64(i+1) * nsPerToken)) if timestamp.AsTime().Before(tokensGeneratedSince) { // We know `i+1` tokens were generated since `tokenGeneratedSince`, // and only `i` certificates were issued, so there's room to allow // for an additional issuance. if overrideKey != "" { utilization := float64(issuanceCount) / float64(threshold) ra.rlOverrideUsageGauge.WithLabelValues(ratelimit.CertificatesPerFQDNSet, overrideKey).Set(utilization) } return nil } } retryTime := prevIssuances.Timestamps[0].AsTime().Add(time.Duration(nsPerToken)) retryAfter := retryTime.Sub(now) return berrors.CertificatesPerFQDNSetError( retryAfter, "too many certificates (%d) already issued for this exact set of domains in the last %.0f hours: %s, retry after %s", threshold, limit.Window.Duration.Hours(), strings.Join(names, ","), retryTime.Format(time.RFC3339), ) } } func (ra *RegistrationAuthorityImpl) checkNewOrderLimits(ctx context.Context, names []string, regID int64, isRenewal bool) error { newOrdersPerAccountLimits := ra.rlPolicies.NewOrdersPerAccount() if newOrdersPerAccountLimits.Enabled() && !isRenewal { started := ra.clk.Now() err := ra.checkNewOrdersPerAccountLimit(ctx, regID, newOrdersPerAccountLimits) elapsed := ra.clk.Since(started) if err != nil { if errors.Is(err, berrors.RateLimit) { ra.rlCheckLatency.WithLabelValues(ratelimit.NewOrdersPerAccount, ratelimits.Denied).Observe(elapsed.Seconds()) } return err } ra.rlCheckLatency.WithLabelValues(ratelimit.NewOrdersPerAccount, ratelimits.Allowed).Observe(elapsed.Seconds()) } certNameLimits := ra.rlPolicies.CertificatesPerName() if certNameLimits.Enabled() && !isRenewal { started := ra.clk.Now() err := ra.checkCertificatesPerNameLimit(ctx, names, certNameLimits, regID) elapsed := ra.clk.Since(started) if err != nil { if errors.Is(err, berrors.RateLimit) { ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerName, ratelimits.Denied).Observe(elapsed.Seconds()) } return err } ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerName, ratelimits.Allowed).Observe(elapsed.Seconds()) } fqdnLimitsFast := ra.rlPolicies.CertificatesPerFQDNSetFast() if fqdnLimitsFast.Enabled() { started := ra.clk.Now() err := ra.checkCertificatesPerFQDNSetLimit(ctx, names, fqdnLimitsFast, regID) elapsed := ra.clk.Since(started) if err != nil { if errors.Is(err, berrors.RateLimit) { ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerFQDNSetFast, ratelimits.Denied).Observe(elapsed.Seconds()) } return err } ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerFQDNSetFast, ratelimits.Allowed).Observe(elapsed.Seconds()) } fqdnLimits := ra.rlPolicies.CertificatesPerFQDNSet() if fqdnLimits.Enabled() { started := ra.clk.Now() err := ra.checkCertificatesPerFQDNSetLimit(ctx, names, fqdnLimits, regID) elapsed := ra.clk.Since(started) if err != nil { if errors.Is(err, berrors.RateLimit) { ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerFQDNSet, ratelimits.Denied).Observe(elapsed.Seconds()) } return err } ra.rlCheckLatency.WithLabelValues(ratelimit.CertificatesPerFQDNSet, ratelimits.Allowed).Observe(elapsed.Seconds()) } invalidAuthzPerAccountLimits := ra.rlPolicies.InvalidAuthorizationsPerAccount() if invalidAuthzPerAccountLimits.Enabled() { started := ra.clk.Now() err := ra.checkInvalidAuthorizationLimits(ctx, regID, names, invalidAuthzPerAccountLimits) elapsed := ra.clk.Since(started) if err != nil { if errors.Is(err, berrors.RateLimit) { ra.rlCheckLatency.WithLabelValues(ratelimit.InvalidAuthorizationsPerAccount, ratelimits.Denied).Observe(elapsed.Seconds()) } return err } ra.rlCheckLatency.WithLabelValues(ratelimit.InvalidAuthorizationsPerAccount, ratelimits.Allowed).Observe(elapsed.Seconds()) } return nil } // UpdateRegistration updates an existing Registration with new values. Caller // is responsible for making sure that update.Key is only different from base.Key // if it is being called from the WFE key change endpoint. // // Deprecated: Use UpdateRegistrationContact or UpdateRegistrationKey instead. func (ra *RegistrationAuthorityImpl) UpdateRegistration(ctx context.Context, req *rapb.UpdateRegistrationRequest) (*corepb.Registration, error) { // Error if the request is nil, there is no account key or IP address if req.Base == nil || len(req.Base.Key) == 0 || req.Base.Id == 0 { return nil, errIncompleteGRPCRequest } err := validateContactsPresent(req.Base.Contact, req.Base.ContactsPresent) if err != nil { return nil, err } err = validateContactsPresent(req.Update.Contact, req.Update.ContactsPresent) if err != nil { return nil, err } err = ra.validateContacts(req.Update.Contact) if err != nil { return nil, err } update, changed := mergeUpdate(req.Base, req.Update) if !changed { // If merging the update didn't actually change the base then our work is // done, we can return before calling ra.SA.UpdateRegistration since there's // nothing for the SA to do return req.Base, nil } _, err = ra.SA.UpdateRegistration(ctx, update) if err != nil { // berrors.InternalServerError since the user-data was validated before being // passed to the SA. err = berrors.InternalServerError("Could not update registration: %s", err) return nil, err } return update, nil } // UpdateRegistrationContact updates an existing Registration's contact. // The updated contacts field may be empty. func (ra *RegistrationAuthorityImpl) UpdateRegistrationContact(ctx context.Context, req *rapb.UpdateRegistrationContactRequest) (*corepb.Registration, error) { if core.IsAnyNilOrZero(req.RegistrationID) { return nil, errIncompleteGRPCRequest } err := ra.validateContacts(req.Contacts) if err != nil { return nil, fmt.Errorf("invalid contact: %w", err) } update, err := ra.SA.UpdateRegistrationContact(ctx, &sapb.UpdateRegistrationContactRequest{ RegistrationID: req.RegistrationID, Contacts: req.Contacts, }) if err != nil { return nil, fmt.Errorf("failed to update registration contact: %w", err) } return update, nil } // UpdateRegistrationKey updates an existing Registration's key. func (ra *RegistrationAuthorityImpl) UpdateRegistrationKey(ctx context.Context, req *rapb.UpdateRegistrationKeyRequest) (*corepb.Registration, error) { if core.IsAnyNilOrZero(req.RegistrationID, req.Jwk) { return nil, errIncompleteGRPCRequest } update, err := ra.SA.UpdateRegistrationKey(ctx, &sapb.UpdateRegistrationKeyRequest{ RegistrationID: req.RegistrationID, Jwk: req.Jwk, }) if err != nil { return nil, fmt.Errorf("failed to update registration key: %w", err) } return update, nil } func contactsEqual(a []string, b []string) bool { if len(a) != len(b) { return false } // If there is an existing contact slice and it has the same length as the // new contact slice we need to look at each contact to determine if there // is a change being made. Use `sort.Strings` here to ensure a consistent // comparison sort.Strings(a) sort.Strings(b) for i := range len(b) { // If the contact's string representation differs at any index they aren't // equal if a[i] != b[i] { return false } } // They are equal! return true } // MergeUpdate returns a new corepb.Registration with the majority of its fields // copies from the base Registration, and a subset (Contact, Agreement, and Key) // copied from the update Registration. It also returns a boolean indicating // whether or not this operation resulted in a Registration which differs from // the base. func mergeUpdate(base *corepb.Registration, update *corepb.Registration) (*corepb.Registration, bool) { var changed bool // Start by copying all of the fields. res := &corepb.Registration{ Id: base.Id, Key: base.Key, Contact: base.Contact, ContactsPresent: base.ContactsPresent, Agreement: base.Agreement, CreatedAt: base.CreatedAt, Status: base.Status, } // Note: we allow update.Contact to overwrite base.Contact even if the former // is empty in order to allow users to remove the contact associated with // a registration. If the update has ContactsPresent set to false, then we // know it is not attempting to update the contacts field. if update.ContactsPresent && !contactsEqual(base.Contact, update.Contact) { res.Contact = update.Contact res.ContactsPresent = update.ContactsPresent changed = true } if len(update.Agreement) > 0 && update.Agreement != base.Agreement { res.Agreement = update.Agreement changed = true } if len(update.Key) > 0 { if len(update.Key) != len(base.Key) { res.Key = update.Key changed = true } else { for i := range len(base.Key) { if update.Key[i] != base.Key[i] { res.Key = update.Key changed = true break } } } } return res, changed } // recordValidation records an authorization validation event, // it should only be used on v2 style authorizations. func (ra *RegistrationAuthorityImpl) recordValidation(ctx context.Context, authID string, authExpires *time.Time, challenge *core.Challenge) error { authzID, err := strconv.ParseInt(authID, 10, 64) if err != nil { return err } var expires time.Time if challenge.Status == core.StatusInvalid { expires = *authExpires } else { expires = ra.clk.Now().Add(ra.authorizationLifetime) } vr, err := bgrpc.ValidationResultToPB(challenge.ValidationRecord, challenge.Error, "", "") if err != nil { return err } var validated *timestamppb.Timestamp if challenge.Validated != nil { validated = timestamppb.New(*challenge.Validated) } _, err = ra.SA.FinalizeAuthorization2(ctx, &sapb.FinalizeAuthorizationRequest{ Id: authzID, Status: string(challenge.Status), Expires: timestamppb.New(expires), Attempted: string(challenge.Type), AttemptedAt: validated, ValidationRecords: vr.Records, ValidationError: vr.Problem, }) return err } // countFailedValidations increments the FailedAuthorizationsPerDomainPerAccount limit. // and the FailedAuthorizationsForPausingPerDomainPerAccountTransaction limit. func (ra *RegistrationAuthorityImpl) countFailedValidations(ctx context.Context, regId int64, ident identifier.ACMEIdentifier) error { if ra.limiter == nil || ra.txnBuilder == nil { // Limiter is disabled. return nil } txn, err := ra.txnBuilder.FailedAuthorizationsPerDomainPerAccountSpendOnlyTransaction(regId, ident.Value) if err != nil { return fmt.Errorf("building rate limit transaction for the %s rate limit: %w", ratelimits.FailedAuthorizationsPerDomainPerAccount, err) } _, err = ra.limiter.Spend(ctx, txn) if err != nil { return fmt.Errorf("spending against the %s rate limit: %w", ratelimits.FailedAuthorizationsPerDomainPerAccount, err) } if features.Get().AutomaticallyPauseZombieClients { txn, err = ra.txnBuilder.FailedAuthorizationsForPausingPerDomainPerAccountTransaction(regId, ident.Value) if err != nil { return fmt.Errorf("building rate limit transaction for the %s rate limit: %w", ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount, err) } decision, err := ra.limiter.Spend(ctx, txn) if err != nil { return fmt.Errorf("spending against the %s rate limit: %s", ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount, err) } if decision.Result(ra.clk.Now()) != nil { resp, err := ra.SA.PauseIdentifiers(ctx, &sapb.PauseRequest{ RegistrationID: regId, Identifiers: []*corepb.Identifier{ { Type: string(ident.Type), Value: ident.Value, }, }, }) if err != nil { return fmt.Errorf("failed to pause %d/%q: %w", regId, ident.Value, err) } ra.pauseCounter.With(prometheus.Labels{ "paused": strconv.FormatBool(resp.Paused > 0), "repaused": strconv.FormatBool(resp.Repaused > 0), "grace": strconv.FormatBool(resp.Paused <= 0 && resp.Repaused <= 0), }).Inc() } } return nil } // resetAccountPausingLimit resets bucket to maximum capacity for given account. // There is no reason to surface errors from this function to the Subscriber. func (ra *RegistrationAuthorityImpl) resetAccountPausingLimit(ctx context.Context, regId int64, ident identifier.ACMEIdentifier) { bucketKey, err := ratelimits.NewRegIdDomainBucketKey(ratelimits.FailedAuthorizationsForPausingPerDomainPerAccount, regId, ident.Value) if err != nil { ra.log.Warningf("creating bucket key for regID=[%d] identifier=[%s]: %s", regId, ident.Value, err) } err = ra.limiter.Reset(ctx, bucketKey) if err != nil { ra.log.Warningf("resetting bucket for regID=[%d] identifier=[%s]: %s", regId, ident.Value, err) } } // doDCVAndCAA performs DCV and CAA checks. When EnforceMPIC is enabled, the // checks are executed sequentially: DCV is performed first and CAA is only // checked if DCV is successful. Validation records from the DCV check are // returned even if the CAA check fails. When EnforceMPIC is disabled, DCV and // CAA checks are performed in the same request. func (ra *RegistrationAuthorityImpl) checkDCVAndCAA(ctx context.Context, dcvReq *vapb.PerformValidationRequest, caaReq *vapb.IsCAAValidRequest) (*corepb.ProblemDetails, []*corepb.ValidationRecord, error) { if !features.Get().EnforceMPIC { performValidationRes, err := ra.VA.PerformValidation(ctx, dcvReq) if err != nil { return nil, nil, err } return performValidationRes.Problem, performValidationRes.Records, nil } else { doDCVRes, err := ra.VA.DoDCV(ctx, dcvReq) if err != nil { return nil, nil, err } if doDCVRes.Problem != nil { return doDCVRes.Problem, doDCVRes.Records, nil } doCAAResp, err := ra.VA.IsCAAValid(ctx, caaReq) if err != nil { return nil, nil, err } return doCAAResp.Problem, doDCVRes.Records, nil } } // PerformValidation initiates validation for a specific challenge associated // with the given base authorization. The authorization and challenge are // updated based on the results. func (ra *RegistrationAuthorityImpl) PerformValidation( ctx context.Context, req *rapb.PerformValidationRequest) (*corepb.Authorization, error) { // Clock for start of PerformValidation. vStart := ra.clk.Now() // TODO(#7153): Check each value via core.IsAnyNilOrZero if req.Authz == nil || req.Authz.Id == "" || req.Authz.DnsName == "" || req.Authz.Status == "" || core.IsAnyNilOrZero(req.Authz.Expires) { return nil, errIncompleteGRPCRequest } authz, err := bgrpc.PBToAuthz(req.Authz) if err != nil { return nil, err } // Refuse to update expired authorizations if authz.Expires == nil || authz.Expires.Before(ra.clk.Now()) { return nil, berrors.MalformedError("expired authorization") } challIndex := int(req.ChallengeIndex) if challIndex >= len(authz.Challenges) { return nil, berrors.MalformedError("invalid challenge index '%d'", challIndex) } ch := &authz.Challenges[challIndex] // This challenge type may have been disabled since the challenge was created. if !ra.PA.ChallengeTypeEnabled(ch.Type) { return nil, berrors.MalformedError("challenge type %q no longer allowed", ch.Type) } // We expect some clients to try and update a challenge for an authorization // that is already valid. In this case we don't need to process the // challenge update. It wouldn't be helpful, the overall authorization is // already good! We return early for the valid authz reuse case. if authz.Status == core.StatusValid { return req.Authz, nil } if authz.Status != core.StatusPending { return nil, berrors.MalformedError("authorization must be pending") } // Look up the account key for this authorization regPB, err := ra.SA.GetRegistration(ctx, &sapb.RegistrationID{Id: authz.RegistrationID}) if err != nil { return nil, berrors.InternalServerError("getting acct for authorization: %s", err.Error()) } reg, err := bgrpc.PbToRegistration(regPB) if err != nil { return nil, berrors.InternalServerError("getting acct for authorization: %s", err.Error()) } // Compute the key authorization field based on the registration key expectedKeyAuthorization, err := ch.ExpectedKeyAuthorization(reg.Key) if err != nil { return nil, berrors.InternalServerError("could not compute expected key authorization value") } // Double check before sending to VA if cErr := ch.CheckPending(); cErr != nil { return nil, berrors.MalformedError("cannot validate challenge: %s", cErr.Error()) } // Dispatch to the VA for service ra.drainWG.Add(1) vaCtx := context.Background() go func(authz core.Authorization) { defer ra.drainWG.Done() // We will mutate challenges later in this goroutine to change status and // add error, but we also return a copy of authz immediately. To avoid a // data race, make a copy of the challenges slice here for mutation. challenges := make([]core.Challenge, len(authz.Challenges)) copy(challenges, authz.Challenges) authz.Challenges = challenges chall, _ := bgrpc.ChallengeToPB(authz.Challenges[challIndex]) checkProb, checkRecords, err := ra.checkDCVAndCAA( vaCtx, &vapb.PerformValidationRequest{ DnsName: authz.Identifier.Value, Challenge: chall, Authz: &vapb.AuthzMeta{Id: authz.ID, RegID: authz.RegistrationID}, ExpectedKeyAuthorization: expectedKeyAuthorization, }, &vapb.IsCAAValidRequest{ Domain: authz.Identifier.Value, ValidationMethod: chall.Type, AccountURIID: authz.RegistrationID, AuthzID: authz.ID, }, ) challenge := &authz.Challenges[challIndex] var prob *probs.ProblemDetails if err != nil { prob = probs.ServerInternal("Could not communicate with VA") ra.log.AuditErrf("Could not communicate with VA: %s", err) } else { if checkProb != nil { prob, err = bgrpc.PBToProblemDetails(checkProb) if err != nil { prob = probs.ServerInternal("Could not communicate with VA") ra.log.AuditErrf("Could not communicate with VA: %s", err) } } // Save the updated records records := make([]core.ValidationRecord, len(checkRecords)) for i, r := range checkRecords { records[i], err = bgrpc.PBToValidationRecord(r) if err != nil { prob = probs.ServerInternal("Records for validation corrupt") } } challenge.ValidationRecord = records } if !challenge.RecordsSane() && prob == nil { prob = probs.ServerInternal("Records for validation failed sanity check") } if prob != nil { challenge.Status = core.StatusInvalid challenge.Error = prob err := ra.countFailedValidations(vaCtx, authz.RegistrationID, authz.Identifier) if err != nil { ra.log.Warningf("incrementing failed validations: %s", err) } } else { challenge.Status = core.StatusValid if features.Get().AutomaticallyPauseZombieClients { ra.resetAccountPausingLimit(vaCtx, authz.RegistrationID, authz.Identifier) } } challenge.Validated = &vStart authz.Challenges[challIndex] = *challenge err = ra.recordValidation(vaCtx, authz.ID, authz.Expires, challenge) if err != nil { if errors.Is(err, berrors.AlreadyRevoked) { ra.log.Infof("Didn't record already-finalized validation: regID=[%d] authzID=[%s] err=[%s]", authz.RegistrationID, authz.ID, err) } else { ra.log.AuditErrf("Failed to record validation: regID=[%d] authzID=[%s] err=[%s]", authz.RegistrationID, authz.ID, err) } } }(authz) return bgrpc.AuthzToPB(authz) } // revokeCertificate updates the database to mark the certificate as revoked, // with the given reason and current timestamp. func (ra *RegistrationAuthorityImpl) revokeCertificate(ctx context.Context, serial *big.Int, issuerID issuance.NameID, reason revocation.Reason) error { serialString := core.SerialToString(serial) _, err := ra.SA.RevokeCertificate(ctx, &sapb.RevokeCertificateRequest{ Serial: serialString, Reason: int64(reason), Date: timestamppb.New(ra.clk.Now()), IssuerID: int64(issuerID), }) if err != nil { return err } ra.revocationReasonCounter.WithLabelValues(revocation.ReasonToString[reason]).Inc() return nil } // updateRevocationForKeyCompromise updates the database to mark the certificate // as revoked, with the given reason and current timestamp. This only works for // certificates that were previously revoked for a reason other than // keyCompromise, and which are now being updated to keyCompromise instead. func (ra *RegistrationAuthorityImpl) updateRevocationForKeyCompromise(ctx context.Context, serial *big.Int, issuerID issuance.NameID) error { serialString := core.SerialToString(serial) status, err := ra.SA.GetCertificateStatus(ctx, &sapb.Serial{Serial: serialString}) if err != nil { return berrors.NotFoundError("unable to confirm that serial %q was ever issued: %s", serialString, err) } if status.Status != string(core.OCSPStatusRevoked) { // Internal server error, because we shouldn't be in the function at all // unless the cert was already revoked. return fmt.Errorf("unable to re-revoke serial %q which is not currently revoked", serialString) } if status.RevokedReason == ocsp.KeyCompromise { return berrors.AlreadyRevokedError("unable to re-revoke serial %q which is already revoked for keyCompromise", serialString) } _, err = ra.SA.UpdateRevokedCertificate(ctx, &sapb.RevokeCertificateRequest{ Serial: serialString, Reason: int64(ocsp.KeyCompromise), Date: timestamppb.New(ra.clk.Now()), Backdate: status.RevokedDate, IssuerID: int64(issuerID), }) if err != nil { return err } ra.revocationReasonCounter.WithLabelValues(revocation.ReasonToString[ocsp.KeyCompromise]).Inc() return nil } // purgeOCSPCache makes a request to akamai-purger to purge the cache entries // for the given certificate. func (ra *RegistrationAuthorityImpl) purgeOCSPCache(ctx context.Context, cert *x509.Certificate, issuerID issuance.NameID) error { issuer, ok := ra.issuersByNameID[issuerID] if !ok { return fmt.Errorf("unable to identify issuer of cert with serial %q", core.SerialToString(cert.SerialNumber)) } purgeURLs, err := akamai.GeneratePurgeURLs(cert, issuer.Certificate) if err != nil { return err } _, err = ra.purger.Purge(ctx, &akamaipb.PurgeRequest{Urls: purgeURLs}) if err != nil { return err } return nil } // RevokeCertByApplicant revokes the certificate in question. It allows any // revocation reason from (0, 1, 3, 4, 5, 9), because Subscribers are allowed to // request any revocation reason for their own certificates. However, if the // requesting RegID is an account which has authorizations for all names in the // cert but is *not* the original subscriber, it overrides the revocation reason // to be 5 (cessationOfOperation), because that code is used to cover instances // where "the certificate subscriber no longer owns the domain names in the // certificate". It does not add the key to the blocked keys list, even if // reason 1 (keyCompromise) is requested, as it does not demonstrate said // compromise. It attempts to purge the certificate from the Akamai cache, but // it does not hard-fail if doing so is not successful, because the cache will // drop the old OCSP response in less than 24 hours anyway. func (ra *RegistrationAuthorityImpl) RevokeCertByApplicant(ctx context.Context, req *rapb.RevokeCertByApplicantRequest) (*emptypb.Empty, error) { if req == nil || req.Cert == nil || req.RegID == 0 { return nil, errIncompleteGRPCRequest } if _, present := revocation.UserAllowedReasons[revocation.Reason(req.Code)]; !present { return nil, berrors.BadRevocationReasonError(req.Code) } cert, err := x509.ParseCertificate(req.Cert) if err != nil { return nil, err } serialString := core.SerialToString(cert.SerialNumber) logEvent := certificateRevocationEvent{ ID: core.NewToken(), SerialNumber: serialString, Reason: req.Code, Method: "applicant", RequesterID: req.RegID, } // Below this point, do not re-declare `err` (i.e. type `err :=`) in a // nested scope. Doing so will create a new `err` variable that is not // captured by this closure. defer func() { if err != nil { logEvent.Error = err.Error() } ra.log.AuditObject("Revocation request:", logEvent) }() metadata, err := ra.SA.GetSerialMetadata(ctx, &sapb.Serial{Serial: serialString}) if err != nil { return nil, err } if req.RegID == metadata.RegistrationID { // The requester is the original subscriber. They can revoke for any reason. logEvent.Method = "subscriber" } else { // The requester is a different account. We need to confirm that they have // authorizations for all names in the cert. logEvent.Method = "control" var authzPB *sapb.Authorizations authzPB, err = ra.SA.GetValidAuthorizations2(ctx, &sapb.GetValidAuthorizationsRequest{ RegistrationID: req.RegID, DnsNames: cert.DNSNames, ValidUntil: timestamppb.New(ra.clk.Now()), }) if err != nil { return nil, err } var authzMap map[identifier.ACMEIdentifier]*core.Authorization authzMap, err = bgrpc.PBToAuthzMap(authzPB) if err != nil { return nil, err } // TODO(#7647): Support other kinds of SANs/identifiers here. for _, name := range cert.DNSNames { if _, present := authzMap[identifier.NewDNS(name)]; !present { return nil, berrors.UnauthorizedError("requester does not control all names in cert with serial %q", serialString) } } // Applicants who are not the original Subscriber are not allowed to // revoke for any reason other than cessationOfOperation, which covers // circumstances where "the certificate subscriber no longer owns the // domain names in the certificate". Override the reason code to match. req.Code = ocsp.CessationOfOperation logEvent.Reason = req.Code } issuerID := issuance.IssuerNameID(cert) err = ra.revokeCertificate( ctx, cert.SerialNumber, issuerID, revocation.Reason(req.Code), ) if err != nil { return nil, err } // Don't propagate purger errors to the client. _ = ra.purgeOCSPCache(ctx, cert, issuerID) return &emptypb.Empty{}, nil } // addToBlockedKeys initiates a GRPC call to have the Base64-encoded SHA256 // digest of a provided public key added to the blockedKeys table. func (ra *RegistrationAuthorityImpl) addToBlockedKeys(ctx context.Context, key crypto.PublicKey, src string, comment string) error { var digest core.Sha256Digest digest, err := core.KeyDigest(key) if err != nil { return err } // Add the public key to the blocked keys list. _, err = ra.SA.AddBlockedKey(ctx, &sapb.AddBlockedKeyRequest{ KeyHash: digest[:], Added: timestamppb.New(ra.clk.Now()), Source: src, Comment: comment, }) if err != nil { return err } return nil } // RevokeCertByKey revokes the certificate in question. It always uses // reason code 1 (keyCompromise). It ensures that they public key is added to // the blocked keys list, even if revocation otherwise fails. It attempts to // purge the certificate from the Akamai cache, but it does not hard-fail if // doing so is not successful, because the cache will drop the old OCSP response // in less than 24 hours anyway. func (ra *RegistrationAuthorityImpl) RevokeCertByKey(ctx context.Context, req *rapb.RevokeCertByKeyRequest) (*emptypb.Empty, error) { if req == nil || req.Cert == nil { return nil, errIncompleteGRPCRequest } cert, err := x509.ParseCertificate(req.Cert) if err != nil { return nil, err } issuerID := issuance.IssuerNameID(cert) logEvent := certificateRevocationEvent{ ID: core.NewToken(), SerialNumber: core.SerialToString(cert.SerialNumber), Reason: ocsp.KeyCompromise, Method: "key", RequesterID: 0, } // Below this point, do not re-declare `err` (i.e. type `err :=`) in a // nested scope. Doing so will create a new `err` variable that is not // captured by this closure. defer func() { if err != nil { logEvent.Error = err.Error() } ra.log.AuditObject("Revocation request:", logEvent) }() // We revoke the cert before adding it to the blocked keys list, to avoid a // race between this and the bad-key-revoker. But we don't check the error // from this operation until after we add the key to the blocked keys list, // since that addition needs to happen no matter what. revokeErr := ra.revokeCertificate( ctx, cert.SerialNumber, issuerID, revocation.Reason(ocsp.KeyCompromise), ) // Failing to add the key to the blocked keys list is a worse failure than // failing to revoke in the first place, because it means that // bad-key-revoker won't revoke the cert anyway. err = ra.addToBlockedKeys(ctx, cert.PublicKey, "API", "") if err != nil { return nil, err } // Check the error returned from revokeCertificate itself. err = revokeErr if err == nil { // If the revocation and blocked keys list addition were successful, then // just purge and return. // Don't propagate purger errors to the client. _ = ra.purgeOCSPCache(ctx, cert, issuerID) return &emptypb.Empty{}, nil } else if errors.Is(err, berrors.AlreadyRevoked) { // If it was an AlreadyRevoked error, try to re-revoke the cert in case // it was revoked for a reason other than keyCompromise. err = ra.updateRevocationForKeyCompromise(ctx, cert.SerialNumber, issuerID) // Perform an Akamai cache purge to handle occurrences of a client // previously successfully revoking a certificate, but the cache purge had // unexpectedly failed. Allows clients to re-attempt revocation and purge the // Akamai cache. _ = ra.purgeOCSPCache(ctx, cert, issuerID) if err != nil { return nil, err } return &emptypb.Empty{}, nil } else { // Error out if the error was anything other than AlreadyRevoked. return nil, err } } // AdministrativelyRevokeCertificate terminates trust in the certificate // provided and does not require the registration ID of the requester since this // method is only called from the admin-revoker tool. It trusts that the admin // is doing the right thing, so if the requested reason is keyCompromise, it // blocks the key from future issuance even though compromise has not been // demonstrated here. It purges the certificate from the Akamai cache, and // returns an error if that purge fails, since this method may be called late // in the BRs-mandated revocation timeframe. func (ra *RegistrationAuthorityImpl) AdministrativelyRevokeCertificate(ctx context.Context, req *rapb.AdministrativelyRevokeCertificateRequest) (*emptypb.Empty, error) { if req == nil || req.AdminName == "" { return nil, errIncompleteGRPCRequest } if req.Serial == "" { return nil, errIncompleteGRPCRequest } reasonCode := revocation.Reason(req.Code) if _, present := revocation.AdminAllowedReasons[reasonCode]; !present { return nil, fmt.Errorf("cannot revoke for reason %d", reasonCode) } if req.SkipBlockKey && reasonCode != ocsp.KeyCompromise { return nil, fmt.Errorf("cannot skip key blocking for reasons other than KeyCompromise") } if reasonCode == ocsp.KeyCompromise && req.Malformed { return nil, fmt.Errorf("cannot revoke malformed certificate for KeyCompromise") } logEvent := certificateRevocationEvent{ ID: core.NewToken(), SerialNumber: req.Serial, Reason: req.Code, Method: "admin", AdminName: req.AdminName, } // Below this point, do not re-declare `err` (i.e. type `err :=`) in a // nested scope. Doing so will create a new `err` variable that is not // captured by this closure. var err error defer func() { if err != nil { logEvent.Error = err.Error() } ra.log.AuditObject("Revocation request:", logEvent) }() var cert *x509.Certificate var issuerID issuance.NameID if req.Cert != nil { // If the incoming request includes a certificate body, just use that and // avoid doing any database queries. This code path is deprecated and will // be removed when req.Cert is removed. cert, err = x509.ParseCertificate(req.Cert) if err != nil { return nil, err } issuerID = issuance.IssuerNameID(cert) } else if !req.Malformed { // As long as we don't believe the cert will be malformed, we should // get the precertificate so we can block its pubkey if necessary and purge // the akamai OCSP cache. var certPB *corepb.Certificate certPB, err = ra.SA.GetLintPrecertificate(ctx, &sapb.Serial{Serial: req.Serial}) if err != nil { return nil, err } // Note that, although the thing we're parsing here is actually a linting // precertificate, it has identical issuer info (and therefore an identical // issuer NameID) to the real thing. cert, err = x509.ParseCertificate(certPB.Der) if err != nil { return nil, err } issuerID = issuance.IssuerNameID(cert) } else { // But if the cert is malformed, we at least still need its IssuerID. var status *corepb.CertificateStatus status, err = ra.SA.GetCertificateStatus(ctx, &sapb.Serial{Serial: req.Serial}) if err != nil { return nil, fmt.Errorf("unable to confirm that serial %q was ever issued: %w", req.Serial, err) } issuerID = issuance.NameID(status.IssuerID) } var serialInt *big.Int serialInt, err = core.StringToSerial(req.Serial) if err != nil { return nil, err } err = ra.revokeCertificate(ctx, serialInt, issuerID, revocation.Reason(req.Code)) // Perform an Akamai cache purge to handle occurrences of a client // successfully revoking a certificate, but the initial cache purge failing. if errors.Is(err, berrors.AlreadyRevoked) { if cert != nil { err = ra.purgeOCSPCache(ctx, cert, issuerID) if err != nil { err = fmt.Errorf("OCSP cache purge for already revoked serial %v failed: %w", serialInt, err) return nil, err } } } if err != nil { if req.Code == ocsp.KeyCompromise && errors.Is(err, berrors.AlreadyRevoked) { err = ra.updateRevocationForKeyCompromise(ctx, serialInt, issuerID) if err != nil { return nil, err } } return nil, err } if req.Code == ocsp.KeyCompromise && !req.SkipBlockKey { if cert == nil { return nil, errors.New("revoking for key compromise requires providing the certificate's DER") } err = ra.addToBlockedKeys(ctx, cert.PublicKey, "admin-revoker", fmt.Sprintf("revoked by %s", req.AdminName)) if err != nil { return nil, err } } if cert != nil { err = ra.purgeOCSPCache(ctx, cert, issuerID) if err != nil { err = fmt.Errorf("OCSP cache purge for serial %v failed: %w", serialInt, err) return nil, err } } return &emptypb.Empty{}, nil } // DeactivateRegistration deactivates a valid registration func (ra *RegistrationAuthorityImpl) DeactivateRegistration(ctx context.Context, reg *corepb.Registration) (*emptypb.Empty, error) { if reg == nil || reg.Id == 0 { return nil, errIncompleteGRPCRequest } // TODO(#5554): Remove this check: this is only enforcing that the WFE has // told us the correct status. The SA will enforce that the current status is // valid during its database update. if reg.Status != string(core.StatusValid) { return nil, berrors.MalformedError("only valid registrations can be deactivated") } _, err := ra.SA.DeactivateRegistration(ctx, &sapb.RegistrationID{Id: reg.Id}) if err != nil { return nil, err } // TODO(#5554): Return the updated account object. return &emptypb.Empty{}, nil } // DeactivateAuthorization deactivates a currently valid authorization func (ra *RegistrationAuthorityImpl) DeactivateAuthorization(ctx context.Context, req *corepb.Authorization) (*emptypb.Empty, error) { if core.IsAnyNilOrZero(req, req.Id, req.Status, req.RegistrationID) { return nil, errIncompleteGRPCRequest } authzID, err := strconv.ParseInt(req.Id, 10, 64) if err != nil { return nil, err } if _, err := ra.SA.DeactivateAuthorization2(ctx, &sapb.AuthorizationID2{Id: authzID}); err != nil { return nil, err } if req.Status == string(core.StatusPending) { // Some clients deactivate pending authorizations without attempting them. // We're not sure exactly when this happens but it's most likely due to // internal errors in the client. From our perspective this uses storage // resources similar to how failed authorizations do, so we increment the // failed authorizations limit. err = ra.countFailedValidations(ctx, req.RegistrationID, identifier.NewDNS(req.DnsName)) if err != nil { return nil, fmt.Errorf("failed to update rate limits: %w", err) } } return &emptypb.Empty{}, nil } // GenerateOCSP looks up a certificate's status, then requests a signed OCSP // response for it from the CA. If the certificate status is not available // or the certificate is expired, it returns berrors.NotFoundError. func (ra *RegistrationAuthorityImpl) GenerateOCSP(ctx context.Context, req *rapb.GenerateOCSPRequest) (*capb.OCSPResponse, error) { status, err := ra.SA.GetCertificateStatus(ctx, &sapb.Serial{Serial: req.Serial}) if errors.Is(err, berrors.NotFound) { _, err := ra.SA.GetSerialMetadata(ctx, &sapb.Serial{Serial: req.Serial}) if errors.Is(err, berrors.NotFound) { return nil, berrors.UnknownSerialError() } else { return nil, berrors.NotFoundError("certificate not found") } } else if err != nil { return nil, err } // If we get an OCSP query for a certificate where the status is still // OCSPStatusNotReady, that means an error occurred, not here but at issuance // time. Specifically, we succeeded in storing the linting certificate (and // corresponding certificateStatus row), but failed before calling // SetCertificateStatusReady. We expect this to be rare, and we expect such // certificates not to get OCSP queries, so InternalServerError is appropriate. if status.Status == string(core.OCSPStatusNotReady) { return nil, errors.New("serial belongs to a certificate that errored during issuance") } if ra.clk.Now().After(status.NotAfter.AsTime()) { return nil, berrors.NotFoundError("certificate is expired") } return ra.OCSP.GenerateOCSP(ctx, &capb.GenerateOCSPRequest{ Serial: req.Serial, Status: status.Status, Reason: int32(status.RevokedReason), RevokedAt: status.RevokedDate, IssuerID: status.IssuerID, }) } // NewOrder creates a new order object func (ra *RegistrationAuthorityImpl) NewOrder(ctx context.Context, req *rapb.NewOrderRequest) (*corepb.Order, error) { if req == nil || req.RegistrationID == 0 { return nil, errIncompleteGRPCRequest } newOrder := &sapb.NewOrderRequest{ RegistrationID: req.RegistrationID, DnsNames: core.UniqueLowerNames(req.DnsNames), CertificateProfileName: req.CertificateProfileName, ReplacesSerial: req.ReplacesSerial, } if len(newOrder.DnsNames) > ra.maxNames { return nil, berrors.MalformedError( "Order cannot contain more than %d DNS names", ra.maxNames) } // Validate that our policy allows issuing for each of the names in the order err := ra.PA.WillingToIssue(newOrder.DnsNames) if err != nil { return nil, err } err = wildcardOverlap(newOrder.DnsNames) if err != nil { return nil, err } // See if there is an existing unexpired pending (or ready) order that can be reused // for this account existingOrder, err := ra.SA.GetOrderForNames(ctx, &sapb.GetOrderForNamesRequest{ AcctID: newOrder.RegistrationID, DnsNames: newOrder.DnsNames, }) // If there was an error and it wasn't an acceptable "NotFound" error, return // immediately if err != nil && !errors.Is(err, berrors.NotFound) { return nil, err } // If there was an order, make sure it has expected fields and return it // Error if an incomplete order is returned. if existingOrder != nil { // Check to see if the expected fields of the existing order are set. // TODO(#7153): Check each value via core.IsAnyNilOrZero if existingOrder.Id == 0 || existingOrder.Status == "" || existingOrder.RegistrationID == 0 || len(existingOrder.DnsNames) == 0 || core.IsAnyNilOrZero(existingOrder.Created, existingOrder.Expires) { return nil, errIncompleteGRPCResponse } // Only re-use the order if the profile (even if it is just the empty // string, leaving us to choose a default profile) matches. if existingOrder.CertificateProfileName == newOrder.CertificateProfileName { // Track how often we reuse an existing order and how old that order is. ra.orderAges.WithLabelValues("NewOrder").Observe(ra.clk.Since(existingOrder.Created.AsTime()).Seconds()) return existingOrder, nil } } // Renewal orders, indicated by ARI, are exempt from NewOrder rate limits. if !req.IsARIRenewal && !features.Get().UseKvLimitsForNewOrder { // Check if there is rate limit space for issuing a certificate. err = ra.checkNewOrderLimits(ctx, newOrder.DnsNames, newOrder.RegistrationID, req.IsRenewal) if err != nil { return nil, err } } // An order's lifetime is effectively bound by the shortest remaining lifetime // of its associated authorizations. For that reason it would be Uncool if // `sa.GetAuthorizations` returned an authorization that was very close to // expiry. The resulting pending order that references it would itself end up // expiring very soon. // To prevent this we only return authorizations that are at least 1 day away // from expiring. authzExpiryCutoff := ra.clk.Now().AddDate(0, 0, 1) var existingAuthz *sapb.Authorizations if features.Get().NoPendingAuthzReuse { getAuthReq := &sapb.GetValidAuthorizationsRequest{ RegistrationID: newOrder.RegistrationID, ValidUntil: timestamppb.New(authzExpiryCutoff), DnsNames: newOrder.DnsNames, } existingAuthz, err = ra.SA.GetValidAuthorizations2(ctx, getAuthReq) } else { getAuthReq := &sapb.GetAuthorizationsRequest{ RegistrationID: newOrder.RegistrationID, ValidUntil: timestamppb.New(authzExpiryCutoff), DnsNames: newOrder.DnsNames, } existingAuthz, err = ra.SA.GetAuthorizations2(ctx, getAuthReq) } if err != nil { return nil, err } identToExistingAuthz, err := bgrpc.PBToAuthzMap(existingAuthz) if err != nil { return nil, err } // For each of the names in the order, if there is an acceptable // existing authz, append it to the order to reuse it. Otherwise track // that there is a missing authz for that name. // TODO(#7647): Support non-dnsName identifier types here. var missingAuthzIdents []identifier.ACMEIdentifier for _, name := range newOrder.DnsNames { ident := identifier.NewDNS(name) // If there isn't an existing authz, note that its missing and continue authz, exists := identToExistingAuthz[ident] if !exists { missingAuthzIdents = append(missingAuthzIdents, ident) continue } authzAge := (ra.authorizationLifetime - authz.Expires.Sub(ra.clk.Now())).Seconds() // If the identifier is a wildcard and the existing authz only has one // DNS-01 type challenge we can reuse it. In theory we will // never get back an authorization for a domain with a wildcard prefix // that doesn't meet this criteria from SA.GetAuthorizations but we verify // again to be safe. if strings.HasPrefix(name, "*.") && len(authz.Challenges) == 1 && authz.Challenges[0].Type == core.ChallengeTypeDNS01 { authzID, err := strconv.ParseInt(authz.ID, 10, 64) if err != nil { return nil, err } newOrder.V2Authorizations = append(newOrder.V2Authorizations, authzID) ra.authzAges.WithLabelValues("NewOrder", string(authz.Status)).Observe(authzAge) continue } else if !strings.HasPrefix(name, "*.") { // If the identifier isn't a wildcard, we can reuse any authz authzID, err := strconv.ParseInt(authz.ID, 10, 64) if err != nil { return nil, err } newOrder.V2Authorizations = append(newOrder.V2Authorizations, authzID) ra.authzAges.WithLabelValues("NewOrder", string(authz.Status)).Observe(authzAge) continue } // Delete the authz from the identToExistingAuthz map since we are not reusing it. delete(identToExistingAuthz, ident) // If we reached this point then the existing authz was not acceptable for // reuse and we need to mark the name as requiring a new pending authz missingAuthzIdents = append(missingAuthzIdents, ident) } // Renewal orders, indicated by ARI, are exempt from NewOrder rate limits. if len(missingAuthzIdents) > 0 && !req.IsARIRenewal && !features.Get().UseKvLimitsForNewOrder { pendingAuthzLimits := ra.rlPolicies.PendingAuthorizationsPerAccount() if pendingAuthzLimits.Enabled() { // The order isn't fully authorized we need to check that the client // has rate limit room for more pending authorizations. started := ra.clk.Now() err := ra.checkPendingAuthorizationLimit(ctx, newOrder.RegistrationID, pendingAuthzLimits) elapsed := ra.clk.Since(started) if err != nil { if errors.Is(err, berrors.RateLimit) { ra.rlCheckLatency.WithLabelValues(ratelimit.PendingAuthorizationsPerAccount, ratelimits.Denied).Observe(elapsed.Seconds()) } return nil, err } ra.rlCheckLatency.WithLabelValues(ratelimit.PendingAuthorizationsPerAccount, ratelimits.Allowed).Observe(elapsed.Seconds()) } } // Loop through each of the names missing authzs and create a new pending // authorization for each. var newAuthzs []*sapb.NewAuthzRequest for _, ident := range missingAuthzIdents { pb, err := ra.createPendingAuthz(newOrder.RegistrationID, ident) if err != nil { return nil, err } newAuthzs = append(newAuthzs, pb) ra.authzAges.WithLabelValues("NewOrder", string(core.StatusPending)).Observe(0) } // Start with the order's own expiry as the minExpiry. We only care // about authz expiries that are sooner than the order's expiry minExpiry := ra.clk.Now().Add(ra.orderLifetime) // Check the reused authorizations to see if any have an expiry before the // minExpiry (the order's lifetime) for _, authz := range identToExistingAuthz { // An authz without an expiry is an unexpected internal server event if core.IsAnyNilOrZero(authz.Expires) { return nil, berrors.InternalServerError( "SA.GetAuthorizations returned an authz (%s) with zero expiry", authz.ID) } // If the reused authorization expires before the minExpiry, it's expiry // is the new minExpiry. if authz.Expires.Before(minExpiry) { minExpiry = *authz.Expires } } // If the newly created pending authz's have an expiry closer than the // minExpiry the minExpiry is the pending authz expiry. if len(newAuthzs) > 0 { newPendingAuthzExpires := ra.clk.Now().Add(ra.pendingAuthorizationLifetime) if newPendingAuthzExpires.Before(minExpiry) { minExpiry = newPendingAuthzExpires } } // Set the order's expiry to the minimum expiry. The db doesn't store // sub-second values, so truncate here. newOrder.Expires = timestamppb.New(minExpiry.Truncate(time.Second)) newOrderAndAuthzsReq := &sapb.NewOrderAndAuthzsRequest{ NewOrder: newOrder, NewAuthzs: newAuthzs, } storedOrder, err := ra.SA.NewOrderAndAuthzs(ctx, newOrderAndAuthzsReq) if err != nil { return nil, err } if core.IsAnyNilOrZero(storedOrder.Id, storedOrder.Status, storedOrder.RegistrationID, storedOrder.DnsNames, storedOrder.Created, storedOrder.Expires) { return nil, errIncompleteGRPCResponse } ra.orderAges.WithLabelValues("NewOrder").Observe(0) // Note how many names are being requested in this certificate order. ra.namesPerCert.With(prometheus.Labels{"type": "requested"}).Observe(float64(len(storedOrder.DnsNames))) return storedOrder, nil } // createPendingAuthz checks that a name is allowed for issuance and creates the // necessary challenges for it and puts this and all of the relevant information // into a corepb.Authorization for transmission to the SA to be stored func (ra *RegistrationAuthorityImpl) createPendingAuthz(reg int64, ident identifier.ACMEIdentifier) (*sapb.NewAuthzRequest, error) { challTypes, err := ra.PA.ChallengeTypesFor(ident) if err != nil { return nil, err } challStrs := make([]string, len(challTypes)) for i, t := range challTypes { challStrs[i] = string(t) } authz := &sapb.NewAuthzRequest{ Identifier: ident.AsProto(), RegistrationID: reg, Expires: timestamppb.New(ra.clk.Now().Add(ra.pendingAuthorizationLifetime).Truncate(time.Second)), ChallengeTypes: challStrs, Token: core.NewToken(), } return authz, nil } // wildcardOverlap takes a slice of domain names and returns an error if any of // them is a non-wildcard FQDN that overlaps with a wildcard domain in the map. func wildcardOverlap(dnsNames []string) error { nameMap := make(map[string]bool, len(dnsNames)) for _, v := range dnsNames { nameMap[v] = true } for name := range nameMap { if name[0] == '*' { continue } labels := strings.Split(name, ".") labels[0] = "*" if nameMap[strings.Join(labels, ".")] { return berrors.MalformedError( "Domain name %q is redundant with a wildcard domain in the same request. Remove one or the other from the certificate request.", name) } } return nil } // validateContactsPresent will return an error if the contacts []string // len is greater than zero and the contactsPresent bool is false. We // don't care about any other cases. If the length of the contacts is zero // and contactsPresent is true, it seems like a mismatch but we have to // assume that the client is requesting to update the contacts field with // by removing the existing contacts value so we don't want to return an // error here. func validateContactsPresent(contacts []string, contactsPresent bool) error { if len(contacts) > 0 && !contactsPresent { return berrors.InternalServerError("account contacts present but contactsPresent false") } return nil } // UnpauseAccount receives a validated account unpause request from the SFE and // instructs the SA to unpause that account. If the account cannot be unpaused, // an error is returned. func (ra *RegistrationAuthorityImpl) UnpauseAccount(ctx context.Context, request *rapb.UnpauseAccountRequest) (*rapb.UnpauseAccountResponse, error) { if core.IsAnyNilOrZero(request.RegistrationID) { return nil, errIncompleteGRPCRequest } count, err := ra.SA.UnpauseAccount(ctx, &sapb.RegistrationID{ Id: request.RegistrationID, }) if err != nil { return nil, berrors.InternalServerError("failed to unpause account ID %d", request.RegistrationID) } return &rapb.UnpauseAccountResponse{Count: count.Count}, nil } func (ra *RegistrationAuthorityImpl) GetAuthorization(ctx context.Context, req *rapb.GetAuthorizationRequest) (*corepb.Authorization, error) { if core.IsAnyNilOrZero(req, req.Id) { return nil, errIncompleteGRPCRequest } authz, err := ra.SA.GetAuthorization2(ctx, &sapb.AuthorizationID2{Id: req.Id}) if err != nil { return nil, fmt.Errorf("getting authz from SA: %w", err) } // Filter out any challenges which are currently disabled, so that the client // doesn't attempt them. challs := []*corepb.Challenge{} for _, chall := range authz.Challenges { if ra.PA.ChallengeTypeEnabled(core.AcmeChallenge(chall.Type)) { challs = append(challs, chall) } } authz.Challenges = challs return authz, nil } // Drain blocks until all detached goroutines are done. // // The RA runs detached goroutines for challenge validation and finalization, // so that ACME responses can be returned to the user promptly while work continues. // // The main goroutine should call this before exiting to avoid canceling the work // being done in detached goroutines. func (ra *RegistrationAuthorityImpl) Drain() { ra.drainWG.Wait() }