package org.keycloak.services.util;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.common.util.Base64Url;
import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.x509.X509ClientCertificateLookup;
public class MtlsHoKTokenUtil {
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3.1
protected static final Logger logger = Logger.getLogger(MtlsHoKTokenUtil.class);
private static final String DIGEST_ALG = "SHA-256";
public static final String CERT_VERIFY_ERROR_DESC = "Client certificate missing, or its thumbprint and one in the refresh token did NOT match";
public static AccessToken.CertConf bindTokenWithClientCertificate(HttpRequest request, KeycloakSession session) {
X509Certificate[] certs = getCertificateChain(request, session);
if (certs == null || certs.length < 1) {
logger.warnf("no client certificate available.");
return null;
}
String DERX509Base64UrlEncoded = null;
try {
// On Certificate Chain, first entry is considered to be client certificate.
DERX509Base64UrlEncoded = getCertificateThumbprintInSHA256DERX509Base64UrlEncoded(certs[0]);
if (logger.isTraceEnabled()) dumpCertInfo(certs);
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
// give up issuing MTLS HoK Token
logger.warnf("give up issuing hok token. %s", e);
return null;
}
AccessToken.CertConf certConf = new AccessToken.CertConf();
certConf.setCertThumbprint(DERX509Base64UrlEncoded);
return certConf;
}
public static boolean verifyTokenBindingWithClientCertificate(AccessToken token, HttpRequest request, KeycloakSession session) {
if (token == null) {
logger.warnf("token is null");
return false;
}
// Bearer Token, not MTLS HoK Token
if (token.getCertConf() == null) {
logger.warnf("bearer token received instead of hok token.");
return false;
}
X509Certificate[] certs = getCertificateChain(request, session);
// HoK Token, but no Client Certificate available
if (certs == null || certs.length < 1) {
logger.warnf("missing client certificate.");
return false;
}
String DERX509Base64UrlEncoded = null;
String x5ts256 = token.getCertConf().getCertThumbprint();
logger.tracef("hok token cnf-x5t#s256 = %s", x5ts256);
try {
// On Certificate Chain, first entry is considered to be client certificate.
DERX509Base64UrlEncoded = getCertificateThumbprintInSHA256DERX509Base64UrlEncoded(certs[0]);
if (logger.isTraceEnabled()) dumpCertInfo(certs);
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
logger.warnf("client certificate exception. %s", e);
return false;
}
if (!MessageDigest.isEqual(x5ts256.getBytes(), DERX509Base64UrlEncoded.getBytes())) {
logger.warnf("certificate's thumbprint and one in the refresh token did not match.");
return false;
}
return true;
}
private static X509Certificate[] getCertificateChain(HttpRequest request, KeycloakSession session) {
try {
// Get a x509 client certificate
X509ClientCertificateLookup provider = session.getProvider(X509ClientCertificateLookup.class);
if (provider == null) {
logger.errorv("\"{0}\" Spi is not available, did you forget to update the configuration?", X509ClientCertificateLookup.class);
return null;
}
X509Certificate[] certs = provider.getCertificateChain(request);
return certs;
} catch (GeneralSecurityException e) {
logger.error(e.getMessage(), e);
}
return null;
}
private static String getCertificateThumbprintInSHA256DERX509Base64UrlEncoded (X509Certificate cert) throws NoSuchAlgorithmException, CertificateEncodingException {
// need to calculate over DER encoding of the X.509 certificate
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3.1
// in order to do that, call getEncoded()
// https://docs.oracle.com/javase/8/docs/api/java/security/cert/Certificate.html#getEncoded--
byte[] DERX509Hash = cert.getEncoded();
MessageDigest md = MessageDigest.getInstance(DIGEST_ALG);
md.update(DERX509Hash);
String DERX509Base64UrlEncoded = Base64Url.encode(md.digest());
return DERX509Base64UrlEncoded;
}
private static void dumpCertInfo(X509Certificate[] certs) throws CertificateEncodingException {
logger.tracef(":: Try Holder of Key Token");
logger.tracef(":: # of x509 Client Certificate in Certificate Chain = %d", certs.length);
for (int i = 0; i < certs.length; i++) {
logger.tracef(":: certs[%d] Raw Bytes Counts of first x509 Client Certificate in Certificate Chain = %d", i, certs[i].toString().length());
logger.tracef(":: certs[%d] Raw Bytes String of first x509 Client Certificate in Certificate Chain = %s", i, certs[i].toString());
logger.tracef(":: certs[%d] DER Dump Bytes of first x509 Client Certificate in Certificate Chain = %d", i, certs[i].getEncoded().length);
String DERX509Base64UrlEncoded = null;
try {
DERX509Base64UrlEncoded = getCertificateThumbprintInSHA256DERX509Base64UrlEncoded(certs[i]);
} catch (Exception e) {}
logger.tracef(":: certs[%d] Base64URL Encoded SHA-256 Hash of DER formatted first x509 Client Certificate in Certificate Chain = %s", i, DERX509Base64UrlEncoded);
logger.tracef(":: certs[%d] DER Dump Bytes of first x509 Client Certificate TBScertificate in Certificate Chain = %d", i, certs[i].getTBSCertificate().length);
logger.tracef(":: certs[%d] Signature Algorithm of first x509 Client Certificate in Certificate Chain = %s", i, certs[i].getSigAlgName());
logger.tracef(":: certs[%d] Certfication Type of first x509 Client Certificate in Certificate Chain = %s", i, certs[i].getType());
logger.tracef(":: certs[%d] Issuer DN of first x509 Client Certificate in Certificate Chain = %s", i, certs[i].getIssuerDN().getName());
logger.tracef(":: certs[%d] Subject DN of first x509 Client Certificate in Certificate Chain = %s", i, certs[i].getSubjectDN().getName());
}
}
}