Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions acme/api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"context"
"crypto/rsa"
"encoding/json"
"errors"
"io"
"net/http"
Expand Down Expand Up @@ -101,6 +102,10 @@ func parseJWS(next nextHTTP) nextHTTP {
render.Error(w, r, acme.WrapErrorISE(err, "failed to read request body"))
return
}
if err := rejectGeneralJWS(body); err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, "failed to parse JWS from request body"))
return
}
jws, err := jose.ParseJWS(string(body))
if err != nil {
render.Error(w, r, acme.WrapError(acme.ErrorMalformedType, err, "failed to parse JWS from request body"))
Expand All @@ -111,6 +116,35 @@ func parseJWS(next nextHTTP) nextHTTP {
}
}

// rejectGeneralJWS enforces RFC 8555 section 6.2: ACME JWS requests MUST use
// the Flattened JSON Serialization. go-jose/v3's ParseSigned accepts both the
// Flattened and the General serialization, so the flattened-only requirement
// has to be checked at the JSON layer before handing off. A General-form body
// carries a top-level "signatures" array (plural); flattened bodies have
// singular "signature" + "protected" alongside "payload". RFC 8555 section 6.2
// also forbids the JWS Unprotected Header, so a top-level "header" member is
// rejected as well.
func rejectGeneralJWS(body []byte) error {
var peek struct {
Signatures json.RawMessage `json:"signatures"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also assert that the message doesn't include the unprotected header

Suggested change
Signatures json.RawMessage `json:"signatures"`
Signatures json.RawMessage `json:"signatures"`
Header json.RawMessage `json:"header"`

Header json.RawMessage `json:"header"`
}
if err := json.Unmarshal(body, &peek); err != nil {
// Not JSON, or malformed. Fall through to ParseJWS so the
// existing ErrorMalformedType with go-jose's message wins.
return nil
}
if len(peek.Signatures) > 0 {
return errors.New("JWS MUST be in the Flattened JSON Serialization (RFC 8555 section 6.2); " +
"General JSON Serialization with a top-level \"signatures\" array is rejected")
}
if len(peek.Header) > 0 {
return errors.New("JWS MUST NOT use the Unprotected Header (RFC 8555 section 6.2); " +
"a top-level \"header\" member is rejected")
}
return nil
}

// validateJWS checks the request body for to verify that it meets ACME
// requirements for a JWS.
//
Expand Down
20 changes: 20 additions & 0 deletions acme/api/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,26 @@ func TestHandler_parseJWS(t *testing.T) {
err: acme.NewError(acme.ErrorMalformedType, "failed to parse JWS from request body: go-jose/go-jose: compact JWS format must have three parts"),
}
},
"fail/general-jws": func(t *testing.T) test {
// General JSON serialization carries a "signatures" array.
// RFC 8555 section 6.2 requires Flattened; reject with
// ErrorMalformedType.
return test{
body: strings.NewReader(`{"payload":"e30","signatures":[{"protected":"e30","signature":"sig"}]}`),
statusCode: 400,
err: acme.NewError(acme.ErrorMalformedType, `failed to parse JWS from request body: JWS MUST be in the Flattened JSON Serialization (RFC 8555 section 6.2); General JSON Serialization with a top-level "signatures" array is rejected`),
}
},
"fail/unprotected-header": func(t *testing.T) test {
// RFC 8555 section 6.2 forbids the JWS Unprotected Header;
// a top-level "header" member must be rejected with
// ErrorMalformedType.
return test{
body: strings.NewReader(`{"payload":"e30","protected":"e30","header":{"kid":"x"},"signature":"sig"}`),
statusCode: 400,
err: acme.NewError(acme.ErrorMalformedType, `failed to parse JWS from request body: JWS MUST NOT use the Unprotected Header (RFC 8555 section 6.2); a top-level "header" member is rejected`),
}
},
"ok": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
Expand Down