mTLS: Implementando Autenticação Mútua entre Cliente e Servidor em Go

mTLS (Mutual TLS) autentica mutuamente cliente e servidor. Ambos apresentam certificados digitais assinados por uma autoridade certificadora confiável. Controlando a emissão dos certificados, você garante que apenas clientes autorizados acessem seu serviço. Além disso, identifica facilmente o cliente sem precisar de uma API Key ou Token. Também recomendo usar um cabeçalho HMAC (Hash-based Message Authentication Code) como camada adicional de segurança e integridade da mensagem.

Criando Certificados

Neste exemplo, criarei os certificados de forma simples para testes locais. Ao implementar em produção, tome cuidado com os parâmetros utilizados. É importante incluir Subject Alternative Names (SANs), pois apenas o CN (Common Name) não é mais recomendado.

Primeiro, crie o par de chaves da CA (Certificate Authority):

openssl genrsa -out ca.key 4096

openssl req -x509 -new -nodes \
    -key ca.key -sha256 -days 3650 \
    -out ca.pem \
    -subj "/C=BR/ST=Estado/L=Cidade/O=MinhaEmpresa/OU=TI/CN=MinhaCA"

Com isso, você terá os arquivos ca.key e ca.pem, que são a chave privada e pública da CA.

Crie um arquivo de texto server_cert_ext.cnf com as configurações do certificado do servidor:

[ req ]
default_bits       = 2048
prompt             = no
default_md         = sha256
distinguished_name = dn
req_extensions     = req_ext

[ dn ]
C  = BR
ST = Estado
L  = Cidade
O  = MinhaEmpresa
OU = TI
CN = localhost

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = localhost
IP.1  = 127.0.0.1

Agora, crie o par de chaves do servidor:

openssl genrsa -out server.key 2048

openssl req -new \
    -key server.key \
    -out server.csr \
    -config server_cert_ext.cnf

Assine o certificado do servidor com a CA, incluindo as extensões:

openssl x509 -req \
    -in server.csr \
    -CA ca.pem \
    -CAkey ca.key \
    -CAcreateserial \
    -out server.pem \
    -days 365 \
    -sha256 \
    -extfile server_cert_ext.cnf \
    -extensions req_ext

Agora, o certificado do servidor está pronto. Crie o par de chaves do cliente. Primeiro, crie o arquivo de configuração client_cert_ext.cnf:

[ req ]
default_bits       = 2048
prompt             = no
default_md         = sha256
distinguished_name = dn
req_extensions     = req_ext

[ dn ]
C  = BR
ST = Estado
L  = Cidade
O  = MinhaEmpresa
OU = CODIGODOCLIENTE
CN = Cliente

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = Cliente

Observe que no campo OU, coloquei um código do cliente para identificar quem acessa o serviço. Agora, crie o par de chaves do cliente:

openssl genrsa -out client.key 2048

openssl req -new -key client.key \
    -out client.csr \
    -config client_cert_ext.cnf

Assine o certificado do cliente com a CA, incluindo as extensões:

openssl x509 -req \
    -in client.csr \
    -CA ca.pem \
    -CAkey ca.key \
    -CAcreateserial \
    -out client.pem \
    -days 365 \
    -sha256 \
    -extfile client_cert_ext.cnf \
    -extensions req_ext

Agora, você possui todos os certificados necessários:

  • ca.pem: Certificado da CA.
  • ca.key: Chave privada da CA.
  • server.pem: Certificado do servidor com SANs.
  • server.key: Chave privada do servidor.
  • client.pem: Certificado do cliente com SANs.
  • client.key: Chave privada do cliente.

Servidor

Crie um servidor simples em Go que utiliza os certificados criados:

cert, err := tls.LoadX509KeyPair("server.pem", "server.key")
if err != nil {
    log.Fatalf("Erro ao carregar certificado do servidor: %v", err)
}

caCert, err := os.ReadFile("ca.pem")
if err != nil {
    log.Fatalf("Erro ao ler CA cert: %v", err)
}
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
    log.Fatalf("Erro ao adicionar CA cert ao pool")
}

tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{cert},
    ClientCAs:    caCertPool,
    ClientAuth:   tls.RequireAndVerifyClientCert,
}

server := &http.Server{
    Addr:      ":8080",
    TLSConfig: tlsConfig,
}

http.HandleFunc("/", helloHandler)

log.Fatal(server.ListenAndServeTLS("", ""))

código fonte completo do servidor

O processo é similar ao de criar um servidor comum. Carregue o certificado e a chave privada do servidor. Carregue também o certificado da CA e adicione ao ClientCAs. Configure ClientAuth para tls.RequireAndVerifyClientCert, exigindo que o cliente apresente um certificado válido. Existem outras opções de configuração que você pode ajustar conforme necessário.

O handler helloHandler verifica se o cliente está autorizado:

func helloHandler(w http.ResponseWriter, r *http.Request) {
    if len(r.TLS.PeerCertificates) > 0 {
        client := r.TLS.PeerCertificates[0]
        ret := fmt.Sprintf(
            "Common Name: %s\nOrganization Unit: %s\nSerial Number: %s\n",
            client.Subject.CommonName,
            client.Subject.OrganizationalUnit[0],
            client.SerialNumber.Text(16))
        fmt.Fprintf(w, ret)
        log.Printf("Client CN: %s, OU: %s, Serial: %s\n",
            client.Subject.CommonName,
            client.Subject.OrganizationalUnit[0],
            client.SerialNumber.Text(16))
        return
    }

    w.WriteHeader(http.StatusUnauthorized)
    fmt.Fprintf(w, `{ "error": "client certificate required" }`)
}

O handler verifica se o cliente possui um certificado. Se sim, formata e retorna informações do certificado. Caso contrário, responde com erro de autorização.

Cliente

Crie um cliente simples em Go que utiliza o certificado criado:

cert, err := tls.LoadX509KeyPair("client.pem", "client.key")
if err != nil {
    log.Fatalf("Erro ao carregar certificado do cliente: %v", err)
}

caCert, err := os.ReadFile("ca.pem")
if err != nil {
    log.Fatalf("Erro ao ler CA cert: %v", err)
}
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
    log.Fatalf("Erro ao adicionar CA cert ao pool")
}

tlsConfig := &tls.Config{
    Certificates:       []tls.Certificate{cert},
    RootCAs:            caCertPool,
    InsecureSkipVerify: false,
}

transport := &http.Transport{
    TLSClientConfig: tlsConfig,
}

client := &http.Client{
    Transport: transport,
}

resp, err := client.Get("https://localhost:8080/")
if err != nil {
    log.Fatalf("Erro na requisição: %v", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
    log.Fatalf("Erro ao ler resposta: %v", err)
}

fmt.Printf("Resposta do servidor: %s\n", body)

código fonte completo do cliente

Como no servidor, carregue o certificado e a chave privada do cliente. Carregue também o certificado da CA e configure o transporte HTTP com TLSClientConfig.

Pontos Importantes

  • Segurança: mTLS oferece autenticação segura. Inclua SANs nos certificados para maior segurança.
  • Expiração: Gerencie a renovação dos certificados para evitar que clientes percam acesso por certificados expirados.
  • Complexidade: mTLS é mais complexo que outras formas de autenticação. Facilite a integração criando SDKs para os clientes.
  • Gerenciamento de Certificados: Mantenha os arquivos de chaves públicas e privadas seguros. Planeje bem o armazenamento e a distribuição desses arquivos.

Conclusão

mTLS é a forma mais segura de autenticação. Após resolver os desafios iniciais de gerenciamento de chaves e certificados, você terá um sistema robusto e seguro. Os códigos de cliente e servidor são simples e podem ser integrados facilmente ao seu projeto, tornando o uso transparente. No entanto, a cada novo cliente, será necessário orientá-lo sobre certificados e segurança da informação.

Vídeo com a explicação:

Cesar Gimenes

Última modificação