Перейти к содержимому
ecdsa.com
← Все статьи

Подпись кода Authenticode: ключи, метки времени, проверка

Мы подписали небольшой исполняемый файл Windows утилитой osslsigncode 2.14 с меткой времени от DigiCert, а затем провели три опыта: подписали копию без метки времени, изменили один байт кода и спрятали данные в таблице сертификатов. Правила взяты из Базовых требований к подписи кода (Code Signing Baseline Requirements) CA/Browser Forum версии 3.11.0 и из документации самой Microsoft по состоянию на октябрь 2026 года.

Коротко. Authenticode встраивает структуру PKCS #7 SignedData в таблицу в конце PE-файла Windows; подпись покрывает хеш файла, при вычислении которого пропускаются контрольная сумма, указатель на эту таблицу и сама таблица. С 1 июня 2023 года ключ подписи кода для публично доверенного сертификата должен генерироваться и использоваться в аппаратном модуле (токене, собственном HSM или облачной службе HSM с полным журналом аудита), поэтому с привычным файлом .pfx на сервере сборки сертификат больше не получить. Сертификаты, выпущенные с 1 марта 2026 года, действуют не более 460 дней, а служба Microsoft Artifact Signing выпускает сертификаты сроком на 72 часа, так что подпись остаётся действительной именно благодаря метке времени RFC 3161: всегда подписывайте с параметрами /fd SHA256 /tr <TSA URL> /td SHA256. EV-сертификаты больше не дают мгновенной репутации в SmartScreen. Проверяйте подпись командой signtool verify /pa /tw в Windows или osslsigncode verify в других системах.

Что именно подписывается

У PE-файла (.exe, .dll, .sys) в необязательном заголовке (optional header) есть каталог данных (data directory); его запись 4, таблица сертификатов (Certificate Table), содержит смещение в файле и размер списка структур WIN_CERTIFICATE, дописанного в конец файла. Для Authenticode у такой записи ревизия 0x0200 и тип WIN_CERT_TYPE_PKCS_SIGNED_DATA (0x0002), а внутри неё лежит структура PKCS #7 SignedData (формат PE).

Подписывается не сам файл, а структура SpcIndirectDataContent, в которой хранится хеш файла. Этот хеш не может покрывать саму подпись, поэтому, по словам Microsoft, при его вычислении «нужно исключить некоторые поля образа PE, например контрольную сумму и запись таблицы сертификатов в каталогах данных необязательного заголовка», а также саму таблицу сертификатов. Всё остальное — заголовки, секции и данные после последней секции — хешируется.

Образец — программа размером 1,6 МБ, собранная кросс-компиляцией на Go 1.27.3 (GOOS=windows GOARCH=amd64). Короткий скрипт на Python читает запись таблицы сертификатов до и после подписи:

python3 pe.py hello.exe show; python3 pe.py hello-signed.exe show
file size 1651712, certificate table at offset 0, size 0
file size 1660952, certificate table at offset 1651712, size 9240
WIN_CERTIFICATE: dwLength 9240, wRevision 0x0200, wCertificateType 0x0002

Эти 9 240 байт — подпись, цепочка сертификатов и токен метки времени. Если вывести список идентификаторов объектов внутри извлечённой подписи, рядом с обычными частями CMS видны части, специфичные для Authenticode:

openssl asn1parse -inform DER -in sig.der | grep -o 'OBJECT *:[^ ]*' | sort | uniq -c (фрагмент)
      1 OBJECT            :1.3.6.1.4.1.311.2.1.11
      1 OBJECT            :1.3.6.1.4.1.311.2.1.12
      1 OBJECT            :1.3.6.1.4.1.311.2.1.15
      2 OBJECT            :1.3.6.1.4.1.311.2.1.4
      1 OBJECT            :1.3.6.1.4.1.311.3.3.1
      2 OBJECT            :id-smime-ct-TSTInfo
      2 OBJECT            :messageDigest
      2 OBJECT            :signingTime

1.3.6.1.4.1.311.2.1.4 — это SpcIndirectDataContent, …2.1.15 — данные образа PE внутри него, …2.1.11 и …2.1.12 — соответственно тип заявления (statement type) и имя и URL программы, а 1.3.6.1.4.1.311.3.3.1 — токен метки времени RFC 3161, сохранённый как неподписанный атрибут, с типом содержимого id-smime-ct-TSTInfo.

Сертификаты и ключи в 2026 году

Публично доверенные сертификаты подписи кода регулируют Базовые требования к подписи кода (Code Signing Baseline Requirements, CSBR) CA/Browser Forum версии 3.11.0 от 16 июня 2026 года. Правила, которые меняют процесс сборки:

  • Аппаратные ключи для всех. «С 1 июня 2023 года» закрытый ключ владельца сертификата должен генерироваться, храниться и использоваться в аппаратном криптографическом модуле, сертифицированном как минимум по FIPS 140-2 уровня 2 или Common Criteria EAL 4+ (§ 6.2.7.4.1). Допустимые варианты: аппаратный модуль, который вы обслуживаете сами, облачная служба ключей, чей HSM соответствует этим требованиям, а подписка «настроена так, чтобы журналировать все обращения, операции и изменения конфигурации», или служба подписи. Центр сертификации (CA) обязан это проверить, например отправив токен с заранее сгенерированным ключом или проверив аттестацию ключа (§ 6.2.7.4.2). До 2023 года это требование касалось только EV-сертификатов.
  • Размеры ключей. Ключи RSA длиной не менее 3072 бит или ECDSA на кривых P-256, P-384 или P-521 (§ 6.1.5.2).
  • Срок действия. Не более 460 дней для сертификатов, выпущенных 1 марта 2026 года или позже; до этой даты — 39 месяцев (§ 6.3.2).
  • Службы меток времени (TSA). Сертификаты TSA действуют не более 135 месяцев, а их ключи можно использовать в течение 15 месяцев после notBefore; токены меток времени с SHA-1 запрещены с апреля 2022 года.

Microsoft добавляет к этому собственные требования. Начиная с Windows 10 версии 1607 драйверы режима ядра должны быть подписаны самой Microsoft через Windows Hardware Dev Center, поэтому одного коммерческого сертификата недостаточно, чтобы драйвер загрузился (политика подписи драйверов). А EV больше не означает «никаких предупреждений SmartScreen»: на странице Microsoft о статусе функций распространения сказано, что после изменения Trusted Root Program в 2024 году «ни один тип сертификата (OV или EV) не даёт немедленного обхода»; репутация накапливается отдельно для каждого хеша файла и каждого удостоверения подписанта.

Управляемая служба Microsoft Artifact Signing (прежнее название — Trusted Signing) хранит ключи в HSM, которые обслуживает Microsoft, и выпускает сертификаты, которые «продлеваются ежедневно и действительны всего 72 часа» (управление сертификатами). Машину сборки покидает только дайджест. При таких коротких сертификатах подпись без метки времени через три дня становится бесполезной. Ещё две детали с той же страницы. Поскольку сертификат меняется каждый день, каждый профиль сертификатов получает постоянное собственное значение EKU (расширенного использования ключа) в ветке 1.3.6.1.4.1.311.97., к которому можно привязать доверие (например, в политиках управления приложениями). А CA этой службы исключаются из CCADB и больше не входят в Microsoft Trusted Root Program, но «остаются в списке доверия платформы Windows» (Windows Platform Trust List), поэтому Windows продолжает им доверять, а другие платформы могут и не доверять.

Подпись и проверка с osslsigncode

osslsigncode реализует Authenticode в Linux и macOS. В эксперименте (пакет Debian версии 2.14 в debian:forky-20261005-slim) мы создали частную тестовую иерархию из корневого CA, выпускающего CA и сертификата подписи кода RSA 3072 с EKU codeSigning, подписали программу и запросили метку времени у публичной TSA компании DigiCert:

lab.sh (фрагмент): подпись с меткой времени RFC 3161 и проверка
osslsigncode sign -certs chain.pem -key codesign.key -h sha256 -n "Hello" -i "https://example.test/hello" \
  -ts http://timestamp.digicert.com -in hello.exe -out hello-signed.exe
osslsigncode verify -CAfile root.pem -TSA-CAfile /etc/ssl/certs/ca-certificates.crt -in hello-signed.exe

В TSA отправляется только хеш подписи. Результат проверки (повторяющийся вывод цепочек сокращён):

osslsigncode verify (сокращено)
Message digest algorithm  : SHA256
Current message digest    : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 
Calculated message digest : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 

Signer's certificate:
	------------------
	Signer #0:
		Subject: CN=Example Software GmbH,O=Example Software GmbH,C=DE
		Issuer : CN=Example Test Code Signing CA,O=Example Test
		Serial : 20ADC33D7219F573885B44765077E96312F72F6D
		Certificate expiration date:
			notBefore : Oct 22 06:32:28 2026 GMT
			notAfter : Nov 21 06:32:28 2026 GMT

Authenticated attributes:
	Signing time: Oct 22 06:32:28 2026 GMT
	Microsoft Individual Code Signing purpose
	Message digest: DE669CE86A064FAEB4DFE8E43E14090CE5912FA036C527C13D5558B0BF110BB7 
	URL description: https://example.test/hello
	Text description: Hello

Countersignatures:
	Timestamp time: Oct 22 06:32:29 2026 GMT
	Signing time: Oct 22 06:32:29 2026 GMT
	Hash Algorithm: sha256
	Issuer: CN=DigiCert Trusted G4 TimeStamping RSA4096 SHA256 2025 CA1,O=DigiCert\, Inc.,C=US
	Serial: 084FDC334F7E454EDBC30F8FF9921835
...
Timestamp Server Signature CRL verification: ok
Timestamp Server Signature verification: ok
Signature verification time: Oct 22 06:32:29 2026 GMT
...
Signature verification: ok

Number of verified signatures: 1
Succeeded

«Current message digest» — хеш, сохранённый в подписи, а «Calculated» — хеш, заново вычисленный по файлу. Собственное «Signing time» подписанта — лишь утверждение, основанное на его часах, а под значением «Timestamp time» стоит подпись TSA, и именно на него опираются проверяющие стороны. Затем утилита osslsigncode проверила подпись на момент метки времени — в этом и состоит смысл всей конструкции.

Зачем нужна метка времени

Чтобы это увидеть, мы подписали ту же программу ещё раз, без -ts, а затем проверили оба файла так, как будто прошло 60 дней и срок действия 30-дневного тестового сертификата уже истёк (osslsigncode verify -time):

Проверка на момент «сейчас + 60 дней» (строки отфильтрованы, место ошибки OpenSSL сокращено)
hello-signed.exe   Signature verification time: Oct 22 06:32:29 2026 GMT Signature verification: ok Succeeded
hello-nots.exe     ...:PKCS7_verify:certificate verify error:...:Verify error: certificate has expired Signature verification time: Dec 21 06:32:29 2026 GMT Error: certificate has expired ... Signature verification: failed Failed

С меткой времени проверка выполняется на доказанный момент подписи, поэтому подпись остаётся действительной и после того, как истечёт срок действия сертификата подписи (сертификат, которым DigiCert подписывает метки времени, в этом запуске и сам действителен до ноября 2037 года). Без метки проверка использует текущее время и перестаёт проходить в тот день, когда истекает срок действия сертификата, — та же ошибка certificate has expired, что и у любого TLS-клиента. Microsoft идёт дальше: если метка времени попадает в сроки действия и сертификата подписи, и сертификата TSA, подпись действительна, причём «остаётся действительной ещё долго после того, как истечёт срок действия сертификата подписи и сертификата TSA (если только ни один из них не отозван)».

Отзыв тоже привязан ко времени. Согласно CSBR, CA может указать в поле revocationDate сертификата подписи кода дату, когда сертификат впервые стал считаться недействительным, а при компрометации ключа — «самую раннюю дату предполагаемой компрометации»; при этом «программному обеспечению следует и дальше считать действительными объекты, содержащие метку времени, которая датирована раньше даты отзыва» (§ 4.9.6). Поэтому утёкший ключ делает недействительными только подписи с метками времени после компрометации, а не все релизы, которые вы когда-либо выпустили.

Что ломает подпись, а что нет

Два изменения файла, проверенные той же командой:

Изменён один байт кода (смещение 4096), затем внутрь таблицы сертификатов дописаны 64 байта
### one byte of code changed
Signature verification: failed
Message digest algorithm  : SHA256
Current message digest    : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 
Calculated message digest : E754D0A7C1AB0CA1716866C67046BA54D2C195CDCBA4A182D56DD5C57FCD37B6     MISMATCH!!!
Failed
### extra bytes inside the certificate table
appended 64 bytes inside the certificate table: size 9240 -> 9304
Warning: invalid PE checksum
Calculated message digest : ADEAAA5366F711E064A9D54630D1E45520727730ECA3B85423D7570B0D41F7A7 
Timestamp Server Signature verification: ok
Signature verification: ok
Succeeded

Во втором файле размер в записи таблицы сертификатов и длина WIN_CERTIFICATE увеличены на 64 байта, а в появившееся место записаны данные. Поскольку таблица исключена из хеша, подпись по-прежнему проходит проверку; неверна только контрольная сумма PE, которая тоже исключена из хеша. Именно такая структура лежит в основе CVE-2013-3900: установщикам, которые читают конфигурацию из конца собственного файла, можно было подсунуть полезную нагрузку злоумышленника, не сделав подпись недействительной. Microsoft исправила это в 2013 году необязательной настройкой реестра EnableCertPaddingCheck, которую нужно включать самостоятельно, а в повторной публикации 2022 года сообщает, что «не планирует по умолчанию применять более строгий режим проверки». В январе 2022 года CISA внесло эту CVE в свой каталог известных эксплуатируемых уязвимостей (Known Exploited Vulnerabilities). Урок для разработчиков: никогда не читайте данные из области таблицы сертификатов собственного исполняемого файла и включите EnableCertPaddingCheck на машинах, которыми вы управляете.

В Windows: signtool

Эталонная реализация — SignTool из Windows SDK. Текущие версии требуют явно указывать алгоритмы дайджеста: «Для команды SignTool sign обязательны параметр алгоритма дайджеста файла (/fd) и параметр алгоритма дайджеста метки времени (/td)» (SignTool). Если сертификат лежит в хранилище Windows, а ключ — на токене, обычные команды такие:

signtool (из документации Microsoft; здесь не запускалось)
signtool sign /n "Example Software GmbH" /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /d "Hello" MyApp.exe
signtool verify /pa /tw /v MyApp.exe
  • /n или /sha1 выбирает сертификат; ключ остаётся в токене или HSM, и доступ к нему идёт через их CSP или KSP (/csp и /kc указывают их явно).
  • /tr обращается к серверу RFC 3161; более старый /t использует устаревший протокол меток времени Authenticode.
  • verify /pa использует стандартную политику Authenticode; без этого параметра SignTool применяет политику для драйверов и отклоняет обычные подписи. /tw выдаёт предупреждение, если у подписи нет метки времени, — этому параметру место в каждом пайплайне релиза.
  • /dlib и /dmdf подключают библиотеку, которая подписывает дайджест удалённо; такая библиотека есть у Artifact Signing, а Jsign с открытым исходным кодом (Apache-2.0, версия 7.6) делает то же самое в любой ОС, работая с AWS KMS, Google Cloud KMS, Azure Key Vault, токенами PKCS #11 и YubiKey.

На стороне проверки командлет PowerShell Get-AuthenticodeSignature возвращает статус и подписанта, а вкладка «Цифровые подписи» в Проводнике Windows показывает метку времени.

Подпись в пайплайне сборки

Когда ключи привязаны к оборудованию, на сервере сборки больше нет файла ключа. Работают три схемы:

  1. Облачная служба ключей: сгенерируйте ключ в AWS KMS, Google Cloud HSM или Azure Key Vault Premium, получите сертификат у CA, который принимает аттестацию ключей этой службы или её отчёт об аудите, и подписывайте дайджест удалённо с помощью Jsign или инструмента поставщика. Требование к журналу аудита входит в CSBR; в статье о подписи с облачным KMS показано, какие журналы по умолчанию выключены.
  2. Управляемая служба подписи, например Artifact Signing или собственная служба CA: служба хранит ключ и сертификат, а вы отправляете дайджесты.
  3. Токен на выделенном хосте подписи, доступном только из задания релиза. Это просто, но теперь одна-единственная машина оказывается на критическом пути.

В любом случае подписывайте только артефакты релиза, только из защищённой ветки и с меткой времени, а хеш файла и удостоверение подписанта записывайте в примечания к релизу, чтобы пользователи и инструменты EDR могли проверить, что именно вы выпустили.

Типичные ошибки

  • Нет метки времени. Всё работает, пока не истечёт срок действия сертификата, а затем каждая установленная копия показывает недействительную подпись. С сертификатами на 72 часа или 460 дней этот день наступает скоро.
  • SHA-1 где бы то ни было. Раньше без /fd по умолчанию использовался SHA-1; текущий SignTool в таком случае отказывается подписывать, но старые скрипты, скопированные с форумов, всё ещё содержат /fd sha1 или /t.
  • Не та политика проверки. signtool verify без /pa объявляет действительную коммерческую подпись недоверенной.
  • Сертификат без EKU codeSigning или TLS-сертификат, используемый не по назначению. Проверяющие стороны его отклоняют, точно так же как OpenSSL выдаёт ошибку unsuitable certificate purpose.
  • Нет промежуточного сертификата. Включайте в подпись полную цепочку (-certs chain.pem или промежуточный сертификат CA, установленный на хосте подписи), иначе результат проверки зависит от того, что окажется на машине пользователя.
  • Расчёт на то, что EV заглушит SmartScreen. Теперь репутация накапливается со временем; подписывайте каждый релиз одним и тем же удостоверением, чтобы она переходила на следующие релизы.
  • Чтение данных из конца собственного подписанного файла (установщики, самораспаковывающиеся архивы): эта область подписью не покрыта.

Источники