Skip to content

Commit 2f8a581

Browse files
authored
fix(crypto): Avoid using legacy RSA algorithms (#235)
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 664646a commit 2f8a581

8 files changed

Lines changed: 60 additions & 29 deletions

File tree

docs/configuration.md

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

437437
### `rest.auth.oauth2.client-auth.jwt.algorithm`
438438

439-
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`.
439+
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`.
440440

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

443445
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).
444446

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: 19 additions & 13 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>.
@@ -137,25 +141,26 @@ public interface JwtClientAuthConfig {
137141
default void validate() {
138142
ConfigValidator validator = new ConfigValidator();
139143
if (getAlgorithm().isPresent()) {
140-
if (JWSAlgorithm.Family.RSA.contains(getAlgorithm().get())
141-
|| JWSAlgorithm.Family.EC.contains(getAlgorithm().get())) {
144+
JWSAlgorithm algorithm = getAlgorithm().get();
145+
if (JWSAlgorithm.Family.RSA.contains(algorithm)
146+
|| JWSAlgorithm.Family.EC.contains(algorithm)) {
142147
validator.check(
143148
getPrivateKey().isPresent(),
144149
List.of(PREFIX + '.' + ALGORITHM, PREFIX + '.' + PRIVATE_KEY),
145-
"client assertion: JWS signing algorithm '%s' requires a private key",
146-
getAlgorithm().get().getName());
147-
} else if (JWSAlgorithm.Family.HMAC_SHA.contains(getAlgorithm().get())) {
150+
"client-auth.jwt: JWS signing algorithm '%s' requires a private key",
151+
algorithm.getName());
152+
} else if (JWSAlgorithm.Family.HMAC_SHA.contains(algorithm)) {
148153
validator.check(
149154
getPrivateKey().isEmpty(),
150155
List.of(PREFIX + '.' + ALGORITHM, PREFIX + '.' + PRIVATE_KEY),
151-
"client assertion: private key must not be set for JWS algorithm '%s'",
152-
getAlgorithm().get().getName());
156+
"client-auth.jwt: private key must not be set for JWS algorithm '%s'",
157+
algorithm.getName());
153158
} else {
154159
validator.check(
155160
false,
156161
PREFIX + '.' + ALGORITHM,
157-
"client assertion: unsupported JWS algorithm '%s', must be one of: %s",
158-
getAlgorithm().get().getName(),
162+
"client-auth.jwt: unsupported JWS algorithm '%s', must be one of: %s",
163+
algorithm.getName(),
159164
Stream.of(
160165
JWSAlgorithm.Family.HMAC_SHA.stream(),
161166
JWSAlgorithm.Family.RSA.stream(),
@@ -164,12 +169,13 @@ default void validate() {
164169
.map(JWSAlgorithm::getName)
165170
.collect(Collectors.joining("', '", "'", "'")));
166171
}
172+
validator.checkAlgorithm(algorithm);
167173
}
168174
if (getPrivateKey().isPresent()) {
169175
validator.check(
170176
Files.isReadable(getPrivateKey().get()),
171177
PREFIX + '.' + PRIVATE_KEY,
172-
"client assertion: private key path '%s' is not a file or is not readable",
178+
"client-auth.jwt: private key path '%s' is not a file or is not readable",
173179
getPrivateKey().get());
174180
}
175181
validator.validate();

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/test/java/com/dremio/iceberg/authmgr/oauth2/config/JwtClientAuthConfigTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ static Stream<Arguments> testValidate() {
6565
Arguments.of(
6666
Map.of(PREFIX + '.' + JwtClientAuthConfig.ALGORITHM, "RS256"),
6767
List.of(
68-
"client assertion: JWS signing algorithm 'RS256' requires a private key "
68+
"client-auth.jwt: JWS signing algorithm 'RS256' requires a private key "
6969
+ "(rest.auth.oauth2.client-auth.jwt.algorithm / rest.auth.oauth2.client-auth.jwt.private-key)")),
7070
Arguments.of(
7171
Map.of(
@@ -74,7 +74,7 @@ static Stream<Arguments> testValidate() {
7474
PREFIX + '.' + JwtClientAuthConfig.PRIVATE_KEY,
7575
tempFile.toString()),
7676
List.of(
77-
"client assertion: private key must not be set for JWS algorithm 'HS256' "
77+
"client-auth.jwt: private key must not be set for JWS algorithm 'HS256' "
7878
+ "(rest.auth.oauth2.client-auth.jwt.algorithm / rest.auth.oauth2.client-auth.jwt.private-key)")),
7979
Arguments.of(
8080
Map.of(
@@ -83,7 +83,7 @@ static Stream<Arguments> testValidate() {
8383
PREFIX + '.' + JwtClientAuthConfig.PRIVATE_KEY,
8484
tempFile.toString()),
8585
List.of(
86-
"client assertion: unsupported JWS algorithm 'RSA_SHA256', must be one of: "
86+
"client-auth.jwt: unsupported JWS algorithm 'RSA_SHA256', must be one of: "
8787
+ "'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES256K', 'ES384', 'ES512' "
8888
+ "(rest.auth.oauth2.client-auth.jwt.algorithm)")),
8989
Arguments.of(
@@ -93,13 +93,13 @@ static Stream<Arguments> testValidate() {
9393
PREFIX + '.' + JwtClientAuthConfig.PRIVATE_KEY,
9494
tempFile.toString()),
9595
List.of(
96-
"client assertion: unsupported JWS algorithm 'EdDSA', must be one of: "
96+
"client-auth.jwt: unsupported JWS algorithm 'EdDSA', must be one of: "
9797
+ "'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES256K', 'ES384', 'ES512' "
9898
+ "(rest.auth.oauth2.client-auth.jwt.algorithm)")),
9999
Arguments.of(
100100
Map.of(PREFIX + '.' + JwtClientAuthConfig.PRIVATE_KEY, "/invalid/path"),
101101
List.of(
102-
"client assertion: private key path '/invalid/path' is not a file or is not readable "
102+
"client-auth.jwt: private key path '/invalid/path' is not a file or is not readable "
103103
+ "(rest.auth.oauth2.client-auth.jwt.private-key)")));
104104
}
105105

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)