Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.

## 4.1.0

- Support additional JWT signature algorithms in `JwtSignatureValidator`. In addition to the previously supported `RS256`, tokens signed with the following RSA-based algorithms (RFC 7518 §3.3 / §3.5) can now be validated:
- `RS384`, `RS512` (RSASSA-PKCS1-v1_5 with SHA-384 / SHA-512)
- `PS256`, `PS384`, `PS512` (RSASSA-PSS with SHA-256 / SHA-384 / SHA-512). The corresponding `PSSParameterSpec` is set automatically before signature verification.
- Selection is driven by the JWT header `alg` value. Unknown values continue to be rejected with the existing "is not supported" error.
- Update dependencies:
- Spring Boot: 4.0.6 → 4.1.0
- Spring Framework: 7.0.7 → 7.0.8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,50 @@
*/
package com.sap.cloud.security.token.validation.validators;

import jakarta.annotation.Nullable;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;

/**
* This is represented by "kty" (Key Type) Parameter. <a href=
* "https://www.rfc-editor.org/rfc/rfc7518.html#section-6.1">https://www.rfc-editor.org/rfc/rfc7518.html#section-6.1</a>
* JWT signature algorithms supported during token validation, as specified in
* <a href="https://www.rfc-editor.org/rfc/rfc7518.html#section-3.1">RFC 7518 §3.1</a>.
* <p>
* Each entry carries:
* <ul>
* <li>the {@code kty} JWK key type ({@code RSA} for RS* and PS* families),
* <li>the {@code alg} value as it appears in JWK and JWT headers,
* <li>the corresponding standard JCA signature algorithm name passed to
* {@link java.security.Signature#getInstance(String)},
* <li>an optional {@link AlgorithmParameterSpec} for algorithms that require explicit
* parameters (RSASSA-PSS).
* </ul>
*/
public enum JwtSignatureAlgorithm {
RS256("RSA", "RS256", "SHA256withRSA")/* , ES256("EC", "ES256", "SHA256withECDSA")// Eliptic curve */;

RS256("RSA", "RS256", "SHA256withRSA", null, 0),
RS384("RSA", "RS384", "SHA384withRSA", null, 0),
RS512("RSA", "RS512", "SHA512withRSA", null, 0),

PS256("RSA", "PS256", "RSASSA-PSS", "SHA-256", 32),
PS384("RSA", "PS384", "RSASSA-PSS", "SHA-384", 48),
PS512("RSA", "PS512", "RSASSA-PSS", "SHA-512", 64);

private final String type;
private final String value;
private final String javaSignatureAlgorithm;
@Nullable
private final AlgorithmParameterSpec parameterSpec;

JwtSignatureAlgorithm(String type, String algorithm, String javaSignatureAlgorithm) {
JwtSignatureAlgorithm(String type, String algorithm, String javaSignatureAlgorithm,
@Nullable String pssHashAlgorithm, int pssSaltLength) {
this.type = type;
this.value = algorithm; // jwks, jwt header
this.javaSignatureAlgorithm = javaSignatureAlgorithm;
this.parameterSpec = pssHashAlgorithm == null
? null
: new PSSParameterSpec(pssHashAlgorithm, "MGF1",
new MGF1ParameterSpec(pssHashAlgorithm), pssSaltLength, 1);
}

public String value() {
Expand All @@ -34,6 +63,15 @@ public String type() {
return type;
}

/**
* @return additional JCA parameters required to verify a signature with this algorithm, or
* {@code null} if no parameters are needed (the default for everything except RSASSA-PSS).
*/
@Nullable
public AlgorithmParameterSpec parameterSpec() {
return parameterSpec;
}

public static JwtSignatureAlgorithm fromValue(String value) {
for (JwtSignatureAlgorithm algorithm : values()) {
if (algorithm.value.equals(value)) {
Expand All @@ -43,12 +81,15 @@ public static JwtSignatureAlgorithm fromValue(String value) {
return null;
}

/**
* Returns the default algorithm for the given JWK key type. Used as a fallback when a JWK
* entry omits its {@code alg}. Falls back to {@link #RS256} for {@code RSA} keys; returns
* {@code null} for unknown types.
*/
public static JwtSignatureAlgorithm fromType(String type) {
for (JwtSignatureAlgorithm algorithm : values()) {
if (algorithm.type.equals(type)) {
return algorithm;
}
if ("RSA".equals(type)) {
return RS256;
}
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;

Expand Down Expand Up @@ -102,6 +103,10 @@ protected ValidationResult validateSignature(Token token, PublicKey publicKey, J
String headerAndPayload = tokenSections[0] + "." + tokenSections[1];
String signature = tokenSections[2];
try {
AlgorithmParameterSpec parameterSpec = algorithm.parameterSpec();
if (parameterSpec != null) {
publicSignature.setParameter(parameterSpec);
}
publicSignature.initVerify(publicKey);
publicSignature.update(headerAndPayload.getBytes(UTF_8));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* SPDX-FileCopyrightText: 2018-2023 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors
* <p>
* SPDX-License-Identifier: Apache-2.0
*/
package com.sap.cloud.security.token.validation.validators;

import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
import com.sap.cloud.security.token.Token;
import com.sap.cloud.security.token.validation.ValidationResult;
import com.sap.cloud.security.xsuaa.client.OAuth2ServiceException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.Mockito;

import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Base64;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Verifies that {@link JwtSignatureValidator#validateSignature(Token, PublicKey, JwtSignatureAlgorithm)}
* accepts every algorithm declared in {@link JwtSignatureAlgorithm}. The test builds a real JWT-shaped
* input (header.payload.signature), signs it with a freshly generated key pair using the JCA algorithm
* the enum advertises, and feeds the result through {@code validateSignature}.
*/
class JwtSignatureAlgorithmTest {

@Test
void enum_values_areTheExpectedSet() {
assertThat(JwtSignatureAlgorithm.values())
.extracting(JwtSignatureAlgorithm::value)
.containsExactly("RS256", "RS384", "RS512", "PS256", "PS384", "PS512");
}

@Test
void fromValue_unknown_returnsNull() {
assertThat(JwtSignatureAlgorithm.fromValue("HS256")).isNull();
}

@Test
void fromType_rsa_returnsRs256() {
assertThat(JwtSignatureAlgorithm.fromType("RSA")).isEqualTo(JwtSignatureAlgorithm.RS256);
}

@Test
void fromType_unknown_returnsNull() {
assertThat(JwtSignatureAlgorithm.fromType("EC")).isNull();
assertThat(JwtSignatureAlgorithm.fromType("oct")).isNull();
}

@ParameterizedTest
@EnumSource(JwtSignatureAlgorithm.class)
void validateSignature_acceptsTokenSignedWithAlgorithm(JwtSignatureAlgorithm algorithm) throws Exception {
KeyPair keyPair = generateKeyPair(algorithm);
String signedToken = createSignedToken(algorithm, keyPair);

Token token = Mockito.mock(Token.class);
Mockito.when(token.getTokenValue()).thenReturn(signedToken);

ValidationResult result = new TestValidator(keyPair.getPublic())
.validateSignature(token, keyPair.getPublic(), algorithm);

assertThat(result.isValid())
.as("validation should accept a token signed with %s", algorithm.value())
.isTrue();
}

@ParameterizedTest
@EnumSource(JwtSignatureAlgorithm.class)
void validateSignature_rejectsTamperedToken(JwtSignatureAlgorithm algorithm) throws Exception {
KeyPair keyPair = generateKeyPair(algorithm);
String signedToken = createSignedToken(algorithm, keyPair);
// Flip a byte in the payload section to invalidate the signature
String[] parts = signedToken.split("\\.");
String tamperedPayload = parts[1].substring(0, parts[1].length() - 1)
+ (parts[1].charAt(parts[1].length() - 1) == 'A' ? 'B' : 'A');
String tampered = parts[0] + "." + tamperedPayload + "." + parts[2];

Token token = Mockito.mock(Token.class);
Mockito.when(token.getTokenValue()).thenReturn(tampered);

ValidationResult result = new TestValidator(keyPair.getPublic())
.validateSignature(token, keyPair.getPublic(), algorithm);

assertThat(result.isValid())
.as("tampered token must be rejected for %s", algorithm.value())
.isFalse();
}

private static KeyPair generateKeyPair(JwtSignatureAlgorithm algorithm) throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm.type());
generator.initialize(2048);
return generator.generateKeyPair();
}

private static String createSignedToken(JwtSignatureAlgorithm algorithm, KeyPair keyPair) throws Exception {
String header = base64Url(("{\"alg\":\"" + algorithm.value() + "\",\"typ\":\"JWT\"}")
.getBytes(StandardCharsets.UTF_8));
String payload = base64Url("{\"sub\":\"test\"}".getBytes(StandardCharsets.UTF_8));
String signingInput = header + "." + payload;

Signature signature = Signature.getInstance(algorithm.javaSignature());
AlgorithmParameterSpec parameterSpec = algorithm.parameterSpec();
if (parameterSpec != null) {
signature.setParameter(parameterSpec);
}
signature.initSign(keyPair.getPrivate());
signature.update(signingInput.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = signature.sign();

return signingInput + "." + base64Url(signatureBytes);
}

private static String base64Url(byte[] bytes) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}

/**
* Concrete {@link JwtSignatureValidator} subclass that bypasses the JWKS lookup so we can
* exercise {@code validateSignature} in isolation.
*/
private static final class TestValidator extends JwtSignatureValidator {
private final PublicKey publicKey;

TestValidator(PublicKey publicKey) {
super(Mockito.mock(OAuth2ServiceConfiguration.class),
Mockito.mock(OAuth2TokenKeyServiceWithCache.class),
Mockito.mock(OidcConfigurationServiceWithCache.class));
this.publicKey = publicKey;
}

@Override
protected PublicKey getPublicKey(Token token, JwtSignatureAlgorithm algorithm) throws OAuth2ServiceException {
return publicKey;
}
}
}