Skip to content

Commit 1f65bc6

Browse files
authored
fix(client): race in cert renewal (#42)
* fix: race in cert renewal this refactor ensures unique cache per instance is initialized, and the global default cert management in certmagic is not triggered. circular dependency is solved in alrernative way: 1. recursive call for newCertmagicConfig is removed and replaced by simpler configGetter 2. TLSConfig() ensures correct GetCertificate() is used * refactor: named acme-broker logger * test: OnCertRenewed basic test that confirms cert renewal works as expected when expiration event is triggered
1 parent fe07320 commit 1f65bc6

File tree

3 files changed

+121
-71
lines changed

3 files changed

+121
-71
lines changed

client/acme.go

+83-64
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type P2PForgeCertMgr struct {
3636
ProvideHost func(host.Host)
3737
hostFn func() host.Host
3838
hasHost func() bool
39-
cfg *certmagic.Config
39+
certmagic *certmagic.Config
4040
log *zap.SugaredLogger
4141
allowPrivateForgeAddresses bool
4242
produceShortAddrs bool
@@ -45,11 +45,6 @@ type P2PForgeCertMgr struct {
4545
certCheckMx sync.RWMutex
4646
}
4747

48-
var (
49-
defaultCertCache *certmagic.Cache
50-
defaultCertCacheMu sync.Mutex
51-
)
52-
5348
func isRelayAddr(a multiaddr.Multiaddr) bool {
5449
found := false
5550
multiaddr.ForEach(a, func(c multiaddr.Component) bool {
@@ -84,9 +79,11 @@ type P2PForgeCertMgrConfig struct {
8479
storage certmagic.Storage
8580
modifyForgeRequest func(r *http.Request) error
8681
onCertLoaded func()
82+
onCertRenewed func()
8783
log *zap.SugaredLogger
8884
allowPrivateForgeAddresses bool
8985
produceShortAddrs bool
86+
renewCheckInterval time.Duration
9087
}
9188

9289
type P2PForgeCertMgrOptions func(*P2PForgeCertMgrConfig) error
@@ -176,6 +173,22 @@ func WithTrustedRoots(trustedRoots *x509.CertPool) P2PForgeCertMgrOptions {
176173
}
177174
}
178175

176+
// WithOnCertRenewed is optional callback executed on cert renewal event
177+
func WithOnCertRenewed(fn func()) P2PForgeCertMgrOptions {
178+
return func(config *P2PForgeCertMgrConfig) error {
179+
config.onCertRenewed = fn
180+
return nil
181+
}
182+
}
183+
184+
// WithRenewCheckInterval is meant for testing
185+
func WithRenewCheckInterval(renewCheckInterval time.Duration) P2PForgeCertMgrOptions {
186+
return func(config *P2PForgeCertMgrConfig) error {
187+
config.renewCheckInterval = renewCheckInterval
188+
return nil
189+
}
190+
}
191+
179192
// WithAllowPrivateForgeAddrs is meant for testing or skipping all the
180193
// connectivity checks libp2p node needs to pass before it can request domain
181194
// and start ACME DNS-01 challenge.
@@ -210,41 +223,13 @@ func WithLogger(log *zap.SugaredLogger) P2PForgeCertMgrOptions {
210223
}
211224
}
212225

213-
// newCertmagicConfig is p2p-forge/client-specific version of
214-
// certmagic.NewDefault() that ensures we have our own cert cache. This is
215-
// necessary to ensure cert maintenance spawned by NewCache does not share
216-
// global certmagic.Default.Storage, and certmagic.Default.Logger and uses
217-
// storage path specific to p2p-forge, and no other instance of certmagic in
218-
// golang application.
219-
func newCertmagicConfig(mgrCfg *P2PForgeCertMgrConfig) *certmagic.Config {
220-
clog := mgrCfg.log.Desugar()
221-
222-
defaultCertCacheMu.Lock()
223-
if defaultCertCache == nil {
224-
defaultCertCache = certmagic.NewCache(certmagic.CacheOptions{
225-
GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
226-
// default getter that does not depend on certmagic defaults
227-
// and respects Config.Storage path
228-
return newCertmagicConfig(mgrCfg), nil
229-
},
230-
Logger: clog,
231-
})
232-
}
233-
certCache := defaultCertCache
234-
defaultCertCacheMu.Unlock()
235-
236-
return certmagic.New(certCache, certmagic.Config{
237-
Storage: mgrCfg.storage,
238-
Logger: clog,
239-
})
240-
}
241-
242226
// NewP2PForgeCertMgr handles the creation and management of certificates that are automatically granted by a forge
243227
// to a libp2p host.
244228
//
245229
// Calling this function signifies your acceptance to
246230
// the CA's Subscriber Agreement and/or Terms of Service. Let's Encrypt is the default CA.
247231
func NewP2PForgeCertMgr(opts ...P2PForgeCertMgrOptions) (*P2PForgeCertMgr, error) {
232+
// Init config + apply optional user settings
248233
mgrCfg := &P2PForgeCertMgrConfig{}
249234
for _, opt := range opts {
250235
if err := opt(mgrCfg); err != nil {
@@ -270,14 +255,11 @@ func NewP2PForgeCertMgr(opts ...P2PForgeCertMgrOptions) (*P2PForgeCertMgr, error
270255
return nil, fmt.Errorf("must specify the forge registration endpoint if using a non-default forge")
271256
}
272257
}
273-
274-
const defaultStorageLocation = "p2p-forge-certs"
275258
if mgrCfg.storage == nil {
276-
mgrCfg.storage = &certmagic.FileStorage{Path: defaultStorageLocation}
259+
mgrCfg.storage = &certmagic.FileStorage{Path: DefaultStorageLocation}
277260
}
278261

279-
certCfg := newCertmagicConfig(mgrCfg)
280-
262+
// Wire up p2p-forge manager instance
281263
hostChan := make(chan host.Host, 1)
282264
provideHost := func(host host.Host) { hostChan <- host }
283265
hasHostChan := make(chan struct{})
@@ -293,39 +275,60 @@ func NewP2PForgeCertMgr(opts ...P2PForgeCertMgrOptions) (*P2PForgeCertMgr, error
293275
defer close(hasHostChan)
294276
return <-hostChan
295277
})
278+
mgr := &P2PForgeCertMgr{
279+
forgeDomain: mgrCfg.forgeDomain,
280+
forgeRegistrationEndpoint: mgrCfg.forgeRegistrationEndpoint,
281+
ProvideHost: provideHost,
282+
hostFn: hostFn,
283+
hasHost: hasHostFn,
284+
log: mgrCfg.log,
285+
allowPrivateForgeAddresses: mgrCfg.allowPrivateForgeAddresses,
286+
produceShortAddrs: mgrCfg.produceShortAddrs,
287+
}
288+
289+
// NOTE: callback getter is necessary to avoid circular dependency
290+
// but also structure code to avoid issues like https://github.com/ipshipyard/p2p-forge/issues/28
291+
configGetter := func(cert certmagic.Certificate) (*certmagic.Config, error) {
292+
if mgr.certmagic == nil {
293+
return nil, errors.New("P2PForgeCertmgr.certmagic is not set")
294+
}
295+
return mgr.certmagic, nil
296+
}
297+
298+
magicCache := certmagic.NewCache(certmagic.CacheOptions{
299+
GetConfigForCert: configGetter,
300+
RenewCheckInterval: mgrCfg.renewCheckInterval,
301+
Logger: mgrCfg.log.Desugar(),
302+
})
296303

297-
myACME := certmagic.NewACMEIssuer(certCfg, certmagic.ACMEIssuer{ // TODO: UX around user passed emails + agreement
304+
// Wire up final certmagic config by calling upstream New with sanity checks
305+
mgr.certmagic = certmagic.New(magicCache, certmagic.Config{
306+
Storage: mgrCfg.storage,
307+
Logger: mgrCfg.log.Desugar(),
308+
})
309+
310+
// Wire up Issuer that does brokered DNS-01 ACME challenge
311+
acmeLog := mgrCfg.log.Named("acme-broker")
312+
brokeredDNS01Issuer := certmagic.NewACMEIssuer(mgr.certmagic, certmagic.ACMEIssuer{
298313
CA: mgrCfg.caEndpoint,
299314
Email: mgrCfg.userEmail,
300315
Agreed: true,
301316
DNS01Solver: &dns01P2PForgeSolver{
302317
forgeRegistrationEndpoint: mgrCfg.forgeRegistrationEndpoint,
303318
forgeAuth: mgrCfg.forgeAuth,
304-
hostFn: hostFn,
319+
hostFn: mgr.hostFn,
305320
modifyForgeRequest: mgrCfg.modifyForgeRequest,
306321
userAgent: mgrCfg.userAgent,
307322
allowPrivateForgeAddresses: mgrCfg.allowPrivateForgeAddresses,
308-
log: mgrCfg.log.Named("dns01solver"),
323+
log: acmeLog.Named("dns01solver"),
309324
},
310325
TrustedRoots: mgrCfg.trustedRoots,
311-
Logger: certCfg.Logger,
326+
Logger: acmeLog.Desugar(),
312327
})
328+
mgr.certmagic.Issuers = []certmagic.Issuer{brokeredDNS01Issuer}
313329

314-
certCfg.Issuers = []certmagic.Issuer{myACME}
315-
316-
mgr := &P2PForgeCertMgr{
317-
forgeDomain: mgrCfg.forgeDomain,
318-
forgeRegistrationEndpoint: mgrCfg.forgeRegistrationEndpoint,
319-
ProvideHost: provideHost,
320-
hostFn: hostFn,
321-
hasHost: hasHostFn,
322-
cfg: certCfg,
323-
log: mgrCfg.log,
324-
allowPrivateForgeAddresses: mgrCfg.allowPrivateForgeAddresses,
325-
produceShortAddrs: mgrCfg.produceShortAddrs,
326-
}
327-
328-
certCfg.OnEvent = func(ctx context.Context, event string, data map[string]any) error {
330+
// Wire up onCertLoaded callback
331+
mgr.certmagic.OnEvent = func(ctx context.Context, event string, data map[string]any) error {
329332
if event == "cached_managed_cert" {
330333
sans, ok := data["sans"]
331334
if !ok {
@@ -352,24 +355,39 @@ func NewP2PForgeCertMgr(opts ...P2PForgeCertMgrOptions) (*P2PForgeCertMgr, error
352355
}
353356
return nil
354357
}
358+
359+
// Execute user function for on certificate cert renewal
360+
if event == "cert_obtained" && mgrCfg.onCertRenewed != nil {
361+
if renewal, ok := data["renewal"].(bool); ok && renewal {
362+
name := certName(hostFn().ID(), mgrCfg.forgeDomain)
363+
if id, ok := data["identifier"].(string); ok && id == name {
364+
mgrCfg.onCertRenewed()
365+
}
366+
}
367+
return nil
368+
}
369+
355370
return nil
356371
}
357372

358373
return mgr, nil
359374
}
360375

361376
func (m *P2PForgeCertMgr) Start() error {
362-
if m.cfg == nil || m.hostFn == nil {
377+
if m.certmagic == nil || m.hostFn == nil {
363378
return errors.New("unable to start without a certmagic and libp2p host")
364379
}
380+
if m.certmagic.Storage == nil {
381+
return errors.New("unable to start without a certmagic Cache and Storage set up")
382+
}
365383
m.ctx, m.cancel = context.WithCancel(context.Background())
366384
go func() {
367385
log := m.log.Named("start")
368386
h := m.hostFn()
369387
name := certName(h.ID(), m.forgeDomain)
370-
certExists := localCertExists(m.ctx, m.cfg, name)
388+
certExists := localCertExists(m.ctx, m.certmagic, name)
371389
startCertManagement := func() {
372-
if err := m.cfg.ManageAsync(m.ctx, []string{name}); err != nil {
390+
if err := m.certmagic.ManageAsync(m.ctx, []string{name}); err != nil {
373391
log.Error(err)
374392
}
375393
}
@@ -433,8 +451,9 @@ func (m *P2PForgeCertMgr) Stop() {
433451

434452
// TLSConfig returns a tls.Config that managed by the P2PForgeCertMgr
435453
func (m *P2PForgeCertMgr) TLSConfig() *tls.Config {
436-
tlsCfg := m.cfg.TLSConfig()
454+
tlsCfg := m.certmagic.TLSConfig()
437455
tlsCfg.NextProtos = nil // remove the ACME ALPN
456+
tlsCfg.GetCertificate = m.certmagic.GetCertificate
438457
return tlsCfg
439458
}
440459

@@ -449,7 +468,7 @@ func (m *P2PForgeCertMgr) AddrStrings() []string {
449468
// This should be used with the libp2p.AddrsFactory option to ensure that a libp2p host with forge managed addresses
450469
// only announces those that are active and valid.
451470
func (m *P2PForgeCertMgr) AddressFactory() config.AddrsFactory {
452-
tlsCfg := m.cfg.TLSConfig()
471+
tlsCfg := m.certmagic.TLSConfig()
453472
tlsCfg.NextProtos = []string{"h2", "http/1.1"} // remove the ACME ALPN and set the HTTP 1.1 and 2 ALPNs
454473

455474
return m.createAddrsFactory(m.allowPrivateForgeAddresses, m.produceShortAddrs)

client/defaults.go

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const (
2020
// ForgeAuthHeader optional HTTP header that client should include when
2121
// talking to a limited access registration endpoint
2222
ForgeAuthHeader = "Forge-Authorization"
23+
24+
DefaultStorageLocation = "p2p-forge-certs"
2325
)
2426

2527
// defaultUserAgent is used as a fallback to inform HTTP server which library

0 commit comments

Comments
 (0)