0

I am trying to get mutual client certification to work in Azure. I am running a web app with this configuration:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate();

        services.AddCertificateForwarding(options =>
            options.CertificateHeader = "X-ARR-ClientCert");

        services.AddHttpClient();
        services.AddControllers();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseAuthentication();
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseCertificateForwarding();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

In extension to this, I have added so the web app sends the client certificate through to my app. When I deploy it, it is fine. I have cloudflare in front and have enabled the Origin Pull, and I can validate that the client certificate is sent through. I can see that when I try to go to the web app directly on the azurewebsites.net domain, my browser is asking for a certificate. If I try to go through Cloudflare, it will show the webpage. I thought this was working, but when I check the logs, I get this:

2020-07-02 13:30:52.711 +00:00 [Information] Microsoft.AspNetCore.Hosting.Diagnostics: Request starting HTTP/1.1 GET https://[REMOVED]/api/ping

2020-07-02 13:30:52.718 +00:00 [Trace] Microsoft.AspNetCore.HostFiltering.HostFilteringMiddleware: All hosts are allowed.

2020-07-02 13:30:53.107 +00:00 [Warning] Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationHandler: Certificate validation failed, the subject was OU=Origin Pull, O="Cloudflare, Inc.", L=San Francisco, S=California, C=US.UntrustedRoot A certificate chain processed but terminated in a root certificate which is not trusted by the trusted provider. RevocationStatusUnknown The revocation function was unable to check revocation for the certificate.OfflineRevocation The revocation function was unable to check revocation because the revocation server was offline.

2020-07-02 13:30:53.107 +00:00 [Information] Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationHandler: Certificate was not authenticated. Failure message: Client certificate failed validation.

2020-07-02 13:30:53.110 +00:00 [Debug] Microsoft.AspNetCore.Routing.Matching.DfaMatcher: 1 candidate(s) found for the request path '/api/ping'

It looks like the client certificate isn't accepted. Should it be? I mean, it is Cloudflare. Am I doing something wrong in my setup? Should I install something extra on my side? I have looked through this guide here: https://support.cloudflare.com/hc/en-us/articles/204899617-Authenticated-Origin-Pulls and it doesn't mention anything about extra installation of certificates. Should I maybe install the origin-pull-ca.pem on the web app itself?

When I compare the certificate sent to me, with the origin-pull-ca.pem, the two is not equal:

  • origin-pull-ca.pem: Thumbprint: 1F5BA8DCF83E6453DD75C47780906710901AD641 (Additional info: CN=origin-pull.cloudflare.net, S=California, L=San Francisco, OU=Origin Pull, O="CloudFlare, Inc.", C=US)
  • Sent from Cloudflare: Thumbprint: A27996CBA564D24731BC76439C48920C1F7D4AA3 (Additional info: OU=Origin Pull, O="Cloudflare, Inc.", L=San Francisco, S=California, C=US)

Shouldn't they be equal?

Please note: I am not an expert into certificates, SSL, etc. I am trying to learn here :)

mslot
  • 4,959
  • 9
  • 44
  • 76

2 Answers2

1

Exactly same question I asked here https://community.cloudflare.com/t/manual-authenticated-origin-pulls-verification/145614. Dunno why, but A27996CBA564D24731BC76439C48920C1F7D4AA3 it's correct.

EDIT: Update with chain

public class CloudflareClientCertificateMiddleware
{
    private static X509Certificate2 _cloudflareOriginPullCert;
    private readonly RequestDelegate _next;

    public CloudflareClientCertificateMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        if (_cloudflareOriginPullCert == null)
            _cloudflareOriginPullCert = Helpers.CertificateHelper.GetCertificateInSpecifiedStore("origin-pull.cloudflare.net", StoreName.Root, StoreLocation.LocalMachine);

        bool isCloudflareCertificate = true;
        X509Certificate2 clientCertificate = context.Connection.ClientCertificate;

        using (X509Chain chain = new X509Chain())
        {
            chain.ChainPolicy.ExtraStore.Add(_cloudflareOriginPullCert);
            chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
            // https://stackoverflow.com/questions/6097671/how-to-verify-x509-cert-without-importing-root-cert (Azure)
            //chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;

            // https://stackoverflow.com/a/7332193
            if (clientCertificate == null || chain.Build(clientCertificate) == false)
                isCloudflareCertificate = false;
            // Make sure we have the same number of elements.
            if (isCloudflareCertificate && chain.ChainElements.Count != chain.ChainPolicy.ExtraStore.Count + 1)
                isCloudflareCertificate = false;

            // Make sure all the thumbprints of the CAs match up.
            // The first one should be 'primaryCert', leading up to the root CA.
            if (isCloudflareCertificate)
            {
                for (int i = 1; i < chain.ChainElements.Count; i++)
                {
                    if (chain.ChainElements[i].Certificate.Thumbprint != chain.ChainPolicy.ExtraStore[i - 1].Thumbprint)
                        isCloudflareCertificate = false;
                }
            }
        }

        if (isCloudflareCertificate)
            await _next.Invoke(context);
        else
            context.Response.StatusCode = StatusCodes.Status403Forbidden;
    }
}
Sauron
  • 2,156
  • 5
  • 17
  • 20
  • Do you have this running in Azure? I have tried this, but cant get it to work (my own example, it looks like we tried the same :) It would be really nice if some one from Cloudflare could answer this. I am beginning to think that their client certificate is broken, or not updated. I am currently investigating this with a VM in Azure. For a brief moment I got it to work, but I really don't think I did it correct. – mslot Jul 04 '20 at 21:01
  • Actually, my fault. The work around i (like yours), works just fine. I had some miconfigs. All tohugh I think it will not ever work on an Azure webapp, because i can't put it into the trusted root store. For that i need an VM or ASE. – mslot Jul 05 '20 at 18:14
  • Unfortunately I don't use azure but a dedicated server, but in the future I would also like to switch to azure web app. So at the moment I can't help you on this. If you can find a way to make it go directly as webapp on azure (without VM) your answer will be very useful. – Sauron Jul 06 '20 at 07:36
  • I don't think it is possible to validate the certificate properly because it has to be in the root certificate store and there is no access to this when running a plain webapp. – mslot Jul 06 '20 at 08:25
  • But just to sum up: we want a solution where we can use the builtin certificate solution provided by Microsoft. The sent certificate should match the one we install in the root store, but it doesn't. Therefore we fallback to this check that doesn't give us that much security. The cert isn't validated properly. – mslot Jul 07 '20 at 06:52
  • I have actually found out. The key is sent with the client. If you check the chain, it is there and thus you can validate it this way. New a X509Chain up, build it with the client certificate and see if the origin pull pem key exists in the chain. I have posted the solution to this as an answer. Hope you can use it. – mslot Jul 09 '20 at 09:02
  • I have no idea if this is a good and secure way of doing it :) – mslot Jul 10 '20 at 13:10
  • Chain method is correct. Check the updated code, it work and security look fine based on https://stackoverflow.com/a/7332193. But I have no idea how to verify it using a fake client certificate – Sauron Jul 11 '20 at 11:15
  • Technically if we could get the origin-pull-ca.pem signed by the CA, we could be able to access the endpoint guarded by this. Or am I wrong? It looks like CF has a CA only for client certificate, so it might be impossible. – mslot Jul 11 '20 at 13:57
  • Mmm maybe you should ask it at Cloudflare forum community, they will certainly be able to give you more specific information about that :) surely I totally trust Cloudflare – Sauron Jul 11 '20 at 16:33
  • I trust them to. I just want to be sure that a bad person doesn't get the knowledge that i use Cloudflare, then orders a certificate from CF, signed, so he can circumvent the proxy, undermining the WAF and throttling. Your code works on Azure to! Just tested. – mslot Jul 12 '20 at 11:58
1

Basically I can do this, to validate the chain

   private bool VerifyCertificate(X509Certificate2 client, ILogger<Startup> logger)
    {
        X509Chain chain = new X509Chain();

        var authority = GetInstalledCert();

        chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
        chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;

        if (!chain.Build(client))
            return false;

        var valid = chain.ChainElements
            .Cast<X509ChainElement>()
            .Any(x => x.Certificate.Thumbprint == authority.Thumbprint);

        if (!valid)
            return false;

        return true;
    }

    private X509Certificate2 GetInstalledCert()
    {
        X509Certificate2 cert = null;
        X509Store certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        certStore.Open(OpenFlags.ReadOnly);
        X509Certificate2Collection certCollection = certStore.Certificates.Find(
                                   X509FindType.FindByThumbprint,
                                   "1F5BA8DCF83E6453DD75C47780906710901AD641",
                                   false);

        if (certCollection.Count > 0)
        {
            cert = certCollection[0];
        }

        certStore.Close();

        return cert;
    }

    private X509Certificate2 GetClientCert(IHeaderDictionary headers)
    {
        var certHeader = headers["X-ARR-ClientCert"];

        if (certHeader.Any())
        {
            byte[] clientCertBytes = Convert.FromBase64String(certHeader);
            var certificate = new X509Certificate2(clientCertBytes);

            return certificate;
        }

        return null;
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.Use(async (context, next) =>
        {
            var clientCert = GetClientCert(context.Request.Headers);

            bool verify = VerifyCertificate(clientCert, logger);

            if(verify)
            {
                await next.Invoke();
            }
            else
            {
                context.Response.StatusCode = 404;
            }
        });


        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
mslot
  • 4,959
  • 9
  • 44
  • 76