From 105fc0069ad670dc2a65d29894f5e5d7509130fa Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Wed, 28 Feb 2024 16:11:17 +0000 Subject: [PATCH] Fix the OIDC token verification failure with the inlined cert chain --- .../io/quarkus/oidc/OidcTenantConfig.java | 28 +++++++- .../runtime/CertChainPublicKeyResolver.java | 32 ++++++++- .../runtime/X509IdentityProvider.java | 2 +- .../io/quarkus/it/keycloak/AdminResource.java | 8 +++ .../src/main/resources/application.properties | 12 ++-- .../main/resources/truststore-rootcert.p12 | Bin 0 -> 1862 bytes .../src/main/resources/truststore.p12 | Bin 1638 -> 3238 bytes .../BearerTokenAuthorizationTest.java | 66 ++++++++++++++++-- 8 files changed, 132 insertions(+), 16 deletions(-) create mode 100644 integration-tests/oidc-wiremock/src/main/resources/truststore-rootcert.p12 diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index f261186045c1c..19419851a9474 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -176,14 +176,32 @@ public void setIncludeClientId(boolean includeClientId) { /** * Configuration of the certificate chain which can be used to verify tokens. - * If the certificate chain trusstore is configured, the tokens can be verified using the certificate + * If the certificate chain truststore is configured, the tokens can be verified using the certificate * chain inlined in the Base64-encoded format as an `x5c` header in the token itself. + *

+ * The certificate chain inlined in the token is verified. + * Signature of every certificate in the chain but the root certificate is verified by the next certificate in the chain. + * Thumbprint of the root certificate in the chain must match a thumbprint of one of the certificates in the truststore. + *

+ * Additionally, a direct trust in the leaf chain certificate which will be used to verify the token signature must + * be established. + * By default, the leaf certificate's thumbprint must match a thumbprint of one of the certificates in the truststore. + * If the truststore does not have the leaf certificate imported, then the leaf certificate must be identified by its Common + * Name. */ @ConfigItem public CertificateChain certificateChain = new CertificateChain(); @ConfigGroup public static class CertificateChain { + /** + * Common name of the leaf certificate. It must be set if the {@link #trustStoreFile} does not have + * this certificate imported. + * + */ + @ConfigItem + public Optional leafCertificateName = Optional.empty(); + /** * Truststore file which keeps thumbprints of the trusted certificates. */ @@ -233,6 +251,14 @@ public Optional getTrustStoreFileType() { public void setTrustStoreFileType(Optional trustStoreFileType) { this.trustStoreFileType = trustStoreFileType; } + + public Optional getLeafCertificateName() { + return leafCertificateName; + } + + public void setLeafCertificateName(String leafCertificateName) { + this.leafCertificateName = Optional.of(leafCertificateName); + } } /** diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java index ae0105fce3bcf..069ad2efb7704 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CertChainPublicKeyResolver.java @@ -3,6 +3,7 @@ import java.security.Key; import java.security.cert.X509Certificate; import java.util.List; +import java.util.Optional; import java.util.Set; import org.jboss.logging.Logger; @@ -12,11 +13,13 @@ import io.quarkus.oidc.OidcTenantConfig.CertificateChain; import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.security.runtime.X509IdentityProvider; import io.vertx.ext.auth.impl.CertificateHelper; public class CertChainPublicKeyResolver implements RefreshableVerificationKeyResolver { private static final Logger LOG = Logger.getLogger(OidcProvider.class); final Set thumbprints; + final Optional expectedLeafCertificateName; public CertChainPublicKeyResolver(CertificateChain chain) { if (chain.trustStorePassword.isEmpty()) { @@ -25,6 +28,7 @@ public CertChainPublicKeyResolver(CertificateChain chain) { } this.thumbprints = TrustStoreUtils.getTrustedCertificateThumbprints(chain.trustStoreFile.get(), chain.trustStorePassword.get(), chain.trustStoreCertAlias, chain.getTrustStoreFileType()); + this.expectedLeafCertificateName = chain.leafCertificateName; } @Override @@ -37,9 +41,29 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex LOG.debug("Token does not have an 'x5c' certificate chain header"); return null; } - String thumbprint = TrustStoreUtils.calculateThumprint(chain.get(0)); - if (!thumbprints.contains(thumbprint)) { - throw new UnresolvableKeyException("Certificate chain thumprint is invalid"); + if (chain.size() == 0) { + LOG.debug("Token 'x5c' certificate chain is empty"); + return null; + } + LOG.debug("Checking a thumbprint of the root chain certificate"); + String rootThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1)); + if (!thumbprints.contains(rootThumbprint)) { + LOG.error("Thumprint of the root chain certificate is invalid"); + throw new UnresolvableKeyException("Thumprint of the root chain certificate is invalid"); + } + if (expectedLeafCertificateName.isEmpty()) { + LOG.debug("Checking a thumbprint of the leaf chain certificate"); + String thumbprint = TrustStoreUtils.calculateThumprint(chain.get(0)); + if (!thumbprints.contains(thumbprint)) { + LOG.error("Thumprint of the leaf chain certificate is invalid"); + throw new UnresolvableKeyException("Thumprint of the leaf chain certificate is invalid"); + } + } else { + String leafCertificateName = X509IdentityProvider.getCommonName(chain.get(0).getSubjectX500Principal()); + if (!expectedLeafCertificateName.get().equals(leafCertificateName)) { + LOG.errorf("Wrong leaf certificate common name: %s", leafCertificateName); + throw new UnresolvableKeyException("Wrong leaf certificate common name"); + } } //TODO: support revocation lists CertificateHelper.checkValidity(chain, null); @@ -50,6 +74,8 @@ public Key resolveKey(JsonWebSignature jws, List nestingContex root.verify(root.getPublicKey()); } return chain.get(0).getPublicKey(); + } catch (UnresolvableKeyException ex) { + throw ex; } catch (Exception ex) { throw new UnresolvableKeyException("Invalid certificate chain", ex); } diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java index d7bcff7deb67c..63d79961e261b 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/X509IdentityProvider.java @@ -60,7 +60,7 @@ private Set extractRoles(X509Certificate certificate, Map&LNQU+thDZTr0|Wso1Q3gq`{dQWc@bB20%|E54N$P+QzS#6<4_SJ8X{N z^QIqSz3_PeGJbvEsINt?>p`bfyt(L#mvQGtN1wG*OS1`~6FT z*+=sLB~J!X#%6@SQG$rib9=|MyseFVrIEAJrT*WtNyBd zc+KPna<_%8YdmAO-rQi>?7JBq+1Odgx zg2!gIcZ2(D?;;QlT#`K41vGP**m4K zv&!3S1P6KCzyvThOKnn`kUzM*fknOk=PXaC>OSDWPpX z2tjKugnA#<(L$WkaV@~wDkcfO6I!n2DF#N&>@3xYbok7xZQmZur=mK*%sHLEy%kCS zYChphDX5ZlfNSdYhsp_V@LgObgUwX6*QoyTvzemWxZJO6`57ub_58O6>IByZRJ<*#!@!(E#ok#YX zcNxACg~_DSmO;tAZ2)q|rYT;2`sKKX?Gc)b zoKra#A*gv(nFXoDoS#wCunoTUE(w|r1(Wa+$cFV)QF3Ic%s|KQRzfkBjN=Sir_~;E z>N3y#B7`V1RBPNz*Nhn-FygYiTE1LM0zO#NB+vY;CVo&UIL$t=+B@_sv*HiC=X9_r z)S=UHiOPAlP0^y=XA?K9K9-&dz&Q293PM!+f>=avLlIT2lL{3dXh##?ZXV~wtrC7C@pC4m0+S9_u zc;;>IvwrZ|#ksofds!Z%=MgRxgj)TY@12e zbvw463b;1Ohy-uBV@7D?&O>cao#!aEEH;Y6MbR(Ac`jqfyXW`FXo0o!4iAlp0{ARh zqp5cis&e>c;af#vM}kz;`6KTMgFZIlRPsUqY2(I%$(WL@^IoC*&Wu(%9RZ5$fE$};f)I74cVZ)JwLG?G3Sli|}C0W)3 zU^L(P*`7J~)-;R_Zr}g|30#FNC9O71OfpC00bb}ni)SqkMA-2 zKt1@jFp48R!rV!`zZ3cq6tZ&=4~0hr6gSQPH-h^YW8?LM;J@nkEt7F+?g9cQ5J6;z AH~;_u literal 0 HcmV?d00001 diff --git a/integration-tests/oidc-wiremock/src/main/resources/truststore.p12 b/integration-tests/oidc-wiremock/src/main/resources/truststore.p12 index 81b0be2ede57e8ec7271b59b475d41bc6789a450..b0c1f8bcb4164b99b5efca0cf4ab719bb66d7210 100644 GIT binary patch delta 3196 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