Remove backward compatibility for ECDSA tokens

Closes https://github.com/keycloak/keycloak/issues/23734
This commit is contained in:
rmartinc 2023-10-05 18:36:02 +02:00 committed by Marek Posolda
parent b1bdf7dd13
commit 890600c33c
10 changed files with 148 additions and 83 deletions

View file

@ -0,0 +1,52 @@
/*
* 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.crypto;
import java.io.IOException;
import org.keycloak.common.crypto.CryptoIntegration;
/**
*
* @author rmartinc
*/
public enum ECDSAAlgorithm {
ES256(64),
ES384(96),
ES512(132);
private final int signatureLength;
ECDSAAlgorithm(int signatureLength) {
this.signatureLength = signatureLength;
}
public int getSignatureLength() {
return this.signatureLength;
}
public static int getSignatureLength(String alg) {
return valueOf(alg).getSignatureLength();
}
public static byte[] concatenatedRSToASN1DER(final byte[] signature, int signLength) throws IOException {
return CryptoIntegration.getProvider().getEcdsaCryptoProvider().concatenatedRSToASN1DER(signature, signLength);
}
public static byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int signLength) throws IOException {
return CryptoIntegration.getProvider().getEcdsaCryptoProvider().asn1derToConcatenatedRS(derEncodedSignatureValue, signLength);
}
}

View file

@ -0,0 +1,38 @@
/*
* 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.crypto;
/**
*
* @author rmartinc
*/
public class ECDSASignatureSignerContext extends AsymmetricSignatureSignerContext {
public ECDSASignatureSignerContext(KeyWrapper key) throws SignatureException {
super(key);
}
@Override
public byte[] sign(byte[] data) throws SignatureException {
try {
int size = ECDSAAlgorithm.getSignatureLength(getAlgorithm());
return ECDSAAlgorithm.asn1derToConcatenatedRS(super.sign(data), size);
} catch (Exception e) {
throw new SignatureException("Signing failed", e);
}
}
}

View file

@ -28,6 +28,7 @@ import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
import org.keycloak.crypto.ECDSASignatureSignerContext;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
@ -62,22 +63,7 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
}
public void setupKeyPair(KeyPair keyPair, String algorithm) {
// check the algorithm is valid
switch (keyPair.getPublic().getAlgorithm()) {
case KeyType.RSA:
if (!JavaAlgorithm.isRSAJavaAlgorithm(algorithm)) {
throw new RuntimeException("Invalid algorithm for a RSA KeyPair: " + algorithm);
}
break;
case KeyType.EC:
if (!JavaAlgorithm.isECJavaAlgorithm(algorithm)) {
throw new RuntimeException("Invalid algorithm for a EC KeyPair: " + algorithm);
}
break;
default:
throw new RuntimeException("Invalid KeyPair algorithm: " + keyPair.getPublic().getAlgorithm());
}
// create the key and signature context
// create a key wrapper for the key pair
KeyWrapper keyWrapper = new KeyWrapper();
keyWrapper.setKid(KeyUtils.createKeyId(keyPair.getPublic()));
keyWrapper.setAlgorithm(algorithm);
@ -85,8 +71,26 @@ public class JWTClientCredentialsProvider implements ClientCredentialsProvider {
keyWrapper.setPublicKey(keyPair.getPublic());
keyWrapper.setType(keyPair.getPublic().getAlgorithm());
keyWrapper.setUse(KeyUse.SIG);
// check the algorithm is valid
switch (keyPair.getPublic().getAlgorithm()) {
case KeyType.RSA:
if (!JavaAlgorithm.isRSAJavaAlgorithm(algorithm)) {
throw new RuntimeException("Invalid algorithm for a RSA KeyPair: " + algorithm);
}
this.sigCtx = new AsymmetricSignatureSignerContext(keyWrapper);
break;
case KeyType.EC:
if (!JavaAlgorithm.isECJavaAlgorithm(algorithm)) {
throw new RuntimeException("Invalid algorithm for a EC KeyPair: " + algorithm);
}
this.sigCtx = new ECDSASignatureSignerContext(keyWrapper);
break;
default:
throw new RuntimeException("Invalid KeyPair algorithm: " + keyPair.getPublic().getAlgorithm());
}
// create the key and signature context
this.keyPair = keyPair;
this.sigCtx = new AsymmetricSignatureSignerContext(keyWrapper);
}
public void setTokenTimeout(int tokenTimeout) {

View file

@ -34,12 +34,8 @@ public class ClientECDSASignatureVerifierContext extends AsymmetricSignatureVeri
@Override
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
try {
/*
Fallback for backwards compatibility of ECDSA signed tokens which were issued in previous versions.
TODO remove by https://issues.jboss.org/browse/KEYCLOAK-11911
*/
int expectedSize = ECDSASignatureProvider.ECDSA.valueOf(getAlgorithm()).getSignatureLength();
byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : ECDSASignatureProvider.concatenatedRSToASN1DER(signature, expectedSize);
int expectedSize = ECDSAAlgorithm.getSignatureLength(getAlgorithm());
byte[] derSignature = ECDSAAlgorithm.concatenatedRSToASN1DER(signature, expectedSize);
return super.verify(data, derSignature);
} catch (Exception e) {
throw new VerificationException("Signing failed", e);

View file

@ -1,11 +1,8 @@
package org.keycloak.crypto;
import org.keycloak.common.VerificationException;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.models.KeycloakSession;
import java.io.IOException;
public class ECDSASignatureProvider implements SignatureProvider {
private final KeycloakSession session;
@ -42,28 +39,4 @@ public class ECDSASignatureProvider implements SignatureProvider {
public boolean isAsymmetricAlgorithm() {
return true;
}
public static byte[] concatenatedRSToASN1DER(final byte[] signature, int signLength) throws IOException {
return CryptoIntegration.getProvider().getEcdsaCryptoProvider().concatenatedRSToASN1DER(signature, signLength);
}
public static byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int signLength) throws IOException {
return CryptoIntegration.getProvider().getEcdsaCryptoProvider().asn1derToConcatenatedRS(derEncodedSignatureValue, signLength);
}
public enum ECDSA {
ES256(64),
ES384(96),
ES512(132);
private final int signatureLength;
ECDSA(int signatureLength) {
this.signatureLength = signatureLength;
}
public int getSignatureLength() {
return this.signatureLength;
}
}
}

View file

@ -2,7 +2,7 @@ package org.keycloak.crypto;
import org.keycloak.models.KeycloakSession;
public class ServerECDSASignatureSignerContext extends AsymmetricSignatureSignerContext {
public class ServerECDSASignatureSignerContext extends ECDSASignatureSignerContext {
public ServerECDSASignatureSignerContext(KeycloakSession session, String algorithm) throws SignatureException {
super(ServerAsymmetricSignatureSignerContext.getKey(session, algorithm));
@ -11,14 +11,4 @@ public class ServerECDSASignatureSignerContext extends AsymmetricSignatureSigner
public ServerECDSASignatureSignerContext(KeyWrapper key) {
super(key);
}
@Override
public byte[] sign(byte[] data) throws SignatureException {
try {
int size = ECDSASignatureProvider.ECDSA.valueOf(getAlgorithm()).getSignatureLength();
return ECDSASignatureProvider.asn1derToConcatenatedRS(super.sign(data), size);
} catch (Exception e) {
throw new SignatureException("Signing failed", e);
}
}
}

View file

@ -15,12 +15,8 @@ public class ServerECDSASignatureVerifierContext extends AsymmetricSignatureVer
@Override
public boolean verify(byte[] data, byte[] signature) throws VerificationException {
try {
/*
Fallback for backwards compatibility of ECDSA signed tokens which were issued in previous versions.
TODO remove by https://issues.jboss.org/browse/KEYCLOAK-11911
*/
int expectedSize = ECDSASignatureProvider.ECDSA.valueOf(getAlgorithm()).getSignatureLength();
byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : ECDSASignatureProvider.concatenatedRSToASN1DER(signature, expectedSize);
int expectedSize = ECDSAAlgorithm.getSignatureLength(getAlgorithm());
byte[] derSignature = ECDSAAlgorithm.concatenatedRSToASN1DER(signature, expectedSize);
return super.verify(data, derSignature);
} catch (Exception e) {
throw new VerificationException("Signing failed", e);

View file

@ -40,7 +40,7 @@ import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.enums.SslRequired;
import org.keycloak.common.util.Base64Url;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.ECDSASignatureProvider;
import org.keycloak.crypto.ECDSAAlgorithm;
import org.keycloak.crypto.KeyUse;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@ -1318,12 +1318,12 @@ public class AccessTokenTest extends AbstractKeycloakTest {
}
private void validateTokenECDSASignature(String expectedAlg) {
assertThat(ECDSASignatureProvider.ECDSA.values(), hasItemInArray(ECDSASignatureProvider.ECDSA.valueOf(expectedAlg)));
assertThat(ECDSAAlgorithm.values(), hasItemInArray(ECDSAAlgorithm.valueOf(expectedAlg)));
try {
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, expectedAlg);
TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), expectedAlg);
validateTokenSignatureLength(ECDSASignatureProvider.ECDSA.valueOf(expectedAlg).getSignatureLength());
validateTokenSignatureLength(ECDSAAlgorithm.getSignatureLength(expectedAlg));
} finally {
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app"), Algorithm.RS256);

View file

@ -57,7 +57,7 @@ import org.keycloak.common.util.UriUtils;
import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.ECDSASignatureProvider;
import org.keycloak.crypto.ECDSAAlgorithm;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.events.Details;
@ -398,7 +398,7 @@ public class ClientAuthSignedJWTTest extends AbstractKeycloakTest {
private void testECDSASignatureLength(String clientSignedToken, String alg) {
String encodedSignature = clientSignedToken.split("\\.",3)[2];
byte[] signature = Base64Url.decode(encodedSignature);
assertEquals(ECDSASignatureProvider.ECDSA.valueOf(alg).getSignatureLength(), signature.length);
assertEquals(ECDSAAlgorithm.getSignatureLength(alg), signature.length);
}
private String getClientSignedToken(String alg) throws Exception {

View file

@ -24,15 +24,12 @@ import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.ECGenParameterSpec;
@ -55,8 +52,13 @@ import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.common.util.Time;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.JavaAlgorithm;
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
import org.keycloak.crypto.KeyType;
import org.keycloak.crypto.KeyUse;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.ECDSASignatureSignerContext;
import org.keycloak.crypto.SignatureException;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jwk.ECPublicJWK;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.RSAPublicJWK;
@ -64,7 +66,6 @@ import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.UserInfo;
import org.keycloak.representations.dpop.DPoP;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@ -566,6 +567,17 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
return keyPair;
}
private static SignatureSignerContext createSignatureSignerContext(KeyWrapper keyWrapper) {
switch (keyWrapper.getType()) {
case KeyType.RSA:
return new AsymmetricSignatureSignerContext(keyWrapper);
case KeyType.EC:
return new ECDSASignatureSignerContext(keyWrapper);
default:
throw new IllegalArgumentException("No signer provider for key algorithm type " + keyWrapper.getType());
}
}
private static String generateSignedDPoPProof(String jti, String htm, String htu, Long iat, String algorithm, JWSHeader jwsHeader, PrivateKey privateKey) throws IOException {
String dpopProofHeaderEncoded = Base64Url.encode(JsonSerialization.writeValueAsBytes(jwsHeader));
@ -579,14 +591,18 @@ public class DPoPTest extends AbstractTestRealmKeycloakTest {
String dpopProofPayloadEncoded = Base64Url.encode(JsonSerialization.writeValueAsBytes(dpop));
try {
Signature signature = Signature.getInstance(JavaAlgorithm.getJavaAlgorithm(algorithm));
signature.initSign(privateKey);
KeyWrapper keyWrapper = new KeyWrapper();
keyWrapper.setKid(jwsHeader.getKeyId());
keyWrapper.setAlgorithm(algorithm);
keyWrapper.setPrivateKey(privateKey);
keyWrapper.setType(privateKey.getAlgorithm());
keyWrapper.setUse(KeyUse.SIG);
SignatureSignerContext sigCtx = createSignatureSignerContext(keyWrapper);
String data = dpopProofHeaderEncoded + "." + dpopProofPayloadEncoded;
byte[] dataByteArray = data.getBytes();
signature.update(dataByteArray);
byte[] signatureByteArray = signature.sign();
byte[] signatureByteArray = sigCtx.sign(data.getBytes());
return data + "." + Base64Url.encode(signatureByteArray);
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
} catch (SignatureException e) {
throw new RuntimeException(e);
}
}