🗒️
Azure Functionsクライアント認証の認可コード(自己署名編)
これらの記事の一部です。
- クライアント認証でAzure Functionsに接続する
- Azure Functionsクライアント認証の認可コード(自己署名編)
- Azure Functionsクライアント認証の認可コード(自己CA署名編)
前記事で、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 = {
"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",
};
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 = {
"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",
};
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