Partilhar via


Configurar a autenticação mútua TLS no Serviço de Aplicativo do Azure

Você pode restringir o acesso ao seu aplicativo do Serviço de Aplicativo do Azure habilitando vários tipos de autenticação para o aplicativo. Uma maneira de configurar a autenticação é solicitar um certificado de cliente quando a solicitação do cliente é enviada usando Transport Layer Security (TLS) / Secure Sockets Layer (SSL) e validar o certificado. Esse mecanismo é chamado de autenticação mútua ou autenticação de certificado de cliente. Este artigo mostra como configurar a aplicação para utilizar a autenticação de certificados de cliente.

Nota

O código do aplicativo deve validar o certificado do cliente. O Serviço de Aplicativo não faz nada com o certificado do cliente além de encaminhá-lo para seu aplicativo.

Se você acessar seu site por HTTP e não HTTPS, não receberá nenhum certificado de cliente. Se seu aplicativo requer certificados de cliente, você não deve permitir solicitações para seu aplicativo por HTTP.

Preparar a sua aplicação Web

Se você quiser criar associações TLS/SSL personalizadas ou habilitar certificados de cliente para seu aplicativo do Serviço de Aplicativo, seu plano do Serviço de Aplicativo deverá estar nas camadas Básica, Padrão, Premium ou Isolada.

Para se certificar de que a sua aplicação Web está num escalão de preços suportado:

Aceda à sua aplicação Web

  1. Na caixa de pesquisa do portal do Azure , insira Serviços de Aplicativo e selecione-o nos resultados da pesquisa.

  2. Na página Serviços de Aplicativo , selecione seu aplicativo Web:

    Captura de ecrã da página Serviços de Aplicações no portal do Azure.

    Agora você está na página de gerenciamento do seu aplicativo Web.

Verificar o escalão de preço

  1. No menu esquerdo do seu aplicativo Web, em Configurações, selecione Aumentar a escala (plano do Serviço de Aplicativo).

  2. Verifique se seu aplicativo Web não está na camada F1 ou D1. Essas camadas não suportam TLS/SSL personalizado.

  3. Se precisar de expandir, siga os passos na secção seguinte. Caso contrário, feche o painel Aumentar escala e ignore a próxima seção.

Ampliar o seu plano de Serviço de Aplicações

  1. Selecione qualquer camada não livre, como B1, B2, B3 ou qualquer outra camada na categoria Produção.

  2. Quando terminar, escolha Selecionar.

    Quando a operação de escala estiver concluída, você verá uma mensagem informando que o plano foi atualizado.

Habilitar certificados de cliente

Ao habilitar certificados de cliente para seu aplicativo, você deve selecionar o modo de certificado de cliente escolhido. O modo define como seu aplicativo lida com certificados de cliente de entrada. Os modos são descritos na tabela a seguir:

Modo de certificado do cliente Descrição
Obrigatório Todas as solicitações exigem um certificado de cliente.
Opcional As solicitações podem usar um certificado de cliente. Os clientes são solicitados a fornecer um certificado por padrão. Por exemplo, os clientes do navegador mostram um prompt para selecionar um certificado para autenticação.
Usuário interativo opcional As solicitações podem usar um certificado de cliente. Os clientes não são solicitados a fornecer um certificado por padrão. Por exemplo, os clientes do navegador não mostram um prompt para selecionar um certificado para autenticação.

Para usar o portal do Azure para habilitar certificados de cliente:

  1. Aceda à página de gestão da aplicação.
  2. No menu à esquerda, selecione Configurações>gerais.
  3. Para o modo de certificado de cliente, selecione sua escolha.
  4. Selecione Guardar.

Remover caminhos da necessidade de autenticação

Quando você habilita a autenticação mútua para seu aplicativo, todos os caminhos sob a raiz do seu aplicativo exigem um certificado de cliente para acesso. Para remover esse requisito para determinados caminhos, defina caminhos de exclusão como parte da configuração do aplicativo.

Nota

O uso de qualquer caminho de exclusão de certificado de cliente aciona a renegociação do TLS para solicitações recebidas no aplicativo.

  1. No menu esquerdo da página de gerenciamento de aplicativos , selecione>Configurações de configuração. Selecione a aba Configurações gerais.

  2. Ao lado de Caminhos de exclusão de certificado, selecione o ícone do lápis.

  3. Selecione Novo caminho, especifique um caminho ou uma lista de caminhos separados por , ou ;e, em seguida, selecione OK.

  4. Selecione Guardar.

A captura de tela a seguir mostra como definir um caminho de exclusão de certificado. Neste exemplo, qualquer caminho para o aplicativo que começa com /public não solicita um certificado de cliente. A correspondência de caminho não é específica de maiúsculas e minúsculas.

Captura de tela que mostra como definir um caminho de exclusão de certificado.

Certificado de cliente e renegociação de TLS

Para algumas configurações de certificado de cliente, o Serviço de Aplicativo requer a renegociação do TLS para ler uma solicitação antes de saber se deseja solicitar um certificado de cliente. Ambas as configurações a seguir acionam a renegociação do TLS:

Nota

TLS 1.3 e HTTP 2.0 não suportam renegociação TLS. Esses protocolos não funcionarão se seu aplicativo estiver configurado com configurações de certificado de cliente que usam renegociação TLS.

Para desabilitar a renegociação de TLS e fazer com que o aplicativo negocie certificados de cliente durante o handshake TLS, você deve executar as seguintes ações em seu aplicativo:

  • Defina o modo de certificado do cliente como Obrigatório ou Opcional.
  • Remova todos os caminhos de exclusão de certificados de cliente.

Carregue grandes arquivos com renegociação TLS

As configurações de certificado de cliente que usam a renegociação TLS não podem suportar solicitações de entrada com arquivos maiores que 100 KB. Esse limite é causado por limitações de tamanho do buffer. Nesse cenário, todas as solicitações POST ou PUT com mais de 100 KB falham com um erro 403. Este limite não é configurável e não pode ser aumentado.

Para abordar o limite de 100 KB, considere estas soluções:

  • Desative a renegociação TLS. Execute as seguintes ações nas configurações de certificado de cliente do seu aplicativo:
    • Defina o modo de certificado do cliente como Obrigatório ou Opcional.
    • Remova todos os caminhos de exclusão de certificados de cliente.
  • Envie uma solicitação HEAD antes da solicitação PUT/POST. A solicitação HEAD lida com o certificado do cliente.
  • Adicione o cabeçalho Expect: 100-Continue ao seu pedido. Esse cabeçalho faz com que o cliente aguarde até que o servidor responda com um 100 Continue antes de enviar o corpo da solicitação e os buffers sejam ignorados.

Aceder ao certificado do cliente

No App Service, a terminação TLS da solicitação acontece no balanceador de carga front-end. Quando o Serviço de Aplicativo encaminha a solicitação para o código do aplicativo com certificados de cliente habilitados, ele injeta um X-ARR-ClientCert cabeçalho de solicitação com o certificado do cliente. O Serviço de Aplicativo não faz nada com esse certificado de cliente além de encaminhá-lo para seu aplicativo. O código da aplicação deve validar o certificado do cliente.

Em ASP.NET, o certificado do cliente está disponível através da HttpRequest.ClientCertificate propriedade.

Em outras pilhas de aplicativos (Node.js, PHP), o certificado do cliente está disponível através de um valor codificado em Base64 no cabeçalho da X-ARR-ClientCert solicitação.

Exemplo ASP.NET Core

Para ASP.NET Core, o middleware está disponível para analisar certificados encaminhados. Middleware separado está disponível destinado ao uso dos cabeçalhos de protocolo encaminhados. Ambos devem estar presentes para que os certificados encaminhados sejam aceitos. Você pode colocar a lógica de validação de certificado personalizada nas opções CertificateAuthentication:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        // Configure the application to use the protocol and client IP address forwarded by the front-end load balancer.
        services.Configure<ForwardedHeadersOptions>(options =>
        {
            options.ForwardedHeaders =
                ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
            // By default, only loopback proxies are allowed. Clear that restriction to enable this explicit configuration.
            options.KnownNetworks.Clear();
            options.KnownProxies.Clear();
        });       
        
        // Configure the application to use the client certificate forwarded by the front-end load balancer.
        services.AddCertificateForwarding(options => { options.CertificateHeader = "X-ARR-ClientCert"; });

        // Add certificate authentication so that when authorization is performed the user will be created from the certificate.
        services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }
        
        app.UseForwardedHeaders();
        app.UseCertificateForwarding();
        app.UseHttpsRedirection();

        app.UseAuthentication()
        app.UseAuthorization();

        app.UseStaticFiles();

        app.UseRouting();
        
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

Exemplo de ASP.NET Web Forms

    using System;
    using System.Collections.Specialized;
    using System.Security.Cryptography.X509Certificates;
    using System.Web;

    namespace ClientCertificateUsageSample
    {
        public partial class Cert : System.Web.UI.Page
        {
            public string certHeader = "";
            public string errorString = "";
            private X509Certificate2 certificate = null;
            public string certThumbprint = "";
            public string certSubject = "";
            public string certIssuer = "";
            public string certSignatureAlg = "";
            public string certIssueDate = "";
            public string certExpiryDate = "";
            public bool isValidCert = false;

            //
            // Read the certificate from the header into an X509Certificate2 object.
            // Display properties of the certificate on the page.
            //
            protected void Page_Load(object sender, EventArgs e)
            {
                NameValueCollection headers = base.Request.Headers;
                certHeader = headers["X-ARR-ClientCert"];
                if (!String.IsNullOrEmpty(certHeader))
                {
                    try
                    {
                        byte[] clientCertBytes = Convert.FromBase64String(certHeader);
                        certificate = new X509Certificate2(clientCertBytes);
                        certSubject = certificate.Subject;
                        certIssuer = certificate.Issuer;
                        certThumbprint = certificate.Thumbprint;
                        certSignatureAlg = certificate.SignatureAlgorithm.FriendlyName;
                        certIssueDate = certificate.NotBefore.ToShortDateString() + " " + certificate.NotBefore.ToShortTimeString();
                        certExpiryDate = certificate.NotAfter.ToShortDateString() + " " + certificate.NotAfter.ToShortTimeString();
                    }
                    catch (Exception ex)
                    {
                        errorString = ex.ToString();
                    }
                    finally 
                    {
                        isValidCert = IsValidClientCertificate();
                        if (!isValidCert) Response.StatusCode = 403;
                        else Response.StatusCode = 200;
                    }
                }
                else
                {
                    certHeader = "";
                }
            }

            //
            // This is a sample verification routine. You should modify this method to suit  your application logic and security requirements. 
            // 
            //
            private bool IsValidClientCertificate()
            {
                // In this example, the certificate is accepted as a valid certificate only if these conditions are met:
                // - The certificate isn't expired and is active for the current time on the server.
                // - The subject name of the certificate has the common name nildevecc.
                // - The issuer name of the certificate has the common name nildevecc and the organization name Microsoft Corp.
                // - The thumbprint of the certificate is 30757A2E831977D8BD9C8496E4C99AB26CB9622B.
                //
                // This example doesn't test that the certificate is chained to a trusted root authority (or revoked) on the server. 
                // It allows self-signed certificates.
                //

                if (certificate == null || !String.IsNullOrEmpty(errorString)) return false;

                // 1. Check time validity of the certificate.
                if (DateTime.Compare(DateTime.Now, certificate.NotBefore) < 0 || DateTime.Compare(DateTime.Now, certificate.NotAfter) > 0) return false;

                // 2. Check the subject name of the certificate.
                bool foundSubject = false;
                string[] certSubjectData = certificate.Subject.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string s in certSubjectData)
                {
                    if (String.Compare(s.Trim(), "CN=nildevecc") == 0)
                    {
                        foundSubject = true;
                        break;
                    }
                }
                if (!foundSubject) return false;

                // 3. Check the issuer name of the certificate.
                bool foundIssuerCN = false, foundIssuerO = false;
                string[] certIssuerData = certificate.Issuer.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string s in certIssuerData)
                {
                    if (String.Compare(s.Trim(), "CN=nildevecc") == 0)
                    {
                        foundIssuerCN = true;
                        if (foundIssuerO) break;
                    }

                    if (String.Compare(s.Trim(), "O=Microsoft Corp") == 0)
                    {
                        foundIssuerO = true;
                        if (foundIssuerCN) break;
                    }
                }

                if (!foundIssuerCN || !foundIssuerO) return false;

                // 4. Check the thumbprint of the certificate.
                if (String.Compare(certificate.Thumbprint.Trim().ToUpper(), "30757A2E831977D8BD9C8496E4C99AB26CB9622B") != 0) return false;

                return true;
            }
        }
    }

Node.js amostra

O código de amostra de Node.js a seguir obtém o X-ARR-ClientCert cabeçalho e usa node-forge para converter a string PEM (Privacy Enhanced Mail) codificada em Base64 em um objeto de certificado e validá-lo:

import { NextFunction, Request, Response } from 'express';
import { pki, md, asn1 } from 'node-forge';

export class AuthorizationHandler {
    public static authorizeClientCertificate(req: Request, res: Response, next: NextFunction): void {
        try {
            // Get header.
            const header = req.get('X-ARR-ClientCert');
            if (!header) throw new Error('UNAUTHORIZED');

            // Convert from PEM to PKI certificate.
            const pem = `-----BEGIN CERTIFICATE-----${header}-----END CERTIFICATE-----`;
            const incomingCert: pki.Certificate = pki.certificateFromPem(pem);

            // Validate certificate thumbprint.
            const fingerPrint = md.sha1.create().update(asn1.toDer(pki.certificateToAsn1(incomingCert)).getBytes()).digest().toHex();
            if (fingerPrint.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            // Validate time validity.
            const currentDate = new Date();
            if (currentDate < incomingCert.validity.notBefore || currentDate > incomingCert.validity.notAfter) throw new Error('UNAUTHORIZED');

            // Validate issuer.
            if (incomingCert.issuer.hash.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            // Validate subject.
            if (incomingCert.subject.hash.toLowerCase() !== 'abcdef1234567890abcdef1234567890abcdef12') throw new Error('UNAUTHORIZED');

            next();
        } catch (e) {
            if (e instanceof Error && e.message === 'UNAUTHORIZED') {
                res.status(401).send();
            } else {
                next(e);
            }
        }
    }
}

Exemplo de Java

A classe Java a seguir codifica o certificado de X-ARR-ClientCert para uma X509Certificate instância. certificateIsValid() Verifica que a impressão digital do certificado corresponde à fornecida no construtor e que o certificado não está expirado.

import java.io.ByteArrayInputStream;
import java.security.NoSuchAlgorithmException;
import java.security.cert.*;
import java.security.MessageDigest;

import sun.security.provider.X509Factory;

import javax.xml.bind.DatatypeConverter;
import java.util.Base64;
import java.util.Date;

public class ClientCertValidator { 

    private String thumbprint;
    private X509Certificate certificate;

    /**
     * Constructor.
     * @param certificate. The certificate from the "X-ARR-ClientCert" HTTP header.
     * @param thumbprint. The thumbprint to check against.
     * @throws CertificateException if the certificate factory can't be created.
     */
    public ClientCertValidator(String certificate, String thumbprint) throws CertificateException {
        certificate = certificate
                .replaceAll(X509Factory.BEGIN_CERT, "")
                .replaceAll(X509Factory.END_CERT, "");
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        byte [] base64Bytes = Base64.getDecoder().decode(certificate);
        X509Certificate X509cert =  (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(base64Bytes));

        this.setCertificate(X509cert);
        this.setThumbprint(thumbprint);
    }

    /**
     * Check that the certificate's thumbprint matches the one given in the constructor, and that the
     * certificate isn't expired.
     * @return True if the certificate's thumbprint matches and isn't expired. False otherwise.
     */
    public boolean certificateIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        return certificateHasNotExpired() && thumbprintIsValid();
    }

    /**
     * Check certificate's timestamp.
     * @return True if the certificate isn't expired. It returns False if it is expired.
     */
    private boolean certificateHasNotExpired() {
        Date currentTime = new java.util.Date();
        try {
            this.getCertificate().checkValidity(currentTime);
        } catch (CertificateExpiredException | CertificateNotYetValidException e) {
            return false;
        }
        return true;
    }

    /**
     * Check whether the certificate's thumbprint matches the given one.
     * @return True if the thumbprints match. False otherwise.
     */
    private boolean thumbprintIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        MessageDigest md = MessageDigest.getInstance("SHA-1");
        byte[] der = this.getCertificate().getEncoded();
        md.update(der);
        byte[] digest = md.digest();
        String digestHex = DatatypeConverter.printHexBinary(digest);
        return digestHex.toLowerCase().equals(this.getThumbprint().toLowerCase());
    }

    // Getters and setters.

    public void setThumbprint(String thumbprint) {
        this.thumbprint = thumbprint;
    }

    public String getThumbprint() {
        return this.thumbprint;
    }

    public X509Certificate getCertificate() {
        return certificate;
    }

    public void setCertificate(X509Certificate certificate) {
        this.certificate = certificate;
    }
}

Exemplo de Python

Os exemplos de código Flask e Django Python a seguir implementam um decorador chamado authorize_certificate que pode ser usado em uma função view para permitir o acesso apenas a chamadores que apresentam um certificado de cliente válido. Ele espera um certificado formatado em PEM no X-ARR-ClientCert cabeçalho e usa o pacote de criptografia Python para validar o certificado com base em sua impressão digital, nome comum do sujeito, nome comum do emissor e datas de início e expiração. Se a validação falhar, o decorador garante que uma resposta HTTP com o código de status 403 (Proibido) seja devolvida ao cliente.

from functools import wraps
from datetime import datetime, timezone
from flask import abort, request
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes


def validate_cert(request):

    try:
        cert_value =  request.headers.get('X-ARR-ClientCert')
        if cert_value is None:
            return False
        
        cert_data = ''.join(['-----BEGIN CERTIFICATE-----\n', cert_value, '\n-----END CERTIFICATE-----\n',])
        cert = x509.load_pem_x509_certificate(cert_data.encode('utf-8'))
    
        fingerprint = cert.fingerprint(hashes.SHA1())
        if fingerprint != b'12345678901234567890':
            return False
        
        subject = cert.subject
        subject_cn = subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
        if subject_cn != "contoso.com":
            return False
        
        issuer = cert.issuer
        issuer_cn = issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
        if issuer_cn != "contoso.com":
            return False
    
        current_time = datetime.now(timezone.utc)
    
        if current_time < cert.not_valid_before_utc:
            return False
        
        if current_time > cert.not_valid_after_utc:
            return False
        
        return True

    except Exception as e:
        # Handle any errors encountered during validation.
        print(f"Encountered the following error during certificate validation: {e}")
        return False
    
def authorize_certificate(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not validate_cert(request):
            abort(403)
        return f(*args, **kwargs)
    return decorated_function

O trecho de código a seguir mostra como usar o decorador em uma função de visualização Flask.

@app.route('/hellocert')
@authorize_certificate
def hellocert():
   print('Request for hellocert page received')
   return render_template('index.html')