Konfigurace vzájemného ověřování TLS pro službu Azure App Service

Přístup k aplikaci Azure App Service můžete omezit tak, že pro ni povolíte různé typy ověřování. Jedním ze způsobů, jak to udělat, je požádat o klientský certifikát, když požadavek klienta používá protokol TLS/SSL, a ověřit certifikát. Tento mechanismus se nazývá vzájemné ověřování TLS nebo ověřování klientským certifikátem. Tento článek popisuje, jak aplikaci nastavit tak, aby používala ověřování klientským certifikátem.

Poznámka

Pokud k webu přistupujete přes protokol HTTP, a ne https, neobdržíte žádný klientský certifikát. Pokud tedy vaše aplikace vyžaduje klientské certifikáty, neměli byste povolovat požadavky na aplikaci prostřednictvím protokolu HTTP.

Příprava webové aplikace

Pokud chcete vytvořit vlastní vazby TLS/SSL nebo povolit klientské certifikáty pro App Service aplikaci, váš plán App Service musí být na úrovni Basic, Standard, Premium nebo Isolated. Pokud se chcete ujistit, že je vaše webová aplikace v podporované cenové úrovni, postupujte takto:

Přechod do webové aplikace

  1. Ve vyhledávacím poli Azure Portal vyhledejte a vyberte App Services.

    Snímek obrazovky s Azure Portal, vyhledávacím polem a vybranými službami App Services

  2. Na stránce App Services vyberte název vaší webové aplikace.

    Snímek obrazovky se stránkou App Services v Azure Portal se seznamem všech spuštěných webových aplikací se zvýrazněnou první aplikací v seznamu

    Teď jste na stránce správy vaší webové aplikace.

Kontrola cenové úrovně

  1. V nabídce vlevo pro vaši webovou aplikaci v části Nastavení vyberte Vertikálně navýšit kapacitu (App Service plán).

    Snímek obrazovky s nabídkou webové aplikace, částí Nastavení a vybranou možností Vertikální navýšení kapacity (App Service plán)

  2. Ujistěte se, že vaše webová aplikace není na úrovni F1 nebo D1 , která nepodporuje vlastní PROTOKOL TLS/SSL.

  3. Pokud potřebujete vertikálně navýšit kapacitu, postupujte podle kroků v další části. Jinak zavřete stránku Vertikální navýšení kapacity a přeskočte část Vertikální navýšení kapacity App Service plánu.

Vertikální navýšení kapacity plánu služby App Service

  1. Vyberte jinou než bezplatnou úroveň, například B1, B2, B3 nebo jakoukoli jinou úroveň v kategorii Produkční .

  2. Až budete hotovi, vyberte Vybrat.

    Jakmile se zobrazí následující zpráva, operace škálování byla dokončena.

    Snímek obrazovky s potvrzovací zprávou pro operaci vertikálního navýšení kapacity

Povolení klientských certifikátů

Nastavení aplikace tak, aby vyžadovala klientské certifikáty:

  1. V levém navigačním panelu stránky správy vaší aplikace vyberte Konfigurace>Obecné nastavení.

  2. Režim klientského certifikátu nastavte na Vyžadovat. Klikněte na Uložit v horní části stránky.

Pokud chcete to samé udělat s Azure CLI, spusťte v Cloud Shell následující příkaz:

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

Vyloučení cest z vyžadování ověřování

Když pro aplikaci povolíte vzájemné ověřování, všechny cesty v kořenovém adresáři vaší aplikace vyžadují klientský certifikát pro přístup. Pokud chcete tento požadavek pro určité cesty odebrat, definujte cesty vyloučení jako součást konfigurace aplikace.

  1. V levém navigačním panelu stránky správy vaší aplikace vyberte Konfigurace>Obecné nastavení.

  2. Vedle možnosti Cesty k vyloučení certifikátů klikněte na ikonu upravit.

  3. Klikněte na Nová cesta, zadejte cestu nebo seznam cest oddělených nebo ,;a klikněte na OK.

  4. Klikněte na Uložit v horní části stránky.

Na následujícím snímku obrazovky žádná cesta pro vaši aplikaci, která začíná /public na , nevyžaduje klientský certifikát. Porovnávání cest nerozlišuje velká a malá písmena.

Cesty pro vyloučení certifikátů

Přístup ke klientskému certifikátu

V App Service dojde k ukončení požadavku protokolem TLS ve front-endovém nástroji pro vyrovnávání zatížení. Při předávání požadavku do kódu aplikace s povolenými klientskými certifikáty App Service vloží do klientského certifikátu hlavičku X-ARR-ClientCert požadavku. App Service s tímto klientským certifikátem nedělá nic jiného než jeho předání do vaší aplikace. Kód vaší aplikace zodpovídá za ověření klientského certifikátu.

Pro ASP.NET je klientský certifikát k dispozici prostřednictvím vlastnosti HttpRequest.ClientCertificate .

Pro jiné zásobníky aplikací (Node.js, PHP atd.) je klientský certifikát k dispozici ve vaší aplikaci prostřednictvím hodnoty zakódované jako base64 v X-ARR-ClientCert hlavičce požadavku.

ASP.NET 5+, ukázka ASP.NET Core 3.1

Pro ASP.NET Core je k dispozici middleware, který parsuje předávané certifikáty. Pro použití předávaných hlaviček protokolu je k dispozici samostatný middleware. Aby bylo možné přijímat předávané certifikáty, musí existovat obě. Do možností CertificateAuthentication můžete umístit vlastní logiku ověření certifikátu.

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

Ukázka 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 is not 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 does NOT 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;
            }
        }
    }

ukázka Node.js

Následující Node.js vzorový kód získá hlavičku X-ARR-ClientCert a pomocí node-forge převede řetězec PEM s kódováním base64 na objekt certifikátu a ověří ho:

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

Ukázka v Javě

Následující třída Java kóduje certifikát z do X-ARR-ClientCertX509Certificate instance. certificateIsValid() ověří, že kryptografický otisk certifikátu odpovídá kryptografickému otisku zadanému v konstruktoru a že nevypršela platnost certifikátu.

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 has not expired.
     * @return True if the certificate's thumbprint matches and has not expired. False otherwise.
     */
    public boolean certificateIsValid() throws NoSuchAlgorithmException, CertificateEncodingException {
        return certificateHasNotExpired() && thumbprintIsValid();
    }

    /**
     * Check certificate's timestamp.
     * @return Returns true if the certificate has not 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;
    }
}