Skip to content

Commit

Permalink
Provide a way to fluently configure a client TLS key and certificates (
Browse files Browse the repository at this point in the history
…#4410)

Motivation:

Currently, a TLS key and certs can be set to a Netty's `SslContextBuilder` using
`ClientFactoryBuilder.tlsCustomize(Consumer)`. The configurations are not
exposed to the API os `ClientFactoryBuilder` and hidden inside Netty's
API. Many users failed to find the configuration API and we have received how to
configure mTLS.
https://line-armeria.slack.com/archives/C1NGPBUH2/p1658760377628999
https://line-armeria.slack.com/archives/C1NGPBUH2/p1662113976506629

Unlike `ClientFactoryBuilder`, `ServerBuilder` already supports APIs to
directly set key and certs without accessing `SslContextBuilder`. We may
provide the same APIs to `ClientFactoryBuilder` for better developer
experience.

Modifications:

- Extract TLS key configure APIs to `TlsSetters` so that
  `ClientFactoryBuilder`, `ServerBuilder` and `VirtualHostBuilder`
  provide the same APIs for TLS configuration to users.

Result:

You can now easily configure client-side a TLS key and certificates to support
mTLS using `ClientFactoryBuilder`.
```java
File keyCertChainFile = ...;
File keyFile = ...;
ClientFactory builder =
  ClientFactory
    .builder()
    .tls(keyCertChainFile, keyFile)
    .build();
WebClient
  .builder()
  .factory(factory)
  .build();
```
  • Loading branch information
ikhoon authored Oct 4, 2022
1 parent 91e571e commit ec7c425
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 211 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_INITIAL_WINDOW_SIZE;
import static java.util.Objects.requireNonNull;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -46,6 +53,7 @@
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.Ints;

import com.linecorp.armeria.client.proxy.ProxyConfig;
Expand All @@ -54,6 +62,7 @@
import com.linecorp.armeria.common.Flags;
import com.linecorp.armeria.common.Http1HeaderNaming;
import com.linecorp.armeria.common.Request;
import com.linecorp.armeria.common.TlsSetters;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.util.EventLoopGroups;
import com.linecorp.armeria.internal.common.RequestContextUtil;
Expand Down Expand Up @@ -86,7 +95,7 @@
* .build();
* }</pre>
*/
public final class ClientFactoryBuilder {
public final class ClientFactoryBuilder implements TlsSetters {

private static final ClientFactoryOptionValue<Long> ZERO_PING_INTERVAL =
ClientFactoryOptions.PING_INTERVAL_MILLIS.newValue(0L);
Expand Down Expand Up @@ -290,12 +299,121 @@ public ClientFactoryBuilder tlsNoVerifyHosts(String... insecureHosts) {
return this;
}

/**
* Configures SSL or TLS for client certificate authentication with the specified {@code keyCertChainFile}
* and cleartext {@code keyFile}.
*/
@Override
public ClientFactoryBuilder tls(File keyCertChainFile, File keyFile) {
return (ClientFactoryBuilder) TlsSetters.super.tls(keyCertChainFile, keyFile);
}

/**
* Configures SSL or TLS for client certificate authentication with the specified {@code keyCertChainFile},
* {@code keyFile} and {@code keyPassword}.
*/
@Override
public ClientFactoryBuilder tls(File keyCertChainFile, File keyFile, @Nullable String keyPassword) {
requireNonNull(keyCertChainFile, "keyCertChainFile");
requireNonNull(keyFile, "keyFile");
return tlsCustomizer(customizer -> customizer.keyManager(keyCertChainFile, keyFile, keyPassword));
}

/**
* Configures SSL or TLS for client certificate authentication with the specified
* {@code keyCertChainInputStream} and cleartext {@code keyInputStream}.
*/
@Override
public ClientFactoryBuilder tls(InputStream keyCertChainInputStream, InputStream keyInputStream) {
return (ClientFactoryBuilder) TlsSetters.super.tls(keyCertChainInputStream, keyInputStream);
}

/**
* Configures SSL or TLS for client certificate authentication with the specified
* {@code keyCertChainInputStream} and {@code keyInputStream} and {@code keyPassword}.
*/
@Override
public ClientFactoryBuilder tls(InputStream keyCertChainInputStream, InputStream keyInputStream,
@Nullable String keyPassword) {
requireNonNull(keyCertChainInputStream, "keyCertChainInputStream");
requireNonNull(keyInputStream, "keyInputStream");

// Retrieve the content of the given streams so that they can be consumed more than once.
final byte[] keyCertChain;
final byte[] key;
try {
keyCertChain = ByteStreams.toByteArray(keyCertChainInputStream);
key = ByteStreams.toByteArray(keyInputStream);
} catch (IOException e) {
throw new IOError(e);
}

return tlsCustomizer(customizer -> customizer.keyManager(new ByteArrayInputStream(keyCertChain),
new ByteArrayInputStream(key),
keyPassword));
}

/**
* Configures SSL or TLS for client certificate authentication with the specified cleartext
* {@link PrivateKey} and {@link X509Certificate} chain.
*/
@Override
public ClientFactoryBuilder tls(PrivateKey key, X509Certificate... keyCertChain) {
return (ClientFactoryBuilder) TlsSetters.super.tls(key, keyCertChain);
}

/**
* Configures SSL or TLS for client certificate authentication with the specified cleartext
* {@link PrivateKey} and {@link X509Certificate} chain.
*/
@Override
public ClientFactoryBuilder tls(PrivateKey key, Iterable<? extends X509Certificate> keyCertChain) {
return (ClientFactoryBuilder) TlsSetters.super.tls(key, keyCertChain);
}

/**
* Configures SSL or TLS for client certificate authentication with the specified {@link PrivateKey},
* {@code keyPassword} and {@link X509Certificate} chain.
*/
@Override
public ClientFactoryBuilder tls(PrivateKey key, @Nullable String keyPassword,
X509Certificate... keyCertChain) {
return (ClientFactoryBuilder) TlsSetters.super.tls(key, keyPassword, keyCertChain);
}

/**
* Configures SSL or TLS for client certificate authentication with the specified {@link PrivateKey},
* {@code keyPassword} and {@link X509Certificate} chain.
*/
@Override
public ClientFactoryBuilder tls(PrivateKey key, @Nullable String keyPassword,
Iterable<? extends X509Certificate> keyCertChain) {
requireNonNull(key, "key");
requireNonNull(keyCertChain, "keyCertChain");

for (X509Certificate keyCert : keyCertChain) {
requireNonNull(keyCert, "keyCertChain contains null.");
}

return tlsCustomizer(customizer -> customizer.keyManager(key, keyPassword, keyCertChain));
}

/**
* Configures SSL or TLS for client certificate authentication with the specified {@link KeyManagerFactory}.
*/
@Override
public ClientFactoryBuilder tls(KeyManagerFactory keyManagerFactory) {
requireNonNull(keyManagerFactory, "keyManagerFactory");
return tlsCustomizer(customizer -> customizer.keyManager(keyManagerFactory));
}

/**
* Adds the {@link Consumer} which can arbitrarily configure the {@link SslContextBuilder} that will be
* applied to the SSL session. For example, use {@link SslContextBuilder#trustManager(TrustManagerFactory)}
* to configure a custom server CA or {@link SslContextBuilder#keyManager(KeyManagerFactory)} to configure
* a client certificate for SSL authorization.
*/
@Override
public ClientFactoryBuilder tlsCustomizer(Consumer<? super SslContextBuilder> tlsCustomizer) {
requireNonNull(tlsCustomizer, "tlsCustomizer");
@SuppressWarnings("unchecked")
Expand Down
112 changes: 112 additions & 0 deletions core/src/main/java/com/linecorp/armeria/common/TlsSetters.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2022 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package com.linecorp.armeria.common;

import static java.util.Objects.requireNonNull;

import java.io.File;
import java.io.InputStream;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.function.Consumer;

import javax.net.ssl.KeyManagerFactory;

import com.google.common.collect.ImmutableList;

import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.annotation.UnstableApi;

import io.netty.handler.ssl.SslContextBuilder;

/**
* Sets properties for TLS or SSL.
*/
@UnstableApi
public interface TlsSetters {

/**
* Configures SSL or TLS with the specified {@code keyCertChainFile}
* and cleartext {@code keyFile}.
*/
default TlsSetters tls(File keyCertChainFile, File keyFile) {
return tls(keyCertChainFile, keyFile, null);
}

/**
* Configures SSL or TLS with the specified {@code keyCertChainFile},
* {@code keyFile} and {@code keyPassword}.
*/
TlsSetters tls(File keyCertChainFile, File keyFile, @Nullable String keyPassword);

/**
* Configures SSL or TLS with the specified {@code keyCertChainInputStream} and
* cleartext {@code keyInputStream}.
*/
default TlsSetters tls(InputStream keyCertChainInputStream, InputStream keyInputStream) {
return tls(keyCertChainInputStream, keyInputStream, null);
}

/**
* Configures SSL or TLS of this with the specified {@code keyCertChainInputStream},
* {@code keyInputStream} and {@code keyPassword}.
*/
TlsSetters tls(InputStream keyCertChainInputStream, InputStream keyInputStream,
@Nullable String keyPassword);

/**
* Configures SSL or TLS with the specified cleartext {@link PrivateKey} and
* {@link X509Certificate} chain.
*/
default TlsSetters tls(PrivateKey key, X509Certificate... keyCertChain) {
return tls(key, null, keyCertChain);
}

/**
* Configures SSL or TLS with the specified cleartext {@link PrivateKey} and
* {@link X509Certificate} chain.
*/
default TlsSetters tls(PrivateKey key, Iterable<? extends X509Certificate> keyCertChain) {
return tls(key, null, keyCertChain);
}

/**
* Configures SSL or TLS with the specified {@link PrivateKey}, {@code keyPassword} and
* {@link X509Certificate} chain.
*/
default TlsSetters tls(PrivateKey key, @Nullable String keyPassword, X509Certificate... keyCertChain) {
return tls(key, keyPassword, ImmutableList.copyOf(requireNonNull(keyCertChain, "keyCertChain")));
}

/**
* Configures SSL or TLS with the specified {@link PrivateKey}, {@code keyPassword} and
* {@link X509Certificate} chain.
*/
TlsSetters tls(PrivateKey key, @Nullable String keyPassword,
Iterable<? extends X509Certificate> keyCertChain);

/**
* Configures SSL or TLS with the specified {@link KeyManagerFactory}.
*/
TlsSetters tls(KeyManagerFactory keyManagerFactory);

/**
* Adds the {@link Consumer} which can arbitrarily configure the {@link SslContextBuilder} that will be
* applied to the SSL session.
*/
TlsSetters tlsCustomizer(Consumer<? super SslContextBuilder> tlsCustomizer);
}
Loading

0 comments on commit ec7c425

Please sign in to comment.