Skip to content

Commit 7bcc95a

Browse files
committed
Add basic support for new tables and types
1 parent 27463e1 commit 7bcc95a

File tree

4 files changed

+154
-2
lines changed

4 files changed

+154
-2
lines changed

sa/database.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,10 @@ func initTables(dbMap *borp.DbMap) {
287287
dbMap.AddTableWithName(revokedCertModel{}, "revokedCertificates").SetKeys(true, "ID")
288288
dbMap.AddTableWithName(replacementOrderModel{}, "replacementOrders").SetKeys(true, "ID")
289289
dbMap.AddTableWithName(pausedModel{}, "paused")
290+
dbMap.AddTableWithName(orders2Model{}, "orders2")
291+
dbMap.AddTableWithName(authorizationsModel{}, "authorizations")
292+
dbMap.AddTableWithName(validationsModel{}, "validations")
293+
dbMap.AddTableWithName(authzReuseModel{}, "authzReuse")
290294

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

sa/model.go

Lines changed: 94 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"
@@ -59,6 +61,54 @@ func badJSONError(msg string, jsonData []byte, err error) error {
5961
}
6062
}
6163

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

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

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
"os"
@@ -27,6 +28,59 @@ import (
2728
"github.com/letsencrypt/boulder/test"
2829
)
2930

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

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)