33import java .net .URI ;
44import java .nio .charset .Charset ;
55import java .security .*;
6+ import java .security .spec .MGF1ParameterSpec ;
7+ import java .security .spec .PSSParameterSpec ;
68import java .util .Arrays ;
79import java .util .Collections ;
810import java .util .HashMap ;
1921 */
2022public 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