From 8e0f740329e2c1181d89d2d95f0db358a5579ba9 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Tue, 5 Sep 2023 13:08:31 -0700 Subject: [PATCH] Enabled key-specific Provider support during JwtParser execution. (#809) * Enabled key-specific Provider support during JwtParser execution. Usage patterns and documentation updated inn JwtParserBuilder#keyLocator and README.md. * Updated Table of Contents with link to Key Locator key-specific Provider section * Added Pkcs11Test test case that ensures explicit .provider calls for all JWS and JWE operations are not needed when the PKCS11 provider is installed in the JVM via Security.addProvider * Removed unnecessary CryptoAlgorithm#nonPkcs11Provider method * Ensured RSA key validation implementation is consistent with name/available-length validation checks in other SecureDigestAlgorithm implementations --- README.md | 89 ++++++++++++++----- .../io/jsonwebtoken/JwtParserBuilder.java | 36 +++++++- .../java/io/jsonwebtoken/security/Keys.java | 74 ++++++++++----- .../security/PrivateKeyBuilder.java | 23 +++++ .../jsonwebtoken/impl/DefaultJwtBuilder.java | 24 ++--- .../jsonwebtoken/impl/DefaultJwtParser.java | 24 ++--- .../security/AbstractSecurityBuilder.java | 44 +++++++++ .../impl/security/CryptoAlgorithm.java | 24 +---- .../impl/security/DefaultKeyPairBuilder.java | 18 +--- .../impl/security/DefaultMacAlgorithm.java | 4 +- .../impl/security/DefaultRsaKeyAlgorithm.java | 33 ++++--- .../security/DefaultSecretKeyBuilder.java | 19 +--- .../impl/security/EcSignatureAlgorithm.java | 37 ++++---- .../impl/security/EcdhKeyAlgorithm.java | 16 +--- .../impl/security/EdSignatureAlgorithm.java | 16 ++-- .../impl/security/KeysBridge.java | 43 +++++---- .../impl/security/ProvidedKeyBuilder.java | 41 +++++++++ .../security/ProvidedPrivateKeyBuilder.java | 57 ++++++++++++ .../security/ProvidedSecretKeyBuilder.java | 36 ++++++++ .../impl/security/ProviderKey.java | 76 ++++++++++++++++ .../impl/security/ProviderPrivateKey.java | 26 ++++++ .../impl/security/ProviderSecretKey.java | 26 ++++++ .../impl/security/RsaSignatureAlgorithm.java | 42 +++++---- .../security/AesGcmKeyAlgorithmTest.groovy | 5 +- .../impl/security/CryptoAlgorithmTest.groovy | 35 -------- .../DefaultRsaKeyAlgorithmTest.groovy | 40 ++++++--- .../security/EcSignatureAlgorithmTest.groovy | 34 ++++--- .../security/EdSignatureAlgorithmTest.groovy | 6 +- .../impl/security/Pkcs11Test.groovy | 77 +++++++++++----- .../security/ProvidedKeyBuilderTest.groovy | 44 +++++++++ .../ProvidedSecretKeyBuilderTest.groovy | 36 ++++++++ .../impl/security/ProviderKeyTest.groovy | 86 ++++++++++++++++++ .../security/RFC7516AppendixA3Test.groovy | 2 +- .../impl/security/RFC7517AppendixCTest.groovy | 2 +- .../security/RsaSignatureAlgorithmTest.groovy | 43 +++++++-- ...tKeyBuilder.groovy => TestProvider.groovy} | 29 ++---- .../io/jsonwebtoken/security/KeysTest.groovy | 16 ++-- pom.xml | 2 +- 38 files changed, 935 insertions(+), 350 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/security/PrivateKeyBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecurityBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedKeyBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedPrivateKeyBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedSecretKeyBuilder.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/ProviderKey.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/ProviderPrivateKey.java create mode 100644 impl/src/main/java/io/jsonwebtoken/impl/security/ProviderSecretKey.java create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidedKeyBuilderTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidedSecretKeyBuilderTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/security/ProviderKeyTest.groovy rename impl/src/test/groovy/io/jsonwebtoken/impl/security/{FixedSecretKeyBuilder.groovy => TestProvider.groovy} (54%) diff --git a/README.md b/README.md index 24bf7737..e3334a53 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ enforcement. * [Custom Key Locator](#key-locator-custom) * [Key Locator Strategy](#key-locator-strategy) * [Key Locator Return Values](#key-locator-retvals) + * [Provider-constrained Keys (PKCS11, HSM, etc)](#key-locator-provider) * [Claim Assertions](#jwt-read-claims) * [Accounting for Clock Skew](#jwt-read-clock) * [Custom Clock Support](#jwt-read-clock-custom) @@ -1059,8 +1060,8 @@ try { jwt = Jwts.parser() // (1) .keyLocator(keyLocator) // (2) dynamically locate signing or encryption keys - //.verifyWith(key) // or a static key used to verify all signed JWTs - //.decryptWith(key) // or a static key used to decrypt all encrypted JWTs + //.verifyWith(key) // or a constant key used to verify all signed JWTs + //.decryptWith(key) // or a constant key used to decrypt all encrypted JWTs .build() // (3) @@ -1084,7 +1085,7 @@ catch (JwtException ex) { // (5) > instead of a generic `Jwt` instance. -### Static Parsing Key +### Constant Parsing Key If the JWT parsed is a JWS or JWE, a key will be necessary to verify the signature or decrypt it. If a JWS and signature verification fails, or if a JWE and decryption fails, the JWT cannot be safely trusted and should be @@ -1285,6 +1286,46 @@ on the type of JWS or JWE algorithm used. That is: `Keys.password(char[] passwordCharacters)`. * For asymmetric key management algorithms, the returned decryption key should be a `PrivateKey` (not a `PublicKey`). + +#### Provider-constrained Keys + +If any verification or decryption key returned from a Key `Locator` must be used with a specific security `Provider` +(such as for PKCS11 or Hardware Security Module (HSM) keys), you must make that `Provider` available for JWT parsing +in one of 3 ways, listed in order of recommendation and simplicity: + +1. [Configure the Provider in the JVM](https://docs.oracle.com/en/java/javase/17/security/howtoimplaprovider.html#GUID-831AA25F-F702-442D-A2E4-8DA6DEA16F33), + either by modifying the `java.security` file or by registering the `Provider` dynamically via + [Security.addProvider(Provider)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/Security.html#addProvider(java.security.Provider)). + This is the recommended approach so you do not need to modify code anywhere that may need to parse JWTs. + + +2. Set the `Provider` as the parser default by calling `JwtParserBuilder#provider(Provider)`. This will + ensure the provider is used by default with _all_ located keys unless overridden by a key-specific Provider. This + is only recommended when you are confident that all JWTs encountered by the parser instance will use keys + attributed to the same `Provider`, unless overridden by a specific key. + + +3. Associate the `Provider` with a specific key using `Keys.builder` so it is used for that key only. This option is + useful if some located keys require a specific provider, while other located keys can assume a default provider. For + example: + + ```java + public Key locate(Header header) { + + PrivateKey /* or SecretKey */ key = findKey(header); // implement me + + Provider keySpecificProvider = findKeyProvider(key); // implement me + if (keySpecificProvider != null) { + // Ensure the key-specific provider (e.g. for PKCS11 or HSM) will be used + // during decryption with the KeyAlgorithm in the JWE 'alg' header + return Keys.builder(key).provider(keySpecificProvider).build(); + } + + // otherwise default provider is fine: + return key; + } + ``` + ### Claim Assertions @@ -1636,13 +1677,15 @@ If your secret key is: ``` * A raw (non-encoded) string (e.g. a password String): ```java - SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8)); + Password key = Keys.password(secretString.toCharArray()); ``` - It is always incorrect to call `secretString.getBytes()` (without providing a charset). - - However, raw password strings like this, e.g. `correcthorsebatterystaple` should be avoided whenever possible - because they can inevitably result in weak or susceptible keys. Secure-random keys are almost always stronger. - If you are able, prefer creating a [new secure-random secret key](#jws-key-create-secret) instead. + +> **Warning** +> +> It is almost always incorrect to call any variant of `secretString.getBytes` in any cryptographic context. +> Safe cryptographic keys are never represented as direct (unencoded) strings. If you have a password that should +> be represented as a `Key` for `HMAC-SHA` algorithms, it is _strongly_ recommended to use a key derivation +> algorithm to derive a cryptographically-strong `Key` from the password, and never use the password directly. ##### SignatureAlgorithm Override @@ -2266,18 +2309,17 @@ However, if your decryption `PrivateKey`s are stored in a Hardware Security Modu it is likely that your `PrivateKey` instances _do not_ implement `ECKey`. In these cases, you need to provide both the PKCS11 `PrivateKey` and it's companion `PublicKey` during decryption -by using the `Keys.wrap` method. For example: -for example: +by using the `Keys.builder` method. For example: ```java KeyPair pair = getMyPkcs11KeyPair(); -PrivateKey priv = pair.getPrivate(); -PublicKey pub = pair.getPublic(); // must implement ECKey or EdECKey or BouncyCastle equivalent -PrivateKey decryptionKey = Keys.wrap(priv, pub); +PrivateKey jwtParserDecryptionKey = Keys.builder(pair.getPrivate()) + .publicKey(pair.getPublic()) // PublicKey must implement ECKey or EdECKey or BouncyCastle equivalent + .build(); ``` -You then use the resulting `decryptionKey` (not `priv`) with the `JwtParserBuilder` or as the return value from -a custom [Key Locator](#key-locator) implementation. For example: +You then use the resulting `jwtParserDecryptionKey` (not `pair.getPrivate()`) with the `JwtParserBuilder` or as +the return value from a custom [Key Locator](#key-locator) implementation. For example: ```java PrivateKey decryptionKey = Keys.wrap(pkcs11PrivateKey, pkcs11PublicKey); @@ -2292,11 +2334,14 @@ Or as the return value from your key locator: ```java Jwts.parser() - .keyLocator(keyLocator) // your keyLocator.locate(header) would return Keys.wrap(privateKey, publicKey) + .keyLocator(keyLocator) // your keyLocator.locate(header) would return Keys.builder... .build() .parseClaimsJwe(jweString); ``` +Please see the [Provider-constrained Keys](#key-locator-provider) section for more information, as well as +code examples of how to implement a Key `Locator` using the `Keys.builder` technique. + #### JWE Decompression @@ -2474,7 +2519,7 @@ The resulting `JwkThumbprint` instance provides some useful methods: * `jwkThumbprint.toByteArray()`: the thumbprint's actual digest bytes - i.e. the raw output from the hash algorithm * `jwkThumbprint.toString()`: the digest bytes as a Base64URL-encoded string * `jwkThumbprint.getHashAlgorithm()`: the specific `HashAlgorithm` used to compute the thumbprint. Many standard IANA - hash algorithms are available as constants in the `Jwts.HASH` utility class. + hash algorithms are available as constants in the `Jwks.HASH` utility class. * `jwkThumbprint.toURI()`: the thumbprint's canonical URI as defined by the [JWK Thumbprint URI](https://www.rfc-editor.org/rfc/rfc9278.html) specification @@ -2589,10 +2634,10 @@ This is true for all secret or private key members in `SecretJwk` and `PrivateJw > **Warning** > -> **The JWT specifications tandardizes compression for JWEs (Encrypted JWTs) ONLY, however JJWT supports it for JWS +> **The JWT specification standardizes compression for JWEs (Encrypted JWTs) ONLY, however JJWT supports it for JWS > (Signed JWTs) as well**. > -> If you are positive that a JWT you create with JJWT will _also_ be parsed with JJWT, +> If you are positive that a JWS you create with JJWT will _also_ be parsed with JJWT, > you can use this feature with both JWEs and JWSs, otherwise it is best to only use it for JWEs. If a JWT's `payload` is sufficiently large - that is, it is a large content byte array or JSON with a lot of @@ -3185,9 +3230,9 @@ This is an example showing how to digitally sign and verify a JWT using the The `EdDSA` signature algorithm is defined for JWS in [RFC 8037, Section 3.1](https://www.rfc-editor.org/rfc/rfc8037#section-3.1) using keys for two Edwards curves: -* `Ed25519`: `EdDSA` using curve `Ed25519`. `Ed25519` algorithm keys must be 256 bits (32 bytes) long and produce +* `Ed25519`: `EdDSA` using curve `Ed25519`. `Ed25519` algorithm keys must be 255 bits long and produce signatures 512 bits (64 bytes) long. -* `Ed448`: `EdDSA` using curve `Ed448`. `Ed448` algorithm keys must be 456 bits (57 bytes) long and produce signatures +* `Ed448`: `EdDSA` using curve `Ed448`. `Ed448` algorithm keys must be 448 bits long and produce signatures 912 bits (114 bytes) long. In this example, Bob will sign a JWT using his Edwards Curve private key, and Alice can verify it came from Bob diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 3ad82b6e..44c0657d 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -95,7 +95,7 @@ public interface JwtParserBuilder extends Builder { JwtParserBuilder enableUnsecuredDecompression(); /** - * Sets the JCA Provider to use during cryptographic signature and decryption operations, or {@code null} if the + * Sets the JCA Provider to use during cryptographic signature and key decryption operations, or {@code null} if the * JCA subsystem preferred provider should be used. * * @param provider the JCA Provider to use during cryptographic signature and decryption operations, or {@code null} @@ -432,7 +432,7 @@ public interface JwtParserBuilder extends Builder { *
  • If the parsed String is a JWE, it will be called to find the appropriate decryption key.
  • * * - *

    Specifying a key {@code Locator} is necessary when the signature verification or decryption key is not + *

    A key {@code Locator} is necessary when the signature verification or decryption key is not * already known before parsing the JWT and the JWT header must be inspected first to determine how to * look up the verification or decryption key. Once returned by the locator, the JwtParser will then either * verify the JWS signature or decrypt the JWE payload with the returned key. For example:

    @@ -453,6 +453,38 @@ public interface JwtParserBuilder extends Builder { * *

    A Key {@code Locator} is invoked once during parsing before performing decryption or signature verification.

    * + *

    Provider-constrained Keys

    + * + *

    If any verification or decryption key returned from a Key {@code Locator} must be used with a specific + * security {@link Provider} (such as for PKCS11 or Hardware Security Module (HSM) keys), you must make that + * Provider available for JWT parsing in one of 3 ways, listed in order of recommendation and simplicity:

    + * + *
      + *
    1. + * Configure the Provider in the JVM, either by modifying the {@code java.security} file or by + * registering the Provider dynamically via + * {@link java.security.Security#addProvider(Provider) Security.addProvider(Provider)}. This is the + * recommended approach so you do not need to modify code anywhere that may need to parse JWTs.
    2. + *
    3. Specify the {@code Provider} as the {@code JwtParser} default via {@link #provider(Provider)}. This will + * ensure the provider is used by default with all located keys unless overridden by a + * key-specific Provider. This is only recommended when you are confident that all JWTs encountered by the + * parser instance will use keys attributed to the same {@code Provider}, unless overridden by a specific + * key.
    4. + *
    5. Associate the {@code Provider} with a specific key so it is used for that key only. This option + * is useful if some located keys require a specific provider, while other located keys can assume a + * default provider.
    6. + *
    + * + *

    If you need to use option #3, you associate a key for the {@code JwtParser}'s needs by using a + * key builder before returning the key as the {@code Locator} return value. For example:

    + *
    +     *     public Key locate(Header<?> header) {
    +     *         PrivateKey key = findKey(header); // or SecretKey
    +     *         Provider keySpecificProvider = getKeyProvider(key); // implement me
    +     *         // associate the key with its required provider:
    +     *         return Keys.builder(key).provider(keySpecificProvider).build();
    +     *     }
    + * * @param keyLocator the locator used to retrieve decryption or signature verification keys. * @return the parser builder for method chaining. * @since JJWT_RELEASE_VERSION diff --git a/api/src/main/java/io/jsonwebtoken/security/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java index c22d55dc..f86e4aaa 100644 --- a/api/src/main/java/io/jsonwebtoken/security/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -23,6 +23,7 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.KeyPair; import java.security.PrivateKey; +import java.security.Provider; import java.security.PublicKey; /** @@ -35,7 +36,12 @@ public final class Keys { private static final String BRIDGE_CLASSNAME = "io.jsonwebtoken.impl.security.KeysBridge"; private static final Class BRIDGE_CLASS = Classes.forName(BRIDGE_CLASSNAME); private static final Class[] FOR_PASSWORD_ARG_TYPES = new Class[]{char[].class}; - private static final Class[] ASSOCIATE_ARG_TYPES = new Class[]{PrivateKey.class, PublicKey.class}; + private static final Class[] SECRET_BUILDER_ARG_TYPES = new Class[]{SecretKey.class}; + private static final Class[] PRIVATE_BUILDER_ARG_TYPES = new Class[]{PrivateKey.class}; + + private static T invokeStatic(String method, Class[] argTypes, Object... args) { + return Classes.invokeStatic(BRIDGE_CLASS, method, argTypes, args); + } //prevent instantiation private Keys() { @@ -268,33 +274,59 @@ public final class Keys { * @since JJWT_RELEASE_VERSION */ public static Password password(char[] password) { - return Classes.invokeStatic(BRIDGE_CLASS, "password", FOR_PASSWORD_ARG_TYPES, new Object[]{password}); + return invokeStatic("password", FOR_PASSWORD_ARG_TYPES, new Object[]{password}); } /** - * Returns a {@code PrivateKey} that may be used by algorithms that require the private key's public information. - * This method is primarily only useful for PKCS11 private keys. + * Returns a {@code SecretKeyBuilder} that produces the specified key, allowing association with a + * {@link SecretKeyBuilder#provider(Provider) provider} that must be used with the key during cryptographic + * operations. For example: * - *

    If the private key instance is already capable of representing public information (because it - * implements one of the java.security.interfaces.{ECKey,RSAKey,XECKey,EdECKey} interfaces), - * this method does nothing and returns the private key unaltered.

    + *
    +     * SecretKey key = Keys.builder(key).provider(mandatoryProvider).build();
    * - *

    If however the private key instance does not implement one of those interfaces, a new private key - * instance that wraps both the specified private key and public key will be created and returned. JJWT - * algorithms that require the key's public information know how to handle these wrapper instances to obtain the - * 'real' private key for cryptography operations while using the associated public key for validation.

    + *

    Cryptographic algorithm implementations can inspect the resulting {@code key} instance and obtain its + * mandatory {@code Provider} if necessary.

    * - * @param priv the private key to use for cryptographic operations - * @param pub the private key's associated PublicKey which must implement one of the required - * java.security.interfaces.{ECKey,RSAKey,XECKey,EdECKey} interfaces - * @return a {@code PrivateKey} that may be used by algorithms that require the private key's public information. - * @throws UnsupportedKeyException if the {@code PublicKey} is required but does not implement one of the required - * java.security.interfaces.{ECKey,RSAKey,XECKey,EdECKey} interfaces + *

    This method is primarily only useful for keys that cannot expose key material, such as PKCS11 or HSM + * (Hardware Security Module) keys, and require a specific {@code Provider} to be used during cryptographic + * operations.

    + * + * @param key the secret key to use for cryptographic operations, potentially associated with a configured + * {@link Provider} + * @return a new {@code SecretKeyBuilder} that produces the specified key, potentially associated with any + * specified provider. * @since JJWT_RELEASE_VERSION */ - public static PrivateKey wrap(PrivateKey priv, PublicKey pub) throws UnsupportedKeyException { - Assert.notNull(priv, "PrivateKey cannot be null."); - Assert.notNull(pub, "PublicKey cannot be null."); - return Classes.invokeStatic(BRIDGE_CLASS, "wrap", ASSOCIATE_ARG_TYPES, new Object[]{priv, pub}); + public static SecretKeyBuilder builder(SecretKey key) { + Assert.notNull(key, "SecretKey cannot be null."); + return invokeStatic("builder", SECRET_BUILDER_ARG_TYPES, key); + } + + /** + * Returns a {@code PrivateKeyBuilder} that produces the specified key, allowing association with a + * {@link PrivateKeyBuilder#publicKey(PublicKey) publicKey} to obtain public key data if necessary, or a + * {@link SecretKeyBuilder#provider(Provider) provider} that must be used with the key during cryptographic + * operations. For example: + * + *
    +     * PrivateKey key = Keys.builder(privateKey).publicKey(publicKey).provider(mandatoryProvider).build();
    + * + *

    Cryptographic algorithm implementations can inspect the resulting {@code key} instance and obtain its + * mandatory {@code Provider} or {@code PublicKey} if necessary.

    + * + *

    This method is primarily only useful for keys that cannot expose key material, such as PKCS11 or HSM + * (Hardware Security Module) keys, and require a specific {@code Provider} or public key data to be used + * during cryptographic operations.

    + * + * @param key the private key to use for cryptographic operations, potentially associated with a configured + * {@link Provider} or {@link PublicKey}. + * @return a new {@code PrivateKeyBuilder} that produces the specified private key, potentially associated with any + * specified provider or {@code PublicKey} + * @since JJWT_RELEASE_VERSION + */ + public static PrivateKeyBuilder builder(PrivateKey key) { + Assert.notNull(key, "PrivateKey cannot be null."); + return invokeStatic("builder", PRIVATE_BUILDER_ARG_TYPES, key); } } diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateKeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PrivateKeyBuilder.java new file mode 100644 index 00000000..b223144b --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateKeyBuilder.java @@ -0,0 +1,23 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.security; + +import java.security.PrivateKey; +import java.security.PublicKey; + +public interface PrivateKeyBuilder extends KeyBuilder { + PrivateKeyBuilder publicKey(PublicKey publicKey); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index d0e34482..3f2605d3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -27,6 +27,7 @@ import io.jsonwebtoken.impl.security.DefaultAeadRequest; import io.jsonwebtoken.impl.security.DefaultKeyRequest; import io.jsonwebtoken.impl.security.DefaultSecureRequest; import io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm; +import io.jsonwebtoken.impl.security.ProviderKey; import io.jsonwebtoken.impl.security.StandardSecureDigestAlgorithms; import io.jsonwebtoken.io.CompressionAlgorithm; import io.jsonwebtoken.io.Decoders; @@ -473,14 +474,16 @@ public class DefaultJwtBuilder implements JwtBuilder { this.headerBuilder.add(DefaultHeader.COMPRESSION_ALGORITHM.getId(), compressionAlgorithm.getId()); } + Provider keyProvider = ProviderKey.getProvider(this.key, this.provider); + Key key = ProviderKey.getKey(this.key); if (jwe) { - return encrypt(payload); + return encrypt(payload, key, keyProvider); } else { - return compact(payload); + return compact(payload, key, keyProvider); } } - private String compact(byte[] payload) { + private String compact(byte[] payload, Key key, Provider provider) { Assert.stateNotNull(sigAlg, "SignatureAlgorithm is required."); // invariant @@ -494,7 +497,7 @@ public class DefaultJwtBuilder implements JwtBuilder { String jwt = base64UrlEncodedHeader + DefaultJwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; - if (this.key != null) { //jwt must be signed: + if (key != null) { //jwt must be signed: Assert.stateNotNull(key, "Signing key cannot be null."); Assert.stateNotNull(signFunction, "signFunction cannot be null."); byte[] data = jwt.getBytes(StandardCharsets.US_ASCII); @@ -511,7 +514,7 @@ public class DefaultJwtBuilder implements JwtBuilder { return jwt; } - private String encrypt(byte[] payload) { + private String encrypt(byte[] payload, Key key, Provider keyProvider) { Assert.stateNotNull(key, "Key is required."); // set by encryptWith* Assert.stateNotNull(enc, "Encryption algorithm is required."); // set by encryptWith* @@ -523,7 +526,7 @@ public class DefaultJwtBuilder implements JwtBuilder { //only expose (mutable) JweHeader functionality to KeyAlgorithm instances, not the full headerBuilder // (which exposes this JwtBuilder and shouldn't be referenced by KeyAlgorithms): JweHeader delegate = new DefaultMutableJweHeader(this.headerBuilder); - KeyRequest keyRequest = new DefaultKeyRequest<>(this.key, this.provider, this.secureRandom, delegate, enc); + KeyRequest keyRequest = new DefaultKeyRequest<>(key, keyProvider, this.secureRandom, delegate, enc); KeyResult keyResult = keyAlgFunction.apply(keyRequest); Assert.stateNotNull(keyResult, "KeyAlgorithm must return a KeyResult."); @@ -541,12 +544,9 @@ public class DefaultJwtBuilder implements JwtBuilder { // During encryption, the configured Provider applies to the KeyAlgorithm, not the AeadAlgorithm, mostly // because all JVMs support the standard AeadAlgorithms (especially with BouncyCastle in the classpath). - // As such, the need for a configured Provider is much more likely necessary for the KeyAlgorithm, - // especially when using a HSM/PKCS11 Provider. However, if the `dir`ect key algorithm was chosen _and_ - // a Provider was configured, then the provider is likely necessary for that key, so we represent that - // here: - Provider aeadProvider = this.keyAlg.getId().equals(Jwts.KEY.DIRECT.getId()) ? this.provider : null; - AeadRequest encRequest = new DefaultAeadRequest(payload, aeadProvider, secureRandom, cek, aad); + // As such, the provider here is intentionally omitted (null): + // TODO: add encProvider(Provider) builder method that applies to this request only? + AeadRequest encRequest = new DefaultAeadRequest(payload, null, secureRandom, cek, aad); AeadResult encResult = encFunction.apply(encRequest); byte[] iv = Assert.notEmpty(encResult.getInitializationVector(), "Encryption result must have a non-empty initialization vector."); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 8f33b453..3b2264a6 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -44,6 +44,7 @@ import io.jsonwebtoken.impl.security.DefaultAeadResult; import io.jsonwebtoken.impl.security.DefaultDecryptionKeyRequest; import io.jsonwebtoken.impl.security.DefaultVerifySecureDigestRequest; import io.jsonwebtoken.impl.security.LocatingKeyResolver; +import io.jsonwebtoken.impl.security.ProviderKey; import io.jsonwebtoken.io.CompressionAlgorithm; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.DecodingException; @@ -310,8 +311,11 @@ public class DefaultJwtParser implements JwtParser { byte[] signature = decode(tokenized.getDigest(), "JWS signature"); try { + Provider provider = ProviderKey.getProvider(key, this.provider); // extract if necessary + key = ProviderKey.getKey(key); // unwrap if necessary, MUST be called after ProviderKey.getProvider + Assert.stateNotNull(key, "ProviderKey cannot be null."); //ProviderKey impl doesn't allow null VerifySecureDigestRequest request = - new DefaultVerifySecureDigestRequest<>(data, this.provider, null, key, signature); + new DefaultVerifySecureDigestRequest<>(data, provider, null, key, signature); if (!algorithm.verify(request)) { String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + "asserted and should not be trusted."; @@ -445,14 +449,17 @@ public class DefaultJwtParser implements JwtParser { @SuppressWarnings("rawtypes") final KeyAlgorithm keyAlg = this.keyAlgFn.apply(jweHeader); Assert.stateNotNull(keyAlg, "JWE Key Algorithm cannot be null."); - final Key key = this.keyLocator.locate(jweHeader); + Key key = this.keyLocator.locate(jweHeader); if (key == null) { String msg = "Cannot decrypt JWE payload: unable to locate key for JWE with header: " + jweHeader; throw new UnsupportedJwtException(msg); } + // extract key-specific provider if necessary; + Provider provider = ProviderKey.getProvider(key, this.provider); + key = ProviderKey.getKey(key); // this must be called after ProviderKey.getProvider DecryptionKeyRequest request = - new DefaultDecryptionKeyRequest<>(cekBytes, this.provider, null, jweHeader, encAlg, key); + new DefaultDecryptionKeyRequest<>(cekBytes, provider, null, jweHeader, encAlg, key); final SecretKey cek = keyAlg.getDecryptionKey(request); if (cek == null) { String msg = "The '" + keyAlg.getId() + "' JWE key algorithm did not return a decryption key. " + @@ -460,15 +467,12 @@ public class DefaultJwtParser implements JwtParser { throw new IllegalStateException(msg); } - // During decryption, the configured Provider applies to the KeyAlgorithm, not the AeadAlgorithm, mostly + // During decryption, the available Provider applies to the KeyAlgorithm, not the AeadAlgorithm, mostly // because all JVMs support the standard AeadAlgorithms (especially with BouncyCastle in the classpath). - // As such, the need for a configured Provider is much more likely necessary for the KeyAlgorithm, - // especially when using a HSM/PKCS11 Provider. However, if the `dir`ect key algorithm was chosen _and_ - // a Provider was configured, then the provider is likely necessary for that key, so we represent that - // here: - final Provider aeadProvider = keyAlg.getId().equalsIgnoreCase(Jwts.KEY.DIRECT.getId()) ? this.provider : null; + // As such, the provider here is intentionally omitted (null): + // TODO: add encProvider(Provider) builder method that applies to this request only? DecryptAeadRequest decryptRequest = - new DefaultAeadResult(aeadProvider, null, payload, cek, aad, tag, iv); + new DefaultAeadResult(null, null, payload, cek, aad, tag, iv); Message result = encAlg.decrypt(decryptRequest); payload = result.getPayload(); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecurityBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecurityBuilder.java new file mode 100644 index 00000000..b826f332 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/AbstractSecurityBuilder.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.SecurityBuilder; + +import java.security.Provider; +import java.security.SecureRandom; + +abstract class AbstractSecurityBuilder> implements SecurityBuilder { + + protected Provider provider; + protected SecureRandom random; + + @SuppressWarnings("unchecked") + protected final B self() { + return (B) this; + } + + @Override + public B provider(Provider provider) { + this.provider = provider; + return self(); + } + + @Override + public B random(SecureRandom random) { + this.random = random != null ? random : Randoms.secureRandom(); + return self(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java index ffe952d8..86864dae 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/CryptoAlgorithm.java @@ -17,7 +17,6 @@ package io.jsonwebtoken.impl.security; import io.jsonwebtoken.Identifiable; import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.AeadAlgorithm; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.Request; @@ -52,29 +51,11 @@ abstract class CryptoAlgorithm implements Identifiable { return this.jcaName; } - SecureRandom ensureSecureRandom(Request request) { + static SecureRandom ensureSecureRandom(Request request) { SecureRandom random = request != null ? request.getSecureRandom() : null; return random != null ? random : Randoms.secureRandom(); } - /** - * Returns the request provider only if it is not a PCKS11 provider. This is used by algorithms that - * generate an ephemeral key(pair) where the resulting key material must exist for inclusion in the JWE. PCS11 - * providers will not expose private key material and therefore can't be used for ephemeral key(pair) generation. - * - * @param request request to inspect - * @return the request provider or {@code null} if there is no provider, or {@code null} if the provider is a - * PCKS11 provider - */ - static Provider nonPkcs11Provider(Request request) { - Provider provider = request != null ? request.getProvider() : null; - String name = provider != null ? Strings.clean(provider.getName()) : null; - if (provider != null && name != null && name.startsWith("SunPKCS11")) { - provider = null; // don't use PKCS11 provider - } - return provider; - } - protected JcaTemplate jca() { return new JcaTemplate(getJcaName()); } @@ -94,8 +75,7 @@ abstract class CryptoAlgorithm implements Identifiable { protected SecretKey generateCek(KeyRequest request) { AeadAlgorithm enc = Assert.notNull(request.getEncryptionAlgorithm(), "Request encryptionAlgorithm cannot be null."); SecretKeyBuilder builder = Assert.notNull(enc.key(), "Request encryptionAlgorithm KeyBuilder cannot be null."); - Provider provider = nonPkcs11Provider(request); // PKCS11 / HSM check - SecretKey key = builder.provider(provider).random(request.getSecureRandom()).build(); + SecretKey key = builder.random(request.getSecureRandom()).build(); return Assert.notNull(key, "Request encryptionAlgorithm SecretKeyBuilder cannot produce null keys."); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java index 3d21081c..0b218ffb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultKeyPairBuilder.java @@ -19,17 +19,13 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.KeyPairBuilder; import java.security.KeyPair; -import java.security.Provider; -import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; -public class DefaultKeyPairBuilder implements KeyPairBuilder { +public class DefaultKeyPairBuilder extends AbstractSecurityBuilder implements KeyPairBuilder { private final String jcaName; private final int bitLength; private final AlgorithmParameterSpec params; - private Provider provider; - private SecureRandom random; public DefaultKeyPairBuilder(String jcaName) { this.jcaName = Assert.hasText(jcaName, "jcaName cannot be null or empty."); @@ -49,18 +45,6 @@ public class DefaultKeyPairBuilder implements KeyPairBuilder { this.bitLength = 0; } - @Override - public KeyPairBuilder provider(Provider provider) { - this.provider = provider; - return this; - } - - @Override - public KeyPairBuilder random(SecureRandom random) { - this.random = random; - return this; - } - @Override public KeyPair build() { JcaTemplate template = new JcaTemplate(this.jcaName, this.provider, this.random); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java index f5cfeae4..6f07a7c0 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java @@ -124,7 +124,7 @@ final class DefaultMacAlgorithm extends AbstractSecureDigestAlgorithm= " + MIN_KEY_BIT_LENGTH + " bits. See " + - "https://www.rfc-editor.org/rfc/rfc7518.html#section-" + section + " for more information."; - throw new WeakKeyException(msg); - } + int size = KeysBridge.findBitLength(key); + if (size < 0) return; // can't validate size: material or length not available (e.g. PKCS11 or HSM) + if (size < MIN_KEY_BIT_LENGTH) { + String id = getId(); + String section = id.startsWith("RSA1") ? "4.2" : "4.3"; + String msg = "The RSA " + keyType(encryption) + " key size (aka modulus bit length) is " + size + + " bits which is not secure enough for the " + id + " algorithm. " + + "The JWT JWA Specification (RFC 7518, Section " + section + ") states that RSA keys MUST " + + "have a size >= " + MIN_KEY_BIT_LENGTH + " bits. See " + + "https://www.rfc-editor.org/rfc/rfc7518.html#section-" + section + " for more information."; + throw new WeakKeyException(msg); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java index c4f2002b..1bc13789 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultSecretKeyBuilder.java @@ -19,18 +19,15 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.SecretKeyBuilder; import javax.crypto.SecretKey; -import java.security.Provider; -import java.security.SecureRandom; /** * @since JJWT_RELEASE_VERSION */ -public class DefaultSecretKeyBuilder implements SecretKeyBuilder { +public class DefaultSecretKeyBuilder extends AbstractSecurityBuilder + implements SecretKeyBuilder { protected final String JCA_NAME; protected final int BIT_LENGTH; - protected Provider provider; - protected SecureRandom random; public DefaultSecretKeyBuilder(String jcaName, int bitLength) { this.JCA_NAME = Assert.hasText(jcaName, "jcaName cannot be null or empty."); @@ -42,18 +39,6 @@ public class DefaultSecretKeyBuilder implements SecretKeyBuilder { random(Randoms.secureRandom()); } - @Override - public SecretKeyBuilder provider(Provider provider) { - this.provider = provider; - return this; - } - - @Override - public SecretKeyBuilder random(SecureRandom random) { - this.random = random != null ? random : Randoms.secureRandom(); - return this; - } - @Override public SecretKey build() { JcaTemplate template = new JcaTemplate(JCA_NAME, this.provider, this.random); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java index 68ca7a54..80342e14 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcSignatureAlgorithm.java @@ -26,6 +26,7 @@ import io.jsonwebtoken.security.KeyPairBuilder; import io.jsonwebtoken.security.SecureRequest; import io.jsonwebtoken.security.SignatureAlgorithm; import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.UnsupportedKeyException; import io.jsonwebtoken.security.VerifySecureDigestRequest; import java.math.BigInteger; @@ -39,6 +40,7 @@ import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; +import java.util.Set; // @since JJWT_RELEASE_VERSION final class EcSignatureAlgorithm extends AbstractSignatureAlgorithm { @@ -51,6 +53,8 @@ final class EcSignatureAlgorithm extends AbstractSignatureAlgorithm { private static final String ES384_OID = "1.2.840.10045.4.3.3"; private static final String ES512_OID = "1.2.840.10045.4.3.4"; + private static final Set KEY_ALG_NAMES = Collections.setOf("EC", "ECDSA", ES256_OID, ES384_OID, ES512_OID); + private final ECGenParameterSpec KEY_PAIR_GEN_PARAMS; private final int orderBitLength; @@ -85,7 +89,6 @@ final class EcSignatureAlgorithm extends AbstractSignatureAlgorithm { static final EcSignatureAlgorithm ES512 = new EcSignatureAlgorithm(521, ES512_OID); private static final Map BY_OID = new LinkedHashMap<>(3); - static { for (EcSignatureAlgorithm alg : Collections.of(ES256, ES384, ES512)) { BY_OID.put(alg.OID, alg); @@ -140,23 +143,19 @@ final class EcSignatureAlgorithm extends AbstractSignatureAlgorithm { @Override protected void validateKey(Key key, boolean signing) { super.validateKey(key, signing); - // Some PKCS11 providers and HSMs won't expose the ECKey interface, so we have to check to see if we can cast - // If so, we can provide the additional safety checks: - if (key instanceof ECKey) { - final String name = getId(); - ECKey ecKey = (ECKey) key; - BigInteger order = ecKey.getParams().getOrder(); - int orderBitLength = order.bitLength(); - int sigFieldByteLength = Bytes.length(orderBitLength); - int concatByteLength = sigFieldByteLength * 2; - - if (concatByteLength != this.signatureByteLength) { - String msg = "The provided Elliptic Curve " + keyType(signing) + - " key's size (aka Order bit length) is " + Bytes.bitsMsg(orderBitLength) + ", but the '" + - name + "' algorithm requires EC Keys with " + Bytes.bitsMsg(this.orderBitLength) + - " per [RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)."; - throw new InvalidKeyException(msg); - } + if (!KEY_ALG_NAMES.contains(KeysBridge.findAlgorithm(key))) { + throw new UnsupportedKeyException("Unsupported EC key algorithm name."); + } + int size = KeysBridge.findBitLength(key); + if (size < 0) return; // likely PKCS11 or HSM key, can't get the data we need + int sigFieldByteLength = Bytes.length(size); + int concatByteLength = sigFieldByteLength * 2; + if (concatByteLength != this.signatureByteLength) { + String msg = "The provided Elliptic Curve " + keyType(signing) + + " key size (aka order bit length) is " + Bytes.bitsMsg(size) + ", but the '" + + getId() + "' algorithm requires EC Keys with " + Bytes.bitsMsg(this.orderBitLength) + + " per [RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)."; + throw new InvalidKeyException(msg); } } @@ -165,7 +164,7 @@ final class EcSignatureAlgorithm extends AbstractSignatureAlgorithm { return jca(request).withSignature(new CheckedFunction() { @Override public byte[] apply(Signature sig) throws Exception { - sig.initSign(request.getKey()); + sig.initSign(KeysBridge.root(request)); sig.update(request.getPayload()); byte[] signature = sig.sign(); return transcodeDERToConcat(signature, signatureByteLength); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java index 9bba9ece..cd7a0e88 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java @@ -34,7 +34,6 @@ import io.jsonwebtoken.security.KeyAlgorithm; import io.jsonwebtoken.security.KeyLengthSupplier; import io.jsonwebtoken.security.KeyRequest; import io.jsonwebtoken.security.KeyResult; -import io.jsonwebtoken.security.KeySupplier; import io.jsonwebtoken.security.OctetPublicJwk; import io.jsonwebtoken.security.PublicJwk; import io.jsonwebtoken.security.Request; @@ -95,7 +94,7 @@ class EcdhKeyAlgorithm extends CryptoAlgorithm implements KeyAlgorithm() { @Override public byte[] apply(KeyAgreement keyAgreement) throws Exception { - keyAgreement.init(priv, ensureSecureRandom(request)); + keyAgreement.init(KeysBridge.root(priv), ensureSecureRandom(request)); keyAgreement.doPhase(pub, true); return keyAgreement.generateSecret(); } @@ -182,9 +181,8 @@ class EcdhKeyAlgorithm extends CryptoAlgorithm implements KeyAlgorithm jwkBuilder = Jwks.builder().random(random).provider(provider); - KeyPair pair = generateKeyPair(curve, provider, random); + DynamicJwkBuilder jwkBuilder = Jwks.builder().random(random); + KeyPair pair = generateKeyPair(curve, null, random); Assert.stateNotNull(pair, "Internal implementation state: KeyPair cannot be null."); @@ -225,14 +223,6 @@ class EcdhKeyAlgorithm extends CryptoAlgorithm implements KeyAlgorithm) privateKey).getKey(); - // wrapped keys are only produced within JJWT internal implementations (not by app devs) - // so the wrapped key should always be a PrivateKey: - privateKey = Assert.isInstanceOf(PrivateKey.class, key, "Wrapped key is not a PrivateKey."); - } - final SecretKey derived = deriveKey(request, epk.toKey(), privateKey); DecryptionKeyRequest unwrapReq = new DefaultDecryptionKeyRequest<>(request.getPayload(), diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/EdSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/EdSignatureAlgorithm.java index d72a3c7c..42c58202 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/EdSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/EdSignatureAlgorithm.java @@ -47,18 +47,13 @@ final class EdSignatureAlgorithm extends AbstractSignatureAlgorithm { @Override protected String getJcaName(Request request) { SecureRequest req = Assert.isInstanceOf(SecureRequest.class, request, "SecureRequests are required."); - Key key = req.getKey(); - + Key key = Assert.notNull(req.getKey(), "Request key cannot be null."); // If we're signing, and this instance's algorithm name is the default/generic 'EdDSA', then prefer the // signing key's curve algorithm ID. This ensures the most specific JCA algorithm is used for signing, // (while generic 'EdDSA' is fine for validation) String jcaName = getJcaName(); //default for JCA interaction - boolean signing = !(request instanceof VerifyDigestRequest); - if (ID.equals(jcaName) && signing) { // see if we can get a more-specific curve algorithm identifier: - EdwardsCurve curve = EdwardsCurve.findByKey(key); - if (curve != null) { - jcaName = curve.getJcaName(); // prefer the key's specific curve algorithm identifier during signing - } + if (!(request instanceof VerifyDigestRequest)) { // then we're signing, not verifying + jcaName = EdwardsCurve.forKey(key).getJcaName(); } return jcaName; } @@ -71,8 +66,9 @@ final class EdSignatureAlgorithm extends AbstractSignatureAlgorithm { @Override protected void validateKey(Key key, boolean signing) { super.validateKey(key, signing); - EdwardsCurve curve = EdwardsCurve.findByKey(key); - if (curve != null && !curve.isSignatureCurve()) { + // should always be non-null due to algorithm name lookup, even without encoded key bytes: + EdwardsCurve curve = EdwardsCurve.forKey(key); + if (!curve.isSignatureCurve()) { String msg = curve.getId() + " keys may not be used with " + getId() + " digital signatures per " + "https://www.rfc-editor.org/rfc/rfc8037.html#section-3.2"; throw new UnsupportedKeyException(msg); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java index d2995960..f843077c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java @@ -21,13 +21,14 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.KeySupplier; import io.jsonwebtoken.security.Password; +import io.jsonwebtoken.security.PrivateKeyBuilder; +import io.jsonwebtoken.security.SecretKeyBuilder; import io.jsonwebtoken.security.UnsupportedKeyException; import javax.crypto.SecretKey; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.interfaces.ECKey; @SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.Keys implementation public final class KeysBridge { @@ -48,24 +49,30 @@ public final class KeysBridge { return new PasswordSpec(password); } - public static PrivateKey wrap(PrivateKey priv, PublicKey pub) { - Assert.notNull(priv, "PrivateKey cannot be null."); - if (priv instanceof KeySupplier) { //already wrapped, don't wrap again - return priv; - } - // We only need to wrap if: - // 1. The private key is not already an ECKey. If it is, we can validate normally - // 2. The public key is an ECKey - this must be true to represent EC params for the private key - // 3. The private key indicates via its algorithm that it is intended to be used as an EC key. - String privAlg = Strings.clean(priv.getAlgorithm()); - if (!(priv instanceof ECKey) && pub instanceof ECKey && - (("EC".equalsIgnoreCase(privAlg) || "ECDSA".equalsIgnoreCase(privAlg)) || - EcSignatureAlgorithm.findByKey(priv) != null /* match by OID just in case for PKCS11 keys */)) { - return new PrivateECKey(priv, ((ECKey) pub).getParams()); - } + public static SecretKeyBuilder builder(SecretKey key) { + return new ProvidedSecretKeyBuilder(key); + } - // otherwise, no need to wrap, return unchanged - return priv; + public static PrivateKeyBuilder builder(PrivateKey key) { + return new ProvidedPrivateKeyBuilder(key); + } + + /** + * If the specified {@code key} is a {@link KeySupplier}, the 'root' (lowest level) key that may exist in + * a {@code KeySupplier} chain is returned, otherwise the {@code key} is returned. + * + * @param key the key to check if it is a {@code KeySupplier} + * @param the key type + * @return the lowest-level/root key available. + */ + @SuppressWarnings("unchecked") + public static K root(K key) { + return (key instanceof KeySupplier) ? (K) root((KeySupplier) key) : key; + } + + public static K root(KeySupplier supplier) { + Assert.notNull(supplier, "KeySupplier canot be null."); + return Assert.notNull(root(supplier.getKey()), "KeySupplier key cannot be null."); } public static String findAlgorithm(Key key) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedKeyBuilder.java new file mode 100644 index 00000000..14f8117e --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedKeyBuilder.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeyBuilder; + +import java.security.Key; + +abstract class ProvidedKeyBuilder> extends AbstractSecurityBuilder + implements KeyBuilder { + + protected final K key; + + ProvidedKeyBuilder(K key) { + this.key = Assert.notNull(key, "Key cannot be null."); + } + + @Override + public final K build() { + if (this.key instanceof ProviderKey) { // already wrapped, don't wrap again: + return this.key; + } + return doBuild(); + } + + abstract K doBuild(); +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedPrivateKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedPrivateKeyBuilder.java new file mode 100644 index 00000000..a7e71cbc --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedPrivateKeyBuilder.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.PrivateKeyBuilder; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.ECKey; + +public class ProvidedPrivateKeyBuilder extends ProvidedKeyBuilder + implements PrivateKeyBuilder { + + private PublicKey publicKey; + + ProvidedPrivateKeyBuilder(PrivateKey key) { + super(key); + } + + @Override + public PrivateKeyBuilder publicKey(PublicKey publicKey) { + this.publicKey = publicKey; + return this; + } + + @Override + public PrivateKey doBuild() { + + PrivateKey key = this.key; + + // We only need to wrap as an ECKey if: + // 1. The private key is not already an ECKey. If it is, we can validate normally + // 2. The private key indicates via its algorithm that it is intended to be used as an EC key. + // 3. The public key is an ECKey - this must be true to represent EC params for the private key + String privAlg = Strings.clean(this.key.getAlgorithm()); + if (!(key instanceof ECKey) && ("EC".equalsIgnoreCase(privAlg) || "ECDSA".equalsIgnoreCase(privAlg)) && + this.publicKey instanceof ECKey) { + key = new PrivateECKey(key, ((ECKey) this.publicKey).getParams()); + } + + return this.provider != null ? new ProviderPrivateKey(this.provider, key) : key; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedSecretKeyBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedSecretKeyBuilder.java new file mode 100644 index 00000000..c9f8784a --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ProvidedSecretKeyBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.security.Password; +import io.jsonwebtoken.security.SecretKeyBuilder; + +import javax.crypto.SecretKey; + +class ProvidedSecretKeyBuilder extends ProvidedKeyBuilder implements SecretKeyBuilder { + + ProvidedSecretKeyBuilder(SecretKey key) { + super(key); + } + + @Override + public SecretKey doBuild() { + if (this.key instanceof Password) { + return this.key; // provider never needed for Password instances. + } + return provider != null ? new ProviderSecretKey(this.provider, this.key) : this.key; + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ProviderKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ProviderKey.java new file mode 100644 index 00000000..9e44ae4d --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ProviderKey.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.KeySupplier; + +import java.security.Key; +import java.security.Provider; + +public class ProviderKey implements Key, KeySupplier { + + private final T key; + + private final Provider provider; + + public static Provider getProvider(Key key, Provider backup) { + if (key instanceof ProviderKey) { + ProviderKey pkey = (ProviderKey) key; + return Assert.stateNotNull(pkey.getProvider(), "ProviderKey provider can never be null."); + } + return backup; + } + + @SuppressWarnings("unchecked") + public static K getKey(K key) { + return key instanceof ProviderKey ? ((ProviderKey) key).getKey() : key; + } + + ProviderKey(Provider provider, T key) { + this.provider = Assert.notNull(provider, "Provider cannot be null."); + this.key = Assert.notNull(key, "Key argument cannot be null."); + if (key instanceof ProviderKey) { + String msg = "Nesting not permitted."; + throw new IllegalArgumentException(msg); + } + } + + @Override + public T getKey() { + return this.key; + } + + @Override + public String getAlgorithm() { + return this.key.getAlgorithm(); + } + + @Override + public String getFormat() { + return this.key.getFormat(); + } + + @Override + public byte[] getEncoded() { + return this.key.getEncoded(); + } + + public final Provider getProvider() { + return this.provider; + } + +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ProviderPrivateKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ProviderPrivateKey.java new file mode 100644 index 00000000..212cc1f6 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ProviderPrivateKey.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import java.security.PrivateKey; +import java.security.Provider; + +public final class ProviderPrivateKey extends ProviderKey implements PrivateKey { + + ProviderPrivateKey(Provider provider, PrivateKey key) { + super(provider, key); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/ProviderSecretKey.java b/impl/src/main/java/io/jsonwebtoken/impl/security/ProviderSecretKey.java new file mode 100644 index 00000000..dfd76f59 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/ProviderSecretKey.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security; + +import javax.crypto.SecretKey; +import java.security.Provider; + +public final class ProviderSecretKey extends ProviderKey implements SecretKey { + + ProviderSecretKey(Provider provider, SecretKey key) { + super(provider, key); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java index 33fb7e57..6d39215a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/RsaSignatureAlgorithm.java @@ -22,6 +22,7 @@ import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.KeyPairBuilder; import io.jsonwebtoken.security.SecureRequest; import io.jsonwebtoken.security.SignatureAlgorithm; +import io.jsonwebtoken.security.UnsupportedKeyException; import io.jsonwebtoken.security.VerifySecureDigestRequest; import io.jsonwebtoken.security.WeakKeyException; @@ -29,7 +30,6 @@ import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; -import java.security.interfaces.RSAKey; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.MGF1ParameterSpec; import java.security.spec.PSSParameterSpec; @@ -57,6 +57,9 @@ final class RsaSignatureAlgorithm extends AbstractSignatureAlgorithm { private static final Set PSS_ALG_NAMES = Collections.setOf(PSS_JCA_NAME, PSS_OID); + private static final Set KEY_ALG_NAMES = + Collections.setOf("RSA", PSS_JCA_NAME, PSS_OID, RS256_OID, RS384_OID, RS512_OID); + private static final int MIN_KEY_BIT_LENGTH = 2048; private static AlgorithmParameterSpec pssParamSpec(int digestBitLength) { @@ -153,6 +156,11 @@ final class RsaSignatureAlgorithm extends AbstractSignatureAlgorithm { return PSS_ALG_NAMES.contains(alg); } + static boolean isRsaAlgorithmName(Key key) { + String alg = KeysBridge.findAlgorithm(key); + return KEY_ALG_NAMES.contains(alg); + } + @Override public KeyPairBuilder keyPair() { final String jcaName = this.algorithmParameterSpec != null ? PSS_JCA_NAME : "RSA"; @@ -170,23 +178,21 @@ final class RsaSignatureAlgorithm extends AbstractSignatureAlgorithm { @Override protected void validateKey(Key key, boolean signing) { super.validateKey(key, signing); - // https://github.com/jwtk/jjwt/issues/68 : - // Some PKCS11 providers and HSMs won't expose the RSAKey interface, so we have to check to see if we can cast - // If so, we can provide additional safety checks: - if (key instanceof RSAKey) { - RSAKey rsaKey = (RSAKey) key; - int size = rsaKey.getModulus().bitLength(); - if (size < MIN_KEY_BIT_LENGTH) { - String id = getId(); - String section = id.startsWith("PS") ? "3.5" : "3.3"; - String msg = "The " + keyType(signing) + " key's size is " + size + " bits which is not secure " + - "enough for the " + id + " algorithm. The JWT JWA Specification (RFC 7518, Section " + - section + ") states that RSA keys MUST have a size >= " + MIN_KEY_BIT_LENGTH + " bits. " + - "Consider using the Jwts.SIG." + id + ".keyPair() builder to create a " + - "KeyPair guaranteed to be secure enough for " + id + ". See " + - "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; - throw new WeakKeyException(msg); - } + if (!isRsaAlgorithmName(key)) { + throw new UnsupportedKeyException("Unsupported RSA or RSASSA-PSS key algorithm name."); + } + int size = KeysBridge.findBitLength(key); + if (size < 0) return; // https://github.com/jwtk/jjwt/issues/68 + if (size < MIN_KEY_BIT_LENGTH) { + String id = getId(); + String section = id.startsWith("PS") ? "3.5" : "3.3"; + String msg = "The RSA " + keyType(signing) + " key size (aka modulus bit length) is " + size + " bits " + + "which is not secure enough for the " + id + " algorithm. The JWT JWA Specification " + + "(RFC 7518, Section " + section + ") states that RSA keys MUST have a size >= " + + MIN_KEY_BIT_LENGTH + " bits. Consider using the Jwts.SIG." + id + + ".keyPair() builder to create a KeyPair guaranteed to be secure enough for " + id + ". See " + + "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; + throw new WeakKeyException(msg); } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy index 73214c55..edb3753b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AesGcmKeyAlgorithmTest.groovy @@ -22,6 +22,7 @@ import io.jsonwebtoken.impl.DefaultMutableJweHeader import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.CheckedFunction import io.jsonwebtoken.lang.Arrays +import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.SecretKeyBuilder import org.junit.Test @@ -89,7 +90,7 @@ class AesGcmKeyAlgorithmTest { def enc = new GcmAesAeadAlgorithm(keyLength) { @Override SecretKeyBuilder key() { - return new FixedSecretKeyBuilder(cek) + return Keys.builder(cek) } } @@ -127,7 +128,7 @@ class AesGcmKeyAlgorithmTest { def enc = new GcmAesAeadAlgorithm(keyLength) { @Override SecretKeyBuilder key() { - return new FixedSecretKeyBuilder(cek) + return Keys.builder(cek) } } def delegate = new DefaultMutableJweHeader(headerBuilder) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy index 26d7ddf1..07cdaa38 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/CryptoAlgorithmTest.groovy @@ -17,8 +17,6 @@ package io.jsonwebtoken.impl.security import org.junit.Test -import java.security.Provider - import static org.junit.Assert.* class CryptoAlgorithmTest { @@ -68,32 +66,6 @@ class CryptoAlgorithmTest { assertSame Randoms.secureRandom(), random } - @Test - void testNonPkcs11ProviderNullRequest() { - assertNull CryptoAlgorithm.nonPkcs11Provider(null) - } - - @Test - void testNonPkcs11ProviderNullRequestProvider() { - def request = new DefaultRequest('foo', null, null) - assertNull CryptoAlgorithm.nonPkcs11Provider(request) - } - - @Test - void testNonPkcs11ProviderEmptyRequestProviderName() { - String name = null - Provider provider = new TestProvider(name) - def request = new DefaultRequest('foo', provider, null) - assertSame provider, CryptoAlgorithm.nonPkcs11Provider(request) - } - - @Test - void testPkcs11ProviderReturnsNull() { - Provider provider = new TestProvider('SunPKCS11-test') - def request = new DefaultRequest('foo', provider, null) - assertNull CryptoAlgorithm.nonPkcs11Provider(request) - } - class TestCryptoAlgorithm extends CryptoAlgorithm { TestCryptoAlgorithm() { this('test', 'jcaName') @@ -103,11 +75,4 @@ class CryptoAlgorithmTest { super(id, jcaName) } } - - static class TestProvider extends Provider { - public TestProvider(String name) { - super(name, 1.0d, 'info') - } - } - } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithmTest.groovy index 4a2d4d57..b243c632 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultRsaKeyAlgorithmTest.groovy @@ -35,9 +35,25 @@ class DefaultRsaKeyAlgorithmTest { void testValidateNonRSAKey() { SecretKey key = Jwts.KEY.A128KW.key().build() for (DefaultRsaKeyAlgorithm alg : algs) { + try { + alg.validate(key, true) + } catch (UnsupportedKeyException e) { + assertEquals 'Unsupported RSA key algorithm name.', e.getMessage() + } + try { + alg.validate(key, false) + } catch (UnsupportedKeyException e) { + assertEquals 'Unsupported RSA key algorithm name.', e.getMessage() + } + } + } + + @Test + void testValidateRsaKeyWithoutKeySize() { + for (def alg : algs) { // if RSAKey interface isn't exposed (e.g. PKCS11 or HSM), don't error: - alg.validate(key, true) - alg.validate(key, false) + alg.validate(new TestPublicKey(algorithm: 'RSA'), true) + alg.validate(new TestPrivateKey(algorithm: 'RSA'), false) } } @@ -45,7 +61,7 @@ class DefaultRsaKeyAlgorithmTest { void testPssKey() { for (DefaultRsaKeyAlgorithm alg : algs) { RSAPublicKey key = createMock(RSAPublicKey) - expect(key.getAlgorithm()).andReturn(RsaSignatureAlgorithm.PSS_JCA_NAME) + expect(key.getAlgorithm()).andStubReturn(RsaSignatureAlgorithm.PSS_JCA_NAME) replay(key) try { alg.validate(key, true) @@ -61,7 +77,7 @@ class DefaultRsaKeyAlgorithmTest { void testPssOidKey() { for (DefaultRsaKeyAlgorithm alg : algs) { RSAPublicKey key = createMock(RSAPublicKey) - expect(key.getAlgorithm()).andReturn(RsaSignatureAlgorithm.PSS_OID) + expect(key.getAlgorithm()).andStubReturn(RsaSignatureAlgorithm.PSS_OID) replay(key) try { alg.validate(key, true) @@ -77,7 +93,7 @@ class DefaultRsaKeyAlgorithmTest { void testWeakEncryptionKey() { for (DefaultRsaKeyAlgorithm alg : algs) { RSAPublicKey key = createMock(RSAPublicKey) - expect(key.getAlgorithm()).andReturn("RSA") + expect(key.getAlgorithm()).andStubReturn("RSA") expect(key.getModulus()).andReturn(BigInteger.ONE) replay(key) try { @@ -85,9 +101,9 @@ class DefaultRsaKeyAlgorithmTest { } catch (WeakKeyException e) { String id = alg.getId() String section = id.equals("RSA1_5") ? "4.2" : "4.3" - String msg = "The RSA encryption key's size (modulus) is 1 bits which is not secure enough for " + - "the $id algorithm. The JWT JWA Specification (RFC 7518, Section $section) states that " + - "RSA keys MUST have a size >= 2048 bits. " + + String msg = "The RSA encryption key size (aka modulus bit length) is 1 bits which is not secure " + + "enough for the $id algorithm. The JWT JWA Specification (RFC 7518, Section $section) " + + "states that RSA keys MUST have a size >= 2048 bits. " + "See https://www.rfc-editor.org/rfc/rfc7518.html#section-$section for more information." assertEquals(msg, e.getMessage()) } @@ -99,7 +115,7 @@ class DefaultRsaKeyAlgorithmTest { void testWeakDecryptionKey() { for (DefaultRsaKeyAlgorithm alg : algs) { RSAPrivateKey key = createMock(RSAPrivateKey) - expect(key.getAlgorithm()).andReturn("RSA") + expect(key.getAlgorithm()).andStubReturn("RSA") expect(key.getModulus()).andReturn(BigInteger.ONE) replay(key) try { @@ -107,9 +123,9 @@ class DefaultRsaKeyAlgorithmTest { } catch (WeakKeyException e) { String id = alg.getId() String section = id.equals("RSA1_5") ? "4.2" : "4.3" - String msg = "The RSA decryption key's size (modulus) is 1 bits which is not secure enough for " + - "the $id algorithm. The JWT JWA Specification (RFC 7518, Section $section) states that " + - "RSA keys MUST have a size >= 2048 bits. " + + String msg = "The RSA decryption key size (aka modulus bit length) is 1 bits which is not secure " + + "enough for the $id algorithm. The JWT JWA Specification (RFC 7518, Section $section) " + + "states that RSA keys MUST have a size >= 2048 bits. " + "See https://www.rfc-editor.org/rfc/rfc7518.html#section-$section for more information." assertEquals(msg, e.getMessage()) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy index 66cad596..a59e107d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EcSignatureAlgorithmTest.groovy @@ -62,7 +62,7 @@ class EcSignatureAlgorithmTest { @Test void testFindOidKeys() { - for(def alg : EcSignatureAlgorithm.BY_OID.values()) { + for (def alg : EcSignatureAlgorithm.BY_OID.values()) { String name = "${alg.getId()}_OID" String oid = EcSignatureAlgorithm.metaClass.getAttribute(EcSignatureAlgorithm, name) as String assertEquals oid, alg.OID @@ -84,14 +84,24 @@ class EcSignatureAlgorithmTest { } @Test - void testValidateKeyWithoutEcKey() { - PublicKey key = createMock(PublicKey) - replay key + void testValidateKeyWithoutECOrECDSAAlgorithmName() { + PublicKey key = new TestPublicKey(algorithm: 'foo') algs().each { - it.validateKey(key, false) - //no exception - can't check for ECKey fields (e.g. PKCS11 or HSM key) + try { + it.validateKey(key, false) + } catch (Exception e) { + String msg = 'Unsupported EC key algorithm name.' + assertEquals msg, e.getMessage() + } + } + } + + @Test + void testValidateECAlgorithmKeyThatDoesntUseECKeyInterface() { + PublicKey key = new TestPublicKey(algorithm: 'EC') + algs().each { + it.validateKey(key, false) //no exception - can't check for ECKey fields (e.g. PKCS11 or HSM key) } - verify key } @Test @@ -124,12 +134,12 @@ class EcSignatureAlgorithmTest { algs().each { BigInteger order = BigInteger.ONE ECParameterSpec spec = new ECParameterSpec(new EllipticCurve(new TestECField(), BigInteger.ONE, BigInteger.ONE), new ECPoint(BigInteger.ONE, BigInteger.ONE), order, 1) - ECPrivateKey priv = new TestECPrivateKey(params: spec) + ECPrivateKey priv = new TestECPrivateKey(algorithm: 'EC', params: spec) def request = new DefaultSecureRequest(new byte[1], null, null, priv) try { it.digest(request) } catch (InvalidKeyException expected) { - String msg = "The provided Elliptic Curve signing key's size (aka Order bit length) is " + + String msg = "The provided Elliptic Curve signing key size (aka order bit length) is " + "${Bytes.bitsMsg(order.bitLength())}, but the '${it.getId()}' algorithm requires EC Keys with " + "${Bytes.bitsMsg(it.orderBitLength)} per " + "[RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)." as String @@ -146,7 +156,7 @@ class EcSignatureAlgorithmTest { try { Jwts.SIG.ES384.digest(req) } catch (InvalidKeyException expected) { - String msg = "The provided Elliptic Curve signing key's size (aka Order bit length) is " + + String msg = "The provided Elliptic Curve signing key size (aka order bit length) is " + "256 bits (32 bytes), but the 'ES384' algorithm requires EC Keys with " + "384 bits (48 bytes) per " + "[RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)." @@ -178,12 +188,12 @@ class EcSignatureAlgorithmTest { algs().each { BigInteger order = BigInteger.ONE ECParameterSpec spec = new ECParameterSpec(new EllipticCurve(new TestECField(), BigInteger.ONE, BigInteger.ONE), new ECPoint(BigInteger.ONE, BigInteger.ONE), order, 1) - ECPublicKey pub = new TestECPublicKey(params: spec) + ECPublicKey pub = new TestECPublicKey(algorithm: 'EC', params: spec) def request = new DefaultVerifySecureDigestRequest(new byte[1], null, null, pub, new byte[1]) try { it.verify(request) } catch (InvalidKeyException expected) { - String msg = "The provided Elliptic Curve verification key's size (aka Order bit length) is " + + String msg = "The provided Elliptic Curve verification key size (aka order bit length) is " + "${Bytes.bitsMsg(order.bitLength())}, but the '${it.getId()}' algorithm requires EC Keys with " + "${Bytes.bitsMsg(it.orderBitLength)} per " + "[RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.4)." as String diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy index fe6bcbd8..9fe17ca1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy @@ -57,11 +57,11 @@ class EdSignatureAlgorithmTest { * Likely when keys are from an HSM or PKCS key store */ @Test - void testGetAlgorithmJcaNameWhenCantFindCurve() { - def key = new TestKey(algorithm: 'foo') + void testGetRequestJcaNameByKeyAlgorithmNameOnly() { + def key = new TestKey(algorithm: EdwardsCurve.X25519.OID) def payload = [0x00] as byte[] def req = new DefaultSecureRequest(payload, null, null, key) - assertEquals alg.getJcaName(), alg.getJcaName(req) + assertEquals 'X25519', alg.getJcaName(req) // Not the EdDSA default } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pkcs11Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pkcs11Test.groovy index 5c6dea26..725427cb 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pkcs11Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/Pkcs11Test.groovy @@ -26,10 +26,7 @@ import org.junit.Test import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec -import java.security.KeyStore -import java.security.PrivateKey -import java.security.Provider -import java.security.Security +import java.security.* import java.security.cert.X509Certificate import static org.junit.Assert.assertEquals @@ -203,8 +200,11 @@ class Pkcs11Test { return findPkcs11(alg as Identifiable)?.pair } - @Test - void testJws() { + /** + * @param keyProvider the explicit provider to use with JwtBuilder/Parser calls or {@code null} to use the JVM default + * provider(s). + */ + static void testJws(Provider keyProvider) { def algs = [] as List algs.addAll(Jwts.SIG.get().values().findAll({ it != Jwts.SIG.EdDSA })) // EdDSA accounted for by next two: @@ -224,23 +224,34 @@ class Pkcs11Test { alg = alg instanceof Curve ? Jwts.SIG.EdDSA : alg as SecureDigestAlgorithm - // We need to specify the PKCS11 provider since we can't access the private key material: - def jws = Jwts.builder().provider(PKCS11).issuer('me').signWith(signKey, alg).compact() + // We might need to specify the PKCS11 provider since we can't access the private key material: + def jws = Jwts.builder().provider(keyProvider).issuer('me').signWith(signKey, alg).compact() - // We only need to specify a provider during parsing for MAC HSM keys: SignatureAlgorithm verification only - // needs the PublicKey, and a recipient doesn't need/won't have an HSM for public material anyway. - Provider provider = verifyKey instanceof SecretKey ? PKCS11 : null - String iss = Jwts.parser().provider(provider).verifyWith(verifyKey).build() - .parseClaimsJws(jws).getPayload().getIssuer() + def builder = Jwts.parser() + if (verifyKey instanceof SecretKey) { + // We only need to specify a provider during parsing for MAC HSM keys: SignatureAlgorithm verification + // only needs the PublicKey, and a recipient doesn't need/won't have an HSM for public material anyway. + verifyKey = Keys.builder(verifyKey).provider(keyProvider).build() + builder.verifyWith(verifyKey as SecretKey) + } else { + builder.verifyWith(verifyKey as PublicKey) + } + String iss = builder.build().parseClaimsJws(jws).getPayload().getIssuer() assertEquals 'me', iss } } + @Test + void testJws() { + testJws(PKCS11) + } + // create a jwe and then decrypt it - static void encRoundtrip(TestKeys.Bundle bundle, def keyalg) { + static void encRoundtrip(TestKeys.Bundle bundle, def keyalg, Provider provider /* may be null */) { def pair = bundle.pair def pub = pair.public + def priv = pair.private if (pub.getAlgorithm().startsWith(EdwardsCurve.OID_PREFIX)) { // If < JDK 11, the PKCS11 KeyStore doesn't understand X25519 and X448 algorithms, and just returns // a generic X509Key from the X.509 certificate, but that can't be used for encryption. So we'll @@ -251,10 +262,9 @@ class Pkcs11Test { def cert = new JcaTemplate("X.509", TestKeys.BC).generateX509Certificate(bundle.cert.getEncoded()) bundle.cert = cert bundle.chain = [cert] - bundle.pair = new java.security.KeyPair(cert.getPublicKey(), bundle.pair.private) + bundle.pair = new java.security.KeyPair(cert.getPublicKey(), priv) pub = bundle.pair.public } - def priv = pair.private != null ? Keys.wrap(pair.private, pub) : null // Encryption uses the public key, and that key material is available, so no need for the PKCS11 provider: String jwe = Jwts.builder().issuer('me').encryptWith(pub, keyalg, Jwts.ENC.A256GCM).compact() @@ -263,15 +273,15 @@ class Pkcs11Test { // encryption only worked because generic X.509 decoding (from the key certificate in the keystore) produced the // public key. So we can only decrypt if SunPKCS11 supports the private key, so check for non-null: if (priv) { - // Decryption needs the private key, and that is inside the HSM, so the PKCS11 provider is required: - String iss = Jwts.parser().provider(PKCS11).decryptWith(priv).build().parseClaimsJwe(jwe).getPayload().getIssuer() + // Decryption may need private material inside the HSM: + priv = Keys.builder(pair.private).publicKey(pub).provider(provider).build() + + String iss = Jwts.parser().decryptWith(priv).build().parseClaimsJwe(jwe).getPayload().getIssuer() assertEquals 'me', iss } } - @Test - void testJwe() { - + static void testJwe(Provider provider) { def algs = [] algs.addAll(Jwts.SIG.get().values().findAll({ it.id.startsWith('RS') || it.id.startsWith('ES') @@ -293,11 +303,11 @@ class Pkcs11Test { if (name == 'RSA') { // SunPKCS11 doesn't support RSA-OAEP* ciphers :( // So we can only try with RSA1_5 and we have to skip RSA_OAEP and RSA_OAEP_256: - encRoundtrip(bundle, Jwts.KEY.RSA1_5) + encRoundtrip(bundle, Jwts.KEY.RSA1_5, provider) } else if (StandardCurves.findByKey(bundle.pair.public) != null) { // EC or Ed key // try all ECDH key algorithms: Jwts.KEY.get().values().findAll({ it.id.startsWith('ECDH-ES') }).each { - encRoundtrip(bundle, it) + encRoundtrip(bundle, it, provider) } } else { throw new IllegalStateException("Unexpected key algorithm: $name") @@ -305,4 +315,25 @@ class Pkcs11Test { } } } + + @Test + void testJwe() { + testJwe(PKCS11) + } + + /** + * Ensures that for all JWE and JWS algorithms, when the PKCS11 provider is installed as a JVM provider, + * no calls to JwtBuilder/Parser .provider are needed, and no ProviderKeys (Keys.builder) calls are needed + * anywhere in application code. + */ + @Test + void testPkcs11JvmProviderDoesNotRequireProviderKeys() { + Security.addProvider(PKCS11) + try { + testJws(null) + testJwe(null) + } finally { + Security.removeProvider(PKCS11.getName()) + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidedKeyBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidedKeyBuilderTest.groovy new file mode 100644 index 00000000..8c34bd16 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidedKeyBuilderTest.groovy @@ -0,0 +1,44 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.security.Keys +import org.junit.Test + +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.security.Provider + +import static org.junit.Assert.assertSame + +class ProvidedKeyBuilderTest { + + @Test + void testBuildWithSpecifiedProviderKey() { + Provider provider = new TestProvider() + SecretKey key = new SecretKeySpec(Bytes.random(256), 'AES') + def providerKey = Keys.builder(key).provider(provider).build() as ProviderSecretKey + + assertSame provider, providerKey.getProvider() + assertSame key, providerKey.getKey() + + // now for the test: ensure that our provider key isn't wrapped again + SecretKey returned = Keys.builder(providerKey).provider(new TestProvider('different')).build() + + assertSame providerKey, returned // not wrapped again + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidedSecretKeyBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidedSecretKeyBuilderTest.groovy new file mode 100644 index 00000000..f1b99707 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProvidedSecretKeyBuilderTest.groovy @@ -0,0 +1,36 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.security.Keys +import org.junit.Test + +import static org.junit.Assert.assertSame + +class ProvidedSecretKeyBuilderTest { + + @Test + void testBuildPasswordWithoutProvider() { + def password = Keys.password('foo'.toCharArray()) + assertSame password, Keys.builder(password).build() // does not wrap in ProviderKey + } + + @Test + void testBuildPasswordWithProvider() { + def password = Keys.password('foo'.toCharArray()) + assertSame password, Keys.builder(password).provider(new TestProvider()).build() // does not wrap in ProviderKey + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProviderKeyTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProviderKeyTest.groovy new file mode 100644 index 00000000..f5c1640d --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/ProviderKeyTest.groovy @@ -0,0 +1,86 @@ +/* + * Copyright © 2023 jsonwebtoken.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.jsonwebtoken.impl.security + +import io.jsonwebtoken.impl.lang.Bytes +import org.junit.Test + +import java.security.Provider + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class ProviderKeyTest { + + static final Provider PROVIDER = new TestProvider() + + @Test(expected = IllegalArgumentException) + void testConstructorWithNullProvider() { + new ProviderKey<>(null, TestKeys.HS256) + } + + @Test(expected = IllegalArgumentException) + void testConstructorWithNullKey() { + new ProviderKey<>(PROVIDER, null) + } + + @Test + void testConstructorWithProviderKey() { + def key = new ProviderKey(PROVIDER, TestKeys.HS256) + // wrapping throws an exception: + try { + new ProviderKey<>(PROVIDER, key) + } catch (IllegalArgumentException iae) { + String msg = 'Nesting not permitted.' + assertEquals msg, iae.getMessage() + } + } + + @Test + void testGetKey() { + def src = new TestKey() + def key = new ProviderKey(PROVIDER, src) + assertSame src, key.getKey() + } + + @Test + void testGetProvider() { + def src = new TestKey() + def key = new ProviderKey(PROVIDER, src) + assertSame PROVIDER, key.getProvider() + } + + @Test + void testGetAlgorithm() { + String name = 'myAlg' + def key = new ProviderKey(PROVIDER, new TestKey(algorithm: name)) + assertEquals name, key.getAlgorithm() + } + + @Test + void testGetFormat() { + String name = 'myFormat' + def key = new ProviderKey(PROVIDER, new TestKey(format: name)) + assertEquals name, key.getFormat() + } + + @Test + void testGetEncoded() { + byte[] encoded = Bytes.random(256) + def key = new ProviderKey(PROVIDER, new TestKey(encoded: encoded)) + assertSame encoded, key.getEncoded() + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy index cf4523aa..b25b0941 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7516AppendixA3Test.groovy @@ -125,7 +125,7 @@ class RFC7516AppendixA3Test { @Override SecretKeyBuilder key() { - return new FixedSecretKeyBuilder(CEK) + return Keys.builder(CEK) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy index f9c711f0..edef0af9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7517AppendixCTest.groovy @@ -285,7 +285,7 @@ class RFC7517AppendixCTest { def enc = new HmacAesAeadAlgorithm(128) { @Override SecretKeyBuilder key() { - return new FixedSecretKeyBuilder(RFC_CEK) + return Keys.builder(RFC_CEK) } @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy index 0626328f..e769cdf1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RsaSignatureAlgorithmTest.groovy @@ -20,6 +20,7 @@ import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.CheckedFunction import io.jsonwebtoken.lang.Assert import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.UnsupportedKeyException import io.jsonwebtoken.security.WeakKeyException import org.junit.Test @@ -29,7 +30,7 @@ import java.security.PublicKey import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey -import static org.easymock.EasyMock.* +import static org.easymock.EasyMock.createMock import static org.junit.Assert.* class RsaSignatureAlgorithmTest { @@ -51,14 +52,37 @@ class RsaSignatureAlgorithmTest { } @Test - void testValidateKeyWithoutRsaKey() { - PublicKey key = createMock(PublicKey) - replay key + void testValidateKeyWithoutRSAorRSASSAPSSAlgorithmName() { + PublicKey key = new TestPublicKey(algorithm: 'foo') algs.each { - it.validateKey(key, false) - //no exception - can't check for RSAKey fields (e.g. PKCS11 or HSM key) + try { + it.validateKey(key, false) + } catch (Exception e) { + String msg = 'Unsupported RSA or RSASSA-PSS key algorithm name.' + assertEquals msg, e.getMessage() + } + } + } + + @Test + void testValidateRSAAlgorithmKeyThatDoesntUseRSAKeyInterface() { + PublicKey key = new TestPublicKey(algorithm: 'RSA') + algs.each { + it.validateKey(key, false) //no exception - can't check for RSAKey length + } + } + + @Test + void testValidateKeyWithoutRsaKey() { + PublicKey key = TestKeys.ES256.pair.public // not an RSA key + algs.each { + try { + it.validateKey(key, false) + } catch (UnsupportedKeyException e) { + String msg = 'Unsupported RSA or RSASSA-PSS key algorithm name.' + assertEquals msg, e.getMessage() + } } - verify key } @Test @@ -99,8 +123,9 @@ class RsaSignatureAlgorithmTest { } catch (WeakKeyException expected) { String id = it.getId() String section = id.startsWith('PS') ? '3.5' : '3.3' - String msg = "The signing key's size is 1024 bits which is not secure enough for the ${it.getId()} " + - "algorithm. The JWT JWA Specification (RFC 7518, Section ${section}) states that RSA keys " + + String msg = "The RSA signing key size (aka modulus bit length) is 1024 bits which is not secure " + + "enough for the ${it.getId()} algorithm. The JWT JWA Specification (RFC 7518, Section " + + "${section}) states that RSA keys " + "MUST have a size >= 2048 bits. Consider using the Jwts.SIG.${id}.keyPair() " + "builder to create a KeyPair guaranteed to be secure enough for ${id}. See " + "https://tools.ietf.org/html/rfc7518#section-${section} for more information." diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/FixedSecretKeyBuilder.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestProvider.groovy similarity index 54% rename from impl/src/test/groovy/io/jsonwebtoken/impl/security/FixedSecretKeyBuilder.groovy rename to impl/src/test/groovy/io/jsonwebtoken/impl/security/TestProvider.groovy index 25066418..197a253e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/FixedSecretKeyBuilder.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/TestProvider.groovy @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 jsonwebtoken.io + * Copyright © 2023 jsonwebtoken.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,32 +15,15 @@ */ package io.jsonwebtoken.impl.security -import io.jsonwebtoken.security.SecretKeyBuilder - -import javax.crypto.SecretKey import java.security.Provider -import java.security.SecureRandom -class FixedSecretKeyBuilder implements SecretKeyBuilder { +class TestProvider extends Provider { - final SecretKey key - - FixedSecretKeyBuilder(SecretKey key) { - this.key = key + TestProvider() { + this('test') } - @Override - SecretKey build() { - return this.key - } - - @Override - SecretKeyBuilder provider(Provider provider) { - return this - } - - @Override - SecretKeyBuilder random(SecureRandom random) { - return this + TestProvider(String name) { + super(name, 1.0d, 'info') } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index 2a73e2b7..0f08ef62 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -209,7 +209,7 @@ class KeysTest { void testKeyPairBuilder() { Collection algs = Jwts.SIG.get().values() - .findAll({it instanceof KeyPairBuilderSupplier}) as Collection + .findAll({ it instanceof KeyPairBuilderSupplier }) as Collection for (SignatureAlgorithm alg : algs) { @@ -288,15 +288,19 @@ class KeysTest { } @Test - void testAssociateWithKeySupplier() { - def pair = TestKeys.ES256.pair - def key = new PrivateECKey(pair.private, pair.public.getParams()) - assertSame key, Keys.wrap(key, pair.public) + void testAssociateWithECKey() { + def priv = new TestPrivateKey(algorithm: 'EC') + def pub = TestKeys.ES256.pair.public as ECPublicKey + def result = Keys.builder(priv).publicKey(pub).build() + assertTrue result instanceof PrivateECKey + def key = result as PrivateECKey + assertSame priv, key.getKey() + assertSame pub.getParams(), key.getParams() } @Test void testAssociateWithKeyThatDoesntNeedToBeWrapped() { def pair = TestKeys.RS256.pair - assertSame pair.private, Keys.wrap(pair.private, pair.public) + assertSame pair.private, Keys.builder(pair.private).publicKey(pair.public).build() } } diff --git a/pom.xml b/pom.xml index 5df554bd..99d653fa 100644 --- a/pom.xml +++ b/pom.xml @@ -327,7 +327,7 @@ **/lombok.config .gitattributes **/genkeys - **/softhsmimport + **/softhsm