fixup: client: conditional content digest

Only calculates the `Content-Digest` header when a request body is
present.

Should address issues discovered during e2e testing.
This commit is contained in:
elle 2026-04-15 19:07:35 +00:00
parent d32bc37556
commit 8eaf11c8dc
No known key found for this signature in database
5 changed files with 75 additions and 40 deletions

View file

@ -619,7 +619,7 @@ func (c *Client) KeyID() string {
// Create an http POST request with forgejo/gitea specific headers
//
//nolint:dupl
func (c *Client) PostRequest(b []byte, to string) (req *http.Request, err error) {
if req, err = c.newRequest(http.MethodPost, b, to); err != nil {
return nil, err
@ -628,7 +628,13 @@ func (c *Client) PostRequest(b []byte, to string) (req *http.Request, err error)
if c.pubID != "" {
if c.useRFC9421 {
config := httpsign9421.NewSignConfig().SignCreated(true)
fields := httpsign9421.Headers(setting.Federation.PostHeadersRFC9421...)
hasBody := len(b) > 0
sigHeaders := setting.Federation.PostHeadersRFC9421
if hasBody {
sigHeaders = append(sigHeaders, "Content-Digest")
}
fields := httpsign9421.Headers(sigHeaders...)
signers, err := c.SignersRFC9421(config, fields)
if err != nil {
@ -636,11 +642,13 @@ func (c *Client) PostRequest(b []byte, to string) (req *http.Request, err error)
}
req.Header.Set("Created", fmt.Sprintf("%d", time.Now().Unix()))
digest, err := ContentDigest(&req.Body, setting.Federation.DigestAlgorithms)
if err != nil {
return nil, err
if hasBody {
digest, err := ContentDigest(&req.Body, setting.Federation.DigestAlgorithms)
if err != nil {
return nil, err
}
req.Header.Set("Content-Digest", digest)
}
req.Header.Set("Content-Digest", digest)
for i, signer := range signers {
sigName := fmt.Sprintf("sig%d", i+1)
@ -677,7 +685,7 @@ func (c *Client) Get(to string) (resp *http.Response, err error) {
// Create an http GET request with forgejo/gitea specific headers
//
//nolint:dupl
func (c *Client) GetRequest(to string) (req *http.Request, err error) {
if req, err = c.newRequest(http.MethodGet, nil, to); err != nil {
return nil, err
@ -694,11 +702,6 @@ func (c *Client) GetRequest(to string) (req *http.Request, err error) {
}
req.Header.Set("Created", fmt.Sprintf("%d", time.Now().Unix()))
digest, err := ContentDigest(&req.Body, setting.Federation.DigestAlgorithms)
if err != nil {
return nil, err
}
req.Header.Set("Content-Digest", digest)
for i, signer := range signers {
sigName := fmt.Sprintf("sig%d", i+1)
@ -760,19 +763,27 @@ func (c *Client) GetRFC9421() bool {
return c.useRFC9421
}
func (c *Client) SignedHeaders(method string) string {
func (c *Client) SignedHeaders(method string, hasBody bool) string {
var ret string
switch method {
case http.MethodGet:
if c.GetRFC9421() {
ret = fmt.Sprintf(`"%v"`, strings.Join(setting.Federation.GetHeadersRFC9421, `" "`))
headers := setting.Federation.GetHeadersRFC9421
if hasBody {
headers = append(headers, "Content-Digest")
}
ret = fmt.Sprintf(`"%v"`, strings.Join(headers, `" "`))
} else {
ret = strings.Join(setting.Federation.GetHeaders, " ")
}
case http.MethodPost:
if c.GetRFC9421() {
ret = fmt.Sprintf(`"%v"`, strings.Join(setting.Federation.PostHeadersRFC9421, `" "`))
headers := setting.Federation.PostHeadersRFC9421
if hasBody {
headers = append(headers, "Content-Digest")
}
ret = fmt.Sprintf(`"%v"`, strings.Join(headers, `" "`))
} else {
ret = strings.Join(setting.Federation.PostHeaders, " ")
}
@ -806,7 +817,7 @@ type APClient interface {
GetRFC9421() bool
SetRFC9421(use bool)
KeyID() string
SignedHeaders(method string) string
SignedHeaders(method string, hasBody bool) string
}
// contextKey is a value for use with context.WithValue.

View file

@ -329,16 +329,21 @@ func TestActivityPubSignedPostRFC9421(t *testing.T) {
expected := "BODY"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Regexp(t, "^"+strings.ToLower(setting.Federation.DigestAlgorithm), r.Header.Get("Content-Digest"))
hasBody := r.Header.Get("Content-Digest") != ""
if hasBody {
assert.Regexp(t, "^"+strings.ToLower(setting.Federation.DigestAlgorithm), r.Header.Get("Content-Digest"))
}
for _, input := range setting.Federation.PostHeadersRFC9421 {
assert.Contains(t, r.Header.Get("Signature-Input"), strings.ToLower(input))
}
assert.Equal(t, activitypub.ActivityStreamsContentType, r.Header.Get("Content-Type"))
require.NoError(t, activitypub.ValidateContentDigest(r.Header.Get("Content-Digest"), &r.Body, digestAlgs))
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
assert.Equal(t, expected, string(body))
fmt.Fprint(w, expected)
if hasBody {
assert.Equal(t, activitypub.ActivityStreamsContentType, r.Header.Get("Content-Type"))
require.NoError(t, activitypub.ValidateContentDigest(r.Header.Get("Content-Digest"), &r.Body, digestAlgs))
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
assert.Equal(t, expected, string(body))
fmt.Fprint(w, expected)
}
}))
defer srv.Close()
@ -363,16 +368,21 @@ func TestActivityPubSignedGetRFC9421(t *testing.T) {
expected := ""
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Regexp(t, "^"+strings.ToLower(setting.Federation.DigestAlgorithm), r.Header.Get("Content-Digest"))
hasBody := r.Header.Get("Content-Digest") != ""
if hasBody {
assert.Regexp(t, "^"+strings.ToLower(setting.Federation.DigestAlgorithm), r.Header.Get("Content-Digest"))
}
for _, input := range setting.Federation.PostHeadersRFC9421 {
assert.Contains(t, r.Header.Get("Signature-Input"), strings.ToLower(input))
}
assert.Equal(t, activitypub.ActivityStreamsContentType, r.Header.Get("Content-Type"))
require.NoError(t, activitypub.ValidateContentDigest(r.Header.Get("Content-Digest"), &r.Body, digestAlgs))
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
assert.Equal(t, expected, string(body))
fmt.Fprint(w, expected)
if hasBody {
assert.Equal(t, activitypub.ActivityStreamsContentType, r.Header.Get("Content-Type"))
require.NoError(t, activitypub.ValidateContentDigest(r.Header.Get("Content-Digest"), &r.Body, digestAlgs))
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
assert.Equal(t, expected, string(body))
fmt.Fprint(w, expected)
}
}))
defer srv.Close()

View file

@ -63,9 +63,9 @@ var (
DigestAlgorithm: "SHA-256",
DigestAlgorithms: []string{"sha-256", "sha-512"},
GetHeaders: []string{"(request-target)", "Date", "Host"},
GetHeadersRFC9421: []string{"@method", "@target-uri", "Content-Digest", "Created"},
GetHeadersRFC9421: []string{"@method", "@target-uri", "Created"},
PostHeaders: []string{"(request-target)", "Date", "Host", "Digest"},
PostHeadersRFC9421: []string{"@method", "@target-uri", "Content-Digest", "Created"},
PostHeadersRFC9421: []string{"@method", "@target-uri", "Created"},
SignatureEnforced: true,
UseRFC9421: false,
}

View file

@ -57,15 +57,27 @@ func verifyHTTPMessageSignature(ctx app_context.APIContext) (authenticated bool,
if len(sigHeaders) == 0 {
return false, fmt.Errorf("missing signature header")
}
hasBody := r.Header.Get("Content-Digest") != ""
var fields httpsign9421.Fields
var headers []string
switch r.Method {
case http.MethodGet:
fields = httpsign9421.Headers(setting.Federation.GetHeadersRFC9421...)
headers = setting.Federation.GetHeadersRFC9421
if hasBody {
headers = append(headers, "Content-Digest")
}
case http.MethodPost:
fields = httpsign9421.Headers(setting.Federation.PostHeadersRFC9421...)
headers = setting.Federation.PostHeadersRFC9421
if hasBody {
headers = append(headers, "Content-Digest")
}
default:
return false, fmt.Errorf("unsupported request type: %v", r.Method)
}
fields = httpsign9421.Headers(headers...)
sigNames, err := httpsign9421.RequestSignatureNames(r, false)
if err != nil {
return false, err
@ -78,8 +90,10 @@ func verifyHTTPMessageSignature(ctx app_context.APIContext) (authenticated bool,
config := httpsign9421.NewVerifyConfig()
config.SetAllowedAlgs(setting.Federation.SignatureAlgorithmsRFC9421)
if err = activitypub.ValidateContentDigest(r.Header.Get("Content-Digest"), &r.Body, setting.Federation.DigestAlgorithms); err != nil {
return false, fmt.Errorf("invalid HTTP Content-Digest header: %v", err)
if hasBody {
if err = activitypub.ValidateContentDigest(r.Header.Get("Content-Digest"), &r.Body, setting.Federation.DigestAlgorithms); err != nil {
return false, fmt.Errorf("invalid HTTP Content-Digest header: %v", err)
}
}
for _, name := range sigNames {

View file

@ -97,7 +97,7 @@ func TestFederationHttpSigValidation(t *testing.T) {
expKeyID := fmt.Sprintf(`keyId="%v"`, apClient.KeyID())
assert.Contains(t, sig, expKeyID)
expHeaders := fmt.Sprintf(`headers="%v"`, apClient.SignedHeaders(http.MethodGet))
expHeaders := fmt.Sprintf(`headers="%v"`, apClient.SignedHeaders(http.MethodGet, false))
assert.Contains(t, sig, expHeaders)
assert.Contains(t, sig, "signature=")
@ -177,7 +177,7 @@ func TestFederationHttpSigValidation(t *testing.T) {
expKeyID := fmt.Sprintf(`keyid="%v"`, apClient.KeyID())
assert.Contains(t, sigInput, expKeyID)
expHeaders := fmt.Sprintf(`sig1=(%v)`, apClient.SignedHeaders(http.MethodGet))
expHeaders := fmt.Sprintf(`sig1=(%v)`, apClient.SignedHeaders(http.MethodGet, false))
assert.Contains(t, sigInput, expHeaders)
resp, err := apClient.Do(req)
@ -265,7 +265,7 @@ func TestFederationHttpSigValidation(t *testing.T) {
expKeyID := fmt.Sprintf(`keyId="%v"`, followClient.KeyID())
assert.Contains(t, sig, expKeyID)
expHeaders := fmt.Sprintf(`headers="%v"`, followClient.SignedHeaders(http.MethodPost))
expHeaders := fmt.Sprintf(`headers="%v"`, followClient.SignedHeaders(http.MethodPost, true))
assert.Contains(t, sig, expHeaders)
assert.Contains(t, sig, "signature=")
@ -362,7 +362,7 @@ func TestFederationHttpSigValidation(t *testing.T) {
expKeyID := fmt.Sprintf(`keyid="%v"`, followClient.KeyID())
assert.Contains(t, sigInput, expKeyID)
expHeaders := fmt.Sprintf(`sig1=(%v)`, followClient.SignedHeaders(http.MethodPost))
expHeaders := fmt.Sprintf(`sig1=(%v)`, followClient.SignedHeaders(http.MethodPost, true))
assert.Contains(t, sigInput, expHeaders)
resp, err := followClient.Do(req)