acmez
acmez copied to clipboard
When I try to request *.example.com and example.com, it will pending
client := acmez.Client{
Client: &acme.Client{
Directory: constants.DefaultCA,
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeDNS01: &acme_issuer.DNS01Solver{
PropagationTimeout: time.Minute * 15,
PollingInterval: time.Second * 10,
TTL: constants.DefaultDnsRecordTTL,
DNSProvider: dnsProvider,
},
},
}
domains:=[]string{"*.example.com","example.com"}
log.Println("-----")
log.Printf("%+v\n", domains)
certs, err := client.ObtainCertificate(context.Background(), account.AcmeAccount, certPrivateKey, domains)
if err != nil {
log.Println(err)
return nil, fmt.Errorf("obtaining certificate: %v", err)
}
log.Println("====")
it use one dns provider, cloudflare.
when i try one domain, it's works fine
domains:=[]string{"*.example.com"}
or domains:=[]string{"example.com"}
log
2022/07/23 14:44:12 -----
2022/07/23 14:44:12 [*.example.com example.com]
timeout is 15m, but still no any response in 30m.
{"level":"info","ts":1658569561.968958,"caller":"[email protected]/client.go:394","msg":"trying to solve challenge","identifier":"*.example.com","challenge_type":"dns-01","ca":"https://acme.zerossl.com/v2/DV90"}
{"level":"info","ts":1658569564.0227418,"caller":"[email protected]/client.go:394","msg":"trying to solve challenge","identifier":"example.com","challenge_type":"dns-01","ca":"https://acme.zerossl.com/v2/DV90"}
after this log, it's pending a few hours.
I'm not sure I understand. Can you provide the full log output? (not just from your own program)
Be sure to set the Logger
field of the acme.Client
struct.
package main
import (
"awesomeProject/acme_issuer"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"github.com/libdns/cloudflare"
"github.com/mholt/acmez"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
"log"
"time"
)
func main() {
domains := []string{"example.com", "*.example.com"}
dnsProvider := &cloudflare.Provider{
APIToken: "Key",
}
logger, err := zap.NewProduction()
if err != nil {
log.Fatalln(err)
}
defer logger.Sync() // flushes buffer, if any
client := acmez.Client{
Logger: logger,
Client: &acme.Client{
Logger: logger,
Directory: "https://acme.zerossl.com/v2/DV90",
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeDNS01: &acme_issuer.DNS01Solver{
PropagationTimeout: time.Minute * 15,
PollingInterval: time.Second * 10,
TTL: 600,
DNSProvider: dnsProvider,
},
},
}
// Before you can get a cert, you'll need an account registered with
// the ACME CA; it needs a private key which should obviously be
// different from any key used for certificates!
accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatalln(err)
}
ctx := context.Background()
account := acme.Account{
Contact: []string{"mailto:[email protected]"},
TermsOfServiceAgreed: true,
PrivateKey: accountPrivateKey,
}
err = account.SetExternalAccountBinding(ctx, client.Client, acme.EAB{
KeyID: `ID`,
MACKey: `Key`,
})
if err != nil {
log.Fatalln(err)
}
// If the account is new, we need to create it; only do this once!
// then be sure to securely store the account key and metadata so
// you can reuse it later!
account, err = client.NewAccount(ctx, account)
if err != nil {
log.Fatalln(fmt.Sprintf("new account: %v", err))
}
// Every certificate needs a key.
certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatalln(fmt.Sprintf("generating certificate key: %v", err))
}
fmt.Println("ObtainCertificate...")
certs, err := client.ObtainCertificate(context.Background(), account, certPrivateKey, domains)
if err != nil {
log.Fatalln(fmt.Sprintf("obtaining certificate: %v", err))
}
for _, cert := range certs {
log.Println(cert)
}
}
Log:
ObtainCertificate...
{"level":"info","ts":1658913829.171802,"caller":"[email protected]/client.go:394","msg":"trying to solve challenge","identifier":"example.com","challenge_type":"dns-01","ca":"https://acme.zerossl.com/v2/DV90"}
{"level":"info","ts":1658913831.3343675,"caller":"[email protected]/client.go:394","msg":"trying to solve challenge","identifier":"*.example.com","challenge_type":"dns-01","ca":"https://acme.zerossl.com/v2/DV90"}
no more any log.
If I use domains := []string{"*.example.com"}
Log:
ObtainCertificate...
{"level":"info","ts":1658915211.612598,"caller":"[email protected]/client.go:394","msg":"trying to solve challenge","identifier":"*.example.com","challenge_type":"dns-01","ca":"https://acme.zerossl.com/v2/DV90"}
{"level":"info","ts":1658915264.9595623,"caller":"[email protected]/client.go:164","msg":"validations succeeded; finalizing order","order":"https://acme.zerossl.com/v2/DV90/order/AYzAMUuxKdymhNRtrvTYw"}
{"level":"info","ts":1658915317.5602314,"caller":"[email protected]/client.go:184","msg":"successfully downloaded available certificate chains","count":1,"first_url":"https://acme.zerossl.com/v2/DV90/cert/qtPr6rDKyKTLlkQUPTgsA"}
If I use domains := []string{"example.com","aaa.example.com"}
, also okay
Log
ObtainCertificate...
{"level":"info","ts":1658915614.185599,"caller":"[email protected]/client.go:394","msg":"trying to solve challenge","identifier":"example.com","challenge_type":"dns-01","ca":"https://acme.zerossl.com/v2/DV90"}
{"level":"info","ts":1658915616.3788662,"caller":"[email protected]/client.go:394","msg":"trying to solve challenge","identifier":"aaa.example.com","challenge_type":"dns-01","ca":"https://acme.zerossl.com/v2/DV90"}
{"level":"info","ts":1658915712.5194244,"caller":"[email protected]/client.go:164","msg":"validations succeeded; finalizing order","order":"https://acme.zerossl.com/v2/DV90/order/2q60LQpdNjoHlef5ZfGvA"}
{"level":"info","ts":1658915760.238464,"caller":"[email protected]/client.go:184","msg":"successfully downloaded available certificate chains","count":1,"first_url":"https://acme.zerossl.com/v2/DV90/cert/qoX3XjHX9dqYymkTI7q9Q"}
issuer.go
is copy from certmagic
package acme_issuer
import (
"context"
"fmt"
"github.com/libdns/libdns"
"github.com/mholt/acmez/acme"
"sync"
"time"
)
// DNS01Solver is a type that makes libdns providers usable
// as ACME dns-01 challenge solvers.
// See https://github.com/libdns/libdns
type DNS01Solver struct {
// The implementation that interacts with the DNS
// provider to set or delete records. (REQUIRED)
DNSProvider ACMEDNSProvider
// The TTL for the temporary challenge records.
TTL time.Duration
// How long to wait before starting propagation checks.
// Default: 0 (no wait).
PropagationDelay time.Duration
// Maximum time to wait for temporary DNS record to appear.
// Set to -1 to disable propagation checks.
// Default: 2 minutes.
PropagationTimeout time.Duration
// PollingInterval.
// Default: 2 seconds.
PollingInterval time.Duration
// Preferred DNS resolver(s) to use when doing DNS lookups.
Resolvers []string
// Override the domain to set the TXT record on. This is
// to delegate the challenge to a different domain. Note
// that the solver doesn't follow CNAME/NS record.
OverrideDomain string
txtRecords map[string]dnsPresentMemory // keyed by domain name
txtRecordsMu sync.Mutex
}
// Present creates the DNS TXT record for the given ACME challenge.
func (s *DNS01Solver) Present(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// multiple identifiers can have the same ACME challenge
// domain (e.g. example.com and *.example.com) so we need
// to ensure that we don't solve those concurrently and
// step on each challenges' metaphorical toes; see
// https://github.com/caddyserver/caddy/issues/3474
activeDNSChallenges.Lock(dnsName)
zone, err := findZoneByFQDN(dnsName, recursiveNameservers(s.Resolvers))
if err != nil {
return fmt.Errorf("could not determine zone for domain %q: %v", dnsName, err)
}
rec := libdns.Record{
Type: "TXT",
Name: libdns.RelativeName(dnsName+".", zone),
Value: keyAuth,
TTL: s.TTL,
}
results, err := s.DNSProvider.AppendRecords(ctx, zone, []libdns.Record{rec})
if err != nil {
return fmt.Errorf("adding temporary record for zone %s: %w", zone, err)
}
if len(results) != 1 {
return fmt.Errorf("expected one record, got %d: %v", len(results), results)
}
// remember the record and zone we got so we can clean up more efficiently
s.txtRecordsMu.Lock()
if s.txtRecords == nil {
s.txtRecords = make(map[string]dnsPresentMemory)
}
s.txtRecords[dnsName] = dnsPresentMemory{dnsZone: zone, rec: results[0]}
s.txtRecordsMu.Unlock()
return nil
}
// Wait blocks until the TXT record created in Present() appears in
// authoritative lookups, i.e. until it has propagated, or until
// timeout, whichever is first.
func (s *DNS01Solver) Wait(ctx context.Context, challenge acme.Challenge) error {
// if configured to, pause before doing propagation checks
// (even if they are disabled, the wait might be desirable on its own)
if s.PropagationDelay > 0 {
select {
case <-time.After(s.PropagationDelay):
case <-ctx.Done():
return ctx.Err()
}
}
// skip propagation checks if configured to do so
if s.PropagationTimeout == -1 {
return nil
}
// prepare for the checks by determining what to look for
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// timings
timeout := s.PropagationTimeout
if timeout == 0 {
timeout = 2 * time.Minute
}
interval := s.PollingInterval
if interval == 0 {
interval = 2 * time.Second
}
// how we'll do the checks
resolvers := recursiveNameservers(s.Resolvers)
var err error
start := time.Now()
for time.Since(start) < timeout {
select {
case <-time.After(interval):
case <-ctx.Done():
return ctx.Err()
}
var ready bool
ready, err = checkDNSPropagation(dnsName, keyAuth, resolvers)
if err != nil {
return fmt.Errorf("checking DNS propagation of %s: %w", dnsName, err)
}
if ready {
return nil
}
}
return fmt.Errorf("timed out waiting for record to fully propagate")
}
// CleanUp deletes the DNS TXT record created in Present().
func (s *DNS01Solver) CleanUp(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
defer func() {
// always forget about it so we don't leak memory
s.txtRecordsMu.Lock()
delete(s.txtRecords, dnsName)
s.txtRecordsMu.Unlock()
// always do this last - but always do it!
activeDNSChallenges.Unlock(dnsName)
}()
// recall the record we created and zone we looked up
s.txtRecordsMu.Lock()
memory, ok := s.txtRecords[dnsName]
if !ok {
s.txtRecordsMu.Unlock()
return fmt.Errorf("no memory of presenting a DNS record for %s (probably OK if presenting failed)", challenge.Identifier.Value)
}
s.txtRecordsMu.Unlock()
// clean up the record
_, err := s.DNSProvider.DeleteRecords(ctx, memory.dnsZone, []libdns.Record{memory.rec})
if err != nil {
return fmt.Errorf("deleting temporary record for zone %s: %w", memory.dnsZone, err)
}
return nil
}
type dnsPresentMemory struct {
dnsZone string
rec libdns.Record
}
// ACMEDNSProvider defines the set of operations required for
// ACME challenges. A DNS provider must be able to append and
// delete records in order to solve ACME challenges. Find one
// you can use at https://github.com/libdns. If your provider
// isn't implemented yet, feel free to contribute!
type ACMEDNSProvider interface {
libdns.RecordAppender
libdns.RecordDeleter
}
// activeDNSChallenges synchronizes DNS challenges for
// names to ensure that challenges for the same ACME
// DNS name do not overlap; for example, the TXT record
// to make for both example.com and *.example.com are
// the same; thus we cannot solve them concurrently.
var activeDNSChallenges = newMapMutex()
// mapMutex implements named mutexes.
type mapMutex struct {
cond *sync.Cond
set map[interface{}]struct{}
}
func newMapMutex() *mapMutex {
return &mapMutex{
cond: sync.NewCond(new(sync.Mutex)),
set: make(map[interface{}]struct{}),
}
}
func (mmu *mapMutex) Lock(key interface{}) {
mmu.cond.L.Lock()
defer mmu.cond.L.Unlock()
for mmu.locked(key) {
mmu.cond.Wait()
}
mmu.set[key] = struct{}{}
}
func (mmu *mapMutex) Unlock(key interface{}) {
mmu.cond.L.Lock()
defer mmu.cond.L.Unlock()
delete(mmu.set, key)
mmu.cond.Broadcast()
}
func (mmu *mapMutex) locked(key interface{}) (ok bool) {
_, ok = mmu.set[key]
return
}
Thanks for the additional information!
I see you're using ZeroSSL. They are working a known issue that involves slow response times.
If you try the []string{"*.example.com","example.com"}
setup (the original one) the same way but with Let's Encrypt, does it work okay?
I'm trying to determine if it's a bug in acmez or in ZeroSSL.
Change Directory to https://acme-v02.api.letsencrypt.org/directory
, still pending
ObtainCertificate...
{"level":"info","ts":1658977033.7748215,"caller":"[email protected]/client.go:394","msg":"trying to solve challenge","identifier":"*.example.com","challenge_type":"dns-01","ca":"https://acme-v02.api.letsencrypt.org/directory"}
{"level":"info","ts":1658977035.9860744,"caller":"[email protected]/client.go:394","msg":"trying to solve challenge","identifier":"example.com","challenge_type":"dns-01","ca":"https://acme-v02.api.letsencrypt.org/directory"}
btw, I use acme.sh to obtain certificate from ZeroSSL for *.example.com,example.com
, works fine. So, maybe isn't ZeroSSL bug.
Thanks, good to know. Will look into this.
I've been able to reproduce this behavior, now looking into a fix.
What is your acme_issuer.DNS01Solver
? And what is your DNS provider? Would like to see that code.
I have confirmed this is not a bug in acmez but rather the interop with whatever the acme_issuer.DNS01Solver
is.
This bug only occurs if using an implementation of a DNS solver like what CertMagic did, where it mutexes challenges for each ACME DNS name, to avoid collisions with *.example.com
and example.com
, which use the same-named TXT record to solve the DNS challenge.
It was done because legacy code required it, but we no longer use those libraries (lego and its solvers) so I removed the mutexing. This removes the deadlock you're experiencing (assuming you're using CertMagic; I don't know what acme_issuer
is though since you didn't share full code) and makes it faster and more efficient as well.
Fix is downstream here: https://github.com/caddyserver/certmagic/commit/dce2de273db7226cadfae948c6d70ddf633553e7