🗒️

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

2024/11/04に公開

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

前記事で、Azure Functions接続を相互認証にして、認証と認可を実装しました。
ただ、ちょっと気になることが。
コードにクライアント証明書を配列で持っていて、ルート証明書が一致するかどうかをループしてチェックしています。この実装方法だと、認証するクライアントの台数が増えると、処理時間も増加してしまいます。
そこで、クライアント証明書を自己署名から自己CAによる署名に変更してみましょう。

1. CA証明書とCA秘密鍵を生成

opensslで、CA秘密鍵を生成してから、CA証明書(自己署名)を作ります。自己署名のクライアント証明書を作る方法と一緒ですね。

openssl genrsa 2048 > ca.key
openssl req -new -key ca.key > ca.csr
openssl x509 -req -days 365 -signkey ca.key < ca.csr > ca.crt

2. クライアント証明書とクライアント秘密鍵を生成

opensslで、クライアント秘密鍵を生成してから、クライアント証明書(CA署名)を作ります。
opensslの署名に、先に作ったCA証明書をCA秘密鍵を使うところがポイントです。

openssl genrsa 2048 > client.key
openssl req -new -key client.key > client.csr
openssl x509 -req -days 365 -CA ca.crt -CAkey ca.key -CAcreateserial < client.csr > client.crt

3. クライアントの認可

VerifyClientCertificate(string?)rootsBase64に、CA証明書を加えます。

string[] rootsBase64 = {
    //"MIIC+TCCAeECFB4iFGrzB7C5viT5SnbztEknJ/a8MA0GCSqGSIb3DQEBCwUAMDkxCzAJBgNVBAYTAkpQMRMwEQYDVQQIDApLaXRhbmFnb3lhMRUwEwYDVQQKDAxtYXRzdWppcnVzaGkwHhcNMjQxMDI2MDI1MTU2WhcNMzQxMDI0MDI1MTU2WjA5MQswCQYDVQQGEwJKUDETMBEGA1UECAwKS2l0YW5hZ295YTEVMBMGA1UECgwMbWF0c3VqaXJ1c2hpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqPZDzSOc51p7nHmzzxm35pYlbimr4UtvQaXGBuTB4ESlvOspY5bOGZKWlkcLOseNDvVskiF4qkI6fsAPTE0OVit33oj4jArvb9h7o3HpPL6sn/WPcazkz14mkpnqIy1fZ+iM21b3CTJkxTYV7waesGVEZq1yD9mt+UvMpB4Dkz61Dbq6p3hq1QQsCiOL93xlDHq3IYTyQQbR4NwXASImpZRsLrqMV0D/FryiMsdsCW8XBnPhtMj7/11tozwDfkmrr2bPsy64f2Uvr//bdOsOTU/F6Dpf6Tp8rJYeLAlgmxN482rgxr6afhSPRDfc430MSATPMuvVL0Wu86s/v2Ur1wIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBSbGn2RTmjPgMHj6uub0JKbzny2Z4Wj4HhCSv/WrZVl+BcKmyae8u1qLnx9OyM5nO7WXs0Q76QzVl/nzkafML3CPEumIiGVxQ/tDjxamkH0nODG3EGMUXXn17i/gicI+tOHJXHHeIADtVdWiwukKJN5li4y9KflelcV4Ebi8mTWXfRaLvAE31thgAU1tV4yqqn8dzQ3u1escXaC68AyCPpF3cUXxiwf7A58DcLI7zw9ITDM75O48Mt6gm0Mg/BFKkRiQ2nMLUGTQ1pECh7ulujYxDKMJkgcT0+0qwM7Mw70ZWGaY1YXIZ+B0An09E54CicnCSK82fiCZxeCce+3AXR",
    //"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",
};

4. curlでアクセス

クライアント証明書(CA署名)でアクセスするとSuccess.となりました。

$ curl --key ./client.key --cert ./client.crt https://functionapp000000000000000.azurewebsites.net/api/HttpExample
Success.

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",
                "MIIDEzCCAfsCFCs2IgC0Q5nSrZMOxih1mue+OTTSMA0GCSqGSIb3DQEBCwUAMEYxCzAJBgNVBAYTAkpQMRMwEQYDVQQIDApLaXRhbmFnb3lhMRUwEwYDVQQKDAxtYXRzdWppcnVzaGkxCzAJBgNVBAMMAmNhMB4XDTI0MTEwMzExNTQ1M1oXDTI1MTEwMzExNTQ1M1owRjELMAkGA1UEBhMCSlAxEzARBgNVBAgMCktpdGFuYWdveWExFTATBgNVBAoMDG1hdHN1amlydXNoaTELMAkGA1UEAwwCY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzGVzEuoercvdIc/6dd2VmEIb5XNn88j3Px/ZjtYlhFneRRrVshwzzeexsp3IUt9sBdDsfKaMtwHdtbvHTWXBoiwwBeB+bLamluDuDZEiFpgNh5/p7YHfEfY0Cv/2XcEn62Mb67RbHsUQYk7lDJA2zrZ0xhCJGpXEyES1GJPIeZU9cGe4VYMn3E7goKdhQL8Rq1E8vMgZJGecGSdqyX2AcPk7GCnr51rzEd5plK4KckqPREo2Cg5N4Jt8o5ad5cU7/ow5RakRG/SLFZ+F7Ot6e+Z13xABYbfyxiGSNlEGdUnPDbJ6pXEua4OopU4VUnvxXOopmo2r0zCfOt7DHXC6ZAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACys+SI1Bt8LZ7GytPol4V63ZP/60gfpKE7dPjd8c5p/nPmsw2fIpIchgITfnIruwOS8fbYau70WaH199MloKbeOHaKJ9Aj1qr9wmneheqZkaoHHC1mkC7tdlLEftMxokJ2P609oo6SHthqAMMBtZdXFCo9MZDmh+75hIEVdnZUF+h/mD/cQUqvRFZ5GJVc4VbyClm+E1IXL7VryFQER5p1WDwOo3bta1ujxWCzgGa4Mgjeqrx5ntdhkbLsMWb/COmXH1LzvZncEbOhHAivSjH+0zAPThKhBuKYCNehxxbQ3p316pYKmByq9FhuxBTK0Y8uxcktd59MK1FT4YtTWNuc=",
            };

            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