SA/ARI: Add method of tracking certificate replacement (#7284)
Part of #6732 Part of #7038
This commit is contained in:
parent
8d7e84b013
commit
f10abd27eb
|
@ -99,6 +99,16 @@ type Config struct {
|
|||
// number of failures is greater than the configured
|
||||
// maxRemoteValidationFailures. Only used when EnforceMultiCAA is true.
|
||||
MultiCAAFullResults bool
|
||||
|
||||
// TrackReplacementCertificatesARI, when enabled, triggers the following
|
||||
// behavior:
|
||||
// - SA.NewOrderAndAuthzs: upon receiving a NewOrderRequest with a
|
||||
// 'replacesSerial' value, will create a new entry in the 'replacement
|
||||
// Orders' table. This will occur inside of the new order transaction.
|
||||
// - SA.FinalizeOrder will update the 'replaced' column of any row with
|
||||
// a 'orderID' matching the finalized order to true. This will occur
|
||||
// inside of the finalize (order) transaction.
|
||||
TrackReplacementCertificatesARI bool
|
||||
}
|
||||
|
||||
var fMu = new(sync.RWMutex)
|
||||
|
|
|
@ -612,6 +612,11 @@ func (sa *StorageAuthority) UpdateCRLShard(ctx context.Context, req *sapb.Update
|
|||
return nil, errors.New("unimplemented")
|
||||
}
|
||||
|
||||
// ReplacementOrderExists is a mock.
|
||||
func (sa *StorageAuthorityReadOnly) ReplacementOrderExists(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.Exists, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// PublisherClient is a mock
|
||||
type PublisherClient struct {
|
||||
// empty
|
||||
|
|
|
@ -284,6 +284,7 @@ func initTables(dbMap *borp.DbMap) {
|
|||
dbMap.AddTable(incidentSerialModel{})
|
||||
dbMap.AddTableWithName(crlShardModel{}, "crlShards").SetKeys(true, "ID")
|
||||
dbMap.AddTableWithName(revokedCertModel{}, "revokedCertificates").SetKeys(true, "ID")
|
||||
dbMap.AddTableWithName(replacementOrderModel{}, "replacementOrders").SetKeys(true, "ID")
|
||||
|
||||
// Read-only maps used for selecting subsets of columns.
|
||||
dbMap.AddTableWithName(CertStatusMetadata{}, "certificateStatus")
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
-- +migrate Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
|
||||
CREATE TABLE `replacementOrders` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`serial` varchar(255) NOT NULL,
|
||||
`orderID` bigint(20) NOT NULL,
|
||||
`orderExpires` datetime NOT NULL,
|
||||
`replaced` boolean DEFAULT false,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `serial_idx` (`serial`),
|
||||
KEY `orderID_idx` (`orderID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
PARTITION BY RANGE(id)
|
||||
(PARTITION p_start VALUES LESS THAN (MAXVALUE));
|
||||
|
||||
-- +migrate Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
||||
DROP TABLE `replacementOrders`;
|
|
@ -34,6 +34,7 @@ GRANT SELECT,INSERT,UPDATE ON newOrdersRL TO 'sa'@'localhost';
|
|||
GRANT SELECT ON incidents TO 'sa'@'localhost';
|
||||
GRANT SELECT,INSERT,UPDATE ON crlShards TO 'sa'@'localhost';
|
||||
GRANT SELECT,INSERT,UPDATE ON revokedCertificates TO 'sa'@'localhost';
|
||||
GRANT SELECT,INSERT,UPDATE ON replacementOrders TO 'sa'@'localhost';
|
||||
|
||||
GRANT SELECT ON certificates TO 'sa_ro'@'localhost';
|
||||
GRANT SELECT ON certificateStatus TO 'sa_ro'@'localhost';
|
||||
|
@ -54,6 +55,7 @@ GRANT SELECT ON newOrdersRL TO 'sa_ro'@'localhost';
|
|||
GRANT SELECT ON incidents TO 'sa_ro'@'localhost';
|
||||
GRANT SELECT ON crlShards TO 'sa_ro'@'localhost';
|
||||
GRANT SELECT ON revokedCertificates TO 'sa_ro'@'localhost';
|
||||
GRANT SELECT ON replacementOrders TO 'sa_ro'@'localhost';
|
||||
|
||||
-- OCSP Responder
|
||||
GRANT SELECT ON certificateStatus TO 'ocsp_resp'@'localhost';
|
||||
|
|
81
sa/model.go
81
sa/model.go
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
@ -1188,3 +1189,83 @@ type revokedCertModel struct {
|
|||
RevokedDate time.Time `db:"revokedDate"`
|
||||
RevokedReason revocation.Reason `db:"revokedReason"`
|
||||
}
|
||||
|
||||
// replacementOrderModel represents one row in the replacementOrders table. It
|
||||
// contains all of the information necessary to link a renewal order to the
|
||||
// certificate it replaces.
|
||||
type replacementOrderModel struct {
|
||||
// ID is an auto-incrementing row ID.
|
||||
ID int64 `db:"id"`
|
||||
// Serial is the serial number of the replaced certificate.
|
||||
Serial string `db:"serial"`
|
||||
// OrderId is the ID of the replacement order
|
||||
OrderID int64 `db:"orderID"`
|
||||
// OrderExpiry is the expiry time of the new order. This is used to
|
||||
// determine if we can accept a new replacement order for the same Serial.
|
||||
OrderExpires time.Time `db:"orderExpires"`
|
||||
// Replaced is a boolean indicating whether the certificate has been
|
||||
// replaced, i.e. whether the new order has been finalized. Once this is
|
||||
// true, no new replacement orders can be accepted for the same Serial.
|
||||
Replaced bool `db:"replaced"`
|
||||
}
|
||||
|
||||
// addReplacementOrder inserts or updates the replacementOrders row matching the
|
||||
// provided serial with the details provided. This function accepts a
|
||||
// transaction so that the insert or update takes place within the new order
|
||||
// transaction.
|
||||
func addReplacementOrder(ctx context.Context, db db.SelectExecer, serial string, orderID int64, orderExpires time.Time) error {
|
||||
var existingID []int64
|
||||
_, err := db.Select(ctx, &existingID, `
|
||||
SELECT id
|
||||
FROM replacementOrders
|
||||
WHERE serial = ?
|
||||
LIMIT 1`,
|
||||
serial,
|
||||
)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("checking for existing replacement order: %w", err)
|
||||
}
|
||||
|
||||
if len(existingID) > 0 {
|
||||
// Update existing replacementOrder row.
|
||||
_, err = db.ExecContext(ctx, `
|
||||
UPDATE replacementOrders
|
||||
SET orderID = ?, orderExpires = ?
|
||||
WHERE id = ?`,
|
||||
orderID, orderExpires,
|
||||
existingID[0],
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating replacement order: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Insert new replacementOrder row.
|
||||
_, err = db.ExecContext(ctx, `
|
||||
INSERT INTO replacementOrders (serial, orderID, orderExpires)
|
||||
VALUES (?, ?, ?)`,
|
||||
serial, orderID, orderExpires,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating replacement order: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setReplacementOrderFinalized sets the replaced flag for the replacementOrder
|
||||
// row matching the provided orderID to true. This function accepts a
|
||||
// transaction so that the update can take place within the finalization
|
||||
// transaction.
|
||||
func setReplacementOrderFinalized(ctx context.Context, db db.Execer, orderID int64) error {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
UPDATE replacementOrders
|
||||
SET replaced = true
|
||||
WHERE orderID = ?
|
||||
LIMIT 1`,
|
||||
orderID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
103
sa/model_test.go
103
sa/model_test.go
|
@ -6,15 +6,18 @@ import (
|
|||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jmhodges/clock"
|
||||
"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"
|
||||
|
@ -413,3 +416,103 @@ func TestIncidentSerialModel(t *testing.T) {
|
|||
test.AssertEquals(t, *res2.OrderID, int64(2))
|
||||
test.AssertEquals(t, *res2.LastNoticeSent, time.Date(2023, 06, 29, 16, 9, 00, 00, time.UTC))
|
||||
}
|
||||
|
||||
func TestAddReplacementOrder(t *testing.T) {
|
||||
if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" {
|
||||
t.Skip("Test requires replacementOrders database table")
|
||||
}
|
||||
|
||||
sa, _, cleanUp := initSA(t)
|
||||
defer cleanUp()
|
||||
|
||||
features.Set(features.Config{TrackReplacementCertificatesARI: true})
|
||||
defer features.Reset()
|
||||
|
||||
oldCertSerial := "1234567890"
|
||||
orderId := int64(1337)
|
||||
orderExpires := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second)
|
||||
|
||||
// Add a replacement order which doesn't exist.
|
||||
err := addReplacementOrder(ctx, sa.dbMap, oldCertSerial, orderId, orderExpires)
|
||||
test.AssertNotError(t, err, "addReplacementOrder failed")
|
||||
|
||||
// Fetch the replacement order so we can ensure it was added.
|
||||
var replacementRow replacementOrderModel
|
||||
err = sa.dbReadOnlyMap.SelectOne(
|
||||
ctx,
|
||||
&replacementRow,
|
||||
"SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1",
|
||||
oldCertSerial,
|
||||
)
|
||||
test.AssertNotError(t, err, "SELECT from replacementOrders failed")
|
||||
test.AssertEquals(t, oldCertSerial, replacementRow.Serial)
|
||||
test.AssertEquals(t, orderId, replacementRow.OrderID)
|
||||
test.AssertEquals(t, orderExpires, replacementRow.OrderExpires)
|
||||
|
||||
nextOrderId := int64(1338)
|
||||
nextOrderExpires := time.Now().Add(48 * time.Hour).UTC().Truncate(time.Second)
|
||||
|
||||
// Add a replacement order which already exists.
|
||||
err = addReplacementOrder(ctx, sa.dbMap, oldCertSerial, nextOrderId, nextOrderExpires)
|
||||
test.AssertNotError(t, err, "addReplacementOrder failed")
|
||||
|
||||
// Fetch the replacement order so we can ensure it was updated.
|
||||
err = sa.dbReadOnlyMap.SelectOne(
|
||||
ctx,
|
||||
&replacementRow,
|
||||
"SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1",
|
||||
oldCertSerial,
|
||||
)
|
||||
test.AssertNotError(t, err, "SELECT from replacementOrders failed")
|
||||
test.AssertEquals(t, oldCertSerial, replacementRow.Serial)
|
||||
test.AssertEquals(t, nextOrderId, replacementRow.OrderID)
|
||||
test.AssertEquals(t, nextOrderExpires, replacementRow.OrderExpires)
|
||||
}
|
||||
|
||||
func TestSetReplacementOrderFinalized(t *testing.T) {
|
||||
if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" {
|
||||
t.Skip("Test requires replacementOrders database table")
|
||||
}
|
||||
|
||||
sa, _, cleanUp := initSA(t)
|
||||
defer cleanUp()
|
||||
|
||||
features.Set(features.Config{TrackReplacementCertificatesARI: true})
|
||||
defer features.Reset()
|
||||
|
||||
oldCertSerial := "1234567890"
|
||||
orderId := int64(1337)
|
||||
orderExpires := time.Now().Add(24 * time.Hour).UTC().Truncate(time.Second)
|
||||
|
||||
// Mark a non-existent certificate as finalized/replaced.
|
||||
err := setReplacementOrderFinalized(ctx, sa.dbMap, orderId)
|
||||
test.AssertNotError(t, err, "setReplacementOrderFinalized failed")
|
||||
|
||||
// Ensure no replacement order was added for some reason.
|
||||
var replacementRow replacementOrderModel
|
||||
err = sa.dbReadOnlyMap.SelectOne(
|
||||
ctx,
|
||||
&replacementRow,
|
||||
"SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1",
|
||||
oldCertSerial,
|
||||
)
|
||||
test.AssertErrorIs(t, err, sql.ErrNoRows)
|
||||
|
||||
// Add a replacement order.
|
||||
err = addReplacementOrder(ctx, sa.dbMap, oldCertSerial, orderId, orderExpires)
|
||||
test.AssertNotError(t, err, "addReplacementOrder failed")
|
||||
|
||||
// Mark the certificate as finalized/replaced.
|
||||
err = setReplacementOrderFinalized(ctx, sa.dbMap, orderId)
|
||||
test.AssertNotError(t, err, "setReplacementOrderFinalized failed")
|
||||
|
||||
// Fetch the replacement order so we can ensure it was finalized.
|
||||
err = sa.dbReadOnlyMap.SelectOne(
|
||||
ctx,
|
||||
&replacementRow,
|
||||
"SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1",
|
||||
oldCertSerial,
|
||||
)
|
||||
test.AssertNotError(t, err, "SELECT from replacementOrders failed")
|
||||
test.Assert(t, replacementRow.Replaced, "replacement order should be marked as finalized")
|
||||
}
|
||||
|
|
1137
sa/proto/sa.pb.go
1137
sa/proto/sa.pb.go
File diff suppressed because it is too large
Load Diff
|
@ -38,6 +38,7 @@ service StorageAuthorityReadOnly {
|
|||
rpc IncidentsForSerial(Serial) returns (Incidents) {}
|
||||
rpc KeyBlocked(KeyBlockedRequest) returns (Exists) {}
|
||||
rpc PreviousCertificateExists(PreviousCertificateExistsRequest) returns (Exists) {}
|
||||
rpc ReplacementOrderExists(Serial) returns (Exists) {}
|
||||
rpc SerialsForIncident (SerialsForIncidentRequest) returns (stream IncidentSerial) {}
|
||||
}
|
||||
|
||||
|
@ -72,6 +73,7 @@ service StorageAuthority {
|
|||
rpc IncidentsForSerial(Serial) returns (Incidents) {}
|
||||
rpc KeyBlocked(KeyBlockedRequest) returns (Exists) {}
|
||||
rpc PreviousCertificateExists(PreviousCertificateExistsRequest) returns (Exists) {}
|
||||
rpc ReplacementOrderExists(Serial) returns (Exists) {}
|
||||
rpc SerialsForIncident (SerialsForIncidentRequest) returns (stream IncidentSerial) {}
|
||||
// Adders
|
||||
rpc AddBlockedKey(AddBlockedKeyRequest) returns (google.protobuf.Empty) {}
|
||||
|
@ -253,12 +255,13 @@ message OrderRequest {
|
|||
}
|
||||
|
||||
message NewOrderRequest {
|
||||
// Next unused field number: 6
|
||||
// Next unused field number: 7
|
||||
int64 registrationID = 1;
|
||||
reserved 2; // Previously expiresNS
|
||||
google.protobuf.Timestamp expires = 5;
|
||||
repeated string names = 3;
|
||||
repeated int64 v2Authorizations = 4;
|
||||
string replacesSerial = 6;
|
||||
}
|
||||
|
||||
message NewOrderAndAuthzsRequest {
|
||||
|
|
|
@ -53,6 +53,7 @@ type StorageAuthorityReadOnlyClient interface {
|
|||
IncidentsForSerial(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*Incidents, error)
|
||||
KeyBlocked(ctx context.Context, in *KeyBlockedRequest, opts ...grpc.CallOption) (*Exists, error)
|
||||
PreviousCertificateExists(ctx context.Context, in *PreviousCertificateExistsRequest, opts ...grpc.CallOption) (*Exists, error)
|
||||
ReplacementOrderExists(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*Exists, error)
|
||||
SerialsForIncident(ctx context.Context, in *SerialsForIncidentRequest, opts ...grpc.CallOption) (StorageAuthorityReadOnly_SerialsForIncidentClient, error)
|
||||
}
|
||||
|
||||
|
@ -339,6 +340,15 @@ func (c *storageAuthorityReadOnlyClient) PreviousCertificateExists(ctx context.C
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *storageAuthorityReadOnlyClient) ReplacementOrderExists(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*Exists, error) {
|
||||
out := new(Exists)
|
||||
err := c.cc.Invoke(ctx, "/sa.StorageAuthorityReadOnly/ReplacementOrderExists", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *storageAuthorityReadOnlyClient) SerialsForIncident(ctx context.Context, in *SerialsForIncidentRequest, opts ...grpc.CallOption) (StorageAuthorityReadOnly_SerialsForIncidentClient, error) {
|
||||
stream, err := c.cc.NewStream(ctx, &StorageAuthorityReadOnly_ServiceDesc.Streams[1], "/sa.StorageAuthorityReadOnly/SerialsForIncident", opts...)
|
||||
if err != nil {
|
||||
|
@ -403,6 +413,7 @@ type StorageAuthorityReadOnlyServer interface {
|
|||
IncidentsForSerial(context.Context, *Serial) (*Incidents, error)
|
||||
KeyBlocked(context.Context, *KeyBlockedRequest) (*Exists, error)
|
||||
PreviousCertificateExists(context.Context, *PreviousCertificateExistsRequest) (*Exists, error)
|
||||
ReplacementOrderExists(context.Context, *Serial) (*Exists, error)
|
||||
SerialsForIncident(*SerialsForIncidentRequest, StorageAuthorityReadOnly_SerialsForIncidentServer) error
|
||||
mustEmbedUnimplementedStorageAuthorityReadOnlyServer()
|
||||
}
|
||||
|
@ -495,6 +506,9 @@ func (UnimplementedStorageAuthorityReadOnlyServer) KeyBlocked(context.Context, *
|
|||
func (UnimplementedStorageAuthorityReadOnlyServer) PreviousCertificateExists(context.Context, *PreviousCertificateExistsRequest) (*Exists, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PreviousCertificateExists not implemented")
|
||||
}
|
||||
func (UnimplementedStorageAuthorityReadOnlyServer) ReplacementOrderExists(context.Context, *Serial) (*Exists, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReplacementOrderExists not implemented")
|
||||
}
|
||||
func (UnimplementedStorageAuthorityReadOnlyServer) SerialsForIncident(*SerialsForIncidentRequest, StorageAuthorityReadOnly_SerialsForIncidentServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method SerialsForIncident not implemented")
|
||||
}
|
||||
|
@ -1019,6 +1033,24 @@ func _StorageAuthorityReadOnly_PreviousCertificateExists_Handler(srv interface{}
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StorageAuthorityReadOnly_ReplacementOrderExists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Serial)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StorageAuthorityReadOnlyServer).ReplacementOrderExists(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/sa.StorageAuthorityReadOnly/ReplacementOrderExists",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StorageAuthorityReadOnlyServer).ReplacementOrderExists(ctx, req.(*Serial))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StorageAuthorityReadOnly_SerialsForIncident_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(SerialsForIncidentRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
|
@ -1155,6 +1187,10 @@ var StorageAuthorityReadOnly_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "PreviousCertificateExists",
|
||||
Handler: _StorageAuthorityReadOnly_PreviousCertificateExists_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ReplacementOrderExists",
|
||||
Handler: _StorageAuthorityReadOnly_ReplacementOrderExists_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
|
@ -1204,6 +1240,7 @@ type StorageAuthorityClient interface {
|
|||
IncidentsForSerial(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*Incidents, error)
|
||||
KeyBlocked(ctx context.Context, in *KeyBlockedRequest, opts ...grpc.CallOption) (*Exists, error)
|
||||
PreviousCertificateExists(ctx context.Context, in *PreviousCertificateExistsRequest, opts ...grpc.CallOption) (*Exists, error)
|
||||
ReplacementOrderExists(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*Exists, error)
|
||||
SerialsForIncident(ctx context.Context, in *SerialsForIncidentRequest, opts ...grpc.CallOption) (StorageAuthority_SerialsForIncidentClient, error)
|
||||
// Adders
|
||||
AddBlockedKey(ctx context.Context, in *AddBlockedKeyRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
|
@ -1509,6 +1546,15 @@ func (c *storageAuthorityClient) PreviousCertificateExists(ctx context.Context,
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (c *storageAuthorityClient) ReplacementOrderExists(ctx context.Context, in *Serial, opts ...grpc.CallOption) (*Exists, error) {
|
||||
out := new(Exists)
|
||||
err := c.cc.Invoke(ctx, "/sa.StorageAuthority/ReplacementOrderExists", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *storageAuthorityClient) SerialsForIncident(ctx context.Context, in *SerialsForIncidentRequest, opts ...grpc.CallOption) (StorageAuthority_SerialsForIncidentClient, error) {
|
||||
stream, err := c.cc.NewStream(ctx, &StorageAuthority_ServiceDesc.Streams[1], "/sa.StorageAuthority/SerialsForIncident", opts...)
|
||||
if err != nil {
|
||||
|
@ -1736,6 +1782,7 @@ type StorageAuthorityServer interface {
|
|||
IncidentsForSerial(context.Context, *Serial) (*Incidents, error)
|
||||
KeyBlocked(context.Context, *KeyBlockedRequest) (*Exists, error)
|
||||
PreviousCertificateExists(context.Context, *PreviousCertificateExistsRequest) (*Exists, error)
|
||||
ReplacementOrderExists(context.Context, *Serial) (*Exists, error)
|
||||
SerialsForIncident(*SerialsForIncidentRequest, StorageAuthority_SerialsForIncidentServer) error
|
||||
// Adders
|
||||
AddBlockedKey(context.Context, *AddBlockedKeyRequest) (*emptypb.Empty, error)
|
||||
|
@ -1847,6 +1894,9 @@ func (UnimplementedStorageAuthorityServer) KeyBlocked(context.Context, *KeyBlock
|
|||
func (UnimplementedStorageAuthorityServer) PreviousCertificateExists(context.Context, *PreviousCertificateExistsRequest) (*Exists, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method PreviousCertificateExists not implemented")
|
||||
}
|
||||
func (UnimplementedStorageAuthorityServer) ReplacementOrderExists(context.Context, *Serial) (*Exists, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReplacementOrderExists not implemented")
|
||||
}
|
||||
func (UnimplementedStorageAuthorityServer) SerialsForIncident(*SerialsForIncidentRequest, StorageAuthority_SerialsForIncidentServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method SerialsForIncident not implemented")
|
||||
}
|
||||
|
@ -2424,6 +2474,24 @@ func _StorageAuthority_PreviousCertificateExists_Handler(srv interface{}, ctx co
|
|||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StorageAuthority_ReplacementOrderExists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(Serial)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StorageAuthorityServer).ReplacementOrderExists(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/sa.StorageAuthority/ReplacementOrderExists",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StorageAuthorityServer).ReplacementOrderExists(ctx, req.(*Serial))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StorageAuthority_SerialsForIncident_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(SerialsForIncidentRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
|
@ -2884,6 +2952,10 @@ var StorageAuthority_ServiceDesc = grpc.ServiceDesc{
|
|||
MethodName: "PreviousCertificateExists",
|
||||
Handler: _StorageAuthority_PreviousCertificateExists_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ReplacementOrderExists",
|
||||
Handler: _StorageAuthority_ReplacementOrderExists_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AddBlockedKey",
|
||||
Handler: _StorageAuthority_AddBlockedKey_Handler,
|
||||
|
|
17
sa/sa.go
17
sa/sa.go
|
@ -19,6 +19,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"
|
||||
blog "github.com/letsencrypt/boulder/log"
|
||||
"github.com/letsencrypt/boulder/revocation"
|
||||
|
@ -566,6 +567,15 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb
|
|||
BeganProcessing: false,
|
||||
}
|
||||
|
||||
if features.Get().TrackReplacementCertificatesARI && 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the order status before returning it. Since it may have reused
|
||||
// all valid authorizations the order may be "born" in a ready status.
|
||||
status, err := statusForOrder(ctx, tx, res, ssa.clk.Now())
|
||||
|
@ -696,6 +706,13 @@ func (ssa *SQLStorageAuthority) FinalizeOrder(ctx context.Context, req *sapb.Fin
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if features.Get().TrackReplacementCertificatesARI {
|
||||
err = setReplacementOrderFinalized(ctx, tx, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
if overallError != nil {
|
||||
|
|
109
sa/sa_test.go
109
sa/sa_test.go
|
@ -4004,3 +4004,112 @@ func TestUpdateCRLShard(t *testing.T) {
|
|||
)
|
||||
test.AssertError(t, err, "updating an unknown shard")
|
||||
}
|
||||
|
||||
func TestReplacementOrderExists(t *testing.T) {
|
||||
if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" {
|
||||
t.Skip("Test requires replacementOrders database table")
|
||||
}
|
||||
|
||||
sa, fc, cleanUp := initSA(t)
|
||||
defer cleanUp()
|
||||
|
||||
features.Set(features.Config{TrackReplacementCertificatesARI: true})
|
||||
defer features.Reset()
|
||||
|
||||
oldCertSerial := "1234567890"
|
||||
|
||||
// Check that a non-existent replacement order does not exist.
|
||||
exists, err := sa.ReplacementOrderExists(ctx, &sapb.Serial{Serial: oldCertSerial})
|
||||
test.AssertNotError(t, err, "failed to check for replacement order")
|
||||
test.Assert(t, !exists.Exists, "replacement for non-existent serial should not exist")
|
||||
|
||||
// Create a test registration to reference.
|
||||
reg := createWorkingRegistration(t, sa)
|
||||
|
||||
// Add one valid authz.
|
||||
expires := fc.Now().Add(time.Hour)
|
||||
attemptedAt := fc.Now()
|
||||
authzID := createFinalizedAuthorization(t, sa, "example.com", expires, "valid", attemptedAt)
|
||||
|
||||
// Add a new order in pending status with no certificate serial.
|
||||
expires1Year := sa.clk.Now().Add(365 * 24 * time.Hour)
|
||||
order, err := sa.NewOrderAndAuthzs(ctx, &sapb.NewOrderAndAuthzsRequest{
|
||||
NewOrder: &sapb.NewOrderRequest{
|
||||
RegistrationID: reg.Id,
|
||||
Expires: timestamppb.New(expires1Year),
|
||||
Names: []string{"example.com"},
|
||||
V2Authorizations: []int64{authzID},
|
||||
},
|
||||
})
|
||||
test.AssertNotError(t, err, "NewOrderAndAuthzs failed")
|
||||
|
||||
// Set the order to processing so it can be finalized
|
||||
_, err = sa.SetOrderProcessing(ctx, &sapb.OrderRequest{Id: order.Id})
|
||||
test.AssertNotError(t, err, "SetOrderProcessing failed")
|
||||
|
||||
// Finalize the order with a certificate oldCertSerial.
|
||||
order.CertificateSerial = oldCertSerial
|
||||
_, err = sa.FinalizeOrder(ctx, &sapb.FinalizeOrderRequest{Id: order.Id, CertificateSerial: order.CertificateSerial})
|
||||
test.AssertNotError(t, err, "FinalizeOrder failed")
|
||||
|
||||
// Create a replacement order.
|
||||
order, err = sa.NewOrderAndAuthzs(ctx, &sapb.NewOrderAndAuthzsRequest{
|
||||
NewOrder: &sapb.NewOrderRequest{
|
||||
RegistrationID: reg.Id,
|
||||
Expires: timestamppb.New(expires1Year),
|
||||
Names: []string{"example.com"},
|
||||
V2Authorizations: []int64{authzID},
|
||||
ReplacesSerial: oldCertSerial,
|
||||
},
|
||||
})
|
||||
test.AssertNotError(t, err, "NewOrderAndAuthzs failed")
|
||||
|
||||
// Check that a pending replacement order exists.
|
||||
exists, err = sa.ReplacementOrderExists(ctx, &sapb.Serial{Serial: oldCertSerial})
|
||||
test.AssertNotError(t, err, "failed to check for replacement order")
|
||||
test.Assert(t, exists.Exists, "replacement order should exist")
|
||||
|
||||
// Set the order to processing so it can be finalized.
|
||||
_, err = sa.SetOrderProcessing(ctx, &sapb.OrderRequest{Id: order.Id})
|
||||
test.AssertNotError(t, err, "SetOrderProcessing failed")
|
||||
|
||||
// Check that a replacement order in processing still exists.
|
||||
exists, err = sa.ReplacementOrderExists(ctx, &sapb.Serial{Serial: oldCertSerial})
|
||||
test.AssertNotError(t, err, "failed to check for replacement order")
|
||||
test.Assert(t, exists.Exists, "replacement order in processing should still exist")
|
||||
|
||||
order.CertificateSerial = "0123456789"
|
||||
_, err = sa.FinalizeOrder(ctx, &sapb.FinalizeOrderRequest{Id: order.Id, CertificateSerial: order.CertificateSerial})
|
||||
test.AssertNotError(t, err, "FinalizeOrder failed")
|
||||
|
||||
// Check that a finalized replacement order still exists.
|
||||
exists, err = sa.ReplacementOrderExists(ctx, &sapb.Serial{Serial: oldCertSerial})
|
||||
test.AssertNotError(t, err, "failed to check for replacement order")
|
||||
test.Assert(t, exists.Exists, "replacement order in processing should still exist")
|
||||
|
||||
// Try updating the replacement order.
|
||||
|
||||
// Create a replacement order.
|
||||
newReplacementOrder, err := sa.NewOrderAndAuthzs(ctx, &sapb.NewOrderAndAuthzsRequest{
|
||||
NewOrder: &sapb.NewOrderRequest{
|
||||
RegistrationID: reg.Id,
|
||||
Expires: timestamppb.New(expires1Year),
|
||||
Names: []string{"example.com"},
|
||||
V2Authorizations: []int64{authzID},
|
||||
ReplacesSerial: oldCertSerial,
|
||||
},
|
||||
})
|
||||
test.AssertNotError(t, err, "NewOrderAndAuthzs failed")
|
||||
|
||||
// Fetch the replacement order so we can ensure it was updated.
|
||||
var replacementRow replacementOrderModel
|
||||
err = sa.dbReadOnlyMap.SelectOne(
|
||||
ctx,
|
||||
&replacementRow,
|
||||
"SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1",
|
||||
oldCertSerial,
|
||||
)
|
||||
test.AssertNotError(t, err, "SELECT from replacementOrders failed")
|
||||
test.AssertEquals(t, newReplacementOrder.Id, replacementRow.OrderID)
|
||||
test.AssertEquals(t, newReplacementOrder.Expires.AsTime(), replacementRow.OrderExpires)
|
||||
}
|
||||
|
|
63
sa/saro.go
63
sa/saro.go
|
@ -1502,3 +1502,66 @@ func (ssa *SQLStorageAuthorityRO) Health(ctx context.Context) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReplacementOrderExists returns whether a valid replacement order exists for
|
||||
// the given certificate serial number. An existing but expired or otherwise
|
||||
// invalid replacement order is not considered to exist.
|
||||
func (ssa *SQLStorageAuthorityRO) ReplacementOrderExists(ctx context.Context, req *sapb.Serial) (*sapb.Exists, error) {
|
||||
if req == nil || req.Serial == "" {
|
||||
return nil, errIncompleteRequest
|
||||
}
|
||||
|
||||
var replacement replacementOrderModel
|
||||
err := ssa.dbReadOnlyMap.SelectOne(
|
||||
ctx,
|
||||
&replacement,
|
||||
"SELECT * FROM replacementOrders WHERE serial = ? LIMIT 1",
|
||||
req.Serial,
|
||||
)
|
||||
if err != nil {
|
||||
if db.IsNoRows(err) {
|
||||
// No replacement order exists.
|
||||
return &sapb.Exists{Exists: false}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if replacement.Replaced {
|
||||
// Certificate has already been replaced.
|
||||
return &sapb.Exists{Exists: true}, nil
|
||||
}
|
||||
if replacement.OrderExpires.Before(ssa.clk.Now()) {
|
||||
// The existing replacement order has expired.
|
||||
return &sapb.Exists{Exists: false}, nil
|
||||
}
|
||||
|
||||
// Pull the replacement order so we can inspect its status.
|
||||
replacementOrder, err := ssa.GetOrder(ctx, &sapb.OrderRequest{Id: replacement.OrderID})
|
||||
if err != nil {
|
||||
if errors.Is(err, berrors.NotFound) {
|
||||
// The existing replacement order has been deleted. This should
|
||||
// never happen.
|
||||
ssa.log.Errf("replacement order %d for serial %q not found", replacement.OrderID, req.Serial)
|
||||
return &sapb.Exists{Exists: false}, nil
|
||||
}
|
||||
}
|
||||
|
||||
switch replacementOrder.Status {
|
||||
case string(core.StatusPending), string(core.StatusReady), string(core.StatusProcessing), string(core.StatusValid):
|
||||
// An existing replacement order is either still being worked on or has
|
||||
// already been finalized.
|
||||
return &sapb.Exists{Exists: true}, nil
|
||||
|
||||
case string(core.StatusInvalid):
|
||||
// The existing replacement order cannot be finalized. The requester
|
||||
// should create a new replacement order.
|
||||
return &sapb.Exists{Exists: false}, nil
|
||||
|
||||
default:
|
||||
// Replacement order is in an unknown state. This should never happen.
|
||||
return nil, fmt.Errorf("unknown replacement order status: %q", replacementOrder.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func (ssa *SQLStorageAuthority) ReplacementOrderExists(ctx context.Context, req *sapb.Serial) (*sapb.Exists, error) {
|
||||
return ssa.SQLStorageAuthorityRO.ReplacementOrderExists(ctx, req)
|
||||
}
|
||||
|
|
|
@ -47,7 +47,9 @@
|
|||
}
|
||||
},
|
||||
"healthCheckInterval": "4s",
|
||||
"features": {}
|
||||
"features": {
|
||||
"TrackReplacementCertificatesARI": true
|
||||
}
|
||||
},
|
||||
"syslog": {
|
||||
"stdoutlevel": 6,
|
||||
|
|
Loading…
Reference in New Issue