rubrapack 매뉴얼←↑→

38 Authenticode 서명

Windows 가 서명된 PE 파일(.exe, .dll)이나 MSI 패키지를 받아들이게 하려면 프로그램이 무엇을 써야 하는가. 출처 표시는 구현하는 사람을 위한 패키지 형식와 같다: [spec] 은 Microsoft 의 "Windows Authenticode Portable Executable Signature Format", PE/COFF 명세, RFC 5652(CMS), [observed] 는 Windows 자신의 서명기 (mssign32.dll 을 쓰는 PowerShell Set-AuthenticodeSignature)가 서명하고 Windows 11 에서 Get-AuthenticodeSignature / WinVerifyTrust 로 확인한 파일이다.

38.1 PE 파일에서 서명이 사는 곳#

38.2 PE 다이제스트#

SHA-256 으로 이 순서대로: [spec]

  1. 머리, 0 부터 SizeOfHeaders 까지. CheckSum(4바이트)과 인증서 표 디렉터리 항목(8바이트)은 뺀다.
  2. SizeOfRawData > 0 인 모든 섹션을 PointerToRawData 가 커지는 순서로, PointerToRawData 부터 SizeOfRawData 바이트.
  3. 그 뒤의 데이터: SizeOfHeaders + sum(SizeOfRawData) 부터 파일 끝에서 인증서 표를 뺀 곳까지.

구멍 둘을 두고 파일을 앞에서부터 해시해도 섹션 사이에 틈이 없을 때만 같은 결과가 나온다. 위 단계가 정의다.

38.3 CMS SignedData#

Windows 의 서명기가 쓰고 받아들이는 것: [observed]

ContentInfo { signedData (1.2.840.113549.1.7.2), [0] SignedData {
  version 1,
  digestAlgorithms SET { sha256 + NULL },
  encapContentInfo { SPC_INDIRECT_DATA (1.3.6.1.4.1.311.2.1.4),
                     [0] SpcIndirectDataContent }          -- SEQUENCE 그대로, OCTET STRING 없음
  certificates [0] { 서명자의 인증서, 그다음 중간 인증서; 자체 서명 루트 없음 },
  signerInfos SET { SignerInfo {
    version 1, 서명자의 IssuerAndSerialNumber,
    digestAlgorithm sha256 + NULL,
    signedAttrs [0] { SpcSpOpusInfo, contentType, SpcStatementType, messageDigest },
    signatureAlgorithm rsaEncryption + NULL,
    signature (RSA PKCS#1 v1.5) } } } }

38.4 MSI 패키지#

Microsoft 는 MSI 패키지를 어떻게 해시하는지 공개하지 않는다. 아래 규칙은 Windows 의 서명기로 패키지에 서명하고 비교해서 알아냈다: 같은 패키지와 키에 대해 rubrapack 의 서명과 Ex 스트림은 Windows 의 것과 바이트까지 같았고, 디렉터리 항목에 0 아닌 상태 비트·시각·CLSID 를 준 패키지들로 필드를 가려냈다. [observed]

38.5 타임스탬프#

타임스탬프 없는 서명은 서명 인증서가 만료되면 더는 유효하지 않다. RFC 3161 타임스탬프가 있으면 Windows 는 대신 찍힌 시각에 인증서를 확인한다. SIGNER_TIMESTAMP_RFC3161 을 준 mssign32!SignerTimeStampEx2 가 쓰는 것: [observed]

Set-AuthenticodeSignature -TimestampServer 는 대신 옛 형태를 쓴다: PKCS#9 counterSignature 속성(1.2.840.113549.1.9.6)과, 바깥 인증서에 더한 서버의 인증서. Windows 는 둘 다 받아들인다. [observed]

38.6 ECDSA#

ECDSA P-256 서명자는 PE 파일, MSI 패키지, MSIX 패키지 모두에 통한다(Windows 11 26100 은 루트를 신뢰하면 셋 모두 Valid 라고 한다). Windows 의 서명기는 SignerInfo 의 signatureAlgorithm 을 ecdsa-with-SHA256 이 아니라 NULL 매개변수의 id-ecPublicKey(1.2.840.10045.2.1)로 쓰고, 서명 값은 DER SEQUENCE { INTEGER r, INTEGER s } 로 쓴다. rubrapack 도 결정적 nonce(RFC 6979)로 같게 쓰고, 두 OID 를 모두 읽는다. [observed]

38.7 Windows 가 알려 주는 것#

Get-AuthenticodeSignature 는 서명하는 쪽에 필요한 경우들을 가려 준다(타임스탬프가 있으면 TimeStamperCertificate 에 타임스탬프 인증서를 적는다): Valid(다이제스트, 서명, 신뢰하는 루트까지의 사슬), "the certificate chain ... not trusted" 를 단 UnknownError(다이제스트와 서명은 맞고, 루트를 이 컴퓨터가 신뢰하지 않음), HashMismatch(서명 뒤에 파일이 바뀜), NotSigned. 그러니 어떤 루트도 신뢰시키기 전에 서명이 맞는지 확인할 수 있다. [observed]

38.8 실제 예: 튜토리얼의 hello.msi#

튜토리얼 첫 패키지의 다이제스트를 위의 규칙대로 키 없이 여기서 계산했다 - 어떤 키로 서명하든 서명에는 이 값이 그대로 들어간다. UTF-16LE 이름 순서로 놓은 스트림들:

#이름(UTF-16LE)이름크기
105 00 53 00 75 00 ...\005SummaryInformation336
20b 43 31 41 35 47 ...Binary.RpCa99840
326 41 65 38 be 41 ...cab1.cab6776
440 48 0b 43 31 41 ...Binary4
540 48 0c 46 f6 45 ...CustomAction56
640 48 0d 43 35 42 ...Directory18
740 48 0f 42 e4 45 ...FeatureComponents4
840 48 0f 42 e4 45 ...Feature16
940 48 0f 43 2f 42File20
1040 48 16 42 27 43 ...Media14
1140 48 3f 3b f2 43 ..._Columns648
1240 48 3f 3f 77 45 ..._StringData3706
1340 48 3f 3f 77 45 ..._StringPool684
1440 48 52 44 f6 45 ...InstallExecuteSequence156
1540 48 52 44 f6 45 ...InstallUISequence42
1640 48 59 45 f2 44 ...Property88
1740 48 7f 3f 64 41 ..._Tables36
1840 48 8c 44 f0 44 ...Component12
1940 48 ca 41 30 43 ...AdminExecuteSequence48
2040 48 ca 41 30 43 ...AdminUISequence24
2140 48 ca 41 f9 45 ...AdvtExecuteSequence42
2240 48 de 44 6a 45 ...Upgrade32
2340 48 ff 3f e4 43 ..._Validation1944

프리해시는 뿌리의 CLSID 와 상태 비트(20 바이트)에, 스트림마다 이름, 8 바이트 크기, 0 인 시각 둘을 이은 것으로 모두 908 바이트다. 그 SHA-256 이 \005MsiDigitalSignatureEx 의 값이다:

be d7 c5 7b 1d aa 0f fd 10 b6 95 f5 e3 eb cc b7 11 91 28 12 d8 8a b2 85 76 29 5a 7a 94 ca 68 9d

SpcIndirectDataContent 의 다이제스트 - 그 값, 같은 순서의 모든 스트림 바이트, 뿌리 CLSID 에 대한 SHA-256:

fe a3 de 0c f6 25 b2 1d ad fb c5 37 ea a5 05 30 49 88 e9 b9 80 8b 6d 1f e5 a0 11 c8 64 a0 06 91