Remove keycloak-core and keycloak-crypto-default from SAML galleon feature pack and upgrade them to Java 17
closes #32586 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
29c8060bda
commit
dad4477995
14 changed files with 294 additions and 374 deletions
|
@ -61,11 +61,6 @@
|
||||||
<artifactId>keycloak-common</artifactId>
|
<artifactId>keycloak-common</artifactId>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>org.keycloak</groupId>
|
|
||||||
<artifactId>keycloak-crypto-default</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jboss.logging</groupId>
|
<groupId>org.jboss.logging</groupId>
|
||||||
<artifactId>jboss-logging</artifactId>
|
<artifactId>jboss-logging</artifactId>
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
* 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.adapters.saml.config;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.common.util.BouncyIntegration;
|
||||||
|
import org.keycloak.common.util.PemException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fork of the PemUtils from common module to avoid dependency on keycloak-crypto-default
|
||||||
|
*/
|
||||||
|
public class PemUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a X509 Certificate from a PEM string
|
||||||
|
*
|
||||||
|
* @param cert
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public static X509Certificate decodeCertificate(String cert) {
|
||||||
|
if (cert == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] der = pemToDer(cert);
|
||||||
|
ByteArrayInputStream bis = new ByteArrayInputStream(der);
|
||||||
|
return decodeCertificate(bis);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new PemException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a Public Key from a PEM string
|
||||||
|
*
|
||||||
|
* @param pem
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public static PublicKey decodePublicKey(String pem) {
|
||||||
|
if (pem == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] der = pemToDer(pem);
|
||||||
|
return decodePublicKey(der, "RSA");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new PemException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a Private Key from a PEM string
|
||||||
|
*
|
||||||
|
* @param pem
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public static PrivateKey decodePrivateKey(String pem){
|
||||||
|
if (pem == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] der = pemToDer(pem);
|
||||||
|
return decodePrivateKey(der);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new PemException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] pemToDer(String pem) {
|
||||||
|
try {
|
||||||
|
pem = removeBeginEnd(pem);
|
||||||
|
return Base64.decode(pem);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new PemException(ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String removeBeginEnd(String pem) {
|
||||||
|
pem = pem.replaceAll("-----BEGIN (.*)-----", "");
|
||||||
|
pem = pem.replaceAll("-----END (.*)----", "");
|
||||||
|
pem = pem.replaceAll("\r\n", "");
|
||||||
|
pem = pem.replaceAll("\n", "");
|
||||||
|
return pem.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PrivateKey decodePrivateKey(byte[] der) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
|
||||||
|
PKCS8EncodedKeySpec spec =
|
||||||
|
new PKCS8EncodedKeySpec(der);
|
||||||
|
KeyFactory kf = KeyFactory.getInstance("RSA", BouncyIntegration.PROVIDER);
|
||||||
|
return kf.generatePrivate(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509Certificate decodeCertificate(InputStream is) throws Exception {
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509", BouncyIntegration.PROVIDER);
|
||||||
|
X509Certificate cert = (X509Certificate) cf.generateCertificate(is);
|
||||||
|
is.close();
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PublicKey decodePublicKey(byte[] der, String type) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
|
||||||
|
X509EncodedKeySpec spec =
|
||||||
|
new X509EncodedKeySpec(der);
|
||||||
|
KeyFactory kf = KeyFactory.getInstance("RSA", BouncyIntegration.PROVIDER);
|
||||||
|
return kf.generatePublic(spec);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,10 +24,9 @@ import org.keycloak.adapters.saml.SamlDeployment;
|
||||||
import org.keycloak.adapters.saml.config.IDP;
|
import org.keycloak.adapters.saml.config.IDP;
|
||||||
import org.keycloak.adapters.saml.config.Key;
|
import org.keycloak.adapters.saml.config.Key;
|
||||||
import org.keycloak.adapters.saml.config.KeycloakSamlAdapter;
|
import org.keycloak.adapters.saml.config.KeycloakSamlAdapter;
|
||||||
|
import org.keycloak.adapters.saml.config.PemUtils;
|
||||||
import org.keycloak.adapters.saml.config.SP;
|
import org.keycloak.adapters.saml.config.SP;
|
||||||
import org.keycloak.common.crypto.CryptoIntegration;
|
|
||||||
import org.keycloak.common.enums.SslRequired;
|
import org.keycloak.common.enums.SslRequired;
|
||||||
import org.keycloak.common.util.PemUtils;
|
|
||||||
import org.keycloak.saml.SignatureAlgorithm;
|
import org.keycloak.saml.SignatureAlgorithm;
|
||||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||||
|
|
||||||
|
@ -58,7 +57,6 @@ public class DeploymentBuilder {
|
||||||
protected static Logger log = Logger.getLogger(DeploymentBuilder.class);
|
protected static Logger log = Logger.getLogger(DeploymentBuilder.class);
|
||||||
|
|
||||||
public SamlDeployment build(InputStream xml, ResourceLoader resourceLoader) throws ParsingException {
|
public SamlDeployment build(InputStream xml, ResourceLoader resourceLoader) throws ParsingException {
|
||||||
CryptoIntegration.init(DeploymentBuilder.class.getClassLoader());
|
|
||||||
DefaultSamlDeployment deployment = new DefaultSamlDeployment();
|
DefaultSamlDeployment deployment = new DefaultSamlDeployment();
|
||||||
DefaultSamlDeployment.DefaultIDP defaultIDP = new DefaultSamlDeployment.DefaultIDP();
|
DefaultSamlDeployment.DefaultIDP defaultIDP = new DefaultSamlDeployment.DefaultIDP();
|
||||||
DefaultSamlDeployment.DefaultSingleSignOnService sso = new DefaultSamlDeployment.DefaultSingleSignOnService();
|
DefaultSamlDeployment.DefaultSingleSignOnService sso = new DefaultSamlDeployment.DefaultSingleSignOnService();
|
||||||
|
|
34
core/pom.xml
34
core/pom.xml
|
@ -32,8 +32,6 @@
|
||||||
<description/>
|
<description/>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- We still need to support EAP 8, set the Java version to 11. -->
|
|
||||||
<maven.compiler.release>11</maven.compiler.release>
|
|
||||||
<timestamp>${maven.build.timestamp}</timestamp>
|
<timestamp>${maven.build.timestamp}</timestamp>
|
||||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||||
</properties>
|
</properties>
|
||||||
|
@ -74,38 +72,6 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<profiles>
|
|
||||||
<profile>
|
|
||||||
<id>jdk-16</id>
|
|
||||||
<activation>
|
|
||||||
<jdk>[16,)</jdk>
|
|
||||||
</activation>
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>compile-java16</id>
|
|
||||||
<phase>compile</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>compile</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<release>16</release>
|
|
||||||
<compileSourceRoots>
|
|
||||||
<compileSourceRoot>${project.basedir}/src/main/java16</compileSourceRoot>
|
|
||||||
</compileSourceRoots>
|
|
||||||
<multiReleaseOutput>true</multiReleaseOutput>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</profile>
|
|
||||||
</profiles>
|
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<resources>
|
<resources>
|
||||||
<resource>
|
<resource>
|
||||||
|
|
|
@ -41,10 +41,13 @@ public abstract class AbstractJWKParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
public PublicKey toPublicKey() {
|
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();
|
String keyType = jwk.getKeyType();
|
||||||
if (keyType.equals(KeyType.RSA)) {
|
if (KeyType.RSA.equals(keyType)) {
|
||||||
return createRSAPublicKey();
|
return createRSAPublicKey();
|
||||||
} else if (keyType.equals(KeyType.EC)) {
|
} else if (KeyType.EC.equals(keyType)) {
|
||||||
return createECPublicKey();
|
return createECPublicKey();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,15 +17,27 @@
|
||||||
|
|
||||||
package org.keycloak.jose.jwk;
|
package org.keycloak.jose.jwk;
|
||||||
|
|
||||||
import org.keycloak.crypto.KeyUse;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
import java.security.Key;
|
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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class JWKBuilder extends AbstractJWKBuilder {
|
public class JWKBuilder extends AbstractJWKBuilder {
|
||||||
|
|
||||||
|
private JWKBuilder() {
|
||||||
|
}
|
||||||
|
|
||||||
public static JWKBuilder create() {
|
public static JWKBuilder create() {
|
||||||
return new JWKBuilder();
|
return new JWKBuilder();
|
||||||
}
|
}
|
||||||
|
@ -42,13 +54,55 @@ public class JWKBuilder extends AbstractJWKBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JWK okp(Key key) {
|
public JWK okp(Key key) {
|
||||||
// not supported if jdk vesion < 17
|
return okp(key, DEFAULT_PUBLIC_KEY_USE);
|
||||||
throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JWK okp(Key key, KeyUse keyUse) {
|
public JWK okp(Key key, KeyUse keyUse) {
|
||||||
// not supported if jdk version < 17
|
EdECPublicKey eddsaPublicKey = (EdECPublicKey) key;
|
||||||
throw new UnsupportedOperationException("EdDSA algorithms not supported in this JDK version");
|
|
||||||
|
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<String> x = edPublicKeyInJwkRepresentation(eddsaPublicKey);
|
||||||
|
k.setX(x.orElse(""));
|
||||||
|
|
||||||
|
return k;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Optional<String> 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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,18 @@
|
||||||
|
|
||||||
package org.keycloak.jose.jwk;
|
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;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,17 +36,17 @@ import org.keycloak.util.JsonSerialization;
|
||||||
*/
|
*/
|
||||||
public class JWKParser extends AbstractJWKParser {
|
public class JWKParser extends AbstractJWKParser {
|
||||||
|
|
||||||
protected JWKParser() {
|
private JWKParser() {
|
||||||
}
|
|
||||||
|
|
||||||
public JWKParser(JWK jwk) {
|
|
||||||
this.jwk = jwk;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static JWKParser create() {
|
public static JWKParser create() {
|
||||||
return new JWKParser();
|
return new JWKParser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public JWKParser(JWK jwk) {
|
||||||
|
this.jwk = jwk;
|
||||||
|
}
|
||||||
|
|
||||||
public static JWKParser create(JWK jwk) {
|
public static JWKParser create(JWK jwk) {
|
||||||
return new JWKParser(jwk);
|
return new JWKParser(jwk);
|
||||||
}
|
}
|
||||||
|
@ -48,4 +60,68 @@ public class JWKParser extends AbstractJWKParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023 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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
|
||||||
*/
|
|
||||||
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<String> x = edPublicKeyInJwkRepresentation(eddsaPublicKey);
|
|
||||||
k.setX(x.orElse(""));
|
|
||||||
|
|
||||||
return k;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<String> 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,124 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023 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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
|
||||||
*/
|
|
||||||
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() {
|
|
||||||
String keyType = jwk.getKeyType();
|
|
||||||
if (keyType.equals(KeyType.RSA)) {
|
|
||||||
return createRSAPublicKey();
|
|
||||||
} else if (keyType.equals(KeyType.EC)) {
|
|
||||||
return createECPublicKey();
|
|
||||||
} else if (keyType.equals(KeyType.OKP)) {
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -30,11 +30,6 @@
|
||||||
<name>Keycloak Crypto Default</name>
|
<name>Keycloak Crypto Default</name>
|
||||||
<description/>
|
<description/>
|
||||||
|
|
||||||
<properties>
|
|
||||||
<!-- We still need to support EAP 8, set the Java version to 11. -->
|
|
||||||
<maven.compiler.release>11</maven.compiler.release>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
~ 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-core">
|
|
||||||
<resources>
|
|
||||||
<artifact name="${org.keycloak:keycloak-core}"/>
|
|
||||||
</resources>
|
|
||||||
<dependencies>
|
|
||||||
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
|
|
||||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
|
||||||
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
|
||||||
<module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
|
|
||||||
<module name="org.keycloak.keycloak-common" />
|
|
||||||
<module name="org.jboss.logging"/>
|
|
||||||
<module name="javax.api"/>
|
|
||||||
<module name="jakarta.activation.api"/>
|
|
||||||
<module name="sun.jdk" optional="true" />
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
</module>
|
|
|
@ -1,40 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
~ Copyright 2022 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.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-crypto-default">
|
|
||||||
<resources>
|
|
||||||
<artifact name="${org.keycloak:keycloak-crypto-default}"/>
|
|
||||||
</resources>
|
|
||||||
<dependencies>
|
|
||||||
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
|
|
||||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
|
||||||
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
|
||||||
<module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
|
|
||||||
<module name="org.keycloak.keycloak-common" />
|
|
||||||
<module name="org.keycloak.keycloak-core" />
|
|
||||||
<module name="org.bouncycastle" />
|
|
||||||
<module name="org.jboss.logging"/>
|
|
||||||
<module name="javax.api"/>
|
|
||||||
<module name="jakarta.activation.api"/>
|
|
||||||
<module name="sun.jdk" optional="true" />
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
</module>
|
|
|
@ -35,10 +35,9 @@
|
||||||
<module name="org.keycloak.keycloak-saml-core-public"/>
|
<module name="org.keycloak.keycloak-saml-core-public"/>
|
||||||
<module name="org.keycloak.keycloak-saml-core"/>
|
<module name="org.keycloak.keycloak-saml-core"/>
|
||||||
<module name="org.keycloak.keycloak-common"/>
|
<module name="org.keycloak.keycloak-common"/>
|
||||||
<module name="org.keycloak.keycloak-core"/>
|
|
||||||
<module name="org.keycloak.keycloak-crypto-default" services="import"/>
|
|
||||||
<module name="org.apache.httpcomponents"/>
|
<module name="org.apache.httpcomponents"/>
|
||||||
<module name="org.apache.santuario.xmlsec"/>
|
<module name="org.apache.santuario.xmlsec"/>
|
||||||
|
<module name="org.bouncycastle" />
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -42,7 +42,7 @@ import org.keycloak.util.JsonSerialization;
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
*/
|
*/
|
||||||
public abstract class ServerJWKTest {
|
public class ServerJWKTest {
|
||||||
|
|
||||||
@ClassRule
|
@ClassRule
|
||||||
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
|
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
|
||||||
|
|
Loading…
Reference in a new issue