Add AuthzClientCryptoProvider to authz-client in keycloak main repository

closes #33831

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda 2024-10-11 17:17:50 +02:00 committed by Marek Posolda
parent 10aca55523
commit b95d12a968
12 changed files with 673 additions and 9 deletions

View file

@ -58,6 +58,17 @@
<artifactId>jackson-annotations</artifactId> <artifactId>jackson-annotations</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View file

@ -30,6 +30,7 @@ import org.keycloak.authorization.client.resource.AuthorizationResource;
import org.keycloak.authorization.client.resource.ProtectionResource; import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.authorization.client.util.Http; import org.keycloak.authorization.client.util.Http;
import org.keycloak.authorization.client.util.TokenCallable; import org.keycloak.authorization.client.util.TokenCallable;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.SystemPropertiesJsonParserFactory; import org.keycloak.util.SystemPropertiesJsonParserFactory;
@ -91,6 +92,7 @@ public class AuthzClient {
* @return a new instance * @return a new instance
*/ */
public static AuthzClient create(Configuration configuration) { public static AuthzClient create(Configuration configuration) {
CryptoIntegration.init(AuthzClient.class.getClassLoader());
return new AuthzClient(configuration); return new AuthzClient(configuration);
} }

View file

@ -0,0 +1,203 @@
/*
* 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.authorization.client.util.crypto;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
*
* @author rmartinc
*/
class ASN1Decoder {
private final ByteArrayInputStream is;
private final int limit;
private int count;
ASN1Decoder(byte[] bytes) {
is = new ByteArrayInputStream(bytes);
count = 0;
limit = bytes.length;
}
public static ASN1Decoder create(byte[] bytes) {
return new ASN1Decoder(bytes);
}
public List<byte[]> readSequence() throws IOException {
int tag = readTag();
int tagNo = readTagNumber(tag);
if (tagNo != ASN1Encoder.SEQUENCE) {
throw new IOException("Invalid Sequence tag " + tagNo);
}
int length = readLength();
List<byte[]> result = new ArrayList<>();
while (length > 0) {
byte[] bytes = readNext();
result.add(bytes);
length = length - bytes.length;
}
return result;
}
public BigInteger readInteger() throws IOException {
int tag = readTag();
int tagNo = readTagNumber(tag);
if (tagNo != ASN1Encoder.INTEGER) {
throw new IOException("Invalid Integer tag " + tagNo);
}
int length = readLength();
byte[] bytes = read(length);
return new BigInteger(bytes);
}
byte[] readNext() throws IOException {
mark();
int tag = readTag();
readTagNumber(tag);
int length = readLength();
length += reset();
return read(length);
}
int readTag() throws IOException {
int tag = read();
if (tag < 0) {
throw new EOFException("EOF found inside tag value.");
}
return tag;
}
int readTagNumber(int tag) throws IOException {
int tagNo = tag & 0x1f;
//
// with tagged object tag number is bottom 5 bits, or stored at the start of the content
//
if (tagNo == 0x1f) {
tagNo = 0;
int b = read();
// X.690-0207 8.1.2.4.2
// "c) bits 7 to 1 of the first subsequent octet shall not all be zero."
if ((b & 0x7f) == 0) // Note: -1 will pass
{
throw new IOException("corrupted stream - invalid high tag number found");
}
while ((b >= 0) && ((b & 0x80) != 0)) {
tagNo |= (b & 0x7f);
tagNo <<= 7;
b = read();
}
if (b < 0) {
throw new EOFException("EOF found inside tag value.");
}
tagNo |= (b & 0x7f);
}
return tagNo;
}
int readLength() throws IOException {
int length = read();
if (length < 0) {
throw new EOFException("EOF found when length expected");
}
if (length == 0x80) {
return -1; // indefinite-length encoding
}
if (length > 127) {
int size = length & 0x7f;
// Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here
if (size > 4) {
throw new IOException("DER length more than 4 bytes: " + size);
}
length = 0;
for (int i = 0; i < size; i++) {
int next = read();
if (next < 0) {
throw new EOFException("EOF found reading length");
}
length = (length << 8) + next;
}
if (length < 0) {
throw new IOException("corrupted stream - negative length found");
}
if (length >= limit) // after all we must have read at least 1 byte
{
throw new IOException("corrupted stream - out of bounds length found");
}
}
return length;
}
byte[] read(int length) throws IOException {
byte[] bytes = new byte[length];
int totalBytesRead = 0;
while (totalBytesRead < length) {
int bytesRead = is.read(bytes, totalBytesRead, length - totalBytesRead);
if (bytesRead == -1) {
throw new IOException(String.format("EOF found reading %d bytes", length));
}
totalBytesRead += bytesRead;
}
count += length;
return bytes;
}
void mark() {
count = 0;
is.mark(is.available());
}
int reset() {
int tmp = count;
is.reset();
return tmp;
}
int read() {
int tmp = is.read();
if (tmp >= 0) {
count++;
}
return tmp;
}
}

View file

@ -0,0 +1,101 @@
/*
* 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.authorization.client.util.crypto;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
/**
*
* @author rmartinc
*/
class ASN1Encoder {
static final int INTEGER = 0x02;
static final int SEQUENCE = 0x10;
static final int CONSTRUCTED = 0x20;
private final ByteArrayOutputStream os;
private ASN1Encoder() {
this.os = new ByteArrayOutputStream();
}
static public ASN1Encoder create() {
return new ASN1Encoder();
}
public ASN1Encoder write(BigInteger value) throws IOException {
writeEncoded(INTEGER, value.toByteArray());
return this;
}
public ASN1Encoder writeDerSeq(ASN1Encoder... objects) throws IOException {
writeEncoded(CONSTRUCTED | SEQUENCE, concatenate(objects));
return this;
}
public byte[] toByteArray() {
return os.toByteArray();
}
void writeEncoded(int tag, byte[] bytes) throws IOException {
write(tag);
writeLength(bytes.length);
write(bytes);
}
void writeLength(int length) throws IOException {
if (length > 127) {
int size = 1;
int val = length;
while ((val >>>= 8) != 0) {
size++;
}
write((byte) (size | 0x80));
for (int i = (size - 1) * 8; i >= 0; i -= 8) {
write((byte) (length >> i));
}
} else {
write((byte) length);
}
}
void write(byte[] bytes) throws IOException {
os.write(bytes);
}
void write(int b) throws IOException {
os.write(b);
}
byte[] concatenate(ASN1Encoder... objects) throws IOException {
ByteArrayOutputStream tmp = new ByteArrayOutputStream();
for (ASN1Encoder object : objects) {
tmp.write(object.toByteArray());
}
return tmp.toByteArray();
}
}

View file

@ -0,0 +1,226 @@
/*
* 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.authorization.client.util.crypto;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.Signature;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECParameterSpec;
import java.util.List;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.net.ssl.SSLSocketFactory;
import org.keycloak.common.crypto.CertificateUtilsProvider;
import org.keycloak.common.crypto.CryptoProvider;
import org.keycloak.common.crypto.ECDSACryptoProvider;
import org.keycloak.common.crypto.PemUtilsProvider;
import org.keycloak.common.crypto.UserIdentityExtractorProvider;
import org.keycloak.common.util.KeystoreUtil;
/**
* <p>Simple crypto provider to be used with the authz-client.</p>
*
* @author rmartinc
*/
public class AuthzClientCryptoProvider implements CryptoProvider {
@Override
public Provider getBouncyCastleProvider() {
try {
return KeyStore.getInstance(KeyStore.getDefaultType()).getProvider();
} catch (KeyStoreException e) {
throw new IllegalStateException(e);
}
}
@Override
public int order() {
return 100;
}
@Override
public <T> T getAlgorithmProvider(Class<T> clazz, String algorithm) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CertificateUtilsProvider getCertificateUtils() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public PemUtilsProvider getPemUtils() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public <T> T getOCSPProver(Class<T> clazz) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public UserIdentityExtractorProvider getIdentityExtractorProvider() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public ECDSACryptoProvider getEcdsaCryptoProvider() {
return new ECDSACryptoProvider() {
@Override
public byte[] concatenatedRSToASN1DER(byte[] signature, int signLength) throws IOException {
int len = signLength / 2;
int arraySize = len + 1;
byte[] r = new byte[arraySize];
byte[] s = new byte[arraySize];
System.arraycopy(signature, 0, r, 1, len);
System.arraycopy(signature, len, s, 1, len);
BigInteger rBigInteger = new BigInteger(r);
BigInteger sBigInteger = new BigInteger(s);
ASN1Encoder.create().write(rBigInteger);
ASN1Encoder.create().write(sBigInteger);
return ASN1Encoder.create()
.writeDerSeq(
ASN1Encoder.create().write(rBigInteger),
ASN1Encoder.create().write(sBigInteger))
.toByteArray();
}
@Override
public byte[] asn1derToConcatenatedRS(byte[] derEncodedSignatureValue, int signLength) throws IOException {
int len = signLength / 2;
List<byte[]> seq = ASN1Decoder.create(derEncodedSignatureValue).readSequence();
if (seq.size() != 2) {
throw new IOException("Invalid sequence with size different to 2");
}
BigInteger rBigInteger = ASN1Decoder.create(seq.get(0)).readInteger();
BigInteger sBigInteger = ASN1Decoder.create(seq.get(1)).readInteger();
byte[] r = integerToBytes(rBigInteger, len);
byte[] s = integerToBytes(sBigInteger, len);
byte[] concatenatedSignatureValue = new byte[signLength];
System.arraycopy(r, 0, concatenatedSignatureValue, 0, len);
System.arraycopy(s, 0, concatenatedSignatureValue, len, len);
return concatenatedSignatureValue;
}
@Override
public ECPublicKey getPublicFromPrivate(ECPrivateKey ecPrivateKey) {
throw new UnsupportedOperationException("Not supported yet.");
}
private byte[] integerToBytes(BigInteger s, int qLength) {
byte[] bytes = s.toByteArray();
if (qLength < bytes.length) {
byte[] tmp = new byte[qLength];
System.arraycopy(bytes, bytes.length - tmp.length, tmp, 0, tmp.length);
return tmp;
} else if (qLength > bytes.length) {
byte[] tmp = new byte[qLength];
System.arraycopy(bytes, 0, tmp, tmp.length - bytes.length, bytes.length);
return tmp;
}
return bytes;
}
};
}
@Override
public ECParameterSpec createECParams(String curveName) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public KeyPairGenerator getKeyPairGen(String algorithm) throws NoSuchAlgorithmException, NoSuchProviderException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public KeyFactory getKeyFactory(String algorithm) throws NoSuchAlgorithmException, NoSuchProviderException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Cipher getAesCbcCipher() throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Cipher getAesGcmCipher() throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public SecretKeyFactory getSecretKeyFact(String keyAlgorithm) throws NoSuchAlgorithmException, NoSuchProviderException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public KeyStore getKeyStore(KeystoreUtil.KeystoreFormat format) throws KeyStoreException, NoSuchProviderException {
return KeyStore.getInstance(format.name());
}
@Override
public CertificateFactory getX509CertFactory() throws CertificateException, NoSuchProviderException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CertStore getCertStore(CollectionCertStoreParameters collectionCertStoreParameters) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CertPathBuilder getCertPathBuilder() throws NoSuchAlgorithmException, NoSuchProviderException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Signature getSignature(String sigAlgName) throws NoSuchAlgorithmException, NoSuchProviderException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public SSLSocketFactory wrapFactoryForTruststore(SSLSocketFactory delegate) {
throw new UnsupportedOperationException("Not supported yet.");
}
}

View file

@ -0,0 +1,20 @@
#
# 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.
#
#
org.keycloak.authorization.client.util.crypto.AuthzClientCryptoProvider

View file

@ -0,0 +1,74 @@
/*
* 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.authorization.client.test;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.authorization.client.util.crypto.AuthzClientCryptoProvider;
import org.keycloak.crypto.ECDSAAlgorithm;
import org.keycloak.crypto.JavaAlgorithm;
/**
*
* @author rmartinc
*/
public class ECDSAAlgorithmTest {
private final KeyPair keyPair;
public ECDSAAlgorithmTest() throws Exception {
keyPair = KeyPairGenerator.getInstance("EC").genKeyPair();
}
private void test(ECDSAAlgorithm algorithm) throws Exception {
AuthzClientCryptoProvider prov = new AuthzClientCryptoProvider();
byte[] data = "Something to sign".getBytes(StandardCharsets.UTF_8);
Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(algorithm.name()));
signature.initSign(keyPair.getPrivate());
signature.update(data);
byte[] sign = signature.sign();
byte[] rsConcat = prov.getEcdsaCryptoProvider().asn1derToConcatenatedRS(sign, algorithm.getSignatureLength());
byte[] asn1Des = prov.getEcdsaCryptoProvider().concatenatedRSToASN1DER(rsConcat, algorithm.getSignatureLength());
byte[] rsConcat2 = prov.getEcdsaCryptoProvider().asn1derToConcatenatedRS(asn1Des, algorithm.getSignatureLength());
Assert.assertArrayEquals(rsConcat, rsConcat2);
}
@Test
public void testES256() throws Exception {
test(ECDSAAlgorithm.ES256);
}
@Test
public void testES384() throws Exception {
test(ECDSAAlgorithm.ES384);
}
@Test
public void testES512() throws Exception {
test(ECDSAAlgorithm.ES512);
}
}

View file

@ -3,6 +3,7 @@ package org.keycloak.common.crypto;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.Provider; import java.security.Provider;
import java.security.Security; import java.security.Security;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -53,15 +54,20 @@ public class CryptoIntegration {
// Try to auto-detect provider // Try to auto-detect provider
private static CryptoProvider detectProvider(ClassLoader classLoader) { private static CryptoProvider detectProvider(ClassLoader classLoader) {
List<CryptoProvider> foundProviders = StreamSupport.stream(ServiceLoader.load(CryptoProvider.class, classLoader).spliterator(), false) List<CryptoProvider> foundProviders = StreamSupport.stream(ServiceLoader.load(CryptoProvider.class, classLoader).spliterator(), false)
.sorted(Comparator.comparingInt(CryptoProvider::order).reversed())
.collect(Collectors.toList()); .collect(Collectors.toList());
if (foundProviders.isEmpty()) { if (foundProviders.isEmpty()) {
throw new IllegalStateException("Not able to load any cryptoProvider with the classLoader: " + classLoader); throw new IllegalStateException("Not able to load any cryptoProvider with the classLoader: " + classLoader);
} else if (foundProviders.size() > 1) {
throw new IllegalStateException("Multiple crypto providers loaded with the classLoader: " + classLoader +
". Make sure only one cryptoProvider available on the classpath. Available providers: " +foundProviders);
} else { } else {
logger.debugf("Detected crypto provider: %s", foundProviders.get(0).getClass().getName()); logger.debugf("Detected crypto provider: %s", foundProviders.get(0).getClass().getName());
if (foundProviders.size() > 1) {
StringBuilder builder = new StringBuilder("Ignored crypto providers: ");
for (int i = 1 ; i < foundProviders.size() ; i++) {
builder.append(foundProviders.get(i).getClass().getName() + ", ");
}
logger.debugf(builder.toString());
}
return foundProviders.get(0); return foundProviders.get(0);
} }
} }

View file

@ -36,6 +36,13 @@ public interface CryptoProvider {
*/ */
Provider getBouncyCastleProvider(); Provider getBouncyCastleProvider();
/**
* Order of this provider. This allows to specify which CryptoProvider will have preference in case that more of them are on the classpath.
*
* The higher number has preference over the lower number
*/
int order();
/** /**
* Get some algorithm provider implementation. Returned implementation can be dependent according to if we have * Get some algorithm provider implementation. Returned implementation can be dependent according to if we have
* non-fips bouncycastle or fips bouncycastle on the classpath. * non-fips bouncycastle or fips bouncycastle on the classpath.

View file

@ -79,6 +79,10 @@ public class DefaultCryptoProvider implements CryptoProvider {
return bcProvider; return bcProvider;
} }
@Override
public int order() {
return 200;
}
@Override @Override
public <T> T getAlgorithmProvider(Class<T> clazz, String algorithmType) { public <T> T getAlgorithmProvider(Class<T> clazz, String algorithmType) {

View file

@ -72,6 +72,11 @@ public class WildFlyElytronProvider implements CryptoProvider {
return null; return null;
} }
@Override
public int order() {
return 200;
}
@Override @Override
public <T> T getAlgorithmProvider(Class<T> clazz, String algorithm) { public <T> T getAlgorithmProvider(Class<T> clazz, String algorithm) {
Object o = providers.get(algorithm); Object o = providers.get(algorithm);

View file

@ -110,6 +110,11 @@ public class FIPS1402Provider implements CryptoProvider {
return bcFipsProvider; return bcFipsProvider;
} }
@Override
public int order() {
return 200;
}
@Override @Override
public <T> T getAlgorithmProvider(Class<T> clazz, String algorithm) { public <T> T getAlgorithmProvider(Class<T> clazz, String algorithm) {
Object o = providers.get(algorithm); Object o = providers.get(algorithm);