Initial implementation of ZeroSSL API issuer (#279)

* Initial implementation of ZeroSSL API issuer

Still needs CA support for CommonName-less certs

* Accommodate ZeroSSL CSR requirements; fix DNS prop check

* Fix README example

* Fix comment
This commit is contained in:
Matt Holt 2024-04-08 12:59:55 -04:00 committed by GitHub
parent c61a4feb39
commit 6095ab8069
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 623 additions and 181 deletions

View File

@ -60,13 +60,16 @@ CertMagic - Automatic HTTPS using Let's Encrypt
- [Advanced use](#advanced-use)
- [Wildcard Certificates](#wildcard-certificates)
- [Behind a load balancer (or in a cluster)](#behind-a-load-balancer-or-in-a-cluster)
- [The ACME Challenges](#the-acme-challenges)
- [HTTP Challenge](#http-challenge)
- [TLS-ALPN Challenge](#tls-alpn-challenge)
- [DNS Challenge](#dns-challenge)
- [On-Demand TLS](#on-demand-tls)
- [Storage](#storage)
- [Cache](#cache)
- [The ACME Challenges](#the-acme-challenges)
- [HTTP Challenge](#http-challenge)
- [TLS-ALPN Challenge](#tls-alpn-challenge)
- [DNS Challenge](#dns-challenge)
- [On-Demand TLS](#on-demand-tls)
- [Storage](#storage)
- [Cache](#cache)
- [Events](#events)
- [ZeroSSL](#zerossl)
- [FAQ](#faq)
- [Contributing](#contributing)
- [Project History](#project-history)
- [Credits and License](#credits-and-license)
@ -402,8 +405,10 @@ To enable it, just set the `DNS01Solver` field on a `certmagic.ACMEIssuer` struc
import "github.com/libdns/cloudflare"
certmagic.DefaultACME.DNS01Solver = &certmagic.DNS01Solver{
DNSProvider: &cloudflare.Provider{
APIToken: "topsecret",
DNSManager: certmagic.DNSManager{
DNSProvider: &cloudflare.Provider{
APIToken: "topsecret",
},
},
}
```
@ -505,6 +510,26 @@ CertMagic emits events when possible things of interest happen. Set the [`OnEven
`OnEvent` can return an error. Some events may be aborted by returning an error. For example, returning an error from `cert_obtained` can cancel obtaining the certificate. Only return an error from `OnEvent` if you want to abort program flow.
## ZeroSSL
ZeroSSL has both ACME and HTTP API services for getting certificates. CertMagic works with both of them.
To use ZeroSSL's ACME server, configure CertMagic with an [`ACMEIssuer`](https://pkg.go.dev/github.com/caddyserver/certmagic#ACMEIssuer) like you would with any other ACME CA (just adjust the directory URL). External Account Binding (EAB) is required for ZeroSSL. You can use the [ZeroSSL API](https://pkg.go.dev/github.com/caddyserver/zerossl) to generate one, or your account dashboard.
To use ZeroSSL's API instead, use the [`ZeroSSLIssuer`](https://pkg.go.dev/github.com/caddyserver/certmagic#ZeroSSLIssuer). Here is a simple example:
```go
magic := certmagic.NewDefault()
magic.Issuers = []certmagic.Issuer{
certmagic.NewZeroSSLIssuer(magic, certmagic.ZeroSSLIssuer{
APIKey: "<your ZeroSSL API key>",
}),
}
err := magic.ManageSync(ctx, []string{"example.com"})
```
## FAQ
### Can I use some of my own certificates while using CertMagic?

View File

@ -20,6 +20,7 @@ import (
"fmt"
weakrand "math/rand"
"net"
"net/http"
"net/url"
"strconv"
"strings"
@ -186,38 +187,24 @@ func (iss *ACMEIssuer) newACMEClient(useTestCA bool) (*acmez.Client, error) {
if iss.DNS01Solver == nil {
// enable HTTP-01 challenge
if !iss.DisableHTTPChallenge {
useHTTPPort := HTTPChallengePort
if HTTPPort > 0 && HTTPPort != HTTPChallengePort {
useHTTPPort = HTTPPort
}
if iss.AltHTTPPort > 0 {
useHTTPPort = iss.AltHTTPPort
}
client.ChallengeSolvers[acme.ChallengeTypeHTTP01] = distributedSolver{
storage: iss.config.Storage,
storageKeyIssuerPrefix: iss.storageKeyCAPrefix(client.Directory),
solver: &httpSolver{
acmeIssuer: iss,
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(useHTTPPort)),
handler: iss.HTTPChallengeHandler(http.NewServeMux()),
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(iss.getHTTPPort())),
},
}
}
// enable TLS-ALPN-01 challenge
if !iss.DisableTLSALPNChallenge {
useTLSALPNPort := TLSALPNChallengePort
if HTTPSPort > 0 && HTTPSPort != TLSALPNChallengePort {
useTLSALPNPort = HTTPSPort
}
if iss.AltTLSALPNPort > 0 {
useTLSALPNPort = iss.AltTLSALPNPort
}
client.ChallengeSolvers[acme.ChallengeTypeTLSALPN01] = distributedSolver{
storage: iss.config.Storage,
storageKeyIssuerPrefix: iss.storageKeyCAPrefix(client.Directory),
solver: &tlsALPNSolver{
config: iss.config,
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(useTLSALPNPort)),
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(iss.getTLSALPNPort())),
},
}
}
@ -248,6 +235,28 @@ func (iss *ACMEIssuer) newACMEClient(useTestCA bool) (*acmez.Client, error) {
return client, nil
}
func (iss *ACMEIssuer) getHTTPPort() int {
useHTTPPort := HTTPChallengePort
if HTTPPort > 0 && HTTPPort != HTTPChallengePort {
useHTTPPort = HTTPPort
}
if iss.AltHTTPPort > 0 {
useHTTPPort = iss.AltHTTPPort
}
return useHTTPPort
}
func (iss *ACMEIssuer) getTLSALPNPort() int {
useTLSALPNPort := TLSALPNChallengePort
if HTTPSPort > 0 && HTTPSPort != TLSALPNChallengePort {
useTLSALPNPort = HTTPSPort
}
if iss.AltTLSALPNPort > 0 {
useTLSALPNPort = iss.AltTLSALPNPort
}
return useTLSALPNPort
}
func (c *acmeClient) throttle(ctx context.Context, names []string) error {
email := c.iss.getEmail()

View File

@ -1,3 +1,17 @@
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (

View File

@ -162,7 +162,7 @@ func (cfg *Config) loadManagedCertificate(ctx context.Context, domain string) (C
//
// This method is safe for concurrent use.
func (cfg *Config) CacheUnmanagedCertificatePEMFile(ctx context.Context, certFile, keyFile string, tags []string) (string, error) {
cert, err := cfg.makeCertificateFromDiskWithOCSP(ctx, cfg.Storage, certFile, keyFile)
cert, err := cfg.makeCertificateFromDiskWithOCSP(ctx, certFile, keyFile)
if err != nil {
return "", err
}
@ -224,7 +224,7 @@ func (cfg *Config) CacheUnmanagedCertificatePEMBytes(ctx context.Context, certBy
// certificate and key files. It fills out all the fields in
// the certificate except for the Managed and OnDemand flags.
// (It is up to the caller to set those.) It staples OCSP.
func (cfg Config) makeCertificateFromDiskWithOCSP(ctx context.Context, storage Storage, certFile, keyFile string) (Certificate, error) {
func (cfg Config) makeCertificateFromDiskWithOCSP(ctx context.Context, certFile, keyFile string) (Certificate, error) {
certPEMBlock, err := os.ReadFile(certFile)
if err != nil {
return Certificate{}, err

View File

@ -562,7 +562,7 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
}
}
csr, err := cfg.generateCSR(privKey, []string{name})
csr, err := cfg.generateCSR(privKey, []string{name}, false)
if err != nil {
return err
}
@ -584,7 +584,19 @@ func (cfg *Config) obtainCert(ctx context.Context, name string, interactive bool
}
}
issuedCert, err = issuer.Issue(ctx, csr)
// TODO: ZeroSSL's API currently requires CommonName to be set, and requires it be
// distinct from SANs. If this was a cert it would violate the BRs, but their certs
// are compliant, so their CSR requirements just needlessly add friction, complexity,
// and inefficiency for clients. CommonName has been deprecated for 25+ years.
useCSR := csr
if _, ok := issuer.(*ZeroSSLIssuer); ok {
useCSR, err = cfg.generateCSR(privKey, []string{name}, true)
if err != nil {
return err
}
}
issuedCert, err = issuer.Issue(ctx, useCSR)
if err == nil {
issuerUsed = issuer
break
@ -808,7 +820,7 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
}
}
csr, err := cfg.generateCSR(privateKey, []string{name})
csr, err := cfg.generateCSR(privateKey, []string{name}, false)
if err != nil {
return err
}
@ -818,6 +830,18 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
var issuerUsed Issuer
var issuerKeys []string
for _, issuer := range cfg.Issuers {
// TODO: ZeroSSL's API currently requires CommonName to be set, and requires it be
// distinct from SANs. If this was a cert it would violate the BRs, but their certs
// are compliant, so their CSR requirements just needlessly add friction, complexity,
// and inefficiency for clients. CommonName has been deprecated for 25+ years.
useCSR := csr
if _, ok := issuer.(*ZeroSSLIssuer); ok {
useCSR, err = cfg.generateCSR(privateKey, []string{name}, true)
if err != nil {
return err
}
}
issuerKeys = append(issuerKeys, issuer.IssuerKey())
if prechecker, ok := issuer.(PreChecker); ok {
err = prechecker.PreCheck(ctx, []string{name}, interactive)
@ -826,7 +850,7 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
}
}
issuedCert, err = issuer.Issue(ctx, csr)
issuedCert, err = issuer.Issue(ctx, useCSR)
if err == nil {
issuerUsed = issuer
break
@ -898,10 +922,16 @@ func (cfg *Config) renewCert(ctx context.Context, name string, force, interactiv
return err
}
func (cfg *Config) generateCSR(privateKey crypto.PrivateKey, sans []string) (*x509.CertificateRequest, error) {
// generateCSR generates a CSR for the given SANs. If useCN is true, CommonName will get the first SAN (TODO: this is only a temporary hack for ZeroSSL API support).
func (cfg *Config) generateCSR(privateKey crypto.PrivateKey, sans []string, useCN bool) (*x509.CertificateRequest, error) {
csrTemplate := new(x509.CertificateRequest)
for _, name := range sans {
// TODO: This is a temporary hack to support ZeroSSL API...
if useCN && csrTemplate.Subject.CommonName == "" && len(name) <= 64 {
csrTemplate.Subject.CommonName = name
continue
}
if ip := net.ParseIP(name); ip != nil {
csrTemplate.IPAddresses = append(csrTemplate.IPAddresses, ip)
} else if strings.Contains(name, "@") {

View File

@ -280,6 +280,11 @@ func hashCertificateChain(certChain [][]byte) string {
func namesFromCSR(csr *x509.CertificateRequest) []string {
var nameSet []string
// TODO: CommonName should not be used (it has been deprecated for 25+ years,
// but Sectigo CA still requires it to be filled out and not overlap SANs...)
if csr.Subject.CommonName != "" {
nameSet = append(nameSet, csr.Subject.CommonName)
}
nameSet = append(nameSet, csr.DNSNames...)
nameSet = append(nameSet, csr.EmailAddresses...)
for _, v := range csr.IPAddresses {

View File

@ -210,42 +210,43 @@ func populateNameserverPorts(servers []string) {
}
}
// checkDNSPropagation checks if the expected TXT record has been propagated.
// If checkAuthoritativeServers is true, the authoritative nameservers are checked directly,
// otherwise only the given resolvers are checked.
func checkDNSPropagation(fqdn, value string, resolvers []string, checkAuthoritativeServers bool) (bool, error) {
// checkDNSPropagation checks if the expected record has been propagated to all authoritative nameservers.
func checkDNSPropagation(fqdn string, recType uint16, expectedValue string, checkAuthoritativeServers bool, resolvers []string) (bool, error) {
if !strings.HasSuffix(fqdn, ".") {
fqdn += "."
}
// Initial attempt to resolve at the recursive NS
r, err := dnsQuery(fqdn, dns.TypeTXT, resolvers, true)
if err != nil {
return false, err
}
if r.Rcode == dns.RcodeSuccess {
fqdn = updateDomainWithCName(r, fqdn)
// Initial attempt to resolve at the recursive NS - but do not actually
// dereference (follow) a CNAME record if we are targeting a CNAME record
// itself
if recType != dns.TypeCNAME {
r, err := dnsQuery(fqdn, recType, resolvers, true)
if err != nil {
return false, fmt.Errorf("CNAME dns query: %v", err)
}
if r.Rcode == dns.RcodeSuccess {
fqdn = updateDomainWithCName(r, fqdn)
}
}
if checkAuthoritativeServers {
authoritativeServers, err := lookupNameservers(fqdn, resolvers)
if err != nil {
return false, err
return false, fmt.Errorf("looking up authoritative nameservers: %v", err)
}
populateNameserverPorts(authoritativeServers)
resolvers = authoritativeServers
}
return checkNameservers(fqdn, value, resolvers)
return checkAuthoritativeNss(fqdn, recType, expectedValue, resolvers)
}
// checkNameservers checks if any of the given nameservers has the expected TXT record.
func checkNameservers(fqdn, value string, nameservers []string) (bool, error) {
// checkAuthoritativeNss queries each of the given nameservers for the expected record.
func checkAuthoritativeNss(fqdn string, recType uint16, expectedValue string, nameservers []string) (bool, error) {
for _, ns := range nameservers {
r, err := dnsQuery(fqdn, dns.TypeTXT, []string{ns}, true)
r, err := dnsQuery(fqdn, recType, []string{ns}, true)
if err != nil {
return false, err
return false, fmt.Errorf("querying authoritative nameservers: %v", err)
}
if r.Rcode != dns.RcodeSuccess {
@ -259,11 +260,23 @@ func checkNameservers(fqdn, value string, nameservers []string) (bool, error) {
}
for _, rr := range r.Answer {
if txt, ok := rr.(*dns.TXT); ok {
record := strings.Join(txt.Txt, "")
if record == value {
return true, nil
switch recType {
case dns.TypeTXT:
if txt, ok := rr.(*dns.TXT); ok {
record := strings.Join(txt.Txt, "")
if record == expectedValue {
return true, nil
}
}
case dns.TypeCNAME:
if cname, ok := rr.(*dns.CNAME); ok {
// TODO: whether a DNS provider assumes a trailing dot or not varies, and we may have to standardize this in libdns packages
if strings.TrimSuffix(cname.Target, ".") == strings.TrimSuffix(expectedValue, ".") {
return true, nil
}
}
default:
return false, fmt.Errorf("unsupported record type: %d", recType)
}
}
}
@ -277,12 +290,12 @@ func lookupNameservers(fqdn string, resolvers []string) ([]string, error) {
zone, err := findZoneByFQDN(fqdn, resolvers)
if err != nil {
return nil, fmt.Errorf("could not determine the zone: %w", err)
return nil, fmt.Errorf("could not determine the zone for '%s': %w", fqdn, err)
}
r, err := dnsQuery(zone, dns.TypeNS, resolvers, true)
if err != nil {
return nil, err
return nil, fmt.Errorf("querying NS resolver for zone '%s' recursively: %v", zone, err)
}
for _, rr := range r.Answer {

22
go.mod
View File

@ -1,23 +1,23 @@
module github.com/caddyserver/certmagic
go 1.19
go 1.22.0
require (
github.com/klauspost/cpuid/v2 v2.2.5
github.com/libdns/libdns v0.2.1
github.com/caddyserver/zerossl v0.1.1
github.com/klauspost/cpuid/v2 v2.2.7
github.com/libdns/libdns v0.2.2
github.com/mholt/acmez v1.2.0
github.com/miekg/dns v1.1.55
github.com/miekg/dns v1.1.58
github.com/zeebo/blake3 v0.2.3
go.uber.org/zap v1.24.0
golang.org/x/crypto v0.17.0
golang.org/x/net v0.17.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.21.0
golang.org/x/net v0.22.0
)
require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.10.0 // indirect
golang.org/x/tools v0.17.0 // indirect
)

54
go.sum
View File

@ -1,42 +1,46 @@
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/caddyserver/zerossl v0.1.1 h1:yQL7QXZnEb/ddH6JsNPGBANETUMHPFlAV5+a+Epxgbo=
github.com/caddyserver/zerossl v0.1.1/go.mod h1:wtiJEHbdvunr40ZzhXlnIkOB8Xj4eKtBKizCcZitJiQ=
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/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -46,9 +46,9 @@ import (
// can access the keyAuth material is by loading it
// from storage, which is done by distributedSolver.
type httpSolver struct {
closed int32 // accessed atomically
acmeIssuer *ACMEIssuer
address string
closed int32 // accessed atomically
handler http.Handler
address string
}
// Present starts an HTTP server if none is already listening on s.address.
@ -88,7 +88,7 @@ func (s *httpSolver) serve(ctx context.Context, si *solverInfo) {
}()
defer close(si.done)
httpServer := &http.Server{
Handler: s.acmeIssuer.HTTPChallengeHandler(http.NewServeMux()),
Handler: s.handler,
BaseContext: func(listener net.Listener) context.Context { return ctx },
}
httpServer.SetKeepAlivesEnabled(false)
@ -250,9 +250,92 @@ func (s *tlsALPNSolver) CleanUp(_ context.Context, chal acme.Challenge) error {
// DNS provider APIs and implementations of the libdns interfaces must also
// support multiple same-named TXT records.
type DNS01Solver struct {
DNSManager
}
// 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()
zrec, err := s.DNSManager.createRecord(ctx, dnsName, "TXT", keyAuth)
if err != nil {
return err
}
// remember the record and zone we got so we can clean up more efficiently
s.saveDNSPresentMemory(dnsPresentMemory{
dnsName: dnsName,
zoneRec: zrec,
})
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 {
// prepare for the checks by determining what to look for
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// wait for the record to propagate
memory, err := s.getDNSPresentMemory(dnsName, "TXT", keyAuth)
if err != nil {
return err
}
return s.DNSManager.wait(ctx, memory.zoneRec)
}
// CleanUp deletes the DNS TXT record created in Present().
//
// We ignore the context because cleanup is often/likely performed after
// a context cancellation, and properly-implemented DNS providers should
// honor cancellation, which would result in cleanup being aborted.
// Cleanup must always occur.
func (s *DNS01Solver) CleanUp(ctx context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// always forget about the record so we don't leak memory
defer s.deleteDNSPresentMemory(dnsName, keyAuth)
// recall the record we created and zone we looked up
memory, err := s.getDNSPresentMemory(dnsName, "TXT", keyAuth)
if err != nil {
return err
}
if err := s.DNSManager.cleanUpRecord(ctx, memory.zoneRec); err != nil {
return err
}
return nil
}
// DNSManager is a type that makes libdns providers usable for performing
// DNS verification. See https://github.com/libdns/libdns
//
// Note that records may be manipulated concurrently by some clients (such as
// acmez, which CertMagic uses), meaning that multiple records may be created
// in a DNS zone simultaneously, and in some cases distinct records of the same
// type may have the same name. For example, solving ACME challenges for both example.com
// and *.example.com create a TXT record named _acme_challenge.example.com,
// but with different tokens as their values. This solver distinguishes between
// different records with the same type and name by looking at their values.
type DNSManager struct {
// The implementation that interacts with the DNS
// provider to set or delete records. (REQUIRED)
DNSProvider ACMEDNSProvider
DNSProvider DNSProvider
// The TTL for the temporary challenge records.
TTL time.Duration
@ -285,52 +368,37 @@ type DNS01Solver struct {
// the value of their TXT records, which should contain
// unique challenge tokens.
// See https://github.com/caddyserver/caddy/issues/3474.
txtRecords map[string][]dnsPresentMemory
txtRecordsMu sync.Mutex
records map[string][]dnsPresentMemory
recordsMu 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()
zone, err := findZoneByFQDN(dnsName, recursiveNameservers(s.Resolvers))
func (m *DNSManager) createRecord(ctx context.Context, dnsName, recordType, recordValue string) (zoneRecord, error) {
zone, err := findZoneByFQDN(dnsName, recursiveNameservers(m.Resolvers))
if err != nil {
return fmt.Errorf("could not determine zone for domain %q: %v", dnsName, err)
return zoneRecord{}, fmt.Errorf("could not determine zone for domain %q: %v", dnsName, err)
}
rec := libdns.Record{
Type: "TXT",
Type: recordType,
Name: libdns.RelativeName(dnsName+".", zone),
Value: keyAuth,
TTL: s.TTL,
Value: recordValue,
TTL: m.TTL,
}
results, err := s.DNSProvider.AppendRecords(ctx, zone, []libdns.Record{rec})
results, err := m.DNSProvider.AppendRecords(ctx, zone, []libdns.Record{rec})
if err != nil {
return fmt.Errorf("adding temporary record for zone %q: %w", zone, err)
return zoneRecord{}, fmt.Errorf("adding temporary record for zone %q: %w", zone, err)
}
if len(results) != 1 {
return fmt.Errorf("expected one record, got %d: %v", len(results), results)
return zoneRecord{}, 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.saveDNSPresentMemory(dnsPresentMemory{
dnsZone: zone,
dnsName: dnsName,
rec: results[0],
})
return nil
return zoneRecord{zone, results[0]}, nil
}
// Wait blocks until the TXT record created in Present() appears in
// 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 {
func (s *DNSManager) wait(ctx context.Context, zrec zoneRecord) 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 {
@ -346,13 +414,6 @@ func (s *DNS01Solver) Wait(ctx context.Context, challenge acme.Challenge) error
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 {
@ -364,6 +425,13 @@ func (s *DNS01Solver) Wait(ctx context.Context, challenge acme.Challenge) error
checkAuthoritativeServers := len(s.Resolvers) == 0
resolvers := recursiveNameservers(s.Resolvers)
recType := dns.TypeTXT
if zrec.record.Type == "CNAME" {
recType = dns.TypeCNAME
}
absName := libdns.AbsoluteName(zrec.record.Name, zrec.zone)
var err error
start := time.Now()
for time.Since(start) < timeout {
@ -373,9 +441,9 @@ func (s *DNS01Solver) Wait(ctx context.Context, challenge acme.Challenge) error
return ctx.Err()
}
var ready bool
ready, err = checkDNSPropagation(dnsName, keyAuth, resolvers, checkAuthoritativeServers)
ready, err = checkDNSPropagation(absName, recType, zrec.record.Value, checkAuthoritativeServers, resolvers)
if err != nil {
return fmt.Errorf("checking DNS propagation of %q: %w", dnsName, err)
return fmt.Errorf("checking DNS propagation of %q (relative=%s zone=%s resolvers=%v): %w", absName, zrec.record.Name, zrec.zone, resolvers, err)
}
if ready {
return nil
@ -385,28 +453,18 @@ func (s *DNS01Solver) Wait(ctx context.Context, challenge acme.Challenge) error
return fmt.Errorf("timed out waiting for record to fully propagate; verify DNS provider configuration is correct - last error: %v", err)
}
type zoneRecord struct {
zone string
record libdns.Record
}
// CleanUp deletes the DNS TXT record created in Present().
//
// We ignore the context because cleanup is often/likely performed after
// a context cancellation, and properly-implemented DNS providers should
// honor cancellation, which would result in cleanup being aborted.
// Cleanup must always occur.
func (s *DNS01Solver) CleanUp(_ context.Context, challenge acme.Challenge) error {
dnsName := challenge.DNS01TXTRecordName()
if s.OverrideDomain != "" {
dnsName = s.OverrideDomain
}
keyAuth := challenge.DNS01KeyAuthorization()
// always forget about the record so we don't leak memory
defer s.deleteDNSPresentMemory(dnsName, keyAuth)
// recall the record we created and zone we looked up
memory, err := s.getDNSPresentMemory(dnsName, keyAuth)
if err != nil {
return err
}
func (s *DNSManager) cleanUpRecord(_ context.Context, zrec zoneRecord) error {
// clean up the record - use a different context though, since
// one common reason cleanup is performed is because a context
// was canceled, and if so, any HTTP requests by this provider
@ -418,68 +476,69 @@ func (s *DNS01Solver) CleanUp(_ context.Context, challenge acme.Challenge) error
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
_, err = s.DNSProvider.DeleteRecords(ctx, memory.dnsZone, []libdns.Record{memory.rec})
_, err := s.DNSProvider.DeleteRecords(ctx, zrec.zone, []libdns.Record{zrec.record})
if err != nil {
return fmt.Errorf("deleting temporary record for name %q in zone %q: %w", memory.dnsName, memory.dnsZone, err)
return fmt.Errorf("deleting temporary record for name %q in zone %q: %w", zrec.zone, zrec.record, err)
}
return nil
}
const defaultDNSPropagationTimeout = 2 * time.Minute
// dnsPresentMemory associates a created DNS record with its zone
// (since libdns Records are zone-relative and do not include zone).
type dnsPresentMemory struct {
dnsZone string
dnsName string
rec libdns.Record
zoneRec zoneRecord
}
func (s *DNS01Solver) saveDNSPresentMemory(mem dnsPresentMemory) {
s.txtRecordsMu.Lock()
if s.txtRecords == nil {
s.txtRecords = make(map[string][]dnsPresentMemory)
func (s *DNSManager) saveDNSPresentMemory(mem dnsPresentMemory) {
s.recordsMu.Lock()
if s.records == nil {
s.records = make(map[string][]dnsPresentMemory)
}
s.txtRecords[mem.dnsName] = append(s.txtRecords[mem.dnsName], mem)
s.txtRecordsMu.Unlock()
s.records[mem.dnsName] = append(s.records[mem.dnsName], mem)
s.recordsMu.Unlock()
}
func (s *DNS01Solver) getDNSPresentMemory(dnsName, keyAuth string) (dnsPresentMemory, error) {
s.txtRecordsMu.Lock()
defer s.txtRecordsMu.Unlock()
func (s *DNSManager) getDNSPresentMemory(dnsName, recType, value string) (dnsPresentMemory, error) {
s.recordsMu.Lock()
defer s.recordsMu.Unlock()
var memory dnsPresentMemory
for _, mem := range s.txtRecords[dnsName] {
if mem.rec.Value == keyAuth {
for _, mem := range s.records[dnsName] {
if mem.zoneRec.record.Type == recType && mem.zoneRec.record.Value == value {
memory = mem
break
}
}
if memory.rec.Name == "" {
if memory.zoneRec.record.Name == "" {
return dnsPresentMemory{}, fmt.Errorf("no memory of presenting a DNS record for %q (usually OK if presenting also failed)", dnsName)
}
return memory, nil
}
func (s *DNS01Solver) deleteDNSPresentMemory(dnsName, keyAuth string) {
s.txtRecordsMu.Lock()
defer s.txtRecordsMu.Unlock()
func (s *DNSManager) deleteDNSPresentMemory(dnsName, keyAuth string) {
s.recordsMu.Lock()
defer s.recordsMu.Unlock()
for i, mem := range s.txtRecords[dnsName] {
if mem.rec.Value == keyAuth {
s.txtRecords[dnsName] = append(s.txtRecords[dnsName][:i], s.txtRecords[dnsName][i+1:]...)
for i, mem := range s.records[dnsName] {
if mem.zoneRec.record.Value == keyAuth {
s.records[dnsName] = append(s.records[dnsName][:i], s.records[dnsName][i+1:]...)
return
}
}
}
// 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 {
// DNSProvider defines the set of operations required for
// ACME challenges or other sorts of domain verification.
// 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 DNSProvider interface {
libdns.RecordAppender
libdns.RecordDeleter
}

283
zerosslissuer.go Normal file
View File

@ -0,0 +1,283 @@
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"context"
"crypto/x509"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/caddyserver/zerossl"
"github.com/mholt/acmez/acme"
"go.uber.org/zap"
)
// NewZeroSSLIssuer returns a ZeroSSL issuer with default values filled in
// for empty fields in the template.
func NewZeroSSLIssuer(cfg *Config, template ZeroSSLIssuer) *ZeroSSLIssuer {
if cfg == nil {
panic("cannot make valid ZeroSSLIssuer without an associated CertMagic config")
}
template.config = cfg
template.logger = defaultLogger.Named("zerossl")
return &template
}
// ZeroSSLIssuer can get certificates from ZeroSSL's API. (To use ZeroSSL's ACME
// endpoint, use the ACMEIssuer instead.) Note that use of the API is restricted
// by payment tier.
type ZeroSSLIssuer struct {
// The API key (or "access key") for using the ZeroSSL API.
APIKey string
// How many days the certificate should be valid for.
ValidityDays int
// The host to bind to when opening a listener for
// verifying domain names (or IPs).
ListenHost string
// If HTTP is forwarded from port 80, specify the
// forwarded port here.
AltHTTPPort int
// To use CNAME validation instead of HTTP
// validation, set this field.
CNAMEValidation *DNSManager
config *Config
logger *zap.Logger
}
// Issue obtains a certificate for the given csr.
func (iss *ZeroSSLIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*IssuedCertificate, error) {
client := iss.getClient()
identifiers := namesFromCSR(csr)
logger := iss.logger.With(zap.Strings("identifiers", identifiers))
logger.Info("creating certificate")
cert, err := client.CreateCertificate(ctx, csr, iss.ValidityDays)
if err != nil {
return nil, fmt.Errorf("creating certificate: %v", err)
}
logger = logger.With(zap.String("cert_id", cert.ID))
logger.Info("created certificate")
defer func(certID string) {
if err != nil {
err := client.CancelCertificate(context.WithoutCancel(ctx), certID)
if err == nil {
logger.Info("canceled certificate")
} else {
logger.Error("unable to cancel certificate", zap.Error(err))
}
}
}(cert.ID)
var verificationMethod zerossl.VerificationMethod
if iss.CNAMEValidation == nil {
verificationMethod = zerossl.HTTPVerification
logger = logger.With(zap.String("verification_method", string(verificationMethod)))
httpVerifier := &httpSolver{
address: net.JoinHostPort(iss.ListenHost, strconv.Itoa(iss.getHTTPPort())),
handler: http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if !strings.HasPrefix(req.URL.Path, zerosslValidationPathPrefix) {
return
}
validation, ok := cert.Validation.OtherMethods[req.Host]
if !ok {
rw.WriteHeader(http.StatusNotFound)
return
}
// ensure URL matches
validationURL, err := url.Parse(validation.FileValidationURLHTTP)
if err != nil {
logger.Error("got invalid URL from CA",
zap.String("file_validation_url", validation.FileValidationURLHTTP),
zap.Error(err))
rw.WriteHeader(http.StatusInternalServerError)
return
}
if req.URL.Path != validationURL.Path {
rw.WriteHeader(http.StatusNotFound)
return
}
logger.Info("served HTTP validation file")
fmt.Fprint(rw, strings.Join(validation.FileValidationContent, "\n"))
}),
}
distSolver := distributedSolver{
storage: iss.config.Storage,
storageKeyIssuerPrefix: "zerossl",
solver: httpVerifier,
}
if err = distSolver.Present(ctx, acme.Challenge{}); err != nil {
return nil, fmt.Errorf("presenting token for verification: %v", err)
}
defer distSolver.CleanUp(ctx, acme.Challenge{})
} else {
verificationMethod = zerossl.CNAMEVerification
logger = logger.With(zap.String("verification_method", string(verificationMethod)))
// create the CNAME record(s)
records := make(map[string]zoneRecord, len(cert.Validation.OtherMethods))
for name, verifyInfo := range cert.Validation.OtherMethods {
zr, err := iss.CNAMEValidation.createRecord(ctx, verifyInfo.CnameValidationP1, "CNAME", verifyInfo.CnameValidationP2)
if err != nil {
return nil, fmt.Errorf("creating CNAME record: %v", err)
}
defer func(name string, zr zoneRecord) {
if err := iss.CNAMEValidation.cleanUpRecord(ctx, zr); err != nil {
logger.Warn("cleaning up temporary validation record failed",
zap.String("dns_name", name),
zap.Error(err))
}
}(name, zr)
records[name] = zr
}
// wait for them to propagate
for name, zr := range records {
if err := iss.CNAMEValidation.wait(ctx, zr); err != nil {
// allow it, since the CA will ultimately decide, but definitely log it
logger.Warn("failed CNAME record propagation check", zap.String("domain", name), zap.Error(err))
}
}
}
logger.Info("validating identifiers")
cert, err = client.VerifyIdentifiers(ctx, cert.ID, verificationMethod, nil)
if err != nil {
return nil, fmt.Errorf("verifying identifiers: %v", err)
}
switch cert.Status {
case "pending_validation":
logger.Info("validations succeeded; waiting for certificate to be issued")
cert, err = iss.waitForCertToBeIssued(ctx, client, cert)
if err != nil {
return nil, fmt.Errorf("waiting for certificate to be issued: %v", err)
}
case "issued":
logger.Info("validations succeeded; downloading certificate bundle")
default:
return nil, fmt.Errorf("unexpected certificate status: %s", cert.Status)
}
bundle, err := client.DownloadCertificate(ctx, cert.ID, false)
if err != nil {
return nil, fmt.Errorf("downloading certificate: %v", err)
}
logger.Info("successfully downloaded issued certificate")
return &IssuedCertificate{
Certificate: []byte(bundle.CertificateCrt + bundle.CABundleCrt),
Metadata: cert,
}, nil
}
func (*ZeroSSLIssuer) waitForCertToBeIssued(ctx context.Context, client zerossl.Client, cert zerossl.CertificateObject) (zerossl.CertificateObject, error) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return cert, ctx.Err()
case <-ticker.C:
var err error
cert, err = client.GetCertificate(ctx, cert.ID)
if err != nil {
return cert, err
}
if cert.Status == "issued" {
return cert, nil
}
if cert.Status != "pending_validation" {
return cert, fmt.Errorf("unexpected certificate status: %s", cert.Status)
}
}
}
}
func (iss *ZeroSSLIssuer) getClient() zerossl.Client {
return zerossl.Client{AccessKey: iss.APIKey}
}
func (iss *ZeroSSLIssuer) getHTTPPort() int {
useHTTPPort := HTTPChallengePort
if HTTPPort > 0 && HTTPPort != HTTPChallengePort {
useHTTPPort = HTTPPort
}
if iss.AltHTTPPort > 0 {
useHTTPPort = iss.AltHTTPPort
}
return useHTTPPort
}
// IssuerKey returns the unique issuer key for ZeroSSL.
func (iss *ZeroSSLIssuer) IssuerKey() string {
return "zerossl"
}
// Revoke revokes the given certificate. Only do this if there is a security or trust
// concern with the certificate.
func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert CertificateResource, reason int) error {
r := zerossl.UnspecifiedReason
switch reason {
case acme.ReasonKeyCompromise:
r = zerossl.KeyCompromise
case acme.ReasonAffiliationChanged:
r = zerossl.AffiliationChanged
case acme.ReasonSuperseded:
r = zerossl.Superseded
case acme.ReasonCessationOfOperation:
r = zerossl.CessationOfOperation
default:
return fmt.Errorf("unsupported reason: %d", reason)
}
return iss.getClient().RevokeCertificate(ctx, cert.IssuerData.(zerossl.CertificateObject).ID, r)
}
const (
zerosslAPIBase = "https://" + zerossl.BaseURL + "/acme"
zerosslValidationPathPrefix = "/.well-known/pki-validation/"
)
// Interface guards
var (
_ Issuer = (*ZeroSSLIssuer)(nil)
_ Revoker = (*ZeroSSLIssuer)(nil)
)