From 0b5e0e9657409a8ef1f8ea34b8be328f6bf87830 Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Sat, 14 Feb 2026 05:41:11 +0800 Subject: [PATCH 01/14] chore: use math/rand/v2 instead of math/rand (#363) * use math/rand/v2 instead of math/rand * fill the contents by manually encoding pseudo-random numbers * fix variable declaration --- cache.go | 4 ++-- certificates.go | 4 ++-- config.go | 21 +++++++++++++++------ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/cache.go b/cache.go index 3b5cfb0b..8f325576 100644 --- a/cache.go +++ b/cache.go @@ -16,7 +16,7 @@ package certmagic import ( "fmt" - weakrand "math/rand" + weakrand "math/rand/v2" "strings" "sync" "time" @@ -244,7 +244,7 @@ func (certCache *Cache) unsyncedCacheCertificate(cert Certificate) { // map with less code, that is a heavily skewed eviction // strategy; generating random numbers is cheap and // ensures a much better distribution. - rnd := weakrand.Intn(cacheSize) + rnd := weakrand.IntN(cacheSize) i := 0 for _, randomCert := range certCache.cache { if i >= rnd && randomCert.managed { // don't evict manually-loaded certs diff --git a/certificates.go b/certificates.go index e0246dde..76087fc1 100644 --- a/certificates.go +++ b/certificates.go @@ -21,7 +21,7 @@ import ( "encoding/json" "errors" "fmt" - "math/rand" + "math/rand/v2" "net" "os" "strings" @@ -128,7 +128,7 @@ func (cfg *Config) certNeedsRenewal(leaf *x509.Certificate, ari acme.RenewalInfo if selectedTime.IsZero() && (!ari.SuggestedWindow.Start.IsZero() && !ari.SuggestedWindow.End.IsZero()) { start, end := ari.SuggestedWindow.Start.Unix()+1, ari.SuggestedWindow.End.Unix() - selectedTime = time.Unix(rand.Int63n(end-start)+start, 0).UTC() + selectedTime = time.Unix(rand.Int64N(end-start)+start, 0).UTC() logger.Warn("no renewal time had been selected with ARI; chose an ephemeral one for now", zap.Time("ephemeral_selected_time", selectedTime)) } diff --git a/config.go b/config.go index 419b3dcb..c8127ba7 100644 --- a/config.go +++ b/config.go @@ -28,7 +28,7 @@ import ( "errors" "fmt" "io/fs" - weakrand "math/rand" + weakrand "math/rand/v2" "net" "net/http" "net/url" @@ -1236,11 +1236,20 @@ func (cfg *Config) checkStorage(ctx context.Context) error { } key := fmt.Sprintf("rw_test_%d", weakrand.Int()) contents := make([]byte, 1024*10) // size sufficient for one or two ACME resources - _, err := weakrand.Read(contents) - if err != nil { - return err - } - err = cfg.Storage.Store(ctx, key, contents) + // This is how ChaCha8.Read works, without handling the case where the slice length is not a multiple of 8. + // This also avoids the use of a mutex and an import. + for i := 0; i < len(contents); i += 8 { + v := weakrand.Uint64() + contents[i] = byte(v) + contents[i+1] = byte(v >> 8) + contents[i+2] = byte(v >> 16) + contents[i+3] = byte(v >> 24) + contents[i+4] = byte(v >> 32) + contents[i+5] = byte(v >> 40) + contents[i+6] = byte(v >> 48) + contents[i+7] = byte(v >> 56) + } + err := cfg.Storage.Store(ctx, key, contents) if err != nil { return err } From 69029254f738600303728bcd08e0f6d625d880e1 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Mon, 16 Feb 2026 12:07:25 -0500 Subject: [PATCH 02/14] chore: Bump zerossl dependency (#370) * chore: Bump zerossl dependency * Bump test Go versions --- .github/workflows/ci.yml | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf0d992c..2eeffd89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] - go: [ '1.21', '1.22' ] + go: [ '1.25', '1.26' ] runs-on: ${{ matrix.os }} diff --git a/go.mod b/go.mod index 698a267f..c7f20320 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/caddyserver/certmagic go 1.24.0 require ( - github.com/caddyserver/zerossl v0.1.4 + github.com/caddyserver/zerossl v0.1.5 github.com/klauspost/cpuid/v2 v2.3.0 github.com/libdns/libdns v1.1.1 github.com/mholt/acmez/v3 v3.1.4 diff --git a/go.sum b/go.sum index 4bf75198..10a406a5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/caddyserver/zerossl v0.1.4 h1:CVJOE3MZeFisCERZjkxIcsqIH4fnFdlYWnPYeFtBHRw= -github.com/caddyserver/zerossl v0.1.4/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= +github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= From 8a2d459864a95f8328a5a635ed584570cef9b884 Mon Sep 17 00:00:00 2001 From: Benedikt <94720610+bndcts@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:59:46 +0100 Subject: [PATCH 03/14] fix: handshake worker starve when loading external certificate (#369) Co-authored-by: Matt Holt --- handshake.go | 55 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/handshake.go b/handshake.go index d14c1382..a227015b 100644 --- a/handshake.go +++ b/handshake.go @@ -298,9 +298,9 @@ func (cfg *Config) getCertDuringHandshake(ctx context.Context, hello *tls.Client // domain, avoid pounding manager or storage thousands of times simultaneously. We use a similar sync // strategy for obtaining certificate during handshake. certLoadWaitChansMu.Lock() - wait, ok := certLoadWaitChans[name] + waiter, ok := certLoadWaitChans[name] if ok { - // another goroutine is already loading the cert; just wait and we'll get it from the in-memory cache + // another goroutine is already loading the cert; just wait certLoadWaitChansMu.Unlock() timeout := time.NewTimer(2 * time.Minute) @@ -310,33 +310,44 @@ func (cfg *Config) getCertDuringHandshake(ctx context.Context, hello *tls.Client case <-ctx.Done(): timeout.Stop() return Certificate{}, ctx.Err() - case <-wait: + case <-waiter.done: timeout.Stop() } - return cfg.getCertDuringHandshake(ctx, hello, false) - } else { - // no other goroutine is currently trying to load this cert - wait = make(chan struct{}) - certLoadWaitChans[name] = wait - certLoadWaitChansMu.Unlock() + // If the leader got a result from an external cert manager, use it + // directly — these certs are not added to the cache, so a recursive + // cache lookup would miss. For cached certs (on-demand, managed), + // the waiter result will be empty and we fall through to the + // original recursive lookup. + if !waiter.cert.Empty() || waiter.err != nil { + return waiter.cert, waiter.err + } - // unblock others and clean up when we're done - defer func() { - certLoadWaitChansMu.Lock() - close(wait) - delete(certLoadWaitChans, name) - certLoadWaitChansMu.Unlock() - }() + return cfg.getCertDuringHandshake(ctx, hello, false) } + // no other goroutine is currently trying to load this cert + waiter = &certLoadWaiter{done: make(chan struct{})} + certLoadWaitChans[name] = waiter + certLoadWaitChansMu.Unlock() + + // unblock others and clean up when we're done + defer func() { + certLoadWaitChansMu.Lock() + close(waiter.done) + delete(certLoadWaitChans, name) + certLoadWaitChansMu.Unlock() + }() + // If an external Manager is configured, try to get it from them. // Only continue to use our own logic if it returns empty+nil. externalCert, err := cfg.getCertFromAnyCertManager(ctx, hello, logger) if err != nil { + waiter.err = err return Certificate{}, err } if !externalCert.Empty() { + waiter.cert = externalCert return externalCert, nil } @@ -946,9 +957,19 @@ var ( obtainCertWaitChansMu sync.Mutex ) +// certLoadWaiter coordinates concurrent certificate loading for the same name. +// The leader populates the result and closes the channel; waiters read the result +// after the channel is closed. This allows externally-managed certificates (which +// are not cached) to be shared directly with waiting goroutines. +type certLoadWaiter struct { + done chan struct{} + cert Certificate + err error +} + // TODO: this lockset should probably be per-cache var ( - certLoadWaitChans = make(map[string]chan struct{}) + certLoadWaitChans = make(map[string]*certLoadWaiter) certLoadWaitChansMu sync.Mutex ) From ce47f46495efb9456e5ba61db2e342b634d8bb6e Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 20 Feb 2026 12:21:15 -0700 Subject: [PATCH 04/14] go.mod: Upgrade dependencies --- go.mod | 16 ++++++++-------- go.sum | 40 ++++++++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index c7f20320..0059c986 100644 --- a/go.mod +++ b/go.mod @@ -6,20 +6,20 @@ require ( github.com/caddyserver/zerossl v0.1.5 github.com/klauspost/cpuid/v2 v2.3.0 github.com/libdns/libdns v1.1.1 - github.com/mholt/acmez/v3 v3.1.4 - github.com/miekg/dns v1.1.69 + github.com/mholt/acmez/v3 v3.1.6 + github.com/miekg/dns v1.1.72 github.com/zeebo/blake3 v0.2.4 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.46.0 - golang.org/x/net v0.48.0 + golang.org/x/crypto v0.48.0 + golang.org/x/net v0.50.0 ) require ( go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/mod v0.33.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.42.0 // indirect ) diff --git a/go.sum b/go.sum index 10a406a5..033475dd 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,25 @@ +code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= +code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU= +github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk= +github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U= +github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ= github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= -github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ= -github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= -github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc= -github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g= +github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= +github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= @@ -30,19 +38,19 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From a7a8ce3b2c2161378323b340b7f9d44f4dd2f86c Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Wed, 4 Mar 2026 17:26:54 -0500 Subject: [PATCH 05/14] logging: Disable stack traces, fix logger name (#372) --- acmeclient.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/acmeclient.go b/acmeclient.go index 5f746d96..08e1a53d 100644 --- a/acmeclient.go +++ b/acmeclient.go @@ -270,7 +270,13 @@ func (iss *ACMEIssuer) newBasicACMEClient() (*acmez.Client, error) { Directory: caURL, UserAgent: buildUAString(), HTTPClient: iss.httpClient, - Logger: slog.New(zapslog.NewHandler(iss.Logger.Named("acme_client").Core())), + Logger: slog.New(zapslog.NewHandler( + iss.Logger.Core(), + zapslog.WithName(iss.Logger.Name()+".acme_client"), + // the default enables traces at ERROR level, this disables + // them by setting it to a level higher than any other level + zapslog.AddStacktraceAt(slog.Level(127)), + )), }, }, nil } From b9e85a9e914f184eb5ae0f45e2fa2df188ff692e Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 4 Mar 2026 21:09:54 -0700 Subject: [PATCH 06/14] Don't log nil errors when stapling OCSP (fix #362) --- certificates.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/certificates.go b/certificates.go index 76087fc1..f92d9b8f 100644 --- a/certificates.go +++ b/certificates.go @@ -435,7 +435,7 @@ func (cfg Config) makeCertificateWithOCSP(ctx context.Context, certPEMBlock, key err = stapleOCSP(ctx, cfg.OCSP, cfg.Storage, &cert, certPEMBlock) if errors.Is(err, ErrNoOCSPServerSpecified) { cfg.Logger.Debug("stapling OCSP", zap.Error(err), zap.Strings("identifiers", cert.Names)) - } else { + } else if err != nil { cfg.Logger.Warn("stapling OCSP", zap.Error(err), zap.Strings("identifiers", cert.Names)) } } From fa1257f02bda2d7de97a5f004114b0062b0a6fbd Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Thu, 19 Mar 2026 15:31:03 -0600 Subject: [PATCH 07/14] Unblock ManageAsync() by putting manageOne() in a goroutine (#374) --- account_test.go | 2 +- config.go | 20 ++++++++++++++++---- ocsp.go | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/account_test.go b/account_test.go index f2a182f1..c6f70a11 100644 --- a/account_test.go +++ b/account_test.go @@ -302,7 +302,7 @@ func TestGetAccountAlreadyExistsSkipsBroken(t *testing.T) { email := "me@foobar.com" // Create a "corrupted" account - am.config.Storage.Store(ctx, am.storageKeyUserReg(am.CA, "notmeatall@foobar.com"), []byte("this is not a valid account")) + _ = am.config.Storage.Store(ctx, am.storageKeyUserReg(am.CA, "notmeatall@foobar.com"), []byte("this is not a valid account")) // Create the actual account account, err := am.newAccount(email) diff --git a/config.go b/config.go index c8127ba7..6a00fb19 100644 --- a/config.go +++ b/config.go @@ -382,11 +382,23 @@ func (cfg *Config) manageAll(ctx context.Context, domainNames []string, async bo continue } - // TODO: consider doing this in a goroutine if async, to utilize multiple cores while loading certs // otherwise, begin management immediately - err := cfg.manageOne(ctx, domainName, async) - if err != nil { - return err + if async { + // don't block loading, since stapling OCSP uses the network and could block all other certs + // from being managed... (kind of tricky to make it truly async any lower-level than this) + go func(subject string) { + err := cfg.manageOne(ctx, subject, async) + if err != nil { + cfg.Logger.Error("initiating certificate management", + zap.String("subject", subject), + zap.Error(err)) + } + }(domainName) + } else { + err := cfg.manageOne(ctx, domainName, async) + if err != nil { + return err + } } } diff --git a/ocsp.go b/ocsp.go index c87a560f..7d0d94dd 100644 --- a/ocsp.go +++ b/ocsp.go @@ -53,7 +53,7 @@ func stapleOCSP(ctx context.Context, ocspConfig OCSPConfig, storage Storage, cer // we need a PEM encoding only for some function calls below bundle := new(bytes.Buffer) for _, derBytes := range cert.Certificate.Certificate { - pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + _ = pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) } pemBundle = bundle.Bytes() } From e03792e204502548bb056f9c86ec72dda63d57f3 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 10 Apr 2026 09:45:27 -0600 Subject: [PATCH 08/14] Modernize TLSConfig() (close #375) --- config.go | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/config.go b/config.go index 6a00fb19..27c39cd8 100644 --- a/config.go +++ b/config.go @@ -1131,10 +1131,10 @@ func (cfg *Config) RevokeCert(ctx context.Context, domain string, reason int, in return nil } -// TLSConfig is an opinionated method that returns a recommended, modern -// TLS configuration that can be used to configure TLS listeners. Aside -// from safe, modern defaults, this method sets two critical fields on the -// TLS config which are required to enable automatic certificate +// TLSConfig returns a recommended, modern TLS configuration that can be used +// to configure TLS listeners. Aside from using the safe, modern defaults +// implemented by the Go standard library, this method sets two critical fields +// on the TLS config which are required to enable automatic certificate // management: GetCertificate and NextProtos. // // The GetCertificate field is necessary to get certificates from memory @@ -1158,15 +1158,6 @@ func (cfg *Config) TLSConfig() *tls.Config { // these two fields necessary for TLS-ALPN challenge GetCertificate: cfg.GetCertificate, NextProtos: []string{acmez.ACMETLS1Protocol}, - - // the rest recommended for modern TLS servers - MinVersion: tls.VersionTLS12, - CurvePreferences: []tls.CurveID{ - tls.X25519, - tls.CurveP256, - }, - CipherSuites: preferredDefaultCipherSuites(), - PreferServerCipherSuites: true, } } From 60d9d8b415d6e3f2fa7aec9b440c7dc2473126d4 Mon Sep 17 00:00:00 2001 From: nekohasekai Date: Tue, 21 Apr 2026 22:38:02 +0800 Subject: [PATCH 09/14] Fix HTTP-01 challenge for IPv6 literal addresses (#377) When the ACME CA sends an HTTP-01 challenge request to an IPv6 address, the Host header is bracketed (e.g. [2001:db8::1]) without a port. net.SplitHostPort fails on this input, causing hostOnly() to return the bracketed form, which doesn't match the bare IP in challenge.Identifier.Value. Strip brackets from bare IPv6 addresses in hostOnly(). --- certificates.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/certificates.go b/certificates.go index f92d9b8f..d4a4dfb2 100644 --- a/certificates.go +++ b/certificates.go @@ -651,6 +651,9 @@ func isInternalIP(addr string) bool { func hostOnly(hostport string) string { host, _, err := net.SplitHostPort(hostport) if err != nil { + if len(hostport) > 2 && hostport[0] == '[' && hostport[len(hostport)-1] == ']' { + return hostport[1 : len(hostport)-1] + } return hostport // OK; probably had no port to begin with } return host From 3229642925944adf6cd82422153f127303c325e5 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 21 Apr 2026 14:11:50 -0600 Subject: [PATCH 10/14] go.mod: Upgrade indirect dependencies --- go.mod | 16 ++++++++-------- go.sum | 32 ++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 0059c986..68987a6c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/caddyserver/certmagic -go 1.24.0 +go 1.25.0 require ( github.com/caddyserver/zerossl v0.1.5 @@ -11,15 +11,15 @@ require ( github.com/zeebo/blake3 v0.2.4 go.uber.org/zap v1.27.1 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.48.0 - golang.org/x/net v0.50.0 + golang.org/x/crypto v0.50.0 + golang.org/x/net v0.53.0 ) require ( go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/tools v0.44.0 // indirect ) diff --git a/go.sum b/go.sum index 033475dd..9a1aec88 100644 --- a/go.sum +++ b/go.sum @@ -38,19 +38,23 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 3e3363fcc7cd7a02a4104478ebf832d56df65182 Mon Sep 17 00:00:00 2001 From: Matt Gros <3311227+mpge@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:15:07 -0400 Subject: [PATCH 11/14] readme: add dark-mode banner for GitHub UI (#379) Vendor the README header PNG under docs/banner and add a dark variant selected via prefers-color-scheme so the banner reads well in GitHub dark mode. Made-with: Cursor Signed-off-by: Matt Gros <3311227+mpge@users.noreply.github.com> --- README.md | 7 ++++++- docs/banner/certmagic-banner-dark.png | Bin 0 -> 51591 bytes docs/banner/certmagic-banner.png | Bin 0 -> 20370 bytes 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/banner/certmagic-banner-dark.png create mode 100644 docs/banner/certmagic-banner.png diff --git a/README.md b/README.md index 8c176f44..513f781a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@

- CertMagic + + + + CertMagic + +

Easy and Powerful TLS Automation

The same library used by the Caddy Web Server

diff --git a/docs/banner/certmagic-banner-dark.png b/docs/banner/certmagic-banner-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..315010e407299c574a3b44ad208b05069d0e89c2 GIT binary patch literal 51591 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 x68^M>iZNb>wr;3DO0d$~ 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 In|%KN08VF~$p8QV literal 0 HcmV?d00001 diff --git a/docs/banner/certmagic-banner.png b/docs/banner/certmagic-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..c55392fb5ef49ba1d94e40a69358d5e01c883f00 GIT binary patch literal 20370 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 literal 0 HcmV?d00001 From 2cf7e0858b0d34834263033536515f91a40ddbec Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 23 Apr 2026 16:04:45 -0600 Subject: [PATCH 12/14] Revert "Fix HTTP-01 challenge for IPv6 literal addresses (#377)" This reverts commit 60d9d8b415d6e3f2fa7aec9b440c7dc2473126d4. --- certificates.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/certificates.go b/certificates.go index d4a4dfb2..f92d9b8f 100644 --- a/certificates.go +++ b/certificates.go @@ -651,9 +651,6 @@ func isInternalIP(addr string) bool { func hostOnly(hostport string) string { host, _, err := net.SplitHostPort(hostport) if err != nil { - if len(hostport) > 2 && hostport[0] == '[' && hostport[len(hostport)-1] == ']' { - return hostport[1 : len(hostport)-1] - } return hostport // OK; probably had no port to begin with } return host From cdc4eb24e7ec1d14fe3a8f71f6069212dae9c265 Mon Sep 17 00:00:00 2001 From: "Y.Horie" Date: Fri, 24 Apr 2026 07:11:35 +0900 Subject: [PATCH 13/14] fix: Normalization IPv6 addresses for ACME challenge (#376) * subsumed https://github.com/caddyserver/caddy/pull/7619 Co-authored-by: Matt Holt --- certificates.go | 7 +++++++ certificates_test.go | 35 +++++++++++++++++++++++++++++++++++ solvers.go | 2 ++ solvers_test.go | 22 ++++++++++++++++++++++ 4 files changed, 66 insertions(+) diff --git a/certificates.go b/certificates.go index f92d9b8f..e7cfa5fe 100644 --- a/certificates.go +++ b/certificates.go @@ -651,6 +651,11 @@ func isInternalIP(addr string) bool { func hostOnly(hostport string) string { host, _, err := net.SplitHostPort(hostport) if err != nil { + // May be a bare IPv6 address in brackets without a port (e.g. "[::1]"). + // net.SplitHostPort requires a port when brackets are present, so strip them. + if len(hostport) > 1 && hostport[0] == '[' && hostport[len(hostport)-1] == ']' { + return hostport[1 : len(hostport)-1] + } return hostport // OK; probably had no port to begin with } return host @@ -665,6 +670,8 @@ func hostOnly(hostport string) string { // It uses DNS wildcard matching logic and is case-insensitive. // https://tools.ietf.org/html/rfc2818#section-3.1 func MatchWildcard(subject, wildcard string) bool { + // Strip brackets from IPv6 addresses (e.g. "[::1]" from HTTP Host headers). + subject = hostOnly(subject) subject, wildcard = strings.ToLower(subject), strings.ToLower(wildcard) if subject == wildcard { return true diff --git a/certificates_test.go b/certificates_test.go index f8ecc3df..a53abcb5 100644 --- a/certificates_test.go +++ b/certificates_test.go @@ -189,6 +189,37 @@ func TestSubjectQualifiesForPublicCert(t *testing.T) { } } +func TestHostOnly(t *testing.T) { + for i, test := range []struct { + input string + expect string + }{ + // hostname without port + {"example.com", "example.com"}, + // hostname with port + {"example.com:443", "example.com"}, + // IPv4 without port + {"1.2.3.4", "1.2.3.4"}, + // IPv4 with port + {"1.2.3.4:80", "1.2.3.4"}, + // IPv6 without port and without brackets + {"::1", "::1"}, + // IPv6 with port (brackets required by RFC 7230) + {"[::1]:80", "::1"}, + // IPv6 without port but with brackets (Go's HTTP server format for host-only) + {"[::1]", "::1"}, + // full IPv6 without port but with brackets + {"[2001:db8::1]", "2001:db8::1"}, + // full IPv6 with port + {"[2001:db8::1]:8080", "2001:db8::1"}, + } { + actual := hostOnly(test.input) + if actual != test.expect { + t.Errorf("Test %d: hostOnly(%q) = %q, want %q", i, test.input, actual, test.expect) + } + } +} + func TestMatchWildcard(t *testing.T) { for i, test := range []struct { subject, wildcard string @@ -217,6 +248,10 @@ func TestMatchWildcard(t *testing.T) { {"1.2.3.4.5.6", "*.*.*.*.*.*", true}, {"0.1.2.3.4.5.6", "*.*.*.*.*.*", false}, {"1.2.3.4", "1.2.3.*", false}, // https://tools.ietf.org/html/rfc2818#section-3.1 + // Bracketed IPv6 subjects (from HTTP Host headers) must match bare IPv6 wildcards. + {"[::1]", "::1", true}, + {"[2001:db8::1]", "2001:db8::1", true}, + {"[::1]", "::2", false}, } { actual := MatchWildcard(test.subject, test.wildcard) if actual != test.expect { diff --git a/solvers.go b/solvers.go index 677fad3f..b4801303 100644 --- a/solvers.go +++ b/solvers.go @@ -768,6 +768,8 @@ func dialTCPSocket(addr string) error { // GetACMEChallenge returns an active ACME challenge for the given identifier, // or false if no active challenge for that identifier is known. func GetACMEChallenge(identifier string) (Challenge, bool) { + // Strip brackets from IPv6 addresses (e.g. "[::1]" from HTTP Host headers). + identifier = hostOnly(identifier) activeChallengesMu.Lock() chalData, ok := activeChallenges[identifier] activeChallengesMu.Unlock() diff --git a/solvers_test.go b/solvers_test.go index d30ce66d..c9fc1974 100644 --- a/solvers_test.go +++ b/solvers_test.go @@ -155,3 +155,25 @@ func Test_challengeKey(t *testing.T) { }) } } + +func TestGetACMEChallenge_IPv6Brackets(t *testing.T) { + // Store a challenge under a bare IPv6 identifier (as CertMagic does internally). + bare := "::1" + activeChallengesMu.Lock() + activeChallenges[bare] = Challenge{} + activeChallengesMu.Unlock() + defer func() { + activeChallengesMu.Lock() + delete(activeChallenges, bare) + activeChallengesMu.Unlock() + }() + + // Lookup with bracketed IPv6 (as received from Go's HTTP server via r.Host). + if _, ok := GetACMEChallenge("[::1]"); !ok { + t.Error("GetACMEChallenge(\"[::1]\") should find challenge stored under \"::1\"") + } + // Lookup with bare IPv6 should still work. + if _, ok := GetACMEChallenge("::1"); !ok { + t.Error("GetACMEChallenge(\"::1\") should find challenge stored under \"::1\"") + } +} From c7496f1a6a93025c528b03cde701bf06394440fd Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Fri, 24 Apr 2026 10:51:44 -0600 Subject: [PATCH 14/14] More validation of delegated OCSP responders (#378) * More validation of delegated OCSP responders * Add tests --- ocsp.go | 27 +++++++++++++++++++++ ocsp_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/ocsp.go b/ocsp.go index 7d0d94dd..82b91dc0 100644 --- a/ocsp.go +++ b/ocsp.go @@ -235,6 +235,10 @@ func getOCSPForCert(ocspConfig OCSPConfig, bundle []byte) ([]byte, *ocsp.Respons return nil, nil, fmt.Errorf("parsing OCSP response: %v", err) } + if err := validateOCSPResponder(ocspRes, issuerCert); err != nil { + return nil, nil, fmt.Errorf("OCSP responder authorization check failed: %v", err) + } + return ocspResBytes, ocspRes, nil } @@ -253,3 +257,26 @@ func freshOCSP(resp *ocsp.Response) bool { refreshTime := resp.ThisUpdate.Add(nextUpdate.Sub(resp.ThisUpdate) / 2) return time.Now().Before(refreshTime) } + +// validateOCSPResponder enforces RFC 6960 §4.2.2.2: "Systems or applications that +// rely on OCSP responses MUST be capable of detecting and enforcing the use of the +// id-kp-OCSPSigning value." An issuer-signed response (where the embedded Certificate +// field is nil, meaning the issuer signed directly) is always acceptable. +func validateOCSPResponder(ocspResp *ocsp.Response, issuerCert *x509.Certificate) error { + respCert := ocspResp.Certificate + + // if response was signed directly by the issuer, or embedded responder cert IS the issuer, accept + if respCert == nil || respCert.Equal(issuerCert) { + // Response was signed directly by the issuer — always valid. + return nil + } + + // RFC 6960 §4.2.2.2 requires id-kp-OCSPSigning for delegated responders + for _, eku := range respCert.ExtKeyUsage { + if eku == x509.ExtKeyUsageOCSPSigning { + return nil + } + } + + return fmt.Errorf("OCSP responder certificate (subject: %s) is not the issuer and does not carry id-kp-OCSPSigning", respCert.Subject) +} diff --git a/ocsp_test.go b/ocsp_test.go index 4c3df27e..8f832da7 100644 --- a/ocsp_test.go +++ b/ocsp_test.go @@ -4,10 +4,13 @@ import ( "bytes" "context" "crypto" + "crypto/x509" + "crypto/x509/pkix" "errors" "io" "net/http" "net/http/httptest" + "strings" "testing" "golang.org/x/crypto/ocsp" @@ -153,6 +156,69 @@ func TestStapleOCSP(t *testing.T) { }) } +func TestValidateOCSPResponder(t *testing.T) { + issuer := mustMakeCertificate(t, caCert, caKey).Leaf + + tests := []struct { + name string + resp *ocsp.Response + wantErr string + }{ + { + name: "issuer signed response with no embedded cert", + resp: &ocsp.Response{Certificate: nil}, + }, + { + name: "embedded responder cert is issuer cert", + resp: &ocsp.Response{Certificate: issuer}, + }, + { + name: "delegated responder with OCSP signing eku", + resp: &ocsp.Response{Certificate: &x509.Certificate{ + Subject: pkix.Name{CommonName: "Delegated OCSP Responder"}, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + x509.ExtKeyUsageOCSPSigning, + }, + }}, + }, + { + name: "delegated responder without OCSP signing eku", + resp: &ocsp.Response{Certificate: &x509.Certificate{ + Subject: pkix.Name{CommonName: "Not Authorized"}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + }}, + wantErr: "does not carry id-kp-OCSPSigning", + }, + { + name: "delegated responder with empty eku", + resp: &ocsp.Response{Certificate: &x509.Certificate{ + Subject: pkix.Name{CommonName: "No EKU"}, + }}, + wantErr: "does not carry id-kp-OCSPSigning", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := validateOCSPResponder(tc.resp, issuer) + if tc.wantErr == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + return + } + + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.wantErr) + } + if !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("expected error containing %q, got %q", tc.wantErr, err.Error()) + } + }) + } +} + func mustMakeCertificate(t *testing.T, cert, key string) Certificate { t.Helper() c, err := makeCertificate([]byte(cert), []byte(key))