ASP.NET Core에서 인증서 인증 구성

Microsoft.AspNetCore.Authentication.Certificate에는 ASP.NET Core의 인증서 인증과 유사한 구현이 포함되어 있습니다. 인증서 인증은 ASP.NET Core에 도달하기 훨씬 전에 TLS 수준에서 수행됩니다. 보다 정확하게 말하면, 인증서의 유효성을 검사한 다음, 해당 인증서를 ClaimsPrincipal로 확인할 수 있는 이벤트를 제공하는 인증 처리기입니다.

IIS, Kestrel, Azure Web Apps 또는 사용 중인 어떤 것이든 인증서 인증을 위해 서버를 구성해야 합니다.

프록시 및 부하 분산 장치 시나리오

인증서 인증은 프록시 또는 부하 분산 장치가 클라이언트와 서버 간의 트래픽을 처리하지 않는 경우 주로 사용되는 상태 비저장 시나리오입니다. 프록시 또는 부하 분산 장치를 사용하는 경우 인증서 인증은 프록시 또는 부하 분산 장치가 다음을 수행하는 경우에만 작동합니다.

  • 인증을 처리합니다.
  • 사용자 인증 정보를 인증 정보에 작용하는 앱(예: 요청 헤더)에 전달합니다.

프록시 및 부하 분산 장치가 사용되는 환경에서 인증서 인증 대신 OIDC(OpenID Connect)가 있는 ADFS(Active Directory Federated Services)를 사용할 수 있습니다.

시작하기

HTTPS 인증서를 획득하고, 적용한 후, 인증서를 요구하도록 서버를 구성합니다.

웹 응용 프로그램에서:

  • Microsoft.AspNetCore.Authentication.Certificate NuGet 패키지에 대한 참조를 추가합니다.
  • Program.cs에서 builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);을 호출합니다. 요청과 함께 전송된 클라이언트 인증서에 대해 보조 유효성 검사를 수행하기 위해 OnCertificateValidated의 대리자를 제공합니다. 해당 정보를 ClaimsPrincipal로 전환하고 context.Principal 속성에 대해 설정합니다.

인증에 실패하면 이 처리기는 예상한 대로 401 (Unauthorized) 대신 403 (Forbidden) 응답을 반환합니다. 이유는 초기 TLS 연결 중에 인증이 발생해야 하기 때문입니다. 처리기에 도달할 때는 너무 늦은 것입니다. 익명 연결에서 인증서가 있는 연결로 업그레이드할 수 있는 방법은 없습니다.

UseAuthentication은 인증서에서 만든 ClaimsPrincipalHttpContext.User를 설정해야 합니다. 예시:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate();

var app = builder.Build();

app.UseAuthentication();

app.MapGet("/", () => "Hello World!");

app.Run();

앞의 예제에서는 인증서 인증을 추가하는 기본 방법을 보여 줍니다. 처리기는 공용 인증서 속성을 사용하여 사용자 보안 주체를 생성합니다.

인증서 유효성 검사 구성

CertificateAuthenticationOptions 처리기에는 인증서에 대해 수행해야 하는 최소 유효성 검사인 몇 가지 기본 제공 유효성 검사가 있습니다. 이러한 각 설정은 기본적으로 사용하도록 설정됩니다.

AllowedCertificateTypes = Chained, SelfSigned 또는 All (Chained | SelfSigned)

기본값: CertificateTypes.Chained

이 검사는 적절한 인증서 유형만 허용되는지 확인합니다. 앱에서 자체 서명된 인증서를 사용하는 경우 이 옵션을 CertificateTypes.All 또는 CertificateTypes.SelfSigned로 설정해야 합니다.

ChainTrustValidationMode

기본값: X509ChainTrustMode.System

클라이언트에서 제공하는 인증서는 신뢰할 수 있는 루트 인증서에 연결해야 합니다. 이 검사 이러한 루트 인증서를 포함 하는 신뢰 저장소를 제어 합니다.

기본적으로 처리기는 시스템 신뢰 저장소를 사용합니다. 제공된 클라이언트 인증서가 시스템 신뢰 저장소에 표시되지 않는 루트 인증서에 연결해야 하는 경우 이 옵션을 X509ChainTrustMode.CustomRootTrust로 설정하여 처리기를 사용할 CustomTrustStore수 있습니다.

CustomTrustStore

기본값: 비어 있음 X509Certificate2Collection

처리기의 ChainTrustValidationMode 속성이 설정된 X509ChainTrustMode.CustomRootTrust경우 신뢰할 X509Certificate2Collection 수 있는 루트를 포함하여 신뢰할 수 있는 루트까지 클라이언트 인증서의 유효성을 검사하는 데 사용할 모든 인증서가 포함됩니다.

클라이언트가 다중 수준 인증서 체인의 일부인 인증서를 제공하는 경우 체인 CustomTrustStore 의 모든 발급 인증서를 포함해야 합니다.

ValidateCertificateUse

기본값: true

이 검사는 클라이언트에서 제공하는 인증서에 클라이언트 인증 EKU(확장된 키 사용)가 있거나 EKU가 없는지 확인합니다. 사양에서 알 수 있듯이 EKU를 지정하지 않으면 모든 EKU가 유효한 것으로 간주됩니다.

ValidateValidityPeriod

기본값: true

이 검사는 인증서가 유효 기간 내에 있는지 확인합니다. 각 요청에서 처리기는 제공될 당시 유효했던 인증서가 현재 세션 중에 만료되지 않았는지 확인합니다.

RevocationFlag

기본값: X509RevocationFlag.ExcludeRoot

체인에서 해지할 인증서를 지정하는 플래그입니다.

해지 검사는 인증서가 루트 인증서에 연결된 경우에만 수행됩니다.

RevocationMode

기본값: X509RevocationMode.Online

해지 검사를 수행하는 방법을 지정하는 플래그입니다.

온라인 검사를 지정하면 인증 기관에 연락하는 동안 장시간 지연될 수 있습니다.

해지 검사는 인증서가 루트 인증서에 연결된 경우에만 수행됩니다.

특정 경로에서만 인증서를 요구하도록 앱을 구성할 수 있나요?

이것은 불가능합니다. 인증서 교환은 HTTPS 대화가 시작될 때 수행되며, 해당 연결에서 첫 번째 요청이 수신되기 전에 서버에서 수행되므로 요청 필드를 기준으로 범위를 지정하는 것이 불가능합니다.

처리기 이벤트

처리기에는 다음 두 개의 이벤트가 있습니다.

  • OnAuthenticationFailed: 인증 중에 예외가 발생하고 대응할 수 있는 경우 호출됩니다.
  • OnCertificateValidated: 인증서의 유효성이 검사되고 유효성 검사를 통과하고, 기본 보안 주체가 만들어지면 호출됩니다. 이 이벤트를 사용하면 사용자 고유의 유효성 검사를 수행하고 보안 주체를 보강하거나 바꿀 수 있습니다. 예를 들면 다음과 같습니다.
    • 인증서가 서비스에 알려져 있는지 확인

    • 사용자 고유의 보안 주체 생성 다음 예제를 참조하세요.

      builder.Services.AddAuthentication(
              CertificateAuthenticationDefaults.AuthenticationScheme)
          .AddCertificate(options =>
          {
              options.Events = new CertificateAuthenticationEvents
              {
                  OnCertificateValidated = context =>
                  {
                      var claims = new[]
                      {
                          new Claim(
                              ClaimTypes.NameIdentifier,
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, context.Options.ClaimsIssuer),
                          new Claim(
                              ClaimTypes.Name,
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, context.Options.ClaimsIssuer)
                      };
      
                      context.Principal = new ClaimsPrincipal(
                          new ClaimsIdentity(claims, context.Scheme.Name));
                      context.Success();
      
                      return Task.CompletedTask;
                  }
              };
          });
      

인바운드 인증서가 추가 유효성 검사를 충족하지 않는 경우 오류 이유를 포함하여 context.Fail("failure reason")을 호출합니다.

더 나은 기능을 위해 데이터베이스 또는 다른 유형의 사용자 저장소에 연결하는 종속성 주입에 등록된 서비스를 호출합니다. 대리자로 전달된 컨텍스트를 사용하여 서비스에 액세스합니다. 다음 예제를 참조하세요.

builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService = context.HttpContext.RequestServices
                    .GetRequiredService<ICertificateValidationService>();

                if (validationService.ValidateCertificate(context.ClientCertificate))
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier,
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, context.Options.ClaimsIssuer),
                        new Claim(
                            ClaimTypes.Name,
                            context.ClientCertificate.Subject,
                            ClaimValueTypes.String, context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }

                return Task.CompletedTask;
            }
        };
    });

개념적으로 인증서의 유효성 검사는 권한 부여 문제입니다. 예를 들어 OnCertificateValidated 내부가 아닌 권한 부여 정책의 발급자 또는 지문에 대해 검사를 추가하는 것은 완벽하게 허용됩니다.

인증서를 요구하도록 서버 구성

Kestrel

Program.cs에서 다음과 같이 Kestrel을 구성합니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<KestrelServerOptions>(options =>
{
    options.ConfigureHttpsDefaults(options =>
        options.ClientCertificateMode = ClientCertificateMode.RequireCertificate);
});

참고 항목

ConfigureHttpsDefaults를 호출하기 전에Listen을 호출하여 생성된 엔드포인트는 기본값이 적용되지 않습니다.

IIS

IIS 관리자에서 다음 단계를 완료합니다.

  1. 연결 탭에서 사이트를 선택합니다.
  2. 기능 보기 창에서 SSL 설정 옵션을 두 번 클릭합니다.
  3. SSL 필요 확인란을 선택하고 클라이언트 인증서 섹션에서 필요 라디오 단추를 선택합니다.

Client certificate settings in IIS

Azure 및 사용자 지정 웹 프록시

인증서 전달 미들웨어를 구성하는 방법은 호스트 및 배포 설명서를 참조하세요.

Azure Web Apps에서 인증서 인증 사용

Azure에는 전달 구성이 필요하지 않습니다. 전달 구성은 인증서 전달 미들웨어에서 설정됩니다.

참고 항목

이 시나리오에서는 인증서 전달 미들웨어가 필요합니다.

자세한 내용은 Azure App Service의 코드에서 TLS/SSL 인증서 사용(Azure 설명서)을 참조하세요.

사용자 지정 웹 프록시에서 인증서 인증 사용

AddCertificateForwarding 메서드는 다음을 지정하는 데 사용됩니다.

  • 클라이언트 헤더 이름
  • 인증서를 로드하는 방법(HeaderConverter 속성 사용)

사용자 지정 웹 프록시에서 인증서는 사용자 지정 요청 헤더(예: X-SSL-CERT)로 전달됩니다. 이를 사용하려면 Program.cs에서 인증서 전달을 구성합니다.

builder.Services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "X-SSL-CERT";

    options.HeaderConverter = headerValue =>
    {
        X509Certificate2? clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            clientCertificate = new X509Certificate2(StringToByteArray(headerValue));
        }

        return clientCertificate!;

        static byte[] StringToByteArray(string hex)
        {
            var numberChars = hex.Length;
            var bytes = new byte[numberChars / 2];

            for (int i = 0; i < numberChars; i += 2)
            {
                bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
            }

            return bytes;
        }
    };
});

앱이 NGINX에서 구성 proxy_set_header ssl-client-cert $ssl_client_escaped_cert를 통해 역방향으로 프록시되거나 NGINX 수신을 사용하여 Kubernetes에 배포된 경우 클라이언트 인증서는 URL 인코딩 폼으로 앱에 전달됩니다. 인증서를 사용하려면 다음과 같이 디코딩합니다.

builder.Services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "ssl-client-cert";

    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2? clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            clientCertificate = X509Certificate2.CreateFromPem(
                WebUtility.UrlDecode(headerValue));
        }

        return clientCertificate!;
    };
});

Program.cs에 미들웨어를 추가합니다. UseCertificateForwardingUseAuthenticationUseAuthorization을 호출하기 전에 호출됩니다.

var app = builder.Build();

app.UseCertificateForwarding();

app.UseAuthentication();
app.UseAuthorization();

별도의 클래스를 사용하여 유효성 검사 논리를 구현할 수 있습니다. 이 예제에서는 동일한 자체 서명된 인증서를 사용하기 때문에 인증서만 사용할 수 있는지 확인합니다. 클라이언트 인증서와 서버 인증서의 두 지문이 일치하는지 확인합니다. 그렇지 않고 어떤 인증서도 사용할 수 있으며 인증하는 데 충분합니다. 이것은 AddCertificate 메서드 내에서 사용됩니다. 중간 또는 자식 인증서를 사용하는 경우 여기에서 주체 또는 발급자의 유효성을 검사할 수도 있습니다.

using System.Security.Cryptography.X509Certificates;

namespace CertAuthSample.Snippets;

public class SampleCertificateValidationService : ICertificateValidationService
{
    public bool ValidateCertificate(X509Certificate2 clientCertificate)
    {
        // Don't hardcode passwords in production code.
        // Use a certificate thumbprint or Azure Key Vault.
        var expectedCertificate = new X509Certificate2(
            Path.Combine("/path/to/pfx"), "1234");

        return clientCertificate.Thumbprint == expectedCertificate.Thumbprint;
    }
}

인증서 및 IHttpClientFactory를 사용하여 HttpClient 구현

다음 예제에서는 처리기의 ClientCertificates 속성을 사용하여 HttpClientHandler에 클라이언트 인증서를 추가합니다. 그런 다음, ConfigurePrimaryHttpMessageHandler 메서드를 사용하여 HttpClient의 명명된 인스턴스에서 이 처리기를 사용할 수 있습니다. 이 내용은 Program.cs에 설정됩니다.

var clientCertificate =
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

builder.Services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});

그런 다음, IHttpClientFactory를 사용하여 처리기와 인증서를 통해 명명된 인스턴스를 가져올 수 있습니다. Program.cs에 정의된 클라이언트 이름을 포함하는 CreateClient 메서드는 인스턴스를 가져오는 데 사용됩니다. 필요에 따라 이 클라이언트를 사용하여 HTTP 요청을 보낼 수 있습니다.

public class SampleHttpService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public SampleHttpService(IHttpClientFactory httpClientFactory)
        => _httpClientFactory = httpClientFactory;

    public async Task<JsonDocument> GetAsync()
    {
        var httpClient = _httpClientFactory.CreateClient("namedClient");
        var httpResponseMessage = await httpClient.GetAsync("https://example.com");

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            return JsonDocument.Parse(
                await httpResponseMessage.Content.ReadAsStringAsync());
        }

        throw new ApplicationException($"Status code: {httpResponseMessage.StatusCode}");
    }
}

서버에 올바른 인증서를 보내면 데이터가 반환됩니다. 인증서를 보내지 않거나 잘못된 인증서를 보낸 경우 HTTP 403 상태 코드가 반환됩니다.

PowerShell에서 인증서 만들기

인증서 만들기는 이 흐름을 설정할 때 가장 어려운 부분입니다. New-SelfSignedCertificate PowerShell cmdlet을 사용하여 루트 인증서를 만들 수 있습니다. 인증서를 만들 때 강력한 암호를 사용합니다. 표시된 대로 KeyUsageProperty 매개 변수 및 KeyUsage 매개 변수를 추가하는 것이 중요합니다.

루트 CA 만들기

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt

참고 항목

-DnsName 매개 변수 값은 앱의 배포 대상과 일치해야 합니다. 예를 들어 개발 환경의 경우 “localhost”입니다.

신뢰할 수 있는 루트에 설치

호스트 시스템에서 루트 인증서를 신뢰할 수 있어야 합니다. 인증 기관에서 만들지 않은 루트 인증서는 기본적으로 신뢰할 수 없습니다. Windows에서 루트 인증서를 신뢰하는 방법에 대한 자세한 내용은 이 질문을 참조하세요.

중간 인증서

이제 루트 인증서에서 중간 인증서를 만들 수 있습니다. 모든 사용 사례에서 이 작업이 필요한 것은 아니지만 많은 인증서를 만들거나 인증서 그룹을 활성화하거나 비활성화해야 할 수도 있습니다. TextExtension 매개 변수는 인증서의 기본 제약 조건에서 경로 길이를 설정하는 데 필요합니다.

그런 다음, Windows 호스트 시스템의 신뢰할 수 있는 중간 인증서에 중간 인증서를 추가할 수 있습니다.

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt

중간 인증서에서 자식 인증서 만들기

중간 인증서에서 자식 인증서를 만들 수 있습니다. 이 엔터티는 최종 엔터티이며 더 이상의 자식 인증서를 만들 필요는 없습니다.

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

루트 인증서에서 자식 인증서 만들기

루트 인증서에서 직접 자식 인증서를 만들 수도 있습니다.

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

예제 루트 - 중간 인증서 - 인증서

$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com" 

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt

루트, 중간 또는 자식 인증서를 사용하는 경우 필요에 따라 지문 또는 PublicKey를 사용하여 인증서의 유효성을 검사할 수 있습니다.

using System.Security.Cryptography.X509Certificates;

namespace CertAuthSample.Snippets;

public class SampleCertificateThumbprintsValidationService : ICertificateValidationService
{
    private readonly string[] validThumbprints = new[]
    {
        "141594A0AE38CBBECED7AF680F7945CD51D8F28A",
        "0C89639E4E2998A93E423F919B36D4009A0F9991",
        "BA9BF91ED35538A01375EFC212A2F46104B33A44"
    };

    public bool ValidateCertificate(X509Certificate2 clientCertificate)
        => validThumbprints.Contains(clientCertificate.Thumbprint);
}

인증서 유효성 검사에 실패했습니다.

ASP.NET Core 5.0 이상 버전에서는 유효성 검사 결과의 캐싱을 사용하도록 설정하는 기능을 지원합니다. 유효성 검사는 비용이 많이 드는 작업이므로 캐싱은 인증서 인증의 성능을 크게 향상시킵니다.

기본적으로 인증서 인증은 캐싱을 사용하지 않습니다. 캐싱을 사용하도록 설정하려면 Program.cs에서 AddCertificateCache를 호출합니다.

builder.Services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate()
    .AddCertificateCache(options =>
    {
        options.CacheSize = 1024;
        options.CacheEntryExpiration = TimeSpan.FromMinutes(2);
    });

기본 캐싱 구현에서는 결과를 메모리에 저장합니다. ICertificateValidationCache를 구현하고 종속성 주입에 등록하여 자체 캐시를 제공할 수 있습니다. 예: services.AddSingleton<ICertificateValidationCache, YourCache>().

선택적 클라이언트 인증서

이 섹션에서는 인증서를 사용하여 앱의 하위 집합을 보호해야 하는 앱에 대한 정보를 제공합니다. 예를 들어 앱의 Razor Page 또는 컨트롤러에 클라이언트 인증서가 필요할 수 있습니다. 챌린지를 클라이언트 인증서로 표시합니다.

  • HTTP 기능이 아닌 TLS 기능입니다.
  • 일반적으로 HTTP 데이터를 사용할 수 있으려면 연결을 시작할 때 연결에 따라 협상됩니다.

선택적 클라이언트 인증서를 구현하는 방법에는 다음 두 가지가 있습니다.

  1. SNI(별도 호스트 이름) 및 리디렉션 사용. 구성 작업은 더 많지만 대부분의 환경 및 프로토콜에서 작동하기 때문에 이 방식이 권장됩니다.
  2. HTTP 요청 동안 재협상. 몇 가지 제한 사항이 있으므로 권장되지 않습니다.

별도 호스트(SNI)

연결이 시작될 때 SNI(서버 이름 표시)†만 알려집니다. 호스트 이름마다 클라이언트 인증서를 구성할 수 있으며, 이 경우 하나의 호스트에는 인증서가 필요하지만 다른 호스트에는 인증서가 필요하지 않습니다.

ASP.NET Core 5 이상에서는 선택적 클라이언트 인증서를 얻기 위해 리디렉션하는 좀 더 편리한 지원 기능을 추가합니다. 자세한 내용은 선택적 인증서 샘플을 참조하세요.

  • 클라이언트 인증서가 필요하고 클라이언트 인증서가 없는 웹앱에 대한 요청의 경우:
    • 클라이언트 인증서로 보호된 하위 도메인을 사용하여 동일한 페이지로 리디렉션합니다.
    • 예를 들어 myClient.contoso.com/requestedPage로 리디렉션합니다. myClient.contoso.com/requestedPage에 대한 요청은 contoso.com/requestedPage와는 다른 호스트 이름이기 때문에 클라이언트에서 다른 연결을 설정하고 클라이언트 인증서가 제공됩니다.
    • 자세한 내용은 ASP.NET Core의 인증 소개를 참조하세요.

† SNI(서버 이름 표시)는 SSL 협상의 일부로 가상 도메인을 포함하기 위한 TLS 확장입니다. 따라서 가상 도메인 이름, 즉 호스트 이름을 네트워크 엔드포인트 식별에 사용할 수 있습니다.

재협상

TLS 재협상은 클라이언트 및 서버가 이전에 제공되지 않은 클라이언트 인증서에 대한 요청을 포함하여 개별 연결에 대한 암호화 요구 사항을 다시 평가할 수 있는 프로세스입니다. TLS 재협상은 보안상 위험하므로 권장되지 않습니다.

  • HTTP/1.1에서 서버는 먼저 POST 요청 본문과 같이 사전 실행 상태인 HTTP 데이터를 버퍼링하거나 사용하여 해당 연결이 재협상을 위해 명확한지 확인해야 합니다. 그러지 않으면 재협상이 응답을 중지하거나 실패할 수 있습니다.
  • HTTP/2 및 HTTP/3은 재협상을 명시적으로 금지합니다.
  • 재협상과 관련해서 보안 위험이 있습니다. TLS 1.3에서는 연결이 시작된 후에만 클라이언트 인증서를 요청할 수 있도록 전체 연결의 재협상을 제거하고 새 확장으로 교체했습니다. 이 메커니즘은 동일한 API를 통해 노출되며 버퍼링 및 HTTP 프로토콜 버전의 이전 제약 조건을 따릅니다.

이 기능의 구현 및 구성은 서버 및 프레임워크 버전에 따라 다릅니다.

IIS

IIS는 사용자 대신 클라이언트 인증서 협상을 관리합니다. 애플리케이션의 하위 섹션은 해당 요청에 대한 클라이언트 인증서를 협상하기 위해 SslRequireCert 옵션을 사용하도록 설정할 수 있습니다. 자세한 내용은 IIS 설명서의 구성을 참조하세요.

IIS는 재협상하기 전에 구성된 크기 제한까지 모든 요청 본문 데이터를 자동으로 버퍼링합니다. 이 제한을 초과하는 요청은 413 응답을 나타내며 거부됩니다. 이 제한은 기본적으로 48KB이며 uploadReadAheadSize설정하여 구성할 수 있습니다.

HttpSys

HttpSys에는 클라이언트 인증서 협상을 제어하는 두 가지 설정이 있으며 둘 다 설정해야 합니다. 첫 번째는 netsh.exe의 http add sslcert clientcertnegotiation=enable/disable 아래에 있습니다. 이 플래그는 연결 시작 시 클라이언트 인증서가 협상될지 여부를 나타내며 선택적 클라이언트 인증서의 경우 disable로 설정해야 합니다. 자세한 내용은 netsh 설명서를 참조하세요.

다른 설정은 ClientCertificateMethod입니다. AllowRenegotation으로 설정하면 요청 중에 클라이언트 인증서를 다시 협상할 수 있습니다.

참고 : 애플리케이션은 재협상을 시도하기 전에 요청 본문 데이터를 버퍼링하거나 사용해야 합니다. 그렇지 않으면 요청이 응답하지 않을 수 있습니다.

애플리케이션을 먼저 ClientCertificate 속성을 확인하여 인증서를 사용할 수 있는지 알아볼 수 있습니다. 사용할 수 없는 경우 GetClientCertificateAsync를 호출하여 협상하기 전에 요청 본문이 사용되었는지 확인합니다. GetClientCertificateAsync는 클라이언트가 인증서 제공을 거부하는 경우 null 인증서를 반환할 수 있습니다.

참고 .NET 6에서 속성의 ClientCertificate 동작이 변경되었습니다. 자세한 내용은 GitHub 이슈를 참조하세요.

Kestrel

Kestrel는 ClientCertificateMode 옵션을 사용하여 클라이언트 인증서 협상을 제어합니다.

ClientCertificateMode.DelayCertificate는 .NET 6 이상에서 사용할 수 있는 새 옵션입니다. 설정되면 앱에서 ClientCertificate 속성을 확인하여 인증서를 사용할 수 있는지 확인할 수 있습니다. 사용할 수 없는 경우 GetClientCertificateAsync를 호출하여 협상하기 전에 요청 본문이 사용되었는지 확인합니다. GetClientCertificateAsync는 클라이언트가 인증서 제공을 거부하는 경우 null 인증서를 반환할 수 있습니다.

참고 : 애플리케이션은 재협상을 시도하기 전에 요청 본문 데이터를 버퍼링하거나 사용해야 합니다. 그렇지 않으면 GetClientCertificateAsync throw InvalidOperationException: Client stream needs to be drained before renegotiation.할 수 있습니다.

호스트당 TLS 설정을 프로그래밍 방식으로 구성하는 경우 TlsHandshakeCallbackOptions를 사용하고 TlsHandshakeCallbackContext.AllowDelayedClientCertificateNegotation을 통해 클라이언트 인증서 재협상을 제어하는 새로운 UseHttps 오버로드를 .NET 6 이상에서 사용할 수 있습니다.

Microsoft.AspNetCore.Authentication.Certificate에는 ASP.NET Core의 인증서 인증과 유사한 구현이 포함되어 있습니다. 인증서 인증은 ASP.NET Core에 도달하기 훨씬 전에 TLS 수준에서 수행됩니다. 보다 정확하게 말하면, 인증서의 유효성을 검사한 다음, 해당 인증서를 ClaimsPrincipal로 확인할 수 있는 이벤트를 제공하는 인증 처리기입니다.

IIS, Kestrel, Azure Web Apps 또는 사용 중인 어떤 것이든 인증서 인증을 위해 서버를 구성합니다.

프록시 및 부하 분산 장치 시나리오

인증서 인증은 프록시 또는 부하 분산 장치가 클라이언트와 서버 간의 트래픽을 처리하지 않는 경우 주로 사용되는 상태 비저장 시나리오입니다. 프록시 또는 부하 분산 장치를 사용하는 경우 인증서 인증은 프록시 또는 부하 분산 장치가 다음을 수행하는 경우에만 작동합니다.

  • 인증을 처리합니다.
  • 사용자 인증 정보를 인증 정보에 작용하는 앱(예: 요청 헤더)에 전달합니다.

프록시 및 부하 분산 장치가 사용되는 환경에서 인증서 인증 대신 OIDC(OpenID Connect)가 있는 ADFS(Active Directory Federated Services)를 사용할 수 있습니다.

시작하기

HTTPS 인증서를 획득하고, 적용한 후, 인증서를 요구하도록 서버를 구성합니다.

웹앱에서 Microsoft.AspNetCore.Authentication.Certificate 패키지에 대한 참조를 추가합니다. 그런 다음, Startup.ConfigureServices 메서드에서 옵션을 사용하여 services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);를 호출하고 요청과 함께 전송된 클라이언트 인증서에 대해 보조 유효성 검사를 수행하기 위해 OnCertificateValidated의 대리자를 제공합니다. 해당 정보를 ClaimsPrincipal로 전환하고 context.Principal 속성에 대해 설정합니다.

인증에 실패하면 이 처리기는 예상한 대로 401 (Unauthorized) 대신 403 (Forbidden) 응답을 반환합니다. 이유는 초기 TLS 연결 중에 인증이 발생해야 하기 때문입니다. 처리기에 도달할 때는 너무 늦은 것입니다. 익명 연결에서 인증서가 있는 연결로 업그레이드할 수 있는 방법은 없습니다.

또한 app.UseAuthentication(); 메서드에 Startup.Configure를 추가합니다. 그러지 않으면 HttpContext.User는 인증서에서 만든 ClaimsPrincipal로 설정되지 않습니다. 예시:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate()
        // Adding an ICertificateValidationCache results in certificate auth caching the results.
        // The default implementation uses a memory cache.
        .AddCertificateCache();

    // All other service configuration
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();

    // All other app configuration
}

앞의 예제에서는 인증서 인증을 추가하는 기본 방법을 보여 줍니다. 처리기는 공용 인증서 속성을 사용하여 사용자 보안 주체를 생성합니다.

인증서 유효성 검사 구성

CertificateAuthenticationOptions 처리기에는 인증서에 대해 수행해야 하는 최소 유효성 검사인 몇 가지 기본 제공 유효성 검사가 있습니다. 이러한 각 설정은 기본적으로 사용하도록 설정됩니다.

AllowedCertificateTypes = Chained, SelfSigned 또는 All (Chained | SelfSigned)

기본값: CertificateTypes.Chained

이 검사는 적절한 인증서 유형만 허용되는지 확인합니다. 앱에서 자체 서명된 인증서를 사용하는 경우 이 옵션을 CertificateTypes.All 또는 CertificateTypes.SelfSigned로 설정해야 합니다.

ValidateCertificateUse

기본값: true

이 검사는 클라이언트에서 제공하는 인증서에 클라이언트 인증 EKU(확장된 키 사용)가 있거나 EKU가 없는지 확인합니다. 사양에서 알 수 있듯이 EKU를 지정하지 않으면 모든 EKU가 유효한 것으로 간주됩니다.

ValidateValidityPeriod

기본값: true

이 검사는 인증서가 유효 기간 내에 있는지 확인합니다. 각 요청에서 처리기는 제공될 당시 유효했던 인증서가 현재 세션 중에 만료되지 않았는지 확인합니다.

RevocationFlag

기본값: X509RevocationFlag.ExcludeRoot

체인에서 해지할 인증서를 지정하는 플래그입니다.

해지 검사는 인증서가 루트 인증서에 연결된 경우에만 수행됩니다.

RevocationMode

기본값: X509RevocationMode.Online

해지 검사를 수행하는 방법을 지정하는 플래그입니다.

온라인 검사를 지정하면 인증 기관에 연락하는 동안 장시간 지연될 수 있습니다.

해지 검사는 인증서가 루트 인증서에 연결된 경우에만 수행됩니다.

특정 경로에서만 인증서를 요구하도록 앱을 구성할 수 있나요?

이것은 불가능합니다. 인증서 교환은 HTTPS 대화가 시작될 때 수행되며, 해당 연결에서 첫 번째 요청이 수신되기 전에 서버에서 수행되므로 요청 필드를 기준으로 범위를 지정하는 것이 불가능합니다.

처리기 이벤트

처리기에는 다음 두 개의 이벤트가 있습니다.

  • OnAuthenticationFailed: 인증 중에 예외가 발생하고 대응할 수 있는 경우 호출됩니다.
  • OnCertificateValidated: 인증서의 유효성이 검사되고 유효성 검사를 통과하고, 기본 보안 주체가 만들어지면 호출됩니다. 이 이벤트를 사용하면 사용자 고유의 유효성 검사를 수행하고 보안 주체를 보강하거나 바꿀 수 있습니다. 예를 들면 다음과 같습니다.
    • 인증서가 서비스에 알려져 있는지 확인

    • 사용자 고유의 보안 주체 생성 Startup.ConfigureServices에서 다음 예제를 참조하세요.

      services.AddAuthentication(
          CertificateAuthenticationDefaults.AuthenticationScheme)
          .AddCertificate(options =>
          {
              options.Events = new CertificateAuthenticationEvents
              {
                  OnCertificateValidated = context =>
                  {
                      var claims = new[]
                      {
                          new Claim(
                              ClaimTypes.NameIdentifier, 
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, 
                              context.Options.ClaimsIssuer),
                          new Claim(ClaimTypes.Name,
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, 
                              context.Options.ClaimsIssuer)
                      };
      
                      context.Principal = new ClaimsPrincipal(
                          new ClaimsIdentity(claims, context.Scheme.Name));
                      context.Success();
      
                      return Task.CompletedTask;
                  }
              };
          });
      

인바운드 인증서가 추가 유효성 검사를 충족하지 않는 경우 오류 이유를 포함하여 context.Fail("failure reason")을 호출합니다.

실제 기능의 경우 데이터베이스 또는 다른 유형의 사용자 저장소에 연결하는 종속성 주입에 등록된 서비스를 호출할 수 있습니다. 대리자로 전달된 컨텍스트를 사용하여 서비스에 액세스합니다. Startup.ConfigureServices에서 다음 예제를 참조하세요.

services.AddAuthentication(
    CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService =
                    context.HttpContext.RequestServices
                        .GetRequiredService<ICertificateValidationService>();

                if (validationService.ValidateCertificate(
                    context.ClientCertificate))
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer),
                        new Claim(
                            ClaimTypes.Name, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }                     

                return Task.CompletedTask;
            }
        };
    });

개념적으로 인증서의 유효성 검사는 권한 부여 문제입니다. 예를 들어 OnCertificateValidated 내부가 아닌 권한 부여 정책의 발급자 또는 지문에 대해 검사를 추가하는 것은 완벽하게 허용됩니다.

인증서를 요구하도록 서버 구성

Kestrel

Program.cs에서 다음과 같이 Kestrel을 구성합니다.

public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            webBuilder.ConfigureKestrel(o =>
            {
                o.ConfigureHttpsDefaults(o => 
                    o.ClientCertificateMode =  ClientCertificateMode.RequireCertificate);
            });
        });
}

참고 항목

ConfigureHttpsDefaults를 호출하기 전에Listen을 호출하여 생성된 엔드포인트는 기본값이 적용되지 않습니다.

IIS

IIS 관리자에서 다음 단계를 완료합니다.

  1. 연결 탭에서 사이트를 선택합니다.
  2. 기능 보기 창에서 SSL 설정 옵션을 두 번 클릭합니다.
  3. SSL 필요 확인란을 선택하고 클라이언트 인증서 섹션에서 필요 라디오 단추를 선택합니다.

Client certificate settings in IIS

Azure 및 사용자 지정 웹 프록시

인증서 전달 미들웨어를 구성하는 방법은 호스트 및 배포 설명서를 참조하세요.

Azure Web Apps에서 인증서 인증 사용

Azure에는 전달 구성이 필요하지 않습니다. 전달 구성은 인증서 전달 미들웨어에서 설정됩니다.

참고 항목

이 시나리오에서는 인증서 전달 미들웨어가 필요합니다.

자세한 내용은 Azure App Service의 코드에서 TLS/SSL 인증서 사용(Azure 설명서)을 참조하세요.

사용자 지정 웹 프록시에서 인증서 인증 사용

AddCertificateForwarding 메서드는 다음을 지정하는 데 사용됩니다.

  • 클라이언트 헤더 이름
  • 인증서를 로드하는 방법(HeaderConverter 속성 사용)

사용자 지정 웹 프록시에서 인증서는 사용자 지정 요청 헤더(예: X-SSL-CERT)로 전달됩니다. 이를 사용하려면 Startup.ConfigureServices에서 인증서 전달을 구성합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options =>
    {
        options.CertificateHeader = "X-SSL-CERT";
        options.HeaderConverter = (headerValue) =>
        {
            X509Certificate2 clientCertificate = null;

            if(!string.IsNullOrWhiteSpace(headerValue))
            {
                byte[] bytes = StringToByteArray(headerValue);
                clientCertificate = new X509Certificate2(bytes);
            }

            return clientCertificate;
        };
    });
}

private static byte[] StringToByteArray(string hex)
{
    int NumberChars = hex.Length;
    byte[] bytes = new byte[NumberChars / 2];

    for (int i = 0; i < NumberChars; i += 2)
    {
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }

    return bytes;
}

앱이 NGINX에서 구성 proxy_set_header ssl-client-cert $ssl_client_escaped_cert를 통해 역방향으로 프록시되거나 NGINX 수신을 사용하여 Kubernetes에 배포된 경우 클라이언트 인증서는 URL 인코딩 폼으로 앱에 전달됩니다. 인증서를 사용하려면 다음과 같이 디코딩합니다.

Startup.ConfigureServices(Startup.cs)에서:

services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "ssl-client-cert";
    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2 clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            string certPem = WebUtility.UrlDecode(headerValue);
            clientCertificate = X509Certificate2.CreateFromPem(certPem);
        }

        return clientCertificate;
    };
});

그런 다음, Startup.Configure 메서드는 미들웨어를 추가합니다. UseCertificateForwardingUseAuthenticationUseAuthorization을 호출하기 전에 호출됩니다.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseRouting();

    app.UseCertificateForwarding();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

별도의 클래스를 사용하여 유효성 검사 논리를 구현할 수 있습니다. 이 예제에서는 동일한 자체 서명된 인증서를 사용하기 때문에 인증서만 사용할 수 있는지 확인합니다. 클라이언트 인증서와 서버 인증서의 두 지문이 일치하는지 확인합니다. 그렇지 않고 어떤 인증서도 사용할 수 있으며 인증하는 데 충분합니다. 이것은 AddCertificate 메서드 내에서 사용됩니다. 중간 또는 자식 인증서를 사용하는 경우 여기에서 주체 또는 발급자의 유효성을 검사할 수도 있습니다.

using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            // Do not hardcode passwords in production code
            // Use thumbprint or key vault
            var cert = new X509Certificate2(
                Path.Combine("sts_dev_cert.pfx"), "1234");

            if (clientCertificate.Thumbprint == cert.Thumbprint)
            {
                return true;
            }

            return false;
        }
    }
}

인증서 및 HttpClientHandler를 사용하여 HttpClient 구현

HttpClientHandlerHttpClient 클래스의 생성자에 직접 추가할 수 있습니다. HttpClient의 인스턴스를 만들 때는 주의해야 합니다. 그러면 HttpClient는 각 요청과 함께 인증서를 보냅니다.

private async Task<JsonDocument> GetApiDataUsingHttpClientHandler()
{
    var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(cert);
    var client = new HttpClient(handler);

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

IHttpClientFactory에서 인증서 및 명명된 HttpClient를 사용하여 HttpClient 구현

다음 예제에서는 처리기의 ClientCertificates 속성을 사용하여 HttpClientHandler에 클라이언트 인증서를 추가합니다. 그런 다음, ConfigurePrimaryHttpMessageHandler 메서드를 사용하여 HttpClient의 명명된 인스턴스에서 이 처리기를 사용할 수 있습니다. 이 내용은 Startup.ConfigureServices에 설정됩니다.

var clientCertificate = 
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});

그런 다음, IHttpClientFactory를 사용하여 처리기와 인증서를 통해 명명된 인스턴스를 가져올 수 있습니다. Startup 클래스에 정의된 클라이언트 이름을 포함하는 CreateClient 메서드는 인스턴스를 가져오는 데 사용됩니다. 필요에 따라 이 클라이언트를 사용하여 HTTP 요청을 보낼 수 있습니다.

private readonly IHttpClientFactory _clientFactory;

public ApiService(IHttpClientFactory clientFactory)
{
    _clientFactory = clientFactory;
}

private async Task<JsonDocument> GetApiDataWithNamedClient()
{
    var client = _clientFactory.CreateClient("namedClient");

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

서버에 올바른 인증서를 보내면 데이터가 반환됩니다. 인증서를 보내지 않거나 잘못된 인증서를 보낸 경우 HTTP 403 상태 코드가 반환됩니다.

PowerShell에서 인증서 만들기

인증서 만들기는 이 흐름을 설정할 때 가장 어려운 부분입니다. New-SelfSignedCertificate PowerShell cmdlet을 사용하여 루트 인증서를 만들 수 있습니다. 인증서를 만들 때 강력한 암호를 사용합니다. 표시된 대로 KeyUsageProperty 매개 변수 및 KeyUsage 매개 변수를 추가하는 것이 중요합니다.

루트 CA 만들기

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt

참고 항목

-DnsName 매개 변수 값은 앱의 배포 대상과 일치해야 합니다. 예를 들어 개발 환경의 경우 “localhost”입니다.

신뢰할 수 있는 루트에 설치

호스트 시스템에서 루트 인증서를 신뢰할 수 있어야 합니다. 인증 기관에서 만들지 않은 루트 인증서는 기본적으로 신뢰할 수 없습니다. Windows에서 루트 인증서를 신뢰하는 방법에 대한 자세한 내용은 이 질문을 참조하세요.

중간 인증서

이제 루트 인증서에서 중간 인증서를 만들 수 있습니다. 모든 사용 사례에서 이 작업이 필요한 것은 아니지만 많은 인증서를 만들거나 인증서 그룹을 활성화하거나 비활성화해야 할 수도 있습니다. TextExtension 매개 변수는 인증서의 기본 제약 조건에서 경로 길이를 설정하는 데 필요합니다.

그런 다음, Windows 호스트 시스템의 신뢰할 수 있는 중간 인증서에 중간 인증서를 추가할 수 있습니다.

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt

중간 인증서에서 자식 인증서 만들기

중간 인증서에서 자식 인증서를 만들 수 있습니다. 이 엔터티는 최종 엔터티이며 더 이상의 자식 인증서를 만들 필요는 없습니다.

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

루트 인증서에서 자식 인증서 만들기

루트 인증서에서 직접 자식 인증서를 만들 수도 있습니다.

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

예제 루트 - 중간 인증서 - 인증서

$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com" 

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt

루트, 중간 또는 자식 인증서를 사용하는 경우 필요에 따라 지문 또는 PublicKey를 사용하여 인증서의 유효성을 검사할 수 있습니다.

using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService 
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            return CheckIfThumbprintIsValid(clientCertificate);
        }

        private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate)
        {
            var listOfValidThumbprints = new List<string>
            {
                "141594A0AE38CBBECED7AF680F7945CD51D8F28A",
                "0C89639E4E2998A93E423F919B36D4009A0F9991",
                "BA9BF91ED35538A01375EFC212A2F46104B33A44"
            };

            if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint))
            {
                return true;
            }

            return false;
        }
    }
}

인증서 유효성 검사에 실패했습니다.

ASP.NET Core 5.0 이상 버전에서는 유효성 검사 결과의 캐싱을 사용하도록 설정하는 기능을 지원합니다. 유효성 검사는 비용이 많이 드는 작업이므로 캐싱은 인증서 인증의 성능을 크게 향상시킵니다.

기본적으로 인증서 인증은 캐싱을 사용하지 않습니다. 캐싱을 사용하도록 설정하려면 Startup.ConfigureServices에서 AddCertificateCache를 호출합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
            .AddCertificate()
            .AddCertificateCache(options =>
            {
                options.CacheSize = 1024;
                options.CacheEntryExpiration = TimeSpan.FromMinutes(2);
            });
}

기본 캐싱 구현에서는 결과를 메모리에 저장합니다. ICertificateValidationCache를 구현하고 종속성 주입에 등록하여 자체 캐시를 제공할 수 있습니다. 예: services.AddSingleton<ICertificateValidationCache, YourCache>().

선택적 클라이언트 인증서

이 섹션에서는 인증서를 사용하여 앱의 하위 집합을 보호해야 하는 앱에 대한 정보를 제공합니다. 예를 들어 앱의 Razor Page 또는 컨트롤러에 클라이언트 인증서가 필요할 수 있습니다. 챌린지를 클라이언트 인증서로 표시합니다.

  • HTTP 기능이 아닌 TLS 기능입니다.
  • 일반적으로 HTTP 데이터를 사용할 수 있으려면 연결을 시작할 때 연결에 따라 협상됩니다.

선택적 클라이언트 인증서를 구현하는 방법에는 다음 두 가지가 있습니다.

  1. SNI(별도 호스트 이름) 및 리디렉션 사용. 구성 작업은 더 많지만 대부분의 환경 및 프로토콜에서 작동하기 때문에 이 방식이 권장됩니다.
  2. HTTP 요청 동안 재협상. 몇 가지 제한 사항이 있으므로 권장되지 않습니다.

별도 호스트(SNI)

연결이 시작될 때 SNI(서버 이름 표시)†만 알려집니다. 호스트 이름마다 클라이언트 인증서를 구성할 수 있으며, 이 경우 하나의 호스트에는 인증서가 필요하지만 다른 호스트에는 인증서가 필요하지 않습니다.

ASP.NET Core 5 이상에서는 선택적 클라이언트 인증서를 얻기 위해 리디렉션하는 좀 더 편리한 지원 기능을 추가합니다. 자세한 내용은 선택적 인증서 샘플을 참조하세요.

  • 클라이언트 인증서가 필요하고 클라이언트 인증서가 없는 웹앱에 대한 요청의 경우:
    • 클라이언트 인증서로 보호된 하위 도메인을 사용하여 동일한 페이지로 리디렉션합니다.
    • 예를 들어 myClient.contoso.com/requestedPage로 리디렉션합니다. myClient.contoso.com/requestedPage에 대한 요청은 contoso.com/requestedPage와는 다른 호스트 이름이기 때문에 클라이언트에서 다른 연결을 설정하고 클라이언트 인증서가 제공됩니다.
    • 자세한 내용은 ASP.NET Core의 인증 소개를 참조하세요.

† SNI(서버 이름 표시)는 SSL 협상의 일부로 가상 도메인을 포함하기 위한 TLS 확장입니다. 따라서 가상 도메인 이름, 즉 호스트 이름을 네트워크 엔드포인트 식별에 사용할 수 있습니다.

재협상

TLS 재협상은 클라이언트 및 서버가 이전에 제공되지 않은 클라이언트 인증서에 대한 요청을 포함하여 개별 연결에 대한 암호화 요구 사항을 다시 평가할 수 있는 프로세스입니다. TLS 재협상은 보안상 위험하므로 권장되지 않습니다.

  • HTTP/1.1에서 서버는 먼저 POST 요청 본문과 같이 사전 실행 상태인 HTTP 데이터를 버퍼링하거나 사용하여 해당 연결이 재협상을 위해 명확한지 확인해야 합니다. 그러지 않으면 재협상이 응답을 중지하거나 실패할 수 있습니다.
  • HTTP/2 및 HTTP/3은 재협상을 명시적으로 금지합니다.
  • 재협상과 관련해서 보안 위험이 있습니다. TLS 1.3에서는 연결이 시작된 후에만 클라이언트 인증서를 요청할 수 있도록 전체 연결의 재협상을 제거하고 새 확장으로 교체했습니다. 이 메커니즘은 동일한 API를 통해 노출되며 버퍼링 및 HTTP 프로토콜 버전의 이전 제약 조건을 따릅니다.

이 기능의 구현 및 구성은 서버 및 프레임워크 버전에 따라 다릅니다.

IIS

IIS는 사용자 대신 클라이언트 인증서 협상을 관리합니다. 애플리케이션의 하위 섹션은 해당 요청에 대한 클라이언트 인증서를 협상하기 위해 SslRequireCert 옵션을 사용하도록 설정할 수 있습니다. 자세한 내용은 IIS 설명서의 구성을 참조하세요.

IIS는 재협상하기 전에 구성된 크기 제한까지 모든 요청 본문 데이터를 자동으로 버퍼링합니다. 이 제한을 초과하는 요청은 413 응답을 나타내며 거부됩니다. 이 제한은 기본적으로 48KB이며 uploadReadAheadSize설정하여 구성할 수 있습니다.

HttpSys

HttpSys에는 클라이언트 인증서 협상을 제어하는 두 가지 설정이 있으며 둘 다 설정해야 합니다. 첫 번째는 netsh.exe의 http add sslcert clientcertnegotiation=enable/disable 아래에 있습니다. 이 플래그는 연결 시작 시 클라이언트 인증서가 협상될지 여부를 나타내며 선택적 클라이언트 인증서의 경우 disable로 설정해야 합니다. 자세한 내용은 netsh 설명서를 참조하세요.

다른 설정은 ClientCertificateMethod입니다. AllowRenegotation으로 설정하면 요청 중에 클라이언트 인증서를 다시 협상할 수 있습니다.

참고 : 애플리케이션은 재협상을 시도하기 전에 요청 본문 데이터를 버퍼링하거나 사용해야 합니다. 그렇지 않으면 요청이 응답하지 않을 수 있습니다.

AllowRenegotation을 사용하도록 설정할 경우 ClientCertificate 속성에 액세스할 때 재협상이 동기식으로 발생할 수 있다는 알려진 문제가 있습니다. 이를 방지하려면 GetClientCertificateAsync 메서드를 호출합니다. 이 내용은 .NET 6에서 해결되었습니다. 자세한 내용은 GitHub 이슈를 참조하세요. GetClientCertificateAsync는 클라이언트가 인증서 제공을 거부하는 경우 null 인증서를 반환할 수 있습니다.

Kestrel

Kestrel는 ClientCertificateMode 옵션을 사용하여 클라이언트 인증서 협상을 제어합니다.

.NET 5 이하 버전에서 Kestrel은 클라이언트 인증서를 획득하기 위한 연결이 시작된 후 재협상을 지원하지 않습니다. 이 기능은 .NET 6에 추가되었습니다.

Microsoft.AspNetCore.Authentication.Certificate에는 ASP.NET Core의 인증서 인증과 유사한 구현이 포함되어 있습니다. 인증서 인증은 ASP.NET Core에 도달하기 훨씬 전에 TLS 수준에서 수행됩니다. 보다 정확하게 말하면, 인증서의 유효성을 검사한 다음, 해당 인증서를 ClaimsPrincipal로 확인할 수 있는 이벤트를 제공하는 인증 처리기입니다.

IIS, Kestrel, Azure Web Apps 또는 사용 중인 어떤 것이든 인증서 인증을 위해 서버를 구성합니다.

프록시 및 부하 분산 장치 시나리오

인증서 인증은 프록시 또는 부하 분산 장치가 클라이언트와 서버 간의 트래픽을 처리하지 않는 경우 주로 사용되는 상태 비저장 시나리오입니다. 프록시 또는 부하 분산 장치를 사용하는 경우 인증서 인증은 프록시 또는 부하 분산 장치가 다음을 수행하는 경우에만 작동합니다.

  • 인증을 처리합니다.
  • 사용자 인증 정보를 인증 정보에 작용하는 앱(예: 요청 헤더)에 전달합니다.

프록시 및 부하 분산 장치가 사용되는 환경에서 인증서 인증 대신 OIDC(OpenID Connect)가 있는 ADFS(Active Directory Federated Services)를 사용할 수 있습니다.

시작하기

HTTPS 인증서를 획득하고, 적용한 후, 인증서를 요구하도록 서버를 구성합니다.

웹앱에서 Microsoft.AspNetCore.Authentication.Certificate 패키지에 대한 참조를 추가합니다. 그런 다음, Startup.ConfigureServices 메서드에서 옵션을 사용하여 services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(...);를 호출하고 요청과 함께 전송된 클라이언트 인증서에 대해 보조 유효성 검사를 수행하기 위해 OnCertificateValidated의 대리자를 제공합니다. 해당 정보를 ClaimsPrincipal로 전환하고 context.Principal 속성에 대해 설정합니다.

인증에 실패하면 이 처리기는 예상한 대로 401 (Unauthorized) 대신 403 (Forbidden) 응답을 반환합니다. 이유는 초기 TLS 연결 중에 인증이 발생해야 하기 때문입니다. 처리기에 도달할 때는 너무 늦은 것입니다. 익명 연결에서 인증서가 있는 연결로 업그레이드할 수 있는 방법은 없습니다.

또한 app.UseAuthentication(); 메서드에 Startup.Configure를 추가합니다. 그러지 않으면 HttpContext.User는 인증서에서 만든 ClaimsPrincipal로 설정되지 않습니다. 예시:

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(
        CertificateAuthenticationDefaults.AuthenticationScheme)
        .AddCertificate();

    // All other service configuration
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAuthentication();

    // All other app configuration
}

앞의 예제에서는 인증서 인증을 추가하는 기본 방법을 보여 줍니다. 처리기는 공용 인증서 속성을 사용하여 사용자 보안 주체를 생성합니다.

인증서 유효성 검사 구성

CertificateAuthenticationOptions 처리기에는 인증서에 대해 수행해야 하는 최소 유효성 검사인 몇 가지 기본 제공 유효성 검사가 있습니다. 이러한 각 설정은 기본적으로 사용하도록 설정됩니다.

AllowedCertificateTypes = Chained, SelfSigned 또는 All (Chained | SelfSigned)

기본값: CertificateTypes.Chained

이 검사는 적절한 인증서 유형만 허용되는지 확인합니다. 앱에서 자체 서명된 인증서를 사용하는 경우 이 옵션을 CertificateTypes.All 또는 CertificateTypes.SelfSigned로 설정해야 합니다.

ValidateCertificateUse

기본값: true

이 검사는 클라이언트에서 제공하는 인증서에 클라이언트 인증 EKU(확장된 키 사용)가 있거나 EKU가 없는지 확인합니다. 사양에서 알 수 있듯이 EKU를 지정하지 않으면 모든 EKU가 유효한 것으로 간주됩니다.

ValidateValidityPeriod

기본값: true

이 검사는 인증서가 유효 기간 내에 있는지 확인합니다. 각 요청에서 처리기는 제공될 당시 유효했던 인증서가 현재 세션 중에 만료되지 않았는지 확인합니다.

RevocationFlag

기본값: X509RevocationFlag.ExcludeRoot

체인에서 해지할 인증서를 지정하는 플래그입니다.

해지 검사는 인증서가 루트 인증서에 연결된 경우에만 수행됩니다.

RevocationMode

기본값: X509RevocationMode.Online

해지 검사를 수행하는 방법을 지정하는 플래그입니다.

온라인 검사를 지정하면 인증 기관에 연락하는 동안 장시간 지연될 수 있습니다.

해지 검사는 인증서가 루트 인증서에 연결된 경우에만 수행됩니다.

특정 경로에서만 인증서를 요구하도록 앱을 구성할 수 있나요?

이것은 불가능합니다. 인증서 교환은 HTTPS 대화가 시작될 때 수행되며, 해당 연결에서 첫 번째 요청이 수신되기 전에 서버에서 수행되므로 요청 필드를 기준으로 범위를 지정하는 것이 불가능합니다.

처리기 이벤트

처리기에는 다음 두 개의 이벤트가 있습니다.

  • OnAuthenticationFailed: 인증 중에 예외가 발생하고 대응할 수 있는 경우 호출됩니다.
  • OnCertificateValidated: 인증서의 유효성이 검사되고 유효성 검사를 통과하고, 기본 보안 주체가 만들어지면 호출됩니다. 이 이벤트를 사용하면 사용자 고유의 유효성 검사를 수행하고 보안 주체를 보강하거나 바꿀 수 있습니다. 예를 들면 다음과 같습니다.
    • 인증서가 서비스에 알려져 있는지 확인

    • 사용자 고유의 보안 주체 생성 Startup.ConfigureServices에서 다음 예제를 참조하세요.

      services.AddAuthentication(
          CertificateAuthenticationDefaults.AuthenticationScheme)
          .AddCertificate(options =>
          {
              options.Events = new CertificateAuthenticationEvents
              {
                  OnCertificateValidated = context =>
                  {
                      var claims = new[]
                      {
                          new Claim(
                              ClaimTypes.NameIdentifier, 
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, 
                              context.Options.ClaimsIssuer),
                          new Claim(ClaimTypes.Name,
                              context.ClientCertificate.Subject,
                              ClaimValueTypes.String, 
                              context.Options.ClaimsIssuer)
                      };
      
                      context.Principal = new ClaimsPrincipal(
                          new ClaimsIdentity(claims, context.Scheme.Name));
                      context.Success();
      
                      return Task.CompletedTask;
                  }
              };
          });
      

인바운드 인증서가 추가 유효성 검사를 충족하지 않는 경우 오류 이유를 포함하여 context.Fail("failure reason")을 호출합니다.

실제 기능의 경우 데이터베이스 또는 다른 유형의 사용자 저장소에 연결하는 종속성 주입에 등록된 서비스를 호출할 수 있습니다. 대리자로 전달된 컨텍스트를 사용하여 서비스에 액세스합니다. Startup.ConfigureServices에서 다음 예제를 참조하세요.

services.AddAuthentication(
    CertificateAuthenticationDefaults.AuthenticationScheme)
    .AddCertificate(options =>
    {
        options.Events = new CertificateAuthenticationEvents
        {
            OnCertificateValidated = context =>
            {
                var validationService =
                    context.HttpContext.RequestServices
                        .GetRequiredService<ICertificateValidationService>();

                if (validationService.ValidateCertificate(
                    context.ClientCertificate))
                {
                    var claims = new[]
                    {
                        new Claim(
                            ClaimTypes.NameIdentifier, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer),
                        new Claim(
                            ClaimTypes.Name, 
                            context.ClientCertificate.Subject, 
                            ClaimValueTypes.String, 
                            context.Options.ClaimsIssuer)
                    };

                    context.Principal = new ClaimsPrincipal(
                        new ClaimsIdentity(claims, context.Scheme.Name));
                    context.Success();
                }                     

                return Task.CompletedTask;
            }
        };
    });

개념적으로 인증서의 유효성 검사는 권한 부여 문제입니다. 예를 들어 OnCertificateValidated 내부가 아닌 권한 부여 정책의 발급자 또는 지문에 대해 검사를 추가하는 것은 완벽하게 허용됩니다.

인증서를 요구하도록 서버 구성

Kestrel

Program.cs에서 다음과 같이 Kestrel을 구성합니다.

public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
            webBuilder.ConfigureKestrel(o =>
            {
                o.ConfigureHttpsDefaults(o => 
                    o.ClientCertificateMode =  ClientCertificateMode.RequireCertificate);
            });
        });
}

참고 항목

ConfigureHttpsDefaults를 호출하기 전에Listen을 호출하여 생성된 엔드포인트는 기본값이 적용되지 않습니다.

IIS

IIS 관리자에서 다음 단계를 완료합니다.

  1. 연결 탭에서 사이트를 선택합니다.
  2. 기능 보기 창에서 SSL 설정 옵션을 두 번 클릭합니다.
  3. SSL 필요 확인란을 선택하고 클라이언트 인증서 섹션에서 필요 라디오 단추를 선택합니다.

Client certificate settings in IIS

Azure 및 사용자 지정 웹 프록시

인증서 전달 미들웨어를 구성하는 방법은 호스트 및 배포 설명서를 참조하세요.

Azure Web Apps에서 인증서 인증 사용

Azure에는 전달 구성이 필요하지 않습니다. 전달 구성은 인증서 전달 미들웨어에서 설정됩니다.

참고 항목

이 시나리오에서는 인증서 전달 미들웨어가 필요합니다.

자세한 내용은 Azure App Service의 코드에서 TLS/SSL 인증서 사용(Azure 설명서)을 참조하세요.

사용자 지정 웹 프록시에서 인증서 인증 사용

AddCertificateForwarding 메서드는 다음을 지정하는 데 사용됩니다.

  • 클라이언트 헤더 이름
  • 인증서를 로드하는 방법(HeaderConverter 속성 사용)

사용자 지정 웹 프록시에서 인증서는 사용자 지정 요청 헤더(예: X-SSL-CERT)로 전달됩니다. 이를 사용하려면 Startup.ConfigureServices에서 인증서 전달을 구성합니다.

public void ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options =>
    {
        options.CertificateHeader = "X-SSL-CERT";
        options.HeaderConverter = (headerValue) =>
        {
            X509Certificate2 clientCertificate = null;

            if(!string.IsNullOrWhiteSpace(headerValue))
            {
                byte[] bytes = StringToByteArray(headerValue);
                clientCertificate = new X509Certificate2(bytes);
            }

            return clientCertificate;
        };
    });
}

private static byte[] StringToByteArray(string hex)
{
    int NumberChars = hex.Length;
    byte[] bytes = new byte[NumberChars / 2];

    for (int i = 0; i < NumberChars; i += 2)
    {
        bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
    }

    return bytes;
}

앱이 NGINX에서 구성 proxy_set_header ssl-client-cert $ssl_client_escaped_cert를 통해 역방향으로 프록시되거나 NGINX 수신을 사용하여 Kubernetes에 배포된 경우 클라이언트 인증서는 URL 인코딩 폼으로 앱에 전달됩니다. 인증서를 사용하려면 다음과 같이 디코딩합니다.

Startup.cs의 맨 위에 System.Net에 대한 네임스페이스를 추가합니다.

using System.Net;

Startup.ConfigureServices의 경우

services.AddCertificateForwarding(options =>
{
    options.CertificateHeader = "ssl-client-cert";
    options.HeaderConverter = (headerValue) =>
    {
        X509Certificate2 clientCertificate = null;

        if (!string.IsNullOrWhiteSpace(headerValue))
        {
            var bytes = UrlEncodedPemToByteArray(headerValue);
            clientCertificate = new X509Certificate2(bytes);
        }

        return clientCertificate;
    };
});

UrlEncodedPemToByteArray 메서드를 추가합니다.

private static byte[] UrlEncodedPemToByteArray(string urlEncodedBase64Pem)
{
    var base64Pem = WebUtility.UrlDecode(urlEncodedBase64Pem);
    var base64Cert = base64Pem
        .Replace("-----BEGIN CERTIFICATE-----", string.Empty)
        .Replace("-----END CERTIFICATE-----", string.Empty)
        .Trim();

    return Convert.FromBase64String(base64Cert);
}

그런 다음, Startup.Configure 메서드는 미들웨어를 추가합니다. UseCertificateForwardingUseAuthenticationUseAuthorization을 호출하기 전에 호출됩니다.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseRouting();

    app.UseCertificateForwarding();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

별도의 클래스를 사용하여 유효성 검사 논리를 구현할 수 있습니다. 이 예제에서는 동일한 자체 서명된 인증서를 사용하기 때문에 인증서만 사용할 수 있는지 확인합니다. 클라이언트 인증서와 서버 인증서의 두 지문이 일치하는지 확인합니다. 그렇지 않고 어떤 인증서도 사용할 수 있으며 인증하는 데 충분합니다. 이것은 AddCertificate 메서드 내에서 사용됩니다. 중간 또는 자식 인증서를 사용하는 경우 여기에서 주체 또는 발급자의 유효성을 검사할 수도 있습니다.

using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            // Do not hardcode passwords in production code
            // Use thumbprint or key vault
            var cert = new X509Certificate2(
                Path.Combine("sts_dev_cert.pfx"), "1234");

            if (clientCertificate.Thumbprint == cert.Thumbprint)
            {
                return true;
            }

            return false;
        }
    }
}

인증서 및 HttpClientHandler를 사용하여 HttpClient 구현

HttpClientHandlerHttpClient 클래스의 생성자에 직접 추가할 수 있습니다. HttpClient의 인스턴스를 만들 때는 주의해야 합니다. 그러면 HttpClient는 각 요청과 함께 인증서를 보냅니다.

private async Task<JsonDocument> GetApiDataUsingHttpClientHandler()
{
    var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(cert);
    var client = new HttpClient(handler);

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

IHttpClientFactory에서 인증서 및 명명된 HttpClient를 사용하여 HttpClient 구현

다음 예제에서는 처리기의 ClientCertificates 속성을 사용하여 HttpClientHandler에 클라이언트 인증서를 추가합니다. 그런 다음, ConfigurePrimaryHttpMessageHandler 메서드를 사용하여 HttpClient의 명명된 인스턴스에서 이 처리기를 사용할 수 있습니다. 이 내용은 Startup.ConfigureServices에 설정됩니다.

var clientCertificate = 
    new X509Certificate2(
      Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

services.AddHttpClient("namedClient", c =>
{
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new HttpClientHandler();
    handler.ClientCertificates.Add(clientCertificate);
    return handler;
});

그런 다음, IHttpClientFactory를 사용하여 처리기와 인증서를 통해 명명된 인스턴스를 가져올 수 있습니다. Startup 클래스에 정의된 클라이언트 이름을 포함하는 CreateClient 메서드는 인스턴스를 가져오는 데 사용됩니다. 필요에 따라 이 클라이언트를 사용하여 HTTP 요청을 보낼 수 있습니다.

private readonly IHttpClientFactory _clientFactory;

public ApiService(IHttpClientFactory clientFactory)
{
    _clientFactory = clientFactory;
}

private async Task<JsonDocument> GetApiDataWithNamedClient()
{
    var client = _clientFactory.CreateClient("namedClient");

    var request = new HttpRequestMessage()
    {
        RequestUri = new Uri("https://localhost:44379/api/values"),
        Method = HttpMethod.Get,
    };
    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var data = JsonDocument.Parse(responseContent);
        return data;
    }

    throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}

서버에 올바른 인증서를 보내면 데이터가 반환됩니다. 인증서를 보내지 않거나 잘못된 인증서를 보낸 경우 HTTP 403 상태 코드가 반환됩니다.

PowerShell에서 인증서 만들기

인증서 만들기는 이 흐름을 설정할 때 가장 어려운 부분입니다. New-SelfSignedCertificate PowerShell cmdlet을 사용하여 루트 인증서를 만들 수 있습니다. 인증서를 만들 때 강력한 암호를 사용합니다. 표시된 대로 KeyUsageProperty 매개 변수 및 KeyUsage 매개 변수를 추가하는 것이 중요합니다.

루트 CA 만들기

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt

참고 항목

-DnsName 매개 변수 값은 앱의 배포 대상과 일치해야 합니다. 예를 들어 개발 환경의 경우 “localhost”입니다.

신뢰할 수 있는 루트에 설치

호스트 시스템에서 루트 인증서를 신뢰할 수 있어야 합니다. 인증 기관에서 만들지 않은 루트 인증서는 기본적으로 신뢰할 수 없습니다. Windows에서 루트 인증서를 신뢰하는 방법에 대한 자세한 내용은 이 질문을 참조하세요.

중간 인증서

이제 루트 인증서에서 중간 인증서를 만들 수 있습니다. 모든 사용 사례에서 이 작업이 필요한 것은 아니지만 많은 인증서를 만들거나 인증서 그룹을 활성화하거나 비활성화해야 할 수도 있습니다. TextExtension 매개 변수는 인증서의 기본 제약 조건에서 경로 길이를 설정하는 데 필요합니다.

그런 다음, Windows 호스트 시스템의 신뢰할 수 있는 중간 인증서에 중간 인증서를 추가할 수 있습니다.

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "intermediate_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "intermediate_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt

중간 인증서에서 자식 인증서 만들기

중간 인증서에서 자식 인증서를 만들 수 있습니다. 이 엔터티는 최종 엔터티이며 더 이상의 자식 인증서를 만들 필요는 없습니다.

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

루트 인증서에서 자식 인증서 만들기

루트 인증서에서 직접 자식 인증서를 만들 수도 있습니다.

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

예제 루트 - 중간 인증서 - 인증서

$mypwdroot = ConvertTo-SecureString -String "1234" -Force -AsPlainText
$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" -CertStoreLocation "cert:\LocalMachine\My" -NotAfter (Get-Date).AddYears(20) -FriendlyName "root_ca_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature

Get-ChildItem -Path cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwdroot

Export-Certificate -Cert cert:\localMachine\my\0C89639E4E2998A93E423F919B36D4009A0F9991 -FilePath root_ca_dev_damienbod.crt

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\0C89639E4E2998A93E423F919B36D4009A0F9991 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_a_dev_damienbod.com" -Signer $rootcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_a_dev_damienbod.com" -KeyUsageProperty All -KeyUsage CertSign, CRLSign, DigitalSignature -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\BA9BF91ED35538A01375EFC212A2F46104B33A44 -FilePath child_a_dev_damienbod.crt

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\BA9BF91ED35538A01375EFC212A2F46104B33A44 )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname "child_b_from_a_dev_damienbod.com" -Signer $parentcert -NotAfter (Get-Date).AddYears(20) -FriendlyName "child_b_from_a_dev_damienbod.com" 

Get-ChildItem -Path cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_b_from_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\141594A0AE38CBBECED7AF680F7945CD51D8F28A -FilePath child_b_from_a_dev_damienbod.crt

루트, 중간 또는 자식 인증서를 사용하는 경우 필요에 따라 지문 또는 PublicKey를 사용하여 인증서의 유효성을 검사할 수 있습니다.

using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService 
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            return CheckIfThumbprintIsValid(clientCertificate);
        }

        private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate)
        {
            var listOfValidThumbprints = new List<string>
            {
                "141594A0AE38CBBECED7AF680F7945CD51D8F28A",
                "0C89639E4E2998A93E423F919B36D4009A0F9991",
                "BA9BF91ED35538A01375EFC212A2F46104B33A44"
            };

            if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint))
            {
                return true;
            }

            return false;
        }
    }
}

선택적 클라이언트 인증서

이 섹션에서는 인증서를 사용하여 앱의 하위 집합을 보호해야 하는 앱에 대한 정보를 제공합니다. 예를 들어 앱의 Razor Page 또는 컨트롤러에 클라이언트 인증서가 필요할 수 있습니다. 챌린지를 클라이언트 인증서로 표시합니다.

  • HTTP 기능이 아닌 TLS 기능입니다.
  • 일반적으로 HTTP 데이터를 사용할 수 있으려면 연결을 시작할 때 연결에 따라 협상됩니다.

선택적 클라이언트 인증서를 구현하는 방법에는 다음 두 가지가 있습니다.

  1. SNI(별도 호스트 이름) 및 리디렉션 사용. 구성 작업은 더 많지만 대부분의 환경 및 프로토콜에서 작동하기 때문에 이 방식이 권장됩니다.
  2. HTTP 요청 동안 재협상. 몇 가지 제한 사항이 있으므로 권장되지 않습니다.

별도 호스트(SNI)

연결이 시작될 때 SNI(서버 이름 표시)†만 알려집니다. 호스트 이름마다 클라이언트 인증서를 구성할 수 있으며, 이 경우 하나의 호스트에는 인증서가 필요하지만 다른 호스트에는 인증서가 필요하지 않습니다.

  • do기본 및 subdo기본에 대한 바인딩을 설정합니다.
    • 예를 들어 contoso.commyClient.contoso.com에 대한 바인딩을 설정합니다. contoso.com 호스트에는 클라이언트 인증서가 필요하지 않지만 myClient.contoso.com에는 필요합니다.
    • 자세한 내용은 다음을 참조하세요.

ASP.NET Core 5 이상에서는 선택적 클라이언트 인증서를 얻기 위해 리디렉션하는 좀 더 편리한 지원 기능을 추가합니다. 자세한 내용은 선택적 인증서 샘플을 참조하세요.

  • 클라이언트 인증서가 필요하고 클라이언트 인증서가 없는 웹앱에 대한 요청의 경우:
    • 클라이언트 인증서로 보호된 하위 도메인을 사용하여 동일한 페이지로 리디렉션합니다.
    • 예를 들어 myClient.contoso.com/requestedPage로 리디렉션합니다. myClient.contoso.com/requestedPage에 대한 요청은 contoso.com/requestedPage와는 다른 호스트 이름이기 때문에 클라이언트에서 다른 연결을 설정하고 클라이언트 인증서가 제공됩니다.
    • 자세한 내용은 ASP.NET Core의 인증 소개를 참조하세요.

† SNI(서버 이름 표시)는 SSL 협상의 일부로 가상 도메인을 포함하기 위한 TLS 확장입니다. 따라서 가상 도메인 이름, 즉 호스트 이름을 네트워크 엔드포인트 식별에 사용할 수 있습니다.

재협상

TLS 재협상은 클라이언트 및 서버가 이전에 제공되지 않은 클라이언트 인증서에 대한 요청을 포함하여 개별 연결에 대한 암호화 요구 사항을 다시 평가할 수 있는 프로세스입니다. TLS 재협상은 보안상 위험하므로 권장되지 않습니다.

  • HTTP/1.1에서 서버는 먼저 POST 요청 본문과 같이 사전 실행 상태인 HTTP 데이터를 버퍼링하거나 사용하여 해당 연결이 재협상을 위해 명확한지 확인해야 합니다. 그러지 않으면 재협상이 응답을 중지하거나 실패할 수 있습니다.
  • HTTP/2 및 HTTP/3은 재협상을 명시적으로 금지합니다.
  • 재협상과 관련해서 보안 위험이 있습니다. TLS 1.3에서는 연결이 시작된 후에만 클라이언트 인증서를 요청할 수 있도록 전체 연결의 재협상을 제거하고 새 확장으로 교체했습니다. 이 메커니즘은 동일한 API를 통해 노출되며 버퍼링 및 HTTP 프로토콜 버전의 이전 제약 조건을 따릅니다.

이 기능의 구현 및 구성은 서버 및 프레임워크 버전에 따라 다릅니다.

IIS

IIS는 사용자 대신 클라이언트 인증서 협상을 관리합니다. 애플리케이션의 하위 섹션은 해당 요청에 대한 클라이언트 인증서를 협상하기 위해 SslRequireCert 옵션을 사용하도록 설정할 수 있습니다. 자세한 내용은 IIS 설명서의 구성을 참조하세요.

IIS는 재협상하기 전에 구성된 크기 제한까지 모든 요청 본문 데이터를 자동으로 버퍼링합니다. 이 제한을 초과하는 요청은 413 응답을 나타내며 거부됩니다. 이 제한은 기본적으로 48KB이며 uploadReadAheadSize설정하여 구성할 수 있습니다.

HttpSys

HttpSys에는 클라이언트 인증서 협상을 제어하는 두 가지 설정이 있으며 둘 다 설정해야 합니다. 첫 번째는 netsh.exe의 http add sslcert clientcertnegotiation=enable/disable 아래에 있습니다. 이 플래그는 연결 시작 시 클라이언트 인증서가 협상될지 여부를 나타내며 선택적 클라이언트 인증서의 경우 disable로 설정해야 합니다. 자세한 내용은 netsh 설명서를 참조하세요.

다른 설정은 ClientCertificateMethod입니다. AllowRenegotation으로 설정하면 요청 중에 클라이언트 인증서를 다시 협상할 수 있습니다.

참고 : 애플리케이션은 재협상을 시도하기 전에 요청 본문 데이터를 버퍼링하거나 사용해야 합니다. 그렇지 않으면 요청이 응답하지 않을 수 있습니다.

AllowRenegotation을 사용하도록 설정할 경우 ClientCertificate 속성에 액세스할 때 재협상이 동기식으로 발생할 수 있다는 알려진 문제가 있습니다. 이를 방지하려면 GetClientCertificateAsync 메서드를 호출합니다. 이 내용은 .NET 6에서 해결되었습니다. 자세한 내용은 GitHub 이슈를 참조하세요. GetClientCertificateAsync는 클라이언트가 인증서 제공을 거부하는 경우 null 인증서를 반환할 수 있습니다.

Kestrel

Kestrel는 ClientCertificateMode 옵션을 사용하여 클라이언트 인증서 협상을 제어합니다.

.NET 5 이하 버전에서 Kestrel은 클라이언트 인증서를 획득하기 위한 연결이 시작된 후 재협상을 지원하지 않습니다. 이 기능은 .NET 6에 추가되었습니다.

이 GitHub 토론 이슈에서 선택적 클라이언트 인증서에 대한 질문, 의견 및 기타 피드백을 남겨 두세요.