Skip to content

Commit e54495e

Browse files
committed
ra: call MTCA when profile indicates MTC
Since we're going to be using profiles to decide whether an issuance uses MTC, remove the isMTC field from the orders table in boulder_sa_next.
1 parent 941778f commit e54495e

8 files changed

Lines changed: 160 additions & 17 deletions

File tree

cmd/boulder-ra/main.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package notmain
33
import (
44
"context"
55
"flag"
6+
"fmt"
67
"os"
78
"time"
89

@@ -20,6 +21,7 @@ import (
2021
"github.com/letsencrypt/boulder/goodkey/sagoodkey"
2122
bgrpc "github.com/letsencrypt/boulder/grpc"
2223
"github.com/letsencrypt/boulder/issuance"
24+
mtcapb "github.com/letsencrypt/boulder/mtca/proto"
2325
"github.com/letsencrypt/boulder/policy"
2426
pubpb "github.com/letsencrypt/boulder/publisher/proto"
2527
"github.com/letsencrypt/boulder/ra"
@@ -41,6 +43,8 @@ type Config struct {
4143

4244
MaxContactsPerRegistration int
4345

46+
ProfileToMTCA map[string]*cmd.GRPCClientConfig
47+
4448
SAService *cmd.GRPCClientConfig
4549
VAService *cmd.GRPCClientConfig
4650
CAService *cmd.GRPCClientConfig
@@ -79,8 +83,7 @@ type Config struct {
7983

8084
// ValidationProfiles is a map of validation profiles to their
8185
// respective issuance allow lists. If a profile is not included in this
82-
// mapping, it cannot be used by any account. If this field is left
83-
// empty, all profiles are open to all accounts.
86+
// mapping, it cannot be used by any account.
8487
ValidationProfiles map[string]*ra.ValidationProfileConfig `validate:"required"`
8588

8689
// DefaultProfileName sets the profile to use if one wasn't provided by the
@@ -182,6 +185,14 @@ func main() {
182185
vac := vapb.NewVAClient(vaConn)
183186
caaClient := vapb.NewCAAClient(vaConn)
184187

188+
profileToMTCA := make(map[string]mtcapb.MTCAClient)
189+
for profile, clientConfig := range c.RA.ProfileToMTCA {
190+
mtcaConn, err := bgrpc.ClientSetup(clientConfig, tlsConfig, scope, clk)
191+
cmd.FailOnError(err, fmt.Sprintf("Unable to create MTCA client for profile %s", profile))
192+
mtcaClient := mtcapb.NewMTCAClient(mtcaConn)
193+
profileToMTCA[profile] = mtcaClient
194+
}
195+
185196
caConn, err := bgrpc.ClientSetup(c.RA.CAService, tlsConfig, scope, clk)
186197
cmd.FailOnError(err, "Unable to create CA client")
187198
cac := capb.NewCertificateAuthorityClient(caConn)
@@ -228,6 +239,11 @@ func main() {
228239
cmd.Fail("At least one profile must be configured")
229240
}
230241

242+
for name, profile := range c.RA.ValidationProfiles {
243+
if profile.MTC && c.RA.ProfileToMTCA[name] == nil {
244+
cmd.Fail(fmt.Sprintf("profile %q is configured for MTC but has no MTCA backend", name))
245+
}
246+
}
231247
validationProfiles, err := ra.NewValidationProfiles(c.RA.DefaultProfileName, c.RA.ValidationProfiles)
232248
cmd.FailOnError(err, "Failed to load validation profiles")
233249

@@ -279,6 +295,7 @@ func main() {
279295
c.RA.FinalizeTimeout.Duration,
280296
ctp,
281297
issuerCerts,
298+
profileToMTCA,
282299
)
283300
defer rai.Drain()
284301

mtca/mtca.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package mtca
22

33
import (
44
"context"
5-
"fmt"
5+
"sync"
66

77
"github.com/letsencrypt/boulder/issuance"
88
mtcapb "github.com/letsencrypt/boulder/mtca/proto"
@@ -13,14 +13,27 @@ var _ mtcapb.MTCAServer = &mtca{}
1313
func New(issuer *issuance.Issuer) *mtca {
1414
return &mtca{
1515
issuer: issuer,
16+
logID: issuer.Cert.Subject.String(),
1617
}
1718
}
1819

1920
type mtca struct {
2021
mtcapb.UnimplementedMTCAServer
21-
issuer *issuance.Issuer
22+
23+
issuer *issuance.Issuer
24+
logID string
25+
entryIndex int64
26+
27+
sequencing sync.Mutex
2228
}
2329

2430
func (m *mtca) Issue(ctx context.Context, req *mtcapb.IssueRequest) (*mtcapb.IssueResponse, error) {
25-
return nil, fmt.Errorf("not implemented")
31+
m.sequencing.Lock()
32+
defer m.sequencing.Unlock()
33+
m.entryIndex++
34+
35+
return &mtcapb.IssueResponse{
36+
MtcLogID: m.logID,
37+
MtcEntryIndex: m.entryIndex,
38+
}, nil
2639
}

ra/ra.go

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/letsencrypt/boulder/issuance"
4141
blog "github.com/letsencrypt/boulder/log"
4242
"github.com/letsencrypt/boulder/metrics"
43+
mtcapb "github.com/letsencrypt/boulder/mtca/proto"
4344
"github.com/letsencrypt/boulder/probs"
4445
pubpb "github.com/letsencrypt/boulder/publisher/proto"
4546
rapb "github.com/letsencrypt/boulder/ra/proto"
@@ -70,11 +71,12 @@ var (
7071
type RegistrationAuthorityImpl struct {
7172
rapb.UnsafeRegistrationAuthorityServer
7273
rapb.UnsafeSCTProviderServer
73-
CA capb.CertificateAuthorityClient
74-
VA va.RemoteClients
75-
SA sapb.StorageAuthorityClient
76-
PA core.PolicyAuthority
77-
publisher pubpb.PublisherClient
74+
CA capb.CertificateAuthorityClient
75+
VA va.RemoteClients
76+
SA sapb.StorageAuthorityClient
77+
PA core.PolicyAuthority
78+
publisher pubpb.PublisherClient
79+
profileToMTCA map[string]mtcapb.MTCAClient
7880

7981
clk clock.Clock
8082
log blog.Logger
@@ -126,6 +128,7 @@ func NewRegistrationAuthorityImpl(
126128
finalizeTimeout time.Duration,
127129
ctp *ctpolicy.CTPolicy,
128130
issuers []*issuance.Certificate,
131+
profileToMTCA map[string]mtcapb.MTCAClient,
129132
) *RegistrationAuthorityImpl {
130133
ctpolicyResults := promauto.With(stats).NewHistogramVec(
131134
prometheus.HistogramOpts{
@@ -215,6 +218,7 @@ func NewRegistrationAuthorityImpl(
215218
limiter: limiter,
216219
txnBuilder: txnBuilder,
217220
publisher: pubc,
221+
profileToMTCA: profileToMTCA,
218222
finalizeTimeout: finalizeTimeout,
219223
ctpolicy: ctp,
220224
ctpolicyResults: ctpolicyResults,
@@ -262,6 +266,9 @@ type ValidationProfileConfig struct {
262266
// specified, the profile is open to all accounts. If the file
263267
// exists but is empty, the profile is closed to all accounts.
264268
AllowList string `validate:"omitempty"`
269+
// MTC indicates that orders with this profile should be sent to an
270+
// MTCA instance for issuance.
271+
MTC bool `validate:"omitempty"`
265272
// IdentifierTypes is a list of identifier types that may be issued under
266273
// this profile.
267274
IdentifierTypes []identifier.IdentifierType `validate:"required,dive,oneof=dns ip"`
@@ -292,6 +299,9 @@ type validationProfile struct {
292299
// identifierTypes is a list of identifier types that may be issued under
293300
// this profile.
294301
identifierTypes []identifier.IdentifierType
302+
// MTC indicates that orders with this profile should be sent to an
303+
// MTCA instance for issuance.
304+
mtc bool
295305
}
296306

297307
// validationProfiles provides access to the set of configured profiles,
@@ -359,6 +369,7 @@ func NewValidationProfiles(defaultName string, configs map[string]*ValidationPro
359369
maxNames: config.MaxNames,
360370
allowList: allowList,
361371
identifierTypes: config.IdentifierTypes,
372+
mtc: config.MTC,
362373
}
363374
}
364375

@@ -923,7 +934,10 @@ func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rap
923934

924935
// Steps 3 (issuance) and 4 (cleanup) are done inside a helper function so
925936
// that we can control whether or not that work happens asynchronously.
926-
if features.Get().AsyncFinalize {
937+
// For MTC issuance we don't immediately go async: we wait on the MTCA
938+
// sequencing an entry. This allows us to quickly return errors if sequencing
939+
// is unavailable for any reason.
940+
if features.Get().AsyncFinalize && !ra.isMTC(order) {
927941
// We do this work in a goroutine so that we can better handle latency from
928942
// getting SCTs and writing the (pre)certificate to the database. This lets
929943
// us return the order in the Processing state to the client immediately,
@@ -954,6 +968,31 @@ func (ra *RegistrationAuthorityImpl) FinalizeOrder(ctx context.Context, req *rap
954968
}
955969
}
956970

971+
func (ra *RegistrationAuthorityImpl) issueMTC(
972+
ctx context.Context,
973+
order *corepb.Order,
974+
subjectPublicKeyInfo []byte,
975+
) error {
976+
profileName := ra.profileName(order)
977+
mtca := ra.profileToMTCA[profileName]
978+
if mtca == nil {
979+
return fmt.Errorf("no MTCA configured for MTC profile %q", profileName)
980+
}
981+
982+
resp, err := mtca.Issue(ctx, &mtcapb.IssueRequest{
983+
Pubkey: subjectPublicKeyInfo,
984+
Identifiers: order.Identifiers,
985+
Profile: profileName,
986+
})
987+
988+
if err != nil {
989+
return fmt.Errorf("issuing MTC: %s", err)
990+
}
991+
992+
ra.log.Infof("issued MTC from %s: %d", resp.MtcLogID, resp.MtcEntryIndex)
993+
return nil
994+
}
995+
957996
// containsMustStaple returns true if the provided set of extensions includes
958997
// an entry whose OID and value both match the expected values for the OCSP
959998
// Must-Staple (a.k.a. id-pe-tlsFeature) extension.
@@ -1138,12 +1177,19 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter(
11381177
logEvent.PreviousCertificateIssued = timestamps.Timestamps[0].AsTime()
11391178
}
11401179

1141-
profileName := order.CertificateProfileName
1142-
if profileName == "" {
1143-
profileName = ra.profiles.defaultName
1180+
if ra.isMTC(order) {
1181+
err := ra.issueMTC(ctx, order, csr.RawSubjectPublicKeyInfo)
1182+
if err != nil {
1183+
ra.failOrder(ctx, order, web.ProblemDetailsForError(err, "Error finalizing order"))
1184+
return nil, err
1185+
}
1186+
1187+
ra.countCertificateIssued(ctx, order.RegistrationID, idents, isRenewal)
1188+
return order, nil
11441189
}
11451190

11461191
// Step 3: Issue the Certificate
1192+
profileName := ra.profileName(order)
11471193
cert, err := ra.issueCertificateInner(
11481194
ctx, csr, authzs, isRenewal, profileName, accountID(order.RegistrationID), orderID(order.Id))
11491195

@@ -1186,6 +1232,19 @@ func (ra *RegistrationAuthorityImpl) issueCertificateOuter(
11861232
return order, err
11871233
}
11881234

1235+
func (ra *RegistrationAuthorityImpl) profileName(order *corepb.Order) string {
1236+
if order.CertificateProfileName == "" {
1237+
return ra.profiles.defaultName
1238+
}
1239+
return order.CertificateProfileName
1240+
}
1241+
1242+
func (ra *RegistrationAuthorityImpl) isMTC(order *corepb.Order) bool {
1243+
profileName := ra.profileName(order)
1244+
profile := ra.profiles.byName[profileName]
1245+
return profile != nil && profile.mtc
1246+
}
1247+
11891248
// countCertificateIssued increments the certificates (per domain and per
11901249
// account) and duplicate certificate rate limits. There is no reason to surface
11911250
// errors from this function to the Subscriber, spends against these limit are

ra/ra_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import (
5252
blog "github.com/letsencrypt/boulder/log"
5353
"github.com/letsencrypt/boulder/metrics"
5454
"github.com/letsencrypt/boulder/mocks"
55+
mtcapb "github.com/letsencrypt/boulder/mtca/proto"
5556
"github.com/letsencrypt/boulder/policy"
5657
pubpb "github.com/letsencrypt/boulder/publisher/proto"
5758
rapb "github.com/letsencrypt/boulder/ra/proto"
@@ -379,10 +380,12 @@ func initAuthorities(t *testing.T) (*DummyValidationAuthority, sapb.StorageAutho
379380
})
380381
test.AssertNotError(t, err, "making validation profiles")
381382

383+
profileToMTCA := make(map[string]mtcapb.MTCAClient)
384+
382385
ra := NewRegistrationAuthorityImpl(
383386
fc, log, stats,
384387
1, testKeyPolicy, limiter, txnBuilder,
385-
profiles, nil, 5*time.Minute, ctp, nil)
388+
profiles, nil, 5*time.Minute, ctp, nil, profileToMTCA)
386389
ra.SA = sa
387390
ra.VA = va
388391
ra.CA = ca

sa/db/01-boulder_sa_next.sql

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,5 @@ ALTER TABLE `certificateStatus` DROP COLUMN `LockCol`;
249249
ALTER TABLE `revokedCertificates` ADD KEY `serial` (`serial`);
250250

251251
ALTER TABLE `orders`
252-
ADD COLUMN `isMTC` bool NOT NULL DEFAULT FALSE,
253252
ADD COLUMN `mtcLogID` varchar(255) DEFAULT NULL,
254253
ADD COLUMN `mtcSerialNumber` bigint(20) unsigned DEFAULT NULL;

test/config-next/ra.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@
6464
"dns",
6565
"ip"
6666
]
67+
},
68+
"mtcshortlived": {
69+
"mtc": true,
70+
"pendingAuthzLifetime": "7h",
71+
"validAuthzLifetime": "7h",
72+
"orderLifetime": "7h",
73+
"maxNames": 10,
74+
"identifierTypes": [
75+
"dns",
76+
"ip"
77+
]
6778
}
6879
},
6980
"defaultProfileName": "legacy",
@@ -72,6 +83,18 @@
7283
"certFile": "test/certs/ipki/ra.boulder/cert.pem",
7384
"keyFile": "test/certs/ipki/ra.boulder/key.pem"
7485
},
86+
"profileToMTCA": {
87+
"mtcshortlived": {
88+
"dnsAuthority": "consul.service.consul",
89+
"srvLookup": {
90+
"service": "mtca",
91+
"domain": "service.consul"
92+
},
93+
"timeout": "20s",
94+
"noWaitForReady": false,
95+
"hostOverride": "mtca.boulder"
96+
}
97+
},
7598
"vaService": {
7699
"dnsAuthority": "consul.service.consul",
77100
"srvLookup": {

test/config-next/wfe2.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@
141141
"certProfiles": {
142142
"legacy": "The normal profile you know and love",
143143
"modern": "Profile 2: Electric Boogaloo",
144-
"shortlived": "Like modern, but smaller"
144+
"shortlived": "Like modern, but smaller",
145+
"mtcshortlived": "Like shortlived, but MTC"
145146
},
146147
"unpause": {
147148
"hmacKey": {

test/integration/issuance_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"crypto/x509/pkix"
1111
"fmt"
1212
"net"
13+
"os"
1314
"strings"
1415
"testing"
1516

@@ -171,6 +172,33 @@ func TestIssuanceProfiles(t *testing.T) {
171172
test.AssertEquals(t, len(modern.SubjectKeyId), 0)
172173
}
173174

175+
// TestIssuanceMTC issues from an MTC profile.
176+
func TestIssuanceMTC(t *testing.T) {
177+
t.Parallel()
178+
if os.Getenv("BOULDER_CONFIG_DIR") != "test/config-next" {
179+
t.Skip("MTC issuance only available in config-next")
180+
}
181+
182+
client, err := makeClient()
183+
if err != nil {
184+
t.Fatalf("creating acme client: %s", err)
185+
}
186+
187+
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
188+
if err != nil {
189+
t.Fatalf("generating keypair: %s", err)
190+
}
191+
192+
idents := []acme.Identifier{{Type: "dns", Value: random_domain()}}
193+
194+
_, err = authAndIssue(client, key, idents, true, "mtcshortlived")
195+
// "finalized order timeout" is as far as we get for now. Once we are collecting
196+
// signatures and constructing standalone certificates, we'll expect this to succeed.
197+
if err == nil || !strings.Contains(err.Error(), "finalized order timeout") {
198+
t.Fatalf("issuing certificate: expected 'finalized order timeout', got %q", err)
199+
}
200+
}
201+
174202
// TestIPShortLived verifies that we will allow IP address identifiers only in
175203
// orders that use the shortlived profile.
176204
func TestIPShortLived(t *testing.T) {

0 commit comments

Comments
 (0)