diff --git a/README.md b/README.md index 23894d54..6a7777d9 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,17 @@ This library is supported for Java LTS versions 8, 11, 17 and 21. For issues on | RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 | | RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 | | RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 | +| PS256 | RSA256PSS | RSASSA-PSS with SHA-256 | +| PS384 | RSA384PSS | RSASSA-PSS with SHA-384 | +| PS512 | RSA512PSS | RSASSA-PSS with SHA-512 | | ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 | | ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 | | ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 | > Note - Support for ECDSA with curve secp256k1 and SHA-256 (ES256K) has been dropped since it has been [disabled in Java 15](https://www.oracle.com/java/technologies/javase/15-relnote-issues.html#JDK-8237219) +> Note - The RSASSA-PSS algorithms (PS256, PS384, PS512) rely on the JVM for RSASSA-PSS support, which is available natively from Java 11 onwards via the `SunRsaSign` provider ([JDK-8146293](https://bugs.openjdk.org/browse/JDK-8146293)). On Java 8, a security provider that implements `RSASSA-PSS` (such as [BouncyCastle](https://www.bouncycastle.org/)) must be registered on the classpath; no additional provider is bundled by this library. + > :warning: **Important security note:** JVM has a critical vulnerability for ECDSA Algorithms - [CVE-2022-21449](https://nvd.nist.gov/vuln/detail/CVE-2022-21449). Please review the details of the vulnerability and update your environment. ### Installation diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java index 248af7c5..dce39c37 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java @@ -7,6 +7,8 @@ import com.auth0.jwt.interfaces.RSAKeyProvider; import java.security.interfaces.*; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; /** * The Algorithm class represents an algorithm to be used in the Signing or Verification process of a Token. @@ -127,6 +129,132 @@ public static Algorithm RSA512(RSAKey key) throws IllegalArgumentException { return RSA512(publicKey, privateKey); } + /** + * Creates a new Algorithm instance using RSASSA-PSS with SHA-256. Tokens specify this as "PS256". + *
+ * On Java 8 runtimes the built-in providers do not implement RSASSA-PSS; a provider that supports it + * (such as BouncyCastle) must be registered on the classpath. Java 11 and above support it natively. + * + * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance. + * @return a valid RSASSA-PSS 256 Algorithm. + * @throws IllegalArgumentException if the Key Provider is null. + */ + public static Algorithm RSA256PSS(RSAKeyProvider keyProvider) throws IllegalArgumentException { + return new RSAAlgorithm("PS256", "RSASSA-PSS", pssParams("SHA-256", MGF1ParameterSpec.SHA256, 32), keyProvider); + } + + /** + * Creates a new Algorithm instance using RSASSA-PSS with SHA-256. Tokens specify this as "PS256". + * + * @param publicKey the key to use in the verify instance. + * @param privateKey the key to use in the signing instance. + * @return a valid RSASSA-PSS 256 Algorithm. + * @throws IllegalArgumentException if both provided Keys are null. + */ + public static Algorithm RSA256PSS(RSAPublicKey publicKey, RSAPrivateKey privateKey) + throws IllegalArgumentException { + return RSA256PSS(RSAAlgorithm.providerForKeys(publicKey, privateKey)); + } + + /** + * Creates a new Algorithm instance using RSASSA-PSS with SHA-256. Tokens specify this as "PS256". + * + * @param key the key to use in the verify or signing instance. + * @return a valid RSASSA-PSS 256 Algorithm. + * @throws IllegalArgumentException if the provided Key is null. + */ + public static Algorithm RSA256PSS(RSAKey key) throws IllegalArgumentException { + RSAPublicKey publicKey = key instanceof RSAPublicKey ? (RSAPublicKey) key : null; + RSAPrivateKey privateKey = key instanceof RSAPrivateKey ? (RSAPrivateKey) key : null; + return RSA256PSS(publicKey, privateKey); + } + + /** + * Creates a new Algorithm instance using RSASSA-PSS with SHA-384. Tokens specify this as "PS384". + *
+ * On Java 8 runtimes the built-in providers do not implement RSASSA-PSS; a provider that supports it + * (such as BouncyCastle) must be registered on the classpath. Java 11 and above support it natively. + * + * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance. + * @return a valid RSASSA-PSS 384 Algorithm. + * @throws IllegalArgumentException if the Key Provider is null. + */ + public static Algorithm RSA384PSS(RSAKeyProvider keyProvider) throws IllegalArgumentException { + return new RSAAlgorithm("PS384", "RSASSA-PSS", pssParams("SHA-384", MGF1ParameterSpec.SHA384, 48), keyProvider); + } + + /** + * Creates a new Algorithm instance using RSASSA-PSS with SHA-384. Tokens specify this as "PS384". + * + * @param publicKey the key to use in the verify instance. + * @param privateKey the key to use in the signing instance. + * @return a valid RSASSA-PSS 384 Algorithm. + * @throws IllegalArgumentException if both provided Keys are null. + */ + public static Algorithm RSA384PSS(RSAPublicKey publicKey, RSAPrivateKey privateKey) + throws IllegalArgumentException { + return RSA384PSS(RSAAlgorithm.providerForKeys(publicKey, privateKey)); + } + + /** + * Creates a new Algorithm instance using RSASSA-PSS with SHA-384. Tokens specify this as "PS384". + * + * @param key the key to use in the verify or signing instance. + * @return a valid RSASSA-PSS 384 Algorithm. + * @throws IllegalArgumentException if the provided Key is null. + */ + public static Algorithm RSA384PSS(RSAKey key) throws IllegalArgumentException { + RSAPublicKey publicKey = key instanceof RSAPublicKey ? (RSAPublicKey) key : null; + RSAPrivateKey privateKey = key instanceof RSAPrivateKey ? (RSAPrivateKey) key : null; + return RSA384PSS(publicKey, privateKey); + } + + /** + * Creates a new Algorithm instance using RSASSA-PSS with SHA-512. Tokens specify this as "PS512". + *
+ * On Java 8 runtimes the built-in providers do not implement RSASSA-PSS; a provider that supports it + * (such as BouncyCastle) must be registered on the classpath. Java 11 and above support it natively. + * + * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance. + * @return a valid RSASSA-PSS 512 Algorithm. + * @throws IllegalArgumentException if the Key Provider is null. + */ + public static Algorithm RSA512PSS(RSAKeyProvider keyProvider) throws IllegalArgumentException { + return new RSAAlgorithm("PS512", "RSASSA-PSS", pssParams("SHA-512", MGF1ParameterSpec.SHA512, 64), keyProvider); + } + + /** + * Creates a new Algorithm instance using RSASSA-PSS with SHA-512. Tokens specify this as "PS512". + * + * @param publicKey the key to use in the verify instance. + * @param privateKey the key to use in the signing instance. + * @return a valid RSASSA-PSS 512 Algorithm. + * @throws IllegalArgumentException if both provided Keys are null. + */ + public static Algorithm RSA512PSS(RSAPublicKey publicKey, RSAPrivateKey privateKey) + throws IllegalArgumentException { + return RSA512PSS(RSAAlgorithm.providerForKeys(publicKey, privateKey)); + } + + /** + * Creates a new Algorithm instance using RSASSA-PSS with SHA-512. Tokens specify this as "PS512". + * + * @param key the key to use in the verify or signing instance. + * @return a valid RSASSA-PSS 512 Algorithm. + * @throws IllegalArgumentException if the provided Key is null. + */ + public static Algorithm RSA512PSS(RSAKey key) throws IllegalArgumentException { + RSAPublicKey publicKey = key instanceof RSAPublicKey ? (RSAPublicKey) key : null; + RSAPrivateKey privateKey = key instanceof RSAPrivateKey ? (RSAPrivateKey) key : null; + return RSA512PSS(publicKey, privateKey); + } + + private static PSSParameterSpec pssParams(String hashAlgorithm, MGF1ParameterSpec mgf1Spec, int saltLength) { + // JWA (RFC 7518 ยง3.5) mandates MGF1 with the same hash as the digest and a salt length equal to + // the hash output size (32/48/64 bytes for SHA-256/384/512). Trailer field is the fixed value 1. + return new PSSParameterSpec(hashAlgorithm, "MGF1", mgf1Spec, saltLength, 1); + } + /** * Creates a new Algorithm instance using HmacSHA256. Tokens specify this as "HS256". * diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java index 7b8c5c2a..ecc945ee 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java @@ -4,6 +4,7 @@ import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.*; +import java.security.spec.AlgorithmParameterSpec; /** * Class used to perform the signature hash calculations. @@ -85,6 +86,31 @@ boolean verifySignatureFor( payload.getBytes(StandardCharsets.UTF_8), signatureBytes); } + /** + * Verify signature for JWT header and payload using a public key and algorithm parameters. + * + * @param algorithm algorithm name. + * @param publicKey the public key to use for verification. + * @param params the algorithm parameters, or null to use the algorithm defaults. + * @param header JWT header. + * @param payload JWT payload. + * @param signatureBytes JWT signature. + * @return true if signature is valid. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + */ + boolean verifySignatureFor( + String algorithm, + PublicKey publicKey, + AlgorithmParameterSpec params, + String header, + String payload, + byte[] signatureBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return verifySignatureFor(algorithm, publicKey, params, header.getBytes(StandardCharsets.UTF_8), + payload.getBytes(StandardCharsets.UTF_8), signatureBytes); + } + /** * Verify signature for JWT header and payload using a public key. * @@ -103,8 +129,33 @@ boolean verifySignatureFor( byte[] headerBytes, byte[] payloadBytes, byte[] signatureBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return verifySignatureFor(algorithm, publicKey, null, headerBytes, payloadBytes, signatureBytes); + } + + /** + * Verify signature for JWT header and payload using a public key and algorithm parameters. + * + * @param algorithm algorithm name. + * @param publicKey the public key to use for verification. + * @param params the algorithm parameters, or null to use the algorithm defaults. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. + * @param signatureBytes JWT signature. + * @return true if signature is valid. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + */ + boolean verifySignatureFor( + String algorithm, + PublicKey publicKey, + AlgorithmParameterSpec params, + byte[] headerBytes, + byte[] payloadBytes, + byte[] signatureBytes ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { final Signature s = Signature.getInstance(algorithm); + setParameterIfPresent(s, params); s.initVerify(publicKey); s.update(headerBytes); s.update(JWT_PART_SEPARATOR); @@ -130,8 +181,33 @@ byte[] createSignatureFor( PrivateKey privateKey, byte[] headerBytes, byte[] payloadBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return createSignatureFor(algorithm, privateKey, null, headerBytes, payloadBytes); + } + + /** + * Create signature for JWT header and payload using a private key and algorithm parameters. + * + * @param algorithm algorithm name. + * @param privateKey the private key to use for signing. + * @param params the algorithm parameters, or null to use the algorithm defaults. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. + * @return the signature bytes. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws SignatureException if this signature object is not initialized properly + * or if this signature algorithm is unable to process the input data provided. + */ + byte[] createSignatureFor( + String algorithm, + PrivateKey privateKey, + AlgorithmParameterSpec params, + byte[] headerBytes, + byte[] payloadBytes ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { final Signature s = Signature.getInstance(algorithm); + setParameterIfPresent(s, params); s.initSign(privateKey); s.update(headerBytes); s.update(JWT_PART_SEPARATOR); @@ -199,10 +275,49 @@ byte[] createSignatureFor( String algorithm, PrivateKey privateKey, byte[] contentBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return createSignatureFor(algorithm, privateKey, (AlgorithmParameterSpec) null, contentBytes); + } + + /** + * Create signature using a private key and algorithm parameters. + * To get the correct JWT Signature, ensure the content is in the format {HEADER}.{PAYLOAD} + * + * @param algorithm algorithm name. + * @param privateKey the private key to use for signing. + * @param params the algorithm parameters, or null to use the algorithm defaults. + * @param contentBytes the content to be signed. + * @return the signature bytes. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws SignatureException if this signature object is not initialized properly + * or if this signature algorithm is unable to process the input data provided. + */ + byte[] createSignatureFor( + String algorithm, + PrivateKey privateKey, + AlgorithmParameterSpec params, + byte[] contentBytes ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { final Signature s = Signature.getInstance(algorithm); + setParameterIfPresent(s, params); s.initSign(privateKey); s.update(contentBytes); return s.sign(); } + + private static void setParameterIfPresent(Signature s, AlgorithmParameterSpec params) + throws NoSuchAlgorithmException { + if (params != null) { + try { + s.setParameter(params); + } catch (InvalidAlgorithmParameterException e) { + // Remapped to NoSuchAlgorithmException to keep the throws clause unchanged. The params + // are library-controlled (see Algorithm#pssParams), so this branch is effectively + // unreachable; it only fires if a provider rejects the fixed PSS spec entirely. + throw new NoSuchAlgorithmException( + "The algorithm parameters are invalid for the signature algorithm.", e); + } + } + } } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java index ca892e60..b7135dc9 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java @@ -10,6 +10,7 @@ import java.security.SignatureException; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.security.spec.AlgorithmParameterSpec; import java.util.Base64; /** @@ -21,20 +22,33 @@ class RSAAlgorithm extends Algorithm { private final RSAKeyProvider keyProvider; private final CryptoHelper crypto; + private final AlgorithmParameterSpec params; //Visible for testing - RSAAlgorithm(CryptoHelper crypto, String id, String algorithm, RSAKeyProvider keyProvider) - throws IllegalArgumentException { + RSAAlgorithm(CryptoHelper crypto, String id, String algorithm, AlgorithmParameterSpec params, + RSAKeyProvider keyProvider) throws IllegalArgumentException { super(id, algorithm); if (keyProvider == null) { throw new IllegalArgumentException("The Key Provider cannot be null."); } this.keyProvider = keyProvider; this.crypto = crypto; + this.params = params; + } + + //Visible for testing + RSAAlgorithm(CryptoHelper crypto, String id, String algorithm, RSAKeyProvider keyProvider) + throws IllegalArgumentException { + this(crypto, id, algorithm, null, keyProvider); + } + + RSAAlgorithm(String id, String algorithm, AlgorithmParameterSpec params, RSAKeyProvider keyProvider) + throws IllegalArgumentException { + this(new CryptoHelper(), id, algorithm, params, keyProvider); } RSAAlgorithm(String id, String algorithm, RSAKeyProvider keyProvider) throws IllegalArgumentException { - this(new CryptoHelper(), id, algorithm, keyProvider); + this(new CryptoHelper(), id, algorithm, null, keyProvider); } @Override @@ -46,7 +60,7 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { throw new IllegalStateException("The given Public Key is null."); } boolean valid = crypto.verifySignatureFor( - getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), signatureBytes); + getDescription(), publicKey, params, jwt.getHeader(), jwt.getPayload(), signatureBytes); if (!valid) { throw new SignatureVerificationException(this); } @@ -63,7 +77,7 @@ public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGene if (privateKey == null) { throw new IllegalStateException("The given Private Key is null."); } - return crypto.createSignatureFor(getDescription(), privateKey, headerBytes, payloadBytes); + return crypto.createSignatureFor(getDescription(), privateKey, params, headerBytes, payloadBytes); } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { throw new SignatureGenerationException(this, e); } @@ -76,7 +90,7 @@ public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { if (privateKey == null) { throw new IllegalStateException("The given Private Key is null."); } - return crypto.createSignatureFor(getDescription(), privateKey, contentBytes); + return crypto.createSignatureFor(getDescription(), privateKey, params, contentBytes); } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { throw new SignatureGenerationException(this, e); } diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java index e09661d3..adda6850 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java @@ -141,6 +141,75 @@ public void shouldThrowRSA512InstanceWithNullKeyProvider() { Algorithm.RSA512(provider); } + @Test + public void shouldThrowRSA256PSSInstanceWithNullKey() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + RSAKey key = null; + Algorithm.RSA256PSS(key); + } + + @Test + public void shouldThrowRSA256PSSInstanceWithNullKeys() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + Algorithm.RSA256PSS(null, null); + } + + @Test + public void shouldThrowRSA256PSSInstanceWithNullKeyProvider() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("The Key Provider cannot be null."); + RSAKeyProvider provider = null; + Algorithm.RSA256PSS(provider); + } + + @Test + public void shouldThrowRSA384PSSInstanceWithNullKey() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + RSAKey key = null; + Algorithm.RSA384PSS(key); + } + + @Test + public void shouldThrowRSA384PSSInstanceWithNullKeys() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + Algorithm.RSA384PSS(null, null); + } + + @Test + public void shouldThrowRSA384PSSInstanceWithNullKeyProvider() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("The Key Provider cannot be null."); + RSAKeyProvider provider = null; + Algorithm.RSA384PSS(provider); + } + + @Test + public void shouldThrowRSA512PSSInstanceWithNullKey() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + RSAKey key = null; + Algorithm.RSA512PSS(key); + } + + @Test + public void shouldThrowRSA512PSSInstanceWithNullKeys() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + Algorithm.RSA512PSS(null, null); + } + + @Test + public void shouldThrowRSA512PSSInstanceWithNullKeyProvider() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("The Key Provider cannot be null."); + RSAKeyProvider provider = null; + Algorithm.RSA512PSS(provider); + } + @Test public void shouldThrowECDSA256InstanceWithNullKey() { exception.expect(IllegalArgumentException.class); @@ -405,6 +474,75 @@ public void shouldCreateRSA512AlgorithmWithProvider() { assertThat(algorithm.getName(), is("RS512")); } + @Test + public void shouldCreateRSA256PSSAlgorithmWithBothKeys() { + RSAPublicKey publicKey = mock(RSAPublicKey.class); + RSAPrivateKey privateKey = mock(RSAPrivateKey.class); + Algorithm algorithm = Algorithm.RSA256PSS(publicKey, privateKey); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("RSASSA-PSS")); + assertThat(algorithm.getName(), is("PS256")); + } + + @Test + public void shouldCreateRSA256PSSAlgorithmWithProvider() { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + Algorithm algorithm = Algorithm.RSA256PSS(provider); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("RSASSA-PSS")); + assertThat(algorithm.getName(), is("PS256")); + } + + @Test + public void shouldCreateRSA384PSSAlgorithmWithBothKeys() { + RSAPublicKey publicKey = mock(RSAPublicKey.class); + RSAPrivateKey privateKey = mock(RSAPrivateKey.class); + Algorithm algorithm = Algorithm.RSA384PSS(publicKey, privateKey); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("RSASSA-PSS")); + assertThat(algorithm.getName(), is("PS384")); + } + + @Test + public void shouldCreateRSA384PSSAlgorithmWithProvider() { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + Algorithm algorithm = Algorithm.RSA384PSS(provider); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("RSASSA-PSS")); + assertThat(algorithm.getName(), is("PS384")); + } + + @Test + public void shouldCreateRSA512PSSAlgorithmWithBothKeys() { + RSAPublicKey publicKey = mock(RSAPublicKey.class); + RSAPrivateKey privateKey = mock(RSAPrivateKey.class); + Algorithm algorithm = Algorithm.RSA512PSS(publicKey, privateKey); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("RSASSA-PSS")); + assertThat(algorithm.getName(), is("PS512")); + } + + @Test + public void shouldCreateRSA512PSSAlgorithmWithProvider() { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + Algorithm algorithm = Algorithm.RSA512PSS(provider); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("RSASSA-PSS")); + assertThat(algorithm.getName(), is("PS512")); + } + @Test public void shouldCreateECDSA256AlgorithmWithPublicKey() { ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java index 115de64c..63ea45e7 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java @@ -214,7 +214,7 @@ public void shouldThrowWhenMacAlgorithmDoesNotExists() throws Exception { exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(), any(String.class), any(String.class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -232,7 +232,7 @@ public void shouldThrowWhenThePublicKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(), any(String.class), any(String.class), any(byte[].class))) .thenThrow(InvalidKeyException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -250,7 +250,7 @@ public void shouldThrowWhenTheSignatureIsNotPrepared() throws Exception { exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(), any(String.class), any(String.class), any(byte[].class))) .thenThrow(SignatureException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -467,7 +467,7 @@ public void shouldThrowOnSignWhenSignatureAlgorithmDoesNotExists() throws Except exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(), any(byte[].class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -484,7 +484,7 @@ public void shouldThrowOnSignWhenThePrivateKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(), any(byte[].class), any(byte[].class))) .thenThrow(InvalidKeyException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -501,7 +501,7 @@ public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(), any(byte[].class), any(byte[].class))) .thenThrow(SignatureException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/RSAPSSAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/RSAPSSAlgorithmTest.java new file mode 100644 index 00000000..237e0e5f --- /dev/null +++ b/lib/src/test/java/com/auth0/jwt/algorithms/RSAPSSAlgorithmTest.java @@ -0,0 +1,181 @@ +package com.auth0.jwt.algorithms; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.AfterClass; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.security.Provider; +import java.security.Security; +import java.security.interfaces.RSAKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +import static com.auth0.jwt.PemUtils.readPrivateKeyFromFile; +import static com.auth0.jwt.PemUtils.readPublicKeyFromFile; +import static com.auth0.jwt.algorithms.CryptoTestHelper.asJWT; +import static com.auth0.jwt.algorithms.CryptoTestHelper.assertSignaturePresent; + +/** + * Round-trip tests for the RSASSA-PSS (PS256/PS384/PS512) algorithms. + *
+ * BouncyCastle is registered as a provider so these tests also pass on Java 8, whose built-in + * providers do not implement RSASSA-PSS. On Java 11+ the built-in provider would suffice, but + * registering BouncyCastle keeps the test behavior identical across the supported Java versions. + */ +public class RSAPSSAlgorithmTest { + + private static final String PRIVATE_KEY_FILE = "src/test/resources/rsa-private.pem"; + private static final String PUBLIC_KEY_FILE = "src/test/resources/rsa-public.pem"; + private static final String INVALID_PUBLIC_KEY_FILE = "src/test/resources/rsa-public_invalid.pem"; + + private static final String PS256Header = "eyJhbGciOiJQUzI1NiJ9"; + private static final String PS384Header = "eyJhbGciOiJQUzM4NCJ9"; + private static final String PS512Header = "eyJhbGciOiJQUzUxMiJ9"; + private static final String auth0IssPayload = "eyJpc3MiOiJhdXRoMCJ9"; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + private static final Provider bcProvider = new BouncyCastleProvider(); + + @BeforeClass + public static void setUp() { + Security.insertProviderAt(bcProvider, 1); + } + + @AfterClass + public static void tearDown() { + Security.removeProvider(bcProvider.getName()); + } + + @Test + public void shouldSignAndVerifyPS256WithBothKeys() throws Exception { + Algorithm algorithm = Algorithm.RSA256PSS( + (RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), + (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + + String jwt = asJWT(algorithm, PS256Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldSignWithPrivateKeyAndVerifyWithPublicKeyPS256() throws Exception { + Algorithm signer = Algorithm.RSA256PSS((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + Algorithm verifier = Algorithm.RSA256PSS((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); + + String jwt = asJWT(signer, PS256Header, auth0IssPayload); + + assertSignaturePresent(jwt); + verifier.verify(JWT.decode(jwt)); + } + + @Test + public void shouldSignAndVerifyPS384WithBothKeys() throws Exception { + Algorithm algorithm = Algorithm.RSA384PSS( + (RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), + (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + + String jwt = asJWT(algorithm, PS384Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldSignAndVerifyPS512WithBothKeys() throws Exception { + Algorithm algorithm = Algorithm.RSA512PSS( + (RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), + (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + + String jwt = asJWT(algorithm, PS512Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldProduceNonDeterministicSignatures() throws Exception { + Algorithm algorithm = Algorithm.RSA256PSS( + (RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), + (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + + String firstJwt = asJWT(algorithm, PS256Header, auth0IssPayload); + String secondJwt = asJWT(algorithm, PS256Header, auth0IssPayload); + + String firstSignature = firstJwt.substring(firstJwt.lastIndexOf('.') + 1); + String secondSignature = secondJwt.substring(secondJwt.lastIndexOf('.') + 1); + org.hamcrest.MatcherAssert.assertThat(firstSignature, + org.hamcrest.CoreMatchers.is(org.hamcrest.CoreMatchers.not(secondSignature))); + algorithm.verify(JWT.decode(firstJwt)); + algorithm.verify(JWT.decode(secondJwt)); + } + + @Test + public void shouldFailPS256VerificationWithInvalidPublicKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: RSASSA-PSS"); + + Algorithm signer = Algorithm.RSA256PSS((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + String jwt = asJWT(signer, PS256Header, auth0IssPayload); + + Algorithm verifier = Algorithm.RSA256PSS((RSAKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE, "RSA")); + verifier.verify(JWT.decode(jwt)); + } + + @Test + public void shouldRejectPS256TokenWhenVerifiedWithPS384() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: RSASSA-PSS"); + + Algorithm signer = Algorithm.RSA256PSS((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + String jwt = asJWT(signer, PS256Header, auth0IssPayload); + + Algorithm verifier = Algorithm.RSA384PSS((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); + verifier.verify(JWT.decode(jwt)); + } + + @Test + public void shouldRejectPS256TokenWhenVerifiedWithPS512() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: RSASSA-PSS"); + + Algorithm signer = Algorithm.RSA256PSS((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + String jwt = asJWT(signer, PS256Header, auth0IssPayload); + + Algorithm verifier = Algorithm.RSA512PSS((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); + verifier.verify(JWT.decode(jwt)); + } + + /** + * Exercises the JDK-native RSASSA-PSS implementation (SunRsaSign, Java 11+) rather than the + * BouncyCastle provider the rest of this class relies on. Skipped on Java 8, whose built-in + * providers do not implement RSASSA-PSS. + */ + @Test + public void shouldSignAndVerifyPS256WithJdkNativeProvider() throws Exception { + Assume.assumeFalse("Requires JDK-native RSASSA-PSS (Java 11+)", + System.getProperty("java.specification.version").equals("1.8")); + + Security.removeProvider(bcProvider.getName()); + try { + Algorithm algorithm = Algorithm.RSA256PSS( + (RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), + (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + + String jwt = asJWT(algorithm, PS256Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } finally { + Security.insertProviderAt(bcProvider, 1); + } + } +}