├── test_data ├── simple ├── pciide.sys ├── simple.res ├── pciide.sys.res ├── SoftwareUpdate.exe └── SoftwareUpdate.exe.res ├── asn1 ├── __init__.py ├── time_test.py ├── oids.py ├── x509_time.py ├── dn.py ├── spc.py ├── x509.py └── pkcs7.py ├── CHANGES ├── generate_test_data.py ├── README ├── pecoff_blob.py ├── auth_data_test.py ├── fingerprinter_test.py ├── print_pe_certs.py ├── COPYING ├── fingerprint.py └── auth_data.py /test_data/simple: -------------------------------------------------------------------------------- 1 | MZ1234567890 2 | -------------------------------------------------------------------------------- /asn1/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | -------------------------------------------------------------------------------- /test_data/pciide.sys: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthrotype/verify-sigs/HEAD/test_data/pciide.sys -------------------------------------------------------------------------------- /test_data/simple.res: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthrotype/verify-sigs/HEAD/test_data/simple.res -------------------------------------------------------------------------------- /test_data/pciide.sys.res: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthrotype/verify-sigs/HEAD/test_data/pciide.sys.res -------------------------------------------------------------------------------- /test_data/SoftwareUpdate.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthrotype/verify-sigs/HEAD/test_data/SoftwareUpdate.exe -------------------------------------------------------------------------------- /test_data/SoftwareUpdate.exe.res: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthrotype/verify-sigs/HEAD/test_data/SoftwareUpdate.exe.res -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 2012/05/08: 1.2 2 | - Tiny fixes 3 | - Validating certificate chains in Authenticode binaries 4 | 5 | 2011/06/06: 1.1 6 | - Bugfixes: 7 | - More permissive handling of trailing data blobs when ASN.1 parsing. 8 | - Correct initialisation of signature verification hash function for 9 | non-SHA1 hashes. 10 | - Correct validation of alternate signature data encoding raw hashes. 11 | 12 | 2011/05/11: 1.0 13 | - Initial release 14 | -------------------------------------------------------------------------------- /asn1/time_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """Tests for time conversion utility.""" 20 | 21 | import time 22 | 23 | 24 | from pyasn1.type import useful 25 | 26 | import unittest as test 27 | 28 | from x509_time import Time 29 | 30 | 31 | class TimeTest(test.TestCase): 32 | 33 | def testConvert(self): 34 | utctime = useful.UTCTime('120614235959Z') 35 | t = Time() 36 | t.setComponentByName('utcTime', utctime) 37 | t_str = time.asctime(time.gmtime(t.ToPythonEpochTime())) 38 | self.assertEquals(t_str, 'Thu Jun 14 23:59:59 2012') 39 | 40 | 41 | def main(): 42 | test.main() 43 | 44 | if __name__ == '__main__': 45 | main() 46 | 47 | -------------------------------------------------------------------------------- /generate_test_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2010 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """Generates reference data for fingerprinter_test.py. 20 | 21 | Please note that this is supposed to be run only for updating 22 | test data in case more data is added or the format changes. 23 | This is a manual operation, that needs to be followed by a 24 | g4 submit of the changed test data. 25 | """ 26 | 27 | import os 28 | import pickle 29 | 30 | import fingerprint 31 | 32 | 33 | def main(): 34 | os.chdir('test_data') 35 | files = os.listdir('.') 36 | for fnam in files: 37 | if not fnam.lower().endswith('.res'): 38 | print 'Scanning %s' % fnam 39 | with file(fnam, 'rb') as objf: 40 | fingerprinter = fingerprint.Fingerprinter(objf) 41 | fingerprinter.EvalPecoff() 42 | fingerprinter.EvalGeneric() 43 | results = fingerprinter.HashIt() 44 | with file(fnam + '.res', 'wb') as resf: 45 | pickle.dump(results, resf, pickle.HIGHEST_PROTOCOL) 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /asn1/oids.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """ASN.1 OIDs mappings to parser classes or strings, where there is no class.""" 20 | 21 | import hashlib 22 | 23 | 24 | import pkcs7 25 | import spc 26 | 27 | # I want the formatting to make sense and be readable, really. 28 | # pylint: disable-msg=C6006,C6007 29 | OID_TO_CLASS = { 30 | (1,2,840,113549,1,7,1) : 'PKCS#7 Data', 31 | (1,2,840,113549,1,7,2) : pkcs7.SignedData, 32 | (1,2,840,113549,2,5) : hashlib.md5, 33 | (1,3,14,3,2,26) : hashlib.sha1, 34 | (1,3,6,1,4,1,311,2,1,4) : spc.SpcIndirectDataContent, 35 | (1,2,840,113549,1,9,3) : pkcs7.ContentType, 36 | (1,2,840,113549,1,9,4) : pkcs7.DigestInfo, 37 | (1,3,6,1,4,1,311,2,1,12) : spc.SpcSpOpusInfo, 38 | (1,2,840,113549,1,9,6) : pkcs7.CountersignInfo, # 'RSA_counterSign' 39 | (1,2,840,113549,1,9,5) : pkcs7.SigningTime, 40 | } 41 | 42 | OID_TO_PUBKEY = { 43 | (1,2,840,113549,1,1,1) : 'rsa', 44 | (1,2,840,113549,1,1,5) : 'rsa-sha1', 45 | (1,2,840,10040,4,1) : 'dsa', 46 | } 47 | -------------------------------------------------------------------------------- /asn1/x509_time.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """X.509 Time class and utility functions. 20 | 21 | Limited interpretation of ASN.1 time formats, 22 | as specified in RFC2459, section 4.1.2.5 23 | """ 24 | 25 | import calendar 26 | import time 27 | 28 | 29 | from pyasn1 import error 30 | from pyasn1.type import namedtype 31 | from pyasn1.type import univ 32 | from pyasn1.type import useful 33 | 34 | 35 | class Time(univ.Choice): 36 | componentType = namedtype.NamedTypes( 37 | namedtype.NamedType('utcTime', useful.UTCTime()), 38 | namedtype.NamedType('generalTime', useful.GeneralizedTime())) 39 | 40 | def ToPythonEpochTime(self): 41 | """Takes a ASN.1 Time choice, and returns seconds since epoch in UTC.""" 42 | utc_time = self.getComponentByName('utcTime') 43 | general_time = self.getComponentByName('generalTime') 44 | if utc_time and general_time: 45 | raise error.PyAsn1Error('Both elements of a choice are present.') 46 | if general_time: 47 | format_str = '%Y%m%d%H%M%SZ' 48 | time_str = str(general_time) 49 | else: 50 | format_str = '%y%m%d%H%M%SZ' 51 | time_str = str(utc_time) 52 | time_tpl = time.strptime(time_str, format_str) 53 | return calendar.timegm(time_tpl) 54 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ABSTRACT 2 | -------- 3 | verify_sigs contains library functions to compute and validate hashes 4 | on different file types, and signatures on PECOFF Authenticode-signed 5 | binaries. 6 | 7 | 8 | DEPENDENCIES 9 | ------------ 10 | You need pyasn1 == 0.13b or >= pyasn1-0.1.4rc4 11 | and M2Crypto to parse and validate signatures. 12 | To exercise some demonstrator code, you may need pefile. 13 | M2Crypto: http://chandlerproject.org/Projects/MeTooCrypto/ 14 | pyasn1: http://pyasn1.sourceforge.net/ 15 | pefile: http://code.google.com/p/pefile/ 16 | 17 | 18 | DETAILS 19 | ------- 20 | Currently the following hashing methods are supported: 21 | - generic files: 22 | md5, sha1, sha256, sha512 23 | - PE-COFF authenticode (windows executables, drivers, dll's, ...): 24 | md5, sha1 25 | 26 | fingerprint.py 27 | The actual library of hashing algorithms, deployable as library and on 28 | 'naked' client systems, running under python 2.7. See embedded docstrings 29 | and tests for usage scenarios. Does not use third party libraries. 30 | 31 | fingerprinter_test.py 32 | Set of tests on the fingerprinter, using pregenerated data. 33 | 34 | generate_test_data.py 35 | Run-once code supposed to be run by hand, creates some of the files 36 | in test_data, that then need to be checked in. 37 | 38 | auth_data.py 39 | Basic container for authenticode data, as represented in ASN.1 together 40 | with accessor and validator functions. Currently provides limited validation, 41 | in particular certificate chain validation is missing. 42 | 43 | auth_data_test.py 44 | Set of tests on auth_data, assuring that pregenerated data still 45 | produces the same reuslts. 46 | 47 | pecoff_blob.py 48 | Container for PECOFF format part of authenticode blobs, as provided 49 | by the fingerprinter library in the SignedData structure. 50 | 51 | print_pe_certs.py 52 | Exercises authenticode validation routines, prints out hashes and certs. 53 | 54 | 55 | 56 | 57 | THANKS 58 | ------ 59 | Many thanks to Darren and Michael for motivating me to work through tangled 60 | standards. 61 | Many thanks to Ero for pefile, and to Ilya Etingof for pyasn1, very useful 62 | examples code for x509 and pkcs7 parsing, and finally for extending the 63 | parser to handle 'any' type! 64 | 65 | Germano Caronni, 2012/4/26 66 | caronni@google.com , gec@acm.org 67 | -------------------------------------------------------------------------------- /pecoff_blob.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """Deal with Microsoft-specific Authenticode data.""" 20 | 21 | # Comments and constant names as extracted from pecoff_v8 specs. 22 | # Variable names are also used as defined in the PECOFF specification. 23 | # pylint: disable-msg=C6409 24 | 25 | # Version 1, legacy version of Win_Certificate structure. It is supported 26 | # only for purposes of verifying legacy Authenticode signatures. 27 | WIN_CERT_REVISION_1_0 = 0x100 28 | 29 | # Version 2 is the current version of the Win_Certificate structure. 30 | WIN_CERT_REVISION_2_0 = 0x200 31 | 32 | # Only type PKCS is supported by the pecoff specification. 33 | WIN_CERT_TYPE_X509 = 1 34 | WIN_CERT_TYPE_PKCS_SIGNED_DATA = 2 35 | WIN_CERT_TYPE_RESERVED_1 = 3 36 | WIN_CERT_TYPE_TS_STACK_SIGNED = 4 37 | 38 | 39 | class PecoffBlob(object): 40 | """Encapsulating class for Microsoft-specific Authenticode data. 41 | 42 | As defined in the PECOFF (v8) and Authenticode specifications. 43 | This is data as it is extracted from the signature_data field by 44 | the fingerprinter. 45 | """ 46 | 47 | def __init__(self, signed_data_tuple): 48 | self._wRevision = signed_data_tuple[0] 49 | self._wCertificateType = signed_data_tuple[1] 50 | self._bCertificate = signed_data_tuple[2] 51 | 52 | if self._wRevision != WIN_CERT_REVISION_2_0: 53 | raise RuntimeError("Unknown revision %#x." % self._wRevision) 54 | if self._wCertificateType != WIN_CERT_TYPE_PKCS_SIGNED_DATA: 55 | raise RuntimeError("Unknown cert type %#x." % self._wCertificateType) 56 | 57 | def getCertificateBlob(self): 58 | return self._bCertificate 59 | -------------------------------------------------------------------------------- /asn1/dn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """dn converts ASN.1 Distinguished Names to strings. 20 | 21 | TODO(user): TraverseRdn should build a better string representation, 22 | see comments below. RFC2253 provides the right way to do this. Instead of 23 | returning a dict, return a string. 24 | May also want an inverse function, parsing a string into an RDN sequence. 25 | """ 26 | 27 | 28 | from pyasn1.codec.ber import decoder 29 | 30 | 31 | class DistinguishedName(object): 32 | """Container for relevant OIDs and static conversion methods.""" 33 | 34 | # I want the formatting to make sense and be readable, really. 35 | # pylint: disable-msg=C6007 36 | OIDs = { 37 | (2, 5, 4, 3) : 'CN', # common name 38 | (2, 5, 4, 6) : 'C', # country 39 | (2, 5, 4, 7) : 'L', # locality 40 | (2, 5, 4, 8) : 'ST', # stateOrProvince 41 | (2, 5, 4, 10) : 'O', # organization 42 | (2, 5, 4, 11) : 'OU', # organizationalUnit 43 | (0, 9, 2342, 19200300, 100, 1, 25) : 'DC', # domainComponent 44 | (1, 2, 840, 113549, 1, 9, 1) : 'EMAIL',# emailaddress 45 | } 46 | # pylint: enable-msg=C6007 47 | 48 | @staticmethod 49 | def OidToName(oid): 50 | return DistinguishedName.OIDs.get(oid, str(oid)) 51 | 52 | @staticmethod 53 | def TraverseRdn(rdn): 54 | """Traverses RDN structure and returns string encoding of the DN. 55 | 56 | Args: 57 | rdn: ASN.1 SET (or SEQUENCE) containing RDNs (relative distinguished 58 | names), as identified by type / value pairs. A typical input would 59 | be of type X.509 RelativeDistinguishedName. 60 | 61 | Returns: 62 | A dict representing the Distinguished Name. 63 | """ 64 | val = dict() 65 | for n in rdn: 66 | # Note that this does not work for e.g. DC which is present 67 | # multiple times. 68 | # For a real DN parser, make sure to follow the spec in regards 69 | # to multiple occurence of a field in subsequent RDNs, maintaining 70 | # original ordering etc. 71 | # TODO(user): What about elements other than [0]?? 72 | name = DistinguishedName.OidToName(n[0]['type']) 73 | value = decoder.decode(n[0]['value']) 74 | if name in val: 75 | val[name] = str(value[0]) + ', ' + val.get(name, '') 76 | else: 77 | val[name] = str(value[0]) 78 | return val 79 | -------------------------------------------------------------------------------- /asn1/spc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """Authenticode-specific ASN.1 data structures.""" 20 | 21 | 22 | from pkcs7 import DigestInfo 23 | from pyasn1.type import char 24 | from pyasn1.type import namedtype 25 | from pyasn1.type import tag 26 | from pyasn1.type import univ 27 | import x509 28 | 29 | 30 | class SpcAttributeTypeAndOptionalValue(univ.Sequence): 31 | componentType = namedtype.NamedTypes( 32 | namedtype.NamedType('type', x509.AttributeType()), 33 | namedtype.OptionalNamedType('value', x509.AttributeValue())) 34 | 35 | 36 | class SpcIndirectDataContent(univ.Sequence): 37 | componentType = namedtype.NamedTypes( 38 | namedtype.NamedType('data', SpcAttributeTypeAndOptionalValue()), 39 | namedtype.NamedType('messageDigest', DigestInfo())) 40 | 41 | 42 | class SpcUuid(univ.OctetString): 43 | pass 44 | 45 | 46 | class SpcSerializedObject(univ.Sequence): 47 | componentType = namedtype.NamedTypes( 48 | namedtype.NamedType('classId', SpcUuid()), 49 | namedtype.NamedType('serializedData', univ.OctetString())) 50 | 51 | 52 | class SpcString(univ.Choice): 53 | componentType = namedtype.NamedTypes( 54 | namedtype.NamedType('unicode', char.BMPString().subtype( 55 | implicitTag=tag.Tag(tag.tagClassContext, 56 | tag.tagFormatConstructed, 0))), 57 | namedtype.NamedType('ascii', char.IA5String().subtype( 58 | implicitTag=tag.Tag(tag.tagClassContext, 59 | tag.tagFormatConstructed, 1)))) 60 | 61 | 62 | class SpcLink(univ.Choice): 63 | """According to Authenticode specification.""" 64 | componentType = namedtype.NamedTypes( 65 | namedtype.NamedType('url', char.IA5String().subtype( 66 | implicitTag=tag.Tag(tag.tagClassContext, 67 | tag.tagFormatConstructed, 0))), 68 | namedtype.NamedType('moniker', SpcSerializedObject().subtype( 69 | implicitTag=tag.Tag(tag.tagClassContext, 70 | tag.tagFormatConstructed, 1))), 71 | namedtype.NamedType('file', SpcString().subtype( 72 | explicitTag=tag.Tag(tag.tagClassContext, 73 | tag.tagFormatConstructed, 2)))) 74 | 75 | 76 | class SpcSpOpusInfo(univ.Sequence): 77 | componentType = namedtype.NamedTypes( 78 | namedtype.OptionalNamedType('programName', SpcString().subtype( 79 | explicitTag=tag.Tag(tag.tagClassContext, 80 | tag.tagFormatConstructed, 0))), 81 | namedtype.OptionalNamedType('moreInfo', SpcLink().subtype( 82 | explicitTag=tag.Tag(tag.tagClassContext, 83 | tag.tagFormatConstructed, 1)))) 84 | -------------------------------------------------------------------------------- /auth_data_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2012 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """Test for auth_data parser of Authenticode signatures.""" 20 | 21 | import os 22 | import pickle 23 | import time 24 | 25 | 26 | import unittest as test 27 | 28 | import auth_data 29 | import pecoff_blob 30 | 31 | 32 | # EVIL EVIL -- Monkeypatch to extend accessor 33 | # TODO(user): This was submitted to pyasn1. Remove when we have it back. 34 | def F(self, idx): 35 | if type(idx) is int: 36 | return self.getComponentByPosition(idx) 37 | else: return self.getComponentByName(idx) 38 | from pyasn1.type import univ # pylint: disable-msg=C6204,C6203 39 | univ.SequenceAndSetBase.__getitem__ = F 40 | del F, univ 41 | # EVIL EVIL 42 | 43 | 44 | class AuthenticodeTest(test.TestCase): 45 | 46 | def testRunTestData(self): 47 | # Walk through one data file in the test_data folder, and compare output 48 | # with precomputed expected output. 49 | data_file = os.path.join('test_data', 'SoftwareUpdate.exe.res') 50 | 51 | with file(data_file, 'rb') as resf: 52 | exp_results = pickle.load(resf) 53 | 54 | # Make sure we have loaded the right data. 55 | expected_generic_sha1 = '8322f1c2c355d88432f1f03a1f231f63912186bd' 56 | loaded_generic_hashes = [x for x in exp_results if x['name'] == 'generic'] 57 | loaded_generic_sha1 = loaded_generic_hashes[0]['sha1'].encode('hex') 58 | self.assertEquals(expected_generic_sha1, loaded_generic_sha1) 59 | 60 | signed_pecoffs = [x for x in exp_results if x['name'] == 'pecoff' and 61 | 'SignedData' in x] 62 | # If the invoker of the fingerprinter specified multiple fingers for pecoff 63 | # hashing (possible, even if not sensible), then there can be more than one 64 | # entry in this list. Should not be the case for this sample. 65 | self.assertEquals(len(signed_pecoffs), 1) 66 | signed_pecoff = signed_pecoffs[0] 67 | 68 | # Make sure PE/COFF hashes match as well. Again, just a sanity check. 69 | expected_auth_sha1 = '978b90ace99c764841d2dd17d278fac4149962a3' 70 | loaded_auth_sha1 = signed_pecoff['sha1'].encode('hex') 71 | self.assertEquals(expected_auth_sha1, loaded_auth_sha1) 72 | 73 | signed_datas = signed_pecoff['SignedData'] 74 | # There may be multiple of these, if the windows binary was signed multiple 75 | # times, e.g. by different entities. Each of them adds a complete SignedData 76 | # blob to the binary. For our sample, there is only one blob. 77 | self.assertEquals(len(signed_datas), 1) 78 | signed_data = signed_datas[0] 79 | 80 | blob = pecoff_blob.PecoffBlob(signed_data) 81 | 82 | auth = auth_data.AuthData(blob.getCertificateBlob()) 83 | content_hasher_name = auth.digest_algorithm().name 84 | computed_content_hash = signed_pecoff[content_hasher_name] 85 | 86 | try: 87 | auth.ValidateAsn1() 88 | auth.ValidateHashes(computed_content_hash) 89 | auth.ValidateSignatures() 90 | auth.ValidateCertChains(time.gmtime()) 91 | except auth_data.Asn1Error: 92 | if auth.openssl_error: 93 | print('OpenSSL Errors:\n%s' % auth.openssl_error) 94 | raise 95 | 96 | print('Program: %s, URL: %s' % (auth.program_name, auth.program_url)) 97 | print('countersig: %d' % auth.has_countersignature) 98 | print('Timestamp: %s' % auth.counter_timestamp) 99 | 100 | self.assertEquals(auth.trailing_data.encode('hex'), '00') 101 | 102 | 103 | def main(): 104 | test.main() 105 | 106 | if __name__ == '__main__': 107 | main() 108 | -------------------------------------------------------------------------------- /asn1/x509.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | # Partially derived from pyasn1 examples. 19 | 20 | """Subset of X.509 message syntax.""" 21 | 22 | 23 | from pyasn1.type import namedtype 24 | from pyasn1.type import namedval 25 | from pyasn1.type import tag 26 | from pyasn1.type import univ 27 | 28 | from x509_time import Time 29 | 30 | 31 | class AttributeValue(univ.Any): 32 | pass 33 | 34 | 35 | class AttributeType(univ.ObjectIdentifier): 36 | pass 37 | 38 | 39 | class AttributeTypeAndValue(univ.Sequence): 40 | componentType = namedtype.NamedTypes( 41 | namedtype.NamedType('type', AttributeType()), 42 | namedtype.NamedType('value', AttributeValue())) 43 | 44 | 45 | class RelativeDistinguishedName(univ.SetOf): 46 | componentType = AttributeTypeAndValue() 47 | 48 | 49 | class RDNSequence(univ.SequenceOf): 50 | componentType = RelativeDistinguishedName() 51 | 52 | 53 | class Name(univ.Choice): 54 | componentType = namedtype.NamedTypes( 55 | namedtype.NamedType('', RDNSequence())) 56 | 57 | 58 | class AlgorithmIdentifier(univ.Sequence): 59 | componentType = namedtype.NamedTypes( 60 | namedtype.NamedType('algorithm', univ.ObjectIdentifier()), 61 | namedtype.OptionalNamedType('parameters', univ.Any())) 62 | 63 | 64 | class Extension(univ.Sequence): 65 | componentType = namedtype.NamedTypes( 66 | namedtype.NamedType('extnID', univ.ObjectIdentifier()), 67 | namedtype.DefaultedNamedType('critical', univ.Boolean('False')), 68 | namedtype.NamedType('extnValue', univ.Any())) 69 | 70 | 71 | class Extensions(univ.SequenceOf): 72 | componentType = Extension() 73 | 74 | 75 | class SubjectPublicKeyInfo(univ.Sequence): 76 | componentType = namedtype.NamedTypes( 77 | namedtype.NamedType('algorithm', AlgorithmIdentifier()), 78 | namedtype.NamedType('subjectPublicKey', univ.BitString())) 79 | 80 | 81 | class UniqueIdentifier(univ.BitString): 82 | pass 83 | 84 | 85 | class Validity(univ.Sequence): 86 | componentType = namedtype.NamedTypes( 87 | namedtype.NamedType('notBefore', Time()), 88 | namedtype.NamedType('notAfter', Time())) 89 | 90 | 91 | class CertificateSerialNumber(univ.Integer): 92 | pass 93 | 94 | 95 | class Version(univ.Integer): 96 | namedValues = namedval.NamedValues(('v1', 0), ('v2', 1), ('v3', 2)) 97 | 98 | 99 | class TBSCertificate(univ.Sequence): 100 | """According to X.509 specification.""" 101 | componentType = namedtype.NamedTypes( 102 | namedtype.DefaultedNamedType( 103 | 'version', Version('v1', tagSet=Version.tagSet.tagExplicitly(tag.Tag( 104 | tag.tagClassContext, tag.tagFormatSimple, 0)))), 105 | namedtype.NamedType('serialNumber', CertificateSerialNumber()), 106 | namedtype.NamedType('signature', AlgorithmIdentifier()), 107 | namedtype.NamedType('issuer', Name()), 108 | namedtype.NamedType('validity', Validity()), 109 | namedtype.NamedType('subject', Name()), 110 | namedtype.NamedType('subjectPublicKeyInfo', SubjectPublicKeyInfo()), 111 | namedtype.OptionalNamedType( 112 | 'issuerUniqueID', UniqueIdentifier().subtype(implicitTag=tag.Tag( 113 | tag.tagClassContext, tag.tagFormatSimple, 1))), 114 | namedtype.OptionalNamedType( 115 | 'subjectUniqueID', UniqueIdentifier().subtype(implicitTag=tag.Tag( 116 | tag.tagClassContext, tag.tagFormatSimple, 2))), 117 | namedtype.OptionalNamedType('extensions', Extensions().subtype( 118 | explicitTag=tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 3)))) 119 | 120 | 121 | class Certificate(univ.Sequence): 122 | componentType = namedtype.NamedTypes( 123 | namedtype.NamedType('tbsCertificate', TBSCertificate()), 124 | namedtype.NamedType('signatureAlgorithm', AlgorithmIdentifier()), 125 | namedtype.NamedType('signatureValue', univ.BitString())) 126 | -------------------------------------------------------------------------------- /fingerprinter_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2010 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """Test for fingerprinter.""" 20 | 21 | import os 22 | import pickle 23 | import StringIO 24 | 25 | 26 | 27 | import unittest as test 28 | import fingerprint 29 | 30 | 31 | class FingerprinterTest(test.TestCase): 32 | 33 | def testRunTestData(self): 34 | # Walk through all data files in the test_data folder, and compare output 35 | # with precomputed expected output. 36 | data_dir = os.path.join("test_data") 37 | files = os.listdir(data_dir) 38 | for fnam in files: 39 | if not fnam.lower().endswith(".res"): 40 | with file(os.path.join(data_dir, fnam), "rb") as objf: 41 | fp = fingerprint.Fingerprinter(objf) 42 | fp.EvalGeneric() 43 | fp.EvalPecoff() 44 | results = fp.HashIt() 45 | with file(os.path.join(data_dir, fnam + ".res"), "rb") as resf: 46 | exp_results = pickle.load(resf) 47 | diff = (exp_results != results) 48 | if diff: 49 | print 50 | print fingerprint.FormatResults(resf, exp_results) 51 | print fingerprint.FormatResults(objf, results) 52 | self.fail() 53 | 54 | def testReasonableInterval(self): 55 | # Check if the limit on maximum blocksize for processing still holds. 56 | dummy = StringIO.StringIO("") 57 | fp = fingerprint.Fingerprinter(dummy) 58 | big_finger = fingerprint.Finger(None, 59 | [fingerprint.Range(0, 1000001)], 60 | None) 61 | fp.fingers.append(big_finger) 62 | start, stop = fp._GetNextInterval() 63 | self.assertEquals(0, start) 64 | self.assertEquals(1000000, stop) 65 | 66 | def testAdjustments(self): 67 | dummy = StringIO.StringIO("") 68 | fp = fingerprint.Fingerprinter(dummy) 69 | big_finger = fingerprint.Finger(None, 70 | [fingerprint.Range(10, 20)], 71 | None) 72 | fp.fingers.append(big_finger) 73 | 74 | # The remaining range should not yet be touched... 75 | fp._AdjustIntervals(9, 10) 76 | self.assertEquals([fingerprint.Range(10, 20)], fp.fingers[0].ranges) 77 | # Trying to consume into the range. Blow up. 78 | self.assertRaises(RuntimeError, fp._AdjustIntervals, 9, 11) 79 | # We forgot a byte. Blow up. 80 | self.assertRaises(RuntimeError, fp._AdjustIntervals, 11, 12) 81 | # Consume a byte 82 | fp._AdjustIntervals(10, 11) 83 | self.assertEquals([fingerprint.Range(11, 20)], fp.fingers[0].ranges) 84 | # Consumed too much. Blow up. 85 | self.assertRaises(RuntimeError, fp._AdjustIntervals, 11, 21) 86 | # Consume exactly. 87 | fp._AdjustIntervals(11, 20) 88 | self.assertEquals(0, len(fp.fingers[0].ranges)) 89 | 90 | class MockHasher(object): 91 | def __init__(self): 92 | self.seen = "" 93 | 94 | def update(self, content): # pylint: disable-msg=C6409 95 | self.seen += content 96 | 97 | def testHashBlock(self): 98 | # Does it invoke a hash function? 99 | dummy = "12345" 100 | fp = fingerprint.Fingerprinter(StringIO.StringIO(dummy)) 101 | big_finger = fingerprint.Finger(None, 102 | [fingerprint.Range(0, len(dummy))], 103 | None) 104 | hasher = self.MockHasher() 105 | big_finger.hashers = [hasher] 106 | fp.fingers.append(big_finger) 107 | # Let's process the block 108 | fp._HashBlock(dummy, 0, len(dummy)) 109 | self.assertEquals(hasher.seen, dummy) 110 | 111 | # TODO(user): Add more tests for the carry-over of HashIt, 112 | # the pecoff parsing pieces, and the parser / collector of the SignedData 113 | # blob. 114 | # Make sure Authenticode hashes are set to MD5, SHA1 by default, since so 115 | # far authenticode does not support other hashing functions. 116 | # Check that default hashers get used when no argument is provided, or 117 | # None is provided. Make sure 'empty iterable' actually works as intended. 118 | 119 | 120 | def main(): 121 | test.main() 122 | 123 | if __name__ == "__main__": 124 | main() 125 | -------------------------------------------------------------------------------- /asn1/pkcs7.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | # Partially derived from pyasn1 examples. 19 | 20 | """Subset of PKCS#7 message syntax.""" 21 | 22 | 23 | from pyasn1.type import namedtype 24 | from pyasn1.type import tag 25 | from pyasn1.type import univ 26 | import x509 27 | from x509_time import Time 28 | 29 | 30 | class Attribute(univ.Sequence): 31 | componentType = namedtype.NamedTypes( 32 | namedtype.NamedType('type', x509.AttributeType()), 33 | namedtype.NamedType('values', univ.SetOf( 34 | componentType=x509.AttributeValue()))) 35 | 36 | 37 | class ContentType(univ.ObjectIdentifier): 38 | pass 39 | 40 | 41 | class Version(univ.Integer): 42 | pass 43 | 44 | 45 | class DigestAlgorithmIdentifier(x509.AlgorithmIdentifier): 46 | pass 47 | 48 | 49 | class DigestAlgorithmIdentifiers(univ.SetOf): 50 | componentType = DigestAlgorithmIdentifier() 51 | 52 | 53 | class Digest(univ.OctetString): 54 | pass 55 | 56 | 57 | class DigestInfo(univ.Sequence): 58 | componentType = namedtype.NamedTypes( 59 | namedtype.NamedType('digestAlgorithm', DigestAlgorithmIdentifier()), 60 | namedtype.NamedType('digest', Digest())) 61 | 62 | 63 | class ContentInfo(univ.Sequence): 64 | componentType = namedtype.NamedTypes( 65 | namedtype.NamedType('contentType', ContentType()), 66 | namedtype.OptionalNamedType('content', univ.Any().subtype( 67 | explicitTag=tag.Tag(tag.tagClassContext, 68 | tag.tagFormatConstructed, 0)))) 69 | 70 | 71 | class IssuerAndSerialNumber(univ.Sequence): 72 | componentType = namedtype.NamedTypes( 73 | namedtype.NamedType('issuer', x509.Name()), 74 | namedtype.NamedType('serialNumber', x509.CertificateSerialNumber())) 75 | 76 | 77 | class Attributes(univ.SetOf): 78 | componentType = Attribute() 79 | 80 | 81 | class ExtendedCertificateInfo(univ.Sequence): 82 | componentType = namedtype.NamedTypes( 83 | namedtype.NamedType('version', Version()), 84 | namedtype.NamedType('certificate', x509.Certificate()), 85 | namedtype.NamedType('attributes', Attributes())) 86 | 87 | 88 | class SignatureAlgorithmIdentifier(x509.AlgorithmIdentifier): 89 | pass 90 | 91 | 92 | class Signature(univ.BitString): 93 | pass 94 | 95 | 96 | class ExtendedCertificate(univ.Sequence): 97 | componentType = namedtype.NamedTypes( 98 | namedtype.NamedType('extendedCertificateInfo', ExtendedCertificateInfo()), 99 | namedtype.NamedType('signatureAlgorithm', SignatureAlgorithmIdentifier()), 100 | namedtype.NamedType('signature', Signature())) 101 | 102 | 103 | class ExtendedCertificateOrCertificate(univ.Choice): 104 | componentType = namedtype.NamedTypes( 105 | namedtype.NamedType('certificate', x509.Certificate()), 106 | namedtype.NamedType('extendedCertificate', ExtendedCertificate().subtype( 107 | implicitTag=tag.Tag(tag.tagClassContext, 108 | tag.tagFormatConstructed, 0)))) 109 | 110 | 111 | class ExtendedCertificatesAndCertificates(univ.SetOf): 112 | componentType = ExtendedCertificateOrCertificate() 113 | 114 | 115 | class SerialNumber(univ.Integer): 116 | pass 117 | 118 | 119 | class CertificateRevocationLists(univ.Any): 120 | pass 121 | 122 | 123 | class DigestEncryptionAlgorithmIdentifier(x509.AlgorithmIdentifier): 124 | pass 125 | 126 | 127 | class EncryptedDigest(univ.OctetString): 128 | pass 129 | 130 | 131 | class SignerInfo(univ.Sequence): 132 | """As defined by PKCS#7.""" 133 | componentType = namedtype.NamedTypes( 134 | namedtype.NamedType('version', Version()), 135 | namedtype.NamedType('issuerAndSerialNumber', IssuerAndSerialNumber()), 136 | namedtype.NamedType('digestAlgorithm', DigestAlgorithmIdentifier()), 137 | namedtype.OptionalNamedType( 138 | 'authenticatedAttributes', Attributes().subtype(implicitTag=tag.Tag( 139 | tag.tagClassContext, tag.tagFormatConstructed, 0))), 140 | namedtype.NamedType('digestEncryptionAlgorithm', 141 | DigestEncryptionAlgorithmIdentifier()), 142 | namedtype.NamedType('encryptedDigest', EncryptedDigest()), 143 | namedtype.OptionalNamedType('unauthenticatedAttributes', 144 | Attributes().subtype(implicitTag=tag.Tag( 145 | tag.tagClassContext, 146 | tag.tagFormatConstructed, 1)))) 147 | 148 | 149 | class SignerInfos(univ.SetOf): 150 | componentType = SignerInfo() 151 | 152 | 153 | class SignedData(univ.Sequence): 154 | """As defined by PKCS#7.""" 155 | componentType = namedtype.NamedTypes( 156 | namedtype.NamedType('version', Version()), 157 | namedtype.NamedType('digestAlgorithms', DigestAlgorithmIdentifiers()), 158 | namedtype.NamedType('contentInfo', ContentInfo()), 159 | namedtype.OptionalNamedType( 160 | 'certificates', ExtendedCertificatesAndCertificates().subtype( 161 | implicitTag=tag.Tag(tag.tagClassContext, 162 | tag.tagFormatConstructed, 0))), 163 | namedtype.OptionalNamedType('crls', CertificateRevocationLists().subtype( 164 | implicitTag=tag.Tag(tag.tagClassContext, 165 | tag.tagFormatConstructed, 1))), 166 | namedtype.NamedType('signerInfos', SignerInfos())) 167 | 168 | 169 | class CountersignInfo(SignerInfo): 170 | pass 171 | 172 | 173 | class SigningTime(Time): 174 | pass 175 | -------------------------------------------------------------------------------- /print_pe_certs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2011 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """Wrapper to exercise fingerprinting and authenticode validation. 20 | 21 | Give it a full path to e.g. a windows binary. 22 | """ 23 | 24 | # I really want to use parens in print statements. 25 | # pylint: disable-msg=C6003 26 | 27 | import hashlib 28 | import pprint 29 | import sys 30 | import time 31 | 32 | 33 | from pyasn1.codec.der import encoder as der_encoder 34 | 35 | import auth_data 36 | import fingerprint 37 | import pecoff_blob 38 | from asn1 import dn 39 | 40 | 41 | # EVIL EVIL -- Monkeypatch to extend accessor 42 | # TODO(user): This was submitted to pyasn1. Remove when we have it back. 43 | def F(self, idx): 44 | if type(idx) is int: 45 | return self.getComponentByPosition(idx) 46 | else: return self.getComponentByName(idx) 47 | from pyasn1.type import univ # pylint: disable-msg=C6204,C6203 48 | univ.SequenceAndSetBase.__getitem__ = F 49 | del F, univ 50 | # EVIL EVIL 51 | 52 | 53 | def main(): 54 | data_file = sys.argv[1] 55 | 56 | with file(data_file, 'rb') as objf: 57 | fingerprinter = fingerprint.Fingerprinter(objf) 58 | is_pecoff = fingerprinter.EvalPecoff() 59 | fingerprinter.EvalGeneric() 60 | results = fingerprinter.HashIt() 61 | 62 | print('Generic hashes:') 63 | hashes = [x for x in results if x['name'] == 'generic'] 64 | if len(hashes) > 1: 65 | print('More than one generic finger? Only printing first one.') 66 | for hname in sorted(hashes[0].keys()): 67 | if hname != 'name': 68 | print('%s: %s' % (hname, hashes[0][hname].encode('hex'))) 69 | print 70 | 71 | if not is_pecoff: 72 | print('This is not a PE/COFF binary. Exiting.') 73 | return 74 | 75 | print('PE/COFF hashes:') 76 | hashes = [x for x in results if x['name'] == 'pecoff'] 77 | if len(hashes) > 1: 78 | print('More than one PE/COFF finger? Only printing first one.') 79 | for hname in sorted(hashes[0].keys()): 80 | if hname != 'name' and hname != 'SignedData': 81 | print('%s: %s' % (hname, hashes[0][hname].encode('hex'))) 82 | print 83 | 84 | signed_pecoffs = [x for x in results if x['name'] == 'pecoff' and 85 | 'SignedData' in x] 86 | 87 | if not signed_pecoffs: 88 | print('This PE/COFF binary has no signature. Exiting.') 89 | return 90 | 91 | signed_pecoff = signed_pecoffs[0] 92 | 93 | signed_datas = signed_pecoff['SignedData'] 94 | # There may be multiple of these, if the windows binary was signed multiple 95 | # times, e.g. by different entities. Each of them adds a complete SignedData 96 | # blob to the binary. 97 | # TODO(user): Process all instances 98 | signed_data = signed_datas[0] 99 | 100 | blob = pecoff_blob.PecoffBlob(signed_data) 101 | 102 | auth = auth_data.AuthData(blob.getCertificateBlob()) 103 | content_hasher_name = auth.digest_algorithm().name 104 | computed_content_hash = signed_pecoff[content_hasher_name] 105 | 106 | try: 107 | auth.ValidateAsn1() 108 | auth.ValidateHashes(computed_content_hash) 109 | auth.ValidateSignatures() 110 | auth.ValidateCertChains(time.gmtime()) 111 | except auth_data.Asn1Error: 112 | if auth.openssl_error: 113 | print('OpenSSL Errors:\n%s' % auth.openssl_error) 114 | raise 115 | 116 | print('Program: %s, URL: %s' % (auth.program_name, auth.program_url)) 117 | if auth.has_countersignature: 118 | print('Countersignature is present. Timestamp: %s UTC' % 119 | time.asctime(time.gmtime(auth.counter_timestamp))) 120 | else: 121 | print('Countersignature is not present.') 122 | 123 | print('Binary is signed with cert issued by:') 124 | pprint.pprint(auth.signing_cert_id) 125 | print 126 | 127 | print('Cert chain head issued by:') 128 | pprint.pprint(auth.cert_chain_head[2]) 129 | print(' Chain not before: %s UTC' % 130 | (time.asctime(time.gmtime(auth.cert_chain_head[0])))) 131 | print(' Chain not after: %s UTC' % 132 | (time.asctime(time.gmtime(auth.cert_chain_head[1])))) 133 | print 134 | 135 | if auth.has_countersignature: 136 | print('Countersig chain head issued by:') 137 | pprint.pprint(auth.counter_chain_head[2]) 138 | print(' Countersig not before: %s UTC' % 139 | (time.asctime(time.gmtime(auth.counter_chain_head[0])))) 140 | print(' Countersig not after: %s UTC' % 141 | (time.asctime(time.gmtime(auth.counter_chain_head[1])))) 142 | print 143 | 144 | print('Certificates') 145 | for (issuer, serial), cert in auth.certificates.items(): 146 | print(' Issuer: %s' % issuer) 147 | print(' Serial: %s' % serial) 148 | subject = cert[0][0]['subject'] 149 | subject_dn = str(dn.DistinguishedName.TraverseRdn(subject[0])) 150 | print(' Subject: %s' % subject_dn) 151 | not_before = cert[0][0]['validity']['notBefore'] 152 | not_after = cert[0][0]['validity']['notAfter'] 153 | not_before_time = not_before.ToPythonEpochTime() 154 | not_after_time = not_after.ToPythonEpochTime() 155 | print(' Not Before: %s UTC (%s)' % 156 | (time.asctime(time.gmtime(not_before_time)), not_before[0])) 157 | print(' Not After: %s UTC (%s)' % 158 | (time.asctime(time.gmtime(not_after_time)), not_after[0])) 159 | bin_cert = der_encoder.encode(cert) 160 | print(' MD5: %s' % hashlib.md5(bin_cert).hexdigest()) 161 | print(' SHA1: %s' % hashlib.sha1(bin_cert).hexdigest()) 162 | print 163 | 164 | if auth.trailing_data: 165 | print('Signature Blob had trailing (unvalidated) data (%d bytes): %s' % 166 | (len(auth.trailing_data), auth.trailing_data.encode('hex'))) 167 | 168 | 169 | if __name__ == '__main__': 170 | main() 171 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /fingerprint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2010 Google Inc. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Author: caronni@google.com (Germano Caronni) 18 | 19 | """Fingerprinter class and some utilty functions to exercise it. 20 | 21 | While this file contains a main and some top-level functions, those 22 | are meant for exploration and debugging. Intended use is through the 23 | Fingerprinter, as exemplified in main. 24 | """ 25 | 26 | # TODO(user): Consider how to organically support multi-level 27 | # hashes such as e.g. the magnet: scheme. 28 | 29 | import collections 30 | import hashlib 31 | import os 32 | import struct 33 | import sys 34 | 35 | 36 | # pylint: disable-msg=C6409 37 | # Two classes given named tupes for ranges and relative ranges. 38 | Range = collections.namedtuple('Range', 'start end') 39 | RelRange = collections.namedtuple('RelRange', 'start len') 40 | # pylint: enable-msg=C6409 41 | 42 | 43 | class Finger(object): 44 | """A Finger defines how to hash a file to get specific fingerprints. 45 | 46 | The Finger contains one or more hash functions, a set of ranges in the 47 | file that are to be processed with these hash functions, and relevant 48 | metadata and accessor methods. 49 | 50 | While one Finger provides potentially multiple hashers, they all get 51 | fed the same ranges of the file. 52 | """ 53 | 54 | def __init__(self, hashers, ranges, metadata_dict): 55 | self.hashers = hashers 56 | self.ranges = ranges 57 | self.metadata = metadata_dict 58 | 59 | def CurrentRange(self): 60 | """The working range of this Finger. Returns None if there is none.""" 61 | if self.ranges: 62 | return self.ranges[0] 63 | return None 64 | 65 | def ConsumeRange(self, start, end): 66 | """Consumes an entire range, or part thereof. 67 | 68 | If the finger has no ranges left, or the curent range start is higher 69 | than the end of the consumed block, nothing happens. Otherwise, 70 | the current range is adjusted for the consumed block, or removed, 71 | if the entire block is consumed. For things to work, the consumed 72 | range and the current finger starts must be equal, and the length 73 | of the consumed range may not exceed the length of the current range. 74 | 75 | Args: 76 | start: Beginning of range to be consumed. 77 | end: First offset after the consumed range (end + 1). 78 | 79 | Raises: 80 | RuntimeError: if the start position of the consumed range is 81 | higher than the start of the current range in the finger, or if 82 | the consumed range cuts accross block boundaries. 83 | """ 84 | old = self.CurrentRange() 85 | if old is None: 86 | return 87 | if old.start > start: 88 | if old.start < end: 89 | raise RuntimeError('Block end too high.') 90 | return 91 | if old.start < start: 92 | raise RuntimeError('Block start too high.') 93 | if old.end == end: 94 | del(self.ranges[0]) 95 | elif old.end > end: 96 | self.ranges[0] = Range(end, old.end) 97 | else: 98 | raise RuntimeError('Block length exceeds range.') 99 | 100 | def HashBlock(self, block): 101 | """Given a data block, feed it to all the registered hashers.""" 102 | for hasher in self.hashers: 103 | hasher.update(block) 104 | 105 | 106 | class Fingerprinter(object): 107 | """Compute different types of cryptographic hashes over a file. 108 | 109 | Depending on type of file and mode of invocation, filetype-specific or 110 | generic hashes get computed over a file. Different hashes can cover 111 | different ranges of the file. The file is read only once. Memory 112 | use of class objects is dominated by min(file size, block size), 113 | as defined below. 114 | 115 | The class delivers an array with dicts of hashes by file type. Where 116 | appropriate, embedded signature data is also returned from the file. 117 | 118 | Suggested use: 119 | - Provide file object at initialisation time. 120 | - Invoke one or more of the Eval* functions, with your choice of hashers. 121 | - Call HashIt and take from the resulting dict what you need. 122 | """ 123 | 124 | BLOCK_SIZE = 1000000 125 | GENERIC_HASH_CLASSES = (hashlib.md5, hashlib.sha1, hashlib.sha256, 126 | hashlib.sha512) 127 | AUTHENTICODE_HASH_CLASSES = (hashlib.md5, hashlib.sha1) 128 | 129 | def __init__(self, file_obj): 130 | self.fingers = [] 131 | self.file = file_obj 132 | self.file.seek(0, os.SEEK_END) 133 | self.filelength = self.file.tell() 134 | 135 | def _GetNextInterval(self): 136 | """Returns the next Range of the file that is to be hashed. 137 | 138 | For all fingers, inspect their next expected range, and return the 139 | lowest uninterrupted range of interest. If the range is larger than 140 | BLOCK_SIZE, truncate it. 141 | 142 | Returns: 143 | Next range of interest in a Range namedtuple. 144 | """ 145 | starts = set([x.CurrentRange().start for x in self.fingers if x.ranges]) 146 | ends = set([x.CurrentRange().end for x in self.fingers if x.ranges]) 147 | if not starts: 148 | return None 149 | min_start = min(starts) 150 | starts.remove(min_start) 151 | ends |= starts 152 | min_end = min(ends) 153 | if min_end - min_start > self.BLOCK_SIZE: 154 | min_end = min_start + self.BLOCK_SIZE 155 | return Range(min_start, min_end) 156 | 157 | def _AdjustIntervals(self, start, end): 158 | for finger in self.fingers: 159 | finger.ConsumeRange(start, end) 160 | 161 | def _HashBlock(self, block, start, end): 162 | """_HashBlock feeds data blocks into the hashers of fingers. 163 | 164 | This function must be called before adjusting fingers for next 165 | interval, otherwise the lack of remaining ranges will cause the 166 | block not to be hashed for a specific finger. 167 | 168 | Start and end are used to validate the expected ranges, to catch 169 | unexpected use of that logic. 170 | 171 | Args: 172 | block: The data block. 173 | start: Beginning offset of this block. 174 | end: Offset of the next byte after the block. 175 | 176 | Raises: 177 | RuntimeError: If the provided and expected ranges don't match. 178 | """ 179 | for finger in self.fingers: 180 | expected_range = finger.CurrentRange() 181 | if expected_range is None: 182 | continue 183 | if (start > expected_range.start or 184 | (start == expected_range.start and end > expected_range.end) or 185 | (start < expected_range.start and end > expected_range.start)): 186 | raise RuntimeError('Cutting across fingers.') 187 | if start == expected_range.start: 188 | finger.HashBlock(block) 189 | 190 | def HashIt(self): 191 | """Finalizing function for the Fingerprint class. 192 | 193 | This method applies all the different hash functions over the 194 | previously specified different ranges of the input file, and 195 | computes the resulting hashes. 196 | 197 | After calling HashIt, the state of the object is reset to its 198 | initial state, with no fingers defined. 199 | 200 | Returns: 201 | An array of dicts, with each dict containing name of fingerprint 202 | type, names of hashes and values, and additional, type-dependent 203 | key / value pairs, such as an array of SignedData tuples for the 204 | PE/COFF fingerprint type. 205 | 206 | Raises: 207 | RuntimeError: when internal inconsistencies occur. 208 | """ 209 | while True: 210 | interval = self._GetNextInterval() 211 | if interval is None: 212 | break 213 | self.file.seek(interval.start, os.SEEK_SET) 214 | block = self.file.read(interval.end - interval.start) 215 | if len(block) != interval.end - interval.start: 216 | raise RuntimeError('Short read on file.') 217 | self._HashBlock(block, interval.start, interval.end) 218 | self._AdjustIntervals(interval.start, interval.end) 219 | 220 | results = [] 221 | for finger in self.fingers: 222 | res = {} 223 | leftover = finger.CurrentRange() 224 | if leftover: 225 | if (len(finger.ranges) > 1 or 226 | leftover.start != self.filelength or 227 | leftover.end != self.filelength): 228 | raise RuntimeError('Non-empty range remains.') 229 | res.update(finger.metadata) 230 | for hasher in finger.hashers: 231 | res[hasher.name] = hasher.digest() 232 | results.append(res) 233 | 234 | # Clean out things for a fresh start (on the same file object). 235 | self.fingers = [] 236 | 237 | # Make sure the results come back in 'standard' order, regardless of the 238 | # order in which fingers were added. Helps with reproducing test results. 239 | return sorted(results, key=lambda r: r['name']) 240 | 241 | def EvalGeneric(self, hashers=None): 242 | """Causes the entire file to be hashed by the given hash functions. 243 | 244 | This sets up a 'finger' for fingerprinting, where the entire file 245 | is passed through a pre-defined (or user defined) set of hash functions. 246 | 247 | Args: 248 | hashers: An iterable of hash classes (e.g. out of hashlib) which will 249 | be instantiated for use. If hashers is not provided, or is 250 | provided as 'None', the default hashers will get used. To 251 | invoke this without hashers, provide an empty list. 252 | 253 | Returns: 254 | Always True, as all files are 'generic' files. 255 | """ 256 | if hashers is None: 257 | hashers = Fingerprinter.GENERIC_HASH_CLASSES 258 | hashfuncs = [x() for x in hashers] 259 | finger = Finger(hashfuncs, 260 | [Range(0, self.filelength)], 261 | {'name': 'generic'}) 262 | self.fingers.append(finger) 263 | return True 264 | 265 | def _PecoffHeaderParser(self): 266 | """Parses PECOFF headers. 267 | 268 | Reads header magic and some data structures in a file to determine if 269 | it is a valid PECOFF header, and figure out the offsets at which 270 | relevant data is stored. 271 | While this code contains multiple seeks and small reads, that is 272 | compensated by the underlying libc buffering mechanism. 273 | 274 | Returns: 275 | None if the parsed file is not PECOFF. 276 | A dict with offsets and lengths for CheckSum, CertTable, and SignedData 277 | fields in the PECOFF binary, for those that are present. 278 | """ 279 | extents = {} 280 | self.file.seek(0, os.SEEK_SET) 281 | buf = self.file.read(2) 282 | if buf != 'MZ': 283 | return None 284 | self.file.seek(0x3C, os.SEEK_SET) 285 | buf = self.file.read(4) 286 | pecoff_sig_offset = struct.unpack('= self.filelength: 288 | return None 289 | self.file.seek(pecoff_sig_offset, os.SEEK_SET) 290 | buf = self.file.read(4) 291 | if buf != 'PE\0\0': 292 | return None 293 | self.file.seek(pecoff_sig_offset + 20, os.SEEK_SET) 294 | buf = self.file.read(2) 295 | optional_header_size = struct.unpack(' self.filelength: 298 | # This is not strictly a failure for windows, but such files better 299 | # be treated as generic files. They can not be carrying SignedData. 300 | return None 301 | if optional_header_size < 68: 302 | # We can't do authenticode-style hashing. If this is a valid binary, 303 | # which it can be, the header still does not even contain a checksum. 304 | return None 305 | self.file.seek(optional_header_offset, os.SEEK_SET) 306 | buf = self.file.read(2) 307 | image_magic = struct.unpack(' self.filelength): 334 | # The location of the SignedData blob is just wrong (or there is none). 335 | # Ignore it -- everything else we did still makes sense. 336 | return extents 337 | extents['SignedData'] = RelRange(start, length) 338 | return extents 339 | 340 | def _CollectSignedData(self, (start, length)): 341 | """Extracts signedData blob from PECOFF binary and parses first layer.""" 342 | self.file.seek(start, os.SEEK_SET) 343 | buf = self.file.read(length) 344 | signed_data = [] 345 | # This loop ignores trailing cruft, or too-short signedData chunks. 346 | while len(buf) >= 8: 347 | dw_length, w_revision, w_cert_type = struct.unpack(' self.counter_timestamp > not_after or 490 | cs_not_before > self.counter_timestamp > cs_not_after): 491 | raise Asn1Error('Cert chain not valid at countersig time.') 492 | else: 493 | # Check if certificate chain was valid at time 'timestamp' 494 | if timestamp: 495 | if not_before > timestamp > not_after: 496 | raise Asn1Error('Cert chain not valid at time timestamp.') 497 | 498 | def _ValidateCertChain(self, signee): 499 | # Get start of 'regular' chain 500 | not_before = signee[0][0]['validity']['notBefore'].ToPythonEpochTime() 501 | not_after = signee[0][0]['validity']['notAfter'].ToPythonEpochTime() 502 | while True: 503 | issuer = signee[0][0]['issuer'] 504 | issuer_dn = str(dn.DistinguishedName.TraverseRdn(issuer[0])) 505 | signer = None 506 | for cert in self.certificates.values(): 507 | subject = cert[0][0]['subject'] 508 | subject_dn = str(dn.DistinguishedName.TraverseRdn(subject[0])) 509 | if subject_dn == issuer_dn: 510 | signer = cert 511 | # Are we at the end of the chain? 512 | if not signer: 513 | break 514 | self.ValidateCertificateSignature(signee, signer) 515 | # Did we hit a self-signed certificate? 516 | if signee == signer: 517 | break 518 | t_not_before = signer[0][0]['validity']['notBefore'].ToPythonEpochTime() 519 | t_not_after = signer[0][0]['validity']['notAfter'].ToPythonEpochTime() 520 | if t_not_before > not_before: 521 | # why would a cert be signed with something that was not valid yet 522 | # just silently absorbing this case for now 523 | not_before = t_not_before 524 | not_after = min(not_after, t_not_after) 525 | # Now let's go up a step in the cert chain. 526 | signee = signer 527 | return not_before, not_after, signee 528 | 529 | @RequiresM2Crypto 530 | def _ValidatePubkeyGeneric(self, signing_cert, digest_alg, payload, 531 | enc_digest): 532 | m2_cert = M2_X509.load_cert_der_string(der_encoder.encode(signing_cert)) 533 | pubkey = m2_cert.get_pubkey() 534 | pubkey.reset_context(digest_alg().name) 535 | pubkey.verify_init() 536 | pubkey.verify_update(payload) 537 | v = pubkey.verify_final(enc_digest) 538 | if v != 1: 539 | self.openssl_error = M2_Err.get_error() 540 | # Let's try a special case. I have no idea how I would determine when 541 | # to use this instead of the above code, so I'll always try. The 542 | # observed problem was that for one countersignature (RSA on MD5), 543 | # the encrypted digest did not contain an ASN.1 structure, but the 544 | # raw hash value instead. 545 | try: 546 | rsa = pubkey.get_rsa() 547 | except ValueError: 548 | # It's not an RSA key, just fall through... 549 | pass 550 | else: 551 | clear = rsa.public_decrypt(enc_digest, M2_RSA.pkcs1_padding) 552 | if digest_alg(payload).digest() == clear: 553 | return 1 554 | return v 555 | 556 | @RequiresM2Crypto 557 | def ValidateCertificateSignature(self, signed_cert, signing_cert): 558 | """Given a cert signed by another cert, validates the signature.""" 559 | # First the naive way -- note this does not check expiry / use etc. 560 | signed_m2 = M2_X509.load_cert_der_string(der_encoder.encode(signed_cert)) 561 | signing_m2 = M2_X509.load_cert_der_string(der_encoder.encode(signing_cert)) 562 | pubkey = signing_m2.get_pubkey() 563 | v = signed_m2.verify(pubkey) 564 | if v != 1: 565 | self.openssl_error = M2_Err.get_error() 566 | raise Asn1Error('1: Validation of cert signature failed.') 567 | 568 | def ValidateSignatures(self): 569 | """Validate encrypted hashes with respective public keys. 570 | 571 | Invokes necessary public key operations to check that signatures 572 | on authAttr hashes are correct for both the basic signature, and 573 | if present the countersignature. 574 | 575 | Raises: 576 | Asn1Error: if signature validation fails. 577 | """ 578 | # Encrypted digest is that of auth_attrs, see comments in ValidateHashes. 579 | signing_cert = self.certificates[self.signing_cert_id] 580 | v = self._ValidatePubkeyGeneric(signing_cert, self.digest_algorithm, 581 | self.computed_auth_attrs_for_hash, 582 | self.encrypted_digest) 583 | if v != 1: 584 | raise Asn1Error('1: Validation of basic signature failed.') 585 | 586 | if self.has_countersignature: 587 | signing_cert = self.certificates[self.counter_sig_cert_id] 588 | v = self._ValidatePubkeyGeneric(signing_cert, self.digest_algorithm, 589 | self.computed_counter_attrs_for_hash, 590 | self.encrypted_counter_digest) 591 | if v != 1: 592 | raise Asn1Error('2: Validation of counterSignature failed.') 593 | --------------------------------------------------------------------------------