Skip to content

Commit def21d8

Browse files
committed
fix(crypto): Avoid using legacy RSA algorithms
Fixes #234. This change moves the default RSA algorithm from RS512 to PS512, thus using RSA-PSS by default rather than "legacy" RSA. It also fixes a bug where the default HMAC algorithm was actually HS256 instead of HS512.
1 parent f9a37dc commit def21d8

7 files changed

Lines changed: 51 additions & 20 deletions

File tree

docs/configuration.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,9 +440,11 @@ The expiration time of the client assertion JWT. Optional. The default is 5 minu
440440

441441
### `rest.auth.oauth2.client-auth.jwt.algorithm`
442442

443-
The signing algorithm to use for the client assertion JWT. Optional. The default is `HS512` if the authentication method is `client_secret_jwt`, or `RS512` if the authentication method is `private_key_jwt`.
443+
The signing algorithm to use for the client assertion JWT. Optional. The default is `HS512` if the authentication method is `client_secret_jwt`, or `PS512` if the authentication method is `private_key_jwt`.
444444

445-
Supported algorithms are: HMAC-SHA for `client_secret_jwt`, and RSA or EC for `private_key_jwt`.
445+
Supported algorithms are: HMAC-SHA for `client_secret_jwt`, and RSA, RSA-PSS, or EC for `private_key_jwt`.
446+
447+
Note: legacy PKCS#1 v1.5 RSA algorithms (`RS256`, `RS384`, `RS512`) are supported but deprecated; prefer the equivalent RSASSA-PSS algorithms (`PS256`, `PS384`, `PS512`).
446448

447449
Algorithm names must match the "alg" Param Value as described in [RFC 7518 Section 3.1](https://datatracker.ietf.org/doc/html/rfc7518#section-3.1).
448450

oauth2/core/src/docs/java/com/dremio/iceberg/authmgr/oauth2/docs/DocumentationGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public class DocumentationGenerator {
6565
refs.put("GrantType#JWT_BEARER", "urn:ietf:params:oauth:grant-type:jwt-bearer");
6666
refs.put("GrantType#TOKEN_EXCHANGE", "urn:ietf:params:oauth:grant-type:token-exchange");
6767
refs.put("JWSAlgorithm#HS512", "HS512");
68-
refs.put("JWSAlgorithm#RS512", "RS512");
68+
refs.put("JWSAlgorithm#PS512", "PS512");
6969
refs.put("ClientAuthenticationMethod#NONE", "none");
7070
refs.put("ClientAuthenticationMethod#CLIENT_SECRET_BASIC", "client_secret_basic");
7171
refs.put("ClientAuthenticationMethod#CLIENT_SECRET_POST", "client_secret_post");

oauth2/core/src/main/java/com/dremio/iceberg/authmgr/oauth2/config/JwtClientAuthConfig.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,15 @@ public interface JwtClientAuthConfig {
8080
/**
8181
* The signing algorithm to use for the client assertion JWT. Optional. The default is {@link
8282
* JWSAlgorithm#HS512} if the authentication method is {@link
83-
* ClientAuthenticationMethod#CLIENT_SECRET_JWT}, or {@link JWSAlgorithm#RS512} if the
83+
* ClientAuthenticationMethod#CLIENT_SECRET_JWT}, or {@link JWSAlgorithm#PS512} if the
8484
* authentication method is {@link ClientAuthenticationMethod#PRIVATE_KEY_JWT}.
8585
*
86-
* <p>Supported algorithms are: HMAC-SHA for {@code client_secret_jwt}, and RSA or EC for {@code
87-
* private_key_jwt}.
86+
* <p>Supported algorithms are: HMAC-SHA for {@code client_secret_jwt}, and RSA, RSA-PSS, or EC
87+
* for {@code private_key_jwt}.
88+
*
89+
* <p>Note: legacy PKCS#1 v1.5 RSA algorithms ({@code RS256}, {@code RS384}, {@code RS512}) are
90+
* supported but deprecated; prefer the equivalent RSASSA-PSS algorithms ({@code PS256}, {@code
91+
* PS384}, {@code PS512}).
8892
*
8993
* <p>Algorithm names must match the "alg" Param Value as described in <a
9094
* href="https://datatracker.ietf.org/doc/html/rfc7518#section-3.1">RFC 7518 Section 3.1</a>.
@@ -136,25 +140,26 @@ public interface JwtClientAuthConfig {
136140
default void validate() {
137141
ConfigValidator validator = new ConfigValidator();
138142
if (getAlgorithm().isPresent()) {
139-
if (JWSAlgorithm.Family.RSA.contains(getAlgorithm().get())
140-
|| JWSAlgorithm.Family.EC.contains(getAlgorithm().get())) {
143+
JWSAlgorithm algorithm = getAlgorithm().get();
144+
if (JWSAlgorithm.Family.RSA.contains(algorithm)
145+
|| JWSAlgorithm.Family.EC.contains(algorithm)) {
141146
validator.check(
142147
getPrivateKey().isPresent(),
143148
List.of(PREFIX + '.' + ALGORITHM, PREFIX + '.' + PRIVATE_KEY),
144149
"client assertion: JWS signing algorithm '%s' requires a private key",
145-
getAlgorithm().get().getName());
146-
} else if (JWSAlgorithm.Family.HMAC_SHA.contains(getAlgorithm().get())) {
150+
algorithm.getName());
151+
} else if (JWSAlgorithm.Family.HMAC_SHA.contains(algorithm)) {
147152
validator.check(
148153
getPrivateKey().isEmpty(),
149154
List.of(PREFIX + '.' + ALGORITHM, PREFIX + '.' + PRIVATE_KEY),
150155
"client assertion: private key must not be set for JWS algorithm '%s'",
151-
getAlgorithm().get().getName());
156+
algorithm.getName());
152157
} else {
153158
validator.check(
154159
false,
155160
PREFIX + '.' + ALGORITHM,
156161
"client assertion: unsupported JWS algorithm '%s', must be one of: %s",
157-
getAlgorithm().get().getName(),
162+
algorithm.getName(),
158163
Stream.of(
159164
JWSAlgorithm.Family.HMAC_SHA.stream(),
160165
JWSAlgorithm.Family.RSA.stream(),
@@ -163,6 +168,7 @@ default void validate() {
163168
.map(JWSAlgorithm::getName)
164169
.collect(Collectors.joining("', '", "'", "'")));
165170
}
171+
validator.checkAlgorithm(algorithm);
166172
}
167173
if (getPrivateKey().isPresent()) {
168174
validator.check(

oauth2/core/src/main/java/com/dremio/iceberg/authmgr/oauth2/config/validator/ConfigValidator.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@
1717

1818
import com.google.errorprone.annotations.FormatMethod;
1919
import com.google.errorprone.annotations.FormatString;
20+
import com.nimbusds.jose.JWSAlgorithm;
2021
import java.net.URI;
2122
import java.util.ArrayList;
2223
import java.util.List;
2324
import java.util.stream.Collectors;
2425
import java.util.stream.Stream;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
2528

2629
public final class ConfigValidator {
2730

31+
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigValidator.class);
32+
2833
private final List<ConfigViolation> violations = new ArrayList<>();
2934

3035
public void check(boolean cond, String offendingKey, String msg) {
@@ -61,6 +66,17 @@ public void checkEndpoint(URI endpoint, String offendingKey, String name) {
6166
check(endpoint.getFragment() == null, offendingKey, name + " must not have a fragment part");
6267
}
6368

69+
public void checkAlgorithm(JWSAlgorithm algorithm) {
70+
if (algorithm.equals(JWSAlgorithm.RS256)
71+
|| algorithm.equals(JWSAlgorithm.RS384)
72+
|| algorithm.equals(JWSAlgorithm.RS512)) {
73+
LOGGER.warn(
74+
"JWS algorithm '{}' uses legacy PKCS#1 v1.5 RSA padding; "
75+
+ "consider using PS256, PS384, or PS512 (RSASSA-PSS) instead",
76+
algorithm.getName());
77+
}
78+
}
79+
6480
public void validate() {
6581
if (!violations.isEmpty()) {
6682
throw new IllegalArgumentException(

oauth2/core/src/main/java/com/dremio/iceberg/authmgr/oauth2/flow/AbstractFlow.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ ClientAuthentication createClientAuthentication() {
199199
} else if (method.equals(CLIENT_SECRET_JWT)) {
200200
JWTAssertionDetails details = createJwtAssertionDetails(tokenEndpoint);
201201
JWSAlgorithm algorithm =
202-
getConfig().getJwtClientAuthConfig().getAlgorithm().orElse(JWSAlgorithm.HS256);
202+
getConfig().getJwtClientAuthConfig().getAlgorithm().orElse(JWSAlgorithm.HS512);
203203
Secret secret = getConfig().getBasicConfig().getClientSecret().orElseThrow();
204204
try {
205205
SignedJWT assertion = JWTAssertionFactory.create(details, algorithm, secret);
@@ -211,7 +211,7 @@ ClientAuthentication createClientAuthentication() {
211211
} else if (method.equals(PRIVATE_KEY_JWT)) {
212212
JWTAssertionDetails details = createJwtAssertionDetails(tokenEndpoint);
213213
JWSAlgorithm algorithm =
214-
getConfig().getJwtClientAuthConfig().getAlgorithm().orElse(JWSAlgorithm.RS256);
214+
getConfig().getJwtClientAuthConfig().getAlgorithm().orElse(JWSAlgorithm.PS512);
215215
Path privateKeyPath = getConfig().getJwtClientAuthConfig().getPrivateKey().orElseThrow();
216216
PrivateKey privateKey = PemReader.getInstance().readPrivateKey(privateKeyPath);
217217
try {

oauth2/core/src/testFixtures/java/com/dremio/iceberg/authmgr/oauth2/test/TestCertificates.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
* Generates test certificates and keystores at runtime. All materials are generated lazily (once
4242
* per JVM) and stored in a temp directory.
4343
*
44-
* <p>The material is divided in 2 main groups: one is based on RSA keys, the second on ECDSA keys.
45-
* Each group exposes: a key pair, a self-signed certificate, PEM-encoded files, and a PKCS#12
44+
* <p>The material is divided in 2 main groups: one is based on RSA-PSS keys, the second on ECDSA
45+
* keys. Each group exposes: a key pair, a self-signed certificate, PEM-encoded files, and a PKCS#12
4646
* keystore containing all the material.
4747
*
4848
* <p>Most of the material (RSA/ECDSA key pairs, PKCS#8 PEM, PKCS#12 keystores) is generated using
@@ -120,7 +120,7 @@ private TestCertificates() {
120120

121121
// Generate RSA material
122122
rsaKeyStoreP12 = baseDir.resolve("keystore.p12");
123-
runKeytool(rsaKeyStoreP12, "RSA", "2048", "SHA256withRSA");
123+
runKeytool(rsaKeyStoreP12, "RSA", "2048", "RSASSA-PSS");
124124
KeyStore rsaKs = loadKeyStore(rsaKeyStoreP12);
125125

126126
rsaCertificate = (X509Certificate) rsaKs.getCertificate(ALIAS);
@@ -364,9 +364,9 @@ private void runKeytool(Path keyStorePath, String keyAlg, String keySpec, String
364364
command.add("BC=ca:true");
365365

366366
Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
367-
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
368367
int exitCode = process.waitFor();
369368
if (exitCode != 0) {
369+
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
370370
throw new RuntimeException("keytool failed (exit code " + exitCode + "): " + output);
371371
}
372372
}

oauth2/core/src/testFixtures/java/com/dremio/iceberg/authmgr/oauth2/test/junit/KeycloakExtension.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import com.dremio.iceberg.authmgr.oauth2.test.TestEnvironmentExtension;
2828
import com.dremio.iceberg.authmgr.oauth2.test.container.KeycloakContainer;
2929
import com.google.common.base.Preconditions;
30-
import com.google.common.base.Strings;
3130
import com.nimbusds.jose.JWSAlgorithm;
3231
import com.nimbusds.jose.JWSHeader;
3332
import com.nimbusds.jose.JWSSigner;
@@ -36,10 +35,12 @@
3635
import com.nimbusds.jwt.JWTClaimsSet;
3736
import com.nimbusds.jwt.SignedJWT;
3837
import java.security.PrivateKey;
38+
import java.security.SecureRandom;
3939
import java.security.interfaces.ECPrivateKey;
4040
import java.security.interfaces.RSAPrivateKey;
4141
import java.time.Duration;
4242
import java.time.Instant;
43+
import java.util.Base64;
4344
import java.util.Date;
4445
import java.util.UUID;
4546
import org.junit.jupiter.api.extension.AfterAllCallback;
@@ -62,7 +63,7 @@ public class KeycloakExtension extends TestEnvironmentExtension
6263

6364
// Client3 is used for client_secret_jwt authentication
6465
public static final String CLIENT_ID3 = "Client3";
65-
public static final String CLIENT_SECRET3 = Strings.repeat("S3CR3T", 10);
66+
public static final String CLIENT_SECRET3 = generateSecret();
6667
public static final String CLIENT_AUTH3 = CLIENT_SECRET_JWT.getValue();
6768

6869
// Client4 is used for private_key_jwt authentication (RSA)
@@ -194,6 +195,12 @@ protected ImmutableTestEnvironment.Builder newTestEnvironmentBuilder(ExtensionCo
194195
.resource(null);
195196
}
196197

198+
private static String generateSecret() {
199+
byte[] raw = new byte[64];
200+
new SecureRandom().nextBytes(raw);
201+
return Base64.getUrlEncoder().withoutPadding().encodeToString(raw);
202+
}
203+
197204
/**
198205
* Creates a JWT bearer assertion signed with the given algorithm and private key, suitable for
199206
* use with the JWT bearer grant against this Keycloak instance.

0 commit comments

Comments
 (0)