Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ type Config struct {
// during certificate issuance. This flag must be set to true in the
// RA, VA, and WFE2 services for full functionality.
DNSAccount01Enabled bool

// StoreAuthzsInTheOrder causes the SA to write to the `authzs`
// column in NewOrder and read from it in GetOrder. It should be enabled
// after the migration to add that column has been run.
StoreAuthzsInTheOrder bool
}

var fMu = new(sync.RWMutex)
Expand Down
1 change: 1 addition & 0 deletions sa/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ func initTables(dbMap *borp.DbMap) {
if !features.Get().StoreARIReplacesInOrders {
tableMap.ColMap("Replaces").SetTransient(true)
}
dbMap.AddTableWithName(orderModelWithAuthzs{}, "orders").SetKeys(true, "ID")

dbMap.AddTableWithName(orderToAuthzModel{}, "orderToAuthz").SetKeys(false, "OrderID", "AuthzID")
dbMap.AddTableWithName(orderFQDNSet{}, "orderFqdnSets").SetKeys(true, "ID")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- +migrate Up
-- SQL in section 'Up' is executed when this migration is applied

ALTER TABLE `orders` ADD COLUMN `authzs` blob DEFAULT NULL;

-- +migrate Down
-- SQL section 'Down' is executed when this migration is rolled back

ALTER TABLE `orders` DROP COLUMN `authzs`;
42 changes: 41 additions & 1 deletion sa/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
"google.golang.org/protobuf/proto"
"math"
"net/netip"
"net/url"
Expand Down Expand Up @@ -348,6 +349,19 @@ type orderModel struct {
Replaces *string
}

type orderModelWithAuthzs struct {
ID int64
RegistrationID int64
Expires time.Time
Created time.Time
Error []byte
CertificateSerial string
BeganProcessing bool
CertificateProfileName *string
Replaces *string
Authzs []byte
}

type orderToAuthzModel struct {
OrderID int64
AuthzID int64
Expand Down Expand Up @@ -382,7 +396,27 @@ func orderToModel(order *corepb.Order) (*orderModel, error) {
return om, nil
}

func modelToOrder(om *orderModel) (*corepb.Order, error) {
func modelToOrder(model any) (*corepb.Order, error) {
om, ok := model.(*orderModelWithAuthzs)
if !ok {
omv1, ok := model.(*orderModel)
if !ok {
return nil, fmt.Errorf("modelToOrder: unexpected type %T", om)
}
om = &orderModelWithAuthzs{
ID: omv1.ID,
RegistrationID: omv1.RegistrationID,
Expires: omv1.Expires,
Created: omv1.Created,
Error: omv1.Error,
CertificateSerial: omv1.CertificateSerial,
BeganProcessing: omv1.BeganProcessing,
CertificateProfileName: omv1.CertificateProfileName,
Replaces: omv1.Replaces,
Authzs: nil,
}
}

profile := ""
if om.CertificateProfileName != nil {
profile = *om.CertificateProfileName
Expand All @@ -391,6 +425,11 @@ func modelToOrder(om *orderModel) (*corepb.Order, error) {
if om.Replaces != nil {
replaces = *om.Replaces
}
var decodedAuthzs sapb.Authzs
err := proto.Unmarshal(om.Authzs, &decodedAuthzs)
if err != nil {
return nil, err
}
order := &corepb.Order{
Id: om.ID,
RegistrationID: om.RegistrationID,
Expand All @@ -400,6 +439,7 @@ func modelToOrder(om *orderModel) (*corepb.Order, error) {
BeganProcessing: om.BeganProcessing,
CertificateProfileName: profile,
Replaces: replaces,
V2Authorizations: decodedAuthzs.AuthzIDs,
}
if len(om.Error) > 0 {
var problem corepb.ProblemDetails
Expand Down
884 changes: 466 additions & 418 deletions sa/proto/sa.pb.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions sa/proto/sa.proto
Original file line number Diff line number Diff line change
Expand Up @@ -447,3 +447,8 @@ message RateLimitOverrideResponse {
bool enabled = 2;
google.protobuf.Timestamp updatedAt = 3;
}

// Used internally for storage in the DB, not for RPCs.
message Authzs {
repeated int64 authzIDs = 1;
}
52 changes: 39 additions & 13 deletions sa/sa.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"google.golang.org/protobuf/proto"
"strings"
"time"

Expand All @@ -20,6 +21,7 @@ import (
corepb "github.com/letsencrypt/boulder/core/proto"
"github.com/letsencrypt/boulder/db"
berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/features"
bgrpc "github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/identifier"
blog "github.com/letsencrypt/boulder/log"
Expand Down Expand Up @@ -477,24 +479,48 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
newAuthzIDs = append(newAuthzIDs, am.ID)
}

// Combine the already-existing and newly-created authzs.
allAuthzIds := append(req.NewOrder.V2Authorizations, newAuthzIDs...)

// 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
var orderID int64
if features.Get().StoreAuthzsInTheOrder {
encodedAuthzs, err := proto.Marshal(&sapb.Authzs{
AuthzIDs: allAuthzIds,
})
if err != nil {
return nil, err
}
om := orderModelWithAuthzs{
RegistrationID: req.NewOrder.RegistrationID,
Expires: req.NewOrder.Expires.AsTime(),
Created: created,
CertificateProfileName: &req.NewOrder.CertificateProfileName,
Replaces: &req.NewOrder.Replaces,
Authzs: encodedAuthzs,
}
err = tx.Insert(ctx, &om)
if err != nil {
return nil, err
}
orderID = om.ID
} else {
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
}
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
Expand Down
81 changes: 75 additions & 6 deletions sa/saro.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"github.com/letsencrypt/boulder/features"
"math"
"regexp"
"strings"
Expand Down Expand Up @@ -355,7 +356,13 @@ func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderR
}

txn := func(tx db.Executor) (any, error) {
omObj, err := tx.Get(ctx, orderModel{}, req.Id)
var typeObj any = orderModel{}
if features.Get().StoreAuthzsInTheOrder {
// Even though this is a read-only path, we observe the feature flag so we don't
// try to query for a column (Authzs) that doesn't exist.
typeObj = orderModelWithAuthzs{}
}
omObj, err := tx.Get(ctx, typeObj, req.Id)
if err != nil {
if db.IsNoRows(err) {
return nil, berrors.NotFoundError("no order found for ID %d", req.Id)
Expand All @@ -366,7 +373,7 @@ func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderR
return nil, berrors.NotFoundError("no order found for ID %d", req.Id)
}

order, err := modelToOrder(omObj.(*orderModel))
order, err := modelToOrder(omObj)
if err != nil {
return nil, err
}
Expand All @@ -376,11 +383,15 @@ func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderR
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
// For orders created before feature flag StoreAuthzsInTheOrder, fetch the list of authz IDs
// from the orderToAuthz2 table.
if len(order.V2Authorizations) == 0 {
authzIDs, err := authzForOrder(ctx, tx, order.Id)
if err != nil {
return nil, err
}
order.V2Authorizations = authzIDs
}
order.V2Authorizations = v2AuthzIDs

// Get the partial Authorization objects for the order
authzValidityInfo, err := getAuthorizationStatuses(ctx, tx, order.V2Authorizations)
Expand Down Expand Up @@ -497,6 +508,38 @@ func (ssa *SQLStorageAuthorityRO) GetOrderForNames(ctx context.Context, req *sap
return order, nil
}

func (ssa *SQLStorageAuthorityRO) getAuthorizationsByID(ctx context.Context, ids []int64) (*sapb.Authorizations, error) {
selector, err := db.NewMappedSelector[authzModel](ssa.dbReadOnlyMap)
if err != nil {
return nil, fmt.Errorf("initializing db map: %w", err)
}

clauses := fmt.Sprintf(`WHERE id IN (%s)`, db.QuestionMarks(len(ids)))

var sliceOfAny []any
for _, id := range ids {
sliceOfAny = append(sliceOfAny, id)
}
rows, err := selector.QueryContext(ctx, clauses, sliceOfAny...)
if err != nil {
return nil, fmt.Errorf("reading db: %w", err)
}

var ret []*corepb.Authorization
err = rows.ForEach(func(row *authzModel) error {
authz, err := modelToAuthzPB(*row)
if err != nil {
return err
}
ret = append(ret, authz)
return nil
})
if err != nil {
return nil, fmt.Errorf("reading db: %w", err)
}
return &sapb.Authorizations{Authzs: ret}, 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) {
Expand Down Expand Up @@ -656,6 +699,32 @@ func (ssa *SQLStorageAuthorityRO) GetValidOrderAuthorizations2(ctx context.Conte
return nil, errIncompleteRequest
}

if features.Get().StoreAuthzsInTheOrder {
om, err := ssa.dbReadOnlyMap.Get(ctx, &orderModelWithAuthzs{}, req.Id)
if err != nil {
if db.IsNoRows(err) {
return nil, berrors.NotFoundError("no order found for ID %d", req.Id)
}
return nil, err
}

order, err := modelToOrder(om)
if err != nil {
return nil, err
}

// If the order had a list of authz IDs (from the `Authzs` column in the DB), query
// them and return. Otherwise, fall through to doing a JOIN query using orderToAuthz2
// and authz2 tables.
if len(order.V2Authorizations) > 0 {
authzs, err := ssa.getAuthorizationsByID(ctx, order.V2Authorizations)
if err != nil {
return nil, err
}
return authzs, nil
}
}

// 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, " ")
Expand Down
1 change: 1 addition & 0 deletions test/config-next/sa.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
},
"healthCheckInterval": "4s",
"features": {
"StoreAuthzsInTheOrder": true,
"StoreARIReplacesInOrders": true
}
},
Expand Down
Loading