Skip to content

Commit 7a31e92

Browse files
author
karen-avetisyan-mc
committed
Subject: Fix RSA signing selection; probe PKCS#1 vs PSS and update JaCoCo for modern JDKs
Body: Implement RSA signing as “try SHA256withRSA, fallback to RSASSA-PSS with explicit PSS params” Add helper to expose which JCA algorithm name is used on the current provider Update unit test accordingly Upgrade jacoco-maven-plugin to 0.8.12 so mvn test works on JDK classfile major 65 (no skip flag)
1 parent 1262bf4 commit 7a31e92

6 files changed

Lines changed: 371 additions & 83 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,26 @@ Charset charset = StandardCharsets.UTF_8;
9090
String authHeader = OAuth.getAuthorizationHeader(uri, method, payload, charset, consumerKey, signingKey);
9191
```
9292

93+
#### RSA-PSS support
94+
95+
This library signs requests using OAuth 1.0a with an RSA + SHA-256 digest.
96+
97+
* When the runtime/provider supports the JCA algorithm `SHA256withRSA`, the library uses it (RSA PKCS#1 v1.5).
98+
In this case, the Authorization header contains `oauth_signature_method="RSA-SHA256"`.
99+
* If `SHA256withRSA` is not usable and RSA-PSS is, the library falls back to the JCA algorithm `RSASSA-PSS` using
100+
`SHA-256 / MGF1(SHA-256) / saltLen=32 / trailerField=1`.
101+
In this case, the Authorization header contains `oauth_signature_method="RSA-PSS"`.
102+
103+
Notes:
104+
* The RSA signature scheme (PKCS#1 v1.5 vs PSS) cannot be inferred from an RSA `PrivateKey`.
105+
The selection is based on provider capabilities.
106+
* If you want to know which JCA algorithm will be used on the current runtime/provider, you can call:
107+
108+
```java
109+
String alg = OAuth.signSignatureBaseStringAlgName("baseString", signingKey, StandardCharsets.UTF_8);
110+
System.out.println(alg); // "SHA256withRSA" or "RSASSA-PSS"
111+
```
112+
93113
### Signing HTTP Client Request Objects <a name="signing-http-client-request-objects"></a>
94114

95115
Alternatively, you can use helper classes for some of the commonly used HTTP clients.

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@
150150
<plugin>
151151
<groupId>org.jacoco</groupId>
152152
<artifactId>jacoco-maven-plugin</artifactId>
153-
<version>0.8.7</version>
153+
<version>0.8.12</version>
154154
<executions>
155155
<execution>
156156
<id>pre-unit-test</id>

src/main/java/com/mastercard/developer/oauth/OAuth.java

Lines changed: 107 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import java.net.URI;
44
import java.nio.charset.Charset;
55
import java.security.*;
6+
import java.security.spec.MGF1ParameterSpec;
7+
import java.security.spec.PSSParameterSpec;
68
import java.util.Arrays;
79
import java.util.Collections;
810
import java.util.HashMap;
@@ -19,16 +21,21 @@
1921
*/
2022
public class OAuth {
2123

22-
private OAuth() {
23-
}
24-
2524
public static final String EMPTY_STRING = "";
2625
public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
27-
26+
private static final String SHA_256_WITH_RSA = "SHA256withRSA";
27+
private static final String RSASSA_PSS = "RSASSA-PSS";
28+
private static final String MGF_1 = "MGF1";
29+
private static final int RSAPSS_SALT_LENGTH = 32;
30+
private static final int TRAILER_FIELD = 1;
2831
private static final Logger LOG = Logger.getLogger(OAuth.class.getName());
2932
private static final String HASH_ALGORITHM = "SHA-256";
3033
private static final int NONCE_LENGTH = 16;
3134
private static final String ALPHA_NUMERIC_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
35+
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
36+
37+
private OAuth() {
38+
}
3239

3340
/**
3441
* Creates a Mastercard API compliant OAuth Authorization header
@@ -42,12 +49,15 @@ private OAuth() {
4249
* @return Valid OAuth1.0a signature with a body hash when payload is present
4350
*/
4451
public static String getAuthorizationHeader(URI uri, String method, String payload, Charset charset, String consumerKey, PrivateKey signingKey) {
52+
if (uri == null || method == null || charset == null || consumerKey == null || signingKey == null) {
53+
throw new IllegalArgumentException("Required parameters (uri, method, charset, consumerKey, signingKey) must not be null");
54+
}
55+
4556
TreeMap<String, List<String>> queryParams = extractQueryParams(uri, charset);
4657

4758
HashMap<String, String> oauthParams = new HashMap<>();
4859
oauthParams.put("oauth_consumer_key", consumerKey);
4960
oauthParams.put("oauth_nonce", getNonce());
50-
oauthParams.put("oauth_signature_method", "RSA-" + HASH_ALGORITHM.replace("-", ""));
5161
oauthParams.put("oauth_timestamp", getTimestamp());
5262
oauthParams.put("oauth_version", "1.0");
5363
oauthParams.put("oauth_body_hash", getBodyHash(payload, charset, HASH_ALGORITHM));
@@ -60,10 +70,7 @@ public static String getAuthorizationHeader(URI uri, String method, String paylo
6070

6171
// Signature base string
6272
String sbs = getSignatureBaseString(method, baseUri, paramString, charset);
63-
64-
// Signature
65-
String signature = signSignatureBaseString(sbs, signingKey, charset);
66-
oauthParams.put("oauth_signature", Util.percentEncode(signature, charset));
73+
String signature = signSignatureBaseString(sbs, signingKey, charset, oauthParams);
6774

6875
return getAuthorizationString(oauthParams);
6976
}
@@ -165,9 +172,8 @@ static String toOauthParamString(SortedMap<String, List<String>> queryParamsMap,
165172
}
166173

167174
// Remove trailing ampersand
168-
int stringLength = oauthParams.length() - 1;
169-
if (oauthParams.charAt(stringLength) == '&') {
170-
oauthParams.deleteCharAt(stringLength);
175+
if (oauthParams.length() > 0 && oauthParams.charAt(oauthParams.length() - 1) == '&') {
176+
oauthParams.deleteCharAt(oauthParams.length() - 1);
171177
}
172178

173179
return oauthParams.toString();
@@ -177,13 +183,12 @@ static String toOauthParamString(SortedMap<String, List<String>> queryParamsMap,
177183
* Generates a random string for replay protection as per
178184
* https://tools.ietf.org/html/rfc5849#section-3.3
179185
*
180-
* @return random string of 16 characters.
186+
* @return Random string of 16 characters
181187
*/
182188
static String getNonce() {
183-
SecureRandom rnd = new SecureRandom();
184189
StringBuilder sb = new StringBuilder(NONCE_LENGTH);
185190
for (int i = 0; i < NONCE_LENGTH; i++) {
186-
sb.append(ALPHA_NUMERIC_CHARS.charAt(rnd.nextInt(ALPHA_NUMERIC_CHARS.length())));
191+
sb.append(ALPHA_NUMERIC_CHARS.charAt(SECURE_RANDOM.nextInt(ALPHA_NUMERIC_CHARS.length())));
187192
}
188193
return sb.toString();
189194
}
@@ -207,8 +212,15 @@ private static String getTimestamp() {
207212
*/
208213
static String getBaseUriString(URI uri) {
209214
// Lowercase scheme and authority
210-
String scheme = uri.getScheme().toLowerCase();
211-
String authority = uri.getAuthority().toLowerCase();
215+
String scheme = uri.getScheme();
216+
String authority = uri.getAuthority();
217+
218+
if (scheme == null || authority == null) {
219+
throw new IllegalArgumentException("URI must have both scheme and authority");
220+
}
221+
222+
scheme = scheme.toLowerCase();
223+
authority = authority.toLowerCase();
212224

213225
// Remove port if it matches the default for scheme
214226
if (("http".equals(scheme) && uri.getPort() == 80)
@@ -233,6 +245,7 @@ static String getBaseUriString(URI uri) {
233245
*
234246
* @param payload Request payload
235247
* @param charset Charset encoding of the request
248+
* @param hashAlg Hash algorithm name (e.g., "SHA-256")
236249
* @return Base64 encoded cryptographic hash of the given payload
237250
*/
238251
static String getBodyHash(String payload, Charset charset, String hashAlg) {
@@ -246,35 +259,96 @@ static String getBodyHash(String payload, Charset charset, String hashAlg) {
246259

247260
digest.reset();
248261
// "If the request does not have an entity body, the hash should be taken over the empty string"
249-
byte[] byteArray = null == payload ? "".getBytes() : payload.getBytes(charset);
262+
byte[] byteArray = (payload == null) ? "".getBytes(charset) : payload.getBytes(charset);
250263
byte[] hash = digest.digest(byteArray);
251264

252265
return Util.b64Encode(hash);
253266
}
254267

255268
/**
256-
* Signs the signature base string using an RSA private key. The methodology is described at
257-
* https://tools.ietf.org/html/rfc5849#section-3.4.3 but Mastercard uses the stronger SHA-256 algorithm
258-
* as a replacement for the described SHA1 which is no longer considered secure.
269+
* Signs the OAuth signature base string using an RSA private key.
259270
*
260-
* @param sbs Signature base string formatted as per https://tools.ietf.org/html/rfc5849#section-3.4.1
261-
* @param signingKey Private key of the RSA key pair that was established with the service provider
262-
* @param charset Charset encoding of the request
263-
* @return RSA signature matching the contents of signature base string
271+
* <p>This method detects which signature algorithm is supported by the current provider:
272+
* <ol>
273+
* <li>Probes if {@code SHA256withRSA} supports PSS parameters (it shouldn't, triggering an exception)</li>
274+
* <li>On expected failure, attempts {@code SHA256withRSA} (RSA PKCS#1 v1.5) signing first</li>
275+
* <li>If {@code SHA256withRSA} fails, falls back to {@code RSASSA-PSS} with parameters:
276+
* SHA-256 / MGF1(SHA-256) / saltLen=32 / trailerField=1</li>
277+
* </ol>
278+
*
279+
* <p>The method sets {@code oauth_signature_method} in the provided {@code oauthParams} map:
280+
* <ul>
281+
* <li>{@code "RSA-SHA256"} when using SHA256withRSA (PKCS#1 v1.5)</li>
282+
* <li>{@code "RSA-PSS"} when using RSASSA-PSS</li>
283+
* </ul>
284+
*
285+
* @param sbs Signature base string formatted as per RFC 5849 section 3.4.1
286+
* @param signingKey Private key of the RSA key pair that was established with the service provider
287+
* @param charset Charset encoding of the request
288+
* @param oauthParams Map of OAuth parameters where {@code oauth_signature_method} will be set based on the algorithm used
289+
* @return Base64-encoded RSA signature of the signature base string
264290
*/
265-
static String signSignatureBaseString(String sbs, PrivateKey signingKey, Charset charset) {
291+
static String signSignatureBaseString(String sbs, PrivateKey signingKey, Charset charset, HashMap<String, String> oauthParams) {
292+
// Probe algorithm support: SHA256withRSA should reject PSS parameters, triggering fallback to standard RSA signing first
293+
String signature = null;
266294
try {
267-
Signature signer = Signature.getInstance("SHA256withRSA");
268-
signer.initSign(signingKey);
269-
byte[] sbsBytes = sbs.getBytes(charset);
270-
signer.update(sbsBytes);
271-
byte[] signatureBytes = signer.sign();
272-
return Util.b64Encode(signatureBytes);
295+
Signature signer = Signature.getInstance(SHA_256_WITH_RSA);
296+
signer.setParameter(new PSSParameterSpec(
297+
HASH_ALGORITHM,
298+
MGF_1,
299+
MGF1ParameterSpec.SHA256,
300+
RSAPSS_SALT_LENGTH,
301+
TRAILER_FIELD));
273302
} catch (GeneralSecurityException e) {
274-
throw new IllegalStateException("Unable to RSA-SHA256 sign the given string with the provided key", e);
303+
try {
304+
signature = doSignSHA256(sbs, signingKey, charset);
305+
oauthParams.put("oauth_signature_method", "RSA-SHA256");
306+
} catch (Exception ex) { // Catch any exception from SHA256withRSA signing and fall back to PSS
307+
signature = doSignWithPss(sbs, signingKey, charset);
308+
oauthParams.put("oauth_signature_method", "RSA-PSS");
309+
}
310+
}
311+
oauthParams.put("oauth_signature", Util.percentEncode(signature, charset));
312+
return signature;
313+
}
314+
315+
static String doSignWithPss(String sbs, PrivateKey signingKey, Charset charset) {
316+
if (signingKey == null) {
317+
throw new IllegalArgumentException("signingKey must not be null");
318+
}
319+
320+
try {
321+
Signature signer = Signature.getInstance(RSASSA_PSS);
322+
signer.setParameter(new PSSParameterSpec(
323+
HASH_ALGORITHM,
324+
MGF_1,
325+
MGF1ParameterSpec.SHA256,
326+
RSAPSS_SALT_LENGTH,
327+
TRAILER_FIELD));
328+
return doSign(sbs, signingKey, charset, signer);
329+
} catch (GeneralSecurityException e) {
330+
throw new IllegalStateException("Unable to sign OAuth signature base string", e);
275331
}
276332
}
277333

334+
static String doSignSHA256(String sbs, PrivateKey signingKey, Charset charset) {
335+
if (signingKey == null) {
336+
throw new IllegalArgumentException("signingKey must not be null");
337+
}
338+
try {
339+
Signature signer = Signature.getInstance(SHA_256_WITH_RSA);
340+
return doSign(sbs, signingKey, charset, signer);
341+
} catch (GeneralSecurityException e) {
342+
throw new IllegalStateException("Unable to sign OAuth signature base string using " + SHA_256_WITH_RSA, e);
343+
}
344+
}
345+
346+
static String doSign(String sbs, PrivateKey signingKey, Charset charset, Signature signer) throws GeneralSecurityException {
347+
signer.initSign(signingKey);
348+
signer.update(sbs.getBytes(charset));
349+
return Util.b64Encode(signer.sign());
350+
}
351+
278352
/**
279353
* Constructs a valid Authorization header as per
280354
* https://tools.ietf.org/html/rfc5849#section-3.5.1

0 commit comments

Comments
 (0)