package sa import ( "context" "errors" "fmt" "math" "regexp" "strings" "time" "github.com/go-jose/go-jose/v4" "github.com/jmhodges/clock" "github.com/prometheus/client_golang/prometheus" "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/db" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" sapb "github.com/letsencrypt/boulder/sa/proto" ) var ( validIncidentTableRegexp = regexp.MustCompile(`^incident_[0-9a-zA-Z_]{1,100}$`) ) // SQLStorageAuthorityRO defines a read-only subset of a Storage Authority type SQLStorageAuthorityRO struct { sapb.UnsafeStorageAuthorityReadOnlyServer dbReadOnlyMap *db.WrappedMap dbIncidentsMap *db.WrappedMap // For RPCs that generate multiple, parallelizable SQL queries, this is the // max parallelism they will use (to avoid consuming too many MariaDB // threads). parallelismPerRPC int // lagFactor is the amount of time we're willing to delay before retrying a // request that may have failed due to replication lag. For example, a user // might create a new account and then immediately create a new order, but // validating that new-order request requires reading their account info from // a read-only database replica... which may not have their brand new data // yet. This value should be less than, but about the same order of magnitude // as, the observed database replication lag. lagFactor time.Duration clk clock.Clock log blog.Logger // lagFactorCounter is a Prometheus counter that tracks the number of times // we've retried a query inside of GetRegistration, GetOrder, and // GetAuthorization2 due to replication lag. It is labeled by method name // and whether data from the retry attempt was found, notfound, or some // other error was encountered. lagFactorCounter *prometheus.CounterVec } var _ sapb.StorageAuthorityReadOnlyServer = (*SQLStorageAuthorityRO)(nil) // NewSQLStorageAuthorityRO provides persistence using a SQL backend for // Boulder. It will modify the given borp.DbMap by adding relevant tables. func NewSQLStorageAuthorityRO( dbReadOnlyMap *db.WrappedMap, dbIncidentsMap *db.WrappedMap, stats prometheus.Registerer, parallelismPerRPC int, lagFactor time.Duration, clk clock.Clock, logger blog.Logger, ) (*SQLStorageAuthorityRO, error) { lagFactorCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "sa_lag_factor", Help: "A counter of SA lagFactor checks labelled by method and pass/fail", }, []string{"method", "result"}) stats.MustRegister(lagFactorCounter) ssaro := &SQLStorageAuthorityRO{ dbReadOnlyMap: dbReadOnlyMap, dbIncidentsMap: dbIncidentsMap, parallelismPerRPC: parallelismPerRPC, lagFactor: lagFactor, clk: clk, log: logger, lagFactorCounter: lagFactorCounter, } return ssaro, nil } // GetRegistration obtains a Registration by ID func (ssa *SQLStorageAuthorityRO) GetRegistration(ctx context.Context, req *sapb.RegistrationID) (*corepb.Registration, error) { if req == nil || req.Id == 0 { return nil, errIncompleteRequest } model, err := selectRegistration(ctx, ssa.dbReadOnlyMap, "id", req.Id) if db.IsNoRows(err) && ssa.lagFactor != 0 { // GetRegistration is often called to validate a JWK belonging to a brand // new account whose registrations table row hasn't propagated to the read // replica yet. If we get a NoRows, wait a little bit and retry, once. ssa.clk.Sleep(ssa.lagFactor) model, err = selectRegistration(ctx, ssa.dbReadOnlyMap, "id", req.Id) if err != nil { if db.IsNoRows(err) { ssa.lagFactorCounter.WithLabelValues("GetRegistration", "notfound").Inc() } else { ssa.lagFactorCounter.WithLabelValues("GetRegistration", "other").Inc() } } else { ssa.lagFactorCounter.WithLabelValues("GetRegistration", "found").Inc() } } if err != nil { if db.IsNoRows(err) { return nil, berrors.NotFoundError("registration with ID '%d' not found", req.Id) } return nil, err } return registrationModelToPb(model) } // GetRegistrationByKey obtains a Registration by JWK func (ssa *SQLStorageAuthorityRO) GetRegistrationByKey(ctx context.Context, req *sapb.JSONWebKey) (*corepb.Registration, error) { if req == nil || len(req.Jwk) == 0 { return nil, errIncompleteRequest } var jwk jose.JSONWebKey err := jwk.UnmarshalJSON(req.Jwk) if err != nil { return nil, err } sha, err := core.KeyDigestB64(jwk.Key) if err != nil { return nil, err } model, err := selectRegistration(ctx, ssa.dbReadOnlyMap, "jwk_sha256", sha) if err != nil { if db.IsNoRows(err) { return nil, berrors.NotFoundError("no registrations with public key sha256 %q", sha) } return nil, err } return registrationModelToPb(model) } // GetSerialMetadata returns metadata stored alongside the serial number, // such as the RegID whose certificate request created that serial, and when // the certificate with that serial will expire. func (ssa *SQLStorageAuthorityRO) GetSerialMetadata(ctx context.Context, req *sapb.Serial) (*sapb.SerialMetadata, error) { if req == nil || req.Serial == "" { return nil, errIncompleteRequest } if !core.ValidSerial(req.Serial) { return nil, fmt.Errorf("invalid serial %q", req.Serial) } recordedSerial := recordedSerialModel{} err := ssa.dbReadOnlyMap.SelectOne( ctx, &recordedSerial, "SELECT * FROM serials WHERE serial = ?", req.Serial, ) if err != nil { if db.IsNoRows(err) { return nil, berrors.NotFoundError("serial %q not found", req.Serial) } return nil, err } return &sapb.SerialMetadata{ Serial: recordedSerial.Serial, RegistrationID: recordedSerial.RegistrationID, Created: timestamppb.New(recordedSerial.Created), Expires: timestamppb.New(recordedSerial.Expires), }, nil } // GetCertificate takes a serial number and returns the corresponding // certificate, or error if it does not exist. func (ssa *SQLStorageAuthorityRO) GetCertificate(ctx context.Context, req *sapb.Serial) (*corepb.Certificate, error) { if req == nil || req.Serial == "" { return nil, errIncompleteRequest } if !core.ValidSerial(req.Serial) { return nil, fmt.Errorf("invalid certificate serial %s", req.Serial) } cert, err := SelectCertificate(ctx, ssa.dbReadOnlyMap, req.Serial) if db.IsNoRows(err) { return nil, berrors.NotFoundError("certificate with serial %q not found", req.Serial) } if err != nil { return nil, err } return cert, nil } // GetLintPrecertificate takes a serial number and returns the corresponding // linting precertificate, or error if it does not exist. The returned precert // is identical to the actual submitted-to-CT-logs precertificate, except for // its signature. func (ssa *SQLStorageAuthorityRO) GetLintPrecertificate(ctx context.Context, req *sapb.Serial) (*corepb.Certificate, error) { if req == nil || req.Serial == "" { return nil, errIncompleteRequest } if !core.ValidSerial(req.Serial) { return nil, fmt.Errorf("invalid precertificate serial %s", req.Serial) } cert, err := SelectPrecertificate(ctx, ssa.dbReadOnlyMap, req.Serial) if db.IsNoRows(err) { return nil, berrors.NotFoundError("precertificate with serial %q not found", req.Serial) } if err != nil { return nil, err } return cert, nil } // GetCertificateStatus takes a hexadecimal string representing the full 128-bit serial // number of a certificate and returns data about that certificate's current // validity. func (ssa *SQLStorageAuthorityRO) GetCertificateStatus(ctx context.Context, req *sapb.Serial) (*corepb.CertificateStatus, error) { if req.Serial == "" { return nil, errIncompleteRequest } if !core.ValidSerial(req.Serial) { err := fmt.Errorf("invalid certificate serial %s", req.Serial) return nil, err } certStatus, err := SelectCertificateStatus(ctx, ssa.dbReadOnlyMap, req.Serial) if db.IsNoRows(err) { return nil, berrors.NotFoundError("certificate status with serial %q not found", req.Serial) } if err != nil { return nil, err } return certStatus, nil } // GetRevocationStatus takes a hexadecimal string representing the full serial // number of a certificate and returns a minimal set of data about that cert's // current validity. func (ssa *SQLStorageAuthorityRO) GetRevocationStatus(ctx context.Context, req *sapb.Serial) (*sapb.RevocationStatus, error) { if req.Serial == "" { return nil, errIncompleteRequest } if !core.ValidSerial(req.Serial) { return nil, fmt.Errorf("invalid certificate serial %s", req.Serial) } status, err := SelectRevocationStatus(ctx, ssa.dbReadOnlyMap, req.Serial) if err != nil { if db.IsNoRows(err) { return nil, berrors.NotFoundError("certificate status with serial %q not found", req.Serial) } return nil, err } return status, nil } // FQDNSetTimestampsForWindow returns the issuance timestamps for each // certificate, issued for a set of identifiers, during a given window of time, // starting from the most recent issuance. // // If req.Limit is nonzero, it returns only the most recent `Limit` results func (ssa *SQLStorageAuthorityRO) FQDNSetTimestampsForWindow(ctx context.Context, req *sapb.CountFQDNSetsRequest) (*sapb.Timestamps, error) { idents := identifier.FromProtoSlice(req.Identifiers) if core.IsAnyNilOrZero(req.Window) || len(idents) == 0 { return nil, errIncompleteRequest } limit := req.Limit if limit == 0 { limit = math.MaxInt64 } type row struct { Issued time.Time } var rows []row _, err := ssa.dbReadOnlyMap.Select( ctx, &rows, `SELECT issued FROM fqdnSets WHERE setHash = ? AND issued > ? ORDER BY issued DESC LIMIT ?`, core.HashIdentifiers(idents), ssa.clk.Now().Add(-req.Window.AsDuration()), limit, ) if err != nil { return nil, err } var results []*timestamppb.Timestamp for _, i := range rows { results = append(results, timestamppb.New(i.Issued)) } return &sapb.Timestamps{Timestamps: results}, nil } // FQDNSetExists returns a bool indicating if one or more FQDN sets |names| // exists in the database func (ssa *SQLStorageAuthorityRO) FQDNSetExists(ctx context.Context, req *sapb.FQDNSetExistsRequest) (*sapb.Exists, error) { idents := identifier.FromProtoSlice(req.Identifiers) if len(idents) == 0 { return nil, errIncompleteRequest } exists, err := ssa.checkFQDNSetExists(ctx, ssa.dbReadOnlyMap.SelectOne, idents) if err != nil { return nil, err } return &sapb.Exists{Exists: exists}, nil } // oneSelectorFunc is a func type that matches both borp.Transaction.SelectOne // and borp.DbMap.SelectOne. type oneSelectorFunc func(ctx context.Context, holder interface{}, query string, args ...interface{}) error // checkFQDNSetExists uses the given oneSelectorFunc to check whether an fqdnSet // for the given names exists. func (ssa *SQLStorageAuthorityRO) checkFQDNSetExists(ctx context.Context, selector oneSelectorFunc, idents identifier.ACMEIdentifiers) (bool, error) { namehash := core.HashIdentifiers(idents) var exists bool err := selector( ctx, &exists, `SELECT EXISTS (SELECT id FROM fqdnSets WHERE setHash = ? LIMIT 1)`, namehash, ) return exists, err } // GetOrder is used to retrieve an already existing order object func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderRequest) (*corepb.Order, error) { if req == nil || req.Id == 0 { return nil, errIncompleteRequest } txn := func(tx db.Executor) (interface{}, error) { omObj, err := tx.Get(ctx, orderModel{}, req.Id) if err != nil { if db.IsNoRows(err) { return nil, berrors.NotFoundError("no order found for ID %d", req.Id) } return nil, err } if omObj == nil { return nil, berrors.NotFoundError("no order found for ID %d", req.Id) } order, err := modelToOrder(omObj.(*orderModel)) if err != nil { return nil, err } orderExp := order.Expires.AsTime() if orderExp.Before(ssa.clk.Now()) { return nil, berrors.NotFoundError("no order found for ID %d", req.Id) } v2AuthzIDs, err := authzForOrder(ctx, tx, order.Id) if err != nil { return nil, err } order.V2Authorizations = v2AuthzIDs // Get the partial Authorization objects for the order authzValidityInfo, err := getAuthorizationStatuses(ctx, tx, order.V2Authorizations) // If there was an error getting the authorizations, return it immediately if err != nil { return nil, err } var idents identifier.ACMEIdentifiers for _, a := range authzValidityInfo { idents = append(idents, identifier.ACMEIdentifier{Type: uintToIdentifierType[a.IdentifierType], Value: a.IdentifierValue}) } order.Identifiers = idents.ToProtoSlice() // Calculate the status for the order status, err := statusForOrder(order, authzValidityInfo, ssa.clk.Now()) if err != nil { return nil, err } order.Status = status return order, nil } output, err := db.WithTransaction(ctx, ssa.dbReadOnlyMap, txn) if (db.IsNoRows(err) || errors.Is(err, berrors.NotFound)) && ssa.lagFactor != 0 { // GetOrder is often called shortly after a new order is created, sometimes // before the order or its associated rows have propagated to the read // replica yet. If we get a NoRows, wait a little bit and retry, once. ssa.clk.Sleep(ssa.lagFactor) output, err = db.WithTransaction(ctx, ssa.dbReadOnlyMap, txn) if err != nil { if db.IsNoRows(err) || errors.Is(err, berrors.NotFound) { ssa.lagFactorCounter.WithLabelValues("GetOrder", "notfound").Inc() } else { ssa.lagFactorCounter.WithLabelValues("GetOrder", "other").Inc() } } else { ssa.lagFactorCounter.WithLabelValues("GetOrder", "found").Inc() } } if err != nil { return nil, err } order, ok := output.(*corepb.Order) if !ok { return nil, fmt.Errorf("casting error in GetOrder") } return order, nil } // GetOrderForNames tries to find a **pending** or **ready** order with the // exact set of names requested, associated with the given accountID. Only // unexpired orders are considered. If no order meeting these requirements is // found a nil corepb.Order pointer is returned. func (ssa *SQLStorageAuthorityRO) GetOrderForNames(ctx context.Context, req *sapb.GetOrderForNamesRequest) (*corepb.Order, error) { idents := identifier.FromProtoSlice(req.Identifiers) if req.AcctID == 0 || len(idents) == 0 { return nil, errIncompleteRequest } // Hash the names requested for lookup in the orderFqdnSets table fqdnHash := core.HashIdentifiers(idents) // Find a possibly-suitable order. We don't include the account ID or order // status in this query because there's no index that includes those, so // including them could require the DB to scan extra rows. // Instead, we select one unexpired order that matches the fqdnSet. If // that order doesn't match the account ID or status we need, just return // nothing. We use `ORDER BY expires ASC` because the index on // (setHash, expires) is in ASC order. DESC would be slightly nicer from a // user experience perspective but would be slow when there are many entries // to sort. // This approach works fine because in most cases there's only one account // issuing for a given name. If there are other accounts issuing for the same // name, it just means order reuse happens less often. var result struct { OrderID int64 RegistrationID int64 } var err error err = ssa.dbReadOnlyMap.SelectOne(ctx, &result, ` SELECT orderID, registrationID FROM orderFqdnSets WHERE setHash = ? AND expires > ? ORDER BY expires ASC LIMIT 1`, fqdnHash, ssa.clk.Now()) if db.IsNoRows(err) { return nil, berrors.NotFoundError("no order matching request found") } else if err != nil { return nil, err } if result.RegistrationID != req.AcctID { return nil, berrors.NotFoundError("no order matching request found") } // Get the order order, err := ssa.GetOrder(ctx, &sapb.OrderRequest{Id: result.OrderID}) if err != nil { return nil, err } // Only return a pending or ready order if order.Status != string(core.StatusPending) && order.Status != string(core.StatusReady) { return nil, berrors.NotFoundError("no order matching request found") } return order, nil } // GetAuthorization2 returns the authz2 style authorization identified by the provided ID or an error. // If no authorization is found matching the ID a berrors.NotFound type error is returned. func (ssa *SQLStorageAuthorityRO) GetAuthorization2(ctx context.Context, req *sapb.AuthorizationID2) (*corepb.Authorization, error) { if req.Id == 0 { return nil, errIncompleteRequest } obj, err := ssa.dbReadOnlyMap.Get(ctx, authzModel{}, req.Id) if db.IsNoRows(err) && ssa.lagFactor != 0 { // GetAuthorization2 is often called shortly after a new order is created, // sometimes before the order's associated authz rows have propagated to the // read replica yet. If we get a NoRows, wait a little bit and retry, once. ssa.clk.Sleep(ssa.lagFactor) obj, err = ssa.dbReadOnlyMap.Get(ctx, authzModel{}, req.Id) if err != nil { if db.IsNoRows(err) { ssa.lagFactorCounter.WithLabelValues("GetAuthorization2", "notfound").Inc() } else { ssa.lagFactorCounter.WithLabelValues("GetAuthorization2", "other").Inc() } } else { ssa.lagFactorCounter.WithLabelValues("GetAuthorization2", "found").Inc() } } if err != nil { return nil, err } if obj == nil { return nil, berrors.NotFoundError("authorization %d not found", req.Id) } return modelToAuthzPB(*(obj.(*authzModel))) } // authzModelMapToPB converts a mapping of identifiers to authzModels into a // protobuf authorizations map func authzModelMapToPB(m map[identifier.ACMEIdentifier]authzModel) (*sapb.Authorizations, error) { resp := &sapb.Authorizations{} for _, v := range m { authzPB, err := modelToAuthzPB(v) if err != nil { return nil, err } resp.Authzs = append(resp.Authzs, authzPB) } return resp, nil } // GetAuthorizations2 returns a single pending or valid authorization owned by // the given account for all given identifiers. If both a valid and pending // authorization exist only the valid one will be returned. // // Deprecated: Use GetValidAuthorizations2, as we stop pending authz reuse. func (ssa *SQLStorageAuthorityRO) GetAuthorizations2(ctx context.Context, req *sapb.GetAuthorizationsRequest) (*sapb.Authorizations, error) { idents := identifier.FromProtoSlice(req.Identifiers) if core.IsAnyNilOrZero(req, req.RegistrationID, idents, req.ValidUntil) { return nil, errIncompleteRequest } // The WHERE clause returned by this function does not contain any // user-controlled strings; all user-controlled input ends up in the // returned placeholder args. identConditions, identArgs := buildIdentifierQueryConditions(idents) query := fmt.Sprintf( `SELECT %s FROM authz2 USE INDEX (regID_identifier_status_expires_idx) WHERE registrationID = ? AND status IN (?,?) AND expires > ? AND (%s)`, authzFields, identConditions, ) params := []interface{}{ req.RegistrationID, statusUint(core.StatusValid), statusUint(core.StatusPending), req.ValidUntil.AsTime(), } params = append(params, identArgs...) var authzModels []authzModel _, err := ssa.dbReadOnlyMap.Select( ctx, &authzModels, query, params..., ) if err != nil { return nil, err } if len(authzModels) == 0 { return &sapb.Authorizations{}, nil } // TODO(#8111): Consider reducing the volume of data in this map. authzModelMap := make(map[identifier.ACMEIdentifier]authzModel, len(authzModels)) for _, am := range authzModels { if req.Profile != "" { // Don't return authzs whose profile doesn't match that requested. if am.CertificateProfileName == nil || *am.CertificateProfileName != req.Profile { continue } } // If there is an existing authorization in the map, only replace it with // one which has a "better" validation state (valid instead of pending). identType, ok := uintToIdentifierType[am.IdentifierType] if !ok { return nil, fmt.Errorf("unrecognized identifier type encoding %d on authz id %d", am.IdentifierType, am.ID) } ident := identifier.ACMEIdentifier{Type: identType, Value: am.IdentifierValue} existing, present := authzModelMap[ident] if !present || uintToStatus[existing.Status] == core.StatusPending && uintToStatus[am.Status] == core.StatusValid { authzModelMap[ident] = am } } return authzModelMapToPB(authzModelMap) } // CountPendingAuthorizations2 returns the number of pending, unexpired authorizations // for the given registration. func (ssa *SQLStorageAuthorityRO) CountPendingAuthorizations2(ctx context.Context, req *sapb.RegistrationID) (*sapb.Count, error) { if req.Id == 0 { return nil, errIncompleteRequest } var count int64 err := ssa.dbReadOnlyMap.SelectOne(ctx, &count, `SELECT COUNT(*) FROM authz2 WHERE registrationID = :regID AND expires > :expires AND status = :status`, map[string]interface{}{ "regID": req.Id, "expires": ssa.clk.Now(), "status": statusUint(core.StatusPending), }, ) if err != nil { return nil, err } return &sapb.Count{Count: count}, nil } // GetValidOrderAuthorizations2 is used to get all authorizations // associated with the given Order ID. // NOTE: The name is outdated. It does *not* filter out invalid or expired // authorizations; that it left to the caller. It also ignores the RegID field // of the input: ensuring that the returned authorizations match the same RegID // as the Order is also left to the caller. This is because the caller is // generally in a better position to provide insightful error messages, whereas // simply omitting an authz from this method's response would leave the caller // wondering why that authz was omitted. func (ssa *SQLStorageAuthorityRO) GetValidOrderAuthorizations2(ctx context.Context, req *sapb.GetValidOrderAuthorizationsRequest) (*sapb.Authorizations, error) { if core.IsAnyNilOrZero(req.Id) { return nil, errIncompleteRequest } // The authz2 and orderToAuthz2 tables both have a column named "id", so we // need to be explicit about which table's "id" column we want to select. qualifiedAuthzFields := strings.Split(authzFields, " ") for i, field := range qualifiedAuthzFields { if field == "id," { qualifiedAuthzFields[i] = "authz2.id," break } } var ams []authzModel _, err := ssa.dbReadOnlyMap.Select( ctx, &ams, fmt.Sprintf(`SELECT %s FROM authz2 LEFT JOIN orderToAuthz2 ON authz2.ID = orderToAuthz2.authzID WHERE orderToAuthz2.orderID = :orderID`, strings.Join(qualifiedAuthzFields, " "), ), map[string]interface{}{ "orderID": req.Id, }, ) if err != nil { return nil, err } // TODO(#8111): Consider reducing the volume of data in this map. byIdent := make(map[identifier.ACMEIdentifier]authzModel) for _, am := range ams { identType, ok := uintToIdentifierType[am.IdentifierType] if !ok { return nil, fmt.Errorf("unrecognized identifier type encoding %d on authz id %d", am.IdentifierType, am.ID) } ident := identifier.ACMEIdentifier{Type: identType, Value: am.IdentifierValue} _, present := byIdent[ident] if present { return nil, fmt.Errorf("identifier %q appears twice in authzs for order %d", am.IdentifierValue, req.Id) } byIdent[ident] = am } return authzModelMapToPB(byIdent) } // CountInvalidAuthorizations2 counts invalid authorizations for a user expiring // in a given time range. func (ssa *SQLStorageAuthorityRO) CountInvalidAuthorizations2(ctx context.Context, req *sapb.CountInvalidAuthorizationsRequest) (*sapb.Count, error) { ident := identifier.FromProto(req.Identifier) if core.IsAnyNilOrZero(req.RegistrationID, ident, req.Range.Earliest, req.Range.Latest) { return nil, errIncompleteRequest } idType, ok := identifierTypeToUint[ident.ToProto().Type] if !ok { return nil, fmt.Errorf("unsupported identifier type %q", ident.ToProto().Type) } var count int64 err := ssa.dbReadOnlyMap.SelectOne( ctx, &count, `SELECT COUNT(*) FROM authz2 WHERE registrationID = :regID AND status = :status AND expires > :expiresEarliest AND expires <= :expiresLatest AND identifierType = :identType AND identifierValue = :identValue`, map[string]interface{}{ "regID": req.RegistrationID, "identType": idType, "identValue": ident.Value, "expiresEarliest": req.Range.Earliest.AsTime(), "expiresLatest": req.Range.Latest.AsTime(), "status": statusUint(core.StatusInvalid), }, ) if err != nil { return nil, err } return &sapb.Count{Count: count}, nil } // GetValidAuthorizations2 returns a single valid authorization owned by the // given account for all given identifiers. If more than one valid authorization // exists, only the one with the latest expiry will be returned. func (ssa *SQLStorageAuthorityRO) GetValidAuthorizations2(ctx context.Context, req *sapb.GetValidAuthorizationsRequest) (*sapb.Authorizations, error) { idents := identifier.FromProtoSlice(req.Identifiers) if core.IsAnyNilOrZero(req, req.RegistrationID, idents, req.ValidUntil) { return nil, errIncompleteRequest } // The WHERE clause returned by this function does not contain any // user-controlled strings; all user-controlled input ends up in the // returned placeholder args. identConditions, identArgs := buildIdentifierQueryConditions(idents) query := fmt.Sprintf( `SELECT %s FROM authz2 USE INDEX (regID_identifier_status_expires_idx) WHERE registrationID = ? AND status = ? AND expires > ? AND (%s)`, authzFields, identConditions, ) params := []interface{}{ req.RegistrationID, statusUint(core.StatusValid), req.ValidUntil.AsTime(), } params = append(params, identArgs...) var authzModels []authzModel _, err := ssa.dbReadOnlyMap.Select( ctx, &authzModels, query, params..., ) if err != nil { return nil, err } if len(authzModels) == 0 { return &sapb.Authorizations{}, nil } // TODO(#8111): Consider reducing the volume of data in this map. authzMap := make(map[identifier.ACMEIdentifier]authzModel, len(authzModels)) for _, am := range authzModels { if req.Profile != "" { // Don't return authzs whose profile doesn't match that requested. if am.CertificateProfileName == nil || *am.CertificateProfileName != req.Profile { continue } } // If there is an existing authorization in the map only replace it with one // which has a later expiry. identType, ok := uintToIdentifierType[am.IdentifierType] if !ok { return nil, fmt.Errorf("unrecognized identifier type encoding %d on authz id %d", am.IdentifierType, am.ID) } ident := identifier.ACMEIdentifier{Type: identType, Value: am.IdentifierValue} existing, present := authzMap[ident] if present && am.Expires.Before(existing.Expires) { continue } authzMap[ident] = am } return authzModelMapToPB(authzMap) } // KeyBlocked checks if a key, indicated by a hash, is present in the blockedKeys table func (ssa *SQLStorageAuthorityRO) KeyBlocked(ctx context.Context, req *sapb.SPKIHash) (*sapb.Exists, error) { if req == nil || req.KeyHash == nil { return nil, errIncompleteRequest } var id int64 err := ssa.dbReadOnlyMap.SelectOne(ctx, &id, `SELECT ID FROM blockedKeys WHERE keyHash = ?`, req.KeyHash) if err != nil { if db.IsNoRows(err) { return &sapb.Exists{Exists: false}, nil } return nil, err } return &sapb.Exists{Exists: true}, nil } // IncidentsForSerial queries each active incident table and returns every // incident that currently impacts `req.Serial`. func (ssa *SQLStorageAuthorityRO) IncidentsForSerial(ctx context.Context, req *sapb.Serial) (*sapb.Incidents, error) { if req == nil { return nil, errIncompleteRequest } var activeIncidents []incidentModel _, err := ssa.dbReadOnlyMap.Select(ctx, &activeIncidents, `SELECT * FROM incidents WHERE enabled = 1`) if err != nil { if db.IsNoRows(err) { return &sapb.Incidents{}, nil } return nil, err } var incidentsForSerial []*sapb.Incident for _, i := range activeIncidents { var count int err := ssa.dbIncidentsMap.SelectOne(ctx, &count, fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE serial = ?", i.SerialTable), req.Serial) if err != nil { if db.IsNoRows(err) { continue } return nil, err } if count > 0 { incident := incidentModelToPB(i) incidentsForSerial = append(incidentsForSerial, &incident) } } if len(incidentsForSerial) == 0 { return &sapb.Incidents{}, nil } return &sapb.Incidents{Incidents: incidentsForSerial}, nil } // SerialsForIncident queries the provided incident table and returns the // resulting rows as a stream of `*sapb.IncidentSerial`s. An `io.EOF` error // signals that there are no more serials to send. If the incident table in // question contains zero rows, only an `io.EOF` error is returned. The // IncidentSerial messages returned may have the zero-value for their OrderID, // RegistrationID, and LastNoticeSent fields, if those are NULL in the database. func (ssa *SQLStorageAuthorityRO) SerialsForIncident(req *sapb.SerialsForIncidentRequest, stream grpc.ServerStreamingServer[sapb.IncidentSerial]) error { if req.IncidentTable == "" { return errIncompleteRequest } // Check that `req.IncidentTable` is a valid incident table name. if !validIncidentTableRegexp.MatchString(req.IncidentTable) { return fmt.Errorf("malformed table name %q", req.IncidentTable) } selector, err := db.NewMappedSelector[incidentSerialModel](ssa.dbIncidentsMap) if err != nil { return fmt.Errorf("initializing db map: %w", err) } rows, err := selector.QueryFrom(stream.Context(), req.IncidentTable, "") if err != nil { return fmt.Errorf("starting db query: %w", err) } return rows.ForEach(func(row *incidentSerialModel) error { // Scan the row into the model. Note: the fields must be passed in the // same order as the columns returned by the query above. ism, err := rows.Get() if err != nil { return err } ispb := &sapb.IncidentSerial{ Serial: ism.Serial, } if ism.RegistrationID != nil { ispb.RegistrationID = *ism.RegistrationID } if ism.OrderID != nil { ispb.OrderID = *ism.OrderID } if ism.LastNoticeSent != nil { ispb.LastNoticeSent = timestamppb.New(*ism.LastNoticeSent) } return stream.Send(ispb) }) } // GetRevokedCertsByShard returns revoked certificates by explicit sharding. // // It returns all unexpired certificates from the revokedCertificates table with the given // shardIdx. It limits the results those revoked before req.RevokedBefore. func (ssa *SQLStorageAuthorityRO) GetRevokedCertsByShard(req *sapb.GetRevokedCertsByShardRequest, stream grpc.ServerStreamingServer[corepb.CRLEntry]) error { if core.IsAnyNilOrZero(req.ShardIdx, req.IssuerNameID, req.RevokedBefore, req.ExpiresAfter) { return errIncompleteRequest } atTime := req.RevokedBefore.AsTime() clauses := ` WHERE issuerID = ? AND shardIdx = ? AND notAfterHour >= ?` params := []interface{}{ req.IssuerNameID, req.ShardIdx, // Round the expiry down to the nearest hour, to take advantage of our // smaller index while still capturing at least as many certs as intended. req.ExpiresAfter.AsTime().Truncate(time.Hour), } selector, err := db.NewMappedSelector[revokedCertModel](ssa.dbReadOnlyMap) if err != nil { return fmt.Errorf("initializing db map: %w", err) } rows, err := selector.QueryContext(stream.Context(), clauses, params...) if err != nil { return fmt.Errorf("reading db: %w", err) } return rows.ForEach(func(row *revokedCertModel) error { // Double-check that the cert wasn't revoked between the time at which we're // constructing this snapshot CRL and right now. If the cert was revoked // at-or-after the "atTime", we'll just include it in the next generation // of CRLs. if row.RevokedDate.After(atTime) || row.RevokedDate.Equal(atTime) { return nil } return stream.Send(&corepb.CRLEntry{ Serial: row.Serial, Reason: int32(row.RevokedReason), //nolint: gosec // Revocation reasons are guaranteed to be small, no risk of overflow. RevokedAt: timestamppb.New(row.RevokedDate), }) }) } // GetRevokedCerts returns revoked certificates based on temporal sharding. // // Based on a request specifying an issuer and a period of time, // it writes to the output stream the set of all certificates issued by that // issuer which expire during that period of time and which have been revoked. // The starting timestamp is treated as inclusive (certs with exactly that // notAfter date are included), but the ending timestamp is exclusive (certs // with exactly that notAfter date are *not* included). func (ssa *SQLStorageAuthorityRO) GetRevokedCerts(req *sapb.GetRevokedCertsRequest, stream grpc.ServerStreamingServer[corepb.CRLEntry]) error { if core.IsAnyNilOrZero(req.IssuerNameID, req.RevokedBefore, req.ExpiresAfter, req.ExpiresBefore) { return errIncompleteRequest } atTime := req.RevokedBefore.AsTime() clauses := ` WHERE notAfter >= ? AND notAfter < ? AND issuerID = ? AND status = ?` params := []interface{}{ req.ExpiresAfter.AsTime(), req.ExpiresBefore.AsTime(), req.IssuerNameID, core.OCSPStatusRevoked, } selector, err := db.NewMappedSelector[crlEntryModel](ssa.dbReadOnlyMap) if err != nil { return fmt.Errorf("initializing db map: %w", err) } rows, err := selector.QueryContext(stream.Context(), clauses, params...) if err != nil { return fmt.Errorf("reading db: %w", err) } return rows.ForEach(func(row *crlEntryModel) error { // Double-check that the cert wasn't revoked between the time at which we're // constructing this snapshot CRL and right now. If the cert was revoked // at-or-after the "atTime", we'll just include it in the next generation // of CRLs. if row.RevokedDate.After(atTime) || row.RevokedDate.Equal(atTime) { return nil } return stream.Send(&corepb.CRLEntry{ Serial: row.Serial, Reason: int32(row.RevokedReason), //nolint: gosec // Revocation reasons are guaranteed to be small, no risk of overflow. RevokedAt: timestamppb.New(row.RevokedDate), }) }) } // GetMaxExpiration returns the timestamp of the farthest-future notAfter date // found in the certificateStatus table. This provides an upper bound on how far // forward operations that need to cover all currently-unexpired certificates // have to look. func (ssa *SQLStorageAuthorityRO) GetMaxExpiration(ctx context.Context, req *emptypb.Empty) (*timestamppb.Timestamp, error) { var model struct { MaxNotAfter *time.Time `db:"maxNotAfter"` } err := ssa.dbReadOnlyMap.SelectOne( ctx, &model, "SELECT MAX(notAfter) AS maxNotAfter FROM certificateStatus", ) if err != nil { return nil, fmt.Errorf("selecting max notAfter: %w", err) } if model.MaxNotAfter == nil { return nil, errors.New("certificateStatus table notAfter column is empty") } return timestamppb.New(*model.MaxNotAfter), err } // Health implements the grpc.checker interface. func (ssa *SQLStorageAuthorityRO) Health(ctx context.Context) error { err := ssa.dbReadOnlyMap.SelectOne(ctx, new(int), "SELECT 1") if err != nil { return err } return nil } // ReplacementOrderExists returns whether a valid replacement order exists for // the given certificate serial number. An existing but expired or otherwise // invalid replacement order is not considered to exist. func (ssa *SQLStorageAuthorityRO) ReplacementOrderExists(ctx context.Context, req *sapb.Serial) (*sapb.Exists, error) { if req == nil || req.Serial == "" { return nil, errIncompleteRequest } var replacement replacementOrderModel err := ssa.dbReadOnlyMap.SelectOne( ctx, &replacement, "SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1", req.Serial, ) if err != nil { if db.IsNoRows(err) { // No replacement order exists. return &sapb.Exists{Exists: false}, nil } return nil, err } if replacement.Replaced { // Certificate has already been replaced. return &sapb.Exists{Exists: true}, nil } if replacement.OrderExpires.Before(ssa.clk.Now()) { // The existing replacement order has expired. return &sapb.Exists{Exists: false}, nil } // Pull the replacement order so we can inspect its status. replacementOrder, err := ssa.GetOrder(ctx, &sapb.OrderRequest{Id: replacement.OrderID}) if err != nil { if errors.Is(err, berrors.NotFound) { // The existing replacement order has been deleted. This should // never happen. ssa.log.Errf("replacement order %d for serial %q not found", replacement.OrderID, req.Serial) return &sapb.Exists{Exists: false}, nil } } switch replacementOrder.Status { case string(core.StatusPending), string(core.StatusReady), string(core.StatusProcessing), string(core.StatusValid): // An existing replacement order is either still being worked on or has // already been finalized. return &sapb.Exists{Exists: true}, nil case string(core.StatusInvalid): // The existing replacement order cannot be finalized. The requester // should create a new replacement order. return &sapb.Exists{Exists: false}, nil default: // Replacement order is in an unknown state. This should never happen. return nil, fmt.Errorf("unknown replacement order status: %q", replacementOrder.Status) } } // GetSerialsByKey returns a stream of serials for all unexpired certificates // whose public key matches the given SPKIHash. This is useful for revoking all // certificates affected by a key compromise. func (ssa *SQLStorageAuthorityRO) GetSerialsByKey(req *sapb.SPKIHash, stream grpc.ServerStreamingServer[sapb.Serial]) error { clauses := ` WHERE keyHash = ? AND certNotAfter > ?` params := []interface{}{ req.KeyHash, ssa.clk.Now(), } selector, err := db.NewMappedSelector[keyHashModel](ssa.dbReadOnlyMap) if err != nil { return fmt.Errorf("initializing db map: %w", err) } rows, err := selector.QueryContext(stream.Context(), clauses, params...) if err != nil { return fmt.Errorf("reading db: %w", err) } return rows.ForEach(func(row *keyHashModel) error { return stream.Send(&sapb.Serial{Serial: row.CertSerial}) }) } // GetSerialsByAccount returns a stream of all serials for all unexpired // certificates issued to the given RegID. This is useful for revoking all of // an account's certs upon their request. func (ssa *SQLStorageAuthorityRO) GetSerialsByAccount(req *sapb.RegistrationID, stream grpc.ServerStreamingServer[sapb.Serial]) error { clauses := ` WHERE registrationID = ? AND expires > ?` params := []interface{}{ req.Id, ssa.clk.Now(), } selector, err := db.NewMappedSelector[recordedSerialModel](ssa.dbReadOnlyMap) if err != nil { return fmt.Errorf("initializing db map: %w", err) } rows, err := selector.QueryContext(stream.Context(), clauses, params...) if err != nil { return fmt.Errorf("reading db: %w", err) } return rows.ForEach(func(row *recordedSerialModel) error { return stream.Send(&sapb.Serial{Serial: row.Serial}) }) } // CheckIdentifiersPaused takes a slice of identifiers and returns a slice of // the first 15 identifier values which are currently paused for the provided // account. If no matches are found, an empty slice is returned. func (ssa *SQLStorageAuthorityRO) CheckIdentifiersPaused(ctx context.Context, req *sapb.PauseRequest) (*sapb.Identifiers, error) { if core.IsAnyNilOrZero(req.RegistrationID, req.Identifiers) { return nil, errIncompleteRequest } idents, err := newIdentifierModelsFromPB(req.Identifiers) if err != nil { return nil, err } if len(idents) == 0 { // No identifier values to check. return nil, nil } identsByType := map[uint8][]string{} for _, id := range idents { identsByType[id.Type] = append(identsByType[id.Type], id.Value) } // Build a query to retrieve up to 15 paused identifiers using OR clauses // for conditions specific to each type. This approach handles mixed // identifier types in a single query. Assuming 3 DNS identifiers and 1 IP // identifier, the resulting query would look like: // // SELECT identifierType, identifierValue // FROM paused WHERE registrationID = ? AND // unpausedAt IS NULL AND // ((identifierType = ? AND identifierValue IN (?, ?, ?)) OR // (identifierType = ? AND identifierValue IN (?))) // LIMIT 15 // // Corresponding args array for placeholders: [, 0, "example.com", // "example.net", "example.org", 1, "1.2.3.4"] var conditions []string args := []interface{}{req.RegistrationID} for idType, values := range identsByType { conditions = append(conditions, fmt.Sprintf("identifierType = ? AND identifierValue IN (%s)", db.QuestionMarks(len(values)), ), ) args = append(args, idType) for _, value := range values { args = append(args, value) } } query := fmt.Sprintf(` SELECT identifierType, identifierValue FROM paused WHERE registrationID = ? AND unpausedAt IS NULL AND (%s) LIMIT 15`, strings.Join(conditions, " OR ")) var matches []identifierModel _, err = ssa.dbReadOnlyMap.Select(ctx, &matches, query, args...) if err != nil && !db.IsNoRows(err) { // Error querying the database. return nil, err } return newPBFromIdentifierModels(matches) } // GetPausedIdentifiers returns a slice of paused identifiers for the provided // account. If no paused identifiers are found, an empty slice is returned. The // results are limited to the first 15 paused identifiers. func (ssa *SQLStorageAuthorityRO) GetPausedIdentifiers(ctx context.Context, req *sapb.RegistrationID) (*sapb.Identifiers, error) { if core.IsAnyNilOrZero(req.Id) { return nil, errIncompleteRequest } var matches []identifierModel _, err := ssa.dbReadOnlyMap.Select(ctx, &matches, ` SELECT identifierType, identifierValue FROM paused WHERE registrationID = ? AND unpausedAt IS NULL LIMIT 15`, req.Id, ) if err != nil && !db.IsNoRows(err) { return nil, err } return newPBFromIdentifierModels(matches) } // GetRateLimitOverride retrieves a rate limit override for the given bucket key // and limit. If no override is found, a NotFound error is returned. func (ssa *SQLStorageAuthorityRO) GetRateLimitOverride(ctx context.Context, req *sapb.GetRateLimitOverrideRequest) (*sapb.RateLimitOverrideResponse, error) { if core.IsAnyNilOrZero(req, req.LimitEnum, req.BucketKey) { return nil, errIncompleteRequest } obj, err := ssa.dbReadOnlyMap.Get(ctx, overrideModel{}, req.LimitEnum, req.BucketKey) if db.IsNoRows(err) { return nil, berrors.NotFoundError( "no rate limit override found for limit %d and bucket key %s", req.LimitEnum, req.BucketKey, ) } if err != nil { return nil, err } row := obj.(*overrideModel) return &sapb.RateLimitOverrideResponse{ Override: newPBFromOverrideModel(row), Enabled: row.Enabled, UpdatedAt: timestamppb.New(row.UpdatedAt), }, nil } // GetEnabledRateLimitOverrides retrieves all enabled rate limit overrides from // the database. The results are returned as a stream. If no enabled overrides // are found, an empty stream is returned. func (ssa *SQLStorageAuthorityRO) GetEnabledRateLimitOverrides(_ *emptypb.Empty, stream sapb.StorageAuthorityReadOnly_GetEnabledRateLimitOverridesServer) error { selector, err := db.NewMappedSelector[overrideModel](ssa.dbReadOnlyMap) if err != nil { return fmt.Errorf("initializing selector: %w", err) } rows, err := selector.QueryContext(stream.Context(), "WHERE enabled = true") if err != nil { return fmt.Errorf("querying enabled overrides: %w", err) } return rows.ForEach(func(m *overrideModel) error { return stream.Send(newPBFromOverrideModel(m)) }) }