1608 lines
51 KiB
Go
1608 lines
51 KiB
Go
package sa
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"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/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"
|
|
bgrpc "github.com/letsencrypt/boulder/grpc"
|
|
"github.com/letsencrypt/boulder/identifier"
|
|
blog "github.com/letsencrypt/boulder/log"
|
|
"github.com/letsencrypt/boulder/revocation"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
"github.com/letsencrypt/boulder/unpause"
|
|
)
|
|
|
|
var (
|
|
errIncompleteRequest = errors.New("incomplete gRPC request message")
|
|
)
|
|
|
|
// SQLStorageAuthority defines a Storage Authority.
|
|
//
|
|
// Note that although SQLStorageAuthority does have methods wrapping all of the
|
|
// read-only methods provided by the SQLStorageAuthorityRO, those wrapper
|
|
// implementations are in saro.go, next to the real implementations.
|
|
type SQLStorageAuthority struct {
|
|
sapb.UnsafeStorageAuthorityServer
|
|
|
|
*SQLStorageAuthorityRO
|
|
|
|
dbMap *db.WrappedMap
|
|
|
|
// rateLimitWriteErrors is a Counter for the number of times
|
|
// a ratelimit update transaction failed during AddCertificate request
|
|
// processing. We do not fail the overall AddCertificate call when ratelimit
|
|
// transactions fail and so use this stat to maintain visibility into the rate
|
|
// this occurs.
|
|
rateLimitWriteErrors prometheus.Counter
|
|
}
|
|
|
|
var _ sapb.StorageAuthorityServer = (*SQLStorageAuthority)(nil)
|
|
|
|
// NewSQLStorageAuthorityWrapping provides persistence using a SQL backend for
|
|
// Boulder. It takes a read-only storage authority to wrap, which is useful if
|
|
// you are constructing both types of implementations and want to share
|
|
// read-only database connections between them.
|
|
func NewSQLStorageAuthorityWrapping(
|
|
ssaro *SQLStorageAuthorityRO,
|
|
dbMap *db.WrappedMap,
|
|
stats prometheus.Registerer,
|
|
) (*SQLStorageAuthority, error) {
|
|
rateLimitWriteErrors := prometheus.NewCounter(prometheus.CounterOpts{
|
|
Name: "rate_limit_write_errors",
|
|
Help: "number of failed ratelimit update transactions during AddCertificate",
|
|
})
|
|
stats.MustRegister(rateLimitWriteErrors)
|
|
|
|
ssa := &SQLStorageAuthority{
|
|
SQLStorageAuthorityRO: ssaro,
|
|
dbMap: dbMap,
|
|
rateLimitWriteErrors: rateLimitWriteErrors,
|
|
}
|
|
|
|
return ssa, nil
|
|
}
|
|
|
|
// NewSQLStorageAuthority provides persistence using a SQL backend for
|
|
// Boulder. It constructs its own read-only storage authority to wrap.
|
|
func NewSQLStorageAuthority(
|
|
dbMap *db.WrappedMap,
|
|
dbReadOnlyMap *db.WrappedMap,
|
|
dbIncidentsMap *db.WrappedMap,
|
|
parallelismPerRPC int,
|
|
lagFactor time.Duration,
|
|
clk clock.Clock,
|
|
logger blog.Logger,
|
|
stats prometheus.Registerer,
|
|
) (*SQLStorageAuthority, error) {
|
|
ssaro, err := NewSQLStorageAuthorityRO(
|
|
dbReadOnlyMap, dbIncidentsMap, stats, parallelismPerRPC, lagFactor, clk, logger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return NewSQLStorageAuthorityWrapping(ssaro, dbMap, stats)
|
|
}
|
|
|
|
// NewRegistration stores a new Registration
|
|
func (ssa *SQLStorageAuthority) NewRegistration(ctx context.Context, req *corepb.Registration) (*corepb.Registration, error) {
|
|
if len(req.Key) == 0 {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
reg, err := registrationPbToModel(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reg.CreatedAt = ssa.clk.Now()
|
|
|
|
err = ssa.dbMap.Insert(ctx, reg)
|
|
if err != nil {
|
|
if db.IsDuplicate(err) {
|
|
// duplicate entry error can only happen when jwk_sha256 collides, indicate
|
|
// to caller that the provided key is already in use
|
|
return nil, berrors.DuplicateError("key is already in use for a different account")
|
|
}
|
|
return nil, err
|
|
}
|
|
return registrationModelToPb(reg)
|
|
}
|
|
|
|
// UpdateRegistrationContact makes no changes, and simply returns the account
|
|
// as it exists in the database.
|
|
//
|
|
// Deprecated: See https://github.com/letsencrypt/boulder/issues/8199 for removal.
|
|
func (ssa *SQLStorageAuthority) UpdateRegistrationContact(ctx context.Context, req *sapb.UpdateRegistrationContactRequest) (*corepb.Registration, error) {
|
|
return ssa.GetRegistration(ctx, &sapb.RegistrationID{Id: req.RegistrationID})
|
|
}
|
|
|
|
// UpdateRegistrationKey stores an updated key in a Registration.
|
|
func (ssa *SQLStorageAuthority) UpdateRegistrationKey(ctx context.Context, req *sapb.UpdateRegistrationKeyRequest) (*corepb.Registration, error) {
|
|
if core.IsAnyNilOrZero(req.RegistrationID, req.Jwk) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
// Even though we don't need to convert from JSON to an in-memory JSONWebKey
|
|
// for the sake of the `Key` field, we do need to do the conversion in order
|
|
// to compute the SHA256 key digest.
|
|
var jwk jose.JSONWebKey
|
|
err := jwk.UnmarshalJSON(req.Jwk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing JWK: %w", err)
|
|
}
|
|
sha, err := core.KeyDigestB64(jwk.Key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("computing key digest: %w", err)
|
|
}
|
|
|
|
result, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
result, err := tx.ExecContext(ctx,
|
|
"UPDATE registrations SET jwk = ?, jwk_sha256 = ? WHERE id = ? LIMIT 1",
|
|
req.Jwk,
|
|
sha,
|
|
req.RegistrationID,
|
|
)
|
|
if err != nil {
|
|
if db.IsDuplicate(err) {
|
|
// duplicate entry error can only happen when jwk_sha256 collides, indicate
|
|
// to caller that the provided key is already in use
|
|
return nil, berrors.DuplicateError("key is already in use for a different account")
|
|
}
|
|
return nil, err
|
|
}
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil || rowsAffected != 1 {
|
|
return nil, berrors.InternalServerError("no registration ID '%d' updated with new jwk", req.RegistrationID)
|
|
}
|
|
|
|
updatedRegistrationModel, err := selectRegistration(ctx, tx, "id", req.RegistrationID)
|
|
if err != nil {
|
|
if db.IsNoRows(err) {
|
|
return nil, berrors.NotFoundError("registration with ID '%d' not found", req.RegistrationID)
|
|
}
|
|
return nil, err
|
|
}
|
|
updatedRegistration, err := registrationModelToPb(updatedRegistrationModel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return updatedRegistration, nil
|
|
})
|
|
if overallError != nil {
|
|
return nil, overallError
|
|
}
|
|
|
|
return result.(*corepb.Registration), nil
|
|
}
|
|
|
|
// AddSerial writes a record of a serial number generation to the DB.
|
|
func (ssa *SQLStorageAuthority) AddSerial(ctx context.Context, req *sapb.AddSerialRequest) (*emptypb.Empty, error) {
|
|
if core.IsAnyNilOrZero(req.Serial, req.RegID, req.Created, req.Expires) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
err := ssa.dbMap.Insert(ctx, &recordedSerialModel{
|
|
Serial: req.Serial,
|
|
RegistrationID: req.RegID,
|
|
Created: req.Created.AsTime(),
|
|
Expires: req.Expires.AsTime(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// SetCertificateStatusReady changes a serial's OCSP status from core.OCSPStatusNotReady to core.OCSPStatusGood.
|
|
// Called when precertificate issuance succeeds. returns an error if the serial doesn't have status core.OCSPStatusNotReady.
|
|
func (ssa *SQLStorageAuthority) SetCertificateStatusReady(ctx context.Context, req *sapb.Serial) (*emptypb.Empty, error) {
|
|
res, err := ssa.dbMap.ExecContext(ctx,
|
|
`UPDATE certificateStatus
|
|
SET status = ?
|
|
WHERE status = ? AND
|
|
serial = ?`,
|
|
string(core.OCSPStatusGood),
|
|
string(core.OCSPStatusNotReady),
|
|
req.Serial,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rows == 0 {
|
|
return nil, errors.New("failed to set certificate status to ready")
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// AddPrecertificate writes a record of a linting certificate to the database.
|
|
//
|
|
// Note: The name "AddPrecertificate" is a historical artifact, and this is now
|
|
// always called with a linting certificate. See #6807.
|
|
//
|
|
// Note: this is not idempotent: it does not protect against inserting the same
|
|
// certificate multiple times. Calling code needs to first insert the cert's
|
|
// serial into the Serials table to ensure uniqueness.
|
|
func (ssa *SQLStorageAuthority) AddPrecertificate(ctx context.Context, req *sapb.AddCertificateRequest) (*emptypb.Empty, error) {
|
|
if core.IsAnyNilOrZero(req.Der, req.RegID, req.IssuerNameID, req.Issued) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
parsed, err := x509.ParseCertificate(req.Der)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
serialHex := core.SerialToString(parsed.SerialNumber)
|
|
|
|
preCertModel := &lintingCertModel{
|
|
Serial: serialHex,
|
|
RegistrationID: req.RegID,
|
|
DER: req.Der,
|
|
Issued: req.Issued.AsTime(),
|
|
Expires: parsed.NotAfter,
|
|
}
|
|
|
|
_, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
// Select to see if precert exists
|
|
var row struct {
|
|
Count int64
|
|
}
|
|
err := tx.SelectOne(ctx, &row, "SELECT COUNT(*) as count FROM precertificates WHERE serial=?", serialHex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if row.Count > 0 {
|
|
return nil, berrors.DuplicateError("cannot add a duplicate cert")
|
|
}
|
|
|
|
err = tx.Insert(ctx, preCertModel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
status := core.OCSPStatusGood
|
|
if req.OcspNotReady {
|
|
status = core.OCSPStatusNotReady
|
|
}
|
|
cs := &certificateStatusModel{
|
|
Serial: serialHex,
|
|
Status: status,
|
|
OCSPLastUpdated: ssa.clk.Now(),
|
|
RevokedDate: time.Time{},
|
|
RevokedReason: 0,
|
|
LastExpirationNagSent: time.Time{},
|
|
NotAfter: parsed.NotAfter,
|
|
IsExpired: false,
|
|
IssuerID: req.IssuerNameID,
|
|
}
|
|
err = ssa.dbMap.Insert(ctx, cs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
idents := identifier.FromCert(parsed)
|
|
|
|
isRenewal, err := ssa.checkFQDNSetExists(
|
|
ctx,
|
|
tx.SelectOne,
|
|
idents)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = addIssuedNames(ctx, tx, parsed, isRenewal)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = addKeyHash(ctx, tx, parsed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, nil
|
|
})
|
|
if overallError != nil {
|
|
return nil, overallError
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// AddCertificate stores an issued certificate, returning an error if it is a
|
|
// duplicate or if any other failure occurs.
|
|
func (ssa *SQLStorageAuthority) AddCertificate(ctx context.Context, req *sapb.AddCertificateRequest) (*emptypb.Empty, error) {
|
|
if core.IsAnyNilOrZero(req.Der, req.RegID, req.Issued) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
parsedCertificate, err := x509.ParseCertificate(req.Der)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
digest := core.Fingerprint256(req.Der)
|
|
serial := core.SerialToString(parsedCertificate.SerialNumber)
|
|
|
|
cert := &core.Certificate{
|
|
RegistrationID: req.RegID,
|
|
Serial: serial,
|
|
Digest: digest,
|
|
DER: req.Der,
|
|
Issued: req.Issued.AsTime(),
|
|
Expires: parsedCertificate.NotAfter,
|
|
}
|
|
|
|
_, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
// Select to see if cert exists
|
|
var row struct {
|
|
Count int64
|
|
}
|
|
err := tx.SelectOne(ctx, &row, "SELECT COUNT(*) as count FROM certificates WHERE serial=?", serial)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if row.Count > 0 {
|
|
return nil, berrors.DuplicateError("cannot add a duplicate cert")
|
|
}
|
|
|
|
// Save the final certificate
|
|
err = tx.Insert(ctx, cert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, err
|
|
})
|
|
if overallError != nil {
|
|
return nil, overallError
|
|
}
|
|
|
|
// In a separate transaction, perform the work required to update the table
|
|
// used for order reuse. Since the effect of failing the write is just a
|
|
// missed opportunity to reuse an order, we choose to not fail the
|
|
// AddCertificate operation if this update transaction fails.
|
|
_, fqdnTransactionErr := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
// Update the FQDN sets now that there is a final certificate to ensure
|
|
// reuse is determined correctly.
|
|
err = addFQDNSet(
|
|
ctx,
|
|
tx,
|
|
identifier.FromCert(parsedCertificate),
|
|
core.SerialToString(parsedCertificate.SerialNumber),
|
|
parsedCertificate.NotBefore,
|
|
parsedCertificate.NotAfter,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, nil
|
|
})
|
|
// If the FQDN sets transaction failed, increment a stat and log a warning
|
|
// but don't return an error from AddCertificate.
|
|
if fqdnTransactionErr != nil {
|
|
ssa.rateLimitWriteErrors.Inc()
|
|
ssa.log.AuditErrf("failed AddCertificate FQDN sets insert transaction: %v", fqdnTransactionErr)
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// DeactivateRegistration deactivates a currently valid registration and removes its contact field
|
|
func (ssa *SQLStorageAuthority) DeactivateRegistration(ctx context.Context, req *sapb.RegistrationID) (*corepb.Registration, error) {
|
|
if req == nil || req.Id == 0 {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
result, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (any, error) {
|
|
result, err := tx.ExecContext(ctx,
|
|
"UPDATE registrations SET status = ? WHERE status = ? AND id = ? LIMIT 1",
|
|
string(core.StatusDeactivated),
|
|
string(core.StatusValid),
|
|
req.Id,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("deactivating account %d: %w", req.Id, err)
|
|
}
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("deactivating account %d: %w", req.Id, err)
|
|
}
|
|
if rowsAffected == 0 {
|
|
return nil, berrors.NotFoundError("no active account with id %d", req.Id)
|
|
} else if rowsAffected > 1 {
|
|
return nil, berrors.InternalServerError("unexpectedly deactivated multiple accounts with id %d", req.Id)
|
|
}
|
|
|
|
updatedRegistrationModel, err := selectRegistration(ctx, tx, "id", req.Id)
|
|
if err != nil {
|
|
if db.IsNoRows(err) {
|
|
return nil, berrors.NotFoundError("fetching account %d: no rows found", req.Id)
|
|
}
|
|
return nil, fmt.Errorf("fetching account %d: %w", req.Id, err)
|
|
}
|
|
|
|
updatedRegistration, err := registrationModelToPb(updatedRegistrationModel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return updatedRegistration, nil
|
|
})
|
|
if overallError != nil {
|
|
return nil, overallError
|
|
}
|
|
|
|
res, ok := result.(*corepb.Registration)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected casting failure in DeactivateRegistration")
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// DeactivateAuthorization2 deactivates a currently valid or pending authorization.
|
|
func (ssa *SQLStorageAuthority) DeactivateAuthorization2(ctx context.Context, req *sapb.AuthorizationID2) (*emptypb.Empty, error) {
|
|
if req.Id == 0 {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
_, err := ssa.dbMap.ExecContext(ctx,
|
|
`UPDATE authz2 SET status = :deactivated WHERE id = :id and status IN (:valid,:pending)`,
|
|
map[string]interface{}{
|
|
"deactivated": statusUint(core.StatusDeactivated),
|
|
"id": req.Id,
|
|
"valid": statusUint(core.StatusValid),
|
|
"pending": statusUint(core.StatusPending),
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// NewOrderAndAuthzs adds the given authorizations to the database, adds their
|
|
// autogenerated IDs to the given order, and then adds the order to the db.
|
|
// This is done inside a single transaction to prevent situations where new
|
|
// authorizations are created, but then their corresponding order is never
|
|
// created, leading to "invisible" pending authorizations.
|
|
func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb.NewOrderAndAuthzsRequest) (*corepb.Order, error) {
|
|
if req.NewOrder == nil {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
for _, authz := range req.NewAuthzs {
|
|
if authz.RegistrationID != req.NewOrder.RegistrationID {
|
|
// This is a belt-and-suspenders check. These were just created by the RA,
|
|
// so their RegIDs should match. But if they don't, the consequences would
|
|
// be very bad, so we do an extra check here.
|
|
return nil, errors.New("new order and authzs must all be associated with same account")
|
|
}
|
|
}
|
|
|
|
output, err := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
// First, insert all of the new authorizations and record their IDs.
|
|
newAuthzIDs := make([]int64, 0, len(req.NewAuthzs))
|
|
for _, authz := range req.NewAuthzs {
|
|
am, err := newAuthzReqToModel(authz, req.NewOrder.CertificateProfileName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = tx.Insert(ctx, am)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newAuthzIDs = append(newAuthzIDs, am.ID)
|
|
}
|
|
|
|
// Second, insert the new order.
|
|
created := ssa.clk.Now()
|
|
om := orderModel{
|
|
RegistrationID: req.NewOrder.RegistrationID,
|
|
Expires: req.NewOrder.Expires.AsTime(),
|
|
Created: created,
|
|
CertificateProfileName: &req.NewOrder.CertificateProfileName,
|
|
Replaces: &req.NewOrder.Replaces,
|
|
}
|
|
err := tx.Insert(ctx, &om)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
orderID := om.ID
|
|
|
|
// Third, insert all of the orderToAuthz relations.
|
|
// Have to combine the already-associated and newly-created authzs.
|
|
allAuthzIds := append(req.NewOrder.V2Authorizations, newAuthzIDs...)
|
|
inserter, err := db.NewMultiInserter("orderToAuthz2", []string{"orderID", "authzID"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, id := range allAuthzIds {
|
|
err := inserter.Add([]interface{}{orderID, id})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
err = inserter.Insert(ctx, tx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Fourth, insert the FQDNSet entry for the order.
|
|
err = addOrderFQDNSet(ctx, tx, identifier.FromProtoSlice(req.NewOrder.Identifiers), orderID, req.NewOrder.RegistrationID, req.NewOrder.Expires.AsTime())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if req.NewOrder.ReplacesSerial != "" {
|
|
// Update the replacementOrders table to indicate that this order
|
|
// replaces the provided certificate serial.
|
|
err := addReplacementOrder(ctx, tx, req.NewOrder.ReplacesSerial, orderID, req.NewOrder.Expires.AsTime())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Get the partial Authorization objects for the order
|
|
authzValidityInfo, err := getAuthorizationStatuses(ctx, tx, allAuthzIds)
|
|
// If there was an error getting the authorizations, return it immediately
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Finally, build the overall Order PB.
|
|
res := &corepb.Order{
|
|
// ID and Created were auto-populated on the order model when it was inserted.
|
|
Id: orderID,
|
|
Created: timestamppb.New(created),
|
|
// These are carried over from the original request unchanged.
|
|
RegistrationID: req.NewOrder.RegistrationID,
|
|
Expires: req.NewOrder.Expires,
|
|
Identifiers: req.NewOrder.Identifiers,
|
|
// This includes both reused and newly created authz IDs.
|
|
V2Authorizations: allAuthzIds,
|
|
// A new order is never processing because it can't be finalized yet.
|
|
BeganProcessing: false,
|
|
// An empty string is allowed. When the RA retrieves the order and
|
|
// transmits it to the CA, the empty string will take the value of
|
|
// DefaultCertProfileName from the //issuance package.
|
|
CertificateProfileName: req.NewOrder.CertificateProfileName,
|
|
Replaces: req.NewOrder.Replaces,
|
|
}
|
|
|
|
// Calculate the order status before returning it. Since it may have reused
|
|
// all valid authorizations the order may be "born" in a ready status.
|
|
status, err := statusForOrder(res, authzValidityInfo, ssa.clk.Now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.Status = status
|
|
|
|
return res, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
order, ok := output.(*corepb.Order)
|
|
if !ok {
|
|
return nil, fmt.Errorf("casting error in NewOrderAndAuthzs")
|
|
}
|
|
|
|
return order, nil
|
|
}
|
|
|
|
// SetOrderProcessing updates an order from pending status to processing
|
|
// status by updating the `beganProcessing` field of the corresponding
|
|
// Order table row in the DB.
|
|
func (ssa *SQLStorageAuthority) SetOrderProcessing(ctx context.Context, req *sapb.OrderRequest) (*emptypb.Empty, error) {
|
|
if req.Id == 0 {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
_, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
result, err := tx.ExecContext(ctx, `
|
|
UPDATE orders
|
|
SET beganProcessing = ?
|
|
WHERE id = ?
|
|
AND beganProcessing = ?`,
|
|
true,
|
|
req.Id,
|
|
false)
|
|
if err != nil {
|
|
return nil, berrors.InternalServerError("error updating order to beganProcessing status")
|
|
}
|
|
|
|
n, err := result.RowsAffected()
|
|
if err != nil || n == 0 {
|
|
return nil, berrors.OrderNotReadyError("Order was already processing. This may indicate your client finalized the same order multiple times, possibly due to a client bug.")
|
|
}
|
|
|
|
return nil, nil
|
|
})
|
|
if overallError != nil {
|
|
return nil, overallError
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// SetOrderError updates a provided Order's error field.
|
|
func (ssa *SQLStorageAuthority) SetOrderError(ctx context.Context, req *sapb.SetOrderErrorRequest) (*emptypb.Empty, error) {
|
|
if req.Id == 0 || req.Error == nil {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
_, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
om, err := orderToModel(&corepb.Order{
|
|
Id: req.Id,
|
|
Error: req.Error,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := tx.ExecContext(ctx, `
|
|
UPDATE orders
|
|
SET error = ?
|
|
WHERE id = ?`,
|
|
om.Error,
|
|
om.ID)
|
|
if err != nil {
|
|
return nil, berrors.InternalServerError("error updating order error field")
|
|
}
|
|
|
|
n, err := result.RowsAffected()
|
|
if err != nil || n == 0 {
|
|
return nil, berrors.InternalServerError("no order updated with new error field")
|
|
}
|
|
|
|
return nil, nil
|
|
})
|
|
if overallError != nil {
|
|
return nil, overallError
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// FinalizeOrder finalizes a provided *corepb.Order by persisting the
|
|
// CertificateSerial and a valid status to the database. No fields other than
|
|
// CertificateSerial and the order ID on the provided order are processed (e.g.
|
|
// this is not a generic update RPC).
|
|
func (ssa *SQLStorageAuthority) FinalizeOrder(ctx context.Context, req *sapb.FinalizeOrderRequest) (*emptypb.Empty, error) {
|
|
if req.Id == 0 || req.CertificateSerial == "" {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
_, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
result, err := tx.ExecContext(ctx, `
|
|
UPDATE orders
|
|
SET certificateSerial = ?
|
|
WHERE id = ? AND
|
|
beganProcessing = true`,
|
|
req.CertificateSerial,
|
|
req.Id)
|
|
if err != nil {
|
|
return nil, berrors.InternalServerError("error updating order for finalization")
|
|
}
|
|
|
|
n, err := result.RowsAffected()
|
|
if err != nil || n == 0 {
|
|
return nil, berrors.InternalServerError("no order updated for finalization")
|
|
}
|
|
|
|
// Delete the orderFQDNSet row for the order now that it has been finalized.
|
|
// We use this table for order reuse and should not reuse a finalized order.
|
|
err = deleteOrderFQDNSet(ctx, tx, req.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = setReplacementOrderFinalized(ctx, tx, req.Id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, nil
|
|
})
|
|
if overallError != nil {
|
|
return nil, overallError
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// FinalizeAuthorization2 moves a pending authorization to either the valid or invalid status. If
|
|
// the authorization is being moved to invalid the validationError field must be set. If the
|
|
// authorization is being moved to valid the validationRecord and expires fields must be set.
|
|
func (ssa *SQLStorageAuthority) FinalizeAuthorization2(ctx context.Context, req *sapb.FinalizeAuthorizationRequest) (*emptypb.Empty, error) {
|
|
if core.IsAnyNilOrZero(req.Status, req.Attempted, req.Id, req.Expires) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
if req.Status != string(core.StatusValid) && req.Status != string(core.StatusInvalid) {
|
|
return nil, berrors.InternalServerError("authorization must have status valid or invalid")
|
|
}
|
|
query := `UPDATE authz2 SET
|
|
status = :status,
|
|
attempted = :attempted,
|
|
attemptedAt = :attemptedAt,
|
|
validationRecord = :validationRecord,
|
|
validationError = :validationError,
|
|
expires = :expires
|
|
WHERE id = :id AND status = :pending`
|
|
var validationRecords []core.ValidationRecord
|
|
for _, recordPB := range req.ValidationRecords {
|
|
record, err := bgrpc.PBToValidationRecord(recordPB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Attempted == string(core.ChallengeTypeHTTP01) {
|
|
// Remove these fields because they can be rehydrated later
|
|
// on from the URL field.
|
|
record.Hostname = ""
|
|
record.Port = ""
|
|
}
|
|
validationRecords = append(validationRecords, record)
|
|
}
|
|
vrJSON, err := json.Marshal(validationRecords)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var veJSON []byte
|
|
if req.ValidationError != nil {
|
|
validationError, err := bgrpc.PBToProblemDetails(req.ValidationError)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
j, err := json.Marshal(validationError)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
veJSON = j
|
|
}
|
|
// Check to see if the AttemptedAt time is non zero and convert to
|
|
// *time.Time if so. If it is zero, leave nil and don't convert. Keep the
|
|
// database attemptedAt field Null instead of 1970-01-01 00:00:00.
|
|
var attemptedTime *time.Time
|
|
if !core.IsAnyNilOrZero(req.AttemptedAt) {
|
|
val := req.AttemptedAt.AsTime()
|
|
attemptedTime = &val
|
|
}
|
|
params := map[string]interface{}{
|
|
"status": statusToUint[core.AcmeStatus(req.Status)],
|
|
"attempted": challTypeToUint[req.Attempted],
|
|
"attemptedAt": attemptedTime,
|
|
"validationRecord": vrJSON,
|
|
"id": req.Id,
|
|
"pending": statusUint(core.StatusPending),
|
|
"expires": req.Expires.AsTime(),
|
|
// if req.ValidationError is nil veJSON should also be nil
|
|
// which should result in a NULL field
|
|
"validationError": veJSON,
|
|
}
|
|
|
|
res, err := ssa.dbMap.ExecContext(ctx, query, params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rows == 0 {
|
|
return nil, berrors.NotFoundError("no pending authorization with id %d", req.Id)
|
|
} else if rows > 1 {
|
|
return nil, berrors.InternalServerError("multiple rows updated for authorization id %d", req.Id)
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// addRevokedCertificate is a helper used by both RevokeCertificate and
|
|
// UpdateRevokedCertificate. It inserts a new row into the revokedCertificates
|
|
// table based on the contents of the input request. The second argument must be
|
|
// a transaction object so that it is safe to conduct multiple queries with a
|
|
// consistent view of the database. It must only be called when the request
|
|
// specifies a non-zero ShardIdx.
|
|
func addRevokedCertificate(ctx context.Context, tx db.Executor, req *sapb.RevokeCertificateRequest, revokedDate time.Time) error {
|
|
if req.ShardIdx == 0 {
|
|
return errors.New("cannot add revoked certificate with shard index 0")
|
|
}
|
|
|
|
var serial struct {
|
|
Expires time.Time
|
|
}
|
|
err := tx.SelectOne(
|
|
ctx, &serial, `SELECT expires FROM serials WHERE serial = ?`, req.Serial)
|
|
if err != nil {
|
|
return fmt.Errorf("retrieving revoked certificate expiration: %w", err)
|
|
}
|
|
|
|
err = tx.Insert(ctx, &revokedCertModel{
|
|
IssuerID: req.IssuerID,
|
|
Serial: req.Serial,
|
|
ShardIdx: req.ShardIdx,
|
|
RevokedDate: revokedDate,
|
|
RevokedReason: revocation.Reason(req.Reason),
|
|
// Round the notAfter up to the next hour, to reduce index size while still
|
|
// ensuring we correctly serve revocation info past the actual expiration.
|
|
NotAfterHour: serial.Expires.Add(time.Hour).Truncate(time.Hour),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("inserting revoked certificate row: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RevokeCertificate stores revocation information about a certificate. It will only store this
|
|
// information if the certificate is not already marked as revoked.
|
|
//
|
|
// If ShardIdx is non-zero, RevokeCertificate also writes an entry for this certificate to
|
|
// the revokedCertificates table, with the provided shard number.
|
|
func (ssa *SQLStorageAuthority) RevokeCertificate(ctx context.Context, req *sapb.RevokeCertificateRequest) (*emptypb.Empty, error) {
|
|
if core.IsAnyNilOrZero(req.Serial, req.IssuerID, req.Date) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
_, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
revokedDate := req.Date.AsTime()
|
|
|
|
res, err := tx.ExecContext(ctx,
|
|
`UPDATE certificateStatus SET
|
|
status = ?,
|
|
revokedReason = ?,
|
|
revokedDate = ?,
|
|
ocspLastUpdated = ?
|
|
WHERE serial = ? AND status != ?`,
|
|
string(core.OCSPStatusRevoked),
|
|
revocation.Reason(req.Reason),
|
|
revokedDate,
|
|
revokedDate,
|
|
req.Serial,
|
|
string(core.OCSPStatusRevoked),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rows == 0 {
|
|
return nil, berrors.AlreadyRevokedError("no certificate with serial %s and status other than %s", req.Serial, string(core.OCSPStatusRevoked))
|
|
}
|
|
|
|
if req.ShardIdx != 0 {
|
|
err = addRevokedCertificate(ctx, tx, req, revokedDate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
})
|
|
if overallError != nil {
|
|
return nil, overallError
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// UpdateRevokedCertificate stores new revocation information about an
|
|
// already-revoked certificate. It will only store this information if the
|
|
// cert is already revoked, if the new revocation reason is `KeyCompromise`,
|
|
// and if the revokedDate is identical to the current revokedDate.
|
|
func (ssa *SQLStorageAuthority) UpdateRevokedCertificate(ctx context.Context, req *sapb.RevokeCertificateRequest) (*emptypb.Empty, error) {
|
|
if core.IsAnyNilOrZero(req.Serial, req.IssuerID, req.Date, req.Backdate) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
if req.Reason != ocsp.KeyCompromise {
|
|
return nil, fmt.Errorf("cannot update revocation for any reason other than keyCompromise (1); got: %d", req.Reason)
|
|
}
|
|
|
|
_, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
thisUpdate := req.Date.AsTime()
|
|
revokedDate := req.Backdate.AsTime()
|
|
|
|
res, err := tx.ExecContext(ctx,
|
|
`UPDATE certificateStatus SET
|
|
revokedReason = ?,
|
|
ocspLastUpdated = ?
|
|
WHERE serial = ? AND status = ? AND revokedReason != ? AND revokedDate = ?`,
|
|
revocation.Reason(ocsp.KeyCompromise),
|
|
thisUpdate,
|
|
req.Serial,
|
|
string(core.OCSPStatusRevoked),
|
|
revocation.Reason(ocsp.KeyCompromise),
|
|
revokedDate,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rows == 0 {
|
|
// InternalServerError because we expected this certificate status to exist,
|
|
// to already be revoked for a different reason, and to have a matching date.
|
|
return nil, berrors.InternalServerError("no certificate with serial %s and revoked reason other than keyCompromise", req.Serial)
|
|
}
|
|
|
|
// Only update the revokedCertificates table if the revocation request
|
|
// specifies the CRL shard that this certificate belongs in. Our shards are
|
|
// one-indexed, so a ShardIdx of zero means no value was set.
|
|
if req.ShardIdx != 0 {
|
|
var rcm revokedCertModel
|
|
// Note: this query MUST be updated to enforce the same preconditions as
|
|
// the "UPDATE certificateStatus SET revokedReason..." above if this
|
|
// query ever becomes the first or only query in this transaction. We are
|
|
// currently relying on the query above to exit early if the certificate
|
|
// does not have an appropriate status and revocation reason.
|
|
err = tx.SelectOne(
|
|
ctx, &rcm, `SELECT * FROM revokedCertificates WHERE serial = ?`, req.Serial)
|
|
if db.IsNoRows(err) {
|
|
// TODO: Remove this fallback codepath once we know that all unexpired
|
|
// certs marked as revoked in the certificateStatus table have
|
|
// corresponding rows in the revokedCertificates table. That should be
|
|
// 90+ days after the RA starts sending ShardIdx in its
|
|
// RevokeCertificateRequest messages.
|
|
err = addRevokedCertificate(ctx, tx, req, revokedDate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, nil
|
|
} else if err != nil {
|
|
return nil, fmt.Errorf("retrieving revoked certificate row: %w", err)
|
|
}
|
|
|
|
rcm.RevokedReason = revocation.Reason(ocsp.KeyCompromise)
|
|
_, err = tx.Update(ctx, &rcm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("updating revoked certificate row: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
})
|
|
if overallError != nil {
|
|
return nil, overallError
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// AddBlockedKey adds a key hash to the blockedKeys table
|
|
func (ssa *SQLStorageAuthority) AddBlockedKey(ctx context.Context, req *sapb.AddBlockedKeyRequest) (*emptypb.Empty, error) {
|
|
if core.IsAnyNilOrZero(req.KeyHash, req.Added, req.Source) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
sourceInt, ok := stringToSourceInt[req.Source]
|
|
if !ok {
|
|
return nil, errors.New("unknown source")
|
|
}
|
|
cols, qs := blockedKeysColumns, "?, ?, ?, ?"
|
|
vals := []interface{}{
|
|
req.KeyHash,
|
|
req.Added.AsTime(),
|
|
sourceInt,
|
|
req.Comment,
|
|
}
|
|
if req.RevokedBy != 0 {
|
|
cols += ", revokedBy"
|
|
qs += ", ?"
|
|
vals = append(vals, req.RevokedBy)
|
|
}
|
|
_, err := ssa.dbMap.ExecContext(ctx,
|
|
fmt.Sprintf("INSERT INTO blockedKeys (%s) VALUES (%s)", cols, qs),
|
|
vals...,
|
|
)
|
|
if err != nil {
|
|
if db.IsDuplicate(err) {
|
|
// Ignore duplicate inserts so multiple certs with the same key can
|
|
// be revoked.
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// Health implements the grpc.checker interface.
|
|
func (ssa *SQLStorageAuthority) Health(ctx context.Context) error {
|
|
err := ssa.dbMap.SelectOne(ctx, new(int), "SELECT 1")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ssa.SQLStorageAuthorityRO.Health(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LeaseCRLShard marks a single crlShards row as leased until the given time.
|
|
// If the request names a specific shard, this function will return an error
|
|
// if that shard is already leased. Otherwise, this function will return the
|
|
// index of the oldest shard for the given issuer.
|
|
func (ssa *SQLStorageAuthority) LeaseCRLShard(ctx context.Context, req *sapb.LeaseCRLShardRequest) (*sapb.LeaseCRLShardResponse, error) {
|
|
if core.IsAnyNilOrZero(req.Until, req.IssuerNameID) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
if req.Until.AsTime().Before(ssa.clk.Now()) {
|
|
return nil, fmt.Errorf("lease timestamp must be in the future, got %q", req.Until.AsTime())
|
|
}
|
|
|
|
if req.MinShardIdx == req.MaxShardIdx {
|
|
return ssa.leaseSpecificCRLShard(ctx, req)
|
|
}
|
|
|
|
return ssa.leaseOldestCRLShard(ctx, req)
|
|
}
|
|
|
|
// leaseOldestCRLShard finds the oldest unleased crl shard for the given issuer
|
|
// and then leases it. Shards within the requested range which have never been
|
|
// leased or are previously-unknown indices are considered older than any other
|
|
// shard. It returns an error if all shards for the issuer are already leased.
|
|
func (ssa *SQLStorageAuthority) leaseOldestCRLShard(ctx context.Context, req *sapb.LeaseCRLShardRequest) (*sapb.LeaseCRLShardResponse, error) {
|
|
shardIdx, err := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
var shards []*crlShardModel
|
|
_, err := tx.Select(
|
|
ctx,
|
|
&shards,
|
|
`SELECT id, issuerID, idx, thisUpdate, nextUpdate, leasedUntil
|
|
FROM crlShards
|
|
WHERE issuerID = ?
|
|
AND idx BETWEEN ? AND ?`,
|
|
req.IssuerNameID, req.MinShardIdx, req.MaxShardIdx,
|
|
)
|
|
if err != nil {
|
|
return -1, fmt.Errorf("selecting candidate shards: %w", err)
|
|
}
|
|
|
|
// Determine which shard index we want to lease.
|
|
var shardIdx int
|
|
if len(shards) < (int(req.MaxShardIdx + 1 - req.MinShardIdx)) {
|
|
// Some expected shards are missing (i.e. never-before-produced), so we
|
|
// pick one at random.
|
|
missing := make(map[int]struct{}, req.MaxShardIdx+1-req.MinShardIdx)
|
|
for i := req.MinShardIdx; i <= req.MaxShardIdx; i++ {
|
|
missing[int(i)] = struct{}{}
|
|
}
|
|
for _, shard := range shards {
|
|
delete(missing, shard.Idx)
|
|
}
|
|
for idx := range missing {
|
|
// Go map iteration is guaranteed to be in randomized key order.
|
|
shardIdx = idx
|
|
break
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx,
|
|
`INSERT INTO crlShards (issuerID, idx, leasedUntil)
|
|
VALUES (?, ?, ?)`,
|
|
req.IssuerNameID,
|
|
shardIdx,
|
|
req.Until.AsTime(),
|
|
)
|
|
if err != nil {
|
|
return -1, fmt.Errorf("inserting selected shard: %w", err)
|
|
}
|
|
} else {
|
|
// We got all the shards we expect, so we pick the oldest unleased shard.
|
|
var oldest *crlShardModel
|
|
for _, shard := range shards {
|
|
if shard.LeasedUntil.After(ssa.clk.Now()) {
|
|
continue
|
|
}
|
|
if oldest == nil ||
|
|
(oldest.ThisUpdate != nil && shard.ThisUpdate == nil) ||
|
|
(oldest.ThisUpdate != nil && shard.ThisUpdate.Before(*oldest.ThisUpdate)) {
|
|
oldest = shard
|
|
}
|
|
}
|
|
if oldest == nil {
|
|
return -1, fmt.Errorf("issuer %d has no unleased shards in range %d-%d", req.IssuerNameID, req.MinShardIdx, req.MaxShardIdx)
|
|
}
|
|
shardIdx = oldest.Idx
|
|
|
|
res, err := tx.ExecContext(ctx,
|
|
`UPDATE crlShards
|
|
SET leasedUntil = ?
|
|
WHERE issuerID = ?
|
|
AND idx = ?
|
|
AND leasedUntil = ?
|
|
LIMIT 1`,
|
|
req.Until.AsTime(),
|
|
req.IssuerNameID,
|
|
shardIdx,
|
|
oldest.LeasedUntil,
|
|
)
|
|
if err != nil {
|
|
return -1, fmt.Errorf("updating selected shard: %w", err)
|
|
}
|
|
rowsAffected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return -1, fmt.Errorf("confirming update of selected shard: %w", err)
|
|
}
|
|
if rowsAffected != 1 {
|
|
return -1, errors.New("failed to lease shard")
|
|
}
|
|
}
|
|
|
|
return shardIdx, err
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("leasing oldest shard: %w", err)
|
|
}
|
|
|
|
return &sapb.LeaseCRLShardResponse{
|
|
IssuerNameID: req.IssuerNameID,
|
|
ShardIdx: int64(shardIdx.(int)),
|
|
}, nil
|
|
}
|
|
|
|
// leaseSpecificCRLShard attempts to lease the crl shard for the given issuer
|
|
// and shard index. It returns an error if the specified shard is already
|
|
// leased.
|
|
func (ssa *SQLStorageAuthority) leaseSpecificCRLShard(ctx context.Context, req *sapb.LeaseCRLShardRequest) (*sapb.LeaseCRLShardResponse, error) {
|
|
if req.MinShardIdx != req.MaxShardIdx {
|
|
return nil, fmt.Errorf("request must identify a single shard index: %d != %d", req.MinShardIdx, req.MaxShardIdx)
|
|
}
|
|
|
|
_, err := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
needToInsert := false
|
|
var shardModel crlShardModel
|
|
err := tx.SelectOne(ctx,
|
|
&shardModel,
|
|
`SELECT leasedUntil
|
|
FROM crlShards
|
|
WHERE issuerID = ?
|
|
AND idx = ?
|
|
LIMIT 1`,
|
|
req.IssuerNameID,
|
|
req.MinShardIdx,
|
|
)
|
|
if db.IsNoRows(err) {
|
|
needToInsert = true
|
|
} else if err != nil {
|
|
return nil, fmt.Errorf("selecting requested shard: %w", err)
|
|
} else if shardModel.LeasedUntil.After(ssa.clk.Now()) {
|
|
return nil, fmt.Errorf("shard %d for issuer %d already leased", req.MinShardIdx, req.IssuerNameID)
|
|
}
|
|
|
|
if needToInsert {
|
|
_, err = tx.ExecContext(ctx,
|
|
`INSERT INTO crlShards (issuerID, idx, leasedUntil)
|
|
VALUES (?, ?, ?)`,
|
|
req.IssuerNameID,
|
|
req.MinShardIdx,
|
|
req.Until.AsTime(),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("inserting selected shard: %w", err)
|
|
}
|
|
} else {
|
|
res, err := tx.ExecContext(ctx,
|
|
`UPDATE crlShards
|
|
SET leasedUntil = ?
|
|
WHERE issuerID = ?
|
|
AND idx = ?
|
|
AND leasedUntil = ?
|
|
LIMIT 1`,
|
|
req.Until.AsTime(),
|
|
req.IssuerNameID,
|
|
req.MinShardIdx,
|
|
shardModel.LeasedUntil,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("updating selected shard: %w", err)
|
|
}
|
|
rowsAffected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return -1, fmt.Errorf("confirming update of selected shard: %w", err)
|
|
}
|
|
if rowsAffected != 1 {
|
|
return -1, errors.New("failed to lease shard")
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("leasing specific shard: %w", err)
|
|
}
|
|
|
|
return &sapb.LeaseCRLShardResponse{
|
|
IssuerNameID: req.IssuerNameID,
|
|
ShardIdx: req.MinShardIdx,
|
|
}, nil
|
|
}
|
|
|
|
// UpdateCRLShard updates the thisUpdate and nextUpdate timestamps of a CRL
|
|
// shard. It rejects the update if it would cause the thisUpdate timestamp to
|
|
// move backwards, but if thisUpdate would stay the same (for instance, multiple
|
|
// CRL generations within a single second), it will succeed.
|
|
//
|
|
// It does *not* reject the update if the shard is no longer
|
|
// leased: although this would be unexpected (because the lease timestamp should
|
|
// be the same as the crl-updater's context expiration), it's not inherently a
|
|
// sign of an update that should be skipped. It does reject the update if the
|
|
// identified CRL shard does not exist in the database (it should exist, as
|
|
// rows are created if necessary when leased). It also sets the leasedUntil time
|
|
// to be equal to thisUpdate, to indicate that the shard is no longer leased.
|
|
func (ssa *SQLStorageAuthority) UpdateCRLShard(ctx context.Context, req *sapb.UpdateCRLShardRequest) (*emptypb.Empty, error) {
|
|
if core.IsAnyNilOrZero(req.IssuerNameID, req.ThisUpdate) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
// Only set the nextUpdate if it's actually present in the request message.
|
|
var nextUpdate *time.Time
|
|
if req.NextUpdate != nil {
|
|
nut := req.NextUpdate.AsTime()
|
|
nextUpdate = &nut
|
|
}
|
|
|
|
_, err := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
res, err := tx.ExecContext(ctx,
|
|
`UPDATE crlShards
|
|
SET thisUpdate = ?, nextUpdate = ?, leasedUntil = ?
|
|
WHERE issuerID = ?
|
|
AND idx = ?
|
|
AND (thisUpdate is NULL OR thisUpdate <= ?)
|
|
LIMIT 1`,
|
|
req.ThisUpdate.AsTime(),
|
|
nextUpdate,
|
|
req.ThisUpdate.AsTime(),
|
|
req.IssuerNameID,
|
|
req.ShardIdx,
|
|
req.ThisUpdate.AsTime(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rowsAffected, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rowsAffected == 0 {
|
|
return nil, fmt.Errorf("unable to update shard %d for issuer %d; possibly because shard exists", req.ShardIdx, req.IssuerNameID)
|
|
}
|
|
if rowsAffected != 1 {
|
|
return nil, errors.New("update affected unexpected number of rows")
|
|
}
|
|
return nil, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// PauseIdentifiers pauses a set of identifiers for the provided account. If an
|
|
// identifier is currently paused, this is a no-op. If an identifier was
|
|
// previously paused and unpaused, it will be repaused unless it was unpaused
|
|
// less than two weeks ago. The response will indicate how many identifiers were
|
|
// paused and how many were repaused. All work is accomplished in a transaction
|
|
// to limit possible race conditions.
|
|
func (ssa *SQLStorageAuthority) PauseIdentifiers(ctx context.Context, req *sapb.PauseRequest) (*sapb.PauseIdentifiersResponse, error) {
|
|
if core.IsAnyNilOrZero(req.RegistrationID, req.Identifiers) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
// Marshal the identifier now that we've crossed the RPC boundary.
|
|
idents, err := newIdentifierModelsFromPB(req.Identifiers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response := &sapb.PauseIdentifiersResponse{}
|
|
_, err = db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
|
|
for _, ident := range idents {
|
|
pauseError := func(op string, err error) error {
|
|
return fmt.Errorf("while %s identifier %s for registration ID %d: %w",
|
|
op, ident.Value, req.RegistrationID, err,
|
|
)
|
|
}
|
|
|
|
var entry pausedModel
|
|
err := tx.SelectOne(ctx, &entry, `
|
|
SELECT pausedAt, unpausedAt
|
|
FROM paused
|
|
WHERE
|
|
registrationID = ? AND
|
|
identifierType = ? AND
|
|
identifierValue = ?`,
|
|
req.RegistrationID,
|
|
ident.Type,
|
|
ident.Value,
|
|
)
|
|
|
|
switch {
|
|
case err != nil && !errors.Is(err, sql.ErrNoRows):
|
|
// Error querying the database.
|
|
return nil, pauseError("querying pause status for", err)
|
|
|
|
case err != nil && errors.Is(err, sql.ErrNoRows):
|
|
// Not currently or previously paused, insert a new pause record.
|
|
err = tx.Insert(ctx, &pausedModel{
|
|
RegistrationID: req.RegistrationID,
|
|
PausedAt: ssa.clk.Now().Truncate(time.Second),
|
|
identifierModel: identifierModel{
|
|
Type: ident.Type,
|
|
Value: ident.Value,
|
|
},
|
|
})
|
|
if err != nil && !db.IsDuplicate(err) {
|
|
return nil, pauseError("pausing", err)
|
|
}
|
|
|
|
// Identifier successfully paused.
|
|
response.Paused++
|
|
continue
|
|
|
|
case entry.UnpausedAt == nil || entry.PausedAt.After(*entry.UnpausedAt):
|
|
// Identifier is already paused.
|
|
continue
|
|
|
|
case entry.UnpausedAt.After(ssa.clk.Now().Add(-14 * 24 * time.Hour)):
|
|
// Previously unpaused less than two weeks ago, skip this identifier.
|
|
continue
|
|
|
|
case entry.UnpausedAt.After(entry.PausedAt):
|
|
// Previously paused (and unpaused), repause the identifier.
|
|
_, err := tx.ExecContext(ctx, `
|
|
UPDATE paused
|
|
SET pausedAt = ?,
|
|
unpausedAt = NULL
|
|
WHERE
|
|
registrationID = ? AND
|
|
identifierType = ? AND
|
|
identifierValue = ? AND
|
|
unpausedAt IS NOT NULL`,
|
|
ssa.clk.Now().Truncate(time.Second),
|
|
req.RegistrationID,
|
|
ident.Type,
|
|
ident.Value,
|
|
)
|
|
if err != nil {
|
|
return nil, pauseError("repausing", err)
|
|
}
|
|
|
|
// Identifier successfully repaused.
|
|
response.Repaused++
|
|
continue
|
|
|
|
default:
|
|
// This indicates a database state which should never occur.
|
|
return nil, fmt.Errorf("impossible database state encountered while pausing identifier %s",
|
|
ident.Value,
|
|
)
|
|
}
|
|
}
|
|
return nil, nil
|
|
})
|
|
if err != nil {
|
|
// Error occurred during transaction.
|
|
return nil, err
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
// UnpauseAccount uses up to 5 iterations of UPDATE queries each with a LIMIT of
|
|
// 10,000 to unpause up to 50,000 identifiers and returns a count of identifiers
|
|
// unpaused. If the returned count is 50,000 there may be more paused identifiers.
|
|
func (ssa *SQLStorageAuthority) UnpauseAccount(ctx context.Context, req *sapb.RegistrationID) (*sapb.Count, error) {
|
|
if core.IsAnyNilOrZero(req.Id) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
total := &sapb.Count{}
|
|
|
|
for i := 0; i < unpause.MaxBatches; i++ {
|
|
result, err := ssa.dbMap.ExecContext(ctx, `
|
|
UPDATE paused
|
|
SET unpausedAt = ?
|
|
WHERE
|
|
registrationID = ? AND
|
|
unpausedAt IS NULL
|
|
LIMIT ?`,
|
|
ssa.clk.Now(),
|
|
req.Id,
|
|
unpause.BatchSize,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
total.Count += rowsAffected
|
|
if rowsAffected < unpause.BatchSize {
|
|
// Fewer than batchSize rows were updated, so we're done.
|
|
break
|
|
}
|
|
}
|
|
|
|
return total, nil
|
|
}
|
|
|
|
// AddRateLimitOverride adds a rate limit override to the database. If the
|
|
// override already exists, it will be updated. If the override does not exist,
|
|
// it will be inserted and enabled. If the override exists but has been
|
|
// disabled, it will be updated but not be re-enabled. The status of the
|
|
// override is returned in Enabled field of the response. To re-enable an
|
|
// override, use the EnableRateLimitOverride method.
|
|
func (ssa *SQLStorageAuthority) AddRateLimitOverride(ctx context.Context, req *sapb.AddRateLimitOverrideRequest) (*sapb.AddRateLimitOverrideResponse, error) {
|
|
if core.IsAnyNilOrZero(req, req.Override, req.Override.LimitEnum, req.Override.BucketKey, req.Override.Count, req.Override.Burst, req.Override.Period, req.Override.Comment) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
|
|
var inserted bool
|
|
var enabled bool
|
|
now := ssa.clk.Now()
|
|
|
|
_, err := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (any, error) {
|
|
var alreadyEnabled bool
|
|
err := tx.SelectOne(ctx, &alreadyEnabled, `
|
|
SELECT enabled
|
|
FROM overrides
|
|
WHERE limitEnum = ? AND
|
|
bucketKey = ?`,
|
|
req.Override.LimitEnum,
|
|
req.Override.BucketKey,
|
|
)
|
|
|
|
switch {
|
|
case err != nil && !db.IsNoRows(err):
|
|
// Error querying the database.
|
|
return nil, fmt.Errorf("querying override for rate limit %d and bucket key %s: %w",
|
|
req.Override.LimitEnum,
|
|
req.Override.BucketKey,
|
|
err,
|
|
)
|
|
|
|
case db.IsNoRows(err):
|
|
// Insert a new overrides row.
|
|
new := overrideModelForPB(req.Override, now, true)
|
|
err = tx.Insert(ctx, &new)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("inserting override for rate limit %d and bucket key %s: %w",
|
|
req.Override.LimitEnum,
|
|
req.Override.BucketKey,
|
|
err,
|
|
)
|
|
}
|
|
inserted = true
|
|
enabled = true
|
|
|
|
default:
|
|
// Update the existing overrides row.
|
|
updated := overrideModelForPB(req.Override, now, alreadyEnabled)
|
|
_, err = tx.Update(ctx, &updated)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("updating override for rate limit %d and bucket key %s override: %w",
|
|
req.Override.LimitEnum,
|
|
req.Override.BucketKey,
|
|
err,
|
|
)
|
|
}
|
|
inserted = false
|
|
enabled = alreadyEnabled
|
|
}
|
|
return nil, nil
|
|
})
|
|
if err != nil {
|
|
// Error occurred during transaction.
|
|
return nil, err
|
|
}
|
|
return &sapb.AddRateLimitOverrideResponse{Inserted: inserted, Enabled: enabled}, nil
|
|
}
|
|
|
|
// setRateLimitOverride sets the enabled field of a rate limit override to the
|
|
// provided value and updates the updatedAt column. If the override does not
|
|
// exist, a NotFoundError is returned. If the override exists but is already in
|
|
// the requested state, this is a no-op.
|
|
func (ssa *SQLStorageAuthority) setRateLimitOverride(ctx context.Context, limitEnum int64, bucketKey string, enabled bool) (*emptypb.Empty, error) {
|
|
overrideColumnsList, err := ssa.dbMap.ColumnsForModel(overrideModel{})
|
|
if err != nil {
|
|
// This should never happen, the model is registered at init time.
|
|
return nil, fmt.Errorf("getting columns for override model: %w", err)
|
|
}
|
|
overrideColumns := strings.Join(overrideColumnsList, ", ")
|
|
_, err = db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (any, error) {
|
|
var existing overrideModel
|
|
err := tx.SelectOne(ctx, &existing,
|
|
// Use SELECT FOR UPDATE to both verify the row exists and lock it
|
|
// for the duration of the transaction.
|
|
`SELECT `+overrideColumns+` FROM overrides
|
|
WHERE limitEnum = ? AND
|
|
bucketKey = ?
|
|
FOR UPDATE`,
|
|
limitEnum,
|
|
bucketKey,
|
|
)
|
|
if err != nil {
|
|
if db.IsNoRows(err) {
|
|
return nil, berrors.NotFoundError(
|
|
"no rate limit override found for limit %d and bucket key %s",
|
|
limitEnum,
|
|
bucketKey,
|
|
)
|
|
}
|
|
return nil, fmt.Errorf("querying status of override for rate limit %d and bucket key %s: %w",
|
|
limitEnum,
|
|
bucketKey,
|
|
err,
|
|
)
|
|
}
|
|
|
|
if existing.Enabled == enabled {
|
|
// No-op
|
|
return nil, nil
|
|
}
|
|
|
|
// Update the existing overrides row.
|
|
updated := existing
|
|
updated.Enabled = enabled
|
|
updated.UpdatedAt = ssa.clk.Now()
|
|
|
|
_, err = tx.Update(ctx, &updated)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("updating status of override for rate limit %d and bucket key %s to %t: %w",
|
|
limitEnum,
|
|
bucketKey,
|
|
enabled,
|
|
err,
|
|
)
|
|
}
|
|
return nil, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
// DisableRateLimitOverride disables a rate limit override. If the override does
|
|
// not exist, a NotFoundError is returned. If the override exists but is already
|
|
// disabled, this is a no-op.
|
|
func (ssa *SQLStorageAuthority) DisableRateLimitOverride(ctx context.Context, req *sapb.DisableRateLimitOverrideRequest) (*emptypb.Empty, error) {
|
|
if core.IsAnyNilOrZero(req, req.LimitEnum, req.BucketKey) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
return ssa.setRateLimitOverride(ctx, req.LimitEnum, req.BucketKey, false)
|
|
}
|
|
|
|
// EnableRateLimitOverride enables a rate limit override. If the override does
|
|
// not exist, a NotFoundError is returned. If the override exists but is already
|
|
// enabled, this is a no-op.
|
|
func (ssa *SQLStorageAuthority) EnableRateLimitOverride(ctx context.Context, req *sapb.EnableRateLimitOverrideRequest) (*emptypb.Empty, error) {
|
|
if core.IsAnyNilOrZero(req, req.LimitEnum, req.BucketKey) {
|
|
return nil, errIncompleteRequest
|
|
}
|
|
return ssa.setRateLimitOverride(ctx, req.LimitEnum, req.BucketKey, true)
|
|
}
|