Skip to content

Commit 66a19fc

Browse files
committed
Progress on reading orders from new schema
1 parent e627f55 commit 66a19fc

File tree

8 files changed

+245
-7
lines changed

8 files changed

+245
-7
lines changed

core/util.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ import (
2727
"unicode"
2828

2929
"github.com/go-jose/go-jose/v4"
30-
"github.com/letsencrypt/boulder/identifier"
3130
"google.golang.org/protobuf/types/known/durationpb"
3231
"google.golang.org/protobuf/types/known/timestamppb"
32+
33+
"github.com/letsencrypt/boulder/identifier"
3334
)
3435

3536
const Unspecified = "Unspecified"

sa/database.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,10 @@ func initTables(dbMap *borp.DbMap) {
291291
dbMap.AddTableWithName(revokedCertModel{}, "revokedCertificates").SetKeys(true, "ID")
292292
dbMap.AddTableWithName(replacementOrderModel{}, "replacementOrders").SetKeys(true, "ID")
293293
dbMap.AddTableWithName(pausedModel{}, "paused")
294+
dbMap.AddTableWithName(orders2Model{}, "orders2")
295+
dbMap.AddTableWithName(authorizationsModel{}, "authorizations")
296+
dbMap.AddTableWithName(validationsModel{}, "validations")
297+
dbMap.AddTableWithName(authzReuseModel{}, "authzReuse")
294298

295299
// Read-only maps used for selecting subsets of columns.
296300
dbMap.AddTableWithName(CertStatusMetadata{}, "certificateStatus")

sa/db-next/boulder_sa/20240801000000_OrderSchema.sql

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ CREATE TABLE `orders2` (
1414
`registrationID` bigint(20) UNSIGNED NOT NULL,
1515
`created` datetime NOT NULL,
1616
`expires` datetime NOT NULL,
17-
`authorizations` json NOT NULL,
17+
`authorizationIDs` json NOT NULL,
1818
`profile` varchar(255) NOT NULL,
1919
`beganProcessing` boolean NOT NULL,
2020
`error` mediumblob DEFAULT NULL,
@@ -40,7 +40,7 @@ CREATE TABLE `authorizations` (
4040
`challenges` tinyint(4) NOT NULL,
4141
`token` binary(32) NOT NULL,
4242
`status` tinyint(4) NOT NULL,
43-
`validations` json DEFAULT NULL,
43+
`validationIDs` json DEFAULT NULL,
4444
PRIMARY KEY (`id`),
4545
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
4646
PARTITION BY RANGE(id)
@@ -52,7 +52,6 @@ CREATE TABLE `authorizations` (
5252
-- invalid), and an opaque blob of our audit record.
5353
CREATE TABLE `validations` (
5454
`id` bigint(20) UNSIGNED NOT NULL,
55-
`registrationID` bigint(20) UNSIGNED NOT NULL,
5655
`challenge` tinyint(4) NOT NULL,
5756
`attemptedAt` datetime NOT NULL,
5857
`status` tinyint(4) NOT NULL,
@@ -66,7 +65,7 @@ CREATE TABLE `validations` (
6665
-- IDs. This allos us to not have expensive indices on the authorizations table.
6766
CREATE TABLE `authzReuse` (
6867
`accountID_identifier` VARCHAR(300) NOT NULL,
69-
`authzID` VARCHAR(255) NOT NULL,
68+
`authzID` bigint(20) UNSIGNED NOT NULL,
7069
`expires` DATETIME NOT NULL
7170
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
7271
PARTITION BY RANGE(id)

sa/model.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package sa
22

33
import (
44
"context"
5+
"crypto/rand"
56
"crypto/sha256"
67
"crypto/x509"
78
"database/sql"
89
"encoding/base64"
10+
"encoding/binary"
911
"encoding/json"
1012
"errors"
1113
"fmt"
@@ -17,6 +19,7 @@ import (
1719
"time"
1820

1921
"github.com/go-jose/go-jose/v4"
22+
"github.com/jmhodges/clock"
2023
"google.golang.org/protobuf/types/known/timestamppb"
2124

2225
"github.com/letsencrypt/boulder/core"
@@ -59,6 +62,54 @@ func badJSONError(msg string, jsonData []byte, err error) error {
5962
}
6063
}
6164

65+
// newRandomID creates a 64-bit mostly-random number to be used as the
66+
// unique ID column in a table which no longer uses auto_increment IDs. It takes
67+
// the clock as an argument so that it can include the current "epoch" as the
68+
// first byte of the ID, for the sake of easily dropping old data.
69+
func newRandomID(clk clock.Clock) (int64, error) {
70+
idBytes := make([]byte, 8) // 8 bytes is 64 bits
71+
72+
// Read random bits into the lower 7 bytes of the id.
73+
_, err := rand.Read(idBytes[1:])
74+
if err != nil {
75+
return 0, fmt.Errorf("while generating unique database id: %w", err)
76+
}
77+
78+
// Epochs are arbitrarily chosen to be 90 day chunks counting from the start
79+
// of 2024. This gives us 127 * 90 = ~31 years worth of epochs before we have
80+
// to worry about a rollover.
81+
epoch := uint8(clk.Now().Sub(time.Date(2024, 01, 01, 00, 00, 00, 00, time.UTC)) / (90 * 24 * time.Hour))
82+
if epoch&0x80 != 0 {
83+
// If the first bit is a 1, either the current date is before the epoch
84+
// start date, or we've gone too far into the future. Error out before we
85+
// accidentally generate a negative ID.
86+
return 0, fmt.Errorf("invalid epoch: %d", epoch)
87+
}
88+
idBytes[0] = epoch
89+
90+
id := binary.BigEndian.Uint64(idBytes)
91+
return int64(id), nil
92+
}
93+
94+
// looksLikeRandomID returns true if the input ID looks like it might belong to
95+
// the new schema which uses epoch-prefixed random IDs instead of auto-increment
96+
// columns. This is only necessary during the migration period when we are
97+
// reading from both the old and new schemas simultaneously.
98+
func looksLikeRandomID(id int64, clk clock.Clock) bool {
99+
// Compute the current and previous epochs. If the input ID starts with one of
100+
// those two epochs, it's one of ours. Otherwise, it came from somewhere
101+
// unknown and we should ask the old schema about it just in case.
102+
currEpoch := uint8(clk.Now().Sub(time.Date(2024, 01, 01, 00, 00, 00, 00, time.UTC)) / (90 * 24 * time.Hour))
103+
prevEpoch := uint8(clk.Now().Add(-90*24*time.Hour).Sub(time.Date(2024, 01, 01, 00, 00, 00, 00, time.UTC)) / (90 * 24 * time.Hour))
104+
105+
buf := make([]byte, 8)
106+
binary.BigEndian.PutUint64(buf, uint64(id))
107+
if buf[0] == currEpoch || buf[0] == prevEpoch {
108+
return true
109+
}
110+
return false
111+
}
112+
62113
const regFields = "id, jwk, jwk_sha256, contact, agreement, initialIP, createdAt, LockCol, status"
63114

64115
// ClearEmail removes the provided email address from one specified registration. If
@@ -1412,3 +1463,47 @@ type pausedModel struct {
14121463
PausedAt time.Time `db:"pausedAt"`
14131464
UnpausedAt *time.Time `db:"unpausedAt"`
14141465
}
1466+
1467+
// orders2Model represents a row in the "orders2" table.
1468+
type orders2Model struct {
1469+
ID int64
1470+
RegistrationID int64
1471+
Created time.Time
1472+
Expires time.Time
1473+
AuthorizationIDs []int64 // Actually a JSON list of ints
1474+
Profile string
1475+
BeganProcessing bool
1476+
Error []byte
1477+
CertificateSerial string
1478+
}
1479+
1480+
// authorizationsModel represents a row in the "authorizations" table.
1481+
type authorizationsModel struct {
1482+
ID int64
1483+
RegistrationID int64
1484+
IdentifierType uint8
1485+
IdentifierValue string
1486+
Created time.Time
1487+
Expires time.Time
1488+
Profile string
1489+
Challenges uint8
1490+
Token []byte
1491+
Status uint8
1492+
ValidationIDs []int64 // Actually a JSON list of ints
1493+
}
1494+
1495+
// validationsModel represents a row in the "validations" table.
1496+
type validationsModel struct {
1497+
ID int64
1498+
Challenge uint8
1499+
AttemptedAt time.Time
1500+
Status uint8
1501+
Record string
1502+
}
1503+
1504+
// authzReuseModel represents a row in the "authzReuse" table.
1505+
type authzReuseModel struct {
1506+
ID int64 `db:"accountID_identifier"`
1507+
AuthzID int64
1508+
Expires time.Time
1509+
}

sa/model_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"crypto/x509/pkix"
99
"database/sql"
1010
"encoding/base64"
11+
"encoding/binary"
1112
"fmt"
1213
"math/big"
1314
"net"
@@ -28,6 +29,59 @@ import (
2829
"github.com/letsencrypt/boulder/test"
2930
)
3031

32+
func TestNewRandomID(t *testing.T) {
33+
t.Parallel()
34+
35+
testCases := []struct {
36+
name string
37+
date time.Time
38+
expectPrefix uint8
39+
expectError string
40+
}{
41+
{
42+
name: "in the past",
43+
date: time.Date(2023, 01, 01, 00, 00, 00, 00, time.UTC),
44+
expectError: "invalid epoch",
45+
},
46+
{
47+
name: "first epoch",
48+
date: time.Date(2024, 05, 01, 00, 00, 00, 00, time.UTC),
49+
expectPrefix: 1,
50+
},
51+
{
52+
name: "last epoch",
53+
date: time.Date(2055, 07, 01, 00, 00, 00, 00, time.UTC),
54+
expectPrefix: 127,
55+
},
56+
{
57+
name: "far future",
58+
date: time.Date(2056, 01, 01, 00, 00, 00, 00, time.UTC),
59+
expectError: "invalid epoch",
60+
},
61+
}
62+
63+
for _, tc := range testCases {
64+
t.Run(tc.name, func(t *testing.T) {
65+
t.Parallel()
66+
fc := clock.NewFake()
67+
fc.Set(tc.date)
68+
id, err := newRandomID(fc)
69+
70+
if tc.expectPrefix != 0 {
71+
test.AssertNotError(t, err, "expected success")
72+
buf := make([]byte, 8)
73+
binary.BigEndian.PutUint64(buf, uint64(id))
74+
test.AssertEquals(t, buf[0], tc.expectPrefix)
75+
}
76+
77+
if tc.expectError != "" {
78+
test.AssertError(t, err, "expected error")
79+
test.AssertContains(t, err.Error(), tc.expectError)
80+
}
81+
})
82+
}
83+
}
84+
3185
func TestRegistrationModelToPb(t *testing.T) {
3286
badCases := []struct {
3387
name string

sa/sa.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,14 @@ func (ssa *SQLStorageAuthority) DeactivateAuthorization2(ctx context.Context, re
458458
// authorizations are created, but then their corresponding order is never
459459
// created, leading to "invisible" pending authorizations.
460460
func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb.NewOrderAndAuthzsRequest) (*corepb.Order, error) {
461+
if !features.Get().WriteNewOrderSchema {
462+
return ssa.deprecatedNewOrderAndAuthzs(ctx, req)
463+
}
464+
465+
return nil, nil
466+
}
467+
468+
func (ssa *SQLStorageAuthority) deprecatedNewOrderAndAuthzs(ctx context.Context, req *sapb.NewOrderAndAuthzsRequest) (*corepb.Order, error) {
461469
if req.NewOrder == nil {
462470
return nil, errIncompleteRequest
463471
}

sa/saro.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,83 @@ func (ssa *SQLStorageAuthorityRO) checkFQDNSetExists(ctx context.Context, select
576576
}
577577

578578
// GetOrder is used to retrieve an already existing order object
579+
// TODO XXX TKTK Update this method
579580
func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderRequest) (*corepb.Order, error) {
581+
if !features.Get().ReadNewOrderSchema {
582+
return ssa.deprecatedGetOrder(ctx, req)
583+
}
584+
585+
if req == nil || req.Id == 0 {
586+
return nil, errIncompleteRequest
587+
}
588+
589+
if !looksLikeRandomID(req.Id, ssa.clk) {
590+
return ssa.deprecatedGetOrder(ctx, req)
591+
}
592+
593+
output, err := db.WithTransaction(ctx, ssa.dbReadOnlyMap, func(tx db.Executor) (interface{}, error) {
594+
oi, err := tx.Get(ctx, orders2Model{}, req.Id)
595+
if err != nil {
596+
if db.IsNoRows(err) {
597+
return nil, berrors.NotFoundError("no order found for ID %d", req.Id)
598+
}
599+
return nil, err
600+
}
601+
om := oi.(orders2Model)
602+
603+
if om.Expires.Before(ssa.clk.Now()) {
604+
return nil, berrors.NotFoundError("no order found for ID %d", req.Id)
605+
}
606+
607+
avis := make([]authzValidity, len(om.AuthorizationIDs))
608+
dnsNames := make([]string, len(om.AuthorizationIDs))
609+
for i, authzId := range om.AuthorizationIDs {
610+
ai, err := tx.Get(ctx, authorizationsModel{}, authzId)
611+
if err != nil {
612+
if db.IsNoRows(err) {
613+
return nil, berrors.NotFoundError("no authorization found for ID %d", authzId)
614+
}
615+
return nil, err
616+
}
617+
am := ai.(authorizationsModel)
618+
619+
avis[i] = authzValidity{
620+
IdentifierType: am.IdentifierType,
621+
IdentifierValue: am.IdentifierValue,
622+
Status: am.Status,
623+
Expires: am.Expires,
624+
}
625+
dnsNames[i] = am.IdentifierValue
626+
}
627+
628+
order := corepb.Order{
629+
Id: om.ID,
630+
RegistrationID: om.RegistrationID,
631+
Expires: timestamppb.New(om.Expires),
632+
DnsNames: dnsNames,
633+
Error: om.Error,
634+
V2Authorizations: om.AuthorizationIDs,
635+
CertificateSerial: om.CertificateSerial,
636+
Created: timestamppb.New(om.Created),
637+
CertificateProfileName: om.Profile,
638+
BeganProcessing: om.BeganProcessing,
639+
}
640+
641+
status, err := statusForOrder(&order, avis, ssa.clk.Now())
642+
order.Status = status
643+
644+
return &order, nil
645+
})
646+
if err != nil {
647+
return nil, err
648+
}
649+
650+
res := output.(*corepb.Order)
651+
return res, nil
652+
}
653+
654+
// deprecatedGetOrder retrieves an order from the old database schema.
655+
func (ssa *SQLStorageAuthorityRO) deprecatedGetOrder(ctx context.Context, req *sapb.OrderRequest) (*corepb.Order, error) {
580656
if req == nil || req.Id == 0 {
581657
return nil, errIncompleteRequest
582658
}
@@ -735,6 +811,7 @@ func (ssa *SQLStorageAuthorityRO) GetOrderForNames(ctx context.Context, req *sap
735811

736812
// GetAuthorization2 returns the authz2 style authorization identified by the provided ID or an error.
737813
// If no authorization is found matching the ID a berrors.NotFound type error is returned.
814+
// TODO XXX TKTK Update this method
738815
func (ssa *SQLStorageAuthorityRO) GetAuthorization2(ctx context.Context, req *sapb.AuthorizationID2) (*corepb.Authorization, error) {
739816
if req.Id == 0 {
740817
return nil, errIncompleteRequest

sa/type-converter.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type BoulderTypeConverter struct{}
2020
// ToDb converts a Boulder object to one suitable for the DB representation.
2121
func (tc BoulderTypeConverter) ToDb(val interface{}) (interface{}, error) {
2222
switch t := val.(type) {
23-
case identifier.ACMEIdentifier, []core.Challenge, []string, [][]int:
23+
case identifier.ACMEIdentifier, []core.Challenge, []string, [][]int, []int64:
2424
jsonBytes, err := json.Marshal(t)
2525
if err != nil {
2626
return nil, err
@@ -56,7 +56,7 @@ func (tc BoulderTypeConverter) ToDb(val interface{}) (interface{}, error) {
5656
// FromDb converts a DB representation back into a Boulder object.
5757
func (tc BoulderTypeConverter) FromDb(target interface{}) (borp.CustomScanner, bool) {
5858
switch target.(type) {
59-
case *identifier.ACMEIdentifier, *[]core.Challenge, *[]string, *[][]int:
59+
case *identifier.ACMEIdentifier, *[]core.Challenge, *[]string, *[][]int, *[]int64:
6060
binder := func(holder, target interface{}) error {
6161
s, ok := holder.(*string)
6262
if !ok {

0 commit comments

Comments
 (0)