Megosztás a következőn keresztül:


A TLS kölcsönös hitelesítésének konfigurálása az Azure App Service számára

Korlátozhatja az Azure App Service-alkalmazás hozzáférését, ha különböző típusú hitelesítéseket engedélyez hozzá. Ennek egyik módja az, ha ügyféltanúsítványt kér, amikor az ügyfélkérés TLS-en/SSL-en keresztül történik, és hitelesíti a tanúsítványt. Ennek a mechanizmusnak a neve TLS kölcsönös hitelesítés vagy ügyfél-tanúsítványos hitelesítés. Ez a cikk bemutatja, hogyan állíthatja be az alkalmazást ügyfél-tanúsítványos hitelesítés használatára.

Feljegyzés

Ha HTTP-en keresztül éri el a webhelyet, és nem HTTPS-en keresztül, akkor nem kap ügyféltanúsítványt. Ezért ha az alkalmazás ügyféltanúsítványokat igényel, ne engedélyezze az alkalmazáshoz http-en keresztüli kéréseket.

A webalkalmazás előkészítése

Egyéni TLS-/SSL-kötések létrehozásához vagy az App Service-alkalmazás ügyféltanúsítványainak engedélyezéséhez az App Service-csomagnak alapszintű, standard, prémium vagy izolált szinten kell lennie. Annak érdekében, hogy a webalkalmazás a támogatott tarifacsomagban legyen, kövesse az alábbi lépéseket:

Ugrás a webalkalmazásra

  1. Az Azure Portal keresőmezőjében keresse meg és válassza ki az App Servicest.

    Képernyőkép az Azure Portalról, a keresőmezőről és az

  2. Az App Services lapon válassza ki a webalkalmazás nevét.

    Képernyőkép az Azure Portal App Services-oldaláról, amelyen az összes futó webalkalmazás listája látható, és az első alkalmazás ki van emelve a listában.

    Most már a webalkalmazás felügyeleti oldalán van.

A tarifacsomag ellenőrzése

  1. A webalkalmazás bal oldali menüjében, a Beállítások szakaszban válassza a Felskálázás (App Service-csomag) lehetőséget.

    Képernyőkép a webalkalmazás menüjéről, a

  2. Győződjön meg arról, hogy a webalkalmazás nem az F1 vagy D1 szinten van, amely nem támogatja az egyéni TLS/SSL protokollt.

  3. Ha vertikális felskálázásra van szüksége, kövesse az alábbi szakaszban található lépéseket. Ellenkező esetben zárja be a Felskálázás lapot, és hagyja ki az App Service-csomag vertikális felskálázása szakaszt.

Az App Service-csomag vertikális felskálázása

  1. Válasszon ki egy nem ingyenes szintet, például B1, B2, B3 vagy bármely más szintet az Éles kategóriában.

  2. Ha elkészült, válassza a Kiválasztás lehetőséget.

    Amikor megjelenik a következő üzenet, a méretezési művelet befejeződött.

    Képernyőkép a vertikális felskálázási művelet megerősítéséről szóló üzenettel.

Ügyféltanúsítványok engedélyezése

Az alkalmazás beállítása ügyféltanúsítványok megkövetelésére:

  1. Az alkalmazás felügyeleti oldalának bal oldali navigációs sávján válassza a Konfiguráció>általános beállításai lehetőséget.

  2. Állítsa be az ügyféltanúsítvány módját kötelezőre. Válassza a Lap tetején található Mentés lehetőséget.

Az Azure CLI-vel ugyanezt a következő parancs futtatásával teheti meg a Cloud Shellben:

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

Elérési utak kizárása hitelesítés megkövetelése alól

Amikor engedélyezi az alkalmazás kölcsönös hitelesítését, az alkalmazás gyökérkönyvtárában lévő összes elérési úthoz ügyféltanúsítvány szükséges a hozzáféréshez. Bizonyos útvonalakra vonatkozó követelmény eltávolításához definiáljon kizárási útvonalakat az alkalmazáskonfiguráció részeként.

  1. Az alkalmazás felügyeleti oldalának bal oldali navigációs sávján válassza a Konfiguráció>általános beállításai lehetőséget.

  2. A tanúsítványkizárási útvonalak mellett válassza a szerkesztés ikont.

  3. Válassza az Új elérési út lehetőséget, adjon meg egy elérési utat vagy az elérési utak , listáját, és ;válassza az OK gombot.

  4. Válassza a Lap tetején található Mentés lehetőséget.

Az alábbi képernyőképen az alkalmazás minden olyan elérési útja, amely elindul /public , nem kér ügyféltanúsítványt. Az elérési út egyeztetése nem érzékeny a kis- és nagybetűkre.

Tanúsítványkizárási útvonalak

Ügyféltanúsítvány elérése

Az App Service-ben a kérés TLS-leállítása az előtérbeli terheléselosztónál történik. Amikor az App Service az ügyféltanúsítványok engedélyezésével továbbítja a kérést az alkalmazáskódnak, egy kérelemfejlécet szúr be az ügyféltanúsítványbaX-ARR-ClientCert. Az App Service nem tesz semmit ezzel az ügyféltanúsítvánnyal, csak azt továbbítja az alkalmazásnak. Az alkalmazáskód felelős az ügyféltanúsítvány érvényesítéséért.

Az ASP.NET esetében az ügyféltanúsítvány a HttpRequest.ClientCertificate tulajdonságon keresztül érhető el.

Más alkalmazásveremek (Node.js, PHP stb.) esetében az ügyfél-tanúsítvány a kérelem fejlécében található base64 kódolású értéken keresztül érhető el az X-ARR-ClientCert alkalmazásban.

ASP.NET Core-minta

A ASP.NET Core esetében a közbenső szoftver biztosítja a továbbított tanúsítványok elemzését. A továbbított protokollfejlécek használatához külön köztes szoftver van megadva. A továbbított tanúsítványok elfogadásához mindkettőnek jelen kell lennie. Az egyéni tanúsítványérvényesítési logikát a CertificateAuthentication beállításai között helyezheti el.

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

ASP.NET WebForms-minta

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

Node.js minta

A következő Node.js mintakód lekéri a X-ARR-ClientCert fejlécet, és csomópont-kovácsolás használatával konvertálja a base64 kódolású PEM-sztringet tanúsítványobjektummá, és érvényesíti azt:

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

Java-minta

Az alábbi Java-osztály egy példányra X-ARR-ClientCert kódolja a tanúsítványt X509Certificate . certificateIsValid() ellenőrzi, hogy a tanúsítvány ujjlenyomata megegyezik-e a konstruktorban megadott ujjlenyomattal, és hogy a tanúsítvány még nem járt le.

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

Python-minta

Az alábbi Flask- és Django Python-kódminták egy olyan dekoratőrt authorize_certificate implementálnak, amely egy nézetfüggvényen használható, hogy csak érvényes ügyféltanúsítványt beadó hívók számára engedélyezze a hozzáférést. Egy PEM formátumú tanúsítványt vár a X-ARR-ClientCert fejlécben, és a Python-titkosítási csomag használatával ellenőrzi a tanúsítványt az ujjlenyomata (ujjlenyomata), a tulajdonos közös neve, a kiállító közös neve, valamint a kezdő és a lejárati dátum alapján. Ha az ellenőrzés sikertelen, a dekorátor biztosítja, hogy a 403-at (Tiltott) tartalmazó HTTP-választ adja vissza az ügyfélnek.

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

Az alábbi kódrészlet bemutatja, hogyan használható a dekorátor egy Flask nézetfüggvényen.

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