تكوين مصادقة TLS المتبادلة لخدمة Azure App Service

يمكنك تقييد الوصول إلى تطبيق Azure App Service من خلال تمكين أنواع مختلفة من المصادقة له. إحدى الطرق للقيام بذلك هو طلب شهادة عميل عند طلب العميل عبر TLS/SSL والتحقق من صحة الشهادة. تسمى هذه الآلية مصادقة TLS المتبادلة أو مصادقة شهادة العميل. توضح هذه المقالة كيفية إعداد تطبيقك لاستخدام مصادقة شهادة العميل.

إشعار

رمز التطبيق الخاص بك مسؤول عن التحقق من صحة شهادة العميل. لا تقوم App Service بأي شيء باستخدام شهادة العميل هذه بخلاف إعادة توجيهها إلى تطبيقك.

في حال الوصول إلى موقعك عبر HTTP وليس HTTPS، لن تحصل على أي شهادة عميل. حتى إذا كان التطبيق الخاص بك يتطلب شهادات العميل، يجب ألّا تسمح طلبات إلى التطبيق الخاص بك عبر HTTP.

إعداد تطبيق الويب لديك

لإنشاء روابط TLS/SSL مخصصة أو تمكين شهادات العميل لتطبيق App Service، يجب أن تكون خطة App Service في المستوى الأساسي أو القياسي أو المتميز أو المعزول. تأكد من أن تطبيق الويب الخاص بك في المستوى السعري المدعوم، في الخطوات التالية:

اذهب إلى تطبيق الويب

  1. في مربع البحث في مدخل Azure، ابحث عن App Services وحددها.

    لقطة شاشة لتحديد مدخل Azure، مربع البحث، وApp Services.

  2. في صفحة App Services، حدد اسم تطبيق الويب الخاص بك.

    يُرجى أخذ لقطة شاشة لصفحة App Services في مدخل Azure حيث عرض قائمة بجميع تطبيقات الويب قيد التشغيل، وتمييز التطبيق الأول في القائمة.

    أنت الآن على صفحة إدارة تطبيق الويب الخاص بك.

تحقق من مستوى التسعير

  1. في القائمة اليسرى لتطبيق الويب، ضمن قسم الإعدادات، حدد توسيع (خطة App Service).

    لقطة شاشة لقائمة تطبيق الويب، مع تحديد قسم

  2. تأكد من أن تطبيق الويب الخاص بك ليس في المستوى F1 أو D1، والذي لا يدعم TLS/SSL المخصص.

  3. إذا كنت بحاجة إلى توسيع النطاق، يُرجى الالتزام بالخطوات الواردة في القسم التالي. وإلا، أغلق صفحة توسيع النطاق وتخطى قسم توسيع نطاق خطتك في App Service.

يمكنك توسيع نطاق خطتك في App Service

  1. حدد أي مستوى غير مجاني، مثل B1 أو B2 أو B3 أو أي مستوى آخر في فئة الإنتاج.

  2. عندما تنتهي، حدد Select.

    عند ظهور الرسالة التالية، اكتملت عملية التوسيع.

    لقطة شاشة بها رسالة تأكيد على عملية الترقية.

تمكين شهادات العميل

لإعداد تطبيقك للمطالبة بشهادات العميل:

  1. من شريط التنقل الأيسر في صفحة إدارة التطبيق لديك، حدد Configuration>General Settings.

  2. حدد وضع شهادة العميل الذي تختاره. حدد حفظ في الجزء العلوي من الصفحة.

أوضاع شهادة العميل ‏‏الوصف
مطلوب تتطلب جميع الطلبات شهادة عميل.
اختياري قد تستخدم الطلبات شهادة عميل أو لا تستخدمها. ستتم مطالبة العملاء بشهادة بشكل افتراضي. على سبيل المثال، سيعرض عملاء المستعرض مطالبة بتحديد شهادة للمصادقة.
مستخدم تفاعلي اختياري قد تستخدم الطلبات شهادة عميل أو لا تستخدمها. لن تتم مطالبة العملاء بشهادة بشكل افتراضي. على سبيل المثال، لن يعرض عملاء المستعرض مطالبة بتحديد شهادة للمصادقة.

للقيام بنفس الشيء مع Azure CLI، قم بتشغيل الأمر التالي في Cloud Shell:

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

استبعاد المسارات من طلب المصادقة

عندما تقوم بتمكين المصادقة المتبادلة لتطبيقك، فإن جميع المسارات الموجودة ضمن جذر التطبيق تتطلب شهادة عميل للوصول. لإزالة هذا المطلب لبعض المسارات، حدد مسارات الاستبعاد كجزء من تكوين التطبيق الخاص بك.

  1. من شريط التنقل الأيسر في صفحة إدارة التطبيق لديك، حدد Configuration>General Settings.

  2. بجوار مسارات استبعاد الشهادة، حدد أيقونة التحرير.

  3. حدد مسار جديد، وحدد مسارا، أو قائمة مسارات مفصولة ب , أو ;، وحدد موافق.

  4. حدد حفظ في الجزء العلوي من الصفحة.

في لقطة الشاشة التالية، لا يطلب أي مسار لتطبيقك يبدأ ب /public شهادة عميل. مطابقة المسار غير حساسة لحالة الأحرف.

مسارات استثناء الشهادة

الوصول إلى شهادة العميل

في خدمة التطبيقات، يحدث إنهاء TLS للطلب في موازن التحميل الأمامي. عندما تقوم App Service بإعادة توجيه الطلب إلى التعليمات البرمجية للتطبيق الخاص بك مع تمكين شهادات العميل، فإنها تقوم بإدخال X-ARR-ClientCert عنوان طلب مع شهادة العميل. لا تقوم App Service بأي شيء باستخدام شهادة العميل هذه بخلاف إعادة توجيهها إلى تطبيقك. رمز التطبيق الخاص بك مسؤول عن التحقق من صحة شهادة العميل.

بالنسبة إلى ASP.NET، تتوفر شهادة العميل من خلال خاصيةHttpRequest.ClientCertificate.

بالنسبة إلى حزم التطبيقات الأخرى (Node.js وPHP وما إلى ذلك)، تتوفر شهادة العميل في تطبيقك من خلال القيمة المشفرة base64 في رأس الطلب X-ARR-ClientCert.

عينة ASP.NET Core

بالنسبة لـ ASP.NET Core، يتم توفير البرامج الوسيطة لتحليل الشهادات المعاد توجيهها. يتم توفير البرامج الوسيطة المنفصلة لاستخدام رؤوس البروتوكول المعاد توجيهها. يجب أن يكون كلاهما حاضراً حتى يتم قبول الشهادات المعاد توجيهها. يمكنك وضع منطق مخصص للتحقق من صحة الشهادة في خيارات 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?}");
        });
    }
}

نموذج 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;
            }
        }
    }

عينة Node.js

يحصل نموذج تعليمة برمجية Node.js التالي على رأس X-ARR-ClientCert ويستخدم node-forge لتحويل سلسلة PEM المشفرة بـ base64 إلى عنصر شهادة والتحقق من صحتها:

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

تقوم فئة Java التالية بترميز الشهادة من X-ARR-ClientCert إلى مثيل X509Certificate. certificateIsValid() التحقق من أن بصمة الإبهام الخاصة بالشهادة تطابق بصمة الإبهام المحددة في الدالة الإنشائية وأن هذه الشهادة لم تنته صلاحيتها.

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

تنفذ نماذج التعليمات البرمجية Flask و Django Python التالية مصمما باسم authorize_certificate يمكن استخدامه في دالة عرض للسماح بالوصول فقط إلى المتصلين الذين يقدمون شهادة عميل صالحة. يتوقع شهادة بتنسيق PEM في X-ARR-ClientCert العنوان ويستخدم حزمة تشفير Python للتحقق من صحة الشهادة استنادا إلى بصمة الإبهام الخاصة بها والاسم الشائع للموضوع والاسم الشائع للمصدر وتواريخ البدء وانتهاء الصلاحية. إذا فشل التحقق من الصحة، يضمن مصمم الديكور إرجاع استجابة HTTP مع رمز الحالة 403 (ممنوع) إلى العميل.

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

يوضح مقتطف التعليمات البرمجية التالي كيفية استخدام مصمم الديكور على دالة عرض Flask.

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