acme: replace ErrAuthorizationFailed with a type

This provides acme users with more insights into authorization failures.

Updates golang/go#19800.

Change-Id: I821298a6c8bd21fc517b2ab9128dd3d32be90249
Reviewed-on: https://go-review.googlesource.com/40450
Run-TryBot: Alex Vaghin <ddos@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/acme/acme.go b/acme/acme.go
index d650604..275844d 100644
--- a/acme/acme.go
+++ b/acme/acme.go
@@ -436,7 +436,7 @@
 //
 // It returns a non-nil Authorization only if its Status is StatusValid.
 // In all other cases WaitAuthorization returns an error.
-// If the Status is StatusInvalid, the returned error is ErrAuthorizationFailed.
+// If the Status is StatusInvalid, the returned error is of type *AuthorizationError.
 func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) {
 	sleep := sleeper(ctx)
 	for {
@@ -465,7 +465,7 @@
 			return raw.authorization(url), nil
 		}
 		if raw.Status == StatusInvalid {
-			return nil, ErrAuthorizationFailed
+			return nil, raw.error(url)
 		}
 		if err := sleep(retry, 0); err != nil {
 			return nil, err
@@ -882,14 +882,8 @@
 	// don't care if ReadAll returns an error:
 	// json.Unmarshal will fail in that case anyway
 	b, _ := ioutil.ReadAll(resp.Body)
-	e := struct {
-		Status int
-		Type   string
-		Detail string
-	}{
-		Status: resp.StatusCode,
-	}
-	if err := json.Unmarshal(b, &e); err != nil {
+	e := &wireError{Status: resp.StatusCode}
+	if err := json.Unmarshal(b, e); err != nil {
 		// this is not a regular error response:
 		// populate detail with anything we received,
 		// e.Status will already contain HTTP response code value
@@ -898,12 +892,7 @@
 			e.Detail = resp.Status
 		}
 	}
-	return &Error{
-		StatusCode:  e.Status,
-		ProblemType: e.Type,
-		Detail:      e.Detail,
-		Header:      resp.Header,
-	}
+	return e.error(resp.Header)
 }
 
 // chainCert fetches CA certificate chain recursively by following "up" links.
diff --git a/acme/acme_test.go b/acme/acme_test.go
index 0210ce3..a4d276d 100644
--- a/acme/acme_test.go
+++ b/acme/acme_test.go
@@ -543,6 +543,9 @@
 		if err == nil {
 			t.Error("err is nil")
 		}
+		if _, ok := err.(*AuthorizationError); !ok {
+			t.Errorf("err is %T; want *AuthorizationError", err)
+		}
 	}
 }
 
diff --git a/acme/types.go b/acme/types.go
index 0513b2e..ea0d235 100644
--- a/acme/types.go
+++ b/acme/types.go
@@ -4,6 +4,7 @@
 	"errors"
 	"fmt"
 	"net/http"
+	"strings"
 )
 
 // ACME server response statuses used to describe Authorization and Challenge states.
@@ -33,14 +34,8 @@
 	CRLReasonAACompromise         CRLReasonCode = 10
 )
 
-var (
-	// ErrAuthorizationFailed indicates that an authorization for an identifier
-	// did not succeed.
-	ErrAuthorizationFailed = errors.New("acme: identifier authorization failed")
-
-	// ErrUnsupportedKey is returned when an unsupported key type is encountered.
-	ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
-)
+// ErrUnsupportedKey is returned when an unsupported key type is encountered.
+var ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
 
 // Error is an ACME error, defined in Problem Details for HTTP APIs doc
 // http://tools.ietf.org/html/draft-ietf-appsawg-http-problem.
@@ -53,6 +48,7 @@
 	// Detail is a human-readable explanation specific to this occurrence of the problem.
 	Detail string
 	// Header is the original server error response headers.
+	// It may be nil.
 	Header http.Header
 }
 
@@ -60,6 +56,29 @@
 	return fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
 }
 
+// AuthorizationError indicates that an authorization for an identifier
+// did not succeed.
+// It contains all errors from Challenge items of the failed Authorization.
+type AuthorizationError struct {
+	// URI uniquely identifies the failed Authorization.
+	URI string
+
+	// Identifier is an AuthzID.Value of the failed Authorization.
+	Identifier string
+
+	// Errors is a collection of non-nil error values of Challenge items
+	// of the failed Authorization.
+	Errors []error
+}
+
+func (a *AuthorizationError) Error() string {
+	e := make([]string, len(a.Errors))
+	for i, err := range a.Errors {
+		e[i] = err.Error()
+	}
+	return fmt.Sprintf("acme: authorization error for %s: %s", a.Identifier, strings.Join(e, "; "))
+}
+
 // Account is a user account. It is associated with a private key.
 type Account struct {
 	// URI is the account unique ID, which is also a URL used to retrieve
@@ -118,6 +137,8 @@
 }
 
 // Challenge encodes a returned CA challenge.
+// Its Error field may be non-nil if the challenge is part of an Authorization
+// with StatusInvalid.
 type Challenge struct {
 	// Type is the challenge type, e.g. "http-01", "tls-sni-02", "dns-01".
 	Type string
@@ -130,6 +151,11 @@
 
 	// Status identifies the status of this challenge.
 	Status string
+
+	// Error indicates the reason for an authorization failure
+	// when this challenge was used.
+	// The type of a non-nil value is *Error.
+	Error error
 }
 
 // Authorization encodes an authorization response.
@@ -187,12 +213,26 @@
 	return a
 }
 
+func (z *wireAuthz) error(uri string) *AuthorizationError {
+	err := &AuthorizationError{
+		URI:        uri,
+		Identifier: z.Identifier.Value,
+	}
+	for _, raw := range z.Challenges {
+		if raw.Error != nil {
+			err.Errors = append(err.Errors, raw.Error.error(nil))
+		}
+	}
+	return err
+}
+
 // wireChallenge is ACME JSON challenge representation.
 type wireChallenge struct {
 	URI    string `json:"uri"`
 	Type   string
 	Token  string
 	Status string
+	Error  *wireError
 }
 
 func (c *wireChallenge) challenge() *Challenge {
@@ -205,5 +245,25 @@
 	if v.Status == "" {
 		v.Status = StatusPending
 	}
+	if c.Error != nil {
+		v.Error = c.Error.error(nil)
+	}
 	return v
 }
+
+// wireError is a subset of fields of the Problem Details object
+// as described in https://tools.ietf.org/html/rfc7807#section-3.1.
+type wireError struct {
+	Status int
+	Type   string
+	Detail string
+}
+
+func (e *wireError) error(h http.Header) *Error {
+	return &Error{
+		StatusCode:  e.Status,
+		ProblemType: e.Type,
+		Detail:      e.Detail,
+		Header:      h,
+	}
+}