C# 6.0 완벽 가이드/ 보안

  • .NET에서 권한(permission)은 운영체제가 강제하는 것과는 독립적인 하나의 보안 계층을 제공한다. .NET Framework에서 권한의 주된 용도는 다음 두 가지 이다.
    • 모래상자 적용
      • 부분적으로 신뢰된 .NET 어셈블리가 수행할 수 있는 연산 종류를 제한한다.
    • 권한 부여(인가)
      • 누가 무엇을 할 수 있는지를 제한한다.
  • .NET의 암, 복호화(cryptography) 기능은 고가 자료의 저장 및 교환, 정보 유출 방지, 메시지 위변조 검출, 패스워드 저장을 위한 단방향 해시 생성, 디지털 서명 생성 등에 쓰인다.

권한

  • .NET Framework는 권한 기능을 모래상자와 권한 부여(authorization; 인가)에 사용한다.
    • .NET Framework에서 하나의 권한은 어떤 코드의 실행을 조건에 따라 금지하는 일종의 관문으로 작용한다.
    • 모래상자 적용에는 코드 접근 권한들이 쓰이고, 권한 부여에는 신원(identity) 권한들과 역할(role) 권한들이 쓰인다.
  • 이들은 모두 비슷한 모형을 따르지만 사용해보면 그 느낌이 상당히 다르다. 부분적인 이유는 관점이 정반대라는 것이다.
    • 일반적으로 코드 접근 보안에서는 독자의 코드가 신뢰받지 않는 쪽, 즉 불신의 대상(untrusted party)이다.
    • 그러나 신원 및 역할 보안에서는 독자의 코드가 다른 어떤 코드를 신뢰하지 않는 쪽, 즉 불신의 주체(unstrustig party)이다.
    • 대부분의 경우 코드 접근 보안은 CLR이나 호스팅 환경(ASP.NET이나 Internet Explorer)이 독자의 코드에 강제하지만, 권한 부여 관련 보안은 독자의 코드가 다른 어떤 단위에 강제한다(독자의 코드에 함부로 접근하지 못하도록)
  • 권한이 제한된 환경에서 실행될 어셈블리를 작성하는 응용 프로그램 개발자라면 누구나 코드 접근 보안(code access security, CAS)을 숙지할 필요가 있다.
    • 예컨대 어떤 구성요소 라이브러리를 작성해서 판매하는 경우, 고객이 독자의 라이브러리를 SQL Server CLR 호스트 같은 모래상자 안의 환경에서 호출할 수도 있음을 간과한다면 좋은 평가를 받기 어려울 것이다.
  • 다른 어셈블리들을 모래상자 안에서 실행하는 호스팅 환경을 직접 만들 때도 CAS를 숙지할 필요가 있다.
    • 예컨대 서드파티 개발자들이 작성한 플러그인 구성요소를 실행할 수 있는 응용 프로그램을 만든다고 하자. 그런 플러그인들을 권한이 제한된 응용 프로그램 도메인에서 실행하면 잘못된 플러그인 때문에 응용 프로그램이 불안정해지거나 보안이 손상될 확률이 줄어든다.
  • 신원/역할 보안은 주로 중간층(middle-tier) 서버나 웹 응용 프로그램 서버를 작성할 떄 쓰인다. 그런 경우 흔히 일단의 역할들을 정해 두고, 서버가 노출하는 메서드마다 그 메서드를 어떤 역할의 구성원들이 호출할 수 있는지 설정한다.

CodeAccessPermission과 PrincipalPermission

  • 앞서 말한 두 종류의 권한을 각각 다음 두 클래스가 대표한다.
    • CodeAccessPermission
      • FileIOPermission, ReflectionPermission, PrintingPermission 같은 모든 코드 접근 보안(CAS) 권한 클래스는 이 추상 기반 클래스를 상속한다.
    • PrincipalPermission
      • 이 클래스는 신원 또는 역할(이를테면 ‘메리’ 또는 ‘인사과’ 등)을 서술한다.
  • permission을 권한이라고 번역하지만 원래의 뜻은 허가 또는 허락에 가깝다. 그러나 CodeAccessPermission의 경우에는 실제로 권한이 더 적합한 용어이다. CodeAccessPermission 객체는 특권 있는 연산(privileged operation)을 서술한다.
  • 예컨대 FileIOPermission 객체는 특정 파일 또는 디렉터리 집합에 대해 Read, Write, Append를 수행할 수 있는 특권을 서술한다. 그러한 객체의 용도는 다음과 같이 여러가지이다.
    • 현재 객체와 현재 객체의 사용자(메서드 호출자 등)가 주어진 연산들을 수행할 권리(right)가 있는지 확인한다(Demand)
    • 현재 객체의 직접적인 사용자가 주어진 연산들을 수행할 권리가 있는지 확인한다(LinkDemand)
    • 임시로 모래상자를 벗어나서 어셈블리로부터 부여받은 연산 수행 권리들(호출자의 특권과는 무관한)을 단언한다(Assert)
  • CLR에서는 Deny, RequestMinimum, RequestOptional, RequestRefuse, PermitOnly 같은 보안 동작들도 가능하다. 그러나 새로운 투명성 모형이 등장하면서 .NET Framework 4.0부터 이들은 (그리고 LinkeDemand도) 폐기 예정으로 분류 되었다.
  • PrincipalPermission은 이보다 훨씬 간단하다. 이 클래스의 유일한 보안 메서드는 Demand인데, 이 메서드는 해당 사용자나 역할이 현재의 실행 스레드에서 유효한지 점검한다.

IPermission 인터페이스

  • CodeAccessPermission과 PrincipalPermission 모두 IPermission 인터페이스를 구현한다.
public interface IPermission
{
  void Demain();
  IPermission Intersect (IPermission target);
  IPermission Union (IPermission target);
  bool IsSubsetOf (IPermission target);
  IPermission Copy();
}
  • 이 인터페이스의 핵심은 Demand이다. 이 메서드는 권한 객체가 서술하는 권한 또는 특권 있는 연산이 현재 실제로 허용되는지를 즉석에서 점검해서 만일 허용되지 않으면 SecurityException을 던진다.
    • 독자의 코드가 불신의 대상일 떄에는 독자의 코드가 호출하는 코드에서 Demand를 적용한다.
  • 예컨대 오직 Mary만 관리 보고를 실행할 수 있게 할 때에는 다음과 같이 한다.
new PrincipalPermission("Mary", ull).Demand();
// ... 관리 보고를 실행한다.
  • 반면 독자의 어셈블리가 파일 입출력이 금지된 모래상자 안에서 실행되는 경우 다음과 같은 코드는 SecurityException을 던진다.
using (FileStream fs = new FileStream("test.txt", FileMode.Create))
  ...
  • 이 경우 Demand는 독자의 어셈블리가 호출한 코드, 즉 FileStream의 생성자에서 호출된다.
...
new FileIOPermission(...).Demand();
  • 코드 접근 보안의 Demand는 호출 스택을 거슬러 올라가면서 요청된 연산이 호출 사슬의 모든 단위(현재 응용 프로그램 안의)에서 허용되는지 점검한다. 따라서 코드 접근 보안의 Demand는 사실상 “이 응용 프로그램 도메인에 이러한 권한이 허용되었는가?”를 점검하는 것이라 할 수 있다.
  • 코드 접근 보안과 GAC안에서 실행되는 어셈블리가 결합하면 흥미로운 상황이 벌어진다.
    • GAC 안에서 실행되는 어셈블리는 완전히 신뢰된 어셈블리로 간주된다. 그러나 그런 어셈블리가 모래상자 안에서 실행될 떄는 여전히 모래상자에 부여된 권한 집합이 적용된다.
    • 단, 완전 신뢰 어셈블리는 CodeAccessPermission 객체에 대해 Assert를 호출함으로써 임시로 모래상자를 탈출할 수 있다.
    • Assert를 호출하고 나면, 해당 권한에 대한 Demand는 항상 성공한다. 그러한 탈출 상황은 현재 메서드가 끝나거나 메서드 안에서 명시적으로 CodeAccessPermission.RevertAssert를 호출하면 끝난다.
  • Intersect 메서드와 Union 메서드는 같은 형식의 두 권한 객체를 하나로 합친다. Intersect의 목적은 ‘더 작은’ 권한 객체(교집합)를 만드는 것이고 Union의 목적은 ‘더 큰’ 권한 객체(합집합)를 만드는 것이다.
  • 코드 접근 보안의 경우 권한 객체가 ‘크다’는 것은 Demand시 제약이 더 많음을 뜻한다. Demand에 성공하는데 필요한 권한이 더 많기 때문이다.
  • 반대로 신원/역할 보안에서는 ‘더 큰’ 권한 객체가 Demand시 제약이 더 적음을 뜻한다. 주어진 신원이나 역할이 해당 권한 중 하나만 만족하면 Demand에 성공할 수 있기 때문이다.
  • IsSubsetOf 메서드는 주어진 권한 객체가 현재 권한 객체의 권한들을 가지고 있으면 true를 돌려준다.
PrincipalPermission jay = new PrincipalPermission("Jay", null);
PrincipalPermission sue = new PrincipalPermission("Sue", null);

PrincipalPermission jayOrSue = (PrincipalPermission) jay.Union(sue);
Console.WriteLine(jay.IsSubsetOf(jayOrSue)); // true
  • 이 예에서 만일 jay와 sue에 대해 Intersect를 호출했다면 빈 권한 객체가 만들어졌을 것이다.(둘의 권한들이 겹치지 않으므로)

PermissionSet 클래스

  • PermissionSet은 IPermission을 구현하는 여러 형식의 객체들을 담는 컬렉션으로 여러 권한으로 이루어진 집합을 대표한다. 다음은 세 가지 코드 접근 권한으로 이루어진 권한 집합을 만들어서 세 권한 모두에 대해 Demand를 한 번에 적용하는 예이다.
PermissionSet ps = new PermissionSet(PermissionState.None);
ps.AddPermission(new UIPermission(PermissionState.Unrestricted));
ps.AddPermission(new SecurityPermission(SecurityPermissionFlag.UnmanagedCode));
ps.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read, @"c:\docs"));
ps.Demand();
  • PermissionSet의 생성자는 PermissionState 열거형의 값을 받는다. 이 값은 주어진 집합을 무제한(unrestricted)으로 간주할 것인지를 결정한다.
    • 무제한 권한 집합은 마치 모든 가능한 권한을 담고 있는 것처럼 취급된다.(컬렉션 자체는 비어 있다고 해도)
    • 무제한의 코드 접근 보안 권한을 가지고 실행되는 어셈블리를 가리켜 완전 신뢰(fill trust) 어셈블리라고 부른다.
  • 집합에 권한을 추가하는 AddPermission 메서드는 ‘더 큰’ 권한 집합을 만들어 낸다. ‘더 큰’의 의미는 앞에서 Union을 설명할 때 말했던 것과 동일하다.
    • 무제한 권한 집합에 대한 AddPermission 호출은 아무런 효과도 없다(논리적으로, 무제한 권한 집합은 이미 모든 가능한 권한을 담고 있으므로)
  • IPermission 객체들에서처럼, Union과 Intersect 메서드를 이용해서 두 권한 집합을 합칠 수 있다.

선언식 보안 대 명령식 보안

  • 지금까지의 예제들은 권한 객체를 직접 생성해서 Demand를 호출했다. 이를 명령식 보안(imperative security)이라고 부른다.
    • 이렇게 하는 대신 메서드나 생성자, 클래스, 구조체, 어셈블리에 특성들을 부여해서 같은 결과를 얻는 것도 가능하다. 그런 방식을 선언식 보안(declarative security)이라고 부른다.
    • 명령식 보안이 좀 더 유연하지만 선언식 보안에는 다음과 같은 세 가지 장점이 있다.
      • 코드가 덜 지저분할 수 있다.
      • 어셈블리에 필요한 권한들을 CLR이 미리 파악할 수 있다.
      • 성능 향상의 여지가 있다.
  • 다음은 선언식 보안의 예이다.
[PrincialPermission(SecurityAction.Demand, Name="Mary")]
public ReportData GetReports()
{
  ...
}

[UIPermission(SecurityAction.Demand, Window=UIPermissionWindow.AllWindows)]
public Form FindForm()
{
  ...
}
  • 선언식 보안을 위해 .NET Framework에는 모든 권한 형식마다 그에 대응하는 특성 형식이 마련되어 있다.
    • 예컨대 PrincipalPermission에 대응하는 특성 형식은 PrincipalPermissionAttribute이다. 이러한 특성의 생성자에서 첫 인수는 항상 SecurityAction 열거형의 값이다.
    • 이 인수는 권한 객체를 생성한 후 호출할 보안 메서드를 가리킨다(보통은 Demand)
    • 나머지 명명된 매개변수들은 해당 권한 객체의 속성들에 대응된다.

코드 접근 보안(CAS)

  • .NET Framework 전반에서 강제되는 CodeAccessPermission 파생 형식들이 아래 표들에 범주별로 정리되어 있다. 전체적으로 이 형식들은 프로그램이 보안과 관련해서 저지를 수 있는 모든 실수를 포괄한다.
형식 허용하는 연산
SecurityPermission 관리되지 않은 코드의 호출 같은 고급 연산
ReflectionPermission 반영 기능 사용
EnvironmentPermission 명령줄 환경 설정 읽기/쓰기
RegistryPermission Windows 레지스트리 읽기/쓰기

 

  • SecurityPermission은 SecurityPermissionFlag 플래그 열거형 인수를 받는다. 다음과 같은 열거형 멤버들의 임의의 조합을 인수로 사용할 수 있다.
AllFlags ControlThread
Assertion Execution
BindingRedirects Infrastructure
ControlAppDomain NoFlags
ControlDomainPolicy RemotingConfiguration
ControlEvidence SerializationFormatter
ControlPolicy SkipVerification
ControlPrincipal UnmanagedCode

 

  • 이중 가장 중요한 멤버는 Execution이다. 이 권한이 없으면 코드가 실행되지 않는다. 다른 멤버들은 완전 신뢰 시나리오에서만 부여해야 한다. 다른 멤버들을 부여하면 해당 코드가 모래상자를 우회하거나 탈출할 수 있기 때문이다.
    • ControlAppDomain은 새 응용 프로그램 도메인의 생성을 하용한다.
    • UnmanagedCode는 네이티브 메서드 호출을 허용한다.
  • ReflectionPermission은 ReflectionPermissionFlag 플래그 열거형 인수를 받는다. 그 열거형에는 MemberAccess라는 멤버와 RestrictedMemberAccess라는 멤버가 있다.
    • 다른 어셈블리를 모래상자 안에서 실행하되 LINQ to SQL 같은 API들에 필요한 반영 기능을 허용하고 싶다면 후자가 더 안전한 선택이다.
형식 허용하는 연산
FileIOPermission 파일 및 디렉터리 읽기/쓰기
FileDialogPermission 파일 열기 대화상자나 파일 저장 대화상자로 선택한 파일의 읽기/쓰기
IsolatedStorageFilePermission 응용 프로그램 자신의 격리된 저장소 읽기/쓰기
ConfigurationPermission 응용 프로그램 구성 파일 접근
SqlClientPermission, OleDbPermission, OdbcPermission SqlClient나 OleDb, Odbc 클래스를 이용한 데이터베이스 서버 통신
DistributedTransactionPermission 분산 트랜잭션 참여

 

  • FileDialogPermission은 OpenFileDialog 클래스와 SaveFileDialog 클래스에 대한 접근을 제어한다. 이 클래스들은 Microsoft.Win32(WPF 응용 프로그램용)과 System.Windows.Forms(Windows Forms 응용 프로그램용)에 정의되어 있다.
    • 그리고 해당 대화상자들을 실제로 띄우러면 UIPermission 권한도 필요하다.
    • 단, OpenFileDialog나 SaveFileDialog 객체에 대해 OpenFile을 호출해서 파일(대화상자에서 사용자가 선택한)에 접근할 때 FileIOPermission 권한은 필요하지 않다.
형식 허용하는 연산
DnsPermission DNS 조회
WebPermission WebRequest 기반 네트워크 접근
SocketPermission Socket 기반 네트워크 접근
SmtpPermission SMTP 라이브러리를 이용한 메일 전송
NetworkInformationPermission Ping이나 NetworkInterface 같은 클래스 사용

 

형식 허용하는 연산
DataProtectionPermission Windows 자료 보호 메서드 사용
KeyContainerPermission 공개키 암호화와 서명
StorePermission XL509 인증서 저장소 접근

 

형식 허용하는 연산
UIPermission 창 생성 및 클립보드 접근
WebBrowserPermission WebBrowser 컨트롤 사용
MediaPermission WPF의 이미지, 오디오, 비디오 지원 기능
PrintingPermission 프린터 접근

 

형식 허용하는 연산
EventLogPermission Windows 이벤트 로그 읽기/쓰기
PerformanceCounterPermission Windows 성능 카운터 사용

 

  • 이러한 권한 형식들에 대한 Demand는 .NET Framework 안에서 강제된다. 또한 독자의 코드에서 직접 Demand를 강제하도록 고안된 권한 클래스들도 있다.
    • 이들 중 가장 중요한 클래스들은 호출하는 어셈블리의 신원 확립과 관련된 것들인데, 아래 표에 그런 클래스들이 나와 있다.
    • 주의할 점은 (다른 모든 CAS 권한과 마찬가지로) 만일 응용 프로그램이 완전 신뢰하에 실행되면 Demand가 항상 성공한다는 점이다.
형식 강제하는 조건
GetIdentityPermission 어셈블리가 GAC에 적재되어 있음
StrongNameIdentityPermission 호출하는 어셈브릴에 특정한 강력한 이름이 있음
PublisherIdentityPermission 호출하는 어셈블리가 특정한 인증서로 Authenticode 서명되었음

 

코드 접근 보안의 적용 방식

  • Windows 셸이나 명령 프롬프트에서 실행한 .NET 실행 파일은 무제한의 권한으로 실행된다. 이를 완전 신뢰(full trust)라고 부른다.
  • 어셈블리를 다른 호스팅 환경(SQL Server CLR 통합 호스트나 ASP.NET, ClickOnce, 커스텀 호스트 등)에서 실행하면, 그 호스트가 어셈블리에게 권한들을 부여한다.
    • 어떤 형태로든 호스트가 권한들을 제한하는 상황을 가리켜 부분 신뢰(partial trust) 또는 모래상자 적용(sandboxing)이라고 부른다.
  • 엄밀히 말하면 호스트가 어셈블리의 권한들을 제한하는 것은 아니다. 호스트는 제한된 권한들로 응용 프로그램 도메인을 만들고 다시 말해 도메인 자체에 모래상자를 적용하고 그러한 도메인 안에 어셈블리를 적재한다.
    • 따라서 그 도메인에 적재되는 다른 모든 어셈블리(독자의 어셈블리가 참조하는 어셈블리 등)는 같은 모래상자 안에서 같은 권한 집합으로 실행된다. 단 다음 두 종류의 어셈블리는 예외이다.
      • GAC에 등록된 어셈블리(.NET Framework 어셈블리들 포함)
      • 호스트가 완전히 신뢰한다고 지정한 어셈블리
  • 이 두 부류의 어셈블리는 완전히 신뢰된 어셈블리로 간주되며, 자신이 원하는 권한에 대해 Assert를 호출함으로써 일시적으로 모래상자에서 벗어날 수 있다.
    • 또한 이런 어셈블리들은 다른 완전 심뢰 어셈블리에 있는 [SecurityCritical] 특성이 부여된 메서드를 호출할 수 있으며, 안전성을 검증할 수 없는 코드(unsafe 코드)를 실행하거나 LinkDemand를 강제하는 메서드를 호출할 수 있다. 그리고 그런 LinkDemand는 항상 성공한다.
  • 따라서 부분 신뢰 어셈블리가 완전 신뢰 어셈블리를 호출한다는 것은 모래상자 안의 응용 프로그램 도메인에서 실행되는 어셈블리가 GAC 어셈블리 또는 호스트가 ‘완전 신뢰’라고 지정한 어셈블리를 호출한다는 뜻이다.

완전 신뢰 여부 판정

  • 현재 코드가 무제한의 권한을 가지고 있는지는 다음과 같이 알아낼 수 있다.
new PermissionSet(PermissionState.Unrestricted).Demand();
  • 만일 현재 응용 프로그램 도메인이 모래상자 안에 있으면 이 코드는 예외를 던진다. 그러나 응용 프로그램 도메인이 모래상자 안에 있다고 해도 현재 어셈블리 자체는 사실은 완전 신뢰 어셈블리일 수도 있으며, 그런 경우에는 Assert를 이용해서 모래상자에 벗어날 수 있다.
    • 완전 신뢰 어셈블리 여부는 해당 Assembly 객체의 IsFullTrusted 속성으로 알아낼 수 있다.

부분 신뢰 호출자 허용

  • 부분적으로만 신뢰된 코드가 어셈블리의 메서드를 호출할 수 있으면 특권 상승 공격(elevation of privilege attack)의 여지가 생긴다. 따라서 CLR은 어셈블리가 명시적으로 요청하지 않는 한 그러한 호출을 금지한다.
    • 왜 그런지 이해하려면 우선 특권 상승 공격이 무엇인지부터 이해할 필요가 있다.

특권 상승 공격

  • 독자가 완전 신뢰 상황만 고려해서 어떤 라이브러리를 작성했으며, 라이브러리의 한 클래스에 다음과 같은 속성이 있다고 하자.
public string ConnectionString => File.ReadAllText(_basePath + "cxString.txt");
  • 라이브러리의 사용자가 해당 어셈블리를 GAC에 적재하고(좋은 의도로든 나쁜 의도로든) 그와는 전혀 무관한 응용 프로그램을 ClickOnce 또는 ASP.NET 호스트의 제한적인 모래상자 안에서 실행한다고 하자.
    • 만일 모래상자 안의 응용 프로그램이 독자의 완전 신뢰 어셈블리를 적재해서 ConnectionString 속성을 호출하려 하면, 다행히 SecurityException 예외가 발생한다.
    • File.ReadAllText는 호출자에게는 없는 FileIOPermission을 요구하기(Demand) 때문이다(Demand는 호출 스택을 따라 올라가면서 권한을 점검한다는 점을 기억할 것이다)
    • 그러나 다음과 같은 메서드는 어떨까?
public unsafe void Poke(int offset, int data)
{
  int* target = (int*)_origin + offset;
  *target = data;
  ...
}
  • 만일 앞에서 말한 CLR의 규칙이 없다면, 즉 CLR이 이런 메서드의 호출에 대해 암묵적으로 Demand를 적용하지 않는다면, 모래상자 안의 어셈블리는 아무 문제 없이 이 메서드를 호출할 수 있다.
    • 이런 허점을 이용해서 시스템에 해를 입히는 것을 특권 상승(elevation of privilege) 공격이라고 한다.
  • 이 경우 근본 문제는 독자가 라이브러리를 작성할 때 부분 신뢰 어셈블리가 그 라이브러리를 호출하는 경우를 고려하지 않았다는 것이다. 다행히 CLR은 그런 상황을 기본적으로 방지해 준다.

APTCA와 [SecurityTransparent] 특성

  • 특권 상승 공격 방지를 위해 CLR은 부분 신뢰 어셈블리의 완전 신뢰 어셈블리 호출을 기본적으로 불허한다.
  • 그런 호출을 허용하려면 완전 신뢰 어셈블리에서 다음 둘 중 하나를 해야 한다.
    • [AlloPartiallyTrustedCaller] 특성(줄여서 APTCA)을 적용한다.
    • [SecurityTransparent] 특성을 적용한다.
  • 어셈블리에 이 특성들을 적용하면 어셈블리가 불신의 주체가(불신의 대상이 아니라) 될 수도 있음을 반드시 고려해야 한다.
  • 버전 4.0 이전의 CLR들은 APTCA 특성만 지원했다. 그리고 당시 APTCA 특성은 부분 신뢰 호출자를 허용하는 효과만 냈다.
    • 그러나 CLR 4.0부터 APTCA는 암묵적으로 어셈블리의 모든 메서드(그리고 모든 함수)를 보안에 투명하게 만드는 효과도 낸다.
    • 보안 투명성에 대해서는 다음 절에서 자세히 설명하겠다. 일단 지금은 보안에 투명한 메서드는 다음과 같은 일들을 전혀 하지 못한다는(부분 신뢰 상황 뿐만 아니라 완전 신뢰 상황에서도) 점만 알면 된다.
      • 안전성을 검증할 수 없는 코드(unsafe 코드)의 실행
      • P/Invoke나 COM을 통한 네이티브 코드 실행
      • 보안 수준 상승을 위한 권한 단언(Assert)
      • 링크 요구 만족(LinkDemand)
      • [SecurityCritical]이 부여된 .NET Framework 메서드 호출. 본질적으로 그런 메서드들은 적절한 안전장치나 보안 점검 없이 앞의 네 가지 중 하나를 수행하는 메서드들이다.
    • 이런 제약에 깔린 근거는 일반적으로 이런 일들을 전혀 하지 않는 어셈블리는 특권 상승 공격을 시도할 가능성이 작다는 것이다.
  • [SecurityTransparent] 특성은 이상의 규칙들의 좀 더 강력한 버전을 적용한다.
    • APTCA와 다른 점은, APTCA에서는 어셈블리의 일부 메서드들을 불투명으로 지정할 수 있지만, [SecurityTransparent]에서는 모든 메서드가 투명해야 한다는 것이다.
    • 만일 독자의 어셈블리에 [SecurityTransparent]를 적용해도 어셈블리가 잘 작동한다면 라이브러리 작성자로서의 일은 끝난 것이다.
  • 특정 메서드를 불투명으로 지정하는 방법을 설명하기 전에, 우선 이 특성들을 언제 적용하는 것이 좋은지부터 살펴보자.
    • 첫째(그리고 더 자명한) 시나리오는 독자의 완전 신뢰 어셈블리가 부분 신뢰 도메인에서도 작동하게 하고 싶을 때이다.
    • 둘째(덜 자명한) 시나리오는 독자의 어셈블리가 어떤 환경에 쓰일지 모르는 경우이다. 예컨대 객체-관계 매핑(ORM) 라이브러리를 만들어서 인터넷에서 판매한다고 하자. 그런 경우 고객이 라이브러리를 호출하는 방식은 다음 세 가지이다.
      • 완전 신뢰 환경에서 호출한다.
      • 모래상자 안의 도메인에서 호출한다.
      • 모래상자 안의 도메인에서 호출하되, 독자의 어셈블리를 완전 신뢰로 만든다(이를테면 GAC에 적재해서)
    • 세 번째 옵션을 간과하기 쉬우며 그런 경우 도움이 되는 것이 바로 투명성 모형이다.

투명성 모형

  • 어셈블리가 완전 신뢰 어셈블리가 된 후 부분 신뢰 코드에서 그 어셈블리를 호출할 여지가 있을 때, 보안 투명성 모형(transparency model)을 이용하면 어셈블리의 보안을 손쉽게 보장할 수 있다.
  • 이해를 돕기 위해 부분 신뢰 어셈블리를 유죄가 입증되어서 교도소에 간 수감자에 비유하자.
    • 수감자는 교도소 안에서 품행이 좋으면 일련의 특권(권한)을 얻을 수 있음을 알게 되었다. 이를테면 TV 시청이나 실외 운동 같은 권한들이 있다.
    • 그러나 절대 허용되지 않는 활동들도 있는데, 이를테면 TV 시청실 열쇠를 획득할 수는 없다. 그런 활동(메서드)이 허용되면 전체 보안 시스템이 무력해질 수 있기 때문이다. 그런 메서드들을 보안에 중요한(security-critical) 메서드라고 부른다.
  • 완전 신뢰 라이브러리를 작성할 때는 라이브러리의 보안 중요 메서드들을 보호하는 것이 바람직하다.
    • 한 가지 방법은 다음 예처럼 완전 신뢰 호출자만 보안 중요 메서드를 호출할 수 있다는 요구조건을 거는 것이다(Demand). 이는 CLR 4.0 이전에 쓰이던 접근 방식이다.
[PermissionSet (SecurityAction.Demand, Unrestricted = true)]
public Key GetTVRoomKey() { ... }
  • 그러나 이 접근방식에는 두 가지 문제점이 있다. 첫째로 Demand는 호출 스택을 따라 올라가면서 권한을 점검하기 때문에 속도가 느리다.
    • 종종 보안에 중요한 메서드가 성능한 중요한 (performance-ciritical) 메서드이기도 하므로, 이 점이 문제가 된다. 루프에서 보안 중요 메서드를 거듭 호출할 때는 Demand가 특히나 소모적일 수 있다. (그리고 그 루프가 .NET Framework의 다른 완전 신뢰 어슴블리에 있을 수도 있다.)
    • 이 문제에 대한 CLR 2.0의 해결책은 직접적인 호출자만 점검하는 링크 요구를 강제하는 것이었다. 그러나 여기에도 대가가 따른다. 보안을 유지하려면 링크 요구가 적용되는 메서드를 호출하는 메서드가 스스로 요구 또는 링크 요구를 수행해야 한다.
    • 그렇게 하지 않으려면 신뢰 수준이 낮은 단위에서 호출되었을 떄에도 잠재적인 위험이 있는 일을 전혀 허용하지 않음을 보장하는 감사(auditing)를 검쳐야 한다. 그러나 호출 그래프가 복잡할 때는 그러한 감사가 큰 부담이 된다.
  • 둘째 문제는 보안 중요 메서드에 대해 요구나 링크 요구를 수행하는 것을 잊기 쉽다는 점이다(이 경우에도 호출 그래프가 복잡하면 문제가 더 심해진다)
    • 보안 중요 함수들이 의도치 않게 수감자들에게 노출되는 일을 방지하는데 CLR이 어떤 형태로둔 도움을 준다면 좋을 것이다.
    • 투명성 모형이 바로 그러한 도움에 해당한다.
  • 투명성 모형의 도입은 코드 접근 보안 정책의 제거와는 완전히 무관하다.

투명성 모형의 작동 방식

  • 투명성 모형에서는 보안 중요 메서드에 [SecurityCritical] 특성을 붙인다.
[SecurityCritical]
public Key GetTVRoomKey() { ... }
  • 모든 위험한 메서드(보안을 침해하고 수감자들의 탈출을 허용할 여지가 있다고 CLR이 간주하는 코드를 담은)에는 반드시 [SecurityCritical] 또는 [SecuritySafeCritical] 특성을 지정해야 한다. 그러한 메서드들은 다음과 같다.
    • 안전성을 검증할 수 없는 메서드(unsafe가 붙은)
    • P/Invoke나 COM을 통해서 비관리 코드(unmanaged code)를 호출하는 메서드
    • Assert로 권한을 얻거나 링크 요구 메서드를 호출하는 메서드
    • [SecurityCritical]이 지정된 메서드를 호출하는 메서드
    • [SecurityCritical]이 지정된 가상 메서드를 재정의하는 메서드
  • [SecurityCritical] 특성은 ‘이 메서드는 부분 신뢰 호출자가 모래상자를 탈출하게 만들 수 있음’을 뜻한다.
  • [SecuritySafeCritical] 특성은 ‘이 메서드는 보안에 중요한 연산을 수행하지만 적절한 안전장치가 있으므로 부분 신뢰 코드에서 호출해도 안전함’을 뜻한다.
  • 부분 신뢰 어셈블리의 메서드는 절대로 완전 신뢰 어셈블리의 보안 중요 메서드를 호출할 수 없다. [SecurityCritical] 메서드를 호출할 수 있는 메서드는 다음 두 종류 뿐이다.
    • 다른 [SecurityCritical] 메서드
    • [SecuritySafeCritical]이 지정된 메서드
  • 보안 안전 중요[security-safe critical) 메서드 즉 [SecuritySafeCritical]이 지정된 메서드는 보안 중요 메서드들에 대한 문지기 역할을 한다.
    • 보안 안전 중요 메서드는 모든 어셈블리(부분 신뢰이든 완전 신뢰이든. 단, 권한 기반 CAS 요구조건은 적용됨)의 모든 메서드에서 호출할 수 있다.
    • 이해를 돕는 예로 수감자가 TV를 보려고 한다고 하자. 수감자는 WatchTV 메서드를 호출하며, 그 메서드는 보안에 중요한 메서드인 GetTVRoomKey를 호출한다. 따라서 WatchTV는 반드시 보안 안전 중요 메서드이어야 한다.
[SecuritySafeCritical]
public void WatchTV()
{
  new TVPermission().Demand();
  using (Key key = GetTVRoomKey())
    PrisonGuard.OpenDoor(key);
}
  • 호출자가 실제로 TV 시청 권한이 있는지 확인하기 위해 TVPermission에 대해 명시적으로 Demand를 호출함을 주목하기 바란다. 또한 TV 시청실 키가 제대로 처분되도록 using 문을 사용했다는 점도 중요하다.
    • 이 메서드는 보안 중요 메서드를 감싸며 이 메서드를 그 누가 호출해도 해당 활동이 안전하게 진행되게 하는 장치를 갖추고 있다.

  • CLR이 ‘위험하다’고 간주하는 활동에 참여하는 메서드 중에는 실제로는 위험하지 않은 것들도 있다. 그런 메서드들에는 [SecurityCritical] 대신 [SecuritySafeCritical]를 직접 지정해도 된다.
    • 좋은 예가 Array.Copy 메서드이다. 효율성을 위해 이 메서드의 일부 구현은 비관리 코드를 사용하지만, 그 코드를 부분 신뢰 호출자가 악용할 여지는 없다.

UnsafeXXX 패턴

  • TV 시청 예제에는 잠재적인 비효율성이 존재한다. 만일 간수가 WatchTV 메서드를 통해서 TV를 보고 싶다면, 간수는 반드시(그리고 불필요하게) TVPermission 요구조건을 만족해야 한다.
    • 이에 대해 CLR 팀은 메서드의 두 가지 버전을 정의하는 패턴을 해결책으로 제시한다. 첫 버전은 이름이 Unsafe로 시작하는 보안 중요 메서드이다.
[SecuritySafeCritical]
public void UnsafeWatchTV()
{
  using (Key key = GetTVRoomKey())
    PrisonGuard.OpenDoor(key);
}
  • 둘째 버전은 보안 안전 중요 메서드로 호출 스택을 완전히 점검하는 Demand를 거친 후에 첫 버전을 호출한다.
[SecuritySafeCritical]
public void WatchTV()
{
  new TVPermission().Demand();
  UnsafeWatchTV();
}

투명한 코드

  • 투명성 모형에서 모든 메서드는 다음 세 범주로 나뉜다.
    • 보안 중요 메서드
    • 보안 안전 중요 메서드
    • 둘 다 아닌 투명한(transparent) 메서드
  • 투명한 메서드라는 이름은 특권 상승 공격을 위한 코드 감사가 필요 없을 정도로 숨길 것이 없는 메서드라는 점에서 비롯된 것이다.
    • 코드 감사는 [SecuritySafeCritical] 메서드(문지기)들에만 집중하면 된다. 대체로 어셈블리의 전체 메서드 중 그런 메서드가 차지하는 비율은 비교적 낮다.
    • 어셈블리가 투명한 메서드들로만 이루어져 있다면, 다음처럼 어셈블리에 [SecurityTransparent] 특성을 부여할 수 있다.
[assembly: SecurityTransparent]
  • 이를 두고 어셈블리 자체가 투명하다고 말한다. 투명한 어셈블리는 특권 상승 공격에 대한 감사가 필요 없으며, 부분 신뢰 호출자들을 암묵적으로 허용한다. 즉, APTCA를 적용할 필요가 없다.

어셈블리의 기본 투명성 설정

  • 지금까지의 논의를 요약하자면 어셈블리 수준에서 투명성을 지정하는 방법은 다음 두 가지다.
    • 어셈블리에 APTCA 특성을 적용한다. 이 경우 암묵적으로 모든 메서드가 투명해지며, 필요하다면 특정 메서드들만 불투명(보안 중요 또는 보안 안전 중요)으로 지정할 수 있다.
    • 어셈블리에 [SecurityTransparent] 특성을 적용한다. 이 경우 모든 메서드가 예외 없이 투명해 진다.
  • 그 외에 투명성과 관련해서 그냥 아무것도 하지 않을 수도 있다. 이 방법도 APTCA 처럼 선택적 허용에 해당한다.
    • 단, 이 경우에는 모든 메서드가 암묵적으로 보안 중요 메서드(SecurityCiritical)가 된다. (단, 어셈블리가 재정의한 가상 [SecuritySafeCritical] 메서드들은 그대로 보안 안전 중요 메서드로 남는다)
    • 결과적으로 독자의 어셈블리에서는 다른 모든 메서드를 호출할 수 있지만(어셈블리가 완전 신뢰라 할 때) 다른 어셈블리의 투명한 메서드는 독자의 어셈블리를 호출할 수 없다.

투명성 모형과 APTCA 특성의 조합

  • 투명성 모형을 따르려면 우선 독자의 어셈블리에서 잠재적으로 ‘위험한’ 메서드들을 찾아내야 한다. CLR은 심지어 완전 신뢰 환경에서도 그런 메서드의 실행을 거부하므로, 단위 검사를 통해서 그런 메서드들을 골라내는 것이 가능하다. (.NET Framework에는 이 과정을 돕는 SecAnnotate.exe라는 도구가 있다). 그런 메서드들 각각에 다음 두 특성 중 하나를 적절히 부여한다.
    • [SecurityCritical] – 만일 신뢰 수준이 낮은 어셈블리에서 그 메서드를 호출할 때 어떤 피해가 발생할 수 있으면
    • [SecuritySafeCritical] – 만일 그 메서드가 적절한 점검/안전장치를 갖추었으며, 산뢰 수준이 낮은 어셈블리에서 호출해도 안전하다면
  • 이해를 돕는 예로 .NET Framework의 한 보안 중요 메서드를 호출하는 다음과 같은 메서드를 생각해 보자.
public static void LoadLibraries()
{
  GC.AddMemoryPressure(1000000);  // 보안 중요 메서드 호출
}
  • 신뢰 수준이 낮은 호출자가 불순한 의도로 이 메서드를 거듭 호출해서 시스템에 피해를 줄 수 있다.
    • 이를 방지하기 위헤 메서드에 [SecurityCritical] 특성을 적용할 수도 있지만, 그러면 다른 신뢰된 어셈블리는 오직 보안 중요 또는 보안 안전 중요 메서드에서만 이 메서드를 호출할 수 있게 된다.
    • 더 나은 방법은 메서드 자체를 안전하게 만들고 [SecuritySafeCritical] 특성을 부여하는 것이다.
static bool _loaded;

[SecuritySafeCritical]
public static void LoadLibraries()
{
  if (_loaded) return;
  _loaded = true;
  GC.AddMemoryPressure(1000000); 
  ...
}
  • 이렇게 하면 신뢰된 호출자 역시 이 메서드를 좀 더 안전하게 사용하라 수 있게 된다는 장점도 생긴다.

unsafe 메서드의 보안

  • 이번에는 신뢰 수준이 낮은 어셈블리에서 호출할 경우 잠재적으로 위험한 unsafe 메서드의 예를 보자. 그런 경우 먼저 메서드 자체에 [SecurityCritical]를 부여하고
[SecurityCritical]
public unsafe void Poke(int offset, int data)
{
  int* target = (int*)_origin + offset;
  *target = data;
  ...
}
  • 투명한 메서드 안에 안전하지 않은 코드가 있으면 CLR은 메서드를 호출하기 전에 VerificationException 예외(오류 메세지는 연산 때문에 런타임이 불안정해질 수 있다는 뜻의 “Operation could destabilize the runtime”)를 던진다.
  • 그런 다음에는 호출 사슬을 따라 올라가면서(상향) 각 메서드에 [SecurityCritical]이나 [SecuritySafeCritical]을 적절히 지정하면 된다.
  • 또 다른 예로 다음의 unsafe 메서드는 비트맵에 필터를 적용한다. 이 연산은 본질적으로 해가 없으므로 [SecuritySafeCritical] 특성을 지정하면 된다.
[SecuritySafeCritical]
unsafe void BlueFilter(int[,] bitmap)
{
  int length = bitmap.Length;
  fixed(int* b = bitmap)
  {
    int* p = b;
    for (int i = 0; i < length; i++)
      *p++ &= 0xFF;
  }
}
  • 반대로 CLR의 관점에서는 ‘위험한’ 일을 전혀 하지 않지만 그래도 보안상의 위험 요소가 되는 코드가 있는데, 다음 예에서처럼 그런 코드에서 [SecurityCritical]을 지정할 수 있다.
public string Password
{
  [SecurityCritical] get { return _password; }
}

P/Invokes와 [SuppressUnmanagedSecurity] 특성

  • 마지막으로 Point 객체를 받고 창 핸들을 돌려주는 다음과 같은 비관리 메서드를 생각해 보자.
[DllImport("user32.dl")]
public static extern IntPtr WindowFromPoint(Point point);
  • 비관리 코드는 [SecurityCritical] 메서드나 [SecuritySafeCritical] 메서드에서만 호출할 수 있음을 기억하기 바란다.
  • 이 WindowFromPoint 메서드는 public으로 선언되어 있으므로 다른 완전 신뢰 어셈블리의 모든 [SecurityCritical] 메서드가 이 메서드를 직접 호출할 수 있다. 부분 신뢰 호출자들을 위해서는 다음과 같은 보안 버전을 제공한다.
    • 보안 침해를 방지하기 위해, 이 버전은 Demand를 통해 UI 권한을 요구하고 IntPtr 대신 관리되는 클래스의 객체를 돌려준다.
[UIPermission(SecurityAction.Demand, Unrestricted = true)]
[SecuritySafeCritiacal]
public static System.Windows.Froms.Control ControlFromPoint(Point point)
{
  IntPtr winPtr = WindowFromPoint(point);
  if (winPtr == IntPtr.Zero)  return null;
  return System.Windows.Forms.Form.FromChildHandle(winPtr);
}
  • 그런데 부분 신뢰 호출자가 이 메서드를 호출할 수 있으려면 한 가지 문제를 더 해결해야 한다. P/Invoke를 사용할 때마다 CLR은 비관리 권한에 대해 암묵적으로 Demand를 적용하는데, Demand는 호출 스택을 따라 올라가면서 권한들을 점검하므로 만ㅇ리 WindowFromPoint 메서드 호출자의 호출자가 부분 신뢰이면 권한 점검이 실패한다.
  • 이 문제를 해결하는 방법은 두 가지이다. 첫째는 ControlFromPoint 메서드의 첫 줄에 있는 비고나리 코드를 위한 권한을 다음과 같은 단언(Assert)을 통해서 획득하는 것이다.
new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Assert();
  • 어셈블리가 부옇나 비관리 코드 실행 권한을 이러한 단언을 통해서 획득하고 나면, 이후에 WindowFromPoint 호출 시에 일어나는 암묵적 Demand가 성공하게 된다. 물론 어셈블리 자체가 완전 신뢰가 아니면(즉, GAC에 적재되지 않았거나 호스트가 완전 신뢰로 지정하지 않았다면) 그러한 단언이 실패할 것이다.
  • 둘째 (그리고 성능이 더 좋은) 해결책은 비관리 코드에 [SuppressUnmanagedCodeSecurity] 특성을 부여하는 것이다.
[DllImport("user32.dl"), SuppressUnmanagedCodeSecurity]
public static extern IntPtr WindowFromPoint(Point point);
  • 이렇게 하면 CLR은 호출 스택을 훑는 고비용 비관리 Demand 과정을 생략한다. (이는 만일 WindowFromPoint를 다른 신뢰된 클래스나 어셈블리에서 호출했다면 특히나 가치가 있는 최적화일 것이다) 따라서 ControlFromPoint에 있는 비관리 실행 권한 단언은 제거해도 된다.
  • 지금 논의에서처럼 투명성 모형을 따르는 경우 extern 메서드에 이 특성을 적용해도 CLR 2.0에서 문제가 되었던 보안 위험은 발생하지 않는다. 이는 P/Invoke를 사용하는 메서드는 여전히 암묵적으로 보안 중요 메서드에 해당하며, 따라서 오직 보안 중요 또는 보안 안전 중요 메서드에서만 호출할 수 있기 때문이다.
  • 모든 extern 메서드는 암묵적으로 [SecurityCritical] 메서드라고 보아도 무방하다. 단, 명시적으로 [SecurityCritical]을 지정한 extern 메서드의 경우에는 보안 점검이 실행시점에서 JIT 시점으로 앞당겨진다는 미묘한 차이가 있다.
    • 예컨대 다음과 같은 메서드를 생각해 보자.
static void Foo (bool exec)
{
  if (exec) WindowFromPoint(...)
}
  • false로 이 메서드를 호출하면 오직 WindowFromPoit에 [SecurityCritical]이 지정된 경우에만 보안 점거이 일어난다.

완전 신뢰 환경의 투명성

  • 완전 신뢰 어셈블리들만 있는 환경에서 실행되는 보안 중요 코드를 작성할 때에는 보안 특성들을 부여하거나 메서드 감사를 수행하는 번거로움을 피하고 싶을 것이다.
    • 그런 경우 가장 쉬운 방법은 그냥 어셈블리에 그 어떤 보안 특성도 지정하지 않는 것이다. 그러면 어셈블리의 모든 메서드에 암묵적으로 [SecurityCritical]이 적용된다.
  • 모든 관련 어셈블리가 그런 식으로 만들어져 있거나, 아니면 투명성 모형을 따르는 어셈블리들이 호출 그래프의 최하단에 있기만 하다면 이런 방식이 잘 통한다.
    • 다른 말로 하면 독자의 어셈블리에서 여전히 서드파티 라이브러리(그리고 .NET Framework)의 투명한 메서드들을 호출할 수 있다.
  • 그 반대 방향으로 가려면 몇 가지 문제를 해결해야 한다. 그러나 그런 문제들을 해결하다 보면 더 나은 해법을 발견하는 경우도 많다. 예컨대 부분적으로 또는 완전히 투명한 T라는 어셈블리에서 보안 특성이 전혀 부여되지 않은 (따라서 모든 메서드가 보안 중요 메서드인) 어셈블리 X를 호출하고 싶다고 하자. 이 경우 T의 작성자가 선택할 수 있는 옵션은 다음 세가지 이다.
    • T의 모든 메서드를 보안 중요 메서드로 만든다. T가 실행될 응용 프로그램 도메인이 항상 완전 신뢰 도메인이라면 부분 신뢰 호출자들을 지원할 필요가 없다. 그런 경우 부분 신뢰 호출자를 명시적으로 지원하지 않는 것이 합당하다.
    • X의 각 메서드를 감싸는 [SecuritySafeCritical] 메서드들을 작성한다. 그런 메서드들은 주목해야 할 보안 취약점들에 해당한다. (단 이 방법은 프로그래머의 부담이 크다)
    • X의 작성자에게 투명성 모형을 고려해 달라고 요청한다. 만일 X가 보안에 중요한 일을 전혀 하지 않는다면, 그냥 X에 [SecurityTransparent] 특성을 지정하기만 하면 된다.
      • 만일 X가 보안에 중요한 연산을 수행한다면, 투명성 모형을 따르기 위해서는 X의 작성자가 적어도 X의 보안 취약점들을 식별해 주어야 (해결하지는 않더라도) 한다.

CLR 2.0의 보안 정책

  • CLR 4.0 이전에는 CLR이 복잡한 규칙들과 대응 관계들에 기초해서 일단의 기본 권한들을 .NET Framework 어셈블리들에 부여했다. CAS(코드 접근 보안) 정책이라고 불렀던 이 권한 집합은 컴퓨터의 .NET Framework 구성 파일들에 정의되어 있었다. 정책 평가 (조직, 컴퓨터, 사용자, 응용 프로그램 도메인 수준에서 커스텀화 할 수 있다)의 결과로 다음 세 가지 표준 권한 집합 중 하나가 어셈블리에 부여된다.
    • FullTrust(‘완전 신뢰’): 지역 하드 드라이브에서 실행되는 어셈블리에 부여된다.
    • LocalIntranet(‘지역 인트라넷’): 네트워크 공유 폴더에서 실행된 어셈블리에 부여된다.
    • Internet(‘인터넷’): Internet Explorer 안에서 실행된 어셈블리에 부여된다.
  • 기본적으로 완전 신뢰 어셈블리에 해당하는 것은 FullTrust 집합 뿐이다. 따라서 네트워크 공유 폴더에 있는 .NET 응용 프로그램 실행 파일(LocalIntranet에 해당)을 실행하면 그 응용 프로그램은 제한된 권한 집합으로 실행되며, 따라서 보안 점검에 실패할 가능성이 크다.
    • 원래 이는 일종의 보호 장치로 고안된 것이지만, 실제로는 아무런 보호 기능도 하지 않는다. 왜냐면 불순한 의도를 가진 공격자가 그냥 .NET 실행 파일을 비관리 실행 파일로 대체하기만 하면 권한에 의한 제약이 사라지기 때문이다.
    • 결과적으로 이러한 보호 장치는 네트워크 공유를 통해서 .NET 어셈블리를 완전 신뢰 하에 실행하려고 했던 사용자를 짜증나게 만들 뿐이었다.
  • 그래서 CLR 4.0 의 설계자들은 이러한 보안 정책을 아예 폐기하기로 했다. 이제 모든 어셈블리는 전적으로 호스팅 환경이 정의하는 권한 집합 안에서 실행된다. Windows 탐색기에서 실행 파일을 더블 클릭하거나 명령줄에서 실행 파일 경로를 입력해서 실행한 프로그램은 항상 완전 신뢰 환경에서 실행된다. 이는 실행 파일이 네트워크 공유 폴더에 있든 지역 하드 드라이브에 있든 마찬가지이다.
  • 다른 말로 하면 이제는 응용 프로그램에 주어지는 권한들을 전적으로 호스트가 결정한다. 컴퓨터에 설정된 CAS 정책은 여기에 전혀 관여하지 않는다.
  • 여전히 CLR 2.0의 보안 정책을 다루어야 한다면(이를테면 .NET Framework 버전 3.5 이하를 대상으로 하는 실행 파일을 만들어야 하는 경우) Microsoft 관리 콘솔(MMC)의 mscorcfg.msc 플러그인(제어판 -> 관리도구 -> Microsoft.NET Framework Configuration) 또는 명령줄 도구 caspol.exe를 이용해서 보안 정책을 조회하거나 수정할 수 있다.
    • 이제는 mscorcfg.msc 플러그인이 .NET Framework와 함께 설치되지 않음을 주의하기 바란다. 이 플러그인을 사용하려면 .NET Framework 3.5 SDK를 설치해야 한다.
  • 궁극적으로 보안 정책 설정은 .NET Framework 구성 폴더에 있는 security.config라는 XML 파일에 저장된다. 그 파일의 구체적은 경로는 다음과 같이 얻을 수 있다.
string dir = Path.Combine(System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(), "config");
string configFile = Path.Combine(dir, "security.config");

다른 어셈블리에 모래상자 적용

  • 사용자가 서드파티 플러그인을 설치할 수 있는 응용 프로그램을 작성한다고 하자. 만일 플러그인이 응용 프로그램과 같은 특권들을 가지고 실행되게 한다면, 응용 프로그램 또는 최종 사용자의 컴퓨터의 안전성을 해칠 수 있다. 따라서 플러그인들의 권한을 제한해야 한다.
    • 가장 좋은 방법은 각 플러그인을 개별적인 응용 프로그램 도메인에서 실행하되, 그 도메인을 모래상자 안에 넣는 것이다.
  • 이번 절의 예제에서는 플러그인이 plugin.exe라는 이름의 .NET 어셈블리로 제공되며, 그냥 그 실행 파일을 실행하면 플러그인이 활성되된다고 가정한다.
  • 다음은 호스트 프로그램의 전체 코드이다.
using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Security;
using System.Security.Policy;
using System.Security.Permission;

class Program
{
  static void Main()
  {
    string pluginFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "plugins");
    string pluginPath = Path.Combine(pluginFolder, "plugin.exe");
    PermissoinSet ps = new PermissionSet(PermissionState.None);
    ps.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
    ps.AddPermission(new FileIOPermission(FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read, pluginPath));
    ps.AddPermission(new UIPermission(PermissionState.Unrestricted));

    AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
    AppDomain sandbox = AppDomain.CreateDomain("sbox", null, setup, ps);
    sandbox.ExecuteAssembly(pluginPath);
    AppDomain.Unload(sandbox);
  }
}
  • 이 프로그램은 우선 모래상자에 부여하고자 하는 특권들을 서술하는 제한된 권한 집합을 만든다. 이 권한 집합은 적어도 어셈블리 실행 권한과 플러그인이 자신의 어셈블리를 읽을 수 있는 권한을 포함해야 한다. 그렇게 하지 않으면 어셈블리가 실행되지 않는다. 또한 무제한의 UI 권한도 부여한다.
    • 그런 다음에는 새 응용 프로그램 도메인을 생성하는데, 이때 앞에서 만든 커스텀 권한 집합을 지정한다. 이렇게 하면 그 도메인에 적재되는 모든 어셈블리에 그 권한 집합이 적용된다.
    • 새 도메인을 만든 후에는 그 도메인 안에서 플러그인 어셈블리를 실행한다.
    • 마지막으로 플러그인의 실행이 끝나면 도메인을 해제한다.
  • 이 예제는 응용 프로그램의 한 하위 디렉터리인 plugins 에 있는 플러그인 어셈블리를 적재한다.
    • 플러그인 어셈블리를 완전 신뢰 호스트와 같은 디렉터리에 두면 특권 상승 공격의 여지가 생긴다. 완전 신뢰 도메인이 형식의 환원을 위해 암묵적으로 플러그인 어셈블리의 코드를 적재해서 실행할 수도 있기 때문이다.
    • 예컨대 플러그인이 자신의 어셈블리에 정의된 커스텀 예외 형식의 예외를 던진다고 하자. 그 예외가 호스트까지 전파되면, 호스트는 그 예외의 역직렬화를 위해 암묵적으로 플러그인 어셈블리를 찾아서 자신의 도메인에 적재하려 한다. 플러그인 어셈블리를 호스트와는 다른 디렉터리에 두면 호스트가 어셈블리를 찾지 못하므로 적재에 실패하며, 따라서 특권 상승 공격의 위험이 사라진다.

권한 단언

  • 부분 신뢰 어셈블리에서 호출할 메서드를 작성할 때에는 권한 단언, 즉 Assert 호출을 통한 권한 획득이 유용하다. 권한 단언이 성공하면 어셈블리는 일시적으로 모래상자를 탈출하게 되며, 그러면 하향 Demandㄷ르 때문에 할 수 없었을 연산들을 수행할 수 있게 된다.
  • CAS 세계에서 말하는 ‘단언’은 진단 또는 계약 기반 단언과는 무관하다. 실제로 Debug.Assert 호출은 권한에 대한 Assert 보다는 Demand에 더 가깝다.
    • 특히 어떤 권한을 단언 했을 때 단언이 성공하면 부수 효과가 발생하지만, Debug.Assert는 부수 효과를 일으키지 않는다.
  • 앞에서 서드파티 플러그인을 제한된 권한 집합 안에서 실행하는 응용 프로그램의 예제를 보았다. 그 예제의 연장선에서 이번에는 플러그인이 호출할 수 있는 안전한 메서드들로 이루어진 라이브러리를 만들어서 서드파티들에 제공한다고 하자.
    • 예컨대 플러그인이 데이터베이스에 직접 접근하지는 못하지만, 우리가 제공하는 라이브러리의 메서드들을 통해서 특정 질의를 수행하게 한다거나 로그 파일에 메시지를 기록하는 메서드를 제공하되 파일 기반 권한은 전혀 부여하지 않는 등의 활용이 가능하다.
  • 그러한 라이브러리를 만드는 첫 단계는 라이브러리를 담을 개별적인 어셈블리(이름은 이를테면 utilities 등)를 만들고 그 어셈블리에 [AllowPartiallyTrustedCallers] 특성을 부여하는 것이다. 그런 다음에는 서드파티에 제공할 메서드들을 추가하면 된다. 다음은 로그 기록 메서드의 예이다.
public static void WriteLog(string msg)
{
  // 메시지를 로그 파일에 기록
  ...
}
  • 이 메서드는 한 가지 문제점을 극복해야 한다. 바로 파일에 로그 메시지를 기록하려면 FileIOPermission이 필요하다는 것이다. utilities 어셈블리 자체는 완전 신뢰 어셈블리지만 이것을 호출하는 호출자는 완전 신뢰가 아니므로 모든 파일 권한 관련 Demand가 실패하게 된다. 해결책은 파일 접근 전에 해당 권한을 Assert로 획득하는 것이다.
public class Utils
{
  string _logsFolder = ...;

  [SecuritySafeCritical]
  public static void WriteLog(string msg)
  {
    FileIOPermission f = new FIleIOPermission)PermissionState.None);
    f.AddPathList(FileIOPermissionAccess.AllAccess, _logFolder);
    f.Assert();

    // 메시지를 로그 파일에 기록
    ...
  }
}
  • 권한 단언을 사용하는 메서드에는 반드시 [SecurityCritical] 또는 [SecuritySafeCritical]를 부여해야 한다. 이 메서드는 부분 신뢰 호출자에 대해 안전하므로 [SecuritySafeCritical]를 부여했다. 물론 어셈블리 전체를 [SecurityTransparent]로 할 수는 없다. 대신 APTCA를 사용해야 한다.
  • 이전에 이야기 했듯이, Demand는 현재 코드가 연산에 필요한 권한을 가졌는지 점검해서 권한이 없으면 예외를 던지고 권한이 있으면 호출 스택을 따라 올라가면서 계속해서 권한을 점검한다.
    • 그러나 Assert는 현재 어셈블리가 필요한 권한을 가지고 있는지만 점검한다. 만일 그 점검이 성공하면 그때부터 런타임은 호출자의 권리들은 고려하지 않고 현재 어셈블리의 권리들만 고려해서 권한을 점검한다.
    • 메서드 실행이 끝나거나 코드가 명시적으로 CodeAccessPermission.RevertAssert를 호출하면 Assert의 효과가 끝난다.
  • 예제를 완성해 보자. 다음으로 할 일은 응용 프로그램 도메인을 생성해서 utilities 어셈블리를 완전 신뢰 대상으로 지정한 후 모래상자 안에서 실행하는 것이다.
    • 이를 위해 utilites 어셈블리를 서술하는 StrongName 객체를 생성하고, AppDomain의 CreateDomain 메서드를 호출할 때 그 객체를 지정한다.
static void Main()
{
  string pluginFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "plugins");
  string pluginPath = Path.Combine(pluginFolder, "plugin.exe");
  PermissoinSet ps = new PermissionSet(PermissionState.None);

  // 앞에서처럼 플러그인에게 허용할 권한들을 ps에 추가한다.
  // ...

  Assembly utilAssembly = typeof(Utils).Assembly;
  StringName utils = utilAssembly.Evidence.GetHostEvidence<StrongName>();

  AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
  AppDomain sandbox = AppDomain.CreateDomain("sbox", null, setup, ps, utils);
  sandbox.ExecuteAssembly(pluginPath);
  AppDomain.Unload(sandbox);
}
  • 이 예제가 제대로 작동하려면 utilities 어셈블리를 반드시 강력한 이름으로 서명해야 한다.
  • .NET Framework 4.0 전에는 이 예제처럼 GetHostEvidence를 호출해서 StrongName을 얻을 수는 없었고, 대신 다음과 같이 해야 했다.
AssemblyName name = utilAssembly.GetName();
StringName utils = new StrongName(
  new StrongNamePublicKeyBlob(name.GetPublicKey()),
  name.Name,
  name.Version);
  • 어셈블리를 호스트의 도메인에 적재하고 싶지 않은 경우엔느 이러한 구식 접근방식이 여전히 유용하다. 다음처럼 Assembly나 Type 객체 없이도 AssemblyName을 얻을 수 있기 때문이다.
AssemblyName name = AssemblyName.GetAssemblyName(@"d:\utils.dll");

운영체제 보안

  • .NET Framework 응용 프로그램이 할 수 있는 일을 운영체제가 사용자의 로그인 특권들에 기초해서 더욱 제약할 수 있다. Windows에서 사용자 계정은 다음 두 종류이다.
    • 지역 컴퓨터에 무제한으로 접근할 수 있는 관리자 계정
    • 관리 기능들과 다른 사용자의 자료에 대한 접근에 제약이 있는 제한된 권한들을 가진 계정
  • Windows Vista에는 UAC(User Access Control: 사용자 계정 컨트롤)라는 기능이 도입되었다. UAC가 활성화된 경우, 관리자는 로그인 시 두 개의 토큰 또는 ‘모자’를 받게 된다. 하나는 관리자가 쓰는 모자이고, 또 하나는 보통 사용자가 쓰는 모자이다.
    • 기본적으로 프로그램은 보통 사용자 모자를 쓴 채로, 다시 말해 제한된 권한들로 실행된다.
    • 그러나 프로그램이 관리자 권한 상승(administrative elevation)을 요청하면 상황이 달라진다. 그런 경우 사용자에게 그 요청을 수락할 것인지를 묻는 대화상자가 나타나며, 사용자가 수락하면 프로그램은 관리자의 모자를 쓴 채로 실행된다.
  • 응용 프로그램 개발자의 관점에서 UAC는 응용 프로그램이 제한된 사용자 특권들로 실행되는 것이 기본이라는 뜻이다. 결과적으로 개발자는 다음 둘 중 하나를 선택해야 한다.
    • 응용 프로그램이 관리자 특권 없이도 실행되게 만든다.
    • 응용 프로그램 매니페스트에서 관리자 권한 상승을 요구한다.
  • 첫 선택이 더 안전하며, 사용자도 편하다. 프로그램이 관리자 특권 없이 실행되게 설계하는 것은 대부분의 경우 어렵지 않다. 관련 제약들이 전형적인 코드 접근 보안 모래상자가 가하는 제약들보다는 훨씬 덜 가혹하다.
  • 프로그램이 현재 관리자 계정으로 실행되는지는 다음 메서드로 알아낼 수 있다.
[DllImport("shell32.dll", EntryPoint="#680")]
static extern bool IsUserAnAdmin();
  • UAC가 활성화되어 있는 경우 이 메서드는 현재 프로세스가 관리자 계정으로 권한이 상승한 경우에만 true를 돌려준다.

표준 사용자 계정으로 실행

  • 다음은 표준 Windows 사용자 로그인에서는 할 수 없는 일 중 중요한 것들이다.
    • 다음 디렉터리에 쓰기
      • 운영체제 시스템 폴더(보통은 \Windows)와 그 하위 디렉터리들
      • 프로그램 파일 폴더(\Program Files)와 그 하위 디렉터리들
      • 운영체제 드라이브릐 루트(이를테면 C:\)
    • 레지스트리의 HKEY_LOCAL_MACHINE 섹션에 쓰기
    • 성능 감시(WMI) 자료 읽기
  • 또한 보통의 사용자 계정에서 실행되는 프로그램은 다른 사용자에 속한 파일이나 자원에 접근하지 못하게 할 수 있다(심지어는 관리자 계정으로 실행되는 프로그램도 그럴 수 있다)
    • Windows는 ACL(Access Control List; 접근 제어 목록) 시스템을 이용해서 그런 자원을 보호한다. 현재 사용자의 ACL들에 설정된 권한들은 System.Security.AccessControl의 형식들을 통해서 조회하거나 단언할 수 있다.
    • ACL은 또한 프로세스 경계를 넘는 대기 핸들에도 적용된다.
  • 운영체제 보안 떄문에 뭔가에 대한 접근이 거부되면 UnauthorizedAccessException 예외가 발생한다. 이것은 .NET의 권한 요구가 실패했을 때 발생하는 SecurityException과는 다른 예외이다.
  • .NET의 코드 접근 권한 클래스들은 ACL과 거의 독립적이다. 예컨대 FileIOPermission에 대한 Demand가 성공해도 해당 파일에 접근할 때 ACL 제약 때문에 UnauthorizedAccessException이 발생할 수 있다.
  • 대부분 표준 사용자 제약은 다음과 같이 우회하거나 극복할 수 있다.
    • 운영체제가 권장하는 장소에 파일을 저장한다.
    • 파일에 저장할 수 있는 정보는 레지스트리에 저장하지 않는다(단, 항상 읽기/쓰기가 가능한 HKEY_CURRENT_USER 섹션은 제외)
    • ActiveX나 COM 구성요소들을 프로그램 설치 도중에 등록한다.
  • 사용자 문서를 저장하는데 권장되는 장소는 SpecialFolder.MyDocuments이다.
string docsFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string path = Path.Combine(docsFolder, "test.txt");
  • 사용자가 응용 프로그램 바깥에서 수정해야 할 수도 있는 구성 파일들의 권장 저장 장소는 SpecialFolder.ApplicationData(현재 사용자 전용) 또는 SpecialFolder.CommonApplicationData(모든 사용자)이다.
    • 보통은 이 폴더에 해당 조직 또는 회사명과 제품명을 반영한 이름의 하위 디렉터리를 만들어서 거기에 구성 파일들을 저장한다.
  • 응용 프로그램 안에서만 접근할 자료는 격리된 저장소에 저장하는 것이 바람직하다.
  • 아마도 응용 프로그램을 표준 사용자 계정으로 실행할 때 가장 불편한 점은 응용 프로그램 자신의 파일들에 대한 쓰기 권한이 없다는 것이다. 이러한 제약은 이를테면 자동 업데이트 시스템을 구현할 때 걸림돌이 된다.
    • 한 가지 옵션은 ClickOnce를 이용해서 응용 프로그램을 배치하는 것이다. 그러면 관리자 권한 상승 없이도 업데이트를 적용할 수 있다. 대신 응용 프로그램 설치 과정에 중요한 제약이 생긴다(이를테면 ActiveX 컨트롤을 등록할 수 없다.)
    • 배포 방식에 따라서는 ClickOnce로 배치한 응용 프로그램이 코드 접근 보안이 적용된 모래상자 안에서 실행될 수도 있다.

관리자 권한 상승과 가상화

  • 응용 프로그램 매니페스트를 이용하면 프로그램을 실행할 때마다 관리자 권한 상승을 요구하는 대화상자를 사용자에게 제시하라고 Windows에 요청할 수 있다. 다음이 그러한 예이다.
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="requireAdministrator" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>
  • requireAdministrator를 asInvoker로 바꾸면, 해당 요소는 Windows에게 관리자 권한 상승이 필요하지 않음을 알려주라는 뜻이 된다. 그렇게 하면 응용 프로그램 매니페스트를 아예 제공하지 않았을 때와 거의 같은 효과가 난다.
    • 차이점은 지금 경우에는 가상화(vitualization)가 비활성화된다는 것이다. 가상화는 Windows Vista에만 일시적으로 도입된 기능으로 기존 으용ㅇ 프로그램이 관리자 특권 없이도 정확히 실행되게 하기 위한 것이다.
    • 응용 프로그램 매니페스트에 requestedExecutionLevel 요소가 없거나 응용 프로그램 매니페스트 자체가 없으면 이 하위 호환성 기능이 활성화 된다.
  • 가상화는 응용 프로그램이 Program Files나 Windows 디렉터리에 파일을 기록하려 하면, 또는 레지스트리의 HKEY_LOCAL_MACHINE 섹션을 변경하려 하면 작동한다.
    • 그런 경우 운영체제는 예외를 발생하는 대신, 요청된 변경 사항들을 하드 디스크 어딘가에 있는 개별적인, 원래의 자료에는 영향을 미치지 못하는 장소에 적용한다.
    • 이 덕분에 응용 프로그램이 오작동해도 운영체제나 다른 잘 작동하는 응용 프로그램에 피해가 가지 않는다.

신원 및 역할 보안

  • 신원(identy) 및 역할(role) 기반 보안은 잠재적으로 다수의 사용자를 다루어야 하는 중간층(middle tier) 서버나 ASP.NET 응용 프로그램을 작성할 때 유용하다. 신원 및 역할 보안 기능을 이용하면 응용 프로그램의 기능성을 인증된 사용자 이름이나 역할에 따라 제한할 수 있다.
    • 신원은 간단히 말해서 사용자 이름(ID)이고 역할은 사용자가 속한 그룹이다. 보안 주체(principal)는 하나의 신원이나 하나의 역할 또는 그 둘의 조합을 서술하는 객체이다.
    • 보안 주체에 관한 권한들을 대표하는 클래스는 PrincipalPermission이다. 신원 및 역할 보안을 강제할 떄 바로 이 클래스의 객체를 사용한다.
  • 전형적인 응용 프로그램 서버에서는 클라이언트에 노출되는 메서드 중 보안을 강제할 필요가 있는 모든 메서드에 대해 PrincipalPermission을 요구한다. 예컨대 다음은 호출자가 반드시 ‘finance’ 역할의 일원이어야 함을 요구한다.
[PrincipalPermission (SecurityAction.Demand, Role="finance")]
public decimal GetGrossTurnover(int year)
{
  ...
}
  • 어떤 메서드를 특정 사용자 하나만 호출할 수 있게 하려면, Role 대신 다음처럼 Name 매개변수를 사용하면 된다.
[PrincipalPermission (SecurityAction.Demand, Name="sally")]

사용자와 역할의 배정

  • PrincipalPermission에 대한 Demand가 성공하려면, 그 전에 반드시 IPrincipal 객체를 현재 스레드에 부착해야 한다.
  • Windows가 현재 로그인된 사용자를 응용 프로그램의 신원으로 사용하게 만드는 방법은 다음 두 가지이다. 전자는 응용 프로그램 도메인 전체에 적용되고, 후자는 현재 스레드에만 적용된다.
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);

// 또는
Thread.CurrentPrincipal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
  • WCF나 ASP.NET을 사용하는 경우에는 클라이언트의 신원 가장(impersonation; 프로그램이 현재 사용자 이외의 사용자 계정으로 실행되는 것처럼 만드는 것을 돕는 기능을 해당 기반구조가 제공한다. 또는 GenericPrincipal 클래스와 GenericIdentity 클래스를 이용해서 프로그램이 직접 처리할 수도 있다.
    • 다음은 ‘Jack’이라는 사용자를 생성해서 세 가지 역할을 부여하는 예이다.
GenericIdentity id = new GenericIdentity("Jack");
GenericPrincipal p = new GenericPrincipal(id, new string[] { "accounts", "finance", "management" } );
  • 이것이 효과를 내려면 해당 보안 주체 객체를 현재 스레드에 다음과 같이 배정해야 한다.
Thread.CurrentPrincipal = p;
  • 이 예에서 보듯이, 보안 주체를 스레드마다 따로 설정할 수 있다. 이는 일반적으로 응용 프로그램 서버가 다수의 클라이언트 요청을 각각 개별 스레드에 배정해서 동시에 처리하기 때문이다.
    • 각 요청이 서로 다른 클라이언트에서 비롯될 수 있으므로, 각자 다른 보안 주체를 설정할 수 있어야 한다.
  • GenericIdentity와 GenericPrincipal을 파생해서 독자적인 보안 주체 클래스를 만들 수도 있고, 또는 IIdentity와 IPrincipal 인터페이스를 직접 구현해서 보안 주체 클래스를 만들 수도 있다. 다음은 두 인터페이스의 정의이다.
public interface IIdentity
{
  string Name { get; }
  string AuthenticationTYpe { get; }
  bool IsAuthenticated { get; }
}

public interface IPrincipal
{
  IIdentity Identity { get; }
  bool IsInRole (string role);
}
  • 여기서 핵심 메서드는 IPrincipal이다. 역할들의 목록을 돌려주는 메서드는 없음을 주목하기 바란다. 따라서 이 인터페이스들을 구현할 때에는 특정한 하나의 역할이 해당 보안 주체에 대해 유효한지만 점검하면 된다.
    • 좀 더 정교한 권한 부여(authorization) 시스템을 만들려면 이런 식으로 커스텀 보안 주체 클래스를 만들 필요가 있을 것이다.

암, 복호화 개요

  • 아래 표는 .NET이 지원하는 암, 복호화 기능들을 요약한 것이다.
  • .NET Framework는 또한 XML 기반 서명의 작성 및 유효성 점검을 지원하는 좀 더 특화된 암, 복호화 기능도 제공한다.
    • 관련 형식들이 System.Security.Cryptography.xml에 있다.
    • 또한 System.Security.Cryptography.X509Certificates에는 디지털 인증서를 다룰 때 사용하는 형식들이 있다.
기능 관리하는 키 개수 속도 강도 참고
File.Encrypt 0 빠름 사용자의 패스워드에 따라 다름 파일 시스템의 지원 하에서 파일들을 투명하게 보호한다. 키는 로그인한 사용자의 신원정보(credential)로부터 암묵적으로 유도된다.
Windows 데이터 보호 0 빠름 사용자의 패스워드에 따라 다름 암묵적으로 유도된 키를 이용해서 바이트 배열을 암호화, 복호화 한다.
해싱 0 빠름 높음 단방향(비가역적) 변환, 패스워드 저장, 파일 비교, 자료 변조 점검에 쓰인다.
대칭 암호화 1 빠름 높음 범용적인 암호화/복호화 기능이다. 하나의 키를 암호화와 복호화에 사용한다. 메시지 전송의 보안에 사용할 수 있다.
공개 키 암호화 2 느림 높음 범용적인 암호화/복호화 기능이다. 암호화와 복호화에 각자 다른 키를 사용한다. 메시지 송수신 시 대칭 암호화 키를 교환할 때 쓰이며, 파일의 디지털 서명에도 쓰인다.

 

Windows 데이터 보호

  • 15장에서 File.Encrypt를 이용해서 운영체제가 투명하게 파일을 암호화하도록 요청하는 방법을 설명했다.
File.WriteAllText("myfile.txt", "");
File.Encrypt("myfile.txt");
File.AppendAllText("myfile.txt", "sensitive data");
  • 이 경우 Windows는 로그인된 사용자의 패스워드에서 유도한 키를 이용해서 파일을 암호화한다. Windows 데이터 보호 API를 이용하면 그와 동일하게 암묵적으로 유도된 키를 이용해서 바이트 배열을 암호화할 수 있다.
    • Windows 데이터 보호 API는 ProtectedData 클래스를 통해서 제공되는데, 이 클래스는 다음과 같은 정적 메서드 두 개로만 이루어진 간단한 형식이다.
public static byte[] Protect(byte[] userData, byte[] optionalEntropy, DataProtectionScope scope);
public static byte[] Unprotect(byte[] encryptedData, byte[] optionalEntropy, DataProtectionScope scope);
  • optionalEntropy로 지정한 모든 바이트는 키에 추가되며 따라서 그만큼 보안 강도가 높아진다.
  • DataProtectionScope 열거형의 멤버는 CurrentUser와 LocalMachine 두 가지이다.
    • CurrentUser를 지정하면 로그인된 사용자의 신원 정보로부터 키가 유도되며, LocalMachine을 지정하면 모든 사용자에 공통인 컴퓨터 전역 키가 쓰인다.
    • LocalMachine 키는 보안 강도가 더 낮지만, 대신 Windows 서비스나 다양한 계정들 하에서 작동해야 하는 프로그램에서도 작동한다.
  • 다음은 암호화와 복호화 방법을 보여주는 간단한 예이다.
byte[] original = { 1, 2, 3, 4, 5 };
DataProtectionScope scope = DataProtectionScope.CurrentUser;

byte[] encrypted = ProtectedData.Protect(original, null, scope);
byte[] decrypted = ProtectedData.Unprotect(encryted, null, scope);
  • Windows 데이터 보호 기능은 컴퓨터에 완전히 접근할 수 있는 공격자의 공격을 어느 정도 방어해 준다. 단, 그러한 보호 수준은 사용자의 패스워드의 강도에 의존한다.
    • LocalMachine 범위의 Windows 데이터 보호는 컴퓨터에 제한적으로 (물리적으로든 전자적으로든) 접근할 수 있는 공격자에 대해서만 효과적이다.

해싱

  • 해싱은 단방향 암호화 기능을 제공한다. 단방향 암호화는 패스워드를 데이터베이스에 저장하는 용도로 이상적이다.
    • 사용자 인증과 관련해서 응용 프로그램은 패스워드의 평문을 알 필요가 없다. 그냥 암호화된 버전(해시 코드)만 데이터베이스에 담아두고, 사용자 인증 시 사용자가 입력한 패스워드의 해시 코드가 데이터베이스에 있는 해시 코드와 같은지만 보면 된다.
  • 해시 코드(줄여서 그냥 해시)는 비교적 짧은 바이트열로 그 길이는 원본 자료의 길이와 무관하다. 그래서 해시 코드는 파일들을 비교하거나 자료 스트림에서 오류를 검출할 때 유용하다(후자는 체크섬과 상당히 비슷하다)
    • 원본 자료의 임의의 비트 하나만 변해도 해시 코드가 크게 달라진다.
  • 해시 코드는 SHA256이나 MD5 같은 HashAlgorithm 파생 클래스 중 하나의 Compute Hash 메서드로 얻을 수 있다. 다음은 MD5를 사용하는 예이다.
byte[] hash;
using (Stream fs = File.OpenRead("checkme.doc"))
  hash = MD5.Create().ComputeHash(fs);  // 해시의 길이는 16바이트
  • ComputeHash 메서드는 바이트 배열도 받는다. 이는 다음처럼 패스워드를 해싱할 때 편리하다.
byte[] data = System.Text.Encoding.UTF8.GetBytes("stRhong%pword");
byte[] hash = SHA256.Create().ComputeHash(data);
  • Encoding 객체의 GetBytes 메서드는 주어진 문자열을 바이트 배열로 변환하고, GetString 메서드는 그 반대로 변환한다. 그러나 암호화된(해싱된) 바이트 배열을 문자열로 변환하는 메서드는 Encoding 밖에 없다. 대체로 암호화된 바이트 배열에는 텍스트 부호화 규칙을 위반하는 바이트들이 존재하기 때문이다.
    • 대신 Convert.ToBase64String과 Convert.FromBase64String을 사용해야 한다. 이들은 임의의 바이트 배열을 유효한(그리고 XML에 바로 사용할 수 있는) 문자열로 변환하거나 그 반대로 변환한다.
  • MD5와 SHA256 외에도 .NET Framework는 여러 HashAlgorithm 파생 클래스를 제공한다.
    • 다음은 그런 클래스들을 보안 강도나 높은 것 순서로(그리고 바이트 단위 해시 길이 순서로) 나열한 것이다.
MD5(16) -> SHA1(20) -> SHA256(32) -> SHA384(48) -> SHA512(64)
  • 해싱 길이가 짧은 알고리즘일수록 해싱 속도가 빠르다. MD5는 SHA512보다 20배 이상 빠르기 때문에 파일 체크섬 계산에 적합하다.
    • 예컨대 MD5로는 초당 수백 메가바이트를 해싱해서 Guid 형식의 결과를 얻을 수 있다(Guid도 정확히 16바이트이다. 게다가 값 형식이라서 바이트 배열보다 다루기 쉽다. 예컨대 두 Guid를 그냥 상등 연사낮로 비교해서 의미 있는 결과를 얻을 수 있다.)
    • 그러나 길이가 짧을수록 충돌(서로 다른 두 자료가 같은 해시를 산출하는 것) 가능성이 커진다.
  • 패스워드다 기타 보안에 중요한 자료를 해싱할 떄는 적어도 SHA256을 사용해야 한다. MD5와 SHA1은 그런 목적에는 안전하지 않으며, 의도적인 변조가 아니라 우연한 자료 꺠짐에 대한 방어 수단으로만 적합한 것으로 간주된다.
  • SHA384는 SHA512보다 빠르지 않다. 따라서 SHA256보다 더 안전한 알고리즘을 찾는다면 SHA512를 사용하는 것이 바람직하다.
  • 긴 SHA 알고리즘들은 패스워드 해싱에 적합하다. 그러나 사전 공격(dictionary attack)을 완화하기 위해서는 강력한 패스워드 정책을 강제할 필요가 있다.
    • 사전 공격이란 공격자가 사전의 모든 단어를 해싱해서 만든 패스워드 참조표를 이요애서 암호화된 패스워드의 평문을 알아내려는 시도를 말한다.
    • 이러한 공격에 대한 방어력을 높이는 한 가지 방법은 패스워드 해시를 연장(stretching)하는 것, 즉 해시를 여러 번 다시 해싱하는 것이다. 그러면 복호화에 걸리는 시간이 늘어난다.
    • 예컨대 해싱을 100번 반복하면 한 달 걸렸던 사전 공격의 시간이 8년으로 늘어난다. Rfc2898DeriveBytes 클래스와 PasswordDeriveBytes 클래스는 바로 이런 종류의 해시 연장을 수행한다.
  • 사전 공격을 피하는 또 다른 방법은 소금 값(salt)을 도입하는 것이다. 소금 값은 해싱 전에 패스워드에 연결하는 긴 바이트열로 보통은 난수 발생기를 이용해서 생성한다.
    • 이처럼 패스워드에 ‘소금을 쳐서’ 해싱하면 공격자의 공격 시도가 두 가지 이유로 어려워진다.
    • 하나는 공격자가 평문을 계산하는데 필요한 시간이 길어진다는 점이고, 또 하나는 공격자가 소금 값 바이트들도 알아내야 한다는 점이다.
  • .NET Framework는 또한 160비트 RIPEMD 해싱 알고리즘도 제공한다. 이 알고리즘은 SHA1보다 약간 더 강하다. 그러나 .NET Framework의 구현이 비효율적이기 때문에 실행 속도가 SHA512보다도 느리다.

대칭 암호화

  • 대칭 암호화(symmetric encryption)는 암호화와 복호화에 같은 키를 사용한다.
  • .NET Framework는 네 가지 대칭 암호화 클래스를 제공하는데, 그중 레인달(Rijndael) 알고리즘을 사용하는 클래스들이 더 뛰어나다. 레인달 알고리즘은 빠르고 안전한 암호화 알고리즘으로 .NET Framework에서는 다음 두 클래스가 이 알고리즘을 구현한다.
    • .NET Framework 1.0부터 있었던 Rijndael 클래스
    • .NET Framework 3.5에서 도입된 Aes 클래스
  • 이 둘은 거의 동일하다. 차이점은 Aes는 블록 크기를 줄여서 암호문을 약하게 만드는 옵션을 지원하지 않는다는 것이다. CLR의 보안 팀은 둘 중 Aes를 추천한다.
  • Rijndael과 Aes는 16, 24, 32바이트의 대칭 키를 지원한다. 현재 이 키들은 모두 안전하다고 간주된다.
    • 다음은 일련의 바이트들을 스트리밍 방식으로 암호화해서 파일에 기록하는 예이다. 대칭 키는 16바이트이다.
byte[] key = { 145, 12, 32, 245, 98, ... };
byte[] iv = {15, 122, 132, 5, 93, ... };
byte[] data = { 1, 2, 3, 4, 5 };  // 암호화할 바이트들

using (SymmetricAlgorithm algorithm = Aes.Create())
using (ICryptoTransform encryptor = algorithm.CreateEncryptor(key, iv))
using (Stream f = File.Create("encrypted.bin"))
using (Stream c = new CryptoStream(f, encryptor, CryptoStreamMode.Write))
  c.Write(data, 0, data.Length);
  • 다음은 그 파일을 복호화 하는 코드 이다.
byte[] key = { 145, 12, 32, 245, 98, ... };
byte[] iv = {15, 122, 132, 5, 93, ... };
byte[] decrypted = new byte[5];

using (SymmetricAlgorithm algorithm = Aes.Create())
using (ICryptoTransform decryptor = algorithm.CreateDecryptor(key, iv))
using (Stream f = File.OpenRead("encrypted.bin"))
using (Stream c = new CryptoStream(f, decryptor, CryptoStreamMode.Read))
  for (int b; (b = c.ReadByte()) > -1;)
    Console.Write(b + " " );  // 1 2 3 4 5
  • 이 예제는 그냥 임의로 정한 바이트 16개를 키로 사용한다. 만일 잘못된 키로 복호화를 시도하면 CrytoStream은 CrytographicException 예외를 던진다. 이 예외를 잡는 것이 주어진 키가 정확한지 판정하는 유일한 방법이다.
  • 예제는 키와 함께 초기화 벡터(initialization vector, IV)도 사용한다. 초기화 벡터는 암호문의 일부가 되는 16바이트 길이의 바이트열인데, 키와는 달리 비밀로 간주하지 않는다.
    • 암호화된 메시지를 전송할 때 이 초기화 벡터를 평문으로 전송해도 된다(이를테면 메시지 헤더에 담아서).
    • 단 메시지마다 초기화 벡터를 다르게 하는 것이 좋다. 그러면 평문 메시지들이 비슷하거나 같아도 암호화된 결과가 아주 다르기 때문에, 한 암호문을 해독한다고 해도 다른 암호문의 해독에는 도움이 되지 않는다.
  • 초기화 벡터를 이요한 보호가 필요 없거나 원하지 않는다면 그냥 키를 초기화 벡터로 사용하면 된다. 그러나 같은 초기화 벡터를 다수의 메시지에 사용하면 암호문이 약해지며, 심지어는 공격자가 키 없이 암호문을 해독할 가능성도 생긴다.
  • 암, 복호화 기능은 여러 클래스에 나뉘어 있다. Aes 클래스와 그로부터 생성한 암호화, 복호화 객체들은 암호화와 복호화를 위한 계산을 수행하는 ‘수학자’들이다.
    • 한편 CrytoStream은 ‘배관공’이다. 즉, 이 클래스는 암호화 또는 복호화할 자료의 스트리밍 기능을 제공한다. 이 둘ㅇ느 독립적이다.
    • 예컨대 Aes 이외의 대칭 알고리즘 클래스를 사용할 때에도 여전히 CryptoStream을 사용할 수 있다.
  • CrytoStream은 양방향이다. 객체를 생성할 떄 CryptoStreamMode.Read를 지정했느냐 아니면 CrytoStreamMode.Write를 지정했느냐에 따라 스트림을 읽거나 쓸 수 있게 된다. 그리고 스트림을 적용하는 대상이 암호화 객체와 복호화 객체 두가지이므로 결과적으로 총 네 가지 조합이 존재한다.
    • 독자의 응용 프로그램에 어떤 것을 선택해야 하는지 좀 혼란스러울 수 있는데, 읽기를 끌어오기(pull)라고 생각하고, 쓰기를 밀어 넣기라고 생각하면 도움이 될 것이다.
    • 그래도 잘 모르겠다면 일단은 암호화는 Write로 복호화는 Read로 시작하기 바란다. 그것이 가장 자연스러운 조합인 경우가 많다.
  • 키와 초기와 벡터는 System.Cryptography의 RandomNumberGenerator 클래스로 생성하는 것이 바람직하다.
    • 이 클래스가 생성하는 값들은 진정으로 예측 불가능이다. 이를 암호학적으로 강력하다라고 말하기도 한다.(반면 System.Random 클래스는 암호학적으로 강력한 난수를 보장하지 않는다)
    • 다음은 이 클래스를 사용하는 예이다.
byte[] key = new byte[16];
byte[] iv = new byte[16];

RandomNumberGenerator rand = RandomNumberGenerator.Create();
rand.GetBytes(key);
rand.GetBytes(iv);
  • 암호화 객체나 복호화 객체를 생성할 때 키와 초기화 벡터를 아예 지정하지 않으면 Aes는 자동으로 암호학적으로 강력한 난수들을 생성해서 적용한다. 그 키와 초기화 벡터는 Aes 객체의 Key와 IV 속성으로 조회할 수 있다.

메모리 내부 암호화

  • MemoryStream을 이용하면 암호화와 복호화를 전적으로 메모리 안에서 수행할 수 있다. 다음은 바이트 배열을 메모리 안에서 암, 복호화하는 과정을 돕는 메서드들이다.
public static byte[] Encrypt(byte[] data, byte[] key, byte[] iv)
{
  using (Aes algorithm = Aes.Create())
  using (ICryptoTransform encryptor = algorithm.CreateEncryptor(key, iv))
    return Crypt(data, encryptor);
}

public static byte[] Decrypt(byte[] data, byte[] key, byte[] iv)
{
  using (Aes algorithm = Aes.Create())
  using (ICryptoTransform decryptor = algorithm.CreateDecryptor(key, iv))
    return Crypt(data, decryptor);
}

public static byte[] Crypt(byte[] data, ICryptoransform cryptor)
{
  MemoryStream m = new MemoryStream();
  using (Stream c = new CryptoStream(m, cryptor, CryptoStreamMode.Write))
    c.Write(data, 0, data.Length);
  return m.ToArray();
}
  • 이 메서드들에서는 암호화 객체와 복호화 객체 모두에 CryptoStreamMode.Write를 사용한다. 둘 다 바이트들을 새 메모리 스트림에 ‘밀어 넣는’ 것이므로 이렇게 하는 것이 자연스럽다.
  • 바이트 배열 대신 문자열을 받고 돌려주는 중복적재 버전들도 추가하자.
public static string Encrypt(string data, byte[] key, byte[] iv)
{
    return Convert.ToBase64String(Encrypt(Encoding.UTF8.GetBytes(data), key, iv));
}

public static string Decrypt(string data, byte[] key, byte[] iv)
{
  return Encoding.UTF8.GetString(Decrypt(Convert.FromBase64String(datga), key, iv));
}
  • 다음은 이 메서드들을 사용하는 예이다.
byte[] kiv = new byte[16];
RandomNumberGenerator.Cteate().GetBytes(kiv);

string encrypted = Encrypt("오예!", kiv, kiv);
Console.WriteLine(encrytped);

string decrypted = Decrypt(encrypted, kiv, kiv);
Console.WriteLine(decrypted);

암호화 스트림 연쇄

  • CryptoStream는 하나의 장식자이다. 따라서 다른 스트림과 사슬처럼 엮을 수 있다. 예컨대 다음 코드는 텍스트를 압축하고 암호화해서 파일에 기록한 후 다시 읽어 들여서 텍스트를 복원하는 예이다.
using (Aes algorithm = Aes.Create())
{
  using (ICryptoTransform encryptor = algorithm.CretaeEncryptor())
  using (Stream f = File.Create("serious.bin"))
  using (Stream c = new CryptoStream(f, encryptor, CryptoStreamMode.Write))
  using (Stream d = new DelfatesStream(c, CompressionMode.Compress))
  using (StreamWriter w = new StreamWriter(d))
    await w.WriteLineAsync("작고 안전함!");

  using (ICryptoTransform decryptor = algorithm.CretaeDecryptor())
  using (Stream f = File.OpenRead("serious.bin"))
  using (Stream c = new CryptoStream(f, decryptor, CryptoStreamMode.Read))
  using (Stream d = new DelfatesStream(c, CompressionMode.Decompress))
  using (StreamReader r = new StreamReader(d))
    Console.WriteLine(await r.ReadLineAsync()); // 작고 안전함!
}
  • 이 예제에서 모든 한 글자 변수는 사슬의 일부이다. 암, 복호화 과정에서 수학자들, 즉, algorithm과 encryptor, decryptor는 CryptoStream을 돕는 역할을 한다. 아래 그림은 이러한 사슬을 도식화한 것이다.

  • 이런 식으로 ㅅ트림들을 연결하면 궁극적인 스트림 크기들과는 무관하게 메모리가 거의 소비되지 않는다.
  • 여러 using 문들을 중첩하는 대신, 다음과 같이 사슬을 만드는 것도 가능하다.
using (ICryptoTransform encryptor = algorithm.CretaeEncryptor())
using (StreamWrite w = new StreamWriter(
  new DelfatesStream(
    new CryptoStream (
      File.Create("serious.bin"),
      encryptor,
      CryptoStreamMode.Write
    ),
    CompressionMode.Compress)
  )
)
  • 그러나 앞의 접근방식보다는 덜 안정적이다. 생성자 중 하나에서(이를테면 DeflateStream 생성자에서) 예외가 발생하면 이미 생성된 객체(이를테면 FileStream)들은 처분되지 않을 것이기 때문이다.

암, 복호화 객체의 처분

  • CryptoStream 객체를 처분하면 내부 자료 캐시가 바탕 스트림에 확실히 배출된다. 암호화 알고리즘들은 자료를 블록 단위로(한 번에 한 바이트씩이 아니라) 처리하기 때문에 내부 캐싱이 꼭 필요하다.
  • CrytoStream의 독특한 점은 Flush 메서드가 아무 일도 하지 않는다는 것이다. 스트림 내부 버퍼를 배출하려면 (스트림 자체는 처분하지 않고) 반드시 FlushFinalBlock 메서드를 호출해야 한다.
    • Flush와는 달리 FlushFinalBlock은 단 한 번만 호출할 수 있으며, 일단 한 번 호출하고 나면 더 이상 스트림에 자료를 기록할 수 없다.
  • 앞의 예제는 수학자들 즉 Aes 알고리즘과 ICryptoTransform 객체들도 처분했다. 사실 레인달 변환에서는 이러한 처분을 생략해도 된다. 레인달 구현들은 순수하게 관리되기 때문이다.
    • 그래도 처분에는 유용한 용도가 있다. 바로 대칭 키와 관련 자료를 메모리에서 지워버려서 같은 컴퓨터에서 실행되는 다른 소프트웨어(특히 악성 코드)가 그런 정보를 찾아내지 못하게 하는 것이다.
    • 이 부분을 쓰레기 수거기에 맡길 수는 없다. 쓰레기 수거기는 그냥 메모리의 해당 영역을 가용 상태로 표시할 뿐, 모든 바이트에 0을 덮어쓰지는 않는다.
  • using 문 바깥에서 Aes 객체를 처분하는 가장 쉬운 방법은 Clear를 호출하는 것이다. Aes의 Dispose 메서드는 명시적 구현을 통해서 숨겨져 있다. (처분 의미론이 통상적이지 않음을 나타내기 위해)

키 관리

  • 암호화 키를 프로그램의 코드 자체에 넣어 두는 것은 바람직하지 않다. 전문 기술 없이도 어셈블리를 손쉽게 역컴파일하는 대중적인 도구들이 있기 때문이다.
    • 더 나은 방법은 프로그램 설치 시 무작위로 생성해서 Windows 데이터 보호 기능으로 안전하게 저장해 두는 것이다(또는 메시지 전체를 Windows 데이터 보호 기능으로 암호화할 수도 있다)
    • 메시지 스트림을 암호화 하는 경우에는 공개키 암호화 기법이 가장 나은 선택이다.

공개키 암호화와 서명

  • 공개키 암, 복호화(public key cryptography)는 비대칭이다. 다른말로 하면 공개키 암, 복호화에서는 암호화와 복호화에 서로 다른 키를 사용한다.
  • 대칭 암호화에서는 길이만 적당하면 그 어떤 바이트열도 키로 사용할 수 있지만, 비대칭 암호화에서는 특별히 제조된 키 쌍이 필요하다. 하나의 키 쌍은 공개키(public key)와 비밀키(private key)라는 두 개의 키로 이루어진다. 공개키 암, 복호화는 두 키를 다음과 같이 함께 사용한다.
    • 공개키로 메시지를 암호화한다.
    • 비밀키로 메시지를 복호화(해독)한다.
  • 키 쌍을 ‘제조’하는 쪽은 비밀키는 이름 그대로 비밀에 부치고, 공개키는 공개적으로 배포한다. 이런 종류의 암, 복호화의 한 가지 특징은 공개키로부터 비밀키를 계산할 수는 없다는 것이다.
    • 따라서 비밀키를 잃어버리면 암호문을 다시 평문으로 복원할 수 없다. 반대로 비밀키가 유출되면 암, 복호화 시스템이 무력해진다.
  • 공개키 교환(public key handshake)이라는 절차 덕분에 이전에 접촉이 없었던, 그리고 그 어떤 비밀도 공유하지 않는 두 컴퓨터가 공용 네트워크에서 안전하게 통신할 수 있다.
  • 이해를 돕기 위해 기원(origin)이라는 컴퓨터가 대상(target)이라는 컴퓨터에 기밀 메시지를 보낸다고 하자. 그 과정은 다음과 같다.
    1. 대상이 공개키/ 비밀키 쌍을 생성하고 공개키만 기원에게 보낸다.
    2. 기원은 기밀 메시지를 대상의 공개키를 이용해서 암호화해서 대상에게 보낸다.
    3. 대상은 자신의 비밀키를 이용해서 기밀 메시지를 복호화한다.
  • 공격자가 중간에서 통신을 훔쳐본다고 해도 문제가 되지 않는다. 공격자가 알 수 있는 것은 다음 두 가지뿐이다.
    • 대상의 공개키
    • 대상의 공개키로 암호화된 기밀 메시지
  • 공개키는 어차피 널리 알려져있고, 기밀 메시지는 대상의 비밀키가 있어야 해독할 수 있다.
  • 단, 이러한 과정이 중간자 공격까지 방어해주지는 않는다. 즉, 중간에서 대상이 아닌 누군가가 대상인 척하고 기원과 통신을 진행할 수 있다.
    • 상대방이 실제 통신 대상인ㅇ지 인증하려면 기원자가 상대방의 공개키를 이미 알고 있거나 또는 디지털 사이트 서명을 이용해서 상대방의 키를 검증할 필요가 있다.
  • 흔히 기원은 이후의 대칭 암호화를 위한 새 키를 기밀 메시지로서 대상에게 보낸다.
    • 일단 대칭키를 안전하게 전달한 후에는 공개키 암호화를 버리고 대칭 알고리즘(더 긴 메시지를 잘 처리할 수 있는)을 사용하면 된다.
    • 세션마다 새로운 공개키/ 비밀키 쌍을 생성한다면 두 컴퓨터 모두 키들을 전혀 저장할 필요가 없으므로 이러한 절차가 특히나 안전해진다.
  • 공개키 암호화 알고리즘들은 메시지가 키보다 짧다고 가정한다. 따라서 이후의 대칭 암호화를 위한 키 같은 짧은 자료를 암호화하는데 적합하다.
    • .NET Framework의 경우, 만일 키의 절반 크기보다 훨씬 긴 메시지를 암호화하려 하면 암호화 서비스 공급자가 예외를 던진다.

RSA 클래스

  • .NET Framework는 여러가지 비대칭 알고리즘을 지원하는데, 그중 가장 널리 쓰이는 것은 RSA 알고리즘이다. 다음은 RSA를 이용해서 바이트 배열을 암호화하고 해독하는 예이다.
byte[] data = { 1, 2, 3, 4, 5 };  // 암호화할 자료

using (var rsa = new RSACryptoServiceProvider())
{
  byte[] encrypted = rsa.Encrypt(data, true);
  byte[] decrypted = rsa.Decrypt(encrypted, true);
}
  • 이 예제처럼 공개키나 비밀키를 전혀 지정하지 않고 암호화 복호화를 수행하면 암호화 서비스 공급자가 자동으로 키 쌍을 생성한다.
    • 이 경우 각 키의 길이는 기본적으로 1024비트이다. 더 긴 키를 원한다면 생성자 호출시 길이를 지정하면 된다.
    • 단 길이 증가치는 반드시 64(8바이트)의 배수이어야 한다.
    • 보안이 중요한 응용 프로그램에서는 적어도 2048비트를 사용하는 것이 바람직하다.
var rsa = new RSACryptoServiceProvider(2048)
  • 키 쌍 생성은 계산량이 많은 연산이다. 아마 100ms는 걸릴 것이다. 그래서 RSA 구현은 키가 실제로 필요해질 때(이를테면 Encrypt가 호출되었을 때)까지 키 쌍 생성을 미룬다. 이 덕분에 필요하다면 RSA 공급자가 객체를 생성한 후 기존의 한 키 또는 키 쌍을 적재할 여유가 생긴다.
  • ImportCspBlob 메서드와 ExportCspBlob 메서드는 바이트 배열 형태로 키를 적재하거나 저장한다.
    • FromXmlString과 ToXmlString은 XML 조각을 담은 문자열 형태로 키를 적재하거나 저장한다.
    • 저장 메서드들은 비밀키도 함께 저장할 것인지를 뜻하는 bool 인수를 받는다.
    • 다음은 키 쌍을 생성해서 디스크에 저장하는 예이다.
using (var rsa = new RSACryptoServiceProvider())
{
  File.WriteAlText("PublicKeyOnly.xml", rsa.ToXmlString(false));
  File.WriteAlText("PublicPrivate.xml", rsa.ToXmlString(true));
}
  • 기존 키들을 지정하지 않았으므로 첫 ToXmlString 호출에서 RSA 서비스 공급자 객체는 새로운 키 쌍을 생성한다.
    • 다음은 파일들에서 키들을 읽어서 메시지를 암호화하고 해독하는 예이다.
byte[] data = Encoding.UTF8.GetBytes("Message to encrypt");

string publicKeyOnly = File.ReadAllText("PublicKeyOnly.xml");
string publicPrivate = File.ReadAllText("PublicPrivate.xml");

byte[] encrypted, decrypted;

using (var rsaPublicOnly = new RSACryptoServiceProvider())
{
  rsaPublicOnly.FromXmlString(publicKeyOnly);
  encrypted = rsaPublicOnly.Encrypt(data, true);

  // 복호화를 위해서는 비밀키가 필요하므로 다음 줄을 실제로 실행한다면 예외가 발생한다.
  // decrypted = rsaPublicOnly.Decrypt(encrypted, true);
}

using (var rsaPublicPrivate = new RSACryptoServiceProvider())
{
  // 이번에는 비밀키가 있으므로 해독이 성공한다.
  rsaPublicPrivate.FromXmlString(publicPrivate);
  decrypted = rsaPublicPrivate.Decrypt(encrypted, true);
}

디지털 서명

  • 공개키 알고리즘을 메시지나 문서에 디지털 서명을 가하는 용도로 사용할 수도 있다. 여기서 서명(signature)은 해시와 비슷하되, 비밀키가 있어야 생성할 수 있다는(따라서 위조가 불가능하다는) 점이 다르다.
    • 공개키는 서명을 검증할 때 쓰인다. 다음 예를 보자.
byte[] data = Encoding.UTF8.GetBytes("서명할 자료");
byte[] publicKey;
byte[] signature;
object hasher = SHA1.Create();  // 사용할 해싱 알고리즘

// 새 키 쌍을 생성해서 자료에 서명한다.
using (var publicPrivate = new RSACryptoServiceProvider())
{
  signature = publicPrivate.GignData(data, hasher);
  publicKey = publicPrivate.ExportCspBlob(false);  // 공개키를 저장
}

// 공개키만 있는 새 RSA 공급자로 서명을 검사한다.
using (var publicOnly = new RSACryptoServiceProvider())
{
  publicOnly.ImportCspBlob(publicKey);
  Console.Write(publicOnly.VerifyData(data, hasher, signature));  // true

  // 자료를 변조한 후 서명을 다시 검사해 본다.
  data[0] = 0;
  Console.Write(publicOnly.VerifyData(data, hasher, signature));  // false

  // 비밀키가 없으므로 다음은 예외를 던진다.
  signature = publicOnly.SignData(data, hasher);
}
  • 디지털 서명에서는 먼저 자료를 해싱한 후 그 해시에 대해 비대칭 알고리즘을 적용한다. 문서 자체가 아니라 고정된 길이의 짧은 해시에 대해 비대칭 알고리즘을 적용하므로 서명이 비교적 빨리 완료된다(공개키 암호화는 해싱보다 CPU를 훨씬 많이 사용한다)
    • 지금 예에서는 해싱과 서명을 하나의 메서드(SignData)로 처리했지만, 필요하다면 다음과 같이 해시를 따로 생성한 후 SignData 대신 SignHash를 호출할 수도 있다.
using (var rsa = new RSACryptoServiceProvider())
{
  byte[] hash = SHA1.Create().ComputeHash(data);
  signature = rsa.SignHash(hash, CryptoConfig.MapNameToOID("SHA1"));
  ...
}
  • SignHash 호출 시 그냥 해시만 지정해서는 안되고 그 해시가 어떤 해시 알고리즘으로 만들어진 것인지도 알려주여야 한다.
    • CryptoConfig.MapNameToOID 메서드는 “SHA1” 같은 익숙한 알고리즘 이름을 받아서 SignHash가 요구하는 형식의 객체를 돌려준다.
  • RSACryptoServiceProvider 클래스는 키와 같은 크기의 서명을 생성한다. 현재 주류 알고리즘 중 128바이트보다 훨씬 짧은 보안 서명을 산출하는 것은 없다(예컨대 제품 활성화 코드에는 128바이트보다 훨씬 짧은 서명이 적합하다)
  • 이러한 서명이 효과적이려면 수신자가 반드시 송신자의 공개키를 알아야 하고, 그것이 실제로 송신자의 공개키임을 믿을 수 있어야 한다. 따라서 그러한 적절한 절차를 통해서 그 공개키를 미리 전달 또는 설정해 두어야 할 것이다.
    • 또는 사이트 인증서를 통해서 제공할 수도 있다. 사이트 인증서(site certificate)는 기원자의 공개키와 이름을 전자적으로 기록한 것인데, 인증서 자체는 기원자와는 독립적인, 신용 있는 기관이 서명한다.
    • System.Security.Cryptography.X509Certificates 이름공간에 인증서를 다루는데 필요한 형식들이 정의되어 있다.
[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.