Skip to content

Commit 83a5a9b

Browse files
acme: add support for subproblems
Add support for RFC 8555 subproblems. The type naming is real bike-shed territory, but I think I've mostly matched the existing style of the package. In a similar vein the format of how to print subproblems when stringing an acme.Error is up for debate (it could just be completely ignored, and require clients to inspect Error.Subproblems themselves). Fixes golang/go#38978 Change-Id: Ice803079bab621ae9410de79e7e75e11c1af21b6 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/233165 Trust: Roland Shoemaker <[email protected]> Run-TryBot: Roland Shoemaker <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Russ Cox <[email protected]>
1 parent 5bf0f12 commit 83a5a9b

File tree

2 files changed

+147
-6
lines changed

2 files changed

+147
-6
lines changed

acme/types.go

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,32 @@ var (
5757
ErrNoAccount = errors.New("acme: account does not exist")
5858
)
5959

60+
// A Subproblem describes an ACME subproblem as reported in an Error.
61+
type Subproblem struct {
62+
// Type is a URI reference that identifies the problem type,
63+
// typically in a "urn:acme:error:xxx" form.
64+
Type string
65+
// Detail is a human-readable explanation specific to this occurrence of the problem.
66+
Detail string
67+
// Instance indicates a URL that the client should direct a human user to visit
68+
// in order for instructions on how to agree to the updated Terms of Service.
69+
// In such an event CA sets StatusCode to 403, Type to
70+
// "urn:ietf:params:acme:error:userActionRequired", and adds a Link header with relation
71+
// "terms-of-service" containing the latest TOS URL.
72+
Instance string
73+
// Identifier may contain the ACME identifier that the error is for.
74+
Identifier *AuthzID
75+
}
76+
77+
func (sp Subproblem) String() string {
78+
str := fmt.Sprintf("%s: ", sp.Type)
79+
if sp.Identifier != nil {
80+
str += fmt.Sprintf("[%s: %s] ", sp.Identifier.Type, sp.Identifier.Value)
81+
}
82+
str += sp.Detail
83+
return str
84+
}
85+
6086
// Error is an ACME error, defined in Problem Details for HTTP APIs doc
6187
// http://tools.ietf.org/html/draft-ietf-appsawg-http-problem.
6288
type Error struct {
@@ -76,10 +102,21 @@ type Error struct {
76102
// Header is the original server error response headers.
77103
// It may be nil.
78104
Header http.Header
105+
// Subproblems may contain more detailed information about the individual problems
106+
// that caused the error. This field is only sent by RFC 8555 compatible ACME
107+
// servers. Defined in RFC 8555 Section 6.7.1.
108+
Subproblems []Subproblem
79109
}
80110

81111
func (e *Error) Error() string {
82-
return fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
112+
str := fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail)
113+
if len(e.Subproblems) > 0 {
114+
str += fmt.Sprintf("; subproblems:")
115+
for _, sp := range e.Subproblems {
116+
str += fmt.Sprintf("\n\t%s", sp)
117+
}
118+
}
119+
return str
83120
}
84121

85122
// AuthorizationError indicates that an authorization for an identifier
@@ -533,20 +570,23 @@ func (c *wireChallenge) challenge() *Challenge {
533570
// wireError is a subset of fields of the Problem Details object
534571
// as described in https://tools.ietf.org/html/rfc7807#section-3.1.
535572
type wireError struct {
536-
Status int
537-
Type string
538-
Detail string
539-
Instance string
573+
Status int
574+
Type string
575+
Detail string
576+
Instance string
577+
Subproblems []Subproblem
540578
}
541579

542580
func (e *wireError) error(h http.Header) *Error {
543-
return &Error{
581+
err := &Error{
544582
StatusCode: e.Status,
545583
ProblemType: e.Type,
546584
Detail: e.Detail,
547585
Instance: e.Instance,
548586
Header: h,
587+
Subproblems: e.Subproblems,
549588
}
589+
return err
550590
}
551591

552592
// CertOption is an optional argument type for the TLS ChallengeCert methods for

acme/types_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package acme
77
import (
88
"errors"
99
"net/http"
10+
"reflect"
1011
"testing"
1112
"time"
1213
)
@@ -116,3 +117,103 @@ func TestAuthorizationError(t *testing.T) {
116117
}
117118
}
118119
}
120+
121+
func TestSubproblems(t *testing.T) {
122+
tests := []struct {
123+
wire wireError
124+
expectedOut Error
125+
}{
126+
{
127+
wire: wireError{
128+
Status: 1,
129+
Type: "urn:error",
130+
Detail: "it's an error",
131+
},
132+
expectedOut: Error{
133+
StatusCode: 1,
134+
ProblemType: "urn:error",
135+
Detail: "it's an error",
136+
},
137+
},
138+
{
139+
wire: wireError{
140+
Status: 1,
141+
Type: "urn:error",
142+
Detail: "it's an error",
143+
Subproblems: []Subproblem{
144+
{
145+
Type: "urn:error:sub",
146+
Detail: "it's a subproblem",
147+
},
148+
},
149+
},
150+
expectedOut: Error{
151+
StatusCode: 1,
152+
ProblemType: "urn:error",
153+
Detail: "it's an error",
154+
Subproblems: []Subproblem{
155+
{
156+
Type: "urn:error:sub",
157+
Detail: "it's a subproblem",
158+
},
159+
},
160+
},
161+
},
162+
{
163+
wire: wireError{
164+
Status: 1,
165+
Type: "urn:error",
166+
Detail: "it's an error",
167+
Subproblems: []Subproblem{
168+
{
169+
Type: "urn:error:sub",
170+
Detail: "it's a subproblem",
171+
Identifier: &AuthzID{Type: "dns", Value: "example"},
172+
},
173+
},
174+
},
175+
expectedOut: Error{
176+
StatusCode: 1,
177+
ProblemType: "urn:error",
178+
Detail: "it's an error",
179+
Subproblems: []Subproblem{
180+
{
181+
Type: "urn:error:sub",
182+
Detail: "it's a subproblem",
183+
Identifier: &AuthzID{Type: "dns", Value: "example"},
184+
},
185+
},
186+
},
187+
},
188+
}
189+
190+
for _, tc := range tests {
191+
out := tc.wire.error(nil)
192+
if !reflect.DeepEqual(*out, tc.expectedOut) {
193+
t.Errorf("Unexpected error: wanted %v, got %v", tc.expectedOut, *out)
194+
}
195+
}
196+
}
197+
198+
func TestErrorStringerWithSubproblems(t *testing.T) {
199+
err := Error{
200+
StatusCode: 1,
201+
ProblemType: "urn:error",
202+
Detail: "it's an error",
203+
Subproblems: []Subproblem{
204+
{
205+
Type: "urn:error:sub",
206+
Detail: "it's a subproblem",
207+
},
208+
{
209+
Type: "urn:error:sub",
210+
Detail: "it's a subproblem",
211+
Identifier: &AuthzID{Type: "dns", Value: "example"},
212+
},
213+
},
214+
}
215+
expectedStr := "1 urn:error: it's an error; subproblems:\n\turn:error:sub: it's a subproblem\n\turn:error:sub: [dns: example] it's a subproblem"
216+
if err.Error() != expectedStr {
217+
t.Errorf("Unexpected error string: wanted %q, got %q", expectedStr, err.Error())
218+
}
219+
}

0 commit comments

Comments
 (0)