diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d6399e506d..9b84ad02e20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: SEP="," done - ./mvnw install -pl "$PROJECTS" -am + ./mvnw test -pl "$PROJECTS" -am - name: Upload JVM Heapdumps if: always() diff --git a/core/pom.xml b/core/pom.xml index 2c58af77ab5..8363871a673 100755 --- a/core/pom.xml +++ b/core/pom.xml @@ -77,9 +77,9 @@ - jdk-16 + jdk-15 - [16,) + [15,) @@ -87,17 +87,16 @@ maven-compiler-plugin - compile-java16 + compile-java15 compile compile - 16 + 15 - ${project.basedir}/src/main/java16 + ${project.basedir}/src/main/java15 - true @@ -117,13 +116,6 @@ maven-jar-plugin - - - - true - - - diff --git a/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKBuilder.java b/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKBuilder.java deleted file mode 100644 index 3a7c993c58b..00000000000 --- a/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKBuilder.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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 org.keycloak.jose.jwk; - -import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes; - -import java.security.Key; -import java.security.PublicKey; -import java.security.cert.X509Certificate; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; -import java.util.Collections; -import java.util.List; - -import org.keycloak.common.util.Base64Url; -import org.keycloak.common.util.KeyUtils; -import org.keycloak.common.util.PemUtils; -import org.keycloak.crypto.Algorithm; -import org.keycloak.crypto.KeyType; -import org.keycloak.crypto.KeyUse; - -/** - * @author Stian Thorgersen - */ -public abstract class AbstractJWKBuilder { - - public static final KeyUse DEFAULT_PUBLIC_KEY_USE = KeyUse.SIG; - - protected String kid; - - protected String algorithm; - - public JWK rs256(PublicKey key) { - this.algorithm = Algorithm.RS256; - return rsa(key); - } - - public JWK rsa(Key key) { - return rsa(key, null, KeyUse.SIG); - } - - public JWK rsa(Key key, X509Certificate certificate) { - return rsa(key, Collections.singletonList(certificate), KeyUse.SIG); - } - - public JWK rsa(Key key, List certificates) { - return rsa(key, certificates, null); - } - - public JWK rsa(Key key, List certificates, KeyUse keyUse) { - RSAPublicKey rsaKey = (RSAPublicKey) key; - - RSAPublicJWK k = new RSAPublicJWK(); - - String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key); - k.setKeyId(kid); - k.setKeyType(KeyType.RSA); - k.setAlgorithm(algorithm); - k.setPublicKeyUse(keyUse == null ? KeyUse.SIG.getSpecName() : keyUse.getSpecName()); - k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus()))); - k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent()))); - - if (certificates != null && !certificates.isEmpty()) { - String[] certificateChain = new String[certificates.size()]; - for (int i = 0; i < certificates.size(); i++) { - certificateChain[i] = PemUtils.encodeCertificate(certificates.get(i)); - } - k.setX509CertificateChain(certificateChain); - } - - return k; - } - - public JWK rsa(Key key, KeyUse keyUse) { - JWK k = rsa(key); - String keyUseString = keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName(); - if (KeyUse.ENC == keyUse) keyUseString = "enc"; - k.setPublicKeyUse(keyUseString); - return k; - } - - public JWK ec(Key key) { - return ec(key, DEFAULT_PUBLIC_KEY_USE); - } - - public JWK ec(Key key, KeyUse keyUse) { - return this.ec(key, null, keyUse); - } - - public JWK ec(Key key, List certificates, KeyUse keyUse) { - ECPublicKey ecKey = (ECPublicKey) key; - - ECPublicJWK k = new ECPublicJWK(); - - String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key); - int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize(); - - k.setKeyId(kid); - k.setKeyType(KeyType.EC); - k.setAlgorithm(algorithm); - k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName()); - k.setCrv("P-" + fieldSize); - k.setX(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineX(), fieldSize))); - k.setY(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineY(), fieldSize))); - - if (certificates != null && !certificates.isEmpty()) { - String[] certificateChain = new String[certificates.size()]; - for (int i = 0; i < certificates.size(); i++) { - certificateChain[i] = PemUtils.encodeCertificate(certificates.get(i)); - } - k.setX509CertificateChain(certificateChain); - } - - return k; - } - - public abstract JWK okp(Key key); - - public abstract JWK okp(Key key, KeyUse keyUse); - - public static byte[] reverseBytes(byte[] array) { - if (array == null || array.length == 0) { - return null; - } - - int length = array.length; - byte[] reversedArray = new byte[length]; - - for (int i = 0; i < length; i++) { - reversedArray[length - 1 - i] = array[i]; - } - - return reversedArray; - } -} diff --git a/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKParser.java b/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKParser.java deleted file mode 100644 index 1e089626dbf..00000000000 --- a/core/src/main/java/org/keycloak/jose/jwk/AbstractJWKParser.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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 org.keycloak.jose.jwk; - -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.PublicKey; -import java.security.spec.ECParameterSpec; -import java.security.spec.ECPoint; -import java.security.spec.ECPublicKeySpec; -import java.security.spec.RSAPublicKeySpec; - -import org.keycloak.common.crypto.CryptoIntegration; -import org.keycloak.common.util.Base64Url; -import org.keycloak.crypto.KeyType; - -/** - * @author Stian Thorgersen - */ -public abstract class AbstractJWKParser { - - protected JWK jwk; - - public JWK getJwk() { - return jwk; - } - - public PublicKey toPublicKey() { - if (jwk == null) { - throw new IllegalStateException("Not possible to convert to the publicKey. The jwk is not set"); - } - String keyType = jwk.getKeyType(); - if (KeyType.RSA.equals(keyType)) { - return createRSAPublicKey(); - } else if (KeyType.EC.equals(keyType)) { - return createECPublicKey(); - - } else { - throw new RuntimeException("Unsupported keyType " + keyType); - } - } - - protected PublicKey createECPublicKey() { - /* Check if jwk.getOtherClaims return an empty map */ - if (jwk.getOtherClaims().size() == 0) { - throw new RuntimeException("JWK Otherclaims map is empty."); - } - - /* Try retrieving the necessary fields */ - String crv = (String) jwk.getOtherClaims().get(ECPublicJWK.CRV); - String xStr = (String) jwk.getOtherClaims().get(ECPublicJWK.X); - String yStr = (String) jwk.getOtherClaims().get(ECPublicJWK.Y); - - /* Check if the retrieving of necessary fields success */ - if (crv == null || xStr == null || yStr == null) { - throw new RuntimeException("Fail to retrieve ECPublicJWK.CRV, ECPublicJWK.X or ECPublicJWK.Y field."); - } - - BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); - BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); - - String name; - switch (crv) { - case "P-256" : - name = "secp256r1"; - break; - case "P-384" : - name = "secp384r1"; - break; - case "P-521" : - name = "secp521r1"; - break; - default : - throw new RuntimeException("Unsupported curve"); - } - - try { - - ECPoint point = new ECPoint(x, y); - ECParameterSpec params = CryptoIntegration.getProvider().createECParams(name); - ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); - - KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory("ECDSA"); - return kf.generatePublic(pubKeySpec); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - protected PublicKey createRSAPublicKey() { - BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.MODULUS).toString())); - BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.PUBLIC_EXPONENT).toString())); - - try { - KeyFactory kf = KeyFactory.getInstance("RSA"); - return kf.generatePublic(new RSAPublicKeySpec(modulus, publicExponent)); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public boolean isKeyTypeSupported(String keyType) { - return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType)); - } - -} diff --git a/core/src/main/java/org/keycloak/jose/jwk/EdECUtils.java b/core/src/main/java/org/keycloak/jose/jwk/EdECUtils.java new file mode 100644 index 00000000000..52cdc720982 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwk/EdECUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.jose.jwk; + +import java.security.Key; +import java.security.PublicKey; +import org.keycloak.crypto.KeyUse; + +/** + *

Interface for the EdECUtils that will be implemented only for JDK 15+.

+ * + * @author rmartinc + */ +interface EdECUtils { + + boolean isEdECSupported(); + + JWK okp(String kid, String algorithm, Key key, KeyUse keyUse); + + PublicKey createOKPPublicKey(JWK jwk); +} diff --git a/core/src/main/java/org/keycloak/jose/jwk/EdECUtilsUnsupportedImpl.java b/core/src/main/java/org/keycloak/jose/jwk/EdECUtilsUnsupportedImpl.java new file mode 100644 index 00000000000..35121e4f800 --- /dev/null +++ b/core/src/main/java/org/keycloak/jose/jwk/EdECUtilsUnsupportedImpl.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.jose.jwk; + +import java.security.Key; +import java.security.PublicKey; +import org.keycloak.crypto.KeyUse; + +/** + *

Unsupported implementation for old jdk versions.

+ * + * @author rmartinc + */ +class EdECUtilsUnsupportedImpl implements EdECUtils { + + @Override + public boolean isEdECSupported() { + return false; + } + + @Override + public JWK okp(String kid, String algorithm, Key key, KeyUse keyUse) { + throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version"); + } + + @Override + public PublicKey createOKPPublicKey(JWK jwk) { + throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version"); + } +} diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java index 77de2785a9b..daeca3df087 100644 --- a/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java +++ b/core/src/main/java/org/keycloak/jose/jwk/JWKBuilder.java @@ -17,14 +17,49 @@ package org.keycloak.jose.jwk; -import org.keycloak.crypto.KeyUse; +import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes; import java.security.Key; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Collections; +import java.util.List; + +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.PemUtils; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.crypto.KeyUse; /** * @author Stian Thorgersen */ -public class JWKBuilder extends AbstractJWKBuilder { +public class JWKBuilder { + + // internal util class only loaded for jdk versions with EdEC support + protected static final EdECUtils EdEC_UTILS; + + static { + EdECUtils tmp; + try { + // check if the impl class for EdEC can be loaded in the runtime + tmp = (EdECUtils) Class.forName("org.keycloak.jose.jwk.EdECUtilsImpl") + .getDeclaredConstructor().newInstance(); + } catch(Throwable e) { + // not supported implementation + tmp = new EdECUtilsUnsupportedImpl(); + } + EdEC_UTILS = tmp; + } + + public static final KeyUse DEFAULT_PUBLIC_KEY_USE = KeyUse.SIG; + + protected String kid; + + protected String algorithm; private JWKBuilder() { } @@ -43,15 +78,95 @@ public class JWKBuilder extends AbstractJWKBuilder { return this; } - @Override - public JWK okp(Key key) { - // not supported if jdk vesion < 17 - throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version"); + public JWK rs256(PublicKey key) { + this.algorithm = Algorithm.RS256; + return rsa(key); + } + + public JWK rsa(Key key) { + return rsa(key, null, KeyUse.SIG); + } + + public JWK rsa(Key key, X509Certificate certificate) { + return rsa(key, Collections.singletonList(certificate), KeyUse.SIG); + } + + public JWK rsa(Key key, List certificates) { + return rsa(key, certificates, null); + } + + public JWK rsa(Key key, List certificates, KeyUse keyUse) { + RSAPublicKey rsaKey = (RSAPublicKey) key; + + RSAPublicJWK k = new RSAPublicJWK(); + + String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key); + k.setKeyId(kid); + k.setKeyType(KeyType.RSA); + k.setAlgorithm(algorithm); + k.setPublicKeyUse(keyUse == null ? KeyUse.SIG.getSpecName() : keyUse.getSpecName()); + k.setModulus(Base64Url.encode(toIntegerBytes(rsaKey.getModulus()))); + k.setPublicExponent(Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent()))); + + if (certificates != null && !certificates.isEmpty()) { + String[] certificateChain = new String[certificates.size()]; + for (int i = 0; i < certificates.size(); i++) { + certificateChain[i] = PemUtils.encodeCertificate(certificates.get(i)); + } + k.setX509CertificateChain(certificateChain); + } + + return k; + } + + public JWK rsa(Key key, KeyUse keyUse) { + JWK k = rsa(key); + String keyUseString = keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName(); + if (KeyUse.ENC == keyUse) keyUseString = "enc"; + k.setPublicKeyUse(keyUseString); + return k; + } + + public JWK ec(Key key) { + return ec(key, DEFAULT_PUBLIC_KEY_USE); + } + + public JWK ec(Key key, KeyUse keyUse) { + return this.ec(key, null, keyUse); + } + + public JWK ec(Key key, List certificates, KeyUse keyUse) { + ECPublicKey ecKey = (ECPublicKey) key; + + ECPublicJWK k = new ECPublicJWK(); + + String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key); + int fieldSize = ecKey.getParams().getCurve().getField().getFieldSize(); + + k.setKeyId(kid); + k.setKeyType(KeyType.EC); + k.setAlgorithm(algorithm); + k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName()); + k.setCrv("P-" + fieldSize); + k.setX(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineX(), fieldSize))); + k.setY(Base64Url.encode(toIntegerBytes(ecKey.getW().getAffineY(), fieldSize))); + + if (certificates != null && !certificates.isEmpty()) { + String[] certificateChain = new String[certificates.size()]; + for (int i = 0; i < certificates.size(); i++) { + certificateChain[i] = PemUtils.encodeCertificate(certificates.get(i)); + } + k.setX509CertificateChain(certificateChain); + } + + return k; + } + + public JWK okp(Key key) { + return okp(key, DEFAULT_PUBLIC_KEY_USE); } - @Override public JWK okp(Key key, KeyUse keyUse) { - // not supported if jdk version < 17 - throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version"); + return EdEC_UTILS.okp(kid, algorithm, key, keyUse); } } diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java old mode 100755 new mode 100644 index 10fe8f45265..9cbb0a643a8 --- a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java +++ b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java @@ -17,12 +17,25 @@ package org.keycloak.jose.jwk; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.RSAPublicKeySpec; + +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.KeyType; import org.keycloak.util.JsonSerialization; /** * @author Stian Thorgersen */ -public class JWKParser extends AbstractJWKParser { +public class JWKParser { + + protected JWK jwk; private JWKParser() { } @@ -48,4 +61,87 @@ public class JWKParser extends AbstractJWKParser { } } + public JWK getJwk() { + return jwk; + } + + public PublicKey toPublicKey() { + if (jwk == null) { + throw new IllegalStateException("Not possible to convert to the publicKey. The jwk is not set"); + } + String keyType = jwk.getKeyType(); + if (KeyType.RSA.equals(keyType)) { + return createRSAPublicKey(); + } else if (KeyType.EC.equals(keyType)) { + return createECPublicKey(); + } else if (KeyType.OKP.equals(keyType)) { + return JWKBuilder.EdEC_UTILS.createOKPPublicKey(jwk); + } else { + throw new RuntimeException("Unsupported keyType " + keyType); + } + } + + protected PublicKey createECPublicKey() { + /* Check if jwk.getOtherClaims return an empty map */ + if (jwk.getOtherClaims().size() == 0) { + throw new RuntimeException("JWK Otherclaims map is empty."); + } + + /* Try retrieving the necessary fields */ + String crv = (String) jwk.getOtherClaims().get(ECPublicJWK.CRV); + String xStr = (String) jwk.getOtherClaims().get(ECPublicJWK.X); + String yStr = (String) jwk.getOtherClaims().get(ECPublicJWK.Y); + + /* Check if the retrieving of necessary fields success */ + if (crv == null || xStr == null || yStr == null) { + throw new RuntimeException("Fail to retrieve ECPublicJWK.CRV, ECPublicJWK.X or ECPublicJWK.Y field."); + } + + BigInteger x = new BigInteger(1, Base64Url.decode(xStr)); + BigInteger y = new BigInteger(1, Base64Url.decode(yStr)); + + String name; + switch (crv) { + case "P-256" : + name = "secp256r1"; + break; + case "P-384" : + name = "secp384r1"; + break; + case "P-521" : + name = "secp521r1"; + break; + default : + throw new RuntimeException("Unsupported curve"); + } + + try { + + ECPoint point = new ECPoint(x, y); + ECParameterSpec params = CryptoIntegration.getProvider().createECParams(name); + ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params); + + KeyFactory kf = CryptoIntegration.getProvider().getKeyFactory("ECDSA"); + return kf.generatePublic(pubKeySpec); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected PublicKey createRSAPublicKey() { + BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.MODULUS).toString())); + BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.PUBLIC_EXPONENT).toString())); + + try { + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(new RSAPublicKeySpec(modulus, publicExponent)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public boolean isKeyTypeSupported(String keyType) { + return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType) + || (JWKBuilder.EdEC_UTILS.isEdECSupported() && OKPPublicJWK.OKP.equals(keyType))); + } } diff --git a/core/src/main/java15/org/keycloak/jose/jwk/EdECUtilsImpl.java b/core/src/main/java15/org/keycloak/jose/jwk/EdECUtilsImpl.java new file mode 100644 index 00000000000..497257469d1 --- /dev/null +++ b/core/src/main/java15/org/keycloak/jose/jwk/EdECUtilsImpl.java @@ -0,0 +1,157 @@ +/* + * Copyright 2024 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.jose.jwk; + +import java.math.BigInteger; +import java.security.Key; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.interfaces.EdECPublicKey; +import java.security.spec.EdECPoint; +import java.security.spec.EdECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.NamedParameterSpec; +import java.util.Optional; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.crypto.Algorithm; +import org.keycloak.crypto.KeyType; +import org.keycloak.crypto.KeyUse; + +/** + *

Class that uses Java 15+ EdEC classes and implements the EdECUtils interface.

+ * + * @author rmartinc + */ +class EdECUtilsImpl implements EdECUtils { + + public EdECUtilsImpl() { + } + + @Override + public boolean isEdECSupported() { + return true; + } + + @Override + public JWK okp(String kid, String algorithm, Key key, KeyUse keyUse) { + EdECPublicKey eddsaPublicKey = (EdECPublicKey) key; + + OKPPublicJWK k = new OKPPublicJWK(); + + kid = kid != null ? kid : KeyUtils.createKeyId(key); + + k.setKeyId(kid); + k.setKeyType(KeyType.OKP); + k.setAlgorithm(algorithm); + k.setPublicKeyUse(keyUse == null ? JWKBuilder.DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName()); + k.setCrv(eddsaPublicKey.getParams().getName()); + + Optional x = edPublicKeyInJwkRepresentation(eddsaPublicKey); + k.setX(x.orElse("")); + + return k; + } + + @Override + public PublicKey createOKPPublicKey(JWK jwk) { + String x = (String) jwk.getOtherClaims().get(OKPPublicJWK.X); + String crv = (String) jwk.getOtherClaims().get(OKPPublicJWK.CRV); + // JWK representation "x" of a public key + int bytesLength = 0; + if (Algorithm.Ed25519.equals(crv)) { + bytesLength = 32; + } else if (Algorithm.Ed448.equals(crv)) { + bytesLength = 57; + } else { + throw new RuntimeException("Invalid JWK representation of OKP type algorithm"); + } + + byte[] decodedX = Base64Url.decode(x); + if (decodedX.length != bytesLength) { + throw new RuntimeException("Invalid JWK representation of OKP type public key"); + } + + // x-coordinate's parity check shown by MSB(bit) of MSB(byte) of decoded "x": 1 is odd, 0 is even + boolean isOddX = false; + if ((decodedX[decodedX.length - 1] & -128) != 0) { // 0b10000000 + isOddX = true; + } + + // MSB(bit) of MSB(byte) showing x-coodinate's parity is set to 0 + decodedX[decodedX.length - 1] &= 127; // 0b01111111 + + // both x and y-coordinate in twisted Edwards curve are always 0 or natural number + BigInteger y = new BigInteger(1, reverseBytes(decodedX)); + NamedParameterSpec spec = new NamedParameterSpec(crv); + EdECPoint ep = new EdECPoint(isOddX, y); + EdECPublicKeySpec keySpec = new EdECPublicKeySpec(spec, ep); + + PublicKey publicKey = null; + try { + publicKey = KeyFactory.getInstance(crv).generatePublic(keySpec); + } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + return publicKey; + } + + private static Optional edPublicKeyInJwkRepresentation(EdECPublicKey eddsaPublicKey) { + EdECPoint edEcPoint = eddsaPublicKey.getPoint(); + BigInteger yCoordinate = edEcPoint.getY(); + + // JWK representation "x" of a public key + int bytesLength = 0; + if (Algorithm.Ed25519.equals(eddsaPublicKey.getParams().getName())) { + bytesLength = 32; + } else if (Algorithm.Ed448.equals(eddsaPublicKey.getParams().getName())) { + bytesLength = 57; + } else { + return Optional.ofNullable(null); + } + + // consider the case where yCoordinate.toByteArray() is less than bytesLength due to relatively small value of y-coordinate. + byte[] yCoordinateLittleEndianBytes = new byte[bytesLength]; + + // convert big endian representation of BigInteger to little endian representation of JWK representation (RFC 8032,8027) + byte[] yCoordinateLittleEndian = reverseBytes(yCoordinate.toByteArray()); + System.arraycopy(yCoordinateLittleEndian, 0, yCoordinateLittleEndianBytes, 0, yCoordinateLittleEndian.length); + + // set a parity of x-coordinate to the most significant bit of the last octet (RFC 8032, 8037) + if (edEcPoint.isXOdd()) { + yCoordinateLittleEndianBytes[yCoordinateLittleEndianBytes.length - 1] |= -128; // 0b10000000 + } + + return Optional.ofNullable(Base64Url.encode(yCoordinateLittleEndianBytes)); + } + + private static byte[] reverseBytes(byte[] array) { + if (array == null || array.length == 0) { + return null; + } + + int length = array.length; + byte[] reversedArray = new byte[length]; + + for (int i = 0; i < length; i++) { + reversedArray[length - 1 - i] = array[i]; + } + + return reversedArray; + } +} diff --git a/core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java b/core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java deleted file mode 100644 index c3943c742ab..00000000000 --- a/core/src/main/java16/org/keycloak/jose/jwk/JWKBuilder.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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 org.keycloak.jose.jwk; - -import java.math.BigInteger; -import java.security.Key; -import java.security.interfaces.EdECPublicKey; -import java.security.spec.EdECPoint; -import java.util.Arrays; -import java.util.Optional; - -import org.keycloak.common.util.Base64Url; -import org.keycloak.common.util.KeyUtils; -import org.keycloak.crypto.Algorithm; -import org.keycloak.crypto.KeyType; -import org.keycloak.crypto.KeyUse; - -/** - * @author Stian Thorgersen - */ -public class JWKBuilder extends AbstractJWKBuilder { - - private JWKBuilder() { - } - - public static JWKBuilder create() { - return new JWKBuilder(); - } - - public JWKBuilder kid(String kid) { - this.kid = kid; - return this; - } - - public JWKBuilder algorithm(String algorithm) { - this.algorithm = algorithm; - return this; - } - - @Override - public JWK okp(Key key) { - return okp(key, DEFAULT_PUBLIC_KEY_USE); - } - - @Override - public JWK okp(Key key, KeyUse keyUse) { - EdECPublicKey eddsaPublicKey = (EdECPublicKey) key; - - OKPPublicJWK k = new OKPPublicJWK(); - - String kid = this.kid != null ? this.kid : KeyUtils.createKeyId(key); - - k.setKeyId(kid); - k.setKeyType(KeyType.OKP); - k.setAlgorithm(algorithm); - k.setPublicKeyUse(keyUse == null ? DEFAULT_PUBLIC_KEY_USE.getSpecName() : keyUse.getSpecName()); - k.setCrv(eddsaPublicKey.getParams().getName()); - - Optional x = edPublicKeyInJwkRepresentation(eddsaPublicKey); - k.setX(x.orElse("")); - - return k; - } - - private Optional edPublicKeyInJwkRepresentation(EdECPublicKey eddsaPublicKey) { - EdECPoint edEcPoint = eddsaPublicKey.getPoint(); - BigInteger yCoordinate = edEcPoint.getY(); - - // JWK representation "x" of a public key - int bytesLength = 0; - if (Algorithm.Ed25519.equals(eddsaPublicKey.getParams().getName())) { - bytesLength = 32; - } else if (Algorithm.Ed448.equals(eddsaPublicKey.getParams().getName())) { - bytesLength = 57; - } else { - return Optional.ofNullable(null); - } - - // consider the case where yCoordinate.toByteArray() is less than bytesLength due to relatively small value of y-coordinate. - byte[] yCoordinateLittleEndianBytes = new byte[bytesLength]; - - // convert big endian representation of BigInteger to little endian representation of JWK representation (RFC 8032,8027) - yCoordinateLittleEndianBytes = Arrays.copyOf(reverseBytes(yCoordinate.toByteArray()), bytesLength); - - // set a parity of x-coordinate to the most significant bit of the last octet (RFC 8032, 8037) - if (edEcPoint.isXOdd()) { - yCoordinateLittleEndianBytes[yCoordinateLittleEndianBytes.length - 1] |= -128; // 0b10000000 - } - - return Optional.ofNullable(Base64Url.encode(yCoordinateLittleEndianBytes)); - } - -} diff --git a/core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java b/core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java deleted file mode 100755 index cfa646cbbd7..00000000000 --- a/core/src/main/java16/org/keycloak/jose/jwk/JWKParser.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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 org.keycloak.jose.jwk; - -import java.math.BigInteger; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.spec.EdECPoint; -import java.security.spec.EdECPublicKeySpec; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.NamedParameterSpec; - -import org.keycloak.common.util.Base64Url; -import org.keycloak.crypto.Algorithm; -import org.keycloak.crypto.KeyType; -import org.keycloak.util.JsonSerialization; - -/** - * @author Stian Thorgersen - */ -public class JWKParser extends AbstractJWKParser { - - private JWKParser() { - } - - public static JWKParser create() { - return new JWKParser(); - } - - public JWKParser(JWK jwk) { - this.jwk = jwk; - } - - public static JWKParser create(JWK jwk) { - return new JWKParser(jwk); - } - - public JWKParser parse(String jwk) { - try { - this.jwk = JsonSerialization.mapper.readValue(jwk, JWK.class); - return this; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public PublicKey toPublicKey() { - if (jwk == null) { - throw new IllegalStateException("Not possible to convert to the publicKey. The jwk is not set"); - } - String keyType = jwk.getKeyType(); - if (KeyType.RSA.equals(keyType)) { - return createRSAPublicKey(); - } else if (KeyType.EC.equals(keyType)) { - return createECPublicKey(); - } else if (KeyType.OKP.equals(keyType)) { - return createOKPPublicKey(); - } else { - throw new RuntimeException("Unsupported keyType " + keyType); - } - } - - private PublicKey createOKPPublicKey() { - String x = (String) jwk.getOtherClaims().get(OKPPublicJWK.X); - String crv = (String) jwk.getOtherClaims().get(OKPPublicJWK.CRV); - // JWK representation "x" of a public key - int bytesLength = 0; - if (Algorithm.Ed25519.equals(crv)) { - bytesLength = 32; - } else if (Algorithm.Ed448.equals(crv)) { - bytesLength = 57; - } else { - throw new RuntimeException("Invalid JWK representation of OKP type algorithm"); - } - - byte[] decodedX = Base64Url.decode(x); - if (decodedX.length != bytesLength) { - throw new RuntimeException("Invalid JWK representation of OKP type public key"); - } - - // x-coordinate's parity check shown by MSB(bit) of MSB(byte) of decoded "x": 1 is odd, 0 is even - boolean isOddX = false; - if ((decodedX[decodedX.length - 1] & -128) != 0) { // 0b10000000 - isOddX = true; - } - - // MSB(bit) of MSB(byte) showing x-coodinate's parity is set to 0 - decodedX[decodedX.length - 1] &= 127; // 0b01111111 - - // both x and y-coordinate in twisted Edwards curve are always 0 or natural number - BigInteger y = new BigInteger(1, JWKBuilder.reverseBytes(decodedX)); - NamedParameterSpec spec = new NamedParameterSpec(crv); - EdECPoint ep = new EdECPoint(isOddX, y); - EdECPublicKeySpec keySpec = new EdECPublicKeySpec(spec, ep); - - PublicKey publicKey = null; - try { - publicKey = KeyFactory.getInstance(crv).generatePublic(keySpec); - } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - return publicKey; - } - - @Override - public boolean isKeyTypeSupported(String keyType) { - return (RSAPublicJWK.RSA.equals(keyType) || ECPublicJWK.EC.equals(keyType) || OKPPublicJWK.OKP.equals(keyType)); - } - -}