diff --git a/grpc/errors.go b/grpc/errors.go index 7a06b9fcf..7f9aabbb6 100644 --- a/grpc/errors.go +++ b/grpc/errors.go @@ -41,7 +41,8 @@ func wrapError(ctx context.Context, appErr error) error { return berrors.InternalServerError( "error marshaling json SubErrors, orig error %q", err) } - pairs = append(pairs, "suberrors", string(jsonSubErrs)) + headerSafeSubErrs := strconv.QuoteToASCII(string(jsonSubErrs)) + pairs = append(pairs, "suberrors", headerSafeSubErrs) } // If there is a RetryAfter value then extend the metadata pairs to @@ -110,7 +111,17 @@ func unwrapError(err error, md metadata.MD) error { ) } - unmarshalErr := json.Unmarshal([]byte(subErrorsVal[0]), &outErr.SubErrors) + unquotedSubErrors, unquoteErr := strconv.Unquote(subErrorsVal[0]) + if unquoteErr != nil { + return fmt.Errorf( + "unquoting 'suberrors' %q, wrapped error %q: %w", + subErrorsVal[0], + inErrMsg, + unquoteErr, + ) + } + + unmarshalErr := json.Unmarshal([]byte(unquotedSubErrors), &outErr.SubErrors) if unmarshalErr != nil { return berrors.InternalServerError( "JSON unmarshaling 'suberrors' %q, wrapped error %q: %s", diff --git a/test/integration/errors_test.go b/test/integration/errors_test.go index 0291b032e..b0039670e 100644 --- a/test/integration/errors_test.go +++ b/test/integration/errors_test.go @@ -148,3 +148,36 @@ func TestAccountEmailError(t *testing.T) { }) } } + +func TestRejectedIdentifier(t *testing.T) { + t.Parallel() + + // When a single malformed name is provided, we correctly reject it. + domains := []string{ + "яџ–Х6яяdь}", + } + _, err := authAndIssue(nil, nil, domains, true) + test.AssertError(t, err, "issuance should fail for one malformed name") + var prob acme.Problem + test.AssertErrorWraps(t, err, &prob) + test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:rejectedIdentifier") + test.AssertContains(t, prob.Detail, "Domain name contains an invalid character") + + // When multiple malformed names are provided, we correctly reject all of + // them and reflect this in suberrors. This test ensures that the way we + // encode these errors across the gRPC boundary is resilient to non-ascii + // characters. + domains = []string{ + "˜o-", + "ш№Ў", + "р±y", + "яџ–Х6яя", + "яџ–Х6яя`ь", + } + _, err = authAndIssue(nil, nil, domains, true) + test.AssertError(t, err, "issuance should fail for multiple malformed names") + test.AssertErrorWraps(t, err, &prob) + test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:rejectedIdentifier") + test.AssertContains(t, prob.Detail, "Domain name contains an invalid character") + test.AssertContains(t, prob.Detail, "and 4 more problems") +}