This is an automated email from the ASF dual-hosted git repository. dblevins pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomee.git
commit 27ba03dc9a46d4775eb160add4cb77902db6898c Author: David Blevins <dblev...@tomitribe.com> AuthorDate: Tue Aug 30 13:43:28 2022 -0700 TOMEE-3947 Elliptic Curve ES256 signature algorithm --- boms/tomee-microprofile/pom.xml | 11 + boms/tomee-plume/pom.xml | 11 + boms/tomee-plus/pom.xml | 11 + .../jwt/itest/PublicKeyLocationTest.java | 1 + mp-jwt/pom.xml | 5 + .../apache/tomee/microprofile/jwt/MPJWTFilter.java | 5 +- .../jwt/config/JWTAuthConfigurationProperties.java | 17 +- .../microprofile/jwt/config/PublicKeyResolver.java | 228 ++++++--------------- pom.xml | 2 +- 9 files changed, 109 insertions(+), 182 deletions(-) diff --git a/boms/tomee-microprofile/pom.xml b/boms/tomee-microprofile/pom.xml index 4af267bce1..996d744a03 100644 --- a/boms/tomee-microprofile/pom.xml +++ b/boms/tomee-microprofile/pom.xml @@ -210,6 +210,17 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>io.churchkey</groupId> + <artifactId>churchkey</artifactId> + <version>1.22</version> + <exclusions> + <exclusion> + <artifactId>*</artifactId> + <groupId>*</groupId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>io.opentracing.contrib</groupId> <artifactId>opentracing-concurrent</artifactId> diff --git a/boms/tomee-plume/pom.xml b/boms/tomee-plume/pom.xml index f4a48441b8..247953c7e2 100644 --- a/boms/tomee-plume/pom.xml +++ b/boms/tomee-plume/pom.xml @@ -221,6 +221,17 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>io.churchkey</groupId> + <artifactId>churchkey</artifactId> + <version>1.22</version> + <exclusions> + <exclusion> + <artifactId>*</artifactId> + <groupId>*</groupId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>io.opentracing.contrib</groupId> <artifactId>opentracing-concurrent</artifactId> diff --git a/boms/tomee-plus/pom.xml b/boms/tomee-plus/pom.xml index 51e84ce84b..91e16806b6 100644 --- a/boms/tomee-plus/pom.xml +++ b/boms/tomee-plus/pom.xml @@ -232,6 +232,17 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>io.churchkey</groupId> + <artifactId>churchkey</artifactId> + <version>1.22</version> + <exclusions> + <exclusion> + <artifactId>*</artifactId> + <groupId>*</groupId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>io.opentracing.contrib</groupId> <artifactId>opentracing-concurrent</artifactId> diff --git a/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/PublicKeyLocationTest.java b/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/PublicKeyLocationTest.java index 4cd49c1e87..453bf1686f 100644 --- a/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/PublicKeyLocationTest.java +++ b/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/PublicKeyLocationTest.java @@ -82,6 +82,7 @@ public class PublicKeyLocationTest { .asJar(); final TomEE tomee = TomEE.microprofile() +// .update() .add("webapps/test/WEB-INF/beans.xml", "") .add("webapps/test/WEB-INF/lib/app.jar", appJar) .build(); diff --git a/mp-jwt/pom.xml b/mp-jwt/pom.xml index cad40a98d7..183868b28d 100644 --- a/mp-jwt/pom.xml +++ b/mp-jwt/pom.xml @@ -84,6 +84,11 @@ <artifactId>jose4j</artifactId> <version>0.7.9</version> </dependency> + <dependency> + <groupId>io.churchkey</groupId> + <artifactId>churchkey</artifactId> + <version>1.22</version> + </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java index ea6a1de6eb..3ded1ff31b 100644 --- a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java @@ -335,7 +335,10 @@ public class MPJWTFilter implements Filter { new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.WHITELIST, AlgorithmIdentifiers.RSA_USING_SHA256, AlgorithmIdentifiers.RSA_USING_SHA384, - AlgorithmIdentifiers.RSA_USING_SHA512 + AlgorithmIdentifiers.RSA_USING_SHA512, + AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256, + AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384, + AlgorithmIdentifiers.ECDSA_USING_P521_CURVE_AND_SHA512 )); if (!authContextInfo.isAllowNoExpiryClaim()) { diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationProperties.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationProperties.java index 47050a6885..87a8350d6b 100644 --- a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationProperties.java +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationProperties.java @@ -16,17 +16,15 @@ */ package org.apache.tomee.microprofile.jwt.config; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.ConfigProvider; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.Initialized; import jakarta.enterprise.event.Observes; import jakarta.enterprise.inject.spi.DeploymentException; import jakarta.servlet.ServletContext; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + import java.security.Key; -import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Optional; @@ -44,7 +42,6 @@ import static org.eclipse.microprofile.jwt.config.Names.VERIFIER_PUBLIC_KEY_LOCA */ @ApplicationScoped public class JWTAuthConfigurationProperties { - public static final List<String> JWK_SUPPORTED_KEY_TYPES = Collections.singletonList("RSA"); public static final String PUBLIC_KEY_ERROR = "Could not read MicroProfile Public Key"; public static final String PUBLIC_KEY_ERROR_LOCATION = PUBLIC_KEY_ERROR + " from Location: "; @@ -75,10 +72,10 @@ public class JWTAuthConfigurationProperties { private JWTAuthConfiguration createJWTAuthConfiguration() { if (getVerifierPublicKey().isPresent() && getPublicKeyLocation().isPresent()) { throw new DeploymentException("Both " + - VERIFIER_PUBLIC_KEY + - " and " + - VERIFIER_PUBLIC_KEY_LOCATION + - " are being supplied. You must use only one."); + VERIFIER_PUBLIC_KEY + + " and " + + VERIFIER_PUBLIC_KEY_LOCATION + + " are being supplied. You must use only one."); } final Optional<String> publicKeyContents = getVerifierPublicKey(); diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/PublicKeyResolver.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/PublicKeyResolver.java index 89b928950a..a32e669473 100644 --- a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/PublicKeyResolver.java +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/PublicKeyResolver.java @@ -16,39 +16,26 @@ */ package org.apache.tomee.microprofile.jwt.config; -import org.jose4j.jwk.JsonWebKey; -import org.jose4j.jwk.JsonWebKeySet; -import org.jose4j.lang.JoseException; - +import io.churchkey.Keys; +import io.churchkey.util.Pem; import jakarta.enterprise.inject.spi.DeploymentException; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonObject; -import jakarta.json.JsonValue; -import jakarta.json.stream.JsonParsingException; -import java.io.BufferedReader; +import org.apache.openejb.loader.IO; + import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.StringReader; -import java.io.StringWriter; import java.net.URISyntaxException; import java.net.URL; import java.security.Key; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; -import java.util.Base64; -import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.jose4j.jwk.JsonWebKeySet.JWK_SET_MEMBER_NAME; +import static io.churchkey.Key.Type.PRIVATE; +import static io.churchkey.Key.Type.SECRET; public class PublicKeyResolver { @@ -65,25 +52,60 @@ public class PublicKeyResolver { } public Map<String, Key> readPublicKeys(final String publicKey) { - final Stream<Supplier<Map<String, Key>>> possiblePublicKeysParses = - Stream.of(() -> parsePCKS8(publicKey), - () -> parseJwk(publicKey), - () -> parseJwkDecoded(publicKey), - () -> parseJwks(publicKey), - () -> parseJwksDecoded(publicKey)); + final List<io.churchkey.Key> keys; + try { + keys = Keys.decodeSet(publicKey); + } catch (Exception e) { + throw new DeploymentException("Unable to decode key contents: " + publicKey); + } - return possiblePublicKeysParses - .map(Supplier::get) - .filter(keys -> !keys.isEmpty()) - .findFirst() - .orElseThrow(() -> new DeploymentException(": " + publicKey)); + if (keys.size() == 0) { + throw new DeploymentException("No keys found in key contents: " + publicKey); + } + + checkInvalidTypes(keys, PRIVATE); + checkInvalidTypes(keys, SECRET); + + int unknown = 0; + + final Map<String, Key> map = new HashMap<>(); + for (final io.churchkey.Key key : keys) { + final String id; + if (defined(key, "kid")) { + id = key.getAttribute("kid"); + } else if (defined(key, "Comment")) { + id = key.getAttribute("Comment"); + } else { + id = "Unknown " + (unknown++); + } + + map.put(id, key.getKey()); + } + + return map; + } + + private boolean defined(final io.churchkey.Key key, final String kid) { + final String attribute = key.getAttribute(kid); + return attribute != null && attribute.length() > 0; + } + + private void checkInvalidTypes(final List<io.churchkey.Key> keys, final io.churchkey.Key.Type type) { + final long invalid = keys.stream() + .map(io.churchkey.Key::getType) + .filter(type::equals) + .count(); + + if (invalid > 0) { + throw new DeploymentException("Found " + invalid + " " + type.name().toLowerCase() + + " keys in MP JWT key configuration. Only Public Keys must be configured for JWT validation"); + } } private Map<String, Key> readPublicKeysFromLocation(final String publicKeyLocation) { final Stream<Supplier<Optional<String>>> possiblePublicKeysLocations = Stream.of(() -> readPublicKeysFromClasspath(publicKeyLocation), () -> readPublicKeysFromFile(publicKeyLocation), - () -> readPublicKeysFromHttp(publicKeyLocation), () -> readPublicKeysFromUrl(publicKeyLocation)); return possiblePublicKeysLocations @@ -103,7 +125,7 @@ public class PublicKeyResolver { if (is == null) { return Optional.empty(); } - return Optional.of(readPublicKeyFromInputStream(is)); + return Optional.of(IO.slurp(is)); } catch (final IOException e) { throw new DeploymentException( JWTAuthConfigurationProperties.PUBLIC_KEY_ERROR_LOCATION + publicKeyLocation, e); @@ -125,155 +147,21 @@ public class PublicKeyResolver { publicKeyLocation + ". File does not exist or it is a directory."); } - return Optional.of(readPublicKeyFromInputStream(locationURL.openStream())); + return Optional.of(IO.slurp(locationURL)); } catch (final IOException | URISyntaxException e) { throw new DeploymentException( JWTAuthConfigurationProperties.PUBLIC_KEY_ERROR_LOCATION + publicKeyLocation, e); } } - private Optional<String> readPublicKeysFromHttp(final String publicKeyLocation) { - if (!publicKeyLocation.startsWith("http")) { - return Optional.empty(); - } - - try { - final URL locationURL = new URL(publicKeyLocation); - return Optional.of(readPublicKeyFromInputStream(locationURL.openStream())); - } catch (final IOException e) { - throw new DeploymentException( - JWTAuthConfigurationProperties.PUBLIC_KEY_ERROR_LOCATION + publicKeyLocation, e); - } - } - private Optional<String> readPublicKeysFromUrl(final String publicKeyLocation) { try { final URL locationURL = new URL(publicKeyLocation); - return Optional.of(readPublicKeyFromInputStream(locationURL.openStream())); + return Optional.of(IO.slurp(locationURL)); } catch (final IOException e) { throw new DeploymentException( JWTAuthConfigurationProperties.PUBLIC_KEY_ERROR_LOCATION + publicKeyLocation, e); } } - private String readPublicKeyFromInputStream(final InputStream publicKey) throws IOException { - final StringWriter content = new StringWriter(); - try (final BufferedReader reader = new BufferedReader(new InputStreamReader(publicKey))) { - String line = reader.readLine(); - while (line != null) { - content.write(line); - content.write('\n'); - line = reader.readLine(); - } - } - return content.toString(); - } - - private Map<String, Key> parsePCKS8(final String publicKey) { - try { - final X509EncodedKeySpec spec = new X509EncodedKeySpec(normalizeAndDecodePCKS8(publicKey)); - final KeyFactory kf = KeyFactory.getInstance("RSA"); - return Collections.singletonMap(null, kf.generatePublic(spec)); - } catch (final NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException e) { - return Collections.emptyMap(); - } - } - - private Map<String, Key> parseJwk(final String publicKey) { - final JsonObject jwk; - try { - jwk = Json.createReader(new StringReader(publicKey)).readObject(); - } catch (final JsonParsingException e) { - return Collections.emptyMap(); - } - - if (jwk.containsKey(JWK_SET_MEMBER_NAME)) { - return Collections.emptyMap(); - } - - validateJwk(jwk); - - try { - final JsonWebKey key = JsonWebKey.Factory.newJwk(publicKey); - return Collections.singletonMap(key.getKeyId(), key.getKey()); - } catch (final JoseException e) { - throw new DeploymentException(JWTAuthConfigurationProperties.PUBLIC_KEY_ERROR + " JWK.", e); - } - } - - private Map<String, Key> parseJwkDecoded(final String publicKey) { - final String publicKeyDecoded; - try { - publicKeyDecoded = new String(Base64.getDecoder().decode(publicKey)); - } catch (final Exception e) { - return Collections.emptyMap(); - } - - return parseJwk(publicKeyDecoded); - } - - private Map<String, Key> parseJwks(final String publicKey) { - final JsonObject jwks; - try { - jwks = Json.createReader(new StringReader(publicKey)).readObject(); - } catch (final JsonParsingException e) { - return Collections.emptyMap(); - } - - try { - final JsonArray keys = jwks.getJsonArray(JWK_SET_MEMBER_NAME); - for (final JsonValue key : keys) { - validateJwk(key.asJsonObject()); - } - } catch (final Exception e) { - throw new DeploymentException("MicroProfile Public Key JWKS invalid format."); - } - - try { - final JsonWebKeySet keySet = new JsonWebKeySet(publicKey); - final Map<String, Key> keys = - keySet.getJsonWebKeys() - .stream() - .collect(Collectors.toMap(JsonWebKey::getKeyId, JsonWebKey::getKey)); - return Collections.unmodifiableMap(keys); - } catch (final JoseException e) { - throw new DeploymentException(JWTAuthConfigurationProperties.PUBLIC_KEY_ERROR + " JWK.", e); - } - } - - private Map<String, Key> parseJwksDecoded(final String publicKey) { - final String publicKeyDecoded; - try { - publicKeyDecoded = new String(Base64.getDecoder().decode(publicKey)); - } catch (final Exception e) { - return Collections.emptyMap(); - } - - return parseJwks(publicKeyDecoded); - } - - private void validateJwk(final JsonObject jwk) { - final String keyType = jwk.getString("kty", null); - if (keyType == null) { - throw new DeploymentException("MicroProfile Public Key JWK kty field is missing."); - } - - if (!JWTAuthConfigurationProperties.JWK_SUPPORTED_KEY_TYPES.contains(keyType)) { - throw new DeploymentException("MicroProfile Public Key JWK kty not supported: " + keyType); - } - } - - private byte[] normalizeAndDecodePCKS8(final String publicKey) { - if (publicKey.contains("PRIVATE KEY")) { - throw new DeploymentException("MicroProfile Public Key is Private."); - } - - final String normalizedKey = - publicKey.replaceAll("-----BEGIN (.*)-----", "") - .replaceAll("-----END (.*)----", "") - .replaceAll("\r\n", "") - .replaceAll("\n", ""); - - return Base64.getDecoder().decode(normalizedKey); - } } diff --git a/pom.xml b/pom.xml index 875b11b4b6..9b35512931 100644 --- a/pom.xml +++ b/pom.xml @@ -648,7 +648,7 @@ <activeByDefault>true</activeByDefault> </activation> <modules> - <module>deps</module> +<!-- <module>deps</module>--> <module>boms</module> <module>itests</module> <module>maven</module>