This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch 3.4.x-fixes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 926ebafc410de012210123e679b83bf5cfca7b13 Author: eberntsen <ei...@fimbulvinter.com> AuthorDate: Thu May 27 13:34:36 2021 +0200 [CXF-8535] Query missing from signature request-target (#795) * [CXF-8535] Include query in request-target * Fix codestyle issues (cherry picked from commit 504cb84e0bf6043d3b787def4070c0649dd51c32) --- parent/pom.xml | 1 + rt/rs/security/http-signature/pom.xml | 20 ++ .../filters/CreateSignatureInterceptor.java | 14 +- .../filters/VerifySignatureFilter.java | 6 +- .../httpsignature/utils/SignatureHeaderUtils.java | 12 +- .../security/httpsignature/SpecExamplesTest.java | 214 +++++++++++++++++++-- 6 files changed, 242 insertions(+), 25 deletions(-) diff --git a/parent/pom.xml b/parent/pom.xml index df207f8..4e20851 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -181,6 +181,7 @@ <cxf.persistence-api.version>2.2.3</cxf.persistence-api.version> <cxf.plexus-archiver.version>4.2.0</cxf.plexus-archiver.version> <cxf.plexus-utils.version>3.2.0</cxf.plexus-utils.version> + <cxf.powermock.version>2.0.4</cxf.powermock.version> <cxf.reactivestreams.version>1.0.3</cxf.reactivestreams.version> <cxf.reactor.version>3.3.17.RELEASE</cxf.reactor.version> <cxf.rhino.version>1.7R2</cxf.rhino.version> diff --git a/rt/rs/security/http-signature/pom.xml b/rt/rs/security/http-signature/pom.xml index 2c43e28..cb26c5c 100644 --- a/rt/rs/security/http-signature/pom.xml +++ b/rt/rs/security/http-signature/pom.xml @@ -70,6 +70,26 @@ <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxrs</artifactId> </dependency> + + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-module-junit4</artifactId> + <version>${cxf.powermock.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito2</artifactId> + <version>${cxf.powermock.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>${cxf.mockito.version}</version> + <scope>test</scope> + </dependency> + </dependencies> </project> diff --git a/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/CreateSignatureInterceptor.java b/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/CreateSignatureInterceptor.java index ca128f4..103cd72 100644 --- a/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/CreateSignatureInterceptor.java +++ b/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/CreateSignatureInterceptor.java @@ -18,10 +18,8 @@ */ package org.apache.cxf.rs.security.httpsignature.filters; -import java.io.IOException; -import java.io.OutputStream; +import java.io.*; import java.nio.charset.StandardCharsets; - import javax.annotation.Priority; import javax.ws.rs.Priorities; import javax.ws.rs.client.ClientRequestContext; @@ -35,7 +33,6 @@ import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Provider; import javax.ws.rs.ext.WriterInterceptor; import javax.ws.rs.ext.WriterInterceptorContext; - import org.apache.cxf.helpers.IOUtils; import org.apache.cxf.io.CachedOutputStream; import org.apache.cxf.jaxrs.utils.HttpUtils; @@ -85,9 +82,8 @@ public class CreateSignatureInterceptor extends AbstractSignatureOutFilter // Only sign the request if we have no Body. if (requestContext.getEntity() == null) { String method = requestContext.getMethod(); - String path = requestContext.getUri().getPath(); - - performSignature(requestContext.getHeaders(), path, method); + performSignature(requestContext.getHeaders(), + SignatureHeaderUtils.createRequestTarget(requestContext.getUri()), method); } } @@ -108,8 +104,8 @@ public class CreateSignatureInterceptor extends AbstractSignatureOutFilter // We don't pass the HTTP method + URI for the response case if (MessageUtils.isRequestor(m)) { method = HttpUtils.getProtocolHeader(JAXRSUtils.getCurrentMessage(), - Message.HTTP_REQUEST_METHOD, ""); - path = uriInfo.getRequestUri().getPath(); + Message.HTTP_REQUEST_METHOD, ""); + path = SignatureHeaderUtils.createRequestTarget(uriInfo.getRequestUri()); } performSignature(writerInterceptorContext.getHeaders(), path, method); diff --git a/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/VerifySignatureFilter.java b/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/VerifySignatureFilter.java index 5e77e1c..444d019 100644 --- a/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/VerifySignatureFilter.java +++ b/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/filters/VerifySignatureFilter.java @@ -18,14 +18,14 @@ */ package org.apache.cxf.rs.security.httpsignature.filters; -import java.io.ByteArrayInputStream; - +import java.io.*; import javax.annotation.Priority; import javax.ws.rs.BadRequestException; import javax.ws.rs.Priorities; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.ext.Provider; +import org.apache.cxf.rs.security.httpsignature.utils.SignatureHeaderUtils; /** * RS CXF container Filter which verifies the Digest header, and then extracts signature data from the context @@ -43,7 +43,7 @@ public class VerifySignatureFilter extends AbstractSignatureInFilter implements } verifySignature(requestCtx.getHeaders(), - requestCtx.getUriInfo().getAbsolutePath().getPath(), + SignatureHeaderUtils.createRequestTarget(requestCtx.getUriInfo().getAbsolutePath()), requestCtx.getMethod()); } diff --git a/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/utils/SignatureHeaderUtils.java b/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/utils/SignatureHeaderUtils.java index 7c14cd8..4bcc440 100644 --- a/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/utils/SignatureHeaderUtils.java +++ b/rt/rs/security/http-signature/src/main/java/org/apache/cxf/rs/security/httpsignature/utils/SignatureHeaderUtils.java @@ -18,6 +18,7 @@ */ package org.apache.cxf.rs.security.httpsignature.utils; +import java.net.*; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Clock; @@ -26,7 +27,6 @@ import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Stream; - import org.apache.cxf.rs.security.httpsignature.exception.DigestFailureException; public final class SignatureHeaderUtils { @@ -101,4 +101,14 @@ public final class SignatureHeaderUtils { }); } + public static String createRequestTarget(URI uri) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(uri.getPath()); + + if (uri.getRawQuery() != null) { + stringBuilder.append("?"); + stringBuilder.append(uri.getRawQuery()); + } + return stringBuilder.toString(); + } } 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 6d164c2..fc5a445 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 @@ -18,8 +18,9 @@ */ package org.apache.cxf.rs.security.httpsignature; -import java.io.File; -import java.io.IOException; +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; @@ -30,25 +31,35 @@ import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import java.util.*; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.UriInfo; +import org.apache.cxf.message.Message; import org.apache.cxf.message.MessageImpl; +import org.apache.cxf.phase.PhaseInterceptorChain; +import org.apache.cxf.rs.security.httpsignature.filters.CreateSignatureInterceptor; +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; import org.apache.cxf.rs.security.httpsignature.provider.MockSecurityProvider; - import org.junit.BeforeClass; import org.junit.Test; - +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; import static org.junit.Assert.assertEquals; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; /** * Some examples from the Appendix C of the spec. */ +@PrepareForTest(PhaseInterceptorChain.class) +@RunWith(PowerMockRunner.class) public class SpecExamplesTest { private static KeyProvider keyProvider; @@ -56,7 +67,6 @@ public class SpecExamplesTest { @BeforeClass public static void setUp() throws IOException, InvalidKeySpecException { - try { // Load keys String basedir = System.getProperty("basedir"); @@ -108,11 +118,61 @@ public class SpecExamplesTest { } @Test + public void interceptorDefaultTest() { + URI uri = URI.create("https://www.example.com/foo?param=value&pet=dog"); + String method = "POST"; + MessageSigner messageSigner = new MessageSigner(keyProvider, "Test", Collections.singletonList("Date")); + + CreateSignatureInterceptor interceptor = new CreateSignatureInterceptor(); + interceptor.setMessageSigner(messageSigner); + + Map<String, List<String>> headers = createMockHeaders(); + MultivaluedMap<String, Object> requestHeaders = new MultivaluedHashMap<>(); + MultivaluedMap<String, String> requestStringHeaders = new MultivaluedHashMap<>(); + headers.forEach((header, value) -> { + requestHeaders.add(header, value.get(0)); + requestStringHeaders.add(header, value.get(0)); + }); + + mockStatic(PhaseInterceptorChain.class); + Message message = mock(Message.class); + when(PhaseInterceptorChain.getCurrentMessage()).thenReturn(message); + + ClientRequestContext requestContext = getClientRequestContextMock(uri, method, requestHeaders); + + interceptor.filter(requestContext); + + String signatureHeader = (String) requestHeaders.get("Signature").get(0); + requestStringHeaders.add("Signature", signatureHeader); + + String expectedHeader = "keyId=\"Test\",algorithm=\"rsa-sha256\"," + + "signature=\"SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz" + + "6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB" + + "6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM=\""; + + assertEquals(signatureHeader.replaceAll("headers=\"date\",", ""), expectedHeader); + + // Verify that the request signature can be verified by the filter + MessageVerifier messageVerifier = new MessageVerifier(keyId -> publicKey); + messageVerifier.setAddDefaultRequiredHeaders(false); + messageVerifier.setSecurityProvider(new MockSecurityProvider()); + messageVerifier.setAlgorithmProvider(new MockAlgorithmProvider()); + + VerifySignatureFilter verifySignatureFilter = new VerifySignatureFilter(); + verifySignatureFilter.setMessageVerifier(messageVerifier); + + ContainerRequestContext containerRequestContext = + getContainerRequestContextMock(uri, method, requestStringHeaders); + + verifySignatureFilter.filter(containerRequestContext); + } + + @Test public void basicTest() throws IOException { Map<String, List<String>> headers = createMockHeaders(); MessageSigner messageSigner = new MessageSigner(keyProvider, "Test", - Arrays.asList("(request-target)", "host", "Date")); + Arrays.asList("(request-target)", "host", "Date")); messageSigner.sign(headers, "/foo?param=value&pet=dog", "POST"); String signatureHeader = headers.get("Signature").get(0); @@ -132,6 +192,58 @@ public class SpecExamplesTest { } @Test + public void interceptorBasicTest() { + URI uri = URI.create("https://www.example.com/foo?param=value&pet=dog"); + String method = "POST"; + MessageSigner messageSigner = new MessageSigner(keyProvider, "Test", + Arrays.asList("(request-target)", "host", "Date")); + + CreateSignatureInterceptor interceptor = new CreateSignatureInterceptor(); + interceptor.setMessageSigner(messageSigner); + + Map<String, List<String>> headers = createMockHeaders(); + MultivaluedMap<String, Object> requestHeaders = new MultivaluedHashMap<>(); + MultivaluedMap<String, String> requestStringHeaders = new MultivaluedHashMap<>(); + headers.forEach((header, value) -> { + requestHeaders.add(header, value.get(0)); + requestStringHeaders.add(header, value.get(0)); + }); + + mockStatic(PhaseInterceptorChain.class); + Message message = mock(Message.class); + when(PhaseInterceptorChain.getCurrentMessage()).thenReturn(message); + + ClientRequestContext requestContext = getClientRequestContextMock(uri, method, requestHeaders); + + interceptor.filter(requestContext); + + String signatureHeader = (String) requestHeaders.get("Signature").get(0); + requestStringHeaders.add("Signature", signatureHeader); + + String expectedHeader = "keyId=\"Test\",algorithm=\"rsa-sha256\"," + + "headers=\"(request-target) host date\",signature=\"qdx+H7PHHDZgy4" + + "y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn" + + "7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBs" + + "kLu6kd9Fswtemr3lgdDEmn04swr2Os0=\""; + + assertEquals(signatureHeader, expectedHeader); + + // Verify that the request signature can be verified by the filter + MessageVerifier messageVerifier = new MessageVerifier(keyId -> publicKey); + messageVerifier.setAddDefaultRequiredHeaders(false); + messageVerifier.setSecurityProvider(new MockSecurityProvider()); + messageVerifier.setAlgorithmProvider(new MockAlgorithmProvider()); + + VerifySignatureFilter verifySignatureFilter = new VerifySignatureFilter(); + verifySignatureFilter.setMessageVerifier(messageVerifier); + + ContainerRequestContext containerRequestContext = + getContainerRequestContextMock(uri, method, requestStringHeaders); + + verifySignatureFilter.filter(containerRequestContext); + } + + @Test public void allHeadersTest() throws IOException { Map<String, List<String>> headers = createMockHeaders(); @@ -155,6 +267,84 @@ public class SpecExamplesTest { messageVerifier.verifyMessage(headers, "POST", "/foo?param=value&pet=dog", new MessageImpl()); } + @Test + public void interceptorAllHeadersTest() { + URI uri = URI.create("https://www.example.com/foo?param=value&pet=dog"); + String method = "POST"; + MessageSigner messageSigner = new MessageSigner(keyProvider, "Test", + Arrays.asList("(request-target)", "host", "date", + "content-type", "digest", "content-length")); + + CreateSignatureInterceptor interceptor = new CreateSignatureInterceptor(); + + interceptor.setMessageSigner(messageSigner); + + Map<String, List<String>> headers = createMockHeaders(); + MultivaluedMap<String, Object> requestHeaders = new MultivaluedHashMap<>(); + MultivaluedMap<String, String> requestStringHeaders = new MultivaluedHashMap<>(); + headers.forEach((header, value) -> { + requestHeaders.add(header, value.get(0)); + requestStringHeaders.add(header, value.get(0)); + }); + + ClientRequestContext requestContext = getClientRequestContextMock(uri, method, requestHeaders); + + mockStatic(PhaseInterceptorChain.class); + Message message = mock(Message.class); + when(PhaseInterceptorChain.getCurrentMessage()).thenReturn(message); + + interceptor.filter(requestContext); + + String signatureHeader = (String) requestHeaders.get("Signature").get(0); + requestStringHeaders.add("Signature", signatureHeader); + + String expectedHeader = "keyId=\"Test\",algorithm=\"rsa-sha256\"," + + "headers=\"(request-target) host date content-type digest content-length\"," + + "signature=\"vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs" + + "8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZF" + + "ukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE=\""; + + assertEquals(signatureHeader, expectedHeader); + + // Verify that the request signature can be verified by the filter + MessageVerifier messageVerifier = new MessageVerifier(keyId -> publicKey); + messageVerifier.setAddDefaultRequiredHeaders(false); + messageVerifier.setSecurityProvider(new MockSecurityProvider()); + messageVerifier.setAlgorithmProvider(new MockAlgorithmProvider()); + + VerifySignatureFilter verifySignatureFilter = new VerifySignatureFilter(); + verifySignatureFilter.setMessageVerifier(messageVerifier); + + ContainerRequestContext containerRequestContext = + getContainerRequestContextMock(uri, method, requestStringHeaders); + InputStream stream = new ByteArrayInputStream("{\"hello\": \"world\"}".getBytes(StandardCharsets.UTF_8)); + when(containerRequestContext.getEntityStream()).thenReturn(stream); + + verifySignatureFilter.filter(containerRequestContext); + } + + private ClientRequestContext getClientRequestContextMock(URI uri, String method, + MultivaluedMap<String, Object> requestHeaders) { + ClientRequestContext requestContext = mock(ClientRequestContext.class); + when(requestContext.getEntity()).thenReturn(null); + when(requestContext.getMethod()).thenReturn(method); + when(requestContext.getHeaders()).thenReturn(requestHeaders); + when(requestContext.getUri()).thenReturn(uri); + return requestContext; + } + + private ContainerRequestContext getContainerRequestContextMock(URI uri, String method, + MultivaluedMap<String, String> + requestStringHeaders) { + ContainerRequestContext containerRequestContext = mock(ContainerRequestContext.class); + UriInfo uriInfo = mock(UriInfo.class); + when(uriInfo.getAbsolutePath()).thenReturn(uri); + when(containerRequestContext.getUriInfo()).thenReturn(uriInfo); + when(containerRequestContext.getMethod()).thenReturn(method); + when(containerRequestContext.getHeaders()).thenReturn(requestStringHeaders); + return containerRequestContext; + } + private static Map<String, List<String>> createMockHeaders() { Map<String, List<String>> headers = new HashMap<>(); headers.put("Host", Collections.singletonList("example.com"));