Saturday, April 17, 2021

Wildfly: Habilitando HTTPS para suas Aplicações 2

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.



No comments:

Post a Comment