Configurare l'autenticazione reciproca TLS per Servizio app di Azure

È possibile limitare l'accesso all'app di Servizio app di Azure abilitandone diversi tipi di autenticazione. Un modo per eseguire questa operazione consiste nel richiedere un certificato client quando la richiesta client avviene tramite TLS/SSL e nel convalidare il certificato. Questo meccanismo è denominato autenticazione reciproca TLS 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 esegue la convalida del certificato client. servizio app non esegue alcuna operazione con questo certificato client diverso dall'inoltro all'app.

se si accede al sito tramite HTTP e non HTTPS, non si riceveranno i certificati client. Pertanto, se l'applicazione richiede i certificati client, è consigliabile non consentire le richieste all'applicazione tramite HTTP.

Preparare l'app Web

Per creare binding TLS/SSL personalizzati o abilitare i certificati client per l'app del servizio app, il livello del piano di servizio app deve essere Basic, Standard, Premium o Isolato. Per assicurarsi che l'app Web sia nel piano tariffario supportato, seguire questa procedura:

Passare all'app Web

  1. Nella casella di ricerca portale di Azure trovare e selezionare servizio app.

    Screenshot del portale di Azure, della casella di ricerca e dell'opzione

  2. Nella pagina servizio app selezionare il nome dell'app Web.

    Screenshot della pagina Servizi app nel portale di Azure che mostra un elenco di tutte le app Web in esecuzione, con la prima app evidenziata.

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

Scegliere il piano tariffario

  1. Nel menu a sinistra per l'app Web, nella sezione Impostazioni selezionare Scale up (servizio app plan).

    Screenshot del menu dell'app Web, della sezione

  2. Assicurarsi che l'app Web non sia nel livello F1 o D1 , che non supporta TLS/SSL personalizzato.

  3. Se è necessario passare a un livello superiore, seguire la procedura della sezione successiva. In caso contrario, chiudere la pagina Aumentare le prestazioni e ignorare la sezione Aumentare le prestazioni del piano servizio app.

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, selezionare Seleziona.

    Quando viene visualizzato il messaggio seguente, l'operazione di ridimensionamento è stata completata.

    Screenshot con messaggio di conferma per l'operazione di aumento delle prestazioni.

Abilitare i certificati client

Per configurare l'app per richiedere certificati client:

  1. Nel riquadro di spostamento sinistro della pagina di gestione dell'app selezionare Impostazioni generali di configurazione>.

  2. Selezionare Modalità certificato client preferita. Seleziona Salva nella parte superiore della pagina.

Modalità certificato client Descrizione
Richiesto Tutte le richieste richiedono un certificato client.
Facoltativo Le richieste possono o meno usare un certificato client. I client verranno richiesti per impostazione predefinita per un certificato. Ad esempio, i client del browser visualizzeranno un prompt per selezionare un certificato per l'autenticazione.
Utente interattivo facoltativo Le richieste possono o meno usare un certificato client. I client non verranno richiesti per impostazione predefinita per un certificato. Ad esempio, i client del browser non visualizzeranno una richiesta di selezionare un certificato per l'autenticazione.

Per eseguire la stessa operazione con l'interfaccia della riga di comando di Azure, eseguire il comando seguente in Cloud Shell:

az webapp update --set clientCertEnabled=true --name <app-name> --resource-group <group-name>

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.

  1. Nel riquadro di spostamento sinistro della pagina di gestione dell'app selezionare Impostazioni generali di configurazione>.

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

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

  4. Seleziona Salva nella parte superiore della pagina.

Nello screenshot seguente, qualsiasi percorso per l'app che inizia con /public non richiede un certificato client. La corrispondenza del percorso non fa distinzione tra maiuscole e minuscole.

Percorsi di esclusione dei certificati

Accedere al certificato client

Nel servizio app la terminazione TLS della richiesta si verifica nel servizio di bilanciamento del 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. servizio app non esegue alcuna operazione con questo certificato client diverso dall'inoltro all'app. Il codice dell'app esegue la convalida del certificato client.

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

Per altri stack di applicazioni (Node.js, PHP e così via), il certificato client è disponibile nell'app tramite un valore codificato base64 nell'intestazione della X-ARR-ClientCert richiesta.

Esempio di ASP.NET Core

Per ASP.NET Core, il middleware viene fornito per analizzare i certificati inoltrati. Il middleware separato viene fornito per usare le intestazioni del protocollo inoltrato. 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 forwared by the frontend load balancer
        services.Configure<ForwardedHeadersOptions>(options =>
        {
            options.ForwardedHeaders =
                ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
            // Only loopback proxies are allowed by default. Clear that restriction to enable this explicit configuration.
            options.KnownNetworks.Clear();
            options.KnownProxies.Clear();
        });       
        
        // Configure the application to client certificate forwarded the frontend load balancer
        services.AddCertificateForwarding(options => { options.CertificateHeader = "X-ARR-ClientCert"; });

        // Add certificate authentication so 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 ASP.NET WebForms

    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. Depending on your application logic and security requirements, 
            // you should modify this method
            //
            private bool IsValidClientCertificate()
            {
                // In this example we will only accept the certificate as a valid certificate if all the conditions below are met:
                // 1. The certificate isn't expired and is active for the current time on server.
                // 2. The subject name of the certificate has the common name nildevecc
                // 3. The issuer name of the certificate has the common name nildevecc and organization name Microsoft Corp
                // 4. The thumbprint of the certificate is 30757A2E831977D8BD9C8496E4C99AB26CB9622B
                //
                // This example doesn't test that this certificate is chained to a Trusted Root Authority (or revoked) on the server 
                // and it allows for self signed certificates
                //

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

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

                // 2. Check subject name of 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 issuer name of 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 thumprint of 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 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.CERT
            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() convalida che l'identificazione personale del certificato corrisponda a quella specificata nel costruttore e che il certificato non è 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 cannot 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 hasn't expired.
     * @return True if the certificate's thumbprint matches and hasn't expired. False otherwise.
     */
    public boolean certificateIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        return certificateHasNotExpired() && thumbprintIsValid();
    }

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

    /**
     * Check the certificate's thumbprint matches the given one.
     * @return Returns 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 con formattazione 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')