Last active
January 10, 2023 17:44
-
-
Save laanwj/a0e00bcd3fe4cd2aa1c0803e91310495 to your computer and use it in GitHub Desktop.
Validate cryptographic signature on macos macho binary
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
import io | |
import struct | |
import sys | |
import pprint | |
import macholib.MachO | |
from macholib.mach_o import LC_CODE_SIGNATURE | |
import asn1crypto.x509 | |
from asn1crypto.cms import ContentInfo, SignedData, CMSAttributes | |
from oscrypto import asymmetric | |
from certvalidator.context import ValidationContext | |
import certvalidator | |
# Apple root certificate | |
APPLE_ROOT = b'0\x82\x04\x040\x82\x02\xec\xa0\x03\x02\x01\x02\x02\x08\x18z\xa9\xa8\xc2\x96!\x0c0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x000b1\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x130\x11\x06\x03U\x04\n\x13\nApple Inc.1&0$\x06\x03U\x04\x0b\x13\x1dApple Certification Authority1\x160\x14\x06\x03U\x04\x03\x13\rApple Root CA0\x1e\x17\r120201221215Z\x17\r270201221215Z0y1-0+\x06\x03U\x04\x03\x0c$Developer ID Certification Authority1&0$\x06\x03U\x04\x0b\x0c\x1dApple Certification Authority1\x130\x11\x06\x03U\x04\n\x0c\nApple Inc.1\x0b0\t\x06\x03U\x04\x06\x13\x02US0\x82\x01"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x01\x0f\x000\x82\x01\n\x02\x82\x01\x01\x00\x89vO\x06[\x9aA\xee\xa5#+\x02\xa3_\xd7s?\xc05\xb0\x8b\x84\n?\x06$\x7f\xa7\x95?\xebO\x0e\x93\xaf\xb4\x0e\xd0\xc8>\xe5m\x18\xb3\x1f\xe8\x89G\xbf\xd7\t\x08\xe4\xffV\x98)\x15\xe7\x94\x9d\xb95\xa3\n\xcd\xb4\xc0\xe1\xe2`\xf4\xca\xec)xEii`k_\x8a\x92\xfc\x9e#\xe6:\xc2"\xb31O\x1c\xba\xf2\xb64YB\xee\xb0\xa9\x02\x03\x18\x91\x04\xb6\xb3x.3\x1f\x80E\rEo\xbb\x0eZ[\x7f:\xe7\xd8\x08\xd7\x0b\x0e2m\xfb\x866\xe4l\xab\xc4\x11\x8ap\x84&\xaa\x9fD\xd1\xf1\xb8\xc6{\x94\x17\x9bH\xf7\x0bX\x16\xba#\xc5\x9f\x159~\xca]\xc32_\x0f\xe0R\x7f@\xea\xbe\xac\x08d\x95[\xc9\x1a\x9c\xe5\x80\xca\x1fjD\x1cl>\xc4\xb0&\x1f\x1d\xec{\xaf^\xa0j=G\xa9X\x121? v(m\x1d\x1c\xb0\xc2N\x11i&\x8b\xcb\xd6\xd0\x11\x82\xc9N\x0f\xf1Vt\xd0\xd9\x08Kfx\xa2\xab\xac\xa7\xe2\xd2L\x87Y\xc9\x02\x03\x01\x00\x01\xa3\x81\xa60\x81\xa30\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x14W\x17\xed\xa2\xcf\xdc|\x98\xa1\x10\xe0\xfc\xbe\x87-,\xf2\xe3\x17T0\x0f\x06\x03U\x1d\x13\x01\x01\xff\x04\x050\x03\x01\x01\xff0\x1f\x06\x03U\x1d#\x04\x180\x16\x80\x14+\xd0iG\x94v\t\xfe\xf4k\x8d.@\xa6\xf7GM\x7f\x08^0.\x06\x03U\x1d\x1f\x04\'0%0#\xa0!\xa0\x1f\x86\x1dhttp://crl.apple.com/root.crl0\x0e\x06\x03U\x1d\x0f\x01\x01\xff\x04\x04\x03\x02\x01\x860\x10\x06\n*\x86H\x86\xf7cd\x06\x02\x06\x04\x02\x05\x000\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x00\x03\x82\x01\x01\x00B9tk\xa1\xdc\xc6\xa4\x8f7*\x8c\xb3\x1d\nD\xbc\x95,\x7f\xbcY\xb8\xaca\xfb\x07\x90\x922\xb9\xd4\xbf;\xc1P9jDt\xa2\xec[\x1fp\xe5\xaa\xddKl\x1c#q-_\xd1\xc5\x93\xbe\xee\x9b\x8ape\x82\x9d\x16\xe3\x1a\x10\x17\x89-\xa8\xcd\xfd\x0cxXI\x0c(\x7f3\xee\x00z\x1b\xb4v\xac\xb6\xb5\xbbO\xdf\xa8\x1b\x9d\xc8\x19\x97J\x0bVg/\xc2>\xb6\xb3\xc4\x83:\xf0wmt\xc4.#Q\xee\x9a\xa5\x03o`\xf4\xa5H\xa7\x06\xc2\xbbZ\xe2\x1f\x1fFE~\xe4\x97\xf5\'\x10\xb7 "ror\xda\xc6Pu\xc5=%\x8f]\xa3\x00\xe9\x9f6\x8cH9\x8f\xb3;\xea\x90\x80.\x95\x9a`\xf4x\xce\xf4\x0e\nS>\xa2\xfaO\xd8\x1e\xae\x84\x95\x8d2\xbcVM\x89\xe9x\x18\xe0\xac\x9aB\xbazF\x1b\x84\xa2\x89\xce\x14\xe8\x88\xd1X\x8b\xf6\xaeV\xc4,\x05*E\xaf\x0b\xd9K\xa9\x02\x0f4\xac\x88\xc7aU\x89D\xc9\'s\x07\xee\x82\xe5N\xf5p' | |
# SuperBlob slot IDs | |
cdInfoSlot = 1 # Info.plist | |
cdRequirementsSlot = 2 # internal requirements | |
cdResourceDirSlot = 3 # resource directory | |
cdTopDirectorySlot = 4 # Application specific slot | |
cdEntitlementSlot = 5 # embedded entitlement configuration | |
cdRepSpecificSlot = 6 # for use by disk rep | |
cdEntitlementDERSlot = 7 # DER representation of entitlements | |
cdCodeDirectorySlot = 0 # CodeDirectory | |
cdAlternateCodeDirectorySlots = 0x1000 # alternate CodeDirectory array | |
cdAlternateCodeDirectoryLimit = 0x1005 # 5+1 hashes should be enough for everyone... | |
cdSignatureSlot = 0x10000 # CMS signature | |
cdIdentificationSlot = 0x10001 # identification blob (detached signatures only) | |
cdTicketSlot = 0x10002 # ticket embedded in signature (DMG only) | |
# Apple custom OIDs used in SignerInfo | |
SEC_OID_APPLE_HASH_AGILITY = '1.2.840.113635.100.9.1' | |
SEC_OID_APPLE_HASH_AGILITY_V2 = '1.2.840.113635.100.9.2' | |
SEC_OID_APPLE_EXPIRATION_TIME = '1.2.840.113635.100.9.3' | |
m = macholib.MachO.MachO(sys.argv[1]) | |
h = m.headers[0] | |
sigmeta = [cmd for cmd in h.commands if cmd[0].cmd == LC_CODE_SIGNATURE] | |
sigmeta = sigmeta[0] | |
with open(sys.argv[1], 'rb') as f: | |
f.seek(sigmeta[1].dataoff) | |
sig = f.read(sigmeta[1].datasize) | |
with io.BytesIO(sig) as f: | |
hdr = struct.unpack('>II', f.read(8)) | |
assert(hdr[0] == 0xfade0cc0) | |
num = struct.unpack('>I', f.read(4))[0] | |
slots = [] | |
for slot in range(num): | |
(slot_id, offset) = struct.unpack('>II', f.read(8)) | |
slots.append((slot_id, offset)) | |
blobs = [] | |
for (slot_id, offset) in slots: | |
f.seek(offset) | |
(blob_id, blob_size) = struct.unpack('>II', f.read(8)) | |
blob_data = f.read(blob_size) | |
blobs.append((slot_id, blob_id, blob_data)) | |
def sort_attributes(attrs_in): | |
''' | |
Sort the authenticated attributes for signing by re-encoding them, asn1crypto | |
takes care of the actual sorting of the set. | |
''' | |
attrs_out = CMSAttributes() | |
for attrval in attrs_in: | |
attrs_out.append(attrval) | |
return attrs_out | |
ctx = ValidationContext(trust_roots=[APPLE_ROOT], allow_fetching=False) | |
validate_chain = True | |
for (slot_id, blob_id, blob_data) in blobs: | |
if slot_id == cdSignatureSlot: | |
content = ContentInfo.load(blob_data) | |
sd = content['content'] | |
assert(isinstance(sd, SignedData)) | |
print('version', sd['version'].native) | |
print('digest_algorithms', [a.native for a in sd['digest_algorithms']]) | |
print('encap_content_info', sd['encap_content_info'].native) | |
# Parse certificates. | |
certs = [] | |
for cert in sd['certificates']: | |
c = cert.chosen | |
assert(isinstance(c, asn1crypto.x509.Certificate)) | |
certs.append(c) | |
intermediates = certs[0:-1] | |
end_entity_cert = certs[-1] | |
# this only works after adding | |
# '1.2.840.113635.100.6.1.13', # devid_execute | |
# to supported_extensions in certvalidator/validate.py | |
if validate_chain: | |
validator = certvalidator.CertificateValidator(end_entity_cert, intermediates, ctx) | |
validator.validate_usage({'digital_signature'}, {'code_signing'}) | |
# Validate SignerInfos | |
# Inspired by https://github.com/ralphje/signify/blob/master/signify/signerinfo.py | |
public_key = asymmetric.load_public_key(end_entity_cert.public_key) | |
for signerinfo in sd['signer_infos']: | |
assert(isinstance(signerinfo, asn1crypto.cms.SignerInfo)) | |
data = sort_attributes(signerinfo['signed_attrs']).dump() | |
signature = signerinfo['signature'].contents | |
digest_algorithm = signerinfo['digest_algorithm']['algorithm'].native | |
signature_algorithm = signerinfo['signature_algorithm']['algorithm'].native | |
assert(signature_algorithm == 'rsassa_pkcs1v15') | |
# raises oscrypto.errors.SignatureError on wrong signature | |
asymmetric.rsa_pkcs1v15_verify(public_key, signature, data, digest_algorithm) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment