check_ota_package_signature.py revision a198b1e964cf9c90c0ddbe21b58cab203d769ebd
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"""
18Verify a given OTA package with the specifed certificate.
19"""
20
21from __future__ import print_function
22
23import argparse
24import common
25import re
26import subprocess
27import sys
28import tempfile
29import zipfile
30
31from hashlib import sha1
32from hashlib import sha256
33
34# 'update_payload' package is under 'system/update_engine/scripts/', which
35# should to be included in PYTHONPATH.
36from update_payload.payload import Payload
37from update_payload.update_metadata_pb2 import Signatures
38
39
40def CertUsesSha256(cert):
41  """Check if the cert uses SHA-256 hashing algorithm."""
42
43  cmd = ['openssl', 'x509', '-text', '-noout', '-in', cert]
44  p1 = common.Run(cmd, stdout=subprocess.PIPE)
45  cert_dump, _ = p1.communicate()
46
47  algorithm = re.search(r'Signature Algorithm: ([a-zA-Z0-9]+)', cert_dump)
48  assert algorithm, "Failed to identify the signature algorithm."
49
50  assert not algorithm.group(1).startswith('ecdsa'), (
51      'This script doesn\'t support verifying ECDSA signed package yet.')
52
53  return algorithm.group(1).startswith('sha256')
54
55
56def VerifyPackage(cert, package):
57  """Verify the given package with the certificate.
58
59  (Comments from bootable/recovery/verifier.cpp:)
60
61  An archive with a whole-file signature will end in six bytes:
62
63    (2-byte signature start) $ff $ff (2-byte comment size)
64
65  (As far as the ZIP format is concerned, these are part of the
66  archive comment.) We start by reading this footer, this tells
67  us how far back from the end we have to start reading to find
68  the whole comment.
69  """
70
71  print('Package: %s' % (package,))
72  print('Certificate: %s' % (cert,))
73
74  # Read in the package.
75  with open(package) as package_file:
76    package_bytes = package_file.read()
77
78  length = len(package_bytes)
79  assert length >= 6, "Not big enough to contain footer."
80
81  footer = [ord(x) for x in package_bytes[-6:]]
82  assert footer[2] == 0xff and footer[3] == 0xff, "Footer is wrong."
83
84  signature_start_from_end = (footer[1] << 8) + footer[0]
85  assert signature_start_from_end > 6, "Signature start is in the footer."
86
87  signature_start = length - signature_start_from_end
88
89  # Determine how much of the file is covered by the signature. This is
90  # everything except the signature data and length, which includes all of the
91  # EOCD except for the comment length field (2 bytes) and the comment data.
92  comment_len = (footer[5] << 8) + footer[4]
93  signed_len = length - comment_len - 2
94
95  print('Package length: %d' % (length,))
96  print('Comment length: %d' % (comment_len,))
97  print('Signed data length: %d' % (signed_len,))
98  print('Signature start: %d' % (signature_start,))
99
100  use_sha256 = CertUsesSha256(cert)
101  print('Use SHA-256: %s' % (use_sha256,))
102
103  if use_sha256:
104    h = sha256()
105  else:
106    h = sha1()
107  h.update(package_bytes[:signed_len])
108  package_digest = h.hexdigest().lower()
109
110  print('Digest: %s' % (package_digest,))
111
112  # Get the signature from the input package.
113  signature = package_bytes[signature_start:-6]
114  sig_file = common.MakeTempFile(prefix='sig-')
115  with open(sig_file, 'wb') as f:
116    f.write(signature)
117
118  # Parse the signature and get the hash.
119  cmd = ['openssl', 'asn1parse', '-inform', 'DER', '-in', sig_file]
120  p1 = common.Run(cmd, stdout=subprocess.PIPE)
121  sig, _ = p1.communicate()
122  assert p1.returncode == 0, "Failed to parse the signature."
123
124  digest_line = sig.strip().split('\n')[-1]
125  digest_string = digest_line.split(':')[3]
126  digest_file = common.MakeTempFile(prefix='digest-')
127  with open(digest_file, 'wb') as f:
128    f.write(digest_string.decode('hex'))
129
130  # Verify the digest by outputing the decrypted result in ASN.1 structure.
131  decrypted_file = common.MakeTempFile(prefix='decrypted-')
132  cmd = ['openssl', 'rsautl', '-verify', '-certin', '-inkey', cert,
133         '-in', digest_file, '-out', decrypted_file]
134  p1 = common.Run(cmd, stdout=subprocess.PIPE)
135  p1.communicate()
136  assert p1.returncode == 0, "Failed to run openssl rsautl -verify."
137
138  # Parse the output ASN.1 structure.
139  cmd = ['openssl', 'asn1parse', '-inform', 'DER', '-in', decrypted_file]
140  p1 = common.Run(cmd, stdout=subprocess.PIPE)
141  decrypted_output, _ = p1.communicate()
142  assert p1.returncode == 0, "Failed to parse the output."
143
144  digest_line = decrypted_output.strip().split('\n')[-1]
145  digest_string = digest_line.split(':')[3].lower()
146
147  # Verify that the two digest strings match.
148  assert package_digest == digest_string, "Verification failed."
149
150  # Verified successfully upon reaching here.
151  print('\nWhole package signature VERIFIED\n')
152
153
154def VerifyAbOtaPayload(cert, package):
155  """Verifies the payload and metadata signatures in an A/B OTA payload."""
156
157  def VerifySignatureBlob(hash_file, blob):
158    """Verifies the input hash_file against the signature blob."""
159    signatures = Signatures()
160    signatures.ParseFromString(blob)
161
162    extracted_sig_file = common.MakeTempFile(
163        prefix='extracted-sig-', suffix='.bin')
164    # In Android, we only expect one signature.
165    assert len(signatures.signatures) == 1, \
166        'Invalid number of signatures: %d' % len(signatures.signatures)
167    signature = signatures.signatures[0]
168    length = len(signature.data)
169    assert length == 256, 'Invalid signature length %d' % (length,)
170    with open(extracted_sig_file, 'w') as f:
171      f.write(signature.data)
172
173    # Verify the signature file extracted from the payload, by reversing the
174    # signing operation. Alternatively, this can be done by calling 'openssl
175    # rsautl -verify -certin -inkey <cert.pem> -in <extracted_sig_file> -out
176    # <output>', then to assert that
177    # <output> == SHA-256 DigestInfo prefix || <hash_file>.
178    cmd = ['openssl', 'pkeyutl', '-verify', '-certin', '-inkey', cert,
179           '-pkeyopt', 'digest:sha256', '-in', hash_file,
180           '-sigfile', extracted_sig_file]
181    p = common.Run(cmd, stdout=subprocess.PIPE)
182    result, _ = p.communicate()
183
184    # https://github.com/openssl/openssl/pull/3213
185    # 'openssl pkeyutl -verify' (prior to 1.1.0) returns non-zero return code,
186    # even on successful verification. To avoid the false alarm with older
187    # openssl, check the output directly.
188    assert result.strip() == 'Signature Verified Successfully', result.strip()
189
190  package_zip = zipfile.ZipFile(package, 'r')
191  if 'payload.bin' not in package_zip.namelist():
192    common.ZipClose(package_zip)
193    return
194
195  print('Verifying A/B OTA payload signatures...')
196
197  package_dir = tempfile.mkdtemp(prefix='package-')
198  common.OPTIONS.tempfiles.append(package_dir)
199
200  payload_file = package_zip.extract('payload.bin', package_dir)
201  payload = Payload(open(payload_file, 'rb'))
202  payload.Init()
203
204  # Extract the payload hash and metadata hash from the payload.bin.
205  payload_hash_file = common.MakeTempFile(prefix='hash-', suffix='.bin')
206  metadata_hash_file = common.MakeTempFile(prefix='hash-', suffix='.bin')
207  cmd = ['brillo_update_payload', 'hash',
208         '--unsigned_payload', payload_file,
209         '--signature_size', '256',
210         '--metadata_hash_file', metadata_hash_file,
211         '--payload_hash_file', payload_hash_file]
212  p = common.Run(cmd, stdout=subprocess.PIPE)
213  p.communicate()
214  assert p.returncode == 0, 'brillo_update_payload hash failed'
215
216  # Payload signature verification.
217  assert payload.manifest.HasField('signatures_offset')
218  payload_signature = payload.ReadDataBlob(
219      payload.manifest.signatures_offset, payload.manifest.signatures_size)
220  VerifySignatureBlob(payload_hash_file, payload_signature)
221
222  # Metadata signature verification.
223  metadata_signature = payload.ReadDataBlob(
224      -payload.header.metadata_signature_len,
225      payload.header.metadata_signature_len)
226  VerifySignatureBlob(metadata_hash_file, metadata_signature)
227
228  common.ZipClose(package_zip)
229
230  # Verified successfully upon reaching here.
231  print('\nPayload signatures VERIFIED\n\n')
232
233
234def main():
235  parser = argparse.ArgumentParser()
236  parser.add_argument('certificate', help='The certificate to be used.')
237  parser.add_argument('package', help='The OTA package to be verified.')
238  args = parser.parse_args()
239
240  VerifyPackage(args.certificate, args.package)
241  VerifyAbOtaPayload(args.certificate, args.package)
242
243
244if __name__ == '__main__':
245  try:
246    main()
247  except AssertionError as err:
248    print('\n    ERROR: %s\n' % (err,))
249    sys.exit(1)
250  finally:
251    common.Cleanup()
252