diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a7b10feda..327c466fe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Change Log All notable changes to this project will be documented in this file. -## 4.1.0 +## 4.0.8 +- Fix IAS proof-token validation regression under Istio / Kyma with `credential-type: X509_GENERATED` + - `SapIdJwtSignatureValidator` was setting the `x-client_cert` request header directly from `X509Certificate#getPEM()`. When the certificate originates from Istio's `x-forwarded-client-cert` (XFCC) header, the PEM includes `-----BEGIN CERTIFICATE-----` / `-----END CERTIFICATE-----` delimiters and CR/LF line breaks + - Since 4.0.0, the token-client HTTP transport uses Java 11's `HttpClient`, which enforces RFC 7230 and rejects any header value containing CR/LF with `IllegalArgumentException` (surfaced as `"Token signature can not be validated because: invalid header value: "`). Every authenticated request behind Istio failed proof-token validation as a result + - The header value is now sanitized to bare base64-encoded DER (PEM delimiters and all whitespace stripped) before being placed on the wire. The IAS JWKS endpoint accepts this form. A new `X509Certificate#getPEMHeaderValue()` accessor keeps the sanitization next to the PEM parsing; `getPEM()`'s existing contract is unchanged + - `JavaHttpClientAdapter` additionally fails fast with a message naming the offending header key (not the value, which may be sensitive) if a header value ever contains CR/LF, turning any future regression from a misleading `"invalid header value: "` into an actionable transport-layer error - Update dependencies: - Spring Boot: 4.0.6 → 4.1.0 - Spring Framework: 7.0.7 → 7.0.8 diff --git a/README.md b/README.md index 029fbf1aa5..c5a24ceaa7 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ If your application uses Spring Boot 3.x and you cannot immediately upgrade to S com.sap.cloud.security resourceserver-security-spring-boot-3-starter - 4.0.7 + 4.0.8 ``` @@ -275,7 +275,7 @@ The SAP Cloud Security Services Integration is published to maven central: https com.sap.cloud.security java-bom - 4.0.7 + 4.0.8 import pom diff --git a/bom/pom.xml b/bom/pom.xml index e7cff585ab..feb73a6f67 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -8,7 +8,7 @@ com.sap.cloud.security java-bom - 4.0.7 + 4.0.8 pom java-bom diff --git a/env/pom.xml b/env/pom.xml index 88127d87ec..2d121d7daa 100644 --- a/env/pom.xml +++ b/env/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 com.sap.cloud.security diff --git a/java-api/README.md b/java-api/README.md index 938287a1e3..138ea9e2d8 100644 --- a/java-api/README.md +++ b/java-api/README.md @@ -5,6 +5,6 @@ com.sap.cloud.security java-api - 4.0.7 + 4.0.8 ``` diff --git a/java-api/pom.xml b/java-api/pom.xml index c384dea075..98b4e58d9b 100644 --- a/java-api/pom.xml +++ b/java-api/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 com.sap.cloud.security diff --git a/java-security-test/README.md b/java-security-test/README.md index a07de8d373..7ae27730b9 100644 --- a/java-security-test/README.md +++ b/java-security-test/README.md @@ -40,7 +40,7 @@ It is pre-configured with a security filter that only accepts valid tokens. Furt com.sap.cloud.security java-security-test - 4.0.7 + 4.0.8 test ``` diff --git a/java-security-test/pom.xml b/java-security-test/pom.xml index daae4130b0..8ed36cb6dd 100644 --- a/java-security-test/pom.xml +++ b/java-security-test/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 com.sap.cloud.security diff --git a/java-security/README.md b/java-security/README.md index dba909ceaa..6d573483e4 100644 --- a/java-security/README.md +++ b/java-security/README.md @@ -69,7 +69,7 @@ Since it requires the Tomcat 10 runtime, it needs to be deployed using the [SAP com.sap.cloud.security java-security - 4.0.7 + 4.0.8 org.apache.httpcomponents diff --git a/java-security/pom.xml b/java-security/pom.xml index 93ee7e8a9b..20a09944ab 100644 --- a/java-security/pom.xml +++ b/java-security/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 com.sap.cloud.security diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidator.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidator.java index abea863516..1855d507f9 100644 --- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidator.java +++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidator.java @@ -79,7 +79,7 @@ protected PublicKey getPublicKey(Token token, JwtSignatureAlgorithm algorithm) t } else { Map cacheKeyParams = new HashMap<>(requestParams); - requestParams.put(HttpHeaders.X_CLIENT_CERT, cert.getPEM()); + requestParams.put(HttpHeaders.X_CLIENT_CERT, cert.getLeafCertificateAsHeaderValue()); cacheKeyParams.put(X509Constants.FWD_CLIENT_CERT_SUB, cert.getSubjectDN()); cacheKey = new OAuth2TokenKeyServiceWithCache.CacheKey(keyParams.keyUri(), cacheKeyParams); diff --git a/java-security/src/main/java/com/sap/cloud/security/x509/X509Certificate.java b/java-security/src/main/java/com/sap/cloud/security/x509/X509Certificate.java index c39e993fb5..18a8c4c767 100644 --- a/java-security/src/main/java/com/sap/cloud/security/x509/X509Certificate.java +++ b/java-security/src/main/java/com/sap/cloud/security/x509/X509Certificate.java @@ -141,4 +141,22 @@ public String getPEM() { return this.pem; } + /** + * @return the leaf certificate as bare base64-encoded DER, safe to use as an HTTP header + * value (RFC 7230 forbids CR/LF in header values). If the PEM contains multiple + * concatenated certificates only the first is kept — consistent with + * {@link #getSubjectDN()} and {@link #getThumbprint()}, which also operate on the first + * (leaf) certificate. + */ + public String getLeafCertificateAsHeaderValue() { + int begin = pem.indexOf(X509Parser.BEGIN_CERTIFICATE); + if (begin < 0) { + return pem.replaceAll("\\s", ""); + } + int bodyStart = begin + X509Parser.BEGIN_CERTIFICATE.length(); + int end = pem.indexOf(X509Parser.END_CERTIFICATE, bodyStart); + int bodyEnd = end > bodyStart ? end : pem.length(); + return pem.substring(bodyStart, bodyEnd).replaceAll("\\s", ""); + } + } diff --git a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidatorTest.java b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidatorTest.java index 480fb5e489..f4653f367e 100644 --- a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidatorTest.java +++ b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/SapIdJwtSignatureValidatorTest.java @@ -167,6 +167,47 @@ public void validate_withEnabledProofTokenCheck_app2service() throws IOException assertTrue(cut.validate(iasToken).isValid()); } + @Test + public void validate_withEnabledProofTokenCheck_multiLinePemCert_headerIsSanitized() throws IOException { + String base64 = IOUtils.resourceToString("/cf-forwarded-client-cert.txt", UTF_8); + String multiLinePem = "-----BEGIN CERTIFICATE-----\n" + + chunk(base64, 64) + + "\n-----END CERTIFICATE-----\n"; + Certificate cert = X509Certificate.newCertificate(multiLinePem); + SecurityContext.setClientCertificate(cert); + + OAuth2TokenKeyService tokenKeyMock = Mockito.mock(OAuth2TokenKeyService.class); + ParamsCapturesClientCert capture = new ParamsCapturesClientCert(); + when(tokenKeyMock + .retrieveTokenKeys(any(), argThat(capture))) + .thenReturn(IOUtils.resourceToString("/iasJsonWebTokenKeys.json", UTF_8)); + + SapIdJwtSignatureValidator cut = new SapIdJwtSignatureValidator( + mockConfiguration, + OAuth2TokenKeyServiceWithCache.getInstance() + .withTokenKeyService(tokenKeyMock), + OidcConfigurationServiceWithCache.getInstance() + .withOidcConfigurationService(oidcConfigServiceMock)); + cut.enableProofTokenValidationCheck(); + assertTrue(cut.validate(iasToken).isValid()); + + assertThat(capture.captured).isNotNull(); + assertThat(capture.captured).doesNotContain("-----BEGIN"); + assertThat(capture.captured).doesNotContain("-----END"); + assertThat(capture.captured).doesNotContain("\n"); + assertThat(capture.captured).doesNotContain("\r"); + assertThat(capture.captured).doesNotContain(" "); + } + + private static String chunk(String s, int width) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i += width) { + if (i > 0) sb.append('\n'); + sb.append(s, i, Math.min(i + width, s.length())); + } + return sb.toString(); + } + @Test public void validate_withEnabledProofTokenCheck_app2app() { Token iasToken = new SapIdToken( @@ -286,4 +327,18 @@ public boolean matches(Map map) { } } + static class ParamsCapturesClientCert implements ArgumentMatcher> { + String captured; + + @Override + public boolean matches(Map map) { + String value = map.get(HttpHeaders.X_CLIENT_CERT); + if (value != null) { + this.captured = value; + return true; + } + return false; + } + } + } diff --git a/java-security/src/test/java/com/sap/cloud/security/x509/X509CertificateTest.java b/java-security/src/test/java/com/sap/cloud/security/x509/X509CertificateTest.java index 245fa4bdc9..146fa273bf 100644 --- a/java-security/src/test/java/com/sap/cloud/security/x509/X509CertificateTest.java +++ b/java-security/src/test/java/com/sap/cloud/security/x509/X509CertificateTest.java @@ -41,6 +41,62 @@ void getPem() { assertThat(cut.getPEM()).isEqualTo(x509_base64); } + @Test + void getLeafCertificateAsHeaderValue_stripsDelimitersAndWhitespace_fromMultilinePEM() { + String pem = "-----BEGIN CERTIFICATE-----\n" + + chunk(x509_base64, 64) + + "\n-----END CERTIFICATE-----\n"; + X509Certificate fromPem = X509Certificate.newCertificate(pem); + + String headerValue = fromPem.getLeafCertificateAsHeaderValue(); + assertThat(headerValue).isEqualTo(x509_base64); + assertThat(headerValue).doesNotContain("-----BEGIN"); + assertThat(headerValue).doesNotContain("-----END"); + assertThat(headerValue).doesNotContain("\n"); + assertThat(headerValue).doesNotContain("\r"); + assertThat(headerValue).doesNotContain(" "); + } + + @Test + void getLeafCertificateAsHeaderValue_fromXfccUrlEncodedPEM() throws Exception { + String pem = "-----BEGIN CERTIFICATE-----\n" + + chunk(x509_base64, 64) + + "\n-----END CERTIFICATE-----\n"; + String xfcc = "Hash=abc;Cert=\"" + + java.net.URLEncoder.encode(pem, java.nio.charset.StandardCharsets.UTF_8) + + "\""; + X509Certificate fromXfcc = X509Certificate.newCertificate(xfcc); + + assertThat(fromXfcc).isNotNull(); + assertThat(fromXfcc.getLeafCertificateAsHeaderValue()).isEqualTo(x509_base64); + } + + @Test + void getLeafCertificateAsHeaderValue_multiCertPEM_keepsOnlyFirstCert() { + String pem = "-----BEGIN CERTIFICATE-----\n" + + chunk(x509_base64, 64) + + "\n-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\nMIIGARBITRARYSECONDCERTDATA==\n-----END CERTIFICATE-----\n"; + X509Certificate cert = X509Certificate.newCertificate(pem); + + assertThat(cert).isNotNull(); + String headerValue = cert.getLeafCertificateAsHeaderValue(); + assertThat(headerValue).isEqualTo(x509_base64); + assertThat(headerValue).doesNotContain("MIIGARBITRARYSECONDCERTDATA"); + assertThat(headerValue).doesNotContain("-----BEGIN"); + assertThat(headerValue).doesNotContain("-----END"); + assertThat(headerValue).doesNotContain("\n"); + } + + private static String chunk(String s, int width) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i += width) { + if (i > 0) sb.append('\n'); + sb.append(s, i, Math.min(i + width, s.length())); + } + return sb.toString(); + } + @Test void getSubjectDN() { assertThat(cut.getSubjectDN()).isEqualTo( diff --git a/pom.xml b/pom.xml index ecee132a45..cd76b85fe7 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 pom parent diff --git a/spring-security-3-starter/pom.xml b/spring-security-3-starter/pom.xml index 53aa007fed..63b9dec74c 100644 --- a/spring-security-3-starter/pom.xml +++ b/spring-security-3-starter/pom.xml @@ -16,7 +16,7 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 com.sap.cloud.security diff --git a/spring-security-3/README.md b/spring-security-3/README.md index 53258457b9..61dc22eaa8 100644 --- a/spring-security-3/README.md +++ b/spring-security-3/README.md @@ -23,7 +23,7 @@ Use the Spring Boot 3.x starter instead: com.sap.cloud.security resourceserver-security-spring-boot-3-starter - 4.0.7 + 4.0.8 ``` diff --git a/spring-security-3/pom.xml b/spring-security-3/pom.xml index f1df8ebe65..259a3ca2ca 100644 --- a/spring-security-3/pom.xml +++ b/spring-security-3/pom.xml @@ -9,14 +9,14 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 com.sap.cloud.security spring-security-3 spring-security-3 jar - 4.0.7 + 4.0.8 https://github.com/SAP/cloud-security-xsuaa-integration Java Spring security library for Spring Boot 3 diff --git a/spring-security-starter/pom.xml b/spring-security-starter/pom.xml index 8d8dbe9311..bc4621fdac 100644 --- a/spring-security-starter/pom.xml +++ b/spring-security-starter/pom.xml @@ -16,7 +16,7 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 com.sap.cloud.security diff --git a/spring-security/README.md b/spring-security/README.md index 43bc8fe495..b8425ea30a 100644 --- a/spring-security/README.md +++ b/spring-security/README.md @@ -69,7 +69,7 @@ These (spring) dependencies need to be provided: com.sap.cloud.security resourceserver-security-spring-boot-starter - 4.0.7 + 4.0.8 ``` diff --git a/spring-security/pom.xml b/spring-security/pom.xml index c25e6445be..7d93cc527d 100644 --- a/spring-security/pom.xml +++ b/spring-security/pom.xml @@ -9,14 +9,14 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 com.sap.cloud.security spring-security spring-security jar - 4.0.7 + 4.0.8 https://github.com/SAP/cloud-security-xsuaa-integration Java Spring security library diff --git a/token-client-spring-3/README.md b/token-client-spring-3/README.md index 609022338f..9cd7568146 100644 --- a/token-client-spring-3/README.md +++ b/token-client-spring-3/README.md @@ -25,7 +25,7 @@ This module is the Spring Boot 3.x compatible version of [token-client-spring](. com.sap.cloud.security.xsuaa token-client-spring-3 - 4.0.7 + 4.0.8 ``` diff --git a/token-client-spring-3/pom.xml b/token-client-spring-3/pom.xml index 2460fd538b..ac1e3116c7 100644 --- a/token-client-spring-3/pom.xml +++ b/token-client-spring-3/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 token-client-spring-3 diff --git a/token-client-spring/README.md b/token-client-spring/README.md index 9db51d8c4a..deb832db7d 100644 --- a/token-client-spring/README.md +++ b/token-client-spring/README.md @@ -27,7 +27,7 @@ Starting with version 4.0.0, Spring-specific implementations have been moved to com.sap.cloud.security.xsuaa token-client-spring - 4.0.7 + 4.0.8 ``` @@ -65,7 +65,7 @@ If you were using `XsuaaOAuth2TokenService`, `SpringOAuth2TokenKeyService`, or ` com.sap.cloud.security.xsuaa token-client-spring - 4.0.7 + 4.0.8 ``` diff --git a/token-client-spring/pom.xml b/token-client-spring/pom.xml index e68bdfddfc..3a89f8e72b 100644 --- a/token-client-spring/pom.xml +++ b/token-client-spring/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 token-client-spring diff --git a/token-client/README.md b/token-client/README.md index 994abcb7cb..42c7b9252e 100644 --- a/token-client/README.md +++ b/token-client/README.md @@ -33,7 +33,7 @@ Additionally, it offers an API with the [XsuaaTokenFlows](./src/main/java/com/sa com.sap.cloud.security.xsuaa token-client-spring - 4.0.7 + 4.0.8 ``` @@ -72,7 +72,7 @@ In context of a Spring Boot application you can leverage autoconfiguration provi com.sap.cloud.security resourceserver-security-spring-boot-starter - 4.0.7 + 4.0.8 ``` In context of Spring Applications you will need the following dependencies: @@ -80,7 +80,7 @@ In context of Spring Applications you will need the following dependencies: com.sap.cloud.security.xsuaa token-client - 4.0.7 + 4.0.8 @@ -181,7 +181,7 @@ See the [OAuth2ServiceConfiguration](#oauth2serviceconfiguration) section and [H com.sap.cloud.security.xsuaa token-client - 4.0.7 + 4.0.8 ``` diff --git a/token-client/pom.xml b/token-client/pom.xml index 25e092cc1e..0eb381260e 100644 --- a/token-client/pom.xml +++ b/token-client/pom.xml @@ -9,7 +9,7 @@ com.sap.cloud.security.xsuaa parent - 4.0.7 + 4.0.8 token-client diff --git a/token-client/src/main/java/com/sap/cloud/security/client/JavaHttpClientAdapter.java b/token-client/src/main/java/com/sap/cloud/security/client/JavaHttpClientAdapter.java index 6776d5d867..8c7565e6b6 100644 --- a/token-client/src/main/java/com/sap/cloud/security/client/JavaHttpClientAdapter.java +++ b/token-client/src/main/java/com/sap/cloud/security/client/JavaHttpClientAdapter.java @@ -37,7 +37,13 @@ public SecurityHttpResponse execute(SecurityHttpRequest request) throws IOExcept .timeout(Duration.ofSeconds(socketTimeoutSeconds)); // Add headers - request.getHeaders().forEach(builder::header); + request.getHeaders().forEach((name, value) -> { + if (value != null && (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0)) { + throw new IllegalArgumentException( + "Header value for '" + name + "' contains illegal CR/LF characters."); + } + builder.header(name, value); + }); // Set method and body if (request.getBody() != null && request.getBody().length > 0) { diff --git a/token-client/src/test/java/com/sap/cloud/security/client/JavaHttpClientAdapterTest.java b/token-client/src/test/java/com/sap/cloud/security/client/JavaHttpClientAdapterTest.java new file mode 100644 index 0000000000..2814aecf28 --- /dev/null +++ b/token-client/src/test/java/com/sap/cloud/security/client/JavaHttpClientAdapterTest.java @@ -0,0 +1,43 @@ +/** + * SPDX-FileCopyrightText: 2018-2023 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors + *

+ * SPDX-License-Identifier: Apache-2.0 + */ +package com.sap.cloud.security.client; + +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.net.http.HttpClient; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JavaHttpClientAdapterTest { + + private final JavaHttpClientAdapter cut = new JavaHttpClientAdapter(HttpClient.newHttpClient(), 30); + + @Test + void execute_rejectsHeaderValueWithLineFeed() { + SecurityHttpRequest request = SecurityHttpRequest.newBuilder() + .uri(URI.create("https://example.com/jwks")) + .header("x-client_cert", "abc\ndef") + .build(); + + assertThatThrownBy(() -> cut.execute(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("x-client_cert") + .hasMessageContaining("CR/LF"); + } + + @Test + void execute_rejectsHeaderValueWithCarriageReturn() { + SecurityHttpRequest request = SecurityHttpRequest.newBuilder() + .uri(URI.create("https://example.com/jwks")) + .header("x-client_cert", "abc\rdef") + .build(); + + assertThatThrownBy(() -> cut.execute(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("x-client_cert"); + } +} \ No newline at end of file