New Upstream Release - golang-github-mholt-acmez

Ready changes

Summary

Merged new upstream version: 1.1.0 (was: 1.0.3+ds).

Diff

diff --git a/README.md b/README.md
index c9c28aa..0d88b28 100644
--- a/README.md
+++ b/README.md
@@ -30,16 +30,36 @@ In other words, the `acmez` package is **porcelain** while the `acme` package is
 - Supports niche aspects of RFC 8555 (such as alt cert chains and account key rollover)
 - Efficient solving of large SAN lists (e.g. for slow DNS record propagation)
 - Utility functions for solving challenges
-- Helpers for RFC 8737 (tls-alpn-01 challenge)
+	- [Device attestation challenges](https://datatracker.ietf.org/doc/draft-acme-device-attest/)
+	- RFC 8737 (tls-alpn-01 challenge)
 
-The `acmez` package is "bring-your-own-solver." It provides helper utilities for http-01, dns-01, and tls-alpn-01 challenges, but does not actually solve them for you. You must write an implementation of `acmez.Solver` in order to get certificates. How this is done depends on the environment in which you're using this code.
 
-This is not a command line utility either. The goal is to not add more external tooling to already-complex infrastructure: ACME and TLS should be built-in to servers rather than tacked on as an afterthought.
+## Examples
 
+See the [`examples` folder](https://github.com/mholt/acmez/tree/master/examples) for tutorials on how to use either package. **Most users should follow the [porcelain guide](https://github.com/mholt/acmez/blob/master/examples/porcelain/main.go) to get started.**
 
-## Examples
 
-See the `examples` folder for tutorials on how to use either package.
+## Challenge solvers
+
+The `acmez` package is "bring-your-own-solver." It provides helper utilities for http-01, dns-01, and tls-alpn-01 challenges, but does not actually solve them for you. You must write or use an implementation of [`acmez.Solver`](https://pkg.go.dev/github.com/mholt/acmez#Solver) in order to get certificates. How this is done depends on your environment/situation.
+
+However, you can find [a general-purpose dns-01 solver in CertMagic](https://pkg.go.dev/github.com/caddyserver/certmagic#DNS01Solver), which uses [libdns](https://github.com/libdns) packages to integrate with numerous DNS providers. You can use it like this:
+
+```go
+// minimal example using Cloudflare
+solver := &certmagic.DNS01Solver{
+	DNSProvider: &cloudflare.Provider{APIToken: "topsecret"},
+}
+client := acmez.Client{
+	ChallengeSolvers: map[string]acmez.Solver{
+		acme.ChallengeTypeDNS01: solver,
+	},
+	// ...
+}
+```
+
+If you're implementing a tls-alpn-01 solver, the `acmez` package can help. It has the constant [`ACMETLS1Protocol`](https://pkg.go.dev/github.com/mholt/acmez#pkg-constants) which you can use to identify challenge handshakes by inspecting the ClientHello's ALPN extension. Simply complete the handshake using a certificate from the [`acmez.TLSALPN01ChallengeCert()`](https://pkg.go.dev/github.com/mholt/acmez#TLSALPN01ChallengeCert) function to solve the challenge.
+
 
 
 ## History
@@ -52,7 +72,7 @@ Since then, Caddy has seen use in production longer than any other ACME client i
 
 A few years later, Caddy's novel auto-HTTPS logic was extracted into a library called [CertMagic](https://github.com/caddyserver/certmagic) to be usable by any Go program. Caddy would continue to use CertMagic, which implemented the certificate _automation and management_ logic on top of the low-level certificate _obtain_ logic that lego provided.
 
-Soon thereafter, the lego project shifted maintainership and the goals and vision of the project diverged from those of Caddy's use case of managing tens of thousands of certificates per instance. Eventually, [the original Caddy author announced work on a new ACME client library in Go](https://github.com/caddyserver/certmagic/issues/71) that exceeded Caddy's harsh requirements for large-scale enterprise deployments, lean builds, and simple API. This work finally came to fruition in 2020 as ACMEz.
+Soon thereafter, the lego project shifted maintainership and the goals and vision of the project diverged from those of Caddy's use case of managing tens of thousands of certificates per instance. Eventually, [the original Caddy author announced work on a new ACME client library in Go](https://github.com/caddyserver/certmagic/issues/71) that satisfied Caddy's harsh requirements for large-scale enterprise deployments, lean builds, and simple API. This work exceeded expectations and finally came to fruition in 2020 as ACMEz. It is much more lightweight with zero core dependencies, has a simple and elegant code base, and is thoroughly documented and easy to build upon.
 
 ---
 
diff --git a/acme/certificate.go b/acme/certificate.go
index a778280..42bbba0 100644
--- a/acme/certificate.go
+++ b/acme/certificate.go
@@ -111,7 +111,7 @@ func (c *Client) GetCertificateChain(ctx context.Context, account Account, certU
 	// heuristics to decide which is optimal." §7.4.2
 	alternates := extractLinks(resp, "alternate")
 	for _, altURL := range alternates {
-		resp, err = addChain(altURL)
+		_, err = addChain(altURL)
 		if err != nil {
 			return nil, fmt.Errorf("retrieving alternate certificate chain at %s: %w", altURL, err)
 		}
diff --git a/acme/challenge.go b/acme/challenge.go
index ccb264c..05b3955 100644
--- a/acme/challenge.go
+++ b/acme/challenge.go
@@ -78,6 +78,12 @@ type Challenge struct {
 	// structure as defined by the spec but is added by us to provide enough
 	// information to solve the DNS-01 challenge.
 	Identifier Identifier `json:"identifier,omitempty"`
+
+	// Payload contains a JSON-marshallable value that will be sent to the CA
+	// when responding to challenges. If not set, an empty JSON body "{}" will
+	// be included in the POST request. This field is applicable when responding
+	// to "device-attest-01" challenges.
+	Payload any `json:"-"`
 }
 
 // HTTP01ResourcePath returns the URI path for solving the http-01 challenge.
@@ -121,13 +127,17 @@ func (c *Client) InitiateChallenge(ctx context.Context, account Account, challen
 	if err := c.provision(ctx); err != nil {
 		return Challenge{}, err
 	}
-	_, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, challenge.URL, struct{}{}, &challenge)
+	if challenge.Payload == nil {
+		challenge.Payload = struct{}{}
+	}
+	_, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, challenge.URL, challenge.Payload, &challenge)
 	return challenge, err
 }
 
 // The standard or well-known ACME challenge types.
 const (
-	ChallengeTypeHTTP01    = "http-01"     // RFC 8555 §8.3
-	ChallengeTypeDNS01     = "dns-01"      // RFC 8555 §8.4
-	ChallengeTypeTLSALPN01 = "tls-alpn-01" // RFC 8737 §3
+	ChallengeTypeHTTP01         = "http-01"          // RFC 8555 §8.3
+	ChallengeTypeDNS01          = "dns-01"           // RFC 8555 §8.4
+	ChallengeTypeTLSALPN01      = "tls-alpn-01"      // RFC 8737 §3
+	ChallengeTypeDeviceAttest01 = "device-attest-01" // draft-acme-device-attest-00 §5
 )
diff --git a/acme/http.go b/acme/http.go
index a910d57..f7f44d2 100644
--- a/acme/http.go
+++ b/acme/http.go
@@ -41,7 +41,7 @@ import (
 // body will have been drained and closed, so there is no need to close it again.
 // It automatically retries in the case of network, I/O, or badNonce errors.
 func (c *Client) httpPostJWS(ctx context.Context, privateKey crypto.Signer,
-	kid, endpoint string, input, output interface{}) (*http.Response, error) {
+	kid, endpoint string, input, output any) (*http.Response, error) {
 
 	if err := c.provision(ctx); err != nil {
 		return nil, err
@@ -117,8 +117,7 @@ func (c *Client) httpPostJWS(ctx context.Context, privateKey crypto.Signer,
 		break
 	}
 
-	return resp, fmt.Errorf("request to %s failed after %d attempts: %w",
-		endpoint, attempts, err)
+	return resp, fmt.Errorf("attempt %d: %s: %w", attempts, endpoint, err)
 }
 
 // httpReq robustly performs an HTTP request using the given method to the given endpoint, honoring
@@ -131,7 +130,7 @@ func (c *Client) httpPostJWS(ctx context.Context, privateKey crypto.Signer,
 //
 // If there are any network or I/O errors, the request will be retried as safely and resiliently as
 // possible.
-func (c *Client) httpReq(ctx context.Context, method, endpoint string, joseJSONPayload []byte, output interface{}) (*http.Response, error) {
+func (c *Client) httpReq(ctx context.Context, method, endpoint string, joseJSONPayload []byte, output any) (*http.Response, error) {
 	// even if the caller doesn't specify an output, we still use a
 	// buffer to store possible error response (we reset it later)
 	buf := bufPool.Get().(*bytes.Buffer)
@@ -272,8 +271,8 @@ func (c *Client) doHTTPRequest(req *http.Request, buf *bytes.Buffer) (resp *http
 			zap.String("method", req.Method),
 			zap.String("url", req.URL.String()),
 			zap.Reflect("headers", req.Header),
-			zap.Int("status_code", resp.StatusCode),
-			zap.Reflect("response_headers", resp.Header))
+			zap.Reflect("response_headers", resp.Header),
+			zap.Int("status_code", resp.StatusCode))
 	}
 
 	// "The server MUST include a Replay-Nonce header field
@@ -374,19 +373,23 @@ func retryAfter(resp *http.Response, fallback time.Duration) (time.Duration, err
 	if resp == nil {
 		return fallback, nil
 	}
-	raSeconds := resp.Header.Get("Retry-After")
-	if raSeconds == "" {
+	raHeader := resp.Header.Get("Retry-After")
+	if raHeader == "" {
 		return fallback, nil
 	}
-	ra, err := strconv.Atoi(raSeconds)
-	if err != nil || ra < 0 {
-		return 0, fmt.Errorf("response had invalid Retry-After header: %s", raSeconds)
+	raSeconds, err := strconv.Atoi(raHeader)
+	if err == nil && raSeconds >= 0 {
+		return time.Duration(raSeconds) * time.Second, nil
 	}
-	return time.Duration(ra) * time.Second, nil
+	raTime, err := time.Parse(http.TimeFormat, raHeader)
+	if err == nil {
+		return time.Until(raTime), nil
+	}
+	return 0, fmt.Errorf("response had invalid Retry-After header: %s", raHeader)
 }
 
 var bufPool = sync.Pool{
-	New: func() interface{} {
+	New: func() any {
 		return new(bytes.Buffer)
 	},
 }
diff --git a/acme/http_test.go b/acme/http_test.go
index ec94a9a..7e58fcf 100644
--- a/acme/http_test.go
+++ b/acme/http_test.go
@@ -18,6 +18,7 @@ import (
 	"net/http"
 	"reflect"
 	"testing"
+	"time"
 )
 
 func TestExtractLinks(t *testing.T) {
@@ -53,3 +54,38 @@ func TestExtractLinks(t *testing.T) {
 		}
 	}
 }
+
+func TestRetryAfter(t *testing.T) {
+	fallback := time.Second * 60
+
+	gmt, _ := time.LoadLocation("GMT")
+	currentTime := time.Now().In(gmt)
+	retryAfterDateStr := currentTime.Add(time.Second * 456).Format(http.TimeFormat)
+
+	tests := []struct {
+		retryHeader string
+		expected    time.Duration
+	}{{
+		retryHeader: "",
+		expected:    fallback,
+	}, {
+		retryHeader: "123",
+		expected:    time.Second * 123,
+	}, {
+		retryHeader: retryAfterDateStr,
+		expected:    time.Second * 456,
+	}}
+
+	for _, test := range tests {
+		h := http.Header{}
+		h.Add("retry-after", test.retryHeader)
+		resp := http.Response{Header: h}
+		got, err := retryAfter(&resp, fallback)
+		if err != nil {
+			t.Error(err)
+			if got != test.expected {
+				t.Errorf("Expected %v, got %v", test.expected, got)
+			}
+		}
+	}
+}
diff --git a/acme/jws.go b/acme/jws.go
index bdbb457..c992acc 100644
--- a/acme/jws.go
+++ b/acme/jws.go
@@ -89,7 +89,7 @@ func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, url st
 // See https://tools.ietf.org/html/rfc7515#section-7.
 //
 // If nonce is empty, it will not be encoded into the header.
-func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid keyID, nonce, url string) ([]byte, error) {
+func jwsEncodeJSON(claimset any, key crypto.Signer, kid keyID, nonce, url string) ([]byte, error) {
 	alg, sha := jwsHasher(key.Public())
 	if alg == "" || !sha.Available() {
 		return nil, errUnsupportedKey
diff --git a/acme/order.go b/acme/order.go
index 579bb3a..245a39b 100644
--- a/acme/order.go
+++ b/acme/order.go
@@ -114,6 +114,15 @@ func (c *Client) NewOrder(ctx context.Context, account Account, order Order) (Or
 	return order, nil
 }
 
+// GetOrder retrieves an order from the server. The Order's Location field must be populated.
+func (c *Client) GetOrder(ctx context.Context, account Account, order Order) (Order, error) {
+	if err := c.provision(ctx); err != nil {
+		return order, err
+	}
+	_, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, order.Location, nil, &order)
+	return order, err
+}
+
 // FinalizeOrder finalizes the order with the server and polls under the server has
 // updated the order status. The CSR must be in ASN.1 DER-encoded format. If this
 // succeeds, the certificate is ready to download once this returns.
diff --git a/acme/problem.go b/acme/problem.go
index 98fdb00..cca9289 100644
--- a/acme/problem.go
+++ b/acme/problem.go
@@ -14,7 +14,11 @@
 
 package acme
 
-import "fmt"
+import (
+	"fmt"
+
+	"go.uber.org/zap/zapcore"
+)
 
 // Problem carries the details of an error from HTTP APIs as
 // defined in RFC 7807: https://tools.ietf.org/html/rfc7807
@@ -66,7 +70,7 @@ type Problem struct {
 	// spec, but, if a challenge fails for example, we can associate the
 	// error with the problematic authz object by setting this field.
 	// Challenge failures will have this set to an Authorization type.
-	Resource interface{} `json:"-"`
+	Resource any `json:"-"`
 }
 
 func (p Problem) Error() string {
@@ -77,6 +81,9 @@ func (p Problem) Error() string {
 	if len(p.Subproblems) > 0 {
 		for _, v := range p.Subproblems {
 			s += fmt.Sprintf(", problem %q: %s", v.Type, v.Detail)
+			if v.Identifier.Type != "" || v.Identifier.Value != "" {
+				s += fmt.Sprintf(" (%s_identifier=%s)", v.Identifier.Type, v.Identifier.Value)
+			}
 		}
 	}
 	if p.Instance != "" {
@@ -85,6 +92,17 @@ func (p Problem) Error() string {
 	return s
 }
 
+// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
+// This allows problems to be serialized by the zap logger.
+func (p Problem) MarshalLogObject(enc zapcore.ObjectEncoder) error {
+	enc.AddString("type", p.Type)
+	enc.AddString("title", p.Title)
+	enc.AddString("detail", p.Detail)
+	enc.AddString("instance", p.Instance)
+	enc.AddArray("subproblems", loggableSubproblems(p.Subproblems))
+	return nil
+}
+
 // Subproblem describes a more specific error in a problem according to
 // RFC 8555 §6.7.1: "An ACME problem document MAY contain the
 // 'subproblems' field, containing a JSON array of problem documents,
@@ -97,6 +115,26 @@ type Subproblem struct {
 	Identifier Identifier `json:"identifier,omitempty"`
 }
 
+// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
+// This allows subproblems to be serialized by the zap logger.
+func (sp Subproblem) MarshalLogObject(enc zapcore.ObjectEncoder) error {
+	enc.AddString("identifier_type", sp.Identifier.Type)
+	enc.AddString("identifier", sp.Identifier.Value)
+	enc.AddObject("subproblem", sp.Problem)
+	return nil
+}
+
+type loggableSubproblems []Subproblem
+
+// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
+// This allows a list of subproblems to be serialized by the zap logger.
+func (ls loggableSubproblems) MarshalLogArray(enc zapcore.ArrayEncoder) error {
+	for _, sp := range ls {
+		enc.AppendObject(sp)
+	}
+	return nil
+}
+
 // Standard token values for the "type" field of problems, as defined
 // in RFC 8555 §6.7: https://tools.ietf.org/html/rfc8555#section-6.7
 //
diff --git a/certificate_request.go b/certificate_request.go
new file mode 100644
index 0000000..018ee12
--- /dev/null
+++ b/certificate_request.go
@@ -0,0 +1,135 @@
+package acmez
+
+import (
+	"crypto/x509"
+	"encoding/asn1"
+	"errors"
+
+	"github.com/mholt/acmez/acme"
+	"golang.org/x/crypto/cryptobyte"
+	cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
+)
+
+var (
+	oidExtensionSubjectAltName = []int{2, 5, 29, 17}
+	oidPermanentIdentifier     = []int{1, 3, 6, 1, 5, 5, 7, 8, 3}
+	oidHardwareModuleName      = []int{1, 3, 6, 1, 5, 5, 7, 8, 4}
+)
+
+// RFC 5280 - https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6
+//
+//	OtherName ::= SEQUENCE {
+//	  type-id    OBJECT IDENTIFIER,
+//	  value      [0] EXPLICIT ANY DEFINED BY type-id }
+type otherName struct {
+	TypeID asn1.ObjectIdentifier
+	Value  asn1.RawValue
+}
+
+// permanentIdentifier is defined in RFC 4043 as an optional feature that can be
+// used by a CA to indicate that two or more certificates relate to the same
+// entity.
+//
+// The OID defined for this SAN is "1.3.6.1.5.5.7.8.3".
+//
+// See https://www.rfc-editor.org/rfc/rfc4043
+//
+//	PermanentIdentifier ::= SEQUENCE {
+//	  identifierValue    UTF8String OPTIONAL,
+//	  assigner           OBJECT IDENTIFIER OPTIONAL
+//	}
+type permanentIdentifier struct {
+	IdentifierValue string                `asn1:"utf8,optional"`
+	Assigner        asn1.ObjectIdentifier `asn1:"optional"`
+}
+
+// hardwareModuleName is defined in RFC 4108 as an optional feature that can be
+// used to identify a hardware module.
+//
+// The OID defined for this SAN is "1.3.6.1.5.5.7.8.4".
+//
+// See https://www.rfc-editor.org/rfc/rfc4108#section-5
+//
+//	HardwareModuleName ::= SEQUENCE {
+//	  hwType OBJECT IDENTIFIER,
+//	  hwSerialNum OCTET STRING
+//	}
+type hardwareModuleName struct {
+	Type         asn1.ObjectIdentifier
+	SerialNumber []byte `asn1:"tag:4"`
+}
+
+func forEachSAN(der cryptobyte.String, callback func(tag int, data []byte) error) error {
+	if !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) {
+		return errors.New("invalid subject alternative name extension")
+	}
+	for !der.Empty() {
+		var san cryptobyte.String
+		var tag cryptobyte_asn1.Tag
+		if !der.ReadAnyASN1Element(&san, &tag) {
+			return errors.New("invalid subject alternative name extension")
+		}
+		if err := callback(int(tag^0x80), san); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// createIdentifiersUsingCSR extracts the list of ACME identifiers from the
+// given Certificate Signing Request.
+func createIdentifiersUsingCSR(csr *x509.CertificateRequest) ([]acme.Identifier, error) {
+	var ids []acme.Identifier
+	for _, name := range csr.DNSNames {
+		ids = append(ids, acme.Identifier{
+			Type:  "dns", // RFC 8555 §9.7.7
+			Value: name,
+		})
+	}
+	for _, ip := range csr.IPAddresses {
+		ids = append(ids, acme.Identifier{
+			Type:  "ip", // RFC 8738
+			Value: ip.String(),
+		})
+	}
+
+	// Extract permanent identifiers and hardware module values.
+	// This block will ignore errors.
+	for _, ext := range csr.Extensions {
+		if ext.Id.Equal(oidExtensionSubjectAltName) {
+			err := forEachSAN(ext.Value, func(tag int, data []byte) error {
+				var on otherName
+				if rest, err := asn1.UnmarshalWithParams(data, &on, "tag:0"); err != nil || len(rest) > 0 {
+					return nil
+				}
+
+				switch {
+				case on.TypeID.Equal(oidPermanentIdentifier):
+					var pi permanentIdentifier
+					if _, err := asn1.Unmarshal(on.Value.Bytes, &pi); err == nil {
+						ids = append(ids, acme.Identifier{
+							Type:  "permanent-identifier", // draft-acme-device-attest-00 §3
+							Value: pi.IdentifierValue,
+						})
+					}
+				case on.TypeID.Equal(oidHardwareModuleName):
+					var hmn hardwareModuleName
+					if _, err := asn1.Unmarshal(on.Value.Bytes, &hmn); err == nil {
+						ids = append(ids, acme.Identifier{
+							Type:  "hardware-module", // draft-acme-device-attest-00 §4
+							Value: string(hmn.SerialNumber),
+						})
+					}
+				}
+				return nil
+			})
+			if err != nil {
+				return nil, err
+			}
+			break
+		}
+	}
+
+	return ids, nil
+}
diff --git a/client.go b/client.go
index a4d0446..02e62e2 100644
--- a/client.go
+++ b/client.go
@@ -21,10 +21,13 @@
 // implementing solvers and using the certificates. It DOES NOT manage
 // certificates, it only gets them from the ACME server.
 //
-// NOTE: This package's main function is to get a certificate, not manage it.
-// Most users will want to *manage* certificates over the lifetime of a
-// long-running program such as a HTTPS or TLS server, and should use CertMagic
+// NOTE: This package's primary purpose is to get a certificate, not manage it.
+// Most users actually want to *manage* certificates over the lifetime of
+// long-running programs such as HTTPS or TLS servers, and should use CertMagic
 // instead: https://github.com/caddyserver/certmagic.
+//
+// COMPATIBILITY: Exported identifiers that are related to draft specifications
+// are subject to change or removal without a major version bump.
 package acmez
 
 import (
@@ -60,9 +63,6 @@ type Client struct {
 
 	// Map of solvers keyed by name of the challenge type.
 	ChallengeSolvers map[string]Solver
-
-	// An optional logger. Default: no logs
-	Logger *zap.Logger
 }
 
 // ObtainCertificateUsingCSR obtains all resulting certificate chains using the given CSR, which
@@ -71,7 +71,7 @@ type Client struct {
 // x509.ParseCertificateRequest on the output). The Subject CommonName is NOT considered.
 //
 // It implements every single part of the ACME flow described in RFC 8555 §7.1 with the exception
-// of "Create account" because this method signature does not have a way to return the udpated
+// of "Create account" because this method signature does not have a way to return the updated
 // account object. The account's status MUST be "valid" in order to succeed.
 //
 // As far as SANs go, this method currently only supports DNSNames and IPAddresses on the csr.
@@ -83,25 +83,15 @@ func (c *Client) ObtainCertificateUsingCSR(ctx context.Context, account acme.Acc
 		return nil, fmt.Errorf("missing CSR")
 	}
 
-	var ids []acme.Identifier
-	for _, name := range csr.DNSNames {
-		ids = append(ids, acme.Identifier{
-			Type:  "dns", // RFC 8555 §9.7.7
-			Value: name,
-		})
-	}
-	for _, ip := range csr.IPAddresses {
-		ids = append(ids, acme.Identifier{
-			Type:  "ip", // RFC 8738
-			Value: ip.String(),
-		})
+	ids, err := createIdentifiersUsingCSR(csr)
+	if err != nil {
+		return nil, err
 	}
 	if len(ids) == 0 {
 		return nil, fmt.Errorf("no identifiers found")
 	}
 
 	order := acme.Order{Identifiers: ids}
-	var err error
 
 	// remember which challenge types failed for which identifiers
 	// so we can retry with other challenge types
@@ -134,16 +124,23 @@ func (c *Client) ObtainCertificateUsingCSR(ctx context.Context, account acme.Acc
 		// for some errors, we can retry with different challenge types
 		var problem acme.Problem
 		if errors.As(err, &problem) {
-			authz := problem.Resource.(acme.Authorization)
+			authz, haveAuthz := problem.Resource.(acme.Authorization)
 			if c.Logger != nil {
-				c.Logger.Error("validating authorization",
-					zap.String("identifier", authz.IdentifierValue()),
-					zap.Error(err),
+				l := c.Logger
+				if haveAuthz {
+					l = l.With(zap.String("identifier", authz.IdentifierValue()))
+				}
+				l.Error("validating authorization",
+					zap.Object("problem", problem),
 					zap.String("order", order.Location),
 					zap.Int("attempt", attempt),
 					zap.Int("max_attempts", maxAttempts))
 			}
-			err = fmt.Errorf("solving challenge: %s: %w", authz.IdentifierValue(), err)
+			errStr := "solving challenge"
+			if haveAuthz {
+				errStr += ": " + authz.IdentifierValue()
+			}
+			err = fmt.Errorf("%s: %w", errStr, err)
 			if errors.As(err, &retryableErr{}) {
 				continue
 			}
@@ -400,6 +397,11 @@ func (c *Client) presentForNextChallenge(ctx context.Context, authz *authzState)
 
 func (c *Client) initiateCurrentChallenge(ctx context.Context, authz *authzState) error {
 	if authz.Status != acme.StatusPending {
+		if c.Logger != nil {
+			c.Logger.Debug("skipping challenge initiation because authorization is not pending",
+				zap.String("identifier", authz.IdentifierValue()),
+				zap.String("authz_status", authz.Status))
+		}
 		return nil
 	}
 
@@ -409,12 +411,42 @@ func (c *Client) initiateCurrentChallenge(ctx context.Context, authz *authzState
 	// that's probably OK, since we can't finalize the order until the slow
 	// challenges are done too)
 	if waiter, ok := authz.currentSolver.(Waiter); ok {
+		if c.Logger != nil {
+			c.Logger.Debug("waiting for solver before continuing",
+				zap.String("identifier", authz.IdentifierValue()),
+				zap.String("challenge_type", authz.currentChallenge.Type))
+		}
 		err := waiter.Wait(ctx, authz.currentChallenge)
+		if c.Logger != nil {
+			c.Logger.Debug("done waiting for solver",
+				zap.String("identifier", authz.IdentifierValue()),
+				zap.String("challenge_type", authz.currentChallenge.Type))
+		}
 		if err != nil {
 			return fmt.Errorf("waiting for solver %T to be ready: %w", authz.currentSolver, err)
 		}
 	}
 
+	// for device-attest-01 challenges the client needs to present a payload
+	// that will be validated by the CA.
+	if payloader, ok := authz.currentSolver.(Payloader); ok {
+		if c.Logger != nil {
+			c.Logger.Debug("getting payload from solver before continuing",
+				zap.String("identifier", authz.IdentifierValue()),
+				zap.String("challenge_type", authz.currentChallenge.Type))
+		}
+		p, err := payloader.Payload(ctx, authz.currentChallenge)
+		if c.Logger != nil {
+			c.Logger.Debug("done getting payload from solver",
+				zap.String("identifier", authz.IdentifierValue()),
+				zap.String("challenge_type", authz.currentChallenge.Type))
+		}
+		if err != nil {
+			return fmt.Errorf("getting payload from solver %T failed: %w", authz.currentSolver, err)
+		}
+		authz.currentChallenge.Payload = p
+	}
+
 	// tell the server to initiate the challenge
 	var err error
 	authz.currentChallenge, err = c.Client.InitiateChallenge(ctx, authz.account, authz.currentChallenge)
@@ -492,7 +524,7 @@ func (c *Client) pollAuthorization(ctx context.Context, account acme.Account, au
 			c.Logger.Error("cleaning up solver",
 				zap.String("identifier", authz.IdentifierValue()),
 				zap.String("challenge_type", authz.currentChallenge.Type),
-				zap.Error(err))
+				zap.Error(cleanupErr))
 		}
 		authz.currentSolver = nil // avoid cleaning it up again later
 	}
@@ -505,28 +537,45 @@ func (c *Client) pollAuthorization(ctx context.Context, account acme.Account, au
 				c.Logger.Error("challenge failed",
 					zap.String("identifier", authz.IdentifierValue()),
 					zap.String("challenge_type", authz.currentChallenge.Type),
-					zap.Int("status_code", problem.Status),
-					zap.String("problem_type", problem.Type),
-					zap.String("error", problem.Detail))
+					zap.Object("problem", problem))
 			}
 
 			failedChallengeTypes.rememberFailedChallenge(authz)
 
-			switch problem.Type {
-			case acme.ProblemTypeConnection,
-				acme.ProblemTypeDNS,
-				acme.ProblemTypeServerInternal,
-				acme.ProblemTypeUnauthorized,
-				acme.ProblemTypeTLS:
-				// this error might be recoverable with another challenge type
-				return retryableErr{err}
+			if c.countAvailableChallenges(authz) > 0 {
+				switch problem.Type {
+				case acme.ProblemTypeConnection,
+					acme.ProblemTypeDNS,
+					acme.ProblemTypeServerInternal,
+					acme.ProblemTypeUnauthorized,
+					acme.ProblemTypeTLS:
+					// this error might be recoverable with another challenge type
+					return retryableErr{err}
+				}
 			}
 		}
 		return fmt.Errorf("[%s] %w", authz.Authorization.IdentifierValue(), err)
 	}
+
+	if c.Logger != nil {
+		c.Logger.Info("authorization finalized",
+			zap.String("identifier", authz.IdentifierValue()),
+			zap.String("authz_status", authz.Status))
+	}
+
 	return nil
 }
 
+func (c *Client) countAvailableChallenges(authz *authzState) int {
+	count := 0
+	for _, remainingChal := range authz.remainingChallenges {
+		if _, ok := c.ChallengeSolvers[remainingChal.Type]; ok {
+			count++
+		}
+	}
+	return count
+}
+
 func (c *Client) enabledChallengeTypes() []string {
 	enabledChallenges := make([]string, 0, len(c.ChallengeSolvers))
 	for name, val := range c.ChallengeSolvers {
diff --git a/debian/changelog b/debian/changelog
index e2de5b4..6908c34 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+golang-github-mholt-acmez (1.1.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 01 Apr 2023 05:52:45 -0000
+
 golang-github-mholt-acmez (0.1.3-2) unstable; urgency=medium
 
   * Team upload.
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..fbda7a2
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,8 @@
+ACMEz Examples
+==============
+
+- `plumbing`: Tutorial showing low-level ACME transaction using the `acme` package
+- `porcelain`: Demonstrates how to use the `acmez` package for convenient ACME transactions
+
+**Most users should follow the porcelain guide.** You only need the plumbing tutorial if you want to dig deep and have greater control over your ACME flow.
+
diff --git a/examples/attestation/attestation.tpl b/examples/attestation/attestation.tpl
new file mode 100644
index 0000000..64255d3
--- /dev/null
+++ b/examples/attestation/attestation.tpl
@@ -0,0 +1,7 @@
+{ 
+    "subject": {{ toJson .Subject }},
+    "sans": [{
+        "type": "permanentIdentifier", 
+        "value": {{ toJson .Subject.CommonName }}
+    }]
+}
\ No newline at end of file
diff --git a/examples/attestation/main.go b/examples/attestation/main.go
new file mode 100644
index 0000000..234b94d
--- /dev/null
+++ b/examples/attestation/main.go
@@ -0,0 +1,305 @@
+// Copyright 2023 Mariano Cano
+//
+// 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 main
+
+import (
+	"bufio"
+	"context"
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/pem"
+	"fmt"
+	"log"
+	"math/big"
+	"net/http"
+	"os"
+
+	"github.com/mholt/acmez"
+	"github.com/mholt/acmez/acme"
+	"go.uber.org/zap"
+)
+
+const usage = `Usage: STTY=-icanon attestation <csr-file>
+
+<csr-file> A file with a certificate signing request or CSR.
+
+To be able to run this example, we need to use a key that can be attested,
+"step-ca" [1], for example, supports attestation using YubiKey 5 Series.
+
+To configure "step-ca" with device-attest-01 support, you need to create an ACME
+provisioner with the device-attest-01 challenge enabled. In the ca.json the
+provisioner looks like this:
+
+  {
+    "type": "ACME",
+    "name": "attestation",
+    "challenges": [ "device-attest-01" ]
+  }
+
+After configuring "step-ca" the first thing that we need is to create a key in
+one of the YubiKey slots. We're picking 82 in this example. To do this, we will
+use "step" [2] with the "step-kms-plugin" [2], and we will run the following:
+
+  step kms create "yubikey:slot-id=82?pin-value=123456"
+
+Then we need to create a CSR signed by this new key. This CSR must include the
+serial number in the Permanent Identifier Subject Alternative Name extension.
+The serial number of a YubiKey is printed on the key, but it is also available
+in an attestation certificate. You can see it running:
+
+  step kms attest "yubikey:slot-id=82?pin-value=123456" | \
+  step certificate inspect
+
+To add the permanent identifier, we will need to use the following template:
+
+  {
+    "subject": {{ toJson .Subject }},
+    "sans": [{
+      "type": "permanentIdentifier",
+      "value": {{ toJson .Subject}}
+    }]
+  }
+
+Having the template in "attestation.tpl", and assuming the serial number is
+123456789, we can get the proper CSR running:
+
+  step certificate create --csr --template attestation.tpl \
+  --kms "yubikey:?pin-value=123456" --key "yubikey:slot-id=82" \
+  123456789 att.csr
+
+With this we can run this program with the new CSR:
+
+  STTY=-icanon attestation att.csr
+
+The program will ask you to create an attestation of the ACME Key Authorization,
+running:
+
+  echo -n <key-authorization> | \
+  step kms attest --format step "yubikey:slot-id=82?pin-value=123456"
+
+Note that because the input that we need to paste is usually more than 1024
+characters, the "STTY=-icanon" environment variable is required.
+
+[1] step-ca         - https://github.com/smallstep/certificates
+[2] step            - https://github.com/smallstep/cli
+[3] step-kms-plugin - https://github.com/smallstep/step-kms-plugin`
+
+func main() {
+	if len(os.Args) != 2 {
+		fmt.Println(usage)
+		os.Exit(1)
+	}
+
+	if os.Getenv("STTY") != "-icanon" {
+		fmt.Fprintln(os.Stderr, "Please run this program with the environment variable STTY=-icanon")
+		os.Exit(2)
+	}
+
+	b, err := os.ReadFile(os.Args[1])
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	block, _ := pem.Decode(b)
+	if block == nil {
+		log.Fatal("error reading CSR")
+	}
+	csr, err := x509.ParseCertificateRequest(block.Bytes)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	if err := attestationExample(csr); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func attestationExample(csr *x509.CertificateRequest) error {
+	// A context allows us to cancel long-running ops
+	ctx := context.Background()
+
+	// Logging is important - replace with your own zap logger
+	logger, err := zap.NewDevelopment()
+	if err != nil {
+		return err
+	}
+
+	// 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 {
+		return fmt.Errorf("generating account key: %v", err)
+	}
+	account := acme.Account{
+		Contact:              []string{"mailto:you@example.com"},
+		TermsOfServiceAgreed: true,
+		PrivateKey:           accountPrivateKey,
+	}
+
+	// A high-level client embeds a low-level client and makes the ACME flow
+	// much easier, but with less flexibility than using the low-level API
+	// directly (see other example).
+	//
+	// This example implements it's own solver that requires you to provide the
+	// device attestation statement.
+	client := acmez.Client{
+		Client: &acme.Client{
+			Directory: "https://localhost:9000/acme/attestation/directory",
+			HTTPClient: &http.Client{
+				Transport: &http.Transport{
+					TLSClientConfig: &tls.Config{
+						InsecureSkipVerify: true, // REMOVE THIS FOR PRODUCTION USE!
+					},
+				},
+			},
+			Logger: logger,
+		},
+		ChallengeSolvers: map[string]acmez.Solver{
+			acme.ChallengeTypeDeviceAttest01: attSolver{account},
+		},
+	}
+
+	// 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 {
+		return fmt.Errorf("new account: %v", err)
+	}
+
+	// Do the ACME dance with the created account and get the certificates.
+	certs, err := client.ObtainCertificateUsingCSR(ctx, account, csr)
+	if err != nil {
+		return fmt.Errorf("obtaining certificate: %v", err)
+	}
+
+	// ACME servers should usually give you the entire certificate chain
+	// in PEM format, and sometimes even alternate chains! It's up to you
+	// which one(s) to store and use, but whatever you do, be sure to
+	// store the certificate and key somewhere safe and secure, i.e. don't
+	// lose them!
+	for _, cert := range certs {
+		fmt.Printf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM)
+	}
+
+	return nil
+}
+
+// attSolver is a acmez.Solver That requires you to provide the attestation
+// statement.
+type attSolver struct {
+	account acme.Account
+}
+
+func (s attSolver) Present(ctx context.Context, chal acme.Challenge) error {
+	log.Printf("[DEBUG] present: %#v", chal)
+	return nil
+}
+
+func (s attSolver) CleanUp(ctx context.Context, chal acme.Challenge) error {
+	log.Printf("[DEBUG] cleanup: %#v", chal)
+	return nil
+}
+
+func (s attSolver) Payload(ctx context.Context, chal acme.Challenge) (any, error) {
+	log.Printf("[DEBUG] payload: %#v", chal)
+
+	// Calculate key authorization. This is the data that we need to sign to
+	// validate the device attestation challenge.
+	thumbprint, err := jwkThumbprint(s.account.PrivateKey.Public())
+	if err != nil {
+		return nil, err
+	}
+	keyAuthorization := fmt.Sprintf("%s.%s", chal.Token, thumbprint)
+
+	fmt.Println()
+	fmt.Println("Now you need to sign following keyAuthorization:")
+	fmt.Println(keyAuthorization)
+	fmt.Println()
+	fmt.Println("To do this you can use step-kms-plugin running:")
+	fmt.Printf("echo -n %s | step kms attest --format step \"yubikey:slot-id=82?pin-value=123456\"\n", keyAuthorization)
+	fmt.Println()
+	fmt.Println("Please enter the base64 output and press Enter:")
+	reader := bufio.NewReaderSize(os.Stdin, 4096)
+	attObj, err := reader.ReadString('\n')
+	if err != nil {
+		return nil, err
+	}
+
+	return map[string]string{
+		"attObj": attObj,
+	}, nil
+}
+
+// jwkThumbprint creates a JWK thumbprint out of pub
+// as specified in https://tools.ietf.org/html/rfc7638.
+func jwkThumbprint(pub crypto.PublicKey) (string, error) {
+	jwk, err := jwkEncode(pub)
+	if err != nil {
+		return "", err
+	}
+	b := sha256.Sum256([]byte(jwk))
+	return base64.RawURLEncoding.EncodeToString(b[:]), nil
+}
+
+// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
+// The result is also suitable for creating a JWK thumbprint.
+// https://tools.ietf.org/html/rfc7517
+func jwkEncode(pub crypto.PublicKey) (string, error) {
+	switch pub := pub.(type) {
+	case *rsa.PublicKey:
+		// https://tools.ietf.org/html/rfc7518#section-6.3.1
+		n := pub.N
+		e := big.NewInt(int64(pub.E))
+		// Field order is important.
+		// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
+		return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
+			base64.RawURLEncoding.EncodeToString(e.Bytes()),
+			base64.RawURLEncoding.EncodeToString(n.Bytes()),
+		), nil
+	case *ecdsa.PublicKey:
+		// https://tools.ietf.org/html/rfc7518#section-6.2.1
+		p := pub.Curve.Params()
+		n := p.BitSize / 8
+		if p.BitSize%8 != 0 {
+			n++
+		}
+		x := pub.X.Bytes()
+		if n > len(x) {
+			x = append(make([]byte, n-len(x)), x...)
+		}
+		y := pub.Y.Bytes()
+		if n > len(y) {
+			y = append(make([]byte, n-len(y)), y...)
+		}
+		// Field order is important.
+		// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
+		return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
+			p.Name,
+			base64.RawURLEncoding.EncodeToString(x),
+			base64.RawURLEncoding.EncodeToString(y),
+		), nil
+	default:
+		return "", fmt.Errorf("unsupported key type %T", pub)
+	}
+}
diff --git a/examples/plumbing/main.go b/examples/plumbing/main.go
index 91f587d..cec2f1b 100644
--- a/examples/plumbing/main.go
+++ b/examples/plumbing/main.go
@@ -26,6 +26,7 @@ import (
 	"net/http"
 
 	"github.com/mholt/acmez/acme"
+	"go.uber.org/zap"
 )
 
 // Run pebble (the ACME server) before running this example:
@@ -46,6 +47,12 @@ func lowLevelExample() error {
 	// a context allows us to cancel long-running ops
 	ctx := context.Background()
 
+	// Logging is important - replace with your own zap logger
+	logger, err := zap.NewDevelopment()
+	if err != nil {
+		return err
+	}
+
 	// first you need a private key for your certificate
 	certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 	if err != nil {
@@ -83,10 +90,11 @@ func lowLevelExample() error {
 		HTTPClient: &http.Client{
 			Transport: &http.Transport{
 				TLSClientConfig: &tls.Config{
-					InsecureSkipVerify: true, // we're just tinkering locally - REMOVE THIS FOR PRODUCTION USE!
+					InsecureSkipVerify: true, // REMOVE THIS FOR PRODUCTION USE!
 				},
 			},
 		},
+		Logger: logger,
 	}
 
 	// if the account is new, we need to create it; only do this once!
diff --git a/examples/porcelain/main.go b/examples/porcelain/main.go
index 9c0a55e..ada39c9 100644
--- a/examples/porcelain/main.go
+++ b/examples/porcelain/main.go
@@ -26,6 +26,7 @@ import (
 
 	"github.com/mholt/acmez"
 	"github.com/mholt/acmez/acme"
+	"go.uber.org/zap"
 )
 
 // Run pebble (the ACME server) before running this example:
@@ -46,6 +47,12 @@ func highLevelExample() error {
 	// A context allows us to cancel long-running ops
 	ctx := context.Background()
 
+	// Logging is important - replace with your own zap logger
+	logger, err := zap.NewDevelopment()
+	if err != nil {
+		return err
+	}
+
 	// A high-level client embeds a low-level client and makes
 	// the ACME flow much easier, but with less flexibility
 	// than using the low-level API directly (see other example).
@@ -58,17 +65,20 @@ func highLevelExample() error {
 	// where others might still succeed.
 	//
 	// Implementing challenge solvers is outside the scope of this
-	// example.
+	// example, but you can find a high-quality, general-purpose
+	// solver for the dns-01 challenge in CertMagic:
+	// https://pkg.go.dev/github.com/caddyserver/certmagic#DNS01Solver
 	client := acmez.Client{
 		Client: &acme.Client{
 			Directory: "https://127.0.0.1:14000/dir", // default pebble endpoint
 			HTTPClient: &http.Client{
 				Transport: &http.Transport{
 					TLSClientConfig: &tls.Config{
-						InsecureSkipVerify: true, // we're just tinkering locally - REMOVE THIS FOR PRODUCTION USE!
+						InsecureSkipVerify: true, // REMOVE THIS FOR PRODUCTION USE!
 					},
 				},
 			},
+			Logger: logger,
 		},
 		ChallengeSolvers: map[string]acmez.Solver{
 			acme.ChallengeTypeHTTP01:    mySolver{}, // provide these!
diff --git a/go.mod b/go.mod
index a0495e5..afd9560 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,15 @@
 module github.com/mholt/acmez
 
-go 1.14
+go 1.18
 
 require (
-	go.uber.org/zap v1.15.0
-	golang.org/x/net v0.0.0-20200707034311-ab3426394381
+	go.uber.org/zap v1.23.0
+	golang.org/x/crypto v0.5.0
+	golang.org/x/net v0.5.0
+)
+
+require (
+	go.uber.org/atomic v1.7.0 // indirect
+	go.uber.org/multierr v1.6.0 // indirect
+	golang.org/x/text v0.6.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 929a2dd..1de7c71 100644
--- a/go.sum
+++ b/go.sum
@@ -1,61 +1,24 @@
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
-go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
-go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
-go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
-go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
-go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
-go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
-go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
-golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
+go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
+go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
+golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
+golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
+golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/solver.go b/solver.go
index 8e77b27..095c9a6 100644
--- a/solver.go
+++ b/solver.go
@@ -70,3 +70,16 @@ type Solver interface {
 type Waiter interface {
 	Wait(context.Context, acme.Challenge) error
 }
+
+// Payloader is an optional interface for Solvers to implement. Its purpose is
+// to return the payload sent to the CA when responding to a challenge. This
+// interface is applicable when responding to "device-attest-01" challenges
+//
+// If implemented, it will be called after Present() and if a Waiter is
+// implemented it will be called after Wait(), just before the challenge is
+// initiated with the server.
+//
+// Implementations MUST honor context cancellation.
+type Payloader interface {
+	Payload(context.Context, acme.Challenge) (any, error)
+}

More details

Full run details

Historical runs