Home | History | Annotate | Download | only in releasetools
      1 #!/usr/bin/env python
      2 #
      3 # Copyright (C) 2016 The Android Open Source Project
      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 """
     18 Verify a given OTA package with the specifed certificate.
     19 """
     20 
     21 from __future__ import print_function
     22 
     23 import argparse
     24 import logging
     25 import re
     26 import subprocess
     27 import sys
     28 import zipfile
     29 from hashlib import sha1
     30 from hashlib import sha256
     31 
     32 import common
     33 
     34 logger = logging.getLogger(__name__)
     35 
     36 
     37 def CertUsesSha256(cert):
     38   """Check if the cert uses SHA-256 hashing algorithm."""
     39 
     40   cmd = ['openssl', 'x509', '-text', '-noout', '-in', cert]
     41   p1 = common.Run(cmd, stdout=subprocess.PIPE)
     42   cert_dump, _ = p1.communicate()
     43 
     44   algorithm = re.search(r'Signature Algorithm: ([a-zA-Z0-9]+)', cert_dump)
     45   assert algorithm, "Failed to identify the signature algorithm."
     46 
     47   assert not algorithm.group(1).startswith('ecdsa'), (
     48       'This script doesn\'t support verifying ECDSA signed package yet.')
     49 
     50   return algorithm.group(1).startswith('sha256')
     51 
     52 
     53 def VerifyPackage(cert, package):
     54   """Verify the given package with the certificate.
     55 
     56   (Comments from bootable/recovery/verifier.cpp:)
     57 
     58   An archive with a whole-file signature will end in six bytes:
     59 
     60     (2-byte signature start) $ff $ff (2-byte comment size)
     61 
     62   (As far as the ZIP format is concerned, these are part of the
     63   archive comment.) We start by reading this footer, this tells
     64   us how far back from the end we have to start reading to find
     65   the whole comment.
     66   """
     67 
     68   print('Package: %s' % (package,))
     69   print('Certificate: %s' % (cert,))
     70 
     71   # Read in the package.
     72   with open(package) as package_file:
     73     package_bytes = package_file.read()
     74 
     75   length = len(package_bytes)
     76   assert length >= 6, "Not big enough to contain footer."
     77 
     78   footer = [ord(x) for x in package_bytes[-6:]]
     79   assert footer[2] == 0xff and footer[3] == 0xff, "Footer is wrong."
     80 
     81   signature_start_from_end = (footer[1] << 8) + footer[0]
     82   assert signature_start_from_end > 6, "Signature start is in the footer."
     83 
     84   signature_start = length - signature_start_from_end
     85 
     86   # Determine how much of the file is covered by the signature. This is
     87   # everything except the signature data and length, which includes all of the
     88   # EOCD except for the comment length field (2 bytes) and the comment data.
     89   comment_len = (footer[5] << 8) + footer[4]
     90   signed_len = length - comment_len - 2
     91 
     92   print('Package length: %d' % (length,))
     93   print('Comment length: %d' % (comment_len,))
     94   print('Signed data length: %d' % (signed_len,))
     95   print('Signature start: %d' % (signature_start,))
     96 
     97   use_sha256 = CertUsesSha256(cert)
     98   print('Use SHA-256: %s' % (use_sha256,))
     99 
    100   h = sha256() if use_sha256 else sha1()
    101   h.update(package_bytes[:signed_len])
    102   package_digest = h.hexdigest().lower()
    103 
    104   print('Digest: %s' % (package_digest,))
    105 
    106   # Get the signature from the input package.
    107   signature = package_bytes[signature_start:-6]
    108   sig_file = common.MakeTempFile(prefix='sig-')
    109   with open(sig_file, 'wb') as f:
    110     f.write(signature)
    111 
    112   # Parse the signature and get the hash.
    113   cmd = ['openssl', 'asn1parse', '-inform', 'DER', '-in', sig_file]
    114   p1 = common.Run(cmd, stdout=subprocess.PIPE)
    115   sig, _ = p1.communicate()
    116   assert p1.returncode == 0, "Failed to parse the signature."
    117 
    118   digest_line = sig.strip().split('\n')[-1]
    119   digest_string = digest_line.split(':')[3]
    120   digest_file = common.MakeTempFile(prefix='digest-')
    121   with open(digest_file, 'wb') as f:
    122     f.write(digest_string.decode('hex'))
    123 
    124   # Verify the digest by outputing the decrypted result in ASN.1 structure.
    125   decrypted_file = common.MakeTempFile(prefix='decrypted-')
    126   cmd = ['openssl', 'rsautl', '-verify', '-certin', '-inkey', cert,
    127          '-in', digest_file, '-out', decrypted_file]
    128   p1 = common.Run(cmd, stdout=subprocess.PIPE)
    129   p1.communicate()
    130   assert p1.returncode == 0, "Failed to run openssl rsautl -verify."
    131 
    132   # Parse the output ASN.1 structure.
    133   cmd = ['openssl', 'asn1parse', '-inform', 'DER', '-in', decrypted_file]
    134   p1 = common.Run(cmd, stdout=subprocess.PIPE)
    135   decrypted_output, _ = p1.communicate()
    136   assert p1.returncode == 0, "Failed to parse the output."
    137 
    138   digest_line = decrypted_output.strip().split('\n')[-1]
    139   digest_string = digest_line.split(':')[3].lower()
    140 
    141   # Verify that the two digest strings match.
    142   assert package_digest == digest_string, "Verification failed."
    143 
    144   # Verified successfully upon reaching here.
    145   print('\nWhole package signature VERIFIED\n')
    146 
    147 
    148 def VerifyAbOtaPayload(cert, package):
    149   """Verifies the payload and metadata signatures in an A/B OTA payload."""
    150   package_zip = zipfile.ZipFile(package, 'r')
    151   if 'payload.bin' not in package_zip.namelist():
    152     common.ZipClose(package_zip)
    153     return
    154 
    155   print('Verifying A/B OTA payload signatures...')
    156 
    157   # Dump pubkey from the certificate.
    158   pubkey = common.MakeTempFile(prefix="key-", suffix=".pem")
    159   with open(pubkey, 'wb') as pubkey_fp:
    160     pubkey_fp.write(common.ExtractPublicKey(cert))
    161 
    162   package_dir = common.MakeTempDir(prefix='package-')
    163 
    164   # Signature verification with delta_generator.
    165   payload_file = package_zip.extract('payload.bin', package_dir)
    166   cmd = ['delta_generator',
    167          '--in_file=' + payload_file,
    168          '--public_key=' + pubkey]
    169   proc = common.Run(cmd)
    170   stdoutdata, _ = proc.communicate()
    171   assert proc.returncode == 0, \
    172       'Failed to verify payload with delta_generator: {}\n{}'.format(
    173           package, stdoutdata)
    174   common.ZipClose(package_zip)
    175 
    176   # Verified successfully upon reaching here.
    177   print('\nPayload signatures VERIFIED\n\n')
    178 
    179 
    180 def main():
    181   parser = argparse.ArgumentParser()
    182   parser.add_argument('certificate', help='The certificate to be used.')
    183   parser.add_argument('package', help='The OTA package to be verified.')
    184   args = parser.parse_args()
    185 
    186   common.InitLogging()
    187 
    188   VerifyPackage(args.certificate, args.package)
    189   VerifyAbOtaPayload(args.certificate, args.package)
    190 
    191 
    192 if __name__ == '__main__':
    193   try:
    194     main()
    195   except AssertionError as err:
    196     print('\n    ERROR: %s\n' % (err,))
    197     sys.exit(1)
    198   finally:
    199     common.Cleanup()
    200