다음을 통해 공유


TLS/SSL 모범 사례

TLS(전송 계층 보안)는 두 컴퓨터 간의 인터넷 통신을 보호하도록 설계된 암호화 프로토콜입니다. TLS 프로토콜은 SslStream 클래스를 통해 .NET에 노출됩니다.

이 문서에서는 .NET을 사용한다는 가정 하에 클라이언트와 서버 간에 보안 통신을 설정하는 모범 사례를 제시합니다. .NET Framework 관련 모범 사례는 .NET Framework 사용 시 TLS(전송 계층 보안) 모범 사례를 참조하세요.

TLS 버전 선택

EnabledSslProtocols 속성을 통해 사용할 TLS 프로토콜의 버전을 지정할 수 있지만 None 값을 사용하여 운영 체제 설정에 맡기는 것이 좋습니다(기본값).

결정을 OS에 맡기면 사용 가능한 최신 버전의 TLS가 자동으로 사용되며 OS 업그레이드 후 애플리케이션에서 변경 내용을 선택할 수 있습니다. 또한 운영 체제가 더 이상 안전하지 않은 TLS 버전을 사용하지 못하게 할 수도 있습니다.

암호화 도구 모음 선택

SslStream을 사용하면 사용자가 CipherSuitesPolicy 클래스를 통해 TLS 핸드셰이크로 협상할 수 있는 암호 그룹을 지정할 수 있습니다. TLS 버전과 마찬가지로 OS가 협상하기에 가장 적합한 암호 그룹을 결정하도록 하는 것이 좋습니다. 따라서 CipherSuitesPolicy를 사용하지 않는 것이 좋습니다.

참고

CipherSuitesPolicy는 Windows에서 지원되지 않으며 인스턴스화하려고 하면 NotSupportedException이 throw됩니다.

서버 인증서 지정

서버로 인증하는 경우 SslStreamX509Certificate2 인스턴스가 필요합니다. 항상 프라이빗 키도 포함된 X509Certificate2 인스턴스를 사용하는 것이 좋습니다.

서버 인증서를 SslStream에 전달할 수 있는 방법은 여러 가지가 있습니다.

권장 방법은 SslServerAuthenticationOptions.ServerCertificateContext 속성을 사용하는 것입니다. 다른 두 가지 방법 중 하나로 인증서를 가져오는 경우 SslStreamCertificateContext 인스턴스는 SslStream 구현에 의해 내부적으로 만들어집니다. SslStreamCertificateContext를 만들려면 X509Chain을 빌드해야 하는데 이는 CPU 집약적 작업입니다. SslStreamCertificateContext를 한 번 만들고 여러 SslStream 인스턴스에 다시 사용하는 것이 더 효율적입니다.

SslStreamCertificateContext 인스턴스를 다시 사용하면 Linux 서버에서 TLS 세션 다시 시작과 같은 추가 기능도 사용할 수 있습니다.

사용자 지정 X509Certificate 유효성 검사

기본 인증서 유효성 검사 절차가 적절하지 않고 일부 사용자 지정 유효성 검사 논리가 필요한 특정 시나리오가 있습니다. SslClientAuthenticationOptions.CertificateChainPolicy 또는 SslServerAuthenticationOptions.CertificateChainPolicy를 지정하여 유효성 검사 논리의 일부를 사용자 지정할 수 있습니다. 또는 <System.Net.Security.SslClientAuthenticationOptions.RemoteCertificateValidationCallback> 속성을 통해 완전히 사용자 지정 논리를 제공할 수 있습니다. 자세한 내용은 사용자 지정 인증서 신뢰를 참조하세요.

사용자 지정 인증서 신뢰

컴퓨터에서 신뢰하는 인증 기관에서 발급하지 않은 인증서(자체 서명된 인증서 포함)가 발견되면 기본 인증서 유효성 검사 절차가 실패합니다. 이 문제를 해결하는 한 가지 방법은 컴퓨터의 신뢰할 수 있는 저장소에 필요한 발급자 인증서를 추가하는 것입니다. 그러나 이는 시스템의 다른 애플리케이션에 영향을 줄 수 있으며 항상 가능한 것도 아닙니다.

대체 솔루션은 X509ChainPolicy를 통해 사용자 지정 신뢰할 수 있는 루트 인증서를 지정하는 것입니다. 유효성 검사 중에 시스템 신뢰 목록 대신 사용할 사용자 지정 신뢰 목록을 지정하려면 다음 예제를 고려하세요.

SslClientAuthenticationOptions clientOptions = new();

clientOptions.CertificateChainPolicy = new X509ChainPolicy()
{
    TrustMode = X509ChainTrustMode.CustomRootTrust,
    CustomTrustStore =
    {
        customIssuerCert
    }
};

이전 정책으로 구성된 클라이언트는 customIssuerCert에서 신뢰할 수 있는 인증서만 허용합니다.

특정 유효성 검사 오류 무시

영구 시계가 없는 IoT 디바이스를 고려합니다. 전원을 켜면 디바이스의 시계가 몇 년 전에서 시작되므로 모든 인증서는 "아직 유효하지 않음"으로 간주됩니다. 다음 코드는 유효성 기간 위반을 무시하는 유효성 검사 콜백 구현을 보여 줍니다.

static bool CustomCertificateValidationCallback(
    object sender,
    X509Certificate? certificate,
    X509Chain? chain,
    SslPolicyErrors sslPolicyErrors)
{
    // Anything that would have been accepted by default is OK
    if (sslPolicyErrors == SslPolicyErrors.None)
    {
        return true;
    }
    
    // If there is something wrong other than a chain processing error, don't trust it.
    if (sslPolicyErrors != SslPolicyErrors.RemoteCertificateChainErrors)
    {
        return false;
    }
    
    Debug.Assert(chain is not null);

    // If the reason for RemoteCertificateChainError is that the chain built empty, don't trust it.
    if (chain.ChainStatus.Length == 0)
    {
        return false;
    }

    foreach (X509ChainStatus status in chain.ChainStatus)
    {
        // If an error other than `NotTimeValid` (or `NoError`) is present, don't trust it.
        if ((status.Status & ~X509ChainStatusFlags.NotTimeValid) != X509ChainStatusFlags.NoError)
        {
            return false;
        }
    }

    return true;
}

인증서 고정

사용자 지정 인증서 유효성 검사가 필요한 또 다른 상황은 클라이언트가 서버에서 특정 인증서 또는 알려진 인증서 집합의 인증서를 사용할 것을 기대하는 경우입니다. 이 방법을 인증서 고정이라고 합니다. 다음 코드 조각은 서버가 알려진 특정 공개 키가 있는 인증서를 제공하는지 확인하는 유효성 검사 콜백을 보여 줍니다.

static bool CustomCertificateValidationCallback(
    object sender,
    X509Certificate? certificate,
    X509Chain? chain,
    SslPolicyErrors sslPolicyErrors)
{
    // If there is something wrong other than a chain processing error, don't trust it.
    if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0)
    {
        return false;
    }
    
    Debug.Assert(certificate is not null);

    const string ExpectedPublicKey =
        "3082010A0282010100C204ECF88CEE04C2B3D850D57058CC9318EB5C" +
        "A86849B022B5F9959EB12B2C763E6CC04B604C4CEAB2B4C00F80B6B0" +
        "F972C98602F95C415D132B7F71C44BBCE9942E5037A6671C618CF641" +
        "42C546D31687279F74EB0A9D11522621736C844C7955E4D16BE8063D" +
        "481552ADB328DBAAFF6EFF60954A776B39F124D131B6DD4DC0C4FC53" +
        "B96D42ADB57CFEAEF515D23348E72271C7C2147A6C28EA374ADFEA6C" +
        "B572B47E5AA216DC69B15744DB0A12ABDEC30F47745C4122E19AF91B" +
        "93E6AD2206292EB1BA491C0C279EA3FB8BF7407200AC9208D98C5784" +
        "538105CBE6FE6B5498402785C710BB7370EF6918410745557CF9643F" +
        "3D2CC3A97CEB931A4C86D1CA850203010001";

    return certificate.GetPublicKeyString().Equals(ExpectedPublicKey);
}

클라이언트 인증서 유효성 검사에 대한 고려 사항

서버 애플리케이션은 클라이언트 인증서를 요구하고 유효성을 검사할 때 주의해야 합니다. 인증서에는 발급자 인증서를 다운로드할 수 있는 위치를 지정하는 AIA(인증 기관 정보 액세스) 확장이 포함될 수 있습니다. 따라서 서버는 클라이언트 인증서에 대한 X509Chain을 빌드할 때 외부 서버에서 발급자 인증서를 다운로드하려고 시도할 수 있습니다. 마찬가지로 서버는 클라이언트 인증서가 해지되지 않았는지 확인하기 위해 외부 서버에 연결해야 할 수 있습니다.

X509Chain을 빌드하고 유효성을 검사할 때 외부 서버에 연결해야 하는 경우 외부 서버의 응답 속도가 느린 경우 애플리케이션이 서비스 거부 공격에 노출될 수 있습니다. 따라서 서버 애플리케이션은 X509Chain를 사용하여 CertificateChainPolicy 빌드 동작을 구성해야 합니다.