diff --git a/probs/probs.go b/probs/probs.go index ae225e1e2..7ff35ca61 100644 --- a/probs/probs.go +++ b/probs/probs.go @@ -4,6 +4,8 @@ import ( "fmt" "net/http" + "github.com/go-jose/go-jose/v4" + "github.com/letsencrypt/boulder/identifier" ) @@ -60,6 +62,10 @@ type ProblemDetails struct { // SubProblems are optional additional per-identifier problems. See // RFC 8555 Section 6.7.1: https://tools.ietf.org/html/rfc8555#section-6.7.1 SubProblems []SubProblemDetails `json:"subproblems,omitempty"` + // Algorithms is an extension field defined only for problem documents of type + // badSignatureAlgorithm. See RFC 8555, Section 6.2: + // https://datatracker.ietf.org/doc/html/rfc8555#section-6.2 + Algorithms []jose.SignatureAlgorithm `json:"algorithms,omitempty"` } // SubProblemDetails represents sub-problems specific to an identifier that are diff --git a/test/integration/errors_test.go b/test/integration/errors_test.go index c00784632..9eaf505e2 100644 --- a/test/integration/errors_test.go +++ b/test/integration/errors_test.go @@ -3,11 +3,19 @@ package integration import ( + "bytes" + "crypto" + "crypto/rand" + "encoding/base64" + "encoding/json" "fmt" + "io" + "net/http" "strings" "testing" "github.com/eggsampler/acme/v3" + "github.com/go-jose/go-jose/v4" "github.com/letsencrypt/boulder/test" ) @@ -183,3 +191,89 @@ func TestRejectedIdentifier(t *testing.T) { test.AssertContains(t, prob.Detail, "Domain name contains an invalid character") test.AssertContains(t, prob.Detail, "and 4 more problems") } + +// TestBadSignatureAlgorithm tests that supplying an unacceptable value for the +// "alg" field of the JWS Protected Header results in a problem document with +// the set of acceptable "alg" values listed in a custom extension field named +// "algorithms". Creating a request with an unacceptable "alg" field requires +// us to do some shenanigans. +func TestBadSignatureAlgorithm(t *testing.T) { + t.Parallel() + + client, err := makeClient() + if err != nil { + t.Fatal("creating test client") + } + + header, err := json.Marshal(&struct { + Alg string `json:"alg"` + KID string `json:"kid"` + Nonce string `json:"nonce"` + URL string `json:"url"` + }{ + Alg: string(jose.RS512), // This is the important bit; RS512 is unacceptable. + KID: client.Account.URL, + Nonce: "deadbeef", // This nonce would fail, but that check comes after the alg check. + URL: client.Directory().NewAccount, + }) + if err != nil { + t.Fatalf("creating JWS protected header: %s", err) + } + protected := base64.RawURLEncoding.EncodeToString(header) + + payload := base64.RawURLEncoding.EncodeToString([]byte(`{"onlyReturnExisting": true}`)) + hash := crypto.SHA512.New() + hash.Write([]byte(protected + "." + payload)) + sig, err := client.Account.PrivateKey.Sign(rand.Reader, hash.Sum(nil), crypto.SHA512) + if err != nil { + t.Fatalf("creating fake signature: %s", err) + } + + data, err := json.Marshal(&struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Signature string `json:"signature"` + }{ + Protected: protected, + Payload: payload, + Signature: base64.RawURLEncoding.EncodeToString(sig), + }) + + req, err := http.NewRequest(http.MethodPost, client.Directory().NewAccount, bytes.NewReader(data)) + if err != nil { + t.Fatalf("creating HTTP request: %s", err) + } + req.Header.Set("Content-Type", "application/jose+json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("making HTTP request: %s", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("reading HTTP response: %s", err) + } + + var prob struct { + Type string `json:"type"` + Detail string `json:"detail"` + Status int `json:"status"` + Algorithms []jose.SignatureAlgorithm `json:"algorithms"` + } + err = json.Unmarshal(body, &prob) + if err != nil { + t.Fatalf("parsing HTTP response: %s", err) + } + + if prob.Type != "urn:ietf:params:acme:error:badSignatureAlgorithm" { + t.Errorf("problem document has wrong type: want badSignatureAlgorithm, got %s", prob.Type) + } + if prob.Status != http.StatusBadRequest { + t.Errorf("problem document has wrong status: want 400, got %d", prob.Status) + } + if len(prob.Algorithms) == 0 { + t.Error("problem document MUST contain acceptable algorithms, got none") + } +} diff --git a/wfe2/wfe.go b/wfe2/wfe.go index 4326d099a..1eb088972 100644 --- a/wfe2/wfe.go +++ b/wfe2/wfe.go @@ -607,6 +607,10 @@ func (wfe *WebFrontEndImpl) sendError(response http.ResponseWriter, logEvent *we panic(fmt.Sprintf("wfe.sendError got %#v (type %T), but expected ProblemDetails or error", eerr, eerr)) } + if prob.Type == probs.BadSignatureAlgorithmProblem { + prob.Algorithms = getSupportedAlgs() + } + var bErr *berrors.BoulderError if errors.As(ierr, &bErr) { retryAfterSeconds := int(bErr.RetryAfter.Round(time.Second).Seconds())