Após habilitarmos e configurarmos o TLS no Wildfly, qualquer endpoint que publicarmos nesse servidor poderá ser acessado via HTTPS. Para tanto, a aplicação cliente precisa baixar o certificado do servidor, o qual está disponível em https://localhost:8443/.
Testando o acesso
Com o TLS configurado, vamos publicar um endpoint. Em seguida criar um cliente que tenta acessar esse endpoint com e sem o certificado.
O endpoint GET terá a seguinte interface: /service/{name} e retorna um Hello name!!!
@Path("/service")
@Produces(TEXT_PLAIN)
public interface Services {
@GET
@Path("/{name}")
public String getMessage(@PathParam("name") String name);
Após fazer o deploy deste serviço no Wildfly, realizamos o teste. Com JUnit 5, criamos uma classe chamada TlsTest que envia uma requisição ao ao endpoint. Nesse teste, para fazer a requisição, usamos a recente API HttpClient nativa do Java introduzida na versão 11:
public class TlsTest {
@Test
public void shouldSuccessOverTLS() throws IOException, InterruptedException {
HttpClient httpClient = HttpClient.newBuilder().version(HTTP_1_1).build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8443/hello-tls/api/service/Rafael"))
.header("Content-Type", "application/json")
.GET()
.build();
HttpResponse<String> resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(resp.statusCode(), HTTP_OK);
}
Como ainda não adicionamos o certificado do servidor ao TrustStore do cliente, recebemos o seguinte erro ao rodar o teste:
java.io.IOException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at java.net.http/jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:565)
at java.net.http/jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:119)
at tls.TlsTest.shouldSuccessOverTLS(TlsTest.java:38)
...
Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
SSLHandshakeException indica que o cliente e o servidor não chegaram a um acordo sobre o nível de segurança desejado, e portanto a conexão será abandonada. Neste caso específico, a JVM não encontrou no TrustStore default ($JAVA_HOME/lib/security/cacerts) o certificado apresentado pelo host localhost:8443.
Então temos que baixar o certificado do servidor e adicioná-lo ao TrustStore do cliente. Baixe o certificado conforme descrito na parte 1.
Vamos criar uma classe chamada CertificateLoader que carrega uma nova TrustStore para o cliente e nela adiciona o certficado de localhost. Criamos o metodo loadCertificate() passando como parâmetros o caminho do certificado e o caminho desejado do novo TrsutStore que, caso não exista, será criado:
public class CertificateLoader {
static void loadCertificate(Path serverCertificatePath, Path clientTrustStorePath) throws Exception {
if (!Files.exists(clientTrustStorePath))
createClientTrustStore(clientTrustStorePath);
String alias = "wildfly23.localhost";
String password = "changeit";
// to load a new truststore other than default cacerts
KeyStore clientTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream in = Files.newInputStream(clientTrustStorePath);
clientTrustStore.load(in, password.toCharArray());
in.close();
// CertficateFactory to create a new reference to the server certificate file
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// read the server certificate
InputStream serverCertstream = Files.newInputStream(serverCertificatePath);
// certificate instance
Certificate serverCertificate = cf.generateCertificate(serverCertstream);
// add the server certificate to our newly truststore
clientTrustStore.setCertificateEntry(alias, serverCertificate);
// save modifications
OutputStream out = Files.newOutputStream(clientTrustStorePath);
clientTrustStore.store(out, password.toCharArray());
out.close();
// dynamically set default truststore for this application from cacerts to newly client.truststore
System.setProperty("javax.net.ssl.trustStore", clientTrustStorePath.toString());
System.setProperty("javax.net.ssl.trustStorePassword", password);
}
E modificamos a nossa classe de teste para carregar o certificado antes da execução do teste:
public class TlsTest {
private static final Path CLIENT_TRUST_STORE = Paths.get("/home/rafael/Library/Practice/_02_httpsLocalHost/client.truststore");
private static final Path LOCALHOST_CERTIFICATE = Paths.get("/home/rafael/Library/Practice/_02_httpsLocalHost/localhost");
@BeforeAll
public static void init() throws Exception {
CertificateLoader.loadCertificate(LOCALHOST_CERTIFICATE, CLIENT_TRUST_STORE);
}
@Test
public void shouldSuccessOverTLS() throws IOException, InterruptedException {
HttpClient httpClient = HttpClient.newBuilder().version(HTTP_1_1).build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8443/hello-tls/api/service/Rafael"))
.header("Content-Type", "application/json")
.GET()
.build();
HttpResponse<String> resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(resp.statusCode(), HTTP_OK);
}
}
Agora sim podemos rodar o teste, e a requisição será bem sucedida uma vez que o certificado do servidor foi adicionado ao TrustStore do cliente:
O código completo do cliente e do endpoint encontram-se no gitHub.