Skip to content

Commit

Permalink
SA: store and return certificate profile name (#7352)
Browse files Browse the repository at this point in the history
Adds `certificateProfileName` to the `orders` database table. The
[maximum
length](https://github.com/letsencrypt/boulder/pull/7325/files#diff-a64a0af7cbf484da8e6d08d3eefdeef9314c5d9888233f0adcecd21b800102acR35)
of a profile name matches the `//issuance` package.

Adds a `MultipleCertificateProfiles` feature flag that, when enabled,
will store the certificate profile name from a `NewOrderRequest`. The
certificate profile name is allowed to be empty and the database will
treat that row as [NULL](https://mariadb.com/kb/en/null-values/). When
the SA retrieves this potentially NULL row, it will be cast as the
golang string zero value `""`.

SRE ticket IN-10145 has been filed to perform the database migration and
enable the new feature flag. The migration must be performed before
enabling the feature flag.

Part of #7324
  • Loading branch information
pgporada committed Mar 20, 2024
1 parent c6b5055 commit 8556eae
Show file tree
Hide file tree
Showing 10 changed files with 336 additions and 36 deletions.
8 changes: 8 additions & 0 deletions features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ type Config struct {
// a 'orderID' matching the finalized order to true. This will occur
// inside of the finalize (order) transaction.
TrackReplacementCertificatesARI bool

// MultipleCertificateProfiles, when enabled, triggers the following
// behavior:
// - SA.NewOrderAndAuthzs: upon receiving a NewOrderRequest with a
// `certificateProfileName` value, will add that value to the database's
// `orders.certificateProfileName` column. Values in this column are
// allowed to be empty.
MultipleCertificateProfiles bool
}

var fMu = new(sync.RWMutex)
Expand Down
24 changes: 13 additions & 11 deletions mocks/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,9 @@ func (sa *StorageAuthority) NewOrderAndAuthzs(_ context.Context, req *sapb.NewOr
Id: rand.Int63(),
Created: timestamppb.Now(),
// A new order is never processing because it can't have been finalized yet.
BeganProcessing: false,
Status: string(core.StatusPending),
BeganProcessing: false,
Status: string(core.StatusPending),
CertificateProfileName: req.NewOrder.CertificateProfileName,
}
return response, nil
}
Expand Down Expand Up @@ -420,15 +421,16 @@ func (sa *StorageAuthorityReadOnly) GetOrder(_ context.Context, req *sapb.OrderR
created := now.AddDate(-30, 0, 0)
exp := now.AddDate(30, 0, 0)
validOrder := &corepb.Order{
Id: req.Id,
RegistrationID: 1,
Created: timestamppb.New(created),
Expires: timestamppb.New(exp),
Names: []string{"example.com"},
Status: string(core.StatusValid),
V2Authorizations: []int64{1},
CertificateSerial: "serial",
Error: nil,
Id: req.Id,
RegistrationID: 1,
Created: timestamppb.New(created),
Expires: timestamppb.New(exp),
Names: []string{"example.com"},
Status: string(core.StatusValid),
V2Authorizations: []int64{1},
CertificateSerial: "serial",
Error: nil,
CertificateProfileName: "defaultBoulderCertificateProfile",
}

// Order ID doesn't have a certificate serial yet
Expand Down
10 changes: 8 additions & 2 deletions sa/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import (
"time"

"github.com/go-sql-driver/mysql"
"github.com/letsencrypt/borp"
"github.com/prometheus/client_golang/prometheus"

"github.com/letsencrypt/borp"

"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
boulderDB "github.com/letsencrypt/boulder/db"
"github.com/letsencrypt/boulder/features"
blog "github.com/letsencrypt/boulder/log"
)

Expand Down Expand Up @@ -271,7 +273,11 @@ func initTables(dbMap *borp.DbMap) {
dbMap.AddTableWithName(core.Certificate{}, "certificates").SetKeys(true, "ID")
dbMap.AddTableWithName(core.CertificateStatus{}, "certificateStatus").SetKeys(true, "ID")
dbMap.AddTableWithName(core.FQDNSet{}, "fqdnSets").SetKeys(true, "ID")
dbMap.AddTableWithName(orderModel{}, "orders").SetKeys(true, "ID")
if features.Get().MultipleCertificateProfiles {
dbMap.AddTableWithName(orderModelv2{}, "orders").SetKeys(true, "ID")
} else {
dbMap.AddTableWithName(orderModelv1{}, "orders").SetKeys(true, "ID")
}
dbMap.AddTableWithName(orderToAuthzModel{}, "orderToAuthz").SetKeys(false, "OrderID", "AuthzID")
dbMap.AddTableWithName(requestedNameModel{}, "requestedNames").SetKeys(false, "OrderID")
dbMap.AddTableWithName(orderFQDNSet{}, "orderFqdnSets").SetKeys(true, "ID")
Expand Down
9 changes: 9 additions & 0 deletions sa/db-next/boulder_sa/20240304000000_CertificateProfiles.sql
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 `certificateProfileName` varchar(32) DEFAULT NULL;

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

ALTER TABLE `orders` DROP COLUMN `certificateProfileName`;
70 changes: 66 additions & 4 deletions sa/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,8 @@ type precertificateModel struct {
Expires time.Time
}

type orderModel struct {
// TODO(#7324) orderModelv1 is deprecated, use orderModelv2 moving forward.
type orderModelv1 struct {
ID int64
RegistrationID int64
Expires time.Time
Expand All @@ -383,6 +384,17 @@ type orderModel struct {
BeganProcessing bool
}

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

type requestedNameModel struct {
ID int64
OrderID int64
Expand All @@ -394,8 +406,9 @@ type orderToAuthzModel struct {
AuthzID int64
}

func orderToModel(order *corepb.Order) (*orderModel, error) {
om := &orderModel{
// TODO(#7324) orderToModelv1 is deprecated, use orderModelv2 moving forward.
func orderToModelv1(order *corepb.Order) (*orderModelv1, error) {
om := &orderModelv1{
ID: order.Id,
RegistrationID: order.RegistrationID,
Expires: order.Expires.AsTime(),
Expand All @@ -417,7 +430,8 @@ func orderToModel(order *corepb.Order) (*orderModel, error) {
return om, nil
}

func modelToOrder(om *orderModel) (*corepb.Order, error) {
// TODO(#7324) modelToOrderv1 is deprecated, use orderModelv2 moving forward.
func modelToOrderv1(om *orderModelv1) (*corepb.Order, error) {
order := &corepb.Order{
Id: om.ID,
RegistrationID: om.RegistrationID,
Expand All @@ -440,6 +454,54 @@ func modelToOrder(om *orderModel) (*corepb.Order, error) {
return order, nil
}

func orderToModelv2(order *corepb.Order) (*orderModelv2, error) {
om := &orderModelv2{
ID: order.Id,
RegistrationID: order.RegistrationID,
Expires: order.Expires.AsTime(),
Created: order.Created.AsTime(),
BeganProcessing: order.BeganProcessing,
CertificateSerial: order.CertificateSerial,
CertificateProfileName: order.CertificateProfileName,
}

if order.Error != nil {
errJSON, err := json.Marshal(order.Error)
if err != nil {
return nil, err
}
if len(errJSON) > mediumBlobSize {
return nil, fmt.Errorf("Error object is too large to store in the database")
}
om.Error = errJSON
}
return om, nil
}

func modelToOrderv2(om *orderModelv2) (*corepb.Order, error) {
order := &corepb.Order{
Id: om.ID,
RegistrationID: om.RegistrationID,
Expires: timestamppb.New(om.Expires),
Created: timestamppb.New(om.Created),
CertificateSerial: om.CertificateSerial,
BeganProcessing: om.BeganProcessing,
CertificateProfileName: om.CertificateProfileName,
}
if len(om.Error) > 0 {
var problem corepb.ProblemDetails
err := json.Unmarshal(om.Error, &problem)
if err != nil {
return &corepb.Order{}, badJSONError(
"failed to unmarshal order model's error",
om.Error,
err)
}
order.Error = &problem
}
return order, nil
}

var challTypeToUint = map[string]uint8{
"http-01": 0,
"dns-01": 1,
Expand Down
42 changes: 39 additions & 3 deletions sa/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import (
"time"

"github.com/jmhodges/clock"
"google.golang.org/protobuf/types/known/timestamppb"

"github.com/letsencrypt/boulder/db"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/grpc"
"github.com/letsencrypt/boulder/probs"
"github.com/letsencrypt/boulder/test/vars"
"google.golang.org/protobuf/types/known/timestamppb"

"github.com/letsencrypt/boulder/core"
corepb "github.com/letsencrypt/boulder/core/proto"
Expand Down Expand Up @@ -248,15 +249,50 @@ func TestAuthzModel(t *testing.T) {
// validation error JSON field to an Order produces the expected bad JSON error.
func TestModelToOrderBadJSON(t *testing.T) {
badJSON := []byte(`{`)
_, err := modelToOrder(&orderModel{
_, err := modelToOrderv2(&orderModelv2{
Error: badJSON,
})
test.AssertError(t, err, "expected error from modelToOrder")
test.AssertError(t, err, "expected error from modelToOrderv2")
var badJSONErr errBadJSON
test.AssertErrorWraps(t, err, &badJSONErr)
test.AssertEquals(t, string(badJSONErr.json), string(badJSON))
}

func TestOrderModelThereAndBackAgain(t *testing.T) {
clk := clock.New()
now := clk.Now()
order := &corepb.Order{
Id: 0,
RegistrationID: 2016,
Expires: timestamppb.New(now.Add(24 * time.Hour)),
Created: timestamppb.New(now),
Error: nil,
CertificateSerial: "1",
BeganProcessing: true,
}
model1, err := orderToModelv1(order)
test.AssertNotError(t, err, "orderToModelv1 should not have errored")
returnOrder, err := modelToOrderv1(model1)
test.AssertNotError(t, err, "modelToOrderv1 should not have errored")
test.AssertDeepEquals(t, order, returnOrder)

anotherOrder := &corepb.Order{
Id: 1,
RegistrationID: 2024,
Expires: timestamppb.New(now.Add(24 * time.Hour)),
Created: timestamppb.New(now),
Error: nil,
CertificateSerial: "2",
BeganProcessing: true,
CertificateProfileName: "phljny",
}
model2, err := orderToModelv2(anotherOrder)
test.AssertNotError(t, err, "orderToModelv2 should not have errored")
returnOrder, err = modelToOrderv2(model2)
test.AssertNotError(t, err, "modelToOrderv2 should not have errored")
test.AssertDeepEquals(t, anotherOrder, returnOrder)
}

// TestPopulateAttemptedFieldsBadJSON tests that populating a challenge from an
// authz2 model with an invalid validation error or an invalid validation record
// produces the expected bad JSON error.
Expand Down
45 changes: 32 additions & 13 deletions sa/sa.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,12 +498,27 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
}

// Second, insert the new order.
order := &orderModel{
RegistrationID: req.NewOrder.RegistrationID,
Expires: req.NewOrder.Expires.AsTime(),
Created: ssa.clk.Now(),
var orderID int64
var err error
created := ssa.clk.Now()
if features.Get().MultipleCertificateProfiles {
omv2 := orderModelv2{
RegistrationID: req.NewOrder.RegistrationID,
Expires: req.NewOrder.Expires.AsTime(),
Created: created,
CertificateProfileName: req.NewOrder.CertificateProfileName,
}
err = tx.Insert(ctx, &omv2)
orderID = omv2.ID
} else {
omv1 := orderModelv1{
RegistrationID: req.NewOrder.RegistrationID,
Expires: req.NewOrder.Expires.AsTime(),
Created: created,
}
err = tx.Insert(ctx, &omv1)
orderID = omv1.ID
}
err := tx.Insert(ctx, order)
if err != nil {
return nil, err
}
Expand All @@ -514,13 +529,13 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
return nil, err
}
for _, id := range req.NewOrder.V2Authorizations {
err = inserter.Add([]interface{}{order.ID, id})
err := inserter.Add([]interface{}{orderID, id})
if err != nil {
return nil, err
}
}
for _, id := range newAuthzIDs {
err = inserter.Add([]interface{}{order.ID, id})
err := inserter.Add([]interface{}{orderID, id})
if err != nil {
return nil, err
}
Expand All @@ -536,7 +551,7 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
return nil, err
}
for _, name := range req.NewOrder.Names {
err = inserter.Add([]interface{}{order.ID, ReverseName(name)})
err := inserter.Add([]interface{}{orderID, ReverseName(name)})
if err != nil {
return nil, err
}
Expand All @@ -547,16 +562,16 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
}

// Fifth, insert the FQDNSet entry for the order.
err = addOrderFQDNSet(ctx, tx, req.NewOrder.Names, order.ID, order.RegistrationID, order.Expires)
err = addOrderFQDNSet(ctx, tx, req.NewOrder.Names, orderID, req.NewOrder.RegistrationID, req.NewOrder.Expires.AsTime())
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: order.ID,
Created: timestamppb.New(order.Created),
Id: orderID,
Created: timestamppb.New(created),
// These are carried over from the original request unchanged.
RegistrationID: req.NewOrder.RegistrationID,
Expires: req.NewOrder.Expires,
Expand All @@ -565,12 +580,16 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
V2Authorizations: append(req.NewOrder.V2Authorizations, newAuthzIDs...),
// 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,
}

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, order.ID, order.Expires)
err := addReplacementOrder(ctx, tx, req.NewOrder.ReplacesSerial, orderID, req.NewOrder.Expires.AsTime())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -643,7 +662,7 @@ func (ssa *SQLStorageAuthority) SetOrderError(ctx context.Context, req *sapb.Set
return nil, errIncompleteRequest
}
_, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) {
om, err := orderToModel(&corepb.Order{
om, err := orderToModelv2(&corepb.Order{
Id: req.Id,
Error: req.Error,
})
Expand Down
Loading

0 comments on commit 8556eae

Please sign in to comment.