From 4be1e3acd29ea00ef85887e875fe8a9410c09442 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 15 May 2026 11:59:33 +0200 Subject: [PATCH 1/2] fix: Always recompute Content-Digest when signing messages with content-digest configured as component --- message_digester.go | 5 ----- signer_test.go | 3 +++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/message_digester.go b/message_digester.go index d6c47d0..069bd15 100644 --- a/message_digester.go +++ b/message_digester.go @@ -20,11 +20,6 @@ type contentDigester struct { } func (c contentDigester) update(msg *Message) error { - if val := msg.Header.Get(headerContentDigest); len(val) != 0 { - // header already present. skipping - return nil - } - body, err := c.readBody(msg.Body) if err != nil { return err diff --git a/signer_test.go b/signer_test.go index 4c41172..9ae8b56 100644 --- a/signer_test.go +++ b/signer_test.go @@ -457,6 +457,7 @@ func TestSignerSign(t *testing.T) { WithTTL(0), WithComponents("@authority", "content-digest", "@query-param;name=\"Pet\""), WithTag("header-example"), + WithContentDigestAlgorithm(Sha512), }, msg: &Message{ Method: http.MethodPost, @@ -498,6 +499,7 @@ func TestSignerSign(t *testing.T) { WithComponents( "date", "@method", "@path", "@query", "@authority", "content-type", "content-digest", "content-length", ), + WithContentDigestAlgorithm(Sha512), }, msg: &Message{ Method: http.MethodPost, @@ -537,6 +539,7 @@ func TestSignerSign(t *testing.T) { WithLabel("sig-b24"), WithTTL(0), WithComponents("@status", "content-type", "content-digest", "content-length"), + WithContentDigestAlgorithm(Sha512), }, msg: &Message{ Method: http.MethodPost, From ea699047af1042c4a465d17d2830f98f25b0d003 Mon Sep 17 00:00:00 2001 From: Dimitrij Drus Date: Fri, 15 May 2026 12:14:56 +0200 Subject: [PATCH 2/2] new tests --- message_digester_test.go | 56 +++++++++++++++++++++++++++ signer_test.go | 84 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/message_digester_test.go b/message_digester_test.go index 09d2c8f..2368b0f 100644 --- a/message_digester_test.go +++ b/message_digester_test.go @@ -258,6 +258,62 @@ func TestContentDigesterUpdate(t *testing.T) { }) }, }, + { + uc: "replaces existing content-digest values", + alg: Sha256, + msg: func() *Message { + req, err := http.NewRequestWithContext( + context.TODO(), + http.MethodPost, + "http://example.com/foo", + strings.NewReader(`{"hello": "world"}`), + ) + require.NoError(t, err) + + req.Header.Add("Content-Digest", "sha-256=:stale-sha256:") + req.Header.Add("Content-Digest", "sha-512=:stale-sha512:") + + return MessageFromRequest(req) + }(), + assert: func(t *testing.T, err error, msg *Message) { + t.Helper() + + require.NoError(t, err) + + require.Equal(t, []string{ + "sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:", + }, msg.Header.Values("Content-Digest")) + }, + }, + { + uc: "replaces existing content-digest values without specified algorithm", + msg: func() *Message { + req, err := http.NewRequestWithContext( + context.TODO(), + http.MethodPost, + "http://example.com/foo", + strings.NewReader(`{"hello": "world"}`), + ) + require.NoError(t, err) + + req.Header.Set("Content-Digest", "sha-256=:stale:") + + return MessageFromRequest(req) + }(), + assert: func(t *testing.T, err error, msg *Message) { + t.Helper() + + require.NoError(t, err) + + values := strings.Split(msg.Header.Get("Content-Digest"), ", ") + assert.ElementsMatch(t, values, []string{ + "sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:", + "sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:", + }) + + assert.NotContains(t, msg.Header.Get("Content-Digest"), "stale") + }, + }, } { t.Run(tc.uc, func(t *testing.T) { cd := &contentDigester{alg: supportedAlgs[tc.alg], algName: tc.alg} diff --git a/signer_test.go b/signer_test.go index 9ae8b56..2cd6c47 100644 --- a/signer_test.go +++ b/signer_test.go @@ -638,6 +638,90 @@ func TestSignerSign(t *testing.T) { assert.Equal(t, "sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:", sig) }, }, + { + uc: "overwrites stale content-digest when signing content-digest component", + key: Key{KeyID: "test-key-rsa-pss", Algorithm: RsaPssSha512, Key: tkRSAPSS}, + opts: []SignerOption{ + WithNonce(NonceGetterFunc(func(_ context.Context) (string, error) { return "", nil })), + WithLabel("sig-content-digest"), + WithTTL(0), + WithComponents("content-digest"), + WithContentDigestAlgorithm(Sha256), + }, + msg: &Message{ + Method: http.MethodPost, + Authority: "example.com", + URL: testURL, + Header: http.Header{ + "Host": []string{"example.com"}, + "Date": []string{"Tue, 20 Apr 2021 02:07:55 GMT"}, + "Content-Type": []string{"application/json"}, + "Content-Digest": []string{"sha-256=:stale:"}, + "Content-Length": []string{"18"}, + }, + IsRequest: true, + Body: func() (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader(`{"hello": "world"}`)), nil + }, + }, + assert: func(t *testing.T, err error, header http.Header) { + t.Helper() + + require.NoError(t, err) + + assert.Equal( + t, + "sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:", + header.Get("Content-Digest"), + ) + + sigInput := header.Get("Signature-Input") + assert.Equal(t, `sig-content-digest=("content-digest");created=1618884473;keyid="test-key-rsa-pss"`, sigInput) + + sig := header.Get("Signature") + assert.True(t, strings.HasPrefix(sig, "sig-content-digest=:")) + }, + }, + { + uc: "preserves existing content-digest when content-digest component is not signed", + key: Key{KeyID: "test-key-rsa-pss", Algorithm: RsaPssSha512, Key: tkRSAPSS}, + opts: []SignerOption{ + WithNonce(NonceGetterFunc(func(_ context.Context) (string, error) { return "", nil })), + WithLabel("sig-no-content-digest"), + WithTTL(0), + WithComponents("date", "@method"), + WithContentDigestAlgorithm(Sha256), + }, + msg: &Message{ + Method: http.MethodPost, + Authority: "example.com", + URL: testURL, + Header: http.Header{ + "Host": []string{"example.com"}, + "Date": []string{"Tue, 20 Apr 2021 02:07:55 GMT"}, + "Content-Type": []string{"application/json"}, + "Content-Digest": []string{"sha-256=:stale:"}, + "Content-Length": []string{"18"}, + }, + IsRequest: true, + Body: func() (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader(`{"hello": "world"}`)), nil + }, + }, + assert: func(t *testing.T, err error, header http.Header) { + t.Helper() + + require.NoError(t, err) + + assert.Equal(t, "sha-256=:stale:", header.Get("Content-Digest")) + + sigInput := header.Get("Signature-Input") + assert.Equal(t, `sig-no-content-digest=("date" "@method");created=1618884473;keyid="test-key-rsa-pss"`, sigInput) + + sig := header.Get("Signature") + assert.True(t, strings.HasPrefix(sig, "sig-no-content-digest=:")) + }, + }, } { t.Run(tc.uc, func(t *testing.T) { s, err := NewSigner(tc.key, tc.opts...)