A Key {@code Locator} is invoked once during parsing before performing decryption or signature verification.
*
+ * 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:
+ *
+ * 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:
+ * 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