fix: refining https file type detection (#33703)

also making common trustore logic align

closes: #33649

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins 2024-10-22 13:05:56 -04:00 committed by GitHub
parent e3936a9b38
commit af1a5ea2a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 164 additions and 97 deletions

View file

@ -39,7 +39,7 @@ public class KeystoreUtil {
public enum KeystoreFormat { public enum KeystoreFormat {
JKS("jks"), JKS("jks"),
PKCS12("p12", "pfx"), PKCS12("p12", "pfx", "pkcs12"),
BCFKS("bcfks"); BCFKS("bcfks");
// Typical file extension for this keystore format // Typical file extension for this keystore format
@ -106,6 +106,17 @@ public class KeystoreUtil {
} }
} }
public static Optional<KeystoreFormat> getKeystoreFormat(String path) {
int lastDotIndex = path.lastIndexOf('.');
if (lastDotIndex > -1) {
String ext = path.substring(lastDotIndex + 1).toLowerCase();
return Arrays.stream(KeystoreUtil.KeystoreFormat.values())
.filter(ksFormat -> ksFormat.getFileExtensions().contains(ext))
.findFirst();
}
return Optional.empty();
}
/** /**
* Try to return supported keystore type * Try to return supported keystore type
@ -120,13 +131,9 @@ public class KeystoreUtil {
if (preferredType != null) return preferredType; if (preferredType != null) return preferredType;
// Fallback to path // Fallback to path
int lastDotIndex = path.lastIndexOf('.'); Optional<KeystoreFormat> format = getKeystoreFormat(path);
if (lastDotIndex > -1) { if (format.isPresent()) {
String ext = path.substring(lastDotIndex + 1).toLowerCase(); return format.get().toString();
Optional<KeystoreFormat> detectedType = Arrays.stream(KeystoreUtil.KeystoreFormat.values())
.filter(ksFormat -> ksFormat.getFileExtensions().contains(ext))
.findFirst();
if (detectedType.isPresent()) return detectedType.get().toString();
} }
// Fallback to default // Fallback to default

View file

@ -1,8 +1,9 @@
package org.keycloak.common.util; package org.keycloak.common.util;
import org.junit.Test;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import org.junit.Test;
public class KeystoreUtilTest { public class KeystoreUtilTest {
@ -11,6 +12,15 @@ public class KeystoreUtilTest {
assertEquals("x", KeystoreUtil.getKeystoreType("x", "y", "z")); assertEquals("x", KeystoreUtil.getKeystoreType("x", "y", "z"));
assertEquals("z", KeystoreUtil.getKeystoreType(null, "y", "z")); assertEquals("z", KeystoreUtil.getKeystoreType(null, "y", "z"));
assertEquals(KeystoreUtil.KeystoreFormat.PKCS12.name(), KeystoreUtil.getKeystoreType(null, "y.pfx", "z")); assertEquals(KeystoreUtil.KeystoreFormat.PKCS12.name(), KeystoreUtil.getKeystoreType(null, "y.pfx", "z"));
assertEquals(KeystoreUtil.KeystoreFormat.PKCS12.name(), KeystoreUtil.getKeystoreType(null, "y.pkcs12", "z"));
}
@Test
public void testGetFormat() {
assertFalse(KeystoreUtil.getKeystoreFormat("some.file").isPresent());
assertFalse(KeystoreUtil.getKeystoreFormat("somepfx").isPresent());
assertEquals(KeystoreUtil.KeystoreFormat.PKCS12, KeystoreUtil.getKeystoreFormat("file.pfx").get());
assertEquals(KeystoreUtil.KeystoreFormat.JKS, KeystoreUtil.getKeystoreFormat("file.jks").get());
} }
} }

View file

@ -275,7 +275,7 @@ To ensure proper TLS configuration, use the `tlsSecret` and `truststores` fields
If you need to provide trusted certificates, the Keycloak CR provides a top level feature for configuring the server's truststore as discussed in <@links.server id="keycloak-truststore"/>. If you need to provide trusted certificates, the Keycloak CR provides a top level feature for configuring the server's truststore as discussed in <@links.server id="keycloak-truststore"/>.
Use the truststores stanza of the Keycloak spec to specify Secrets containing PEM encoded files, or PKCS12 files with extension `.p12` or `.pfx`, for example: Use the truststores stanza of the Keycloak spec to specify Secrets containing PEM encoded files, or PKCS12 files with extension `.p12`, `.pfx`, or `.pkcs12`, for example:
[source,yaml] [source,yaml]
---- ----

View file

@ -22,12 +22,20 @@ When you use a pair of matching certificate and private key files in PEM format,
{project_name} creates a keystore out of these files in memory and uses this keystore afterwards. {project_name} creates a keystore out of these files in memory and uses this keystore afterwards.
== Providing a Java Keystore == Providing a Keystore
When no keystore file is explicitly configured, but `http-enabled` is set to false, {project_name} looks for a `conf/server.keystore` file. When no keystore file is explicitly configured, but `http-enabled` is set to false, {project_name} looks for a `conf/server.keystore` file.
As an alternative, you can use an existing keystore by running the following command: As an alternative, you can use an existing keystore by running the following command:
<@kc.start parameters="--https-key-store-file=/path/to/existing-keystore-file"/> <@kc.start parameters="--https-key-store-file=/path/to/existing-keystore-file"/>
Recognized file extensions for a keystore:
* `.p12`, `.pkcs12`, and `.pfx` for a pkcs12 file
* `.jks`, and `.keystore` for a jks file
* `.key`, `.crt`, and `.pem` for a pem file
If your keystore does not have an extension matching its file type, you will also need to set the `https-key-store-type` option.
=== Setting the Keystore password === Setting the Keystore password
You can set a secure password for your keystore using the `https-key-store-password` option: You can set a secure password for your keystore using the `https-key-store-password` option:
<@kc.start parameters="--https-key-store-password=<value>"/> <@kc.start parameters="--https-key-store-password=<value>"/>

View file

@ -12,7 +12,7 @@ The certificates of these clients or servers, or the CA that signed these certif
== Configuring the System Truststore == Configuring the System Truststore
The existing Java default truststore certs will always be trusted. If you need additional certificates, which will be the case if you have self-signed or internal certificate authorities that are not recognized by the JRE, they can be included in the `conf/truststores` directory or subdirectories. The certs may be in PEM files, or PKCS12 files with extension `.p12` or `.pfx`. If in PKCS12, the certs must be unencrypted - meaning no password is expected. The existing Java default truststore certs will always be trusted. If you need additional certificates, which will be the case if you have self-signed or internal certificate authorities that are not recognized by the JRE, they can be included in the `conf/truststores` directory or subdirectories. The certs may be in PEM files, or PKCS12 files with extension `.p12`, `.pfx`, or `.pkcs12`. If in PKCS12, the certs must be unencrypted - meaning no password is expected.
If you need an alternative path, use the `--truststore-paths` option to specify additional files or directories where PEM or PKCS12 files are located. Paths are relative to where you launched {project_name}, so absolute paths are recommended instead. If a directory is specified, it will be recursively scanned for truststore files. If you need an alternative path, use the `--truststore-paths` option to specify additional files or directories where PEM or PKCS12 files are located. Paths are relative to where you launched {project_name}, so absolute paths are recommended instead. If a directory is specified, it will be recursively scanned for truststore files.

View file

@ -35,6 +35,14 @@ By default, {project_name} uses the System Truststore to validate certificates.
If you need to use a dedicated truststore for mTLS, you can configure the location of this truststore by running the following command: If you need to use a dedicated truststore for mTLS, you can configure the location of this truststore by running the following command:
<@kc.start parameters="--https-trust-store-file=/path/to/file --https-trust-store-password=<value>"/> <@kc.start parameters="--https-trust-store-file=/path/to/file --https-trust-store-password=<value>"/>
Recognized file extensions for a truststore:
* `.p12`, `.pkcs12`, and `.pfx` for a pkcs12 file
* `.jks`, and `.truststore` for a jks file
* `.ca`, `.crt`, and `.pem` for a pem file
If your truststore does not have an extension matching its file type, you will also need to set the `https-key-store-type` option.
== Additional resources == Additional resources
=== Using mTLS for outgoing HTTP requests === Using mTLS for outgoing HTTP requests

View file

@ -9,7 +9,7 @@ public class TruststoreOptions {
public static final Option<List<String>> TRUSTSTORE_PATHS = OptionBuilder.listOptionBuilder("truststore-paths", String.class) public static final Option<List<String>> TRUSTSTORE_PATHS = OptionBuilder.listOptionBuilder("truststore-paths", String.class)
.category(OptionCategory.TRUSTSTORE) .category(OptionCategory.TRUSTSTORE)
.description("List of pkcs12 (p12 or pfx file extensions), PEM files, or directories containing those files that will be used as a system truststore.") .description("List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or directories containing those files that will be used as a system truststore.")
.build(); .build();
public static final Option<HostnameVerificationPolicy> HOSTNAME_VERIFICATION_POLICY = new OptionBuilder<>("tls-hostname-verifier", HostnameVerificationPolicy.class) public static final Option<HostnameVerificationPolicy> HOSTNAME_VERIFICATION_POLICY = new OptionBuilder<>("tls-hostname-verifier", HostnameVerificationPolicy.class)

View file

@ -44,6 +44,9 @@ public final class ExecutionExceptionHandler implements CommandLine.IExecutionEx
if (cause instanceof PropertyException) { if (cause instanceof PropertyException) {
PrintWriter writer = cmd.getErr(); PrintWriter writer = cmd.getErr();
writer.println(cmd.getColorScheme().errorText(cause.getMessage())); writer.println(cmd.getColorScheme().errorText(cause.getMessage()));
if (verbose && cause.getCause() != null) {
dumpException(writer, cause.getCause());
}
return ShortErrorMessageHandler.getInvalidInputExitCode(cause, cmd); return ShortErrorMessageHandler.getInvalidInputExitCode(cause, cmd);
} }
error(cmd.getErr(), "Failed to run '" + parseResult.subcommands().stream() error(cmd.getErr(), "Failed to run '" + parseResult.subcommands().stream()

View file

@ -1,5 +1,6 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import io.quarkus.runtime.util.ClassPathUtils;
import io.quarkus.vertx.http.runtime.CertificateConfig; import io.quarkus.vertx.http.runtime.CertificateConfig;
import io.quarkus.vertx.http.runtime.options.TlsUtils; import io.quarkus.vertx.http.runtime.options.TlsUtils;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
@ -14,9 +15,10 @@ import org.keycloak.quarkus.runtime.configuration.Configuration;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.BiFunction;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
@ -24,6 +26,10 @@ public final class HttpPropertyMappers {
private static final int MIN_MAX_THREADS = 50; private static final int MIN_MAX_THREADS = 50;
private static final String QUARKUS_HTTPS_CERT_FILES = "quarkus.http.ssl.certificate.files"; private static final String QUARKUS_HTTPS_CERT_FILES = "quarkus.http.ssl.certificate.files";
private static final String QUARKUS_HTTPS_CERT_KEY_FILES = "quarkus.http.ssl.certificate.key-files"; private static final String QUARKUS_HTTPS_CERT_KEY_FILES = "quarkus.http.ssl.certificate.key-files";
private static final String QUARKUS_HTTPS_KEY_STORE_FILE = "quarkus.http.ssl.certificate.key-store-file";
private static final String QUARKUS_HTTPS_TRUST_STORE_FILE = "quarkus.http.ssl.certificate.trust-store-file";
private static final String QUARKUS_HTTPS_TRUST_STORE_FILE_TYPE = "quarkus.http.ssl.certificate.trust-store-file-type";
private static final String QUARKUS_HTTPS_KEY_STORE_FILE_TYPE = "quarkus.http.ssl.certificate.key-store-file-type";
private HttpPropertyMappers(){} private HttpPropertyMappers(){}
@ -72,17 +78,18 @@ public final class HttpPropertyMappers {
.build(), .build(),
fromOption(HttpOptions.HTTPS_CERTIFICATE_FILE) fromOption(HttpOptions.HTTPS_CERTIFICATE_FILE)
.to(QUARKUS_HTTPS_CERT_FILES) .to(QUARKUS_HTTPS_CERT_FILES)
.transformer(HttpPropertyMappers.validatePath(QUARKUS_HTTPS_CERT_FILES)) .transformer(HttpPropertyMappers::transformPath)
.paramLabel("file") .paramLabel("file")
.build(), .build(),
fromOption(HttpOptions.HTTPS_CERTIFICATE_KEY_FILE) fromOption(HttpOptions.HTTPS_CERTIFICATE_KEY_FILE)
.to(QUARKUS_HTTPS_CERT_KEY_FILES) .to(QUARKUS_HTTPS_CERT_KEY_FILES)
.transformer(HttpPropertyMappers.validatePath(QUARKUS_HTTPS_CERT_KEY_FILES)) .transformer(HttpPropertyMappers::transformPath)
.paramLabel("file") .paramLabel("file")
.build(), .build(),
fromOption(HttpOptions.HTTPS_KEY_STORE_FILE fromOption(HttpOptions.HTTPS_KEY_STORE_FILE
.withRuntimeSpecificDefault(getDefaultKeystorePathValue())) .withRuntimeSpecificDefault(getDefaultKeystorePathValue()))
.to("quarkus.http.ssl.certificate.key-store-file") .to(QUARKUS_HTTPS_KEY_STORE_FILE)
.transformer(HttpPropertyMappers::transformPath)
.paramLabel("file") .paramLabel("file")
.build(), .build(),
fromOption(HttpOptions.HTTPS_KEY_STORE_PASSWORD) fromOption(HttpOptions.HTTPS_KEY_STORE_PASSWORD)
@ -91,12 +98,13 @@ public final class HttpPropertyMappers {
.isMasked(true) .isMasked(true)
.build(), .build(),
fromOption(HttpOptions.HTTPS_KEY_STORE_TYPE) fromOption(HttpOptions.HTTPS_KEY_STORE_TYPE)
.to("quarkus.http.ssl.certificate.key-store-file-type")
.mapFrom(SecurityOptions.FIPS_MODE, HttpPropertyMappers::resolveKeyStoreType) .mapFrom(SecurityOptions.FIPS_MODE, HttpPropertyMappers::resolveKeyStoreType)
.to(QUARKUS_HTTPS_KEY_STORE_FILE_TYPE)
.paramLabel("type") .paramLabel("type")
.build(), .build(),
fromOption(HttpOptions.HTTPS_TRUST_STORE_FILE) fromOption(HttpOptions.HTTPS_TRUST_STORE_FILE)
.to("quarkus.http.ssl.certificate.trust-store-file") .to(QUARKUS_HTTPS_TRUST_STORE_FILE)
.transformer(HttpPropertyMappers::transformPath)
.paramLabel("file") .paramLabel("file")
.build(), .build(),
fromOption(HttpOptions.HTTPS_TRUST_STORE_PASSWORD) fromOption(HttpOptions.HTTPS_TRUST_STORE_PASSWORD)
@ -105,8 +113,9 @@ public final class HttpPropertyMappers {
.isMasked(true) .isMasked(true)
.build(), .build(),
fromOption(HttpOptions.HTTPS_TRUST_STORE_TYPE) fromOption(HttpOptions.HTTPS_TRUST_STORE_TYPE)
.to("quarkus.http.ssl.certificate.trust-store-file-type")
.mapFrom(SecurityOptions.FIPS_MODE, HttpPropertyMappers::resolveKeyStoreType) .mapFrom(SecurityOptions.FIPS_MODE, HttpPropertyMappers::resolveKeyStoreType)
.to(QUARKUS_HTTPS_TRUST_STORE_FILE_TYPE)
.transformer(HttpPropertyMappers::resolveKeyStoreType)
.paramLabel("type") .paramLabel("type")
.build(), .build(),
fromOption(HttpOptions.HTTP_MAX_QUEUED_REQUESTS) fromOption(HttpOptions.HTTP_MAX_QUEUED_REQUESTS)
@ -130,68 +139,60 @@ public final class HttpPropertyMappers {
public static void validateConfig() { public static void validateConfig() {
boolean enabled = isHttpEnabled(Configuration.getOptionalKcValue(HttpOptions.HTTP_ENABLED.getKey()).orElse(null)); boolean enabled = isHttpEnabled(Configuration.getOptionalKcValue(HttpOptions.HTTP_ENABLED.getKey()).orElse(null));
boolean trustStoreFile = Configuration.getOptionalKcValue(HttpOptions.HTTPS_TRUST_STORE_FILE.getKey()).isPresent(); Optional<String> certFile = Configuration.getOptionalKcValue(HttpOptions.HTTPS_CERTIFICATE_FILE.getKey());
boolean keyStoreFile = Configuration.getOptionalKcValue(HttpOptions.HTTPS_KEY_STORE_FILE.getKey()).isPresent(); Optional<String> keystoreFile = Configuration.getOptionalKcValue(HttpOptions.HTTPS_KEY_STORE_FILE.getKey());
if (!enabled && certFile.isEmpty() && keystoreFile.isEmpty()) {
throw new PropertyException(Messages.httpsConfigurationNotSet());
}
if (trustStoreFile) { CertificateConfig config = new CertificateConfig();
CertificateConfig config = new CertificateConfig();
config.trustStoreFile = Configuration.getOptionalKcValue(HttpOptions.HTTPS_TRUST_STORE_FILE.getKey()).map(Paths::get); config.trustStoreFile = Configuration.getOptionalKcValue(HttpOptions.HTTPS_TRUST_STORE_FILE.getKey()).map(Paths::get);
config.trustStorePassword = Configuration.getOptionalKcValue(HttpOptions.HTTPS_TRUST_STORE_PASSWORD.getKey()); config.trustStorePassword = Configuration.getOptionalKcValue(HttpOptions.HTTPS_TRUST_STORE_PASSWORD.getKey());
config.trustStoreFileType = Configuration.getOptionalKcValue(HttpOptions.HTTPS_TRUST_STORE_TYPE.getKey()); config.trustStoreFileType = Configuration.getOptionalKcValue(HttpOptions.HTTPS_TRUST_STORE_TYPE.getKey());
config.trustStoreProvider = Configuration.getOptionalValue("quarkus.http.ssl.certificate.trust-store-provider"); config.trustStoreProvider = Configuration.getOptionalValue("quarkus.http.ssl.certificate.trust-store-provider");
config.trustStoreCertAlias = Configuration.getOptionalValue("quarkus.http.ssl.certificate.trust-store-cert-alias"); config.trustStoreCertAlias = Configuration.getOptionalValue("quarkus.http.ssl.certificate.trust-store-cert-alias");
config.trustStoreFiles = Optional.empty(); config.trustStoreFiles = Optional.empty();
try { config.keyStoreFile = keystoreFile.map(Paths::get);
TlsUtils.computeTrustOptions(config, config.trustStorePassword); config.keyStorePassword = Configuration.getOptionalKcValue(HttpOptions.HTTPS_KEY_STORE_PASSWORD.getKey());
} catch (IOException e) { config.keyStoreFileType = Configuration.getOptionalKcValue(HttpOptions.HTTPS_KEY_STORE_TYPE.getKey());
throw new PropertyException("Failed to load 'https-trust-store' material.", e); config.keyStoreProvider = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-provider");
} catch (IllegalArgumentException e) { config.keyStoreAlias = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-alias");
config.keyStoreAliasPassword = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-alias-password");
config.keyStoreAliasPasswordKey = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-alias-password-key");
config.keyStoreKeyAlias = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-key-alias");
config.keyFiles = Configuration.getOptionalKcValue(HttpOptions.HTTPS_CERTIFICATE_KEY_FILE.getKey()).map(Paths::get).map(List::of);
config.files = certFile.map(Paths::get).map(List::of);
try {
TlsUtils.computeTrustOptions(config, config.trustStorePassword);
} catch (IOException e) {
throw new PropertyException("Failed to load 'https-trust-store' material: " + e.getClass().getSimpleName() + " " + e.getMessage(), e);
} catch (IllegalArgumentException e) {
if (e.getMessage().contains(QUARKUS_HTTPS_TRUST_STORE_FILE_TYPE)) {
throw new PropertyException("Unable to determine 'https-trust-store-type' automatically. " + throw new PropertyException("Unable to determine 'https-trust-store-type' automatically. " +
"Adjust the file extension or specify the property.", e); "Adjust the file extension or specify the property.", e);
} }
throw new PropertyException(e.getMessage(), e);
} }
if (keyStoreFile) { try {
CertificateConfig config = new CertificateConfig(); TlsUtils.computeKeyStoreOptions(config, config.keyStorePassword, config.keyStoreAliasPassword);
} catch (IOException e) {
config.keyStoreFile = Configuration.getOptionalKcValue(HttpOptions.HTTPS_KEY_STORE_FILE.getKey()).map(Paths::get); throw new PropertyException("Failed to load 'https-key-' material: " + e.getClass().getSimpleName() + " " + e.getMessage(), e);
config.keyStorePassword = Configuration.getOptionalKcValue(HttpOptions.HTTPS_KEY_STORE_PASSWORD.getKey()); } catch (IllegalArgumentException e) {
config.keyStoreFileType = Configuration.getOptionalKcValue(HttpOptions.HTTPS_KEY_STORE_TYPE.getKey()); if (e.getMessage().contains(QUARKUS_HTTPS_KEY_STORE_FILE_TYPE)) {
config.keyStoreProvider = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-provider");
config.keyStoreAlias = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-alias");
config.keyStoreAliasPassword = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-alias-password");
config.keyStoreAliasPasswordKey = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-alias-password-key");
config.keyStoreKeyAlias = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-key-alias");
config.keyFiles = Optional.empty();
config.files = Optional.empty();
try {
TlsUtils.computeKeyStoreOptions(config, config.keyStorePassword, config.keyStoreAliasPassword);
} catch (IOException e) {
throw new PropertyException("Failed to load 'https-key-store' material.", e);
} catch (IllegalArgumentException e) {
throw new PropertyException("Unable to determine 'https-key-store-type' automatically. " + throw new PropertyException("Unable to determine 'https-key-store-type' automatically. " +
"Adjust the file extension or specify the property.", e); "Adjust the file extension or specify the property.", e);
} }
} throw new PropertyException(e.getMessage(), e);
if (!enabled) {
Optional<String> value = Configuration.getOptionalKcValue(HttpOptions.HTTPS_CERTIFICATE_FILE.getKey());
if (value.isEmpty()) {
value = Configuration.getOptionalValue("quarkus.http.ssl.certificate.key-store-file");
}
if (value.isEmpty()) {
throw new PropertyException(Messages.httpsConfigurationNotSet());
}
} }
} }
private static BiFunction<String, ConfigSourceInterceptorContext, String> validatePath(String key) { private static String transformPath(String value, ConfigSourceInterceptorContext context) {
return (value, context) -> Environment.isWindows() && value != null && value.equals(context.proceed(key).getValue()) ? value.replace("\\", "/") : value; return value == null ? value : ClassPathUtils.toResourceName(Path.of(value));
} }
private static String getHttpEnabledTransformer(String value, ConfigSourceInterceptorContext context) { private static String getHttpEnabledTransformer(String value, ConfigSourceInterceptorContext context) {

View file

@ -288,6 +288,8 @@ public class PropertyMapper<T> {
* NOTE: This transformer will not apply to the mapFrom value. When using * NOTE: This transformer will not apply to the mapFrom value. When using
* {@link #mapFrom} you generally need a transformer specifically for the parent * {@link #mapFrom} you generally need a transformer specifically for the parent
* value, see {@link #mapFrom(Option, BiFunction)} * value, see {@link #mapFrom(Option, BiFunction)}
* <p>
* The value passed into the transformer may be null if the property has no value set, and no default
*/ */
public Builder<T> transformer(BiFunction<String, ConfigSourceInterceptorContext, String> mapper) { public Builder<T> transformer(BiFunction<String, ConfigSourceInterceptorContext, String> mapper) {
this.mapper = mapper; this.mapper = mapper;

View file

@ -207,6 +207,21 @@ public class PicocliTest extends AbstractConfigurationTest {
+ "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-driver, --db")); + "\nPossible solutions: --db-url, --db-url-host, --db-url-database, --db-url-port, --db-url-properties, --db-username, --db-password, --db-schema, --db-pool-initial-size, --db-pool-min-size, --db-pool-max-size, --db-driver, --db"));
} }
@Test
public void httpStoreTypeValidation() {
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start", "--https-key-store-file=not-there.ks", "--hostname-strict=false");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Unable to determine 'https-key-store-type' automatically. Adjust the file extension or specify the property"));
nonRunningPicocli = pseudoLaunch("start", "--https-key-store-file=not-there.ks", "--hostname-strict=false", "--https-key-store-type=jdk");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Failed to load 'https-key-' material: NoSuchFileException not-there.ks"));
nonRunningPicocli = pseudoLaunch("start", "--https-trust-store-file=not-there.jks", "--https-key-store-file=not-there.ks", "--hostname-strict=false", "--https-key-store-type=jdk");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("No trust store password provided"));
}
@Test @Test
public void failSingleParamWithSpace() { public void failSingleParamWithSpace() {
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start", "--db postgres"); NonRunningPicocli nonRunningPicocli = pseudoLaunch("start", "--db postgres");

View file

@ -23,6 +23,8 @@ import static org.junit.Assert.assertTrue;
import static org.keycloak.quarkus.runtime.Environment.isWindows; import static org.keycloak.quarkus.runtime.Environment.isWindows;
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.CLI_ARGS; import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.CLI_ARGS;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
@ -492,6 +494,17 @@ public class ConfigurationTest extends AbstractConfigurationTest {
assertEquals("2h", createConfig().getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue()); assertEquals("2h", createConfig().getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue());
} }
@Test
public void testHttpsPaths() {
ConfigArgsConfigSource.setCliArgs("--https-certificate-file=\\some\\file");
String expected = "\\some\\file";
if (FileSystems.getDefault().getSeparator().equals("\\")) {
expected = "/some/file";
}
assertEquals(expected, createConfig().getConfigValue("quarkus.http.ssl.certificate.files").getValue());
}
@Test @Test
public void testCacheMaxCount() { public void testCacheMaxCount() {
int maxCount = 500; int maxCount = 500;

View file

@ -173,13 +173,13 @@ public class QuarkusPropertiesDistTest {
@Test @Test
@BeforeStartDistribution(ForceRebuild.class) @BeforeStartDistribution(ForceRebuild.class)
@DisabledOnOs(value = { OS.WINDOWS }, disabledReason = "Windows uses a different path separator.") @DisabledOnOs(value = { OS.WINDOWS }, disabledReason = "Windows uses a different path separator.")
@Launch({ "start", "--http-enabled=true", "--hostname-strict=false", @Launch({ "start", "--verbose", "--http-enabled=true", "--hostname-strict=false",
"--https-certificate-file=/tmp/kc/bin/../conf/server.crt.pem", "--https-certificate-file=/tmp/kc/bin/../conf/server.crt.pem",
"--https-certificate-key-file=/tmp/kc/bin/../conf/server.key.pem" }) "--https-certificate-key-file=/tmp/kc/bin/../conf/server.key.pem" })
@Order(13) @Order(13)
void testHttpCertsPathTransformer(LaunchResult result) { void testHttpCertsPathTransformer(LaunchResult result) {
CLIResult cliResult = (CLIResult) result; CLIResult cliResult = (CLIResult) result;
assertThat(cliResult.getOutput(),containsString("ERROR: /tmp/kc/bin/../conf/server.crt.pem")); assertThat(cliResult.getErrorOutput(),containsString("Failed to load 'https-key-' material: NoSuchFileException /tmp/kc/bin/../conf/server.crt.pem"));
} }
@Test @Test
@ -191,7 +191,7 @@ public class QuarkusPropertiesDistTest {
@Order(14) @Order(14)
void testHttpCertsPathTransformerOnWindows(LaunchResult result) { void testHttpCertsPathTransformerOnWindows(LaunchResult result) {
CLIResult cliResult = (CLIResult) result; CLIResult cliResult = (CLIResult) result;
assertThat(cliResult.getOutput(),containsString("ERROR: C:/tmp/kc/bin/../conf/server.crt.pem")); assertThat(cliResult.getErrorOutput(),containsString("Failed to load 'https-key-' material: NoSuchFileException C:/tmp/kc/bin/../conf/server.crt.pem"));
} }
public static class AddConsoleHandlerFromQuarkusProps implements Consumer<KeycloakDistribution> { public static class AddConsoleHandlerFromQuarkusProps implements Consumer<KeycloakDistribution> {

View file

@ -150,8 +150,8 @@ Truststore:
Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated), Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated),
DEFAULT. Default: DEFAULT. DEFAULT. Default: DEFAULT.
--truststore-paths <truststore-paths> --truststore-paths <truststore-paths>
List of pkcs12 (p12 or pfx file extensions), PEM files, or directories List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or
containing those files that will be used as a system truststore. directories containing those files that will be used as a system truststore.
Export: Export:

View file

@ -248,8 +248,8 @@ Truststore:
Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated), Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated),
DEFAULT. Default: DEFAULT. DEFAULT. Default: DEFAULT.
--truststore-paths <truststore-paths> --truststore-paths <truststore-paths>
List of pkcs12 (p12 or pfx file extensions), PEM files, or directories List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or
containing those files that will be used as a system truststore. directories containing those files that will be used as a system truststore.
Export: Export:

View file

@ -150,8 +150,8 @@ Truststore:
Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated), Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated),
DEFAULT. Default: DEFAULT. DEFAULT. Default: DEFAULT.
--truststore-paths <truststore-paths> --truststore-paths <truststore-paths>
List of pkcs12 (p12 or pfx file extensions), PEM files, or directories List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or
containing those files that will be used as a system truststore. directories containing those files that will be used as a system truststore.
Import: Import:

View file

@ -248,8 +248,8 @@ Truststore:
Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated), Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated),
DEFAULT. Default: DEFAULT. DEFAULT. Default: DEFAULT.
--truststore-paths <truststore-paths> --truststore-paths <truststore-paths>
List of pkcs12 (p12 or pfx file extensions), PEM files, or directories List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or
containing those files that will be used as a system truststore. directories containing those files that will be used as a system truststore.
Import: Import:

View file

@ -321,8 +321,8 @@ Truststore:
Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated), Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated),
DEFAULT. Default: DEFAULT. DEFAULT. Default: DEFAULT.
--truststore-paths <truststore-paths> --truststore-paths <truststore-paths>
List of pkcs12 (p12 or pfx file extensions), PEM files, or directories List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or
containing those files that will be used as a system truststore. directories containing those files that will be used as a system truststore.
Security: Security:

View file

@ -454,8 +454,8 @@ Truststore:
Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated), Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated),
DEFAULT. Default: DEFAULT. DEFAULT. Default: DEFAULT.
--truststore-paths <truststore-paths> --truststore-paths <truststore-paths>
List of pkcs12 (p12 or pfx file extensions), PEM files, or directories List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or
containing those files that will be used as a system truststore. directories containing those files that will be used as a system truststore.
Security: Security:

View file

@ -322,8 +322,8 @@ Truststore:
Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated), Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated),
DEFAULT. Default: DEFAULT. DEFAULT. Default: DEFAULT.
--truststore-paths <truststore-paths> --truststore-paths <truststore-paths>
List of pkcs12 (p12 or pfx file extensions), PEM files, or directories List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or
containing those files that will be used as a system truststore. directories containing those files that will be used as a system truststore.
Security: Security:

View file

@ -455,8 +455,8 @@ Truststore:
Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated), Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated),
DEFAULT. Default: DEFAULT. DEFAULT. Default: DEFAULT.
--truststore-paths <truststore-paths> --truststore-paths <truststore-paths>
List of pkcs12 (p12 or pfx file extensions), PEM files, or directories List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or
containing those files that will be used as a system truststore. directories containing those files that will be used as a system truststore.
Security: Security:

View file

@ -273,8 +273,8 @@ Truststore:
Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated), Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated),
DEFAULT. Default: DEFAULT. DEFAULT. Default: DEFAULT.
--truststore-paths <truststore-paths> --truststore-paths <truststore-paths>
List of pkcs12 (p12 or pfx file extensions), PEM files, or directories List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or
containing those files that will be used as a system truststore. directories containing those files that will be used as a system truststore.
Bootstrap Admin: Bootstrap Admin:

View file

@ -395,8 +395,8 @@ Truststore:
Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated), Possible values are: ANY, WILDCARD (deprecated), STRICT (deprecated),
DEFAULT. Default: DEFAULT. DEFAULT. Default: DEFAULT.
--truststore-paths <truststore-paths> --truststore-paths <truststore-paths>
List of pkcs12 (p12 or pfx file extensions), PEM files, or directories List of pkcs12 (p12, pfx, or pkcs12 file extensions), PEM files, or
containing those files that will be used as a system truststore. directories containing those files that will be used as a system truststore.
Bootstrap Admin: Bootstrap Admin:

View file

@ -19,6 +19,7 @@ package org.keycloak.truststore;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.KeystoreUtil; import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.common.util.KeystoreUtil.KeystoreFormat;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -101,15 +102,14 @@ public class TruststoreBuilder {
if (f.isDirectory()) { if (f.isDirectory()) {
mergeFiles(Stream.of(f.listFiles()).map(File::getAbsolutePath).toArray(String[]::new), truststore, false, discoveredFiles); mergeFiles(Stream.of(f.listFiles()).map(File::getAbsolutePath).toArray(String[]::new), truststore, false, discoveredFiles);
} else { } else {
if (file.endsWith(".p12") || file.endsWith(".pfx")) { var format = KeystoreUtil.getKeystoreFormat(file).orElse(null);
if (format == KeystoreFormat.PKCS12) {
mergeTrustStore(truststore, file, loadStore(file, PKCS12, null)); mergeTrustStore(truststore, file, loadStore(file, PKCS12, null));
if (!topLevel) { if (!topLevel) {
discoveredFiles.add(f.getAbsolutePath()); discoveredFiles.add(f.getAbsolutePath());
} }
} else { } else if (mergePemFile(truststore, file, topLevel) && !topLevel) {
if (mergePemFile(truststore, file, topLevel) && !topLevel) { discoveredFiles.add(f.getAbsolutePath());
discoveredFiles.add(f.getAbsolutePath());
}
} }
} }
} }