Skip to content

Commit 491e1e9

Browse files
authored
sa: getAuthorizationStatuses checks results (#8726)
If this function gets fewer responses than expected, that means some of the authorizations were deleted before their containing order expired (or were never written due to a bug). Error out. Fix a test that fails with this extra check in place.
1 parent 81b90d1 commit 491e1e9

7 files changed

Lines changed: 91 additions & 21 deletions

File tree

sa/db/01-boulder_sa.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ CREATE TABLE `orders` (
149149
`created` datetime NOT NULL,
150150
`certificateProfileName` varchar(32) DEFAULT NULL,
151151
`replaces` varchar(255) DEFAULT NULL,
152+
-- Contains protobuf-encoded list of authorization IDs without duplicates.
153+
-- See sa/proto/sadb.proto
152154
`authzs` blob DEFAULT NULL,
153155
PRIMARY KEY (`id`),
154156
KEY `reg_expires` (`registrationID`,`expires`),

sa/db/01-boulder_sa_next.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ CREATE TABLE `orders` (
153153
`created` datetime NOT NULL,
154154
`certificateProfileName` varchar(32) DEFAULT NULL,
155155
`replaces` varchar(255) DEFAULT NULL,
156+
-- Contains protobuf-encoded list of authorization IDs without duplicates.
157+
-- See sa/proto/sadb.proto
156158
`authzs` blob DEFAULT NULL,
157159
PRIMARY KEY (`id`),
158160
KEY `reg_expires` (`registrationID`,`expires`),

sa/model.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -335,19 +335,22 @@ func (model certificateStatusModel) toPb() *corepb.CertificateStatus {
335335
}
336336
}
337337

338-
// orderModel represents one row in the orders table. The CertificateProfileName
339-
// column is a pointer because the column is NULL-able.
338+
// orderModel represents one row in the orders table.
340339
type orderModel struct {
341-
ID int64
342-
RegistrationID int64
343-
Expires time.Time
344-
Created time.Time
345-
Error []byte
346-
CertificateSerial string
347-
BeganProcessing bool
340+
ID int64
341+
RegistrationID int64
342+
Expires time.Time
343+
Created time.Time
344+
Error []byte
345+
CertificateSerial string
346+
BeganProcessing bool
347+
// CertificateProfileName is a pointer because the column is NULL-able.
348348
CertificateProfileName *string
349-
Replaces *string
350-
Authzs []byte
349+
// Replaces is a pointer because the column is NULL-able.
350+
Replaces *string
351+
// Contains protobuf-encoded list of authorization IDs without duplicates.
352+
// See sa/proto/sadb.proto
353+
Authzs []byte
351354
}
352355

353356
func modelToOrder(om *orderModel) (*corepb.Order, error) {
@@ -1154,7 +1157,7 @@ type authzValidity struct {
11541157
Expires time.Time `db:"expires"`
11551158
}
11561159

1157-
// getAuthorizationStatuses takes a sequence of authz IDs, and returns the
1160+
// getAuthorizationStatuses takes a sequence of distinct authz IDs, and returns the
11581161
// status and expiration date of each of them.
11591162
func getAuthorizationStatuses(ctx context.Context, s db.Selector, ids []int64) ([]authzValidity, error) {
11601163
var params []any
@@ -1173,6 +1176,11 @@ func getAuthorizationStatuses(ctx context.Context, s db.Selector, ids []int64) (
11731176
return nil, err
11741177
}
11751178

1179+
if len(validities) != len(ids) {
1180+
return nil, fmt.Errorf("getAuthorizationStatuses got %d results for for %d ids: %v",
1181+
len(validities), len(ids), ids)
1182+
}
1183+
11761184
return validities, nil
11771185
}
11781186

sa/proto/sadb.pb.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sa/proto/sadb.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package sa;
44
option go_package = "github.com/letsencrypt/boulder/sa/proto";
55

66
// Used internally for storage in the DB, not for RPCs.
7+
// Invariant maintained by sa.NewOrderAndAuthzs: does not contain duplicates.
78
message Authzs {
89
repeated int64 authzIDs = 1;
910
}

sa/sa.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -450,11 +450,16 @@ func (ssa *SQLStorageAuthority) DeactivateAuthorization2(ctx context.Context, re
450450
return &emptypb.Empty{}, nil
451451
}
452452

453-
// NewOrderAndAuthzs adds the given authorizations to the database, adds their
454-
// autogenerated IDs to the given order, and then adds the order to the db.
455-
// This is done inside a single transaction to prevent situations where new
456-
// authorizations are created, but then their corresponding order is never
457-
// created, leading to "invisible" pending authorizations.
453+
// NewOrderAndAuthzs creates an order in the database.
454+
//
455+
// The order will include reused authorization IDs from the V2Authorizations slice
456+
// and newly created authorizations based on NewAuthzs slice. Either slice may be
457+
// empty but not both. Duplicate authorization IDs in V2Authorizations will error.
458+
//
459+
// Creation of new authorizations is done inside a transaction along with creation
460+
// of the order to prevent situations where new authorizations are created, but then
461+
// their corresponding order is never created, leading to "invisible" pending
462+
// authorizations.
458463
func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb.NewOrderAndAuthzsRequest) (*corepb.Order, error) {
459464
if req.NewOrder == nil {
460465
return nil, errIncompleteRequest
@@ -490,6 +495,10 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
490495
// Combine the already-existing and newly-created authzs.
491496
allAuthzIds := append(req.NewOrder.V2Authorizations, newAuthzIDs...)
492497

498+
if containsDuplicates(allAuthzIds) {
499+
return nil, errors.New("cannot add duplicate authorizations to order")
500+
}
501+
493502
// Second, insert the new order.
494503
created := ssa.clk.Now()
495504
encodedAuthzs, err := proto.Marshal(&sapb.Authzs{
@@ -577,6 +586,17 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
577586
return order, nil
578587
}
579588

589+
func containsDuplicates(ids []int64) bool {
590+
seen := make(map[int64]bool, len(ids))
591+
for _, id := range ids {
592+
if seen[id] {
593+
return true
594+
}
595+
seen[id] = true
596+
}
597+
return false
598+
}
599+
580600
// SetOrderProcessing updates an order from pending status to processing
581601
// status by updating the `beganProcessing` field of the corresponding
582602
// Order table row in the DB.

sa/sa_test.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,33 @@ func TestNewOrderAndAuthzs(t *testing.T) {
10051005
test.Assert(t, newAuthzIDs[0] != newAuthzIDs[1], "expected distinct new authz IDs")
10061006
}
10071007

1008+
func TestNewOrderAndAuthzsRejectsDuplicates(t *testing.T) {
1009+
sa, fc := initSA(t)
1010+
1011+
reg := createWorkingRegistration(t, sa)
1012+
1013+
idA := createPendingAuthorization(t, sa, reg.Id, identifier.NewDNS("a.com"), sa.clk.Now().Add(time.Hour))
1014+
_, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{
1015+
NewOrder: &sapb.NewOrderRequest{
1016+
RegistrationID: reg.Id,
1017+
Expires: timestamppb.New(fc.Now().Add(2 * time.Hour)),
1018+
Identifiers: []*corepb.Identifier{
1019+
identifier.NewDNS("a.com").ToProto(),
1020+
identifier.NewDNS("b.com").ToProto(),
1021+
},
1022+
V2Authorizations: []int64{idA, idA},
1023+
},
1024+
})
1025+
1026+
if err == nil {
1027+
t.Fatal("sa.NewOrderAndAuthzs with duplicate authorizations: got nil error, want error")
1028+
}
1029+
expected := "cannot add duplicate authorizations to order"
1030+
if err.Error() != expected {
1031+
t.Errorf("sa.NewOrderAndAuthzs with duplicate authorizations: got error %q, want error %q", err, expected)
1032+
}
1033+
}
1034+
10081035
func TestNewOrderAndAuthzs_ReuseOnly(t *testing.T) {
10091036
sa, fc := initSA(t)
10101037

@@ -2685,12 +2712,21 @@ func TestGetOrderExpired(t *testing.T) {
26852712
fc.Add(time.Hour * 5)
26862713
now := fc.Now()
26872714
reg := createWorkingRegistration(t, sa)
2715+
exampleDotCom := identifier.NewDNS("example.com").ToProto()
26882716
order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{
26892717
NewOrder: &sapb.NewOrderRequest{
2690-
RegistrationID: reg.Id,
2691-
Expires: timestamppb.New(now.Add(-time.Hour)),
2692-
Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()},
2693-
V2Authorizations: []int64{666},
2718+
RegistrationID: reg.Id,
2719+
Expires: timestamppb.New(now.Add(-time.Hour)),
2720+
Identifiers: []*corepb.Identifier{exampleDotCom},
2721+
},
2722+
NewAuthzs: []*sapb.NewAuthzRequest{
2723+
{
2724+
Identifier: exampleDotCom,
2725+
RegistrationID: reg.Id,
2726+
Expires: timestamppb.New(now.Add(time.Hour)),
2727+
ChallengeTypes: []string{string(core.ChallengeTypeHTTP01)},
2728+
Token: core.NewToken(),
2729+
},
26942730
},
26952731
})
26962732
test.AssertNotError(t, err, "NewOrderAndAuthzs failed")

0 commit comments

Comments
 (0)