Testes automatizados em sistemas autenticados com certificados digitais, usando Selenium e PhantomJS

A automatização de testes é uma disciplina muito importante hoje em dia. Entre várias técnicas e ferramentas diferentes, uma das que podemos utilizar para termos um conjunto de testes funcionais que possam ser facilmente repetidos é o Selenium. O Selenium é uma ferramenta que permite que a execução de passos que uma pessoa faria em um browser web (Chrome, Edge, etc.) possa ser programada. Com isso, toda vez que for necessária a validação de uma nova versão de sistema ou correção, este grupo de testes pode ser rodado de forma automática.

Ainda falando sobre a execução do Selenium, ele trabalha criando uma instância do respectivo browser e enviando comandos para ele. Dessa forma é feita a simulação de preenchimento de caixas de texto, cliques em botões, etc. Quando um teste automatizado com Selenium é rodado, pode-se ver todo o teste acontecendo. Entretanto, há um problema nesta abordagem. Caso a sessão do usuário que está executando os testes seja bloqueada, o Selenium não consegue mais simular a interação com a interface de usuário do browser.

Para resolver esse tipo de situação, é possível utilizar um browser sem interface. A ideia é que este programa possa renderizar em memória a interface das páginas web sem efetivamente criar janelas com estes componentes. Assim, é possível executar o conjunto de testes sem uma sessão de usuário. É seguindo este conceito que o Selenium faz o uso do PhantomJS.

Toda essa introdução serviu para chegar em um problema que tive recentemente ao trabalhar desta forma. Alguns sistemas web são programados para considerar autenticação via certificado digital, seja um certificado instalado na máquina (por exemplo, A1) ou um smart card (A3). Quando um sistema com essa característica é acessado, o browser exibe uma janela pop-up para a seleção de qual certificado digital deve ser utilizado (e no caso dos smart cards, é solicitado também a digitação do PIN associado). Só assim o browser envia essas informações ao servidor para que se possa ser feita a autenticação.

Browser Chrome solicitando um certificado digital para acesso de sistema.


Só que as implementações dos drivers disponíveis para trabalhar com o Selenium e os principais browsers não permite o controle dessa caixa de seleção de certificados. Ou seja, o Selenium por si só não tem a capacidade de fazer com que o browser seja autenticado dessa forma. Com o driver do Internet Explorer até é possível fazer os passos de seleção de certificado utilizando uma outra ferramenta chamada AutoIT (que trabalha com automatização de interação em janelas nativas do Windows), mas com o Chrome isso não funciona.

Já o PhantomJS na versão 2.1 permite que passemos um certificado para autenticação. Para que isso possa ser feito, é necessário termos em mãos um arquivo de certificado digital de extensão PFX. Esse PFX deve ser um certificado válido para acesso ao sistema, ou seja, deve estar dentro da data de validade, possuir a cadeira de validação correta, entre outros requisitos. Não é simplesmente usar um certificado digital qualquer para acesso ao sistema, o sistema deve estar preparado para aceitar os certificados corretos.

Somente o arquivo PFX não é suficiente para trabalharmos. O PhantomJS exige que seja passado o certificado digital e a sua chave em arquivos separados. Para isso, precisamos utilizar uma ferramenta que faça a exportação desses dados em arquivos separados. Essa ferramenta é o OpenSSL e pode ser baixado em http://gnuwin32.sourceforge.net/packages/openssl.htm.

Tendo em posse essa ferramenta, o certificado digital e a senha deste certificado, é necessária a execução dos comandos abaixo para a extração e geração dos arquivos necessários:

fabiogouw@SHIVA D:\Tools\OpenSSL\bin
$ openssl pkcs12 -nokeys -in C:\certs\client-certificate.pfx -out c:\certs\client-certificate.crt
WARNING: can't open config file: C:/OpenSSL/openssl.cnf
Enter Import Password:
MAC verified OK

fabiogouw@SHIVA D:\Tools\OpenSSL\bin
$ openssl pkcs12 -nocerts -in C:\certs\client-certificate.pfx -out c:\certs\client-certificate.key
WARNING: can't open config file: C:/OpenSSL/openssl.cnf
Enter Import Password:
MAC verified OK
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:

fabiogouw@SHIVA D:\Tools\OpenSSL\bin
$


Veja que durante a execução a ferramenta pede a senha do certificado (nas linhas "Enter Import Password") e uma outra senha para o acesso da chave do certificado (na linha "Enter PEM pass phrase"). Como resultado final, teremos:
  • Um arquivo contendo o certificado digital, na extensão CRT.
  • Um arquivo contendo a chave do certificado digital, na extensão KEY.
  • Uma senha para a leitura da chave privada do arquivo de extensão KEY.
Com isso, podemos executar o teste abaixo no Visual Studio. Esse teste acessa um site hospedado no Azure que foi configurado para exigir autenticação via certificado digital (instruções de como fazer isso podem ser encontradas em https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth) que, depois de autenticado, exibe uma simples tela onde temos uma calculadora que soma dois números, via JavaScript.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.PhantomJS;
using System;

namespace ClientCertificate.UnitTest
{
    [TestClass]
    public class UnitTest1
    {
        private IWebDriver _driver;
        public TestContext TestContext { get; set; }

        [TestInitialize]
        public void Inicializar()
        {
            var service = PhantomJSDriverService.CreateDefaultService();
            service.IgnoreSslErrors = true;
            service.AddArgument($"--ssl-client-certificate-file=C:\\certs\\client-certificate.crt");
            service.AddArgument($"--ssl-client-key-file=C:\\certs\\client-certificate.key");
            service.AddArgument($"--ssl-client-key-passphrase=1234");
            _driver = new PhantomJSDriver(service);
            _driver.Manage().Timeouts().PageLoad = new TimeSpan(0, 0, 10);
            _driver.Manage().Timeouts().ImplicitWait = new TimeSpan(0, 0, 10);
            

        }

        [TestMethod]
        public void TestarFuncionalidadeComAutenticacaoViaCertificado()
        {
            _driver.Navigate().GoToUrl("https://clientcertificatewebapp331.azurewebsites.net/Home/Soma");
            try
            {
                _driver.FindElement(By.Id("valor1")).SendKeys("1");
                _driver.FindElement(By.Id("valor2")).SendKeys("2");
                _driver.FindElement(By.Id("btnSoma")).Click();
                Assert.AreEqual("3", _driver.FindElement(By.Id("resultado")).Text);
            }
            finally
            {
                TirarImagem();
            }
        }

        private void TirarImagem()
        {
            Screenshot ss = ((ITakesScreenshot)_driver).GetScreenshot();
            ss.SaveAsFile($"{TestContext.TestRunResultsDirectory}\\imagem.jpg", ScreenshotImageFormat.Png);
        }

        [TestCleanup]
        public void Finalizar()
        {
            if(_driver != null)
            {
                _driver.Dispose();
            }
        }
    }
}


Observe que na criação da instância do driver do PhantomJS os dois arquivos (CRT e KEY) e a senha são passados na criação do browser em memória, usando a classe PhantomJSDriverService. Sem esta linha, quando o teste executar e o browser tentar acessar o site, ele não conseguirá se autenticar e por consequência os próximos passos de seleção de campos não funcionarão, ocasionando na falha do teste. Notem que eu sempre tiro um print de tela no teste usando PhantomJS, para que seja possível identificar o problema de um teste ou mesmo apenas validar a sua execução. No caso de falha de autenticação (código sem a preparação que eu mostrei, ou com o certificado inválido), a imagem que apareceria é a abaixo.


Já quando tudo esta correto, o teste é executado sem falhas, exibindo como resultado final a soma da calculadora.

Até a próxima!

Comentários

  1. Me ajudou muito! Obrigado!

    ResponderExcluir
  2. Muito bom! e como funcionaria com os certificados do tipo a3?

    ResponderExcluir
    Respostas
    1. Hm, A3 não cheguei a explorar, como é pra rotinas de testes entendo que a geração de um certificado A1 já atenderia o propósito.

      Excluir
  3. Uma pergunta, realmente usando o ChromDriver não funciona os parâmetros --ssl-client-certificate-file e os outros???

    Eu na verdade não consegui.
    Não queria usar o PhantomJS, mesmo porque vou utilizar o Selenium Grid.

    ResponderExcluir
    Respostas
    1. Para o Chrome existe um esquema de especificar um certificado por domínio, na época eu não cheguei a testar, mas acho que vale a tentativa. O lado ruim é ter que ficar fazendo configurações no registro do Windows pra fazer funcionar.

      https://stackoverflow.com/a/7501113/250329

      Excluir
    2. Pois é, eu vi na época.

      Tem que ficar colocando regras no registry do tipo site tal usar certificado tal, mas para automação isso não serve, é possível para Selenium "em desktop/local" essa alteração no nível de Windows (automação não em grid - usando ChromDriver), mas então ficar alterando o registry para cada situação não rola.
      Em Selenium Grid (RemoteDriver) isso não funciona porque não há comandos que interajam com o sistema operacional.

      Para Selenium "em desktop" eu fiz um esquema de automação que interage com a janela de escolha do certificado de acordo com algum CN informado, funciona há anos, mas cai no mesmo problema, em grid não funciona.

      O negócio é que o Chrome utiliza o certificados instalados no Windows, não numa área própria (perfil do usuário no caso do Firefox).

      Excluir
    3. Richardson, cara estou nesse momento tentando desenvolver uma ferramenta pra automatizar essa interação com a janela de escolha de certificado, como você fez isso?

      Excluir
    4. Tinha feito algumas versões com automação desktop com o Test Stack White, depois com o FlaUI, mas depois vi que não precisava de tanto então usei só Windows Automation básico para recuperar o certificado corrente e injeção de teclas DOWN e ENTER, como a navegação por um usuário.

      É um cara-crachá, com a janela em foco fazer um loop eterno até acabar de rolar a listagem e comparar o valor do elemento corrente com o certificado esperado, isso até não ter mais seleção a rolar.

      Excluir
  4. Olá eu estou em um problema parecido, porém usando Python, eu fiquei um pouco em dúvida com algumas partes do código que você disponibilizou, você teria algum conteúdo semelhante para me apresentar em Python? Desde já agradeço pelo seu post, me deu um norte muito bom

    ResponderExcluir
    Respostas
    1. Olá, tudo bem? Infelizmente eu não tenho uma contrapartida pra esse código em Python. No entanto, eu dei uma olhada no Stack Overflow e acredito que essa questão aqui possa te ajudar: https://stackoverflow.com/questions/13287490/is-there-a-way-to-use-phantomjs-in-python
      Abraços

      Excluir
  5. Tem alguma maneira de realizar download de arquivos com o PhantomJS? Pois eu consegui realizar o acesso com o certificado digital, porém, download não foi possível ainda

    ResponderExcluir

Postar um comentário

Postagens mais visitadas deste blog

Trocando configurações padrão do Live TIM

Uma proposta de Clean Architecure com Modelo de Atores