🗒️

Azure Functionsクライアント認証の認可コード(自己署名編)

2024/10/31に公開

これらの記事の一部です。

前記事で、Azure Functions接続を相互認証にして、クライアント証明書を自コードで取れるところまで確認しました。
それでは、特定のクライアント(特定のクライアント証明書を提示)だけの接続を認可するように変更しましょう。

1. クライアント証明書の取得

req.Headers["X-ARR-ClientCert"]で取得します。

var clientCert = req.Headers["X-ARR-ClientCert"];

2. クライアントの認可

とりあえず、VerifyClientCertificate(string?)に判断を委ねます。

if (!VerifyClientCertificate(clientCert)) return new UnauthorizedObjectResult("Invalid client certificate.");
return new OkObjectResult("Success.");

VerifyClientCertificate(string?)では、X509Chainクラスを使って、証明書の諸々なチェック(有効期限とか)と、証明書チェーンを構築します。そのときに、あらかじめ証明書ストアにクライアント証明書を加えておきます。

X509Chain chain = new X509Chain();

// 証明書ストアにクライアント証明書を加える
string[] rootsBase64 = {
    "MIIC+TCCAeECFB4iFGrzB7C5viT5SnbztEknJ/a8MA0GCSqGSIb3DQEBCwUAMDkxCzAJBgNVBAYTAkpQMRMwEQYDVQQIDApLaXRhbmFnb3lhMRUwEwYDVQQKDAxtYXRzdWppcnVzaGkwHhcNMjQxMDI2MDI1MTU2WhcNMzQxMDI0MDI1MTU2WjA5MQswCQYDVQQGEwJKUDETMBEGA1UECAwKS2l0YW5hZ295YTEVMBMGA1UECgwMbWF0c3VqaXJ1c2hpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqPZDzSOc51p7nHmzzxm35pYlbimr4UtvQaXGBuTB4ESlvOspY5bOGZKWlkcLOseNDvVskiF4qkI6fsAPTE0OVit33oj4jArvb9h7o3HpPL6sn/WPcazkz14mkpnqIy1fZ+iM21b3CTJkxTYV7waesGVEZq1yD9mt+UvMpB4Dkz61Dbq6p3hq1QQsCiOL93xlDHq3IYTyQQbR4NwXASImpZRsLrqMV0D/FryiMsdsCW8XBnPhtMj7/11tozwDfkmrr2bPsy64f2Uvr//bdOsOTU/F6Dpf6Tp8rJYeLAlgmxN482rgxr6afhSPRDfc430MSATPMuvVL0Wu86s/v2Ur1wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBSbGn2RTmjPgMHj6uub0JKbzny2Z4Wj4HhCSv/WrZVl+BcKmyae8u1qLnx9OyM5nO7WXs0Q76QzVl/nzkafML3CPEumIiGVxQ/tDjxamkH0nODG3EGMUXXn17i/gicI+tOHJXHHeIADtVdWiwukKJN5li4y9KflelcV4Ebi8mTWXfRaLvAE31thgAU1tV4yqqn8dzQ3u1escXaC68AyCPpF3cUXxiwf7A58DcLI7zw9ITDM75O48Mt6gm0Mg/BFKkRiQ2nMLUGTQ1pECh7ulujYxDKMJkgcT0+0qwM7Mw70ZWGaY1YXIZ+B0An09E54CicnCSK82fiCZxeCce+3AXR",
    "MIIC+TCCAeECFGWrDPrZdhFMTGdn1fQMsTyjqnUjMA0GCSqGSIb3DQEBCwUAMDkxCzAJBgNVBAYTAkpQMRMwEQYDVQQIDApLaXRhbmFnb3lhMRUwEwYDVQQKDAxtYXRzdWppcnVzaGkwHhcNMjQxMDI2MTMwOTA1WhcNMjUxMDI2MTMwOTA1WjA5MQswCQYDVQQGEwJKUDETMBEGA1UECAwKS2l0YW5hZ295YTEVMBMGA1UECgwMbWF0c3VqaXJ1c2hpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtVgZIqvL0kQ9r2b02eF6G51SceL+oP3VBOAKC71F9gg6jjV6IbA/E3wMVnf3AZ1uGzvlWhTg/aNw5wXzuoN+Z1KT7r8iEQ3s68WZ+3DKIZOQyZ+U2q40PqUesTS50HKxGwC0dATzAi2m9y8UgjkbGqGGjCowSeygWVCcWkb5zV7NZC8TAJDXVxOfWW21JKHxrGuM9WjEcoH67FQhFWHzEcDU/T+YofFzlNQCc9IleaXFzDjJ4wlOTlccoADbCKAOnXvrW/cV2YmCPYDXOovdfaG1GMlx5Of+WbfYj9VbLKgDGIWamy/FmTrsWdM1LhWlFE1e44DVPzBm4fDH/PFCCQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBDbNaSpUvFRnyCvKnssblXeCkEMeB9ewpTDOTkmv9sttax2ZfzCc/afLbJv1drpgWmg/vFinBMxUBzjopKWqRIVL8yHe8AkFZ9Ihkj4EFAw2kQgIDlZgkc4FrgeqDVtD9cRJniEHocXOudbSBFZwDQikYUkeSjknV2sfspPNX26Eo0CMuiU2FeTiGXj4K9zBx59I8Ewfz9fxZUouiJxOd0X9WzKnjXbB48RqfSfYObZyEXQIS0LDcRn8X/3LDgGGBOf31dAWMpCYERACf3nvfjvXmK2eer8asQgTpuZdpZN7KLfUj96bxYjAvL3NDMMcv4l9VwQ0CrhcOv+9P7j1ys",
};
foreach (var rootBase64 in rootsBase64)
{
    chain.ChainPolicy.ExtraStore.Add(new X509Certificate2(Convert.FromBase64String(rootBase64)));
}

// 証明書チェーンを構築
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
if (!chain.Build(new X509Certificate2(Convert.FromBase64String(clientBase64)))) return false;

最後に、証明書チェーンのルート証明書が、今回認可するものか否かチェックします。

var chainRoot = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;

foreach (var root in chain.ChainPolicy.ExtraStore)
{
    if (chainRoot.RawData.SequenceEqual(root.RawData)) return true;
}
return false;

3. curlでアクセス

認可されたクライアント証明書(client.key, client2.key)のときはSuccess.で、認可されていないクライアント証明書(client3.key)のときはInvalid client certificate.となりました。

$ curl --key ./client.key --cert ./client.crt https://functionapp000000000000000.azurewebsites.net/api/HttpExample
Success.
$ curl --key ./client2.key --cert ./client2.crt https://functionapp000000000000000.azurewebsites.net/api/HttpExample
Success.
$ curl --key ./client3.key --cert ./client3.crt https://functionapp000000000000000.azurewebsites.net/api/HttpExample
Invalid client certificate.ubuntu

Azure Functionsの完全なコード

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using System.Security.Cryptography.X509Certificates;

namespace FunctionApp1
{
    public class HttpExample
    {
        private readonly ILogger<HttpExample> _logger;

        public HttpExample(ILogger<HttpExample> logger)
        {
            _logger = logger;
        }

        [Function("HttpExample")]
        public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
        {
            var clientCert = req.Headers["X-ARR-ClientCert"];
            _logger.LogInformation($"ClientCert={clientCert}");

            if (!VerifyClientCertificate(clientCert)) return new UnauthorizedObjectResult("Invalid client certificate.");

            return new OkObjectResult("Success.");
        }

        private bool VerifyClientCertificate(string? clientBase64)
        {
            string[] rootsBase64 = {
                "MIIC+TCCAeECFB4iFGrzB7C5viT5SnbztEknJ/a8MA0GCSqGSIb3DQEBCwUAMDkxCzAJBgNVBAYTAkpQMRMwEQYDVQQIDApLaXRhbmFnb3lhMRUwEwYDVQQKDAxtYXRzdWppcnVzaGkwHhcNMjQxMDI2MDI1MTU2WhcNMzQxMDI0MDI1MTU2WjA5MQswCQYDVQQGEwJKUDETMBEGA1UECAwKS2l0YW5hZ295YTEVMBMGA1UECgwMbWF0c3VqaXJ1c2hpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqPZDzSOc51p7nHmzzxm35pYlbimr4UtvQaXGBuTB4ESlvOspY5bOGZKWlkcLOseNDvVskiF4qkI6fsAPTE0OVit33oj4jArvb9h7o3HpPL6sn/WPcazkz14mkpnqIy1fZ+iM21b3CTJkxTYV7waesGVEZq1yD9mt+UvMpB4Dkz61Dbq6p3hq1QQsCiOL93xlDHq3IYTyQQbR4NwXASImpZRsLrqMV0D/FryiMsdsCW8XBnPhtMj7/11tozwDfkmrr2bPsy64f2Uvr//bdOsOTU/F6Dpf6Tp8rJYeLAlgmxN482rgxr6afhSPRDfc430MSATPMuvVL0Wu86s/v2Ur1wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBSbGn2RTmjPgMHj6uub0JKbzny2Z4Wj4HhCSv/WrZVl+BcKmyae8u1qLnx9OyM5nO7WXs0Q76QzVl/nzkafML3CPEumIiGVxQ/tDjxamkH0nODG3EGMUXXn17i/gicI+tOHJXHHeIADtVdWiwukKJN5li4y9KflelcV4Ebi8mTWXfRaLvAE31thgAU1tV4yqqn8dzQ3u1escXaC68AyCPpF3cUXxiwf7A58DcLI7zw9ITDM75O48Mt6gm0Mg/BFKkRiQ2nMLUGTQ1pECh7ulujYxDKMJkgcT0+0qwM7Mw70ZWGaY1YXIZ+B0An09E54CicnCSK82fiCZxeCce+3AXR",
                "MIIC+TCCAeECFGWrDPrZdhFMTGdn1fQMsTyjqnUjMA0GCSqGSIb3DQEBCwUAMDkxCzAJBgNVBAYTAkpQMRMwEQYDVQQIDApLaXRhbmFnb3lhMRUwEwYDVQQKDAxtYXRzdWppcnVzaGkwHhcNMjQxMDI2MTMwOTA1WhcNMjUxMDI2MTMwOTA1WjA5MQswCQYDVQQGEwJKUDETMBEGA1UECAwKS2l0YW5hZ295YTEVMBMGA1UECgwMbWF0c3VqaXJ1c2hpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtVgZIqvL0kQ9r2b02eF6G51SceL+oP3VBOAKC71F9gg6jjV6IbA/E3wMVnf3AZ1uGzvlWhTg/aNw5wXzuoN+Z1KT7r8iEQ3s68WZ+3DKIZOQyZ+U2q40PqUesTS50HKxGwC0dATzAi2m9y8UgjkbGqGGjCowSeygWVCcWkb5zV7NZC8TAJDXVxOfWW21JKHxrGuM9WjEcoH67FQhFWHzEcDU/T+YofFzlNQCc9IleaXFzDjJ4wlOTlccoADbCKAOnXvrW/cV2YmCPYDXOovdfaG1GMlx5Of+WbfYj9VbLKgDGIWamy/FmTrsWdM1LhWlFE1e44DVPzBm4fDH/PFCCQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBDbNaSpUvFRnyCvKnssblXeCkEMeB9ewpTDOTkmv9sttax2ZfzCc/afLbJv1drpgWmg/vFinBMxUBzjopKWqRIVL8yHe8AkFZ9Ihkj4EFAw2kQgIDlZgkc4FrgeqDVtD9cRJniEHocXOudbSBFZwDQikYUkeSjknV2sfspPNX26Eo0CMuiU2FeTiGXj4K9zBx59I8Ewfz9fxZUouiJxOd0X9WzKnjXbB48RqfSfYObZyEXQIS0LDcRn8X/3LDgGGBOf31dAWMpCYERACf3nvfjvXmK2eer8asQgTpuZdpZN7KLfUj96bxYjAvL3NDMMcv4l9VwQ0CrhcOv+9P7j1ys",
            };

            if (clientBase64 == null) return false;

            X509Chain chain = new X509Chain();
            foreach (var rootBase64 in rootsBase64)
            {
                chain.ChainPolicy.ExtraStore.Add(new X509Certificate2(Convert.FromBase64String(rootBase64)));
            }

            chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
            chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
            if (!chain.Build(new X509Certificate2(Convert.FromBase64String(clientBase64)))) return false;

            var chainRoot = chain.ChainElements[chain.ChainElements.Count - 1].Certificate;
            foreach (var root in chain.ChainPolicy.ExtraStore)
            {
                if (chainRoot.RawData.SequenceEqual(root.RawData)) return true;
            }
            return false;
        }
    }
}

Discussion