Include supported algs in badSignatureAlgorithm problem doc (#8170)
Add an "algorithms" field to all problem documents, but tag it so it won't be included in the serialized json unless populated. Populate it only when the problem type is "badSignatureAlgorithm", as specified in RFC 8555 Section 6.2. The resulting problem document looks like this: ```json { "type": "urn:ietf:params:acme:error:badSignatureAlgorithm", "detail": "Unable to validate JWS :: JWS signature header contains unsupported algorithm \"RS512\", expected one of [RS256 ES256 ES384 ES512]", "status": 400, "algorithms": [ "RS256", "ES256", "ES384", "ES512" ] } ``` Fixes https://github.com/letsencrypt/boulder/issues/8155
This commit is contained in:
parent
52615d9060
commit
f86f88d563
|
@ -4,6 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/identifier"
|
"github.com/letsencrypt/boulder/identifier"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -60,6 +62,10 @@ type ProblemDetails struct {
|
||||||
// SubProblems are optional additional per-identifier problems. See
|
// SubProblems are optional additional per-identifier problems. See
|
||||||
// RFC 8555 Section 6.7.1: https://tools.ietf.org/html/rfc8555#section-6.7.1
|
// RFC 8555 Section 6.7.1: https://tools.ietf.org/html/rfc8555#section-6.7.1
|
||||||
SubProblems []SubProblemDetails `json:"subproblems,omitempty"`
|
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
|
// SubProblemDetails represents sub-problems specific to an identifier that are
|
||||||
|
|
|
@ -3,11 +3,19 @@
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/eggsampler/acme/v3"
|
"github.com/eggsampler/acme/v3"
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/test"
|
"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, "Domain name contains an invalid character")
|
||||||
test.AssertContains(t, prob.Detail, "and 4 more problems")
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
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
|
var bErr *berrors.BoulderError
|
||||||
if errors.As(ierr, &bErr) {
|
if errors.As(ierr, &bErr) {
|
||||||
retryAfterSeconds := int(bErr.RetryAfter.Round(time.Second).Seconds())
|
retryAfterSeconds := int(bErr.RetryAfter.Round(time.Second).Seconds())
|
||||||
|
|
Loading…
Reference in New Issue