diff --git a/ssl/config/src/main/java/io/deephaven/ssl/config/Trust.java b/ssl/config/src/main/java/io/deephaven/ssl/config/Trust.java index 6e48160cb15..f6f44dabb14 100644 --- a/ssl/config/src/main/java/io/deephaven/ssl/config/Trust.java +++ b/ssl/config/src/main/java/io/deephaven/ssl/config/Trust.java @@ -14,6 +14,7 @@ * @see TrustJdk * @see TrustProperties * @see TrustSystem + * @see TrustCustom * @see TrustAll * @see TrustList */ @@ -48,6 +49,8 @@ interface Visitor { T visit(TrustSystem system); + T visit(TrustCustom custom); + T visit(TrustAll all); T visit(TrustList list); diff --git a/ssl/config/src/main/java/io/deephaven/ssl/config/TrustCustom.java b/ssl/config/src/main/java/io/deephaven/ssl/config/TrustCustom.java new file mode 100644 index 00000000000..f8b023eb881 --- /dev/null +++ b/ssl/config/src/main/java/io/deephaven/ssl/config/TrustCustom.java @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2016-2023 Deephaven Data Labs and Patent Pending + */ +package io.deephaven.ssl.config; + +import io.deephaven.annotations.BuildableStyle; +import org.immutables.value.Value.Immutable; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.List; + +/** + * A custom trust configuration provided via {@link Certificate}. This is not currently deserializable via JSON. + */ +@Immutable +@BuildableStyle +public abstract class TrustCustom extends TrustBase { + private static final String X_509 = "X.509"; + + public static Builder builder() { + return ImmutableTrustCustom.builder(); + } + + /** + * Creates a trust from the given {@code factory} and {@code in}. Equivalent to + * {@code builder().addAllCertificates(factory.generateCertificates(in)).build()}. + * + * @param factory the certificate factory + * @param in the input stream + * @return the trust + * @throws IOException if an IO exception occurs + * @throws CertificateException on parsing errors + */ + public static TrustCustom of(CertificateFactory factory, InputStream in) throws IOException, CertificateException { + return builder().addAllCertificates(factory.generateCertificates(in)).build(); + } + + /** + * Creates a trust from the given {@code factory} and {@code path}. + * + * @param factory the certificate factory + * @param path the path + * @return the trust + * @throws IOException if an IO exception occurs + * @throws CertificateException on parsing errors + */ + public static TrustCustom of(CertificateFactory factory, Path path) throws IOException, CertificateException { + try (final InputStream in = new BufferedInputStream(Files.newInputStream(path))) { + return of(factory, in); + } + } + + /** + * Creates a trust from the given {@code factory} and {@code in}. Equivalent to + * {@code of(factory, new ByteArrayInputStream(in, offset, length))}. + * + * @param factory the certificate factory + * @param in the input bytes + * @param offset the input offset + * @param length the input length + * @return the trust + * @throws IOException if an IO exception occurs + * @throws CertificateException on parsing errors + * @see ByteArrayInputStream#ByteArrayInputStream(byte[], int, int) + * @see #of(CertificateFactory, InputStream). + */ + public static TrustCustom of(CertificateFactory factory, byte[] in, int offset, int length) + throws IOException, CertificateException { + return of(factory, new ByteArrayInputStream(in, offset, length)); + } + + /** + * Creates an X509 trust from the given {@code in}. Equivalent to + * {@code of(CertificateFactory.getInstance("X.509"), in)}. + * + * @param in the input stream + * @return the trust + * @throws IOException if an IO exception occurs + * @throws CertificateException on parsing errors + * @see CertificateFactory#getInstance(String) + * @see #of(CertificateFactory, InputStream) + */ + public static TrustCustom ofX509(InputStream in) throws CertificateException, IOException { + return of(CertificateFactory.getInstance(X_509), in); + } + + /** + * Creates an X509 trust from the given {@code path}. Equivalent to + * {@code of(CertificateFactory.getInstance("X.509"), path)}. + * + * @param path the path + * @return the trust + * @throws IOException if an IO exception occurs + * @throws CertificateException on parsing errors + * @see CertificateFactory#getInstance(String) + * @see #of(CertificateFactory, Path) + */ + public static TrustCustom ofX509(Path path) throws CertificateException, IOException { + return of(CertificateFactory.getInstance(X_509), path); + } + + /** + * Creates an X509 trust from the given {@code in}. Equivalent to + * {@code of(CertificateFactory.getInstance("X.509"), in, offset, length)}. + * + * @param in the input bytes + * @param offset the input offset + * @param length the input length + * @return the trust + * @throws IOException if an IO exception occurs + * @throws CertificateException on parsing errors + * @see CertificateFactory#getInstance(String) + * @see #of(CertificateFactory, byte[], int, int) + */ + public static TrustCustom ofX509(byte[] in, int offset, int length) throws CertificateException, IOException { + return of(CertificateFactory.getInstance(X_509), in, offset, length); + } + + // This is structurally more specific than what we could achieve with TrustList. There's potential in the future to + // extend this with Function if callers need more configurability. + + public abstract List certificates(); + + @Override + public final T walk(Visitor visitor) { + return visitor.visit(this); + } + + public interface Builder { + + Builder addCertificates(Certificate element); + + Builder addCertificates(Certificate... elements); + + Builder addAllCertificates(Iterable elements); + + TrustCustom build(); + } +} diff --git a/ssl/config/src/main/java/io/deephaven/ssl/config/TrustStore.java b/ssl/config/src/main/java/io/deephaven/ssl/config/TrustStore.java index 852796cb0d5..5aa7edcca00 100644 --- a/ssl/config/src/main/java/io/deephaven/ssl/config/TrustStore.java +++ b/ssl/config/src/main/java/io/deephaven/ssl/config/TrustStore.java @@ -24,7 +24,7 @@ public static TrustStore of(String path, String password) { public abstract String path(); /** - * The trust storce password. + * The trust store password. */ public abstract String password(); diff --git a/ssl/config/src/test/java/io/deephaven/ssl/config/SSLConfigTest.java b/ssl/config/src/test/java/io/deephaven/ssl/config/SSLConfigTest.java index 20806b75b99..a9156db1eb4 100644 --- a/ssl/config/src/test/java/io/deephaven/ssl/config/SSLConfigTest.java +++ b/ssl/config/src/test/java/io/deephaven/ssl/config/SSLConfigTest.java @@ -7,8 +7,14 @@ import io.deephaven.ssl.config.SSLConfig.ClientAuth; import org.junit.jupiter.api.Test; +import javax.security.auth.x500.X500Principal; +import java.io.BufferedInputStream; import java.io.IOException; +import java.io.InputStream; import java.net.URL; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Objects; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; @@ -87,6 +93,23 @@ void trustAll() throws IOException { check("trust-all.json", SSLConfig.builder().trust(TrustAll.of()).build()); } + @Test + void trustCustom() throws IOException, CertificateException { + // See server/dev-certs/README.md for information about server.chain.crt. + final TrustCustom custom; + try (final InputStream in = new BufferedInputStream( + Objects.requireNonNull(SSLConfigTest.class.getResourceAsStream("server.chain.crt")))) { + custom = TrustCustom.ofX509(in); + } + assertThat(custom.certificates()).hasSize(2); + assertThat(custom.certificates().get(0)).isInstanceOf(X509Certificate.class); + assertThat(((X509Certificate) custom.certificates().get(0)).getSubjectX500Principal()) + .isEqualTo(new X500Principal("CN=localhost")); + assertThat(custom.certificates().get(1)).isInstanceOf(X509Certificate.class); + assertThat(((X509Certificate) custom.certificates().get(1)).getSubjectX500Principal()) + .isEqualTo(new X500Principal("CN=deephaven-localhost-testing-ca")); + } + @Test void ciphersExplicit() throws IOException { check("ciphers-explicit.json", diff --git a/ssl/config/src/test/resources/io/deephaven/ssl/config/server.chain.crt b/ssl/config/src/test/resources/io/deephaven/ssl/config/server.chain.crt new file mode 120000 index 00000000000..f9f13def545 --- /dev/null +++ b/ssl/config/src/test/resources/io/deephaven/ssl/config/server.chain.crt @@ -0,0 +1 @@ +../../../../../../../../../server/dev-certs/server.chain.crt \ No newline at end of file diff --git a/ssl/kickstart/src/main/java/io/deephaven/ssl/config/impl/KickstartUtils.java b/ssl/kickstart/src/main/java/io/deephaven/ssl/config/impl/KickstartUtils.java index b8baa0373ca..3178fbff080 100644 --- a/ssl/kickstart/src/main/java/io/deephaven/ssl/config/impl/KickstartUtils.java +++ b/ssl/kickstart/src/main/java/io/deephaven/ssl/config/impl/KickstartUtils.java @@ -24,6 +24,7 @@ import io.deephaven.ssl.config.Trust; import io.deephaven.ssl.config.TrustAll; import io.deephaven.ssl.config.TrustCertificates; +import io.deephaven.ssl.config.TrustCustom; import io.deephaven.ssl.config.TrustJdk; import io.deephaven.ssl.config.TrustList; import io.deephaven.ssl.config.TrustProperties; @@ -90,6 +91,12 @@ public Void visit(TrustSystem system) { return null; } + @Override + public Void visit(TrustCustom custom) { + builder.withTrustMaterial(custom.certificates()); + return null; + } + @Override public Void visit(TrustAll all) { builder.withTrustingAllCertificatesWithoutValidation();