Support configuration of web SSL using PEM-encoded certificates
This commit adds the ability to configure SSL in embedded web containers using PEM-encoded certificate and private key files, as an alternative to configuring SSL with Java KeyStore files. Closes gh-29273
This commit is contained in:
parent
f032690d0a
commit
d387b3fa81
|
@ -243,6 +243,22 @@
|
||||||
"name": "server.ssl.trust-store-type",
|
"name": "server.ssl.trust-store-type",
|
||||||
"description": "Type of the trust store."
|
"description": "Type of the trust store."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "server.ssl.certificate",
|
||||||
|
"description": "Path to a PEM-encoded SSL certificate file."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "server.ssl.certificate-private-key",
|
||||||
|
"description": "Path to a PEM-encoded private key file for the SSL certificate."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "server.ssl.trust-certificate",
|
||||||
|
"description": "Path to a PEM-encoded SSL certificate authority file."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "server.ssl.trust-certificate-private-key",
|
||||||
|
"description": "Path to a PEM-encoded private key file for the SSL certificate authority."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "server.tomcat.max-http-post-size",
|
"name": "server.tomcat.max-http-post-size",
|
||||||
"type": "org.springframework.util.unit.DataSize",
|
"type": "org.springframework.util.unit.DataSize",
|
||||||
|
|
|
@ -183,7 +183,7 @@ You can configure this behavior by setting the configprop:server.compression.mim
|
||||||
[[howto.webserver.configure-ssl]]
|
[[howto.webserver.configure-ssl]]
|
||||||
=== Configure SSL
|
=== Configure SSL
|
||||||
SSL can be configured declaratively by setting the various `+server.ssl.*+` properties, typically in `application.properties` or `application.yml`.
|
SSL can be configured declaratively by setting the various `+server.ssl.*+` properties, typically in `application.properties` or `application.yml`.
|
||||||
The following example shows setting SSL properties in `application.properties`:
|
The following example shows setting SSL properties using a Java KeyStore file:
|
||||||
|
|
||||||
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
|
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
|
||||||
----
|
----
|
||||||
|
@ -195,6 +195,19 @@ The following example shows setting SSL properties in `application.properties`:
|
||||||
key-password: "another-secret"
|
key-password: "another-secret"
|
||||||
----
|
----
|
||||||
|
|
||||||
|
The following example shows setting SSL properties using PEM-encoded certificate and private key files:
|
||||||
|
|
||||||
|
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
|
||||||
|
----
|
||||||
|
server:
|
||||||
|
port: 8443
|
||||||
|
ssl:
|
||||||
|
certificate: "classpath:my-cert.crt"
|
||||||
|
certificate-private-key: "classpath:my-cert.key"
|
||||||
|
trust-certificate: "classpath:ca-cert.crt"
|
||||||
|
key-store-password: "secret"
|
||||||
|
----
|
||||||
|
|
||||||
See {spring-boot-module-code}/web/server/Ssl.java[`Ssl`] for details of all of the supported properties.
|
See {spring-boot-module-code}/web/server/Ssl.java[`Ssl`] for details of all of the supported properties.
|
||||||
|
|
||||||
Using configuration such as the preceding example means the application no longer supports a plain HTTP connector at port 8080.
|
Using configuration such as the preceding example means the application no longer supports a plain HTTP connector at port 8080.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2021 the original author or authors.
|
* Copyright 2012-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -39,6 +39,7 @@ import org.springframework.boot.rsocket.server.ConfigurableRSocketServerFactory;
|
||||||
import org.springframework.boot.rsocket.server.RSocketServer;
|
import org.springframework.boot.rsocket.server.RSocketServer;
|
||||||
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
|
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
|
||||||
import org.springframework.boot.rsocket.server.RSocketServerFactory;
|
import org.springframework.boot.rsocket.server.RSocketServerFactory;
|
||||||
|
import org.springframework.boot.web.server.CertificateFileSslStoreProvider;
|
||||||
import org.springframework.boot.web.server.Ssl;
|
import org.springframework.boot.web.server.Ssl;
|
||||||
import org.springframework.boot.web.server.SslStoreProvider;
|
import org.springframework.boot.web.server.SslStoreProvider;
|
||||||
import org.springframework.http.client.reactive.ReactorResourceFactory;
|
import org.springframework.http.client.reactive.ReactorResourceFactory;
|
||||||
|
@ -51,6 +52,7 @@ import org.springframework.util.unit.DataSize;
|
||||||
*
|
*
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Chris Bono
|
* @author Chris Bono
|
||||||
|
* @author Scott Frederick
|
||||||
* @since 2.2.0
|
* @since 2.2.0
|
||||||
*/
|
*/
|
||||||
public class NettyRSocketServerFactory implements RSocketServerFactory, ConfigurableRSocketServerFactory {
|
public class NettyRSocketServerFactory implements RSocketServerFactory, ConfigurableRSocketServerFactory {
|
||||||
|
@ -179,7 +181,7 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
private HttpServer customizeSslConfiguration(HttpServer httpServer) {
|
private HttpServer customizeSslConfiguration(HttpServer httpServer) {
|
||||||
org.springframework.boot.web.embedded.netty.SslServerCustomizer sslServerCustomizer = new org.springframework.boot.web.embedded.netty.SslServerCustomizer(
|
org.springframework.boot.web.embedded.netty.SslServerCustomizer sslServerCustomizer = new org.springframework.boot.web.embedded.netty.SslServerCustomizer(
|
||||||
this.ssl, null, this.sslStoreProvider);
|
this.ssl, null, getOrCreateSslStoreProvider());
|
||||||
return sslServerCustomizer.apply(httpServer);
|
return sslServerCustomizer.apply(httpServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,12 +191,20 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur
|
||||||
tcpServer = tcpServer.runOn(this.resourceFactory.getLoopResources());
|
tcpServer = tcpServer.runOn(this.resourceFactory.getLoopResources());
|
||||||
}
|
}
|
||||||
if (this.ssl != null && this.ssl.isEnabled()) {
|
if (this.ssl != null && this.ssl.isEnabled()) {
|
||||||
TcpSslServerCustomizer sslServerCustomizer = new TcpSslServerCustomizer(this.ssl, this.sslStoreProvider);
|
TcpSslServerCustomizer sslServerCustomizer = new TcpSslServerCustomizer(this.ssl,
|
||||||
|
getOrCreateSslStoreProvider());
|
||||||
tcpServer = sslServerCustomizer.apply(tcpServer);
|
tcpServer = sslServerCustomizer.apply(tcpServer);
|
||||||
}
|
}
|
||||||
return TcpServerTransport.create(tcpServer.bindAddress(this::getListenAddress));
|
return TcpServerTransport.create(tcpServer.bindAddress(this::getListenAddress));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SslStoreProvider getOrCreateSslStoreProvider() {
|
||||||
|
if (this.sslStoreProvider != null) {
|
||||||
|
return this.sslStoreProvider;
|
||||||
|
}
|
||||||
|
return CertificateFileSslStoreProvider.from(this.ssl);
|
||||||
|
}
|
||||||
|
|
||||||
private InetSocketAddress getListenAddress() {
|
private InetSocketAddress getListenAddress() {
|
||||||
if (this.address != null) {
|
if (this.address != null) {
|
||||||
return new InetSocketAddress(this.address.getHostAddress(), this.port);
|
return new InetSocketAddress(this.address.getHostAddress(), this.port);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2021 the original author or authors.
|
* Copyright 2012-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -236,7 +236,7 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
|
||||||
}
|
}
|
||||||
|
|
||||||
private void customizeSsl(Server server, InetSocketAddress address) {
|
private void customizeSsl(Server server, InetSocketAddress address) {
|
||||||
new SslServerCustomizer(address, getSsl(), getSslStoreProvider(), getHttp2()).customize(server);
|
new SslServerCustomizer(address, getSsl(), getOrCreateSslStoreProvider(), getHttp2()).customize(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -222,7 +222,7 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor
|
||||||
}
|
}
|
||||||
|
|
||||||
private void customizeSsl(Server server, InetSocketAddress address) {
|
private void customizeSsl(Server server, InetSocketAddress address) {
|
||||||
new SslServerCustomizer(address, getSsl(), getSslStoreProvider(), getHttp2()).customize(server);
|
new SslServerCustomizer(address, getSsl(), getOrCreateSslStoreProvider(), getHttp2()).customize(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2021 the original author or authors.
|
* Copyright 2012-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -179,7 +179,8 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
private HttpServer customizeSslConfiguration(HttpServer httpServer) {
|
private HttpServer customizeSslConfiguration(HttpServer httpServer) {
|
||||||
SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(getSsl(), getHttp2(), getSslStoreProvider());
|
SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(getSsl(), getHttp2(),
|
||||||
|
getOrCreateSslStoreProvider());
|
||||||
return sslServerCustomizer.apply(httpServer);
|
return sslServerCustomizer.apply(httpServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -221,7 +221,7 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac
|
||||||
}
|
}
|
||||||
|
|
||||||
private void customizeSsl(Connector connector) {
|
private void customizeSsl(Connector connector) {
|
||||||
new SslConnectorCustomizer(getSsl(), getSslStoreProvider()).customize(connector);
|
new SslConnectorCustomizer(getSsl(), getOrCreateSslStoreProvider()).customize(connector);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -361,7 +361,7 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto
|
||||||
}
|
}
|
||||||
|
|
||||||
private void customizeSsl(Connector connector) {
|
private void customizeSsl(Connector connector) {
|
||||||
new SslConnectorCustomizer(getSsl(), getSslStoreProvider()).customize(connector);
|
new SslConnectorCustomizer(getSsl(), getOrCreateSslStoreProvider()).customize(connector);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2021 the original author or authors.
|
* Copyright 2012-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -163,7 +163,8 @@ class UndertowWebServerFactoryDelegate {
|
||||||
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, http2.isEnabled());
|
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, http2.isEnabled());
|
||||||
}
|
}
|
||||||
if (ssl != null && ssl.isEnabled()) {
|
if (ssl != null && ssl.isEnabled()) {
|
||||||
new SslBuilderCustomizer(factory.getPort(), address, ssl, factory.getSslStoreProvider()).customize(builder);
|
new SslBuilderCustomizer(factory.getPort(), address, ssl, factory.getOrCreateSslStoreProvider())
|
||||||
|
.customize(builder);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
builder.addHttpListener(port, (address != null) ? address.getHostAddress() : "0.0.0.0");
|
builder.addHttpListener(port, (address != null) ? address.getHostAddress() : "0.0.0.0");
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2020 the original author or authors.
|
* Copyright 2012-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -36,6 +36,7 @@ import org.springframework.util.Assert;
|
||||||
* @author Ivan Sopov
|
* @author Ivan Sopov
|
||||||
* @author Eddú Meléndez
|
* @author Eddú Meléndez
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
|
* @author Scott Frederick
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractConfigurableWebServerFactory implements ConfigurableWebServerFactory {
|
public abstract class AbstractConfigurableWebServerFactory implements ConfigurableWebServerFactory {
|
||||||
|
@ -179,6 +180,18 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab
|
||||||
return this.shutdown;
|
return this.shutdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the provided {@link SslStoreProvider} or create one using {@link Ssl}
|
||||||
|
* properties.
|
||||||
|
* @return the {@code SslStoreProvider}
|
||||||
|
*/
|
||||||
|
public final SslStoreProvider getOrCreateSslStoreProvider() {
|
||||||
|
if (this.sslStoreProvider != null) {
|
||||||
|
return this.sslStoreProvider;
|
||||||
|
}
|
||||||
|
return CertificateFileSslStoreProvider.from(this.ssl);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the absolute temp dir for given web server.
|
* Return the absolute temp dir for given web server.
|
||||||
* @param prefix server name
|
* @param prefix server name
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed 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 org.springframework.boot.web.server;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link SslStoreProvider} that creates key and trust stores from certificate and
|
||||||
|
* private key PEM files.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
* @since 2.7.0
|
||||||
|
*/
|
||||||
|
public final class CertificateFileSslStoreProvider implements SslStoreProvider {
|
||||||
|
|
||||||
|
private static final char[] NO_PASSWORD = {};
|
||||||
|
|
||||||
|
private static final String DEFAULT_KEY_ALIAS = "spring-boot-web";
|
||||||
|
|
||||||
|
private final Ssl ssl;
|
||||||
|
|
||||||
|
private CertificateFileSslStoreProvider(Ssl ssl) {
|
||||||
|
this.ssl = ssl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyStore getKeyStore() throws Exception {
|
||||||
|
return createKeyStore(this.ssl.getCertificate(), this.ssl.getCertificatePrivateKey(),
|
||||||
|
this.ssl.getKeyStorePassword(), this.ssl.getKeyStoreType(), this.ssl.getKeyAlias());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyStore getTrustStore() throws Exception {
|
||||||
|
if (this.ssl.getTrustCertificate() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return createKeyStore(this.ssl.getTrustCertificate(), this.ssl.getTrustCertificatePrivateKey(),
|
||||||
|
this.ssl.getTrustStorePassword(), this.ssl.getTrustStoreType(), this.ssl.getKeyAlias());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link KeyStore} populated with the certificate stored at the
|
||||||
|
* specified file path and an optional private key.
|
||||||
|
* @param certPath the path to the certificate authority file
|
||||||
|
* @param keyPath the path to the private file
|
||||||
|
* @param password the key store password
|
||||||
|
* @param storeType the {@code KeyStore} type to create
|
||||||
|
* @param keyAlias the alias to use when adding keys to the {@code KeyStore}
|
||||||
|
* @return the {@code KeyStore}
|
||||||
|
*/
|
||||||
|
private KeyStore createKeyStore(String certPath, String keyPath, String password, String storeType,
|
||||||
|
String keyAlias) {
|
||||||
|
try {
|
||||||
|
KeyStore keyStore = KeyStore.getInstance((storeType != null) ? storeType : KeyStore.getDefaultType());
|
||||||
|
keyStore.load(null);
|
||||||
|
X509Certificate[] certificates = CertificateParser.parse(certPath);
|
||||||
|
PrivateKey privateKey = (keyPath != null) ? PrivateKeyParser.parse(keyPath) : null;
|
||||||
|
try {
|
||||||
|
addCertificates(keyStore, certificates, privateKey, password, keyAlias);
|
||||||
|
}
|
||||||
|
catch (KeyStoreException ex) {
|
||||||
|
throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
return keyStore;
|
||||||
|
}
|
||||||
|
catch (GeneralSecurityException | IOException ex) {
|
||||||
|
throw new IllegalStateException("Error creating KeyStore: " + ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey,
|
||||||
|
String password, String keyAlias) throws KeyStoreException {
|
||||||
|
String alias = (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS;
|
||||||
|
if (privateKey != null) {
|
||||||
|
keyStore.setKeyEntry(alias, privateKey, ((password != null) ? password.toCharArray() : NO_PASSWORD),
|
||||||
|
certificates);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (int index = 0; index < certificates.length; index++) {
|
||||||
|
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@link SslStoreProvider} if the appropriate SSL properties are configured.
|
||||||
|
* @param ssl the SSL properties
|
||||||
|
* @return a {@code SslStoreProvider} or {@code null}
|
||||||
|
*/
|
||||||
|
public static SslStoreProvider from(Ssl ssl) {
|
||||||
|
if (ssl != null && ssl.isEnabled()) {
|
||||||
|
if (ssl.getCertificate() != null && ssl.getCertificatePrivateKey() != null) {
|
||||||
|
return new CertificateFileSslStoreProvider(ssl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed 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 org.springframework.boot.web.server;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.springframework.util.Base64Utils;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
import org.springframework.util.ResourceUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for X.509 certificates in PEM format.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
final class CertificateParser {
|
||||||
|
|
||||||
|
private static final String HEADER = "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+";
|
||||||
|
|
||||||
|
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)";
|
||||||
|
|
||||||
|
private static final String FOOTER = "-+END\\s+.*CERTIFICATE[^-]*-+";
|
||||||
|
|
||||||
|
private static final Pattern PATTERN = Pattern.compile(HEADER + BASE64_TEXT + FOOTER, Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private CertificateParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load certificates from the specified resource.
|
||||||
|
* @param path the certificate to parse
|
||||||
|
* @return the parsed certificates
|
||||||
|
*/
|
||||||
|
static X509Certificate[] parse(String path) {
|
||||||
|
CertificateFactory factory = getCertificateFactory();
|
||||||
|
List<X509Certificate> certificates = new ArrayList<>();
|
||||||
|
readCertificates(path, factory, certificates::add);
|
||||||
|
return certificates.toArray(new X509Certificate[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CertificateFactory getCertificateFactory() {
|
||||||
|
try {
|
||||||
|
return CertificateFactory.getInstance("X.509");
|
||||||
|
}
|
||||||
|
catch (CertificateException ex) {
|
||||||
|
throw new IllegalStateException("Unable to get X.509 certificate factory", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readCertificates(String resource, CertificateFactory factory,
|
||||||
|
Consumer<X509Certificate> consumer) {
|
||||||
|
try {
|
||||||
|
String text = readText(resource);
|
||||||
|
Matcher matcher = PATTERN.matcher(text);
|
||||||
|
while (matcher.find()) {
|
||||||
|
String encodedText = matcher.group(1);
|
||||||
|
byte[] decodedBytes = decodeBase64(encodedText);
|
||||||
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedBytes);
|
||||||
|
while (inputStream.available() > 0) {
|
||||||
|
consumer.accept((X509Certificate) factory.generateCertificate(inputStream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (CertificateException | IOException ex) {
|
||||||
|
throw new IllegalStateException("Error reading certificate from '" + resource + "' : " + ex.getMessage(),
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readText(String resource) throws IOException {
|
||||||
|
URL url = ResourceUtils.getURL(resource);
|
||||||
|
try (Reader reader = new InputStreamReader(url.openStream())) {
|
||||||
|
return FileCopyUtils.copyToString(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] decodeBase64(String content) {
|
||||||
|
byte[] bytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
|
||||||
|
return Base64Utils.decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed 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 org.springframework.boot.web.server;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.springframework.util.Base64Utils;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
import org.springframework.util.ResourceUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for PKCS private key files in PEM format.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
final class PrivateKeyParser {
|
||||||
|
|
||||||
|
private static final String PKCS1_HEADER = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
|
||||||
|
|
||||||
|
private static final String PKCS1_FOOTER = "-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+";
|
||||||
|
|
||||||
|
private static final String PKCS8_FOOTER = "-+END\\s+PRIVATE\\s+KEY[^-]*-+";
|
||||||
|
|
||||||
|
private static final String PKCS8_HEADER = "-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
|
||||||
|
|
||||||
|
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)";
|
||||||
|
|
||||||
|
private static final Pattern PKCS1_PATTERN = Pattern.compile(PKCS1_HEADER + BASE64_TEXT + PKCS1_FOOTER,
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final Pattern PKCS8_KEY_PATTERN = Pattern.compile(PKCS8_HEADER + BASE64_TEXT + PKCS8_FOOTER,
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private PrivateKeyParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a private key from the specified resource.
|
||||||
|
* @param resource the private key to parse
|
||||||
|
* @return the parsed private key
|
||||||
|
*/
|
||||||
|
static PrivateKey parse(String resource) {
|
||||||
|
try {
|
||||||
|
String text = readText(resource);
|
||||||
|
Matcher matcher = PKCS1_PATTERN.matcher(text);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return parsePkcs1(decodeBase64(matcher.group(1)));
|
||||||
|
}
|
||||||
|
matcher = PKCS8_KEY_PATTERN.matcher(text);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return parsePkcs8(decodeBase64(matcher.group(1)));
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Unrecognized private key format in " + resource);
|
||||||
|
}
|
||||||
|
catch (GeneralSecurityException | IOException ex) {
|
||||||
|
throw new IllegalStateException("Error loading private key file " + resource, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PrivateKey parsePkcs1(byte[] privateKeyBytes) throws GeneralSecurityException {
|
||||||
|
byte[] pkcs8Bytes = convertPkcs1ToPkcs8(privateKeyBytes);
|
||||||
|
return parsePkcs8(pkcs8Bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] convertPkcs1ToPkcs8(byte[] pkcs1) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||||
|
int pkcs1Length = pkcs1.length;
|
||||||
|
int totalLength = pkcs1Length + 22;
|
||||||
|
// Sequence + total length
|
||||||
|
result.write(bytes(0x30, 0x82));
|
||||||
|
result.write((totalLength >> 8) & 0xff);
|
||||||
|
result.write(totalLength & 0xff);
|
||||||
|
// Integer (0)
|
||||||
|
result.write(bytes(0x02, 0x01, 0x00));
|
||||||
|
// Sequence: 1.2.840.113549.1.1.1, NULL
|
||||||
|
result.write(
|
||||||
|
bytes(0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00));
|
||||||
|
// Octet string + length
|
||||||
|
result.write(bytes(0x04, 0x82));
|
||||||
|
result.write((pkcs1Length >> 8) & 0xff);
|
||||||
|
result.write(pkcs1Length & 0xff);
|
||||||
|
// PKCS1
|
||||||
|
result.write(pkcs1);
|
||||||
|
return result.toByteArray();
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] bytes(int... elements) {
|
||||||
|
byte[] result = new byte[elements.length];
|
||||||
|
for (int i = 0; i < elements.length; i++) {
|
||||||
|
result[i] = (byte) elements[i];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PrivateKey parsePkcs8(byte[] privateKeyBytes) throws GeneralSecurityException {
|
||||||
|
try {
|
||||||
|
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||||
|
return keyFactory.generatePrivate(keySpec);
|
||||||
|
}
|
||||||
|
catch (InvalidKeySpecException ex) {
|
||||||
|
throw new IllegalArgumentException("Unexpected key format", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readText(String resource) throws IOException {
|
||||||
|
URL url = ResourceUtils.getURL(resource);
|
||||||
|
try (Reader reader = new InputStreamReader(url.openStream())) {
|
||||||
|
return FileCopyUtils.copyToString(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] decodeBase64(String content) {
|
||||||
|
byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
|
||||||
|
return Base64Utils.decode(contentBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2019 the original author or authors.
|
* Copyright 2012-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -22,6 +22,7 @@ package org.springframework.boot.web.server;
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
* @author Vladimir Tsanev
|
* @author Vladimir Tsanev
|
||||||
* @author Stephane Nicoll
|
* @author Stephane Nicoll
|
||||||
|
* @author Scott Frederick
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
public class Ssl {
|
public class Ssl {
|
||||||
|
@ -54,6 +55,14 @@ public class Ssl {
|
||||||
|
|
||||||
private String trustStoreProvider;
|
private String trustStoreProvider;
|
||||||
|
|
||||||
|
private String certificate;
|
||||||
|
|
||||||
|
private String certificatePrivateKey;
|
||||||
|
|
||||||
|
private String trustCertificate;
|
||||||
|
|
||||||
|
private String trustCertificatePrivateKey;
|
||||||
|
|
||||||
private String protocol = "TLS";
|
private String protocol = "TLS";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -226,6 +235,54 @@ public class Ssl {
|
||||||
this.trustStoreProvider = trustStoreProvider;
|
this.trustStoreProvider = trustStoreProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the location of the certificate in PEM format.
|
||||||
|
* @return the certificate location
|
||||||
|
*/
|
||||||
|
public String getCertificate() {
|
||||||
|
return this.certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCertificate(String certificate) {
|
||||||
|
this.certificate = certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the location of the private key for the certificate in PEM format.
|
||||||
|
* @return the location of the certificate private key
|
||||||
|
*/
|
||||||
|
public String getCertificatePrivateKey() {
|
||||||
|
return this.certificatePrivateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCertificatePrivateKey(String certificatePrivateKey) {
|
||||||
|
this.certificatePrivateKey = certificatePrivateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the location of the trust certificate authority chain in PEM format.
|
||||||
|
* @return the location of the trust certificate
|
||||||
|
*/
|
||||||
|
public String getTrustCertificate() {
|
||||||
|
return this.trustCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTrustCertificate(String trustCertificate) {
|
||||||
|
this.trustCertificate = trustCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the location of the private key for the trust certificate in PEM format.
|
||||||
|
* @return the location of the trust certificate private key
|
||||||
|
*/
|
||||||
|
public String getTrustCertificatePrivateKey() {
|
||||||
|
return this.trustCertificatePrivateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTrustCertificatePrivateKey(String trustCertificatePrivateKey) {
|
||||||
|
this.trustCertificatePrivateKey = trustCertificatePrivateKey;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the SSL protocol to use.
|
* Return the SSL protocol to use.
|
||||||
* @return the SSL protocol
|
* @return the SSL protocol
|
||||||
|
|
|
@ -64,6 +64,7 @@ import static org.mockito.Mockito.mock;
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
* @author Leo Li
|
* @author Leo Li
|
||||||
* @author Chris Bono
|
* @author Chris Bono
|
||||||
|
* @author Scott Frederick
|
||||||
*/
|
*/
|
||||||
class NettyRSocketServerFactoryTests {
|
class NettyRSocketServerFactoryTests {
|
||||||
|
|
||||||
|
@ -166,6 +167,30 @@ class NettyRSocketServerFactoryTests {
|
||||||
testBasicSslWithKeyStore("src/test/resources/test.jks", "password", Transport.WEBSOCKET);
|
testBasicSslWithKeyStore("src/test/resources/test.jks", "password", Transport.WEBSOCKET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tcpTransportBasicSslCertificateFromClassPath() {
|
||||||
|
testBasicSslWithPemCertificate("classpath:test-cert.pem", "classpath:test-key.pem", "classpath:test-cert.pem",
|
||||||
|
Transport.TCP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tcpTransportBasicSslCertificateFromFileSystem() {
|
||||||
|
testBasicSslWithPemCertificate("src/test/resources/test-cert.pem", "src/test/resources/test-key.pem",
|
||||||
|
"src/test/resources/test-cert.pem", Transport.TCP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void websocketTransportBasicSslCertificateFromClassPath() {
|
||||||
|
testBasicSslWithPemCertificate("classpath:test-cert.pem", "classpath:test-key.pem", "classpath:test-cert.pem",
|
||||||
|
Transport.WEBSOCKET);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void websocketTransportBasicSslCertificateFromFileSystem() {
|
||||||
|
testBasicSslWithPemCertificate("src/test/resources/test-cert.pem", "src/test/resources/test-key.pem",
|
||||||
|
"src/test/resources/test-cert.pem", Transport.WEBSOCKET);
|
||||||
|
}
|
||||||
|
|
||||||
private void checkEchoRequest() {
|
private void checkEchoRequest() {
|
||||||
String payload = "test payload";
|
String payload = "test payload";
|
||||||
Mono<String> response = this.requester.route("test").data(payload).retrieveMono(String.class);
|
Mono<String> response = this.requester.route("test").data(payload).retrieveMono(String.class);
|
||||||
|
@ -186,6 +211,23 @@ class NettyRSocketServerFactoryTests {
|
||||||
checkEchoRequest();
|
checkEchoRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void testBasicSslWithPemCertificate(String certificate, String certificatePrivateKey,
|
||||||
|
String trustCertificate, Transport transport) {
|
||||||
|
NettyRSocketServerFactory factory = getFactory();
|
||||||
|
factory.setTransport(transport);
|
||||||
|
Ssl ssl = new Ssl();
|
||||||
|
ssl.setCertificate(certificate);
|
||||||
|
ssl.setCertificatePrivateKey(certificatePrivateKey);
|
||||||
|
ssl.setTrustCertificate(trustCertificate);
|
||||||
|
ssl.setKeyStorePassword("");
|
||||||
|
factory.setSsl(ssl);
|
||||||
|
this.server = factory.create(new EchoRequestResponseAcceptor());
|
||||||
|
this.server.start();
|
||||||
|
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
|
||||||
|
: createSecureRSocketWebSocketClient();
|
||||||
|
checkEchoRequest();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void tcpTransportSslRejectsInsecureClient() {
|
void tcpTransportSslRejectsInsecureClient() {
|
||||||
NettyRSocketServerFactory factory = getFactory();
|
NettyRSocketServerFactory factory = getFactory();
|
||||||
|
|
|
@ -86,6 +86,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
* Base for testing classes that extends {@link AbstractReactiveWebServerFactory}.
|
* Base for testing classes that extends {@link AbstractReactiveWebServerFactory}.
|
||||||
*
|
*
|
||||||
* @author Brian Clozel
|
* @author Brian Clozel
|
||||||
|
* @author Scott Frederick
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractReactiveWebServerFactoryTests {
|
public abstract class AbstractReactiveWebServerFactoryTests {
|
||||||
|
|
||||||
|
@ -215,7 +216,7 @@ public abstract class AbstractReactiveWebServerFactoryTests {
|
||||||
ssl.setKeyPassword("password");
|
ssl.setKeyPassword("password");
|
||||||
ssl.setKeyStorePassword("secret");
|
ssl.setKeyStorePassword("secret");
|
||||||
ssl.setTrustStore("classpath:test.jks");
|
ssl.setTrustStore("classpath:test.jks");
|
||||||
testClientAuthSuccess(ssl, buildTrustAllSslWithClientKeyConnector());
|
testClientAuthSuccess(ssl, buildTrustAllSslWithClientKeyConnector("test.jks", "password"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -229,14 +230,15 @@ public abstract class AbstractReactiveWebServerFactoryTests {
|
||||||
testClientAuthSuccess(ssl, buildTrustAllSslConnector());
|
testClientAuthSuccess(ssl, buildTrustAllSslConnector());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ReactorClientHttpConnector buildTrustAllSslWithClientKeyConnector() throws Exception {
|
protected ReactorClientHttpConnector buildTrustAllSslWithClientKeyConnector(String keyStoreFile,
|
||||||
|
String keyStorePassword) throws Exception {
|
||||||
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||||
try (InputStream stream = new FileInputStream("src/test/resources/test.jks")) {
|
try (InputStream stream = new FileInputStream("src/test/resources/" + keyStoreFile)) {
|
||||||
clientKeyStore.load(stream, "secret".toCharArray());
|
clientKeyStore.load(stream, "secret".toCharArray());
|
||||||
}
|
}
|
||||||
KeyManagerFactory clientKeyManagerFactory = KeyManagerFactory
|
KeyManagerFactory clientKeyManagerFactory = KeyManagerFactory
|
||||||
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||||
clientKeyManagerFactory.init(clientKeyStore, "password".toCharArray());
|
clientKeyManagerFactory.init(clientKeyStore, keyStorePassword.toCharArray());
|
||||||
|
|
||||||
Http11SslContextSpec sslContextSpec = Http11SslContextSpec.forClient()
|
Http11SslContextSpec sslContextSpec = Http11SslContextSpec.forClient()
|
||||||
.configure((builder) -> builder.sslProvider(SslProvider.JDK)
|
.configure((builder) -> builder.sslProvider(SslProvider.JDK)
|
||||||
|
@ -265,7 +267,7 @@ public abstract class AbstractReactiveWebServerFactoryTests {
|
||||||
ssl.setKeyStorePassword("secret");
|
ssl.setKeyStorePassword("secret");
|
||||||
ssl.setKeyPassword("password");
|
ssl.setKeyPassword("password");
|
||||||
ssl.setTrustStore("classpath:test.jks");
|
ssl.setTrustStore("classpath:test.jks");
|
||||||
testClientAuthSuccess(ssl, buildTrustAllSslWithClientKeyConnector());
|
testClientAuthSuccess(ssl, buildTrustAllSslWithClientKeyConnector("test.jks", "password"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -279,6 +281,17 @@ public abstract class AbstractReactiveWebServerFactoryTests {
|
||||||
testClientAuthFailure(ssl, buildTrustAllSslConnector());
|
testClientAuthFailure(ssl, buildTrustAllSslConnector());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sslWithPemCertificates() throws Exception {
|
||||||
|
Ssl ssl = new Ssl();
|
||||||
|
ssl.setClientAuth(Ssl.ClientAuth.NEED);
|
||||||
|
ssl.setCertificate("classpath:test-cert.pem");
|
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||||
|
ssl.setTrustCertificate("classpath:test-cert.pem");
|
||||||
|
ssl.setKeyStorePassword("secret");
|
||||||
|
testClientAuthSuccess(ssl, buildTrustAllSslWithClientKeyConnector("test.p12", "secret"));
|
||||||
|
}
|
||||||
|
|
||||||
protected void testClientAuthFailure(Ssl sslConfiguration, ReactorClientHttpConnector clientConnector) {
|
protected void testClientAuthFailure(Ssl sslConfiguration, ReactorClientHttpConnector clientConnector) {
|
||||||
AbstractReactiveWebServerFactory factory = getFactory();
|
AbstractReactiveWebServerFactory factory = getFactory();
|
||||||
factory.setSsl(sslConfiguration);
|
factory.setSsl(sslConfiguration);
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed 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 org.springframework.boot.web.server;
|
||||||
|
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.UnrecoverableKeyException;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link CertificateFileSslStoreProvider}.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
*/
|
||||||
|
class CertificateFileSslStoreProviderTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromSslWhenNullReturnsNull() {
|
||||||
|
assertThat(CertificateFileSslStoreProvider.from(null)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromSslWhenDisabledReturnsNull() {
|
||||||
|
assertThat(CertificateFileSslStoreProvider.from(new Ssl())).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromSslWithCertAndKeyReturnsStoreProvider() throws Exception {
|
||||||
|
Ssl ssl = new Ssl();
|
||||||
|
ssl.setEnabled(true);
|
||||||
|
ssl.setCertificate("classpath:test-cert.pem");
|
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||||
|
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl);
|
||||||
|
assertThat(storeProvider).isNotNull();
|
||||||
|
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web");
|
||||||
|
assertThat(storeProvider.getTrustStore()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromSslWithCertAndKeyAndTrustCertReturnsStoreProvider() throws Exception {
|
||||||
|
Ssl ssl = new Ssl();
|
||||||
|
ssl.setEnabled(true);
|
||||||
|
ssl.setCertificate("classpath:test-cert.pem");
|
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||||
|
ssl.setTrustCertificate("classpath:test-cert.pem");
|
||||||
|
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl);
|
||||||
|
assertThat(storeProvider).isNotNull();
|
||||||
|
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web");
|
||||||
|
assertStoreContainsCert(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "spring-boot-web-0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromSslWithCertAndKeyAndTrustCertAndTrustKeyReturnsStoreProvider() throws Exception {
|
||||||
|
Ssl ssl = new Ssl();
|
||||||
|
ssl.setEnabled(true);
|
||||||
|
ssl.setCertificate("classpath:test-cert.pem");
|
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||||
|
ssl.setTrustCertificate("classpath:test-cert.pem");
|
||||||
|
ssl.setTrustCertificatePrivateKey("classpath:test-key.pem");
|
||||||
|
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl);
|
||||||
|
assertThat(storeProvider).isNotNull();
|
||||||
|
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "spring-boot-web");
|
||||||
|
assertStoreContainsCertAndKey(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "spring-boot-web");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromSslWithKeyAliasReturnsStoreProvider() throws Exception {
|
||||||
|
Ssl ssl = new Ssl();
|
||||||
|
ssl.setEnabled(true);
|
||||||
|
ssl.setKeyAlias("test-alias");
|
||||||
|
ssl.setCertificate("classpath:test-cert.pem");
|
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||||
|
ssl.setTrustCertificate("classpath:test-cert.pem");
|
||||||
|
ssl.setTrustCertificatePrivateKey("classpath:test-key.pem");
|
||||||
|
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl);
|
||||||
|
assertThat(storeProvider).isNotNull();
|
||||||
|
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), KeyStore.getDefaultType(), "test-alias");
|
||||||
|
assertStoreContainsCertAndKey(storeProvider.getTrustStore(), KeyStore.getDefaultType(), "test-alias");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fromSslWithStoreTypeReturnsStoreProvider() throws Exception {
|
||||||
|
Ssl ssl = new Ssl();
|
||||||
|
ssl.setEnabled(true);
|
||||||
|
ssl.setKeyStoreType("PKCS12");
|
||||||
|
ssl.setTrustStoreType("PKCS12");
|
||||||
|
ssl.setCertificate("classpath:test-cert.pem");
|
||||||
|
ssl.setCertificatePrivateKey("classpath:test-key.pem");
|
||||||
|
ssl.setTrustCertificate("classpath:test-cert.pem");
|
||||||
|
ssl.setTrustCertificatePrivateKey("classpath:test-key.pem");
|
||||||
|
SslStoreProvider storeProvider = CertificateFileSslStoreProvider.from(ssl);
|
||||||
|
assertThat(storeProvider).isNotNull();
|
||||||
|
assertStoreContainsCertAndKey(storeProvider.getKeyStore(), "PKCS12", "spring-boot-web");
|
||||||
|
assertStoreContainsCertAndKey(storeProvider.getTrustStore(), "PKCS12", "spring-boot-web");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertStoreContainsCertAndKey(KeyStore keyStore, String keyStoreType, String keyAlias)
|
||||||
|
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||||
|
assertThat(keyStore).isNotNull();
|
||||||
|
assertThat(keyStore.getType()).isEqualTo(keyStoreType);
|
||||||
|
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
|
||||||
|
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
|
||||||
|
assertThat(keyStore.getKey(keyAlias, new char[] {})).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertStoreContainsCert(KeyStore keyStore, String keyStoreType, String keyAlias)
|
||||||
|
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||||
|
assertThat(keyStore).isNotNull();
|
||||||
|
assertThat(keyStore.getType()).isEqualTo(keyStoreType);
|
||||||
|
assertThat(keyStore.containsAlias(keyAlias)).isTrue();
|
||||||
|
assertThat(keyStore.getCertificate(keyAlias)).isNotNull();
|
||||||
|
assertThat(keyStore.getKey(keyAlias, new char[] {})).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed 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 org.springframework.boot.web.server;
|
||||||
|
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link CertificateParser}.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
*/
|
||||||
|
class CertificateParserTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseCertificate() {
|
||||||
|
X509Certificate[] certificates = CertificateParser.parse("classpath:test-cert.pem");
|
||||||
|
assertThat(certificates).isNotNull();
|
||||||
|
assertThat(certificates.length).isEqualTo(1);
|
||||||
|
assertThat(certificates[0].getType()).isEqualTo("X.509");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseCertificateChain() {
|
||||||
|
X509Certificate[] certificates = CertificateParser.parse("classpath:test-cert-chain.pem");
|
||||||
|
assertThat(certificates).isNotNull();
|
||||||
|
assertThat(certificates.length).isEqualTo(2);
|
||||||
|
assertThat(certificates[0].getType()).isEqualTo("X.509");
|
||||||
|
assertThat(certificates[1].getType()).isEqualTo("X.509");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseWithInvalidPathWillThrowException() {
|
||||||
|
String path = "file:///bad/path/cert.pem";
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> CertificateParser.parse("file:///bad/path/cert.pem"))
|
||||||
|
.withMessageContaining(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed 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 org.springframework.boot.web.server;
|
||||||
|
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link PrivateKeyParser}.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
*/
|
||||||
|
class PrivateKeyParserTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsePkcs8KeyFile() {
|
||||||
|
PrivateKey privateKey = PrivateKeyParser.parse("classpath:test-key.pem");
|
||||||
|
assertThat(privateKey).isNotNull();
|
||||||
|
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseWithNonKeyFileWillThrowException() {
|
||||||
|
String path = "classpath:test-banner.txt";
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse("file://" + path))
|
||||||
|
.withMessageContaining(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parseWithInvalidPathWillThrowException() {
|
||||||
|
String path = "file:///bad/path/key.pem";
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path)).withMessageContaining(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -167,6 +167,7 @@ import static org.mockito.Mockito.mock;
|
||||||
* @author Greg Turnquist
|
* @author Greg Turnquist
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
* @author Raja Kolli
|
* @author Raja Kolli
|
||||||
|
* @author Scott Frederick
|
||||||
*/
|
*/
|
||||||
@ExtendWith(OutputCaptureExtension.class)
|
@ExtendWith(OutputCaptureExtension.class)
|
||||||
public abstract class AbstractServletWebServerFactoryTests {
|
public abstract class AbstractServletWebServerFactoryTests {
|
||||||
|
@ -559,6 +560,23 @@ public abstract class AbstractServletWebServerFactoryTests {
|
||||||
assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test");
|
assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pemKeyStoreAndTrustStore() throws Exception {
|
||||||
|
AbstractServletWebServerFactory factory = getFactory();
|
||||||
|
addTestTxtFile(factory);
|
||||||
|
factory.setSsl(getSsl("classpath:test-cert.pem", "classpath:test-key.pem"));
|
||||||
|
this.webServer = factory.getWebServer();
|
||||||
|
this.webServer.start();
|
||||||
|
KeyStore keyStore = KeyStore.getInstance("pkcs12");
|
||||||
|
loadStore(keyStore, new FileSystemResource("src/test/resources/test.p12"));
|
||||||
|
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
|
||||||
|
new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy())
|
||||||
|
.loadKeyMaterial(keyStore, "secret".toCharArray()).build());
|
||||||
|
HttpClient httpClient = this.httpClientBuilder.get().setSSLSocketFactory(socketFactory).build();
|
||||||
|
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||||
|
assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void sslNeedsClientAuthenticationSucceedsWithClientCertificate() throws Exception {
|
void sslNeedsClientAuthenticationSucceedsWithClientCertificate() throws Exception {
|
||||||
AbstractServletWebServerFactory factory = getFactory();
|
AbstractServletWebServerFactory factory = getFactory();
|
||||||
|
@ -710,6 +728,16 @@ public abstract class AbstractServletWebServerFactoryTests {
|
||||||
return ssl;
|
return ssl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Ssl getSsl(String cert, String privateKey) {
|
||||||
|
Ssl ssl = new Ssl();
|
||||||
|
ssl.setClientAuth(ClientAuth.NEED);
|
||||||
|
ssl.setCertificate(cert);
|
||||||
|
ssl.setCertificatePrivateKey(privateKey);
|
||||||
|
ssl.setTrustCertificate(cert);
|
||||||
|
ssl.setKeyStorePassword("secret");
|
||||||
|
return ssl;
|
||||||
|
}
|
||||||
|
|
||||||
protected void testRestrictedSSLProtocolsAndCipherSuites(String[] protocols, String[] ciphers) throws Exception {
|
protected void testRestrictedSSLProtocolsAndCipherSuites(String[] protocols, String[] ciphers) throws Exception {
|
||||||
AbstractServletWebServerFactory factory = getFactory();
|
AbstractServletWebServerFactory factory = getFactory();
|
||||||
factory.setSsl(getSsl(null, "password", "src/test/resources/restricted.jks", null, protocols, ciphers));
|
factory.setSsl(getSsl(null, "password", "src/test/resources/restricted.jks", null, protocols, ciphers));
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
-----BEGIN TRUSTED CERTIFICATE-----
|
||||||
|
MIIClzCCAgACCQCPbjkRoMVEQDANBgkqhkiG9w0BAQUFADCBjzELMAkGA1UEBhMC
|
||||||
|
VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28x
|
||||||
|
DTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxFDASBgNVBAMMC2V4YW1wbGUu
|
||||||
|
Y29tMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMB4XDTIwMDMyNzIx
|
||||||
|
NTgwNFoXDTIxMDMyNzIxNTgwNFowgY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD
|
||||||
|
YWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARUZXN0
|
||||||
|
MQ0wCwYDVQQLDARUZXN0MRQwEgYDVQQDDAtleGFtcGxlLmNvbTEfMB0GCSqGSIb3
|
||||||
|
DQEJARYQdGVzdEBleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC
|
||||||
|
gYEA1YzixWEoyzrd20C2R1gjyPCoPfFLlG6UYTyT0tueNy6yjv6qbJ8lcZg7616O
|
||||||
|
3I9LuOHhZh9U+fCDCgPfiDdyJfDEW/P+dsOMFyMUXPrJPze2yPpOnvV8iJ5DM93u
|
||||||
|
fEVhCCyzLdYu0P2P3hU2W+T3/Im9DA7FOPA2vF1SrIJ2qtUCAwEAATANBgkqhkiG
|
||||||
|
9w0BAQUFAAOBgQBdShkwUv78vkn1jAdtfbB+7mpV9tufVdo29j7pmotTCz3ny5fc
|
||||||
|
zLEfeu6JPugAR71JYbc2CqGrMneSk1zT91EH6ohIz8OR5VNvzB7N7q65Ci7OFMPl
|
||||||
|
ly6k3rHpMCBtHoyNFhNVfPLxGJ9VlWFKLgIAbCmL4OIQm1l6Fr1MSM38Zw==
|
||||||
|
-----END TRUSTED CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICjzCCAfgCAQEwDQYJKoZIhvcNAQEFBQAwgY8xCzAJBgNVBAYTAlVTMRMwEQYD
|
||||||
|
VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQK
|
||||||
|
DARUZXN0MQ0wCwYDVQQLDARUZXN0MRQwEgYDVQQDDAtleGFtcGxlLmNvbTEfMB0G
|
||||||
|
CSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTAeFw0yMDAzMjcyMjAxNDZaFw0y
|
||||||
|
MTAzMjcyMjAxNDZaMIGPMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5p
|
||||||
|
YTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwEVGVzdDENMAsGA1UE
|
||||||
|
CwwEVGVzdDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xHzAdBgkqhkiG9w0BCQEWEHRl
|
||||||
|
c3RAZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM7kd2cj
|
||||||
|
F49wm1+OQ7Q5GE96cXueWNPr/Nwei71tf6G4BmE0B+suXHEvnLpHTj9pdX/ZzBIK
|
||||||
|
8jIZ/x8RnSduK/Ky+zm1QMYUWZtWCAgCW8WzgB69Cn/hQG8KSX3S9bqODuQAvP54
|
||||||
|
GQJD7+4kVuNBGjFb4DaD4nvMmPtALSZf8ZCZAgMBAAEwDQYJKoZIhvcNAQEFBQAD
|
||||||
|
gYEAOn6X8+0VVlDjF+TvTgI0KIasA6nDm+KXe7LVtfvqWqQZH4qyd2uiwcDM3Aux
|
||||||
|
a/OsPdOw0j+NqFDBd3mSMhSVgfvXdK6j9WaxY1VGXyaidLARgvn63wfzgr857sQW
|
||||||
|
c8eSxbwEQxwlMvVxW6Os4VhCfUQr8VrBrvPa2zs+6IlK+Ug=
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,17 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICpDCCAYwCCQCDOqHKPjAhCTANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAls
|
||||||
|
b2NhbGhvc3QwHhcNMTQwOTEwMjE0MzA1WhcNMTQxMDEwMjE0MzA1WjAUMRIwEAYD
|
||||||
|
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDR
|
||||||
|
0KfxUw7MF/8RB5/YXOM7yLnoHYb/M/6dyoulMbtEdKKhQhU28o5FiDkHcEG9PJQL
|
||||||
|
gqrRgAjl3VmCC9omtfZJQ2EpfkTttkJjnKOOroXhYE51/CYSckapBYCVh8GkjUEJ
|
||||||
|
uEfnp07cTfYZFqViIgIWPZyjkzl3w4girS7kCuzNdDntVJVx5F/EsFwMA8n3C0Qa
|
||||||
|
zHQoM5s00Fer6aTwd6AW0JD5QkADavpfzZ554e4HrVGwHlM28WKQQkFzzGu44FFX
|
||||||
|
yVuEF3HeyVPug8GRHAc8UU7ijVgJB5TmbvRGYowIErD5i4VvGLuOv9mgR3aVyN0S
|
||||||
|
dJ1N7aJnXpeSQjAgf03jAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBAE4yvwhbPldg
|
||||||
|
Bpl7sBw/m2B3bfiNeSqa4tII1PQ7ysgWVb9HbFNKkriScwDWlqo6ljZfJ+SDFCoj
|
||||||
|
bQz4fOFdMAOzRnpTrG2NAKMoJLY0/g/p7XO00PiC8T3h3BOJ5SHuW3gUyfGXmAYs
|
||||||
|
DnJxJOrwPzj57xvNXjNSbDOJ3DRfCbB0CWBexOeGDiUokoEq3Gnz04Q4ZfHyAcpZ
|
||||||
|
3deMw8Od5p9WAoCh3oClpFyOSzXYKZd+3ppMMtfc4wnbfocnfSFxj0UCpOEJw4Ez
|
||||||
|
+lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO
|
||||||
|
32C9XWHwRA4=
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,28 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDR0KfxUw7MF/8R
|
||||||
|
B5/YXOM7yLnoHYb/M/6dyoulMbtEdKKhQhU28o5FiDkHcEG9PJQLgqrRgAjl3VmC
|
||||||
|
C9omtfZJQ2EpfkTttkJjnKOOroXhYE51/CYSckapBYCVh8GkjUEJuEfnp07cTfYZ
|
||||||
|
FqViIgIWPZyjkzl3w4girS7kCuzNdDntVJVx5F/EsFwMA8n3C0QazHQoM5s00Fer
|
||||||
|
6aTwd6AW0JD5QkADavpfzZ554e4HrVGwHlM28WKQQkFzzGu44FFXyVuEF3HeyVPu
|
||||||
|
g8GRHAc8UU7ijVgJB5TmbvRGYowIErD5i4VvGLuOv9mgR3aVyN0SdJ1N7aJnXpeS
|
||||||
|
QjAgf03jAgMBAAECggEBAIhQyzwj3WJGWOZkkLqOpufJotcmj/Wwf0VfOdkq9WMl
|
||||||
|
cB/bAlN/xWVxerPVgDCFch4EWBzi1WUaqbOvJZ2u7QNubmr56aiTmJCFTVI/GyZx
|
||||||
|
XqiTGN01N6lKtN7xo6LYTyAUhUsBTWAemrx0FSErvTVb9C/mUBj6hbEZ2XQ5kN5t
|
||||||
|
7qYX4Lu0zyn7s1kX5SLtm5I+YRq7HSwB6wLy+DSroO71izZ/VPwME3SwT5SN+c87
|
||||||
|
3dkklR7fumNd9dOpSWKrLPnq4aMko00rvIGc63xD1HrEpXUkB5v24YEn7HwCLEH7
|
||||||
|
b8jrp79j2nCvvR47inpf+BR8FIWAHEOUUqCEzjQkdiECgYEA6ifjMM0f02KPeIs7
|
||||||
|
zXd1lI7CUmJmzkcklCIpEbKWf/t/PHv3QgqIkJzERzRaJ8b+GhQ4zrSwAhrGUmI8
|
||||||
|
kDkXIqe2/2ONgIOX2UOHYHyTDQZHnlXyDecvHUTqs2JQZCGBZkXyZ9i0j3BnTymC
|
||||||
|
iZ8DvEa0nxsbP+U3rgzPQmXiQVMCgYEA5WN2Y/RndbriNsNrsHYRldbPO5nfV9rp
|
||||||
|
cDzcQU66HRdK5VIdbXT9tlMYCJIZsSqE0tkOwTgEB/sFvF/tIHSCY5iO6hpIyk6g
|
||||||
|
kkUzPcld4eM0dEPAge7SYUbakB9CMvA7MkDQSXQNFyZ0mH83+UikwT6uYHFh7+ox
|
||||||
|
N1P+psDhXzECgYEA1gXLVQnIcy/9LxMkgDMWV8j8uMyUZysDthpbK3/uq+A2dhRg
|
||||||
|
9g4msPd5OBQT65OpIjElk1n4HpRWfWqpLLHiAZ0GWPynk7W0D7P3gyuaRSdeQs0P
|
||||||
|
x8FtgPVDCN9t13gAjHiWjnC26Py2kNbCKAQeJ/MAmQTvrUFX2VCACJKTcV0CgYAj
|
||||||
|
xJWSUmrLfb+GQISLOG3Xim434e9keJsLyEGj4U29+YLRLTOvfJ2PD3fg5j8hU/rw
|
||||||
|
Ea5uTHi8cdTcIa0M8X3fX8txD3YoLYh2JlouGTcNYOst8d6TpBSj3HN6I5Wj8beZ
|
||||||
|
R2fy/CiKYpGtsbCdq0kdZNO18BgQW9kewncjs1GxEQKBgQCf8q34h6KuHpHSDh9h
|
||||||
|
YkDTypk0FReWBAVJCzDNDUMhVLFivjcwtaMd2LiC3FMKZYodr52iKg60cj43vbYI
|
||||||
|
frmFFxoL37rTmUocCTBKc0LhWj6MicI+rcvQYe1uwTrpWdFf1aZJMYRLRczeKtev
|
||||||
|
OWaE/9hVZ5+9pild1NukGpOydw==
|
||||||
|
-----END PRIVATE KEY-----
|
Loading…
Reference in New Issue