This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 71bc5cda37be17b54b234818cbf816586216cb8b
Author: Naveed Khan <[email protected]>
AuthorDate: Thu Oct 29 14:23:07 2026 +0530

    require a signed digest for any http signature message body (#3632)
    
    * require a signed digest for any http signature message body
    
    * only skip the digest requirement for gets and error responses without a 
body
    
    * pass a null body in the default required header not present tests
    
    (cherry picked from commit 23e0bc0c34497473394b9220f77d5ec4def45d85)
---
 .../rs/security/httpsignature/MessageVerifier.java |   8 +-
 .../filters/AbstractSignatureInFilter.java         |  14 +-
 .../httpsignature/MessageVerifierTest.java         |   8 +-
 .../security/httpsignature/SpecExamplesTest.java   | 176 +++++++++++++++++++++
 4 files changed, 193 insertions(+), 13 deletions(-)

diff --git 
a/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/MessageVerifier.java
 
b/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/MessageVerifier.java
index c07381047dd..0402c81602d 100644
--- 
a/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/MessageVerifier.java
+++ 
b/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/MessageVerifier.java
@@ -134,10 +134,12 @@ public class MessageVerifier {
 
         // Now dynamically check to see whether the "digest" header is 
required or not. It's not required
         // for the service case on a GET/HEAD request as there is no request, 
and also not required on
-        // the client side unless it is an OK response that is not 204
+        // the client side unless it is an OK response that is not 204. It's 
still required in these cases
+        // if there is a message body
         Integer status = (Integer)m.get(Message.RESPONSE_CODE);
-        if ((requestor && (status < 200 || status >= 300 || status == 204))
-            || (!requestor && ("GET".equalsIgnoreCase(method) || 
"HEAD".equalsIgnoreCase(method)))) {
+        boolean noBodyExpected = (requestor && (status < 200 || status >= 300 
|| status == 204))
+            || (!requestor && ("GET".equalsIgnoreCase(method) || 
"HEAD".equalsIgnoreCase(method)));
+        if (noBodyExpected && (messageBody == null || messageBody.length == 
0)) {
             signedHeaders.remove("digest");
         }
 
diff --git 
a/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/AbstractSignatureInFilter.java
 
b/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/AbstractSignatureInFilter.java
index 597759eed39..491eaa9543f 100644
--- 
a/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/AbstractSignatureInFilter.java
+++ 
b/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/AbstractSignatureInFilter.java
@@ -70,16 +70,18 @@ abstract class AbstractSignatureInFilter {
             return messageBody;
         }
 
-        // Verify digest if we have the appropriate header and a non-empty 
request body. Note that it is up to
-        // the MessageVerifier configuration, or the 
HTTPSignatureConstants.RSSEC_HTTP_SIGNATURE_IN_HEADERS
-        // configuration to require that the digest is signed (and hence 
present)
-        if (entityStream != null && headers.containsKey("Digest")) {
-            LOG.fine("Digesting message body");
+        // Always read the body so the MessageVerifier can require a signed 
digest when one is present
+        if (entityStream != null) {
             try {
                 messageBody = IOUtils.readBytesFromStream(entityStream);
             } catch (IOException e) {
-                throw new DigestFailureException("failed to validate the 
digest", e);
+                throw new DigestFailureException("failed to read the message 
body", e);
             }
+        }
+
+        // Verify digest if we have the appropriate header
+        if (messageBody != null && headers.containsKey("Digest")) {
+            LOG.fine("Digesting message body");
             DigestVerifier digestVerifier = new DigestVerifier();
 
             try {
diff --git 
a/rt/rs/security/http-signature/src/test/java/org/apache/cxf/rs/security/httpsignature/MessageVerifierTest.java
 
b/rt/rs/security/http-signature/src/test/java/org/apache/cxf/rs/security/httpsignature/MessageVerifierTest.java
index 876df0681ad..002000f5caf 100644
--- 
a/rt/rs/security/http-signature/src/test/java/org/apache/cxf/rs/security/httpsignature/MessageVerifierTest.java
+++ 
b/rt/rs/security/http-signature/src/test/java/org/apache/cxf/rs/security/httpsignature/MessageVerifierTest.java
@@ -230,7 +230,7 @@ public class MessageVerifierTest {
                                                              
Collections.singletonList("test"));
         headerVerifier.setSecurityProvider(new MockSecurityProvider());
         headerVerifier.setAlgorithmProvider(new MockAlgorithmProvider());
-        headerVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), 
MESSAGE_BODY.getBytes());
+        headerVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), 
null);
     }
 
     @Test
@@ -243,7 +243,7 @@ public class MessageVerifierTest {
         MessageVerifier headerVerifier = new MessageVerifier(keyId -> 
keyPair.getPublic());
         headerVerifier.setSecurityProvider(new MockSecurityProvider());
         headerVerifier.setAlgorithmProvider(new MockAlgorithmProvider());
-        headerVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), 
MESSAGE_BODY.getBytes());
+        headerVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), 
null);
     }
 
     @Test
@@ -270,7 +270,7 @@ public class MessageVerifierTest {
                                                              
Collections.singletonList("test"));
         headerVerifier.setSecurityProvider(new MockSecurityProvider());
         headerVerifier.setAlgorithmProvider(new MockAlgorithmProvider());
-        headerVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), 
MESSAGE_BODY.getBytes());
+        headerVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), 
null);
     }
 
     @Test(expected = InvalidDataToVerifySignatureException.class)
@@ -282,7 +282,7 @@ public class MessageVerifierTest {
         MessageVerifier headerVerifier = new MessageVerifier(keyId -> 
keyPair.getPublic());
         headerVerifier.setSecurityProvider(new MockSecurityProvider());
         headerVerifier.setAlgorithmProvider(new MockAlgorithmProvider());
-        headerVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), 
MESSAGE_BODY.getBytes());
+        headerVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), 
null);
     }
 
     private static void createAndAddSignature(Map<String, List<String>> 
headers) throws IOException {
diff --git 
a/rt/rs/security/http-signature/src/test/java/org/apache/cxf/rs/security/httpsignature/SpecExamplesTest.java
 
b/rt/rs/security/http-signature/src/test/java/org/apache/cxf/rs/security/httpsignature/SpecExamplesTest.java
index 8f5bbe898dd..6da1dcb59a4 100644
--- 
a/rt/rs/security/http-signature/src/test/java/org/apache/cxf/rs/security/httpsignature/SpecExamplesTest.java
+++ 
b/rt/rs/security/http-signature/src/test/java/org/apache/cxf/rs/security/httpsignature/SpecExamplesTest.java
@@ -41,8 +41,11 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
 
+import jakarta.ws.rs.BadRequestException;
 import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientResponseContext;
 import jakarta.ws.rs.container.ContainerRequestContext;
 import jakarta.ws.rs.core.MultivaluedHashMap;
 import jakarta.ws.rs.core.MultivaluedMap;
@@ -56,7 +59,9 @@ import org.apache.cxf.phase.Phase;
 import org.apache.cxf.phase.PhaseInterceptor;
 import org.apache.cxf.phase.PhaseInterceptorChain;
 import org.apache.cxf.phase.PhaseManager;
+import org.apache.cxf.rs.security.httpsignature.exception.SignatureException;
 import 
org.apache.cxf.rs.security.httpsignature.filters.CreateSignatureInterceptor;
+import 
org.apache.cxf.rs.security.httpsignature.filters.VerifySignatureClientFilter;
 import org.apache.cxf.rs.security.httpsignature.filters.VerifySignatureFilter;
 import org.apache.cxf.rs.security.httpsignature.provider.KeyProvider;
 import org.apache.cxf.rs.security.httpsignature.provider.MockAlgorithmProvider;
@@ -68,6 +73,8 @@ import org.junit.Test;
 import org.mockito.Mockito;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 /**
  * Some examples from the Appendix C of the spec.
@@ -342,6 +349,175 @@ public class SpecExamplesTest {
         });
     }
 
+    @Test
+    public void filterUnsignedBodyOnDeleteTest() throws IOException {
+        URI uri = URI.create("https://www.example.com/foo/1";);
+        String method = "DELETE";
+
+        // The client signs a DELETE without a body, so no digest is added or 
signed
+        Map<String, List<String>> headers = new HashMap<>();
+        headers.put("Host", Collections.singletonList("example.com"));
+        headers.put("Date", Collections.singletonList("Sun, 05 Jan 2014 
21:31:40 GMT"));
+        MessageSigner messageSigner = new MessageSigner(keyProvider, "Test",
+            Arrays.asList("(request-target)", "host", "date"));
+        messageSigner.sign(headers, "/foo/1", method);
+
+        MultivaluedMap<String, String> requestStringHeaders = new 
MultivaluedHashMap<>();
+        headers.forEach((header, value) -> requestStringHeaders.add(header, 
value.get(0)));
+
+        // A body can not be added to the signed request without a signed 
digest
+        AtomicReference<Exception> result = new AtomicReference<>();
+        doInPhaseInterceptor(new MessageImpl(), () -> {
+            ContainerRequestContext containerRequestContext =
+                getContainerRequestContextMock(uri, method, 
requestStringHeaders);
+            InputStream stream = new ByteArrayInputStream("{\"all\": 
true}".getBytes(StandardCharsets.UTF_8));
+            
Mockito.when(containerRequestContext.getEntityStream()).thenReturn(stream);
+            result.set(runFilter(() -> 
createVerifySignatureFilter().filter(containerRequestContext)));
+        });
+        assertTrue(String.valueOf(result.get()), result.get() instanceof 
BadRequestException);
+
+        // The signed request without a body is still accepted
+        result.set(null);
+        doInPhaseInterceptor(new MessageImpl(), () -> {
+            ContainerRequestContext containerRequestContext =
+                getContainerRequestContextMock(uri, method, 
requestStringHeaders);
+            
Mockito.when(containerRequestContext.getEntityStream()).thenReturn(new 
ByteArrayInputStream(new byte[0]));
+            result.set(runFilter(() -> 
createVerifySignatureFilter().filter(containerRequestContext)));
+        });
+        assertNull(result.get());
+    }
+
+    @Test
+    public void clientFilterUnsignedResponseBodyTest() throws IOException {
+        // The service signs a response without a body, so no digest is added 
or signed
+        Map<String, List<String>> headers = new HashMap<>();
+        headers.put("Date", Collections.singletonList("Sun, 05 Jan 2014 
21:31:40 GMT"));
+        MessageSigner messageSigner = new MessageSigner(keyProvider, "Test");
+        messageSigner.sign(headers, "", "");
+
+        MultivaluedMap<String, String> responseHeaders = new 
MultivaluedHashMap<>();
+        headers.forEach((header, value) -> responseHeaders.add(header, 
value.get(0)));
+
+        // A body can not be added to the signed response without a signed 
digest
+        Message message = new MessageImpl();
+        message.put(Message.REQUESTOR_ROLE, Boolean.TRUE);
+        message.put(Message.RESPONSE_CODE, 200);
+        AtomicReference<Exception> result = new AtomicReference<>();
+        doInPhaseInterceptor(message, () -> {
+            ClientResponseContext responseContext = 
Mockito.mock(ClientResponseContext.class);
+            
Mockito.when(responseContext.getHeaders()).thenReturn(responseHeaders);
+            InputStream stream = new ByteArrayInputStream("{\"all\": 
true}".getBytes(StandardCharsets.UTF_8));
+            Mockito.when(responseContext.getEntityStream()).thenReturn(stream);
+
+            MessageVerifier messageVerifier = new MessageVerifier(keyId -> 
publicKey);
+            messageVerifier.setSecurityProvider(new MockSecurityProvider());
+            messageVerifier.setAlgorithmProvider(new MockAlgorithmProvider());
+            VerifySignatureClientFilter verifySignatureFilter = new 
VerifySignatureClientFilter();
+            verifySignatureFilter.setMessageVerifier(messageVerifier);
+
+            result.set(runFilter(() -> verifySignatureFilter.filter(null, 
responseContext)));
+        });
+        assertTrue(String.valueOf(result.get()), result.get() instanceof 
SignatureException);
+    }
+
+    @Test
+    public void filterUnsignedBodyOnGetTest() throws IOException {
+        URI uri = URI.create("https://www.example.com/foo/1";);
+        String method = "GET";
+
+        // The client signs a GET without a body, so no digest is added or 
signed
+        Map<String, List<String>> headers = new HashMap<>();
+        headers.put("Host", Collections.singletonList("example.com"));
+        headers.put("Date", Collections.singletonList("Sun, 05 Jan 2014 
21:31:40 GMT"));
+        MessageSigner messageSigner = new MessageSigner(keyProvider, "Test",
+            Arrays.asList("(request-target)", "host", "date"));
+        messageSigner.sign(headers, "/foo/1", method);
+
+        MultivaluedMap<String, String> requestStringHeaders = new 
MultivaluedHashMap<>();
+        headers.forEach((header, value) -> requestStringHeaders.add(header, 
value.get(0)));
+
+        // A body can not be added to the signed GET request without a signed 
digest
+        AtomicReference<Exception> result = new AtomicReference<>();
+        doInPhaseInterceptor(new MessageImpl(), () -> {
+            ContainerRequestContext containerRequestContext =
+                getContainerRequestContextMock(uri, method, 
requestStringHeaders);
+            InputStream stream = new ByteArrayInputStream("{\"all\": 
true}".getBytes(StandardCharsets.UTF_8));
+            
Mockito.when(containerRequestContext.getEntityStream()).thenReturn(stream);
+            result.set(runFilter(() -> 
createVerifySignatureFilter().filter(containerRequestContext)));
+        });
+        assertTrue(String.valueOf(result.get()), result.get() instanceof 
BadRequestException);
+
+        // The signed GET request without a body is still accepted
+        result.set(null);
+        doInPhaseInterceptor(new MessageImpl(), () -> {
+            ContainerRequestContext containerRequestContext =
+                getContainerRequestContextMock(uri, method, 
requestStringHeaders);
+            
Mockito.when(containerRequestContext.getEntityStream()).thenReturn(new 
ByteArrayInputStream(new byte[0]));
+            result.set(runFilter(() -> 
createVerifySignatureFilter().filter(containerRequestContext)));
+        });
+        assertNull(result.get());
+    }
+
+    @Test
+    public void clientFilterUnsignedErrorResponseBodyTest() throws IOException 
{
+        // The service signs a response without a body, so no digest is added 
or signed
+        Map<String, List<String>> headers = new HashMap<>();
+        headers.put("Date", Collections.singletonList("Sun, 05 Jan 2014 
21:31:40 GMT"));
+        MessageSigner messageSigner = new MessageSigner(keyProvider, "Test");
+        messageSigner.sign(headers, "", "");
+
+        MultivaluedMap<String, String> responseHeaders = new 
MultivaluedHashMap<>();
+        headers.forEach((header, value) -> responseHeaders.add(header, 
value.get(0)));
+
+        for (int status : new int[] {204, 404}) {
+            // A body can not be added to a signed non-2xx or 204 response 
without a signed digest
+            assertTrue(verifyResponse(responseHeaders, status, "{\"all\": 
true}") instanceof SignatureException);
+
+            // The signed response without a body is still accepted
+            assertNull(verifyResponse(responseHeaders, status, ""));
+        }
+    }
+
+    private Exception verifyResponse(MultivaluedMap<String, String> 
responseHeaders, int status, String body) {
+        Message message = new MessageImpl();
+        message.put(Message.REQUESTOR_ROLE, Boolean.TRUE);
+        message.put(Message.RESPONSE_CODE, status);
+        AtomicReference<Exception> result = new AtomicReference<>();
+        doInPhaseInterceptor(message, () -> {
+            ClientResponseContext responseContext = 
Mockito.mock(ClientResponseContext.class);
+            
Mockito.when(responseContext.getHeaders()).thenReturn(responseHeaders);
+            InputStream stream = new 
ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
+            Mockito.when(responseContext.getEntityStream()).thenReturn(stream);
+
+            MessageVerifier messageVerifier = new MessageVerifier(keyId -> 
publicKey);
+            messageVerifier.setSecurityProvider(new MockSecurityProvider());
+            messageVerifier.setAlgorithmProvider(new MockAlgorithmProvider());
+            VerifySignatureClientFilter verifySignatureFilter = new 
VerifySignatureClientFilter();
+            verifySignatureFilter.setMessageVerifier(messageVerifier);
+
+            result.set(runFilter(() -> verifySignatureFilter.filter(null, 
responseContext)));
+        });
+        return result.get();
+    }
+
+    private VerifySignatureFilter createVerifySignatureFilter() {
+        MessageVerifier messageVerifier = new MessageVerifier(keyId -> 
publicKey);
+        messageVerifier.setSecurityProvider(new MockSecurityProvider());
+        messageVerifier.setAlgorithmProvider(new MockAlgorithmProvider());
+        VerifySignatureFilter verifySignatureFilter = new 
VerifySignatureFilter();
+        verifySignatureFilter.setMessageVerifier(messageVerifier);
+        return verifySignatureFilter;
+    }
+
+    private static Exception runFilter(Runnable r) {
+        try {
+            r.run();
+            return null;
+        } catch (Exception ex) {
+            return ex;
+        }
+    }
+
     private ClientRequestContext getClientRequestContextMock(URI uri, String 
method,
                                                              
MultivaluedMap<String, Object> requestHeaders) {
         ClientRequestContext requestContext = 
Mockito.mock(ClientRequestContext.class);

Reply via email to