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