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
This commit is contained in:
lhazlewood 2023-09-05 13:08:31 -07:00 committed by GitHub
parent c3ff0bbf12
commit 8e0f740329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 935 additions and 350 deletions

View File

@ -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.
<a name="jwt-read-key"></a>
### 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`).
<a name="key-locator-provider"></a>
#### 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;
}
```
<a name="jwt-read-claims"></a><a name="jws-read-claims"></a> <!-- legacy anchor for old links -->
### 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.
<a name="jws-create-key-algoverride"></a>
##### 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.
<a name="jwe-read-decompression"></a>
#### 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
<a name="jwk-thumbprint-kid"></a>
@ -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

View File

@ -95,7 +95,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
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<JwtParser> {
* <li>If the parsed String is a JWE, it will be called to find the appropriate decryption key.</li>
* </ul>
*
* <p>Specifying a key {@code Locator} is necessary when the signature verification or decryption key is not
* <p>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:</p>
@ -453,6 +453,38 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
*
* <p>A Key {@code Locator} is invoked once during parsing before performing decryption or signature verification.</p>
*
* <p><b>Provider-constrained Keys</b></p>
*
* <p>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:</p>
*
* <ol>
* <li><a href="https://docs.oracle.com/en/java/javase/17/security/howtoimplaprovider.html#GUID-831AA25F-F702-442D-A2E4-8DA6DEA16F33">
* Configure the Provider in the JVM</a>, 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.</li>
* <li>Specify the {@code Provider} as the {@code JwtParser} default via {@link #provider(Provider)}. This will
* ensure the provider is used by default with <em>all</em> 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.</li>
* <li>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.</li>
* </ol>
*
* <p>If you need to use option &#35;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:</p>
* <blockquote><pre>
* public Key locate(Header&lt;?&gt; 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();
* }</pre></blockquote>
*
* @param keyLocator the locator used to retrieve decryption or signature verification keys.
* @return the parser builder for method chaining.
* @since JJWT_RELEASE_VERSION

View File

@ -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> 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:
*
* <p>If the private key instance is already capable of representing public information (because it
* implements one of the <code>java.security.interfaces.{ECKey,RSAKey,XECKey,EdECKey}</code> interfaces),
* this method does nothing and returns the private key unaltered.</p>
* <blockquote><pre>
* SecretKey key = Keys.builder(key).provider(mandatoryProvider).build();</pre></blockquote>
*
* <p>If however the private key instance does <em>not</em> 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.</p>
* <p>Cryptographic algorithm implementations can inspect the resulting {@code key} instance and obtain its
* mandatory {@code Provider} if necessary.</p>
*
* @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
* <code>java.security.interfaces.{ECKey,RSAKey,XECKey,EdECKey}</code> 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
* <code>java.security.interfaces.{ECKey,RSAKey,XECKey,EdECKey}</code> interfaces
* <p>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.</p>
*
* @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:
*
* <blockquote><pre>
* PrivateKey key = Keys.builder(privateKey).publicKey(publicKey).provider(mandatoryProvider).build();</pre></blockquote>
*
* <p>Cryptographic algorithm implementations can inspect the resulting {@code key} instance and obtain its
* mandatory {@code Provider} or {@code PublicKey} if necessary.</p>
*
* <p>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.</p>
*
* @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);
}
}

View File

@ -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<PrivateKey, PrivateKeyBuilder> {
PrivateKeyBuilder publicKey(PublicKey publicKey);
}

View File

@ -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<Key> keyRequest = new DefaultKeyRequest<>(this.key, this.provider, this.secureRandom, delegate, enc);
KeyRequest<Key> 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.");

View File

@ -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<Key> 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<Key> 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<byte[]> result = encAlg.decrypt(decryptRequest);
payload = result.getPayload();

View File

@ -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<T, B extends SecurityBuilder<T, B>> implements SecurityBuilder<T, B> {
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();
}
}

View File

@ -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 <em>not</em> 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.");
}

View File

@ -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<KeyPair, KeyPairBuilder> 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);

View File

@ -124,7 +124,7 @@ final class DefaultMacAlgorithm extends AbstractSecureDigestAlgorithm<SecretKey,
return new DefaultSecretKeyBuilder(getJcaName(), getKeyBitLength());
}
private String assertAlgorithmName(SecretKey key, boolean signing) {
private void assertAlgorithmName(SecretKey key, boolean signing) {
String name = key.getAlgorithm();
if (!Strings.hasText(name)) {
@ -143,8 +143,6 @@ final class DefaultMacAlgorithm extends AbstractSecureDigestAlgorithm<SecretKey,
"' does not equal a valid HmacSHA* algorithm name or PKCS12 OID and cannot be used with " +
getId() + ".");
}
return name;
}
@Override

View File

@ -27,11 +27,9 @@ import io.jsonwebtoken.security.WeakKeyException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.math.BigInteger;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAKey;
import java.security.spec.AlgorithmParameterSpec;
/**
@ -58,28 +56,27 @@ public class DefaultRsaKeyAlgorithm extends CryptoAlgorithm implements KeyAlgori
protected void validate(Key key, boolean encryption) { // true = encryption, false = decryption
if (!RsaSignatureAlgorithm.isRsaAlgorithmName(key)) {
throw new UnsupportedKeyException("Unsupported RSA key algorithm name.");
}
if (RsaSignatureAlgorithm.isPss(key)) {
String msg = "RSASSA-PSS keys may not be used for " + keyType(encryption) +
", only digital signature algorithms.";
throw new UnsupportedKeyException(msg);
}
// 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;
BigInteger modulus = Assert.notNull(rsaKey.getModulus(), "RSAKey modulus cannot be null.");
int size = modulus.bitLength();
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's size (modulus) 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);
}
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);
}
}

View File

@ -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<SecretKey, SecretKeyBuilder>
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);

View File

@ -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<String> 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<String, SignatureAlgorithm> 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<Signature, byte[]>() {
@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);

View File

@ -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<PublicKey
return jca(request).withKeyAgreement(new CheckedFunction<KeyAgreement, byte[]>() {
@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<PublicKey
// Generate our ephemeral key pair:
final SecureRandom random = ensureSecureRandom(request);
final Provider provider = nonPkcs11Provider(request); // PKCS11 / HSM check
DynamicJwkBuilder<?, ?> 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<PublicKey
throw new InvalidKeyException(msg);
}
// PKCS11 unwrap if necessary:
if (privateKey instanceof KeySupplier) {
Key key = ((KeySupplier<?>) 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<SecretKey> unwrapReq = new DefaultDecryptionKeyRequest<>(request.getPayload(),

View File

@ -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);

View File

@ -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 <K> the key type
* @return the lowest-level/root key available.
*/
@SuppressWarnings("unchecked")
public static <K extends Key> K root(K key) {
return (key instanceof KeySupplier<?>) ? (K) root((KeySupplier<?>) key) : key;
}
public static <K extends Key> K root(KeySupplier<K> 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) {

View File

@ -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<K extends Key, B extends KeyBuilder<K, B>> extends AbstractSecurityBuilder<K, B>
implements KeyBuilder<K, B> {
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();
}

View File

@ -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<PrivateKey, PrivateKeyBuilder>
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;
}
}

View File

@ -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<SecretKey, SecretKeyBuilder> 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;
}
}

View File

@ -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<T extends Key> implements Key, KeySupplier<T> {
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 extends Key> K getKey(K key) {
return key instanceof ProviderKey ? ((ProviderKey<K>) 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;
}
}

View File

@ -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<PrivateKey> implements PrivateKey {
ProviderPrivateKey(Provider provider, PrivateKey key) {
super(provider, key);
}
}

View File

@ -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<SecretKey> implements SecretKey {
ProviderSecretKey(Provider provider, SecretKey key) {
super(provider, key);
}
}

View File

@ -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<String> PSS_ALG_NAMES = Collections.setOf(PSS_JCA_NAME, PSS_OID);
private static final Set<String> 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);
}
}

View File

@ -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)

View File

@ -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')
}
}
}

View File

@ -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())
}

View File

@ -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

View File

@ -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

View File

@ -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<Identifiable>
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())
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -125,7 +125,7 @@ class RFC7516AppendixA3Test {
@Override
SecretKeyBuilder key() {
return new FixedSecretKeyBuilder(CEK)
return Keys.builder(CEK)
}
}

View File

@ -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

View File

@ -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."

View File

@ -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')
}
}

View File

@ -209,7 +209,7 @@ class KeysTest {
void testKeyPairBuilder() {
Collection<SignatureAlgorithm> algs = Jwts.SIG.get().values()
.findAll({it instanceof KeyPairBuilderSupplier}) as Collection<SignatureAlgorithm>
.findAll({ it instanceof KeyPairBuilderSupplier }) as Collection<SignatureAlgorithm>
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()
}
}

View File

@ -327,7 +327,7 @@
<exclude>**/lombok.config</exclude>
<exclude>.gitattributes</exclude>
<exclude>**/genkeys</exclude>
<exclude>**/softhsmimport</exclude>
<exclude>**/softhsm</exclude>
</excludes>
</licenseSet>
</licenseSets>