Condividi tramite


Configurare l'autenticazione reciproca TLS nel servizio app di Azure

È possibile limitare l'accesso all'app del servizio app di Azure abilitando vari tipi di autenticazione per l'app. Un modo per configurare l'autenticazione consiste nel richiedere un certificato client quando la richiesta client viene inviata usando Transport Layer Security (TLS) /Secure Sockets Layer (SSL) e per convalidare il certificato. Questo meccanismo è detto autenticazione reciproca o autenticazione del certificato client. Questo articolo illustra come configurare l'app per l'uso dell'autenticazione del certificato client.

Nota

Il codice dell'app deve convalidare il certificato client. Il servizio app non fa nulla con il certificato client a parte inoltrarlo alla tua app.

Se si accede al sito tramite HTTP e non HTTPS, non si ricevono certificati client. Se l'applicazione richiede certificati client, non è consigliabile consentire le richieste all'applicazione tramite HTTP.

Preparare l'app Web

Se si vogliono creare associazioni TLS/SSL personalizzate o abilitare i certificati client per l'app del servizio app, il piano di servizio app deve trovarsi nei livelli Basic, Standard, Premium o Isolato.

Per garantirsi che l'app Web sia in un piano tariffario supportato:

Passare all'app Web

  1. Nella casella di ricerca del portale di Azure immettere Servizi app e quindi selezionarla nei risultati della ricerca.

  2. Nella pagina Servizi app selezionare l'app Web:

    Screenshot della pagina Servizi app nel portale di Azure.

    Si è ora nella pagina di gestione dell'app Web.

Scegliere il piano tariffario

  1. Nel menu a sinistra per l'app Web, in Impostazioni selezionare Scale up (Piano di servizio app).

  2. Assicurarsi che l'app Web non sia nel livello F1 o D1. Questi livelli non supportano TLS/SSL personalizzato.

  3. Se è necessario passare a un livello superiore, seguire la procedura della sezione successiva. In caso contrario, chiudere il riquadro Aumenta e ignorare la sezione successiva.

Passare a un piano di servizio app superiore

  1. Selezionare qualsiasi livello non gratuito, ad esempio B1, B2, B3 o qualsiasi altro livello nella categoria Produzione .

  2. Al termine, scegliere Seleziona.

    Al termine dell'operazione di scalabilità, verrà visualizzato un messaggio che informa che il piano è stato aggiornato.

Abilitare i certificati client

Quando si abilitano i certificati client per l'app, è necessario selezionare la modalità di certificato client scelta. La modalità definisce il modo in cui l'app gestisce i certificati client in ingresso. Le modalità sono descritte nella tabella seguente:

Modalità dei certificati client Descrizione
Richiesto Tutte le richieste richiedono un certificato client.
Facoltativo Le richieste possono usare un certificato client. Ai clienti viene richiesto, di default, un certificato. Ad esempio, i client del browser visualizzano un prompt per selezionare un certificato per l'autenticazione.
Utente interattivo facoltativo Le richieste possono usare un certificato client. I client non vengono richiesti per impostazione predefinita per un certificato. Ad esempio, i client del browser non visualizzano una richiesta di selezionare un certificato per l'autenticazione.

Per usare il portale di Azure per abilitare i certificati client:

  1. Passare alla pagina di gestione delle app.
  2. Nel menu a sinistra selezionare Impostazioni generali di configurazione>.
  3. In Modalità certificato client selezionare la scelta.
  4. Seleziona Salva.

Escludere i percorsi dalla richiesta di autenticazione

Quando si abilita l'autenticazione reciproca per l'applicazione, tutti i percorsi nella radice dell'app richiedono un certificato client per l'accesso. Per rimuovere questo requisito per determinati percorsi, definire i percorsi di esclusione come parte della configurazione dell'applicazione.

Nota

L'uso di qualsiasi percorso di esclusione del certificato client attiva la rinegoziazione TLS per le richieste in ingresso all'app.

  1. Nel menu a sinistra della pagina di gestione delle app selezionare Configurazione impostazioni>. Seleziona la scheda Impostazioni generali.

  2. Accanto a Percorsi di esclusione certificati selezionare l'icona a forma di matita.

  3. Selezionare Nuovo percorso, specificare un percorso o un elenco di percorsi separati da , o ;e quindi selezionare OK.

  4. Seleziona Salva.

Lo screenshot seguente mostra come impostare un percorso di esclusione del certificato. In questo esempio, qualsiasi percorso per l'app che inizia con /public non richiede un certificato client. La corrispondenza del percorso non è specifica del caso.

Screenshot che mostra come impostare un percorso di esclusione del certificato.

Rinegoziazione del certificato client e TLS

Per alcune impostazioni del certificato client, servizio app richiede la rinegoziazione TLS per leggere una richiesta prima di sapere se richiedere un certificato client. Entrambe le impostazioni seguenti attivano la rinegoziazione TLS:

Nota

TLS 1.3 e HTTP 2.0 non supportano la rinegoziazione TLS. Questi protocolli non funzionano se l'app è configurata con le impostazioni del certificato client che usano la rinegoziazione TLS.

Per disabilitare la rinegoziazione TLS e fare in modo che l'app negozii i certificati client durante l'handshake TLS, è necessario eseguire le azioni seguenti nell'app:

  • Impostare la modalità certificato client su Obbligatorio o Facoltativo.
  • Rimuovere tutti i percorsi di esclusione dei certificati client.

Caricare file di grandi dimensioni con la rinegoziazione TLS

Le configurazioni dei certificati client che usano la rinegoziazione TLS non possono supportare le richieste in ingresso con file di dimensioni superiori a 100 KB. Questo limite è causato dalle limitazioni delle dimensioni del buffer. In questo scenario, tutte le richieste POST o PUT superiori a 100 KB hanno esito negativo con un errore 403. Questo limite non è configurabile e non può essere aumentato.

Per risolvere il limite di 100 KB, prendere in considerazione queste soluzioni:

  • Disabilitare la rinegoziazione TLS. Eseguire le azioni seguenti nelle configurazioni del certificato client dell'app:
    • Impostare la modalità certificato client su Obbligatorio o Facoltativo.
    • Rimuovere tutti i percorsi di esclusione dei certificati client.
  • Inviare una richiesta HEAD prima della richiesta PUT/POST. La richiesta HEAD gestisce il certificato client.
  • Aggiungere l'intestazione Expect: 100-Continue alla richiesta. Questa intestazione fa sì che il client attenda che il server risponda con un 100 Continue prima di inviare il corpo della richiesta e che i buffer vengano ignorati.

Accedere al certificato client

In App Service, la terminazione TLS della richiesta avviene nel bilanciatore di carico front-end. Quando servizio app inoltra la richiesta al codice dell'app con i certificati client abilitati, inserisce un'intestazione X-ARR-ClientCert della richiesta con il certificato client. Il servizio app non esegue alcuna operazione con questo certificato client diverso da inoltrarlo all'app. Il codice dell'app deve convalidare il certificato client.

In ASP.NET il certificato client è disponibile tramite la HttpRequest.ClientCertificate proprietà .

In altri stack di applicazioni (Node.js, PHP), il certificato client è disponibile tramite un valore con codifica Base64 nell'intestazione della X-ARR-ClientCert richiesta.

Esempio di ASP.NET Core

Per ASP.NET Core, il middleware è disponibile per analizzare i certificati inoltrati. Il middleware separato è disponibile per l'uso delle intestazioni di protocollo trasmesse. Entrambi devono essere presenti per l'accettazione dei certificati inoltrati. È possibile inserire la logica di convalida del certificato personalizzata nelle opzioni 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?}");
        });
    }
}

Esempio di Web Form ASP.NET

    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;
            }
        }
    }

esempio di Node.js

Il codice di esempio seguente Node.js ottiene l'intestazione X-ARR-ClientCert e usa node-forge per convertire la stringa pem (Privacy Enhanced Mail) con codifica Base64 in un oggetto certificato e convalidarla:

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);
            }
        }
    }
}

Esempio Java

La classe Java seguente codifica il certificato da X-ARR-ClientCert a un'istanza X509Certificate di . certificateIsValid() verifica che l'impronta digitale del certificato corrisponda a quella specificata nel costruttore e che il certificato non sia scaduto.

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;
    }
}

Esempio in Python

Gli esempi di codice Flask e Django Python seguenti implementano un elemento Decorator denominato authorize_certificate che può essere usato in una funzione di visualizzazione per consentire l'accesso solo ai chiamanti che presentano un certificato client valido. Prevede un certificato in formato PEM nell'intestazione X-ARR-ClientCert e usa il pacchetto di crittografia Python per convalidare il certificato in base all'impronta digitale (identificazione personale), al nome comune del soggetto, al nome comune dell'autorità emittente e alle date di inizio e scadenza. Se la convalida ha esito negativo, l'elemento Decorator garantisce che al client venga restituita una risposta HTTP con codice di stato 403 (Accesso negato).

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

Il frammento di codice seguente illustra come usare l'elemento Decorator in una funzione di visualizzazione Flask.

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