1*d289c2baSAndroid Build Coastguard Worker#!/usr/bin/env python 2*d289c2baSAndroid Build Coastguard Worker# 3*d289c2baSAndroid Build Coastguard Worker# Copyright 2018 The Android Open Source Project 4*d289c2baSAndroid Build Coastguard Worker# 5*d289c2baSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 6*d289c2baSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 7*d289c2baSAndroid Build Coastguard Worker# 8*d289c2baSAndroid Build Coastguard Worker# You may obtain a copy of the License at 9*d289c2baSAndroid Build Coastguard Worker# 10*d289c2baSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 11*d289c2baSAndroid Build Coastguard Worker# 12*d289c2baSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 13*d289c2baSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 14*d289c2baSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15*d289c2baSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 16*d289c2baSAndroid Build Coastguard Worker# limitations under the License. 17*d289c2baSAndroid Build Coastguard Worker"""Unit tests for at_auth_unlock.""" 18*d289c2baSAndroid Build Coastguard Worker 19*d289c2baSAndroid Build Coastguard Workerimport argparse 20*d289c2baSAndroid Build Coastguard Workerimport filecmp 21*d289c2baSAndroid Build Coastguard Workerimport os 22*d289c2baSAndroid Build Coastguard Workerimport shutil 23*d289c2baSAndroid Build Coastguard Workerimport subprocess 24*d289c2baSAndroid Build Coastguard Workerimport unittest 25*d289c2baSAndroid Build Coastguard Worker 26*d289c2baSAndroid Build Coastguard Workerfrom at_auth_unlock import * 27*d289c2baSAndroid Build Coastguard Workerfrom Crypto.PublicKey import RSA 28*d289c2baSAndroid Build Coastguard Workerfrom unittest.mock import patch 29*d289c2baSAndroid Build Coastguard Worker 30*d289c2baSAndroid Build Coastguard Worker 31*d289c2baSAndroid Build Coastguard Workerdef dataPath(file): 32*d289c2baSAndroid Build Coastguard Worker return os.path.join(os.path.dirname(__file__), 'data', file) 33*d289c2baSAndroid Build Coastguard Worker 34*d289c2baSAndroid Build Coastguard Worker 35*d289c2baSAndroid Build Coastguard WorkerDATA_FILE_PIK_CERTIFICATE = dataPath('cert_pik_certificate.bin') 36*d289c2baSAndroid Build Coastguard WorkerDATA_FILE_PUK_CERTIFICATE = dataPath('cert_puk_certificate.bin') 37*d289c2baSAndroid Build Coastguard WorkerDATA_FILE_PUK_KEY = dataPath('testkey_cert_puk.pem') 38*d289c2baSAndroid Build Coastguard WorkerDATA_FILE_UNLOCK_CHALLENGE = dataPath('cert_unlock_challenge.bin') 39*d289c2baSAndroid Build Coastguard WorkerDATA_FILE_UNLOCK_CREDENTIAL = dataPath('cert_unlock_credential.bin') 40*d289c2baSAndroid Build Coastguard Worker 41*d289c2baSAndroid Build Coastguard Worker 42*d289c2baSAndroid Build Coastguard Workerdef createTempZip(contents): 43*d289c2baSAndroid Build Coastguard Worker tempzip = tempfile.NamedTemporaryFile() 44*d289c2baSAndroid Build Coastguard Worker with zipfile.ZipFile(tempzip, 'w') as zip: 45*d289c2baSAndroid Build Coastguard Worker for arcname in contents: 46*d289c2baSAndroid Build Coastguard Worker zip.write(contents[arcname], arcname) 47*d289c2baSAndroid Build Coastguard Worker return tempzip 48*d289c2baSAndroid Build Coastguard Worker 49*d289c2baSAndroid Build Coastguard Worker 50*d289c2baSAndroid Build Coastguard Workerdef validUnlockCredsZip(): 51*d289c2baSAndroid Build Coastguard Worker return createTempZip({ 52*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 53*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v1.bin': DATA_FILE_PUK_CERTIFICATE, 54*d289c2baSAndroid Build Coastguard Worker 'puk_v1.pem': DATA_FILE_PUK_KEY 55*d289c2baSAndroid Build Coastguard Worker }) 56*d289c2baSAndroid Build Coastguard Worker 57*d289c2baSAndroid Build Coastguard Worker 58*d289c2baSAndroid Build Coastguard Workerclass UnlockCredentialsTest(unittest.TestCase): 59*d289c2baSAndroid Build Coastguard Worker 60*d289c2baSAndroid Build Coastguard Worker def testFromValidZipArchive(self): 61*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as zip: 62*d289c2baSAndroid Build Coastguard Worker creds = UnlockCredentials.from_credential_archive(zip) 63*d289c2baSAndroid Build Coastguard Worker self.assertIsNotNone(creds.intermediate_cert) 64*d289c2baSAndroid Build Coastguard Worker self.assertIsNotNone(creds.unlock_cert) 65*d289c2baSAndroid Build Coastguard Worker self.assertIsNotNone(creds.unlock_key) 66*d289c2baSAndroid Build Coastguard Worker 67*d289c2baSAndroid Build Coastguard Worker def testFromInvalidZipArchive(self): 68*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(zipfile.BadZipfile): 69*d289c2baSAndroid Build Coastguard Worker UnlockCredentials.from_credential_archive(DATA_FILE_PUK_KEY) 70*d289c2baSAndroid Build Coastguard Worker 71*d289c2baSAndroid Build Coastguard Worker def testFromArchiveMissingPikCertificate(self): 72*d289c2baSAndroid Build Coastguard Worker with createTempZip({ 73*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v1.bin': DATA_FILE_PUK_CERTIFICATE, 74*d289c2baSAndroid Build Coastguard Worker 'puk_v1.pem': DATA_FILE_PUK_KEY 75*d289c2baSAndroid Build Coastguard Worker }) as zip: 76*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 77*d289c2baSAndroid Build Coastguard Worker UnlockCredentials.from_credential_archive(zip) 78*d289c2baSAndroid Build Coastguard Worker 79*d289c2baSAndroid Build Coastguard Worker def testFromArchiveMissingPukCertificate(self): 80*d289c2baSAndroid Build Coastguard Worker with createTempZip({ 81*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 82*d289c2baSAndroid Build Coastguard Worker 'puk_v1.pem': DATA_FILE_PUK_KEY 83*d289c2baSAndroid Build Coastguard Worker }) as zip: 84*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 85*d289c2baSAndroid Build Coastguard Worker UnlockCredentials.from_credential_archive(zip) 86*d289c2baSAndroid Build Coastguard Worker 87*d289c2baSAndroid Build Coastguard Worker def testFromArchiveMissingPuk(self): 88*d289c2baSAndroid Build Coastguard Worker with createTempZip({ 89*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 90*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v1.bin': DATA_FILE_PUK_CERTIFICATE, 91*d289c2baSAndroid Build Coastguard Worker }) as zip: 92*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 93*d289c2baSAndroid Build Coastguard Worker UnlockCredentials.from_credential_archive(zip) 94*d289c2baSAndroid Build Coastguard Worker 95*d289c2baSAndroid Build Coastguard Worker def testFromArchiveMultiplePikCertificates(self): 96*d289c2baSAndroid Build Coastguard Worker with createTempZip({ 97*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 98*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v2.bin': DATA_FILE_PIK_CERTIFICATE, 99*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v1.bin': DATA_FILE_PUK_CERTIFICATE, 100*d289c2baSAndroid Build Coastguard Worker 'puk_v1.pem': DATA_FILE_PUK_KEY 101*d289c2baSAndroid Build Coastguard Worker }) as zip: 102*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 103*d289c2baSAndroid Build Coastguard Worker UnlockCredentials.from_credential_archive(zip) 104*d289c2baSAndroid Build Coastguard Worker 105*d289c2baSAndroid Build Coastguard Worker def testFromArchiveMultiplePukCertificates(self): 106*d289c2baSAndroid Build Coastguard Worker with createTempZip({ 107*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 108*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v1.bin': DATA_FILE_PUK_CERTIFICATE, 109*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v2.bin': DATA_FILE_PUK_CERTIFICATE, 110*d289c2baSAndroid Build Coastguard Worker 'puk_v1.pem': DATA_FILE_PUK_KEY 111*d289c2baSAndroid Build Coastguard Worker }) as zip: 112*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 113*d289c2baSAndroid Build Coastguard Worker UnlockCredentials.from_credential_archive(zip) 114*d289c2baSAndroid Build Coastguard Worker 115*d289c2baSAndroid Build Coastguard Worker def testFromArchiveMultiplePuks(self): 116*d289c2baSAndroid Build Coastguard Worker with createTempZip({ 117*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 118*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v1.bin': DATA_FILE_PUK_CERTIFICATE, 119*d289c2baSAndroid Build Coastguard Worker 'puk_v1.pem': DATA_FILE_PUK_KEY, 120*d289c2baSAndroid Build Coastguard Worker 'puk_v2.pem': DATA_FILE_PUK_KEY 121*d289c2baSAndroid Build Coastguard Worker }) as zip: 122*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 123*d289c2baSAndroid Build Coastguard Worker UnlockCredentials.from_credential_archive(zip) 124*d289c2baSAndroid Build Coastguard Worker 125*d289c2baSAndroid Build Coastguard Worker def testFromFiles(self): 126*d289c2baSAndroid Build Coastguard Worker creds = UnlockCredentials( 127*d289c2baSAndroid Build Coastguard Worker intermediate_cert_file=DATA_FILE_PIK_CERTIFICATE, 128*d289c2baSAndroid Build Coastguard Worker unlock_cert_file=DATA_FILE_PUK_CERTIFICATE, 129*d289c2baSAndroid Build Coastguard Worker unlock_key_file=DATA_FILE_PUK_KEY) 130*d289c2baSAndroid Build Coastguard Worker self.assertIsNotNone(creds.intermediate_cert) 131*d289c2baSAndroid Build Coastguard Worker self.assertIsNotNone(creds.unlock_cert) 132*d289c2baSAndroid Build Coastguard Worker self.assertIsNotNone(creds.unlock_key) 133*d289c2baSAndroid Build Coastguard Worker 134*d289c2baSAndroid Build Coastguard Worker def testInvalidPuk(self): 135*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 136*d289c2baSAndroid Build Coastguard Worker UnlockCredentials( 137*d289c2baSAndroid Build Coastguard Worker intermediate_cert_file=DATA_FILE_PIK_CERTIFICATE, 138*d289c2baSAndroid Build Coastguard Worker unlock_cert_file=DATA_FILE_PUK_CERTIFICATE, 139*d289c2baSAndroid Build Coastguard Worker unlock_key_file=DATA_FILE_PUK_CERTIFICATE) 140*d289c2baSAndroid Build Coastguard Worker 141*d289c2baSAndroid Build Coastguard Worker def testPukNotPrivateKey(self): 142*d289c2baSAndroid Build Coastguard Worker tempdir = tempfile.mkdtemp() 143*d289c2baSAndroid Build Coastguard Worker try: 144*d289c2baSAndroid Build Coastguard Worker with open(DATA_FILE_PUK_KEY, 'rb') as f: 145*d289c2baSAndroid Build Coastguard Worker key = RSA.importKey(f.read()) 146*d289c2baSAndroid Build Coastguard Worker pubkey = os.path.join(tempdir, 'pubkey.pub') 147*d289c2baSAndroid Build Coastguard Worker with open(pubkey, 'wb') as f: 148*d289c2baSAndroid Build Coastguard Worker f.write(key.publickey().exportKey()) 149*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 150*d289c2baSAndroid Build Coastguard Worker UnlockCredentials( 151*d289c2baSAndroid Build Coastguard Worker intermediate_cert_file=DATA_FILE_PIK_CERTIFICATE, 152*d289c2baSAndroid Build Coastguard Worker unlock_cert_file=DATA_FILE_PUK_CERTIFICATE, 153*d289c2baSAndroid Build Coastguard Worker unlock_key_file=pubkey) 154*d289c2baSAndroid Build Coastguard Worker finally: 155*d289c2baSAndroid Build Coastguard Worker shutil.rmtree(tempdir) 156*d289c2baSAndroid Build Coastguard Worker 157*d289c2baSAndroid Build Coastguard Worker def testWrongSizeCerts(self): 158*d289c2baSAndroid Build Coastguard Worker pik_cert = DATA_FILE_PIK_CERTIFICATE 159*d289c2baSAndroid Build Coastguard Worker tempdir = tempfile.mkdtemp() 160*d289c2baSAndroid Build Coastguard Worker try: 161*d289c2baSAndroid Build Coastguard Worker # Copy a valid cert and truncate a single byte from the end to create a 162*d289c2baSAndroid Build Coastguard Worker # too-short cert. 163*d289c2baSAndroid Build Coastguard Worker shortfile = os.path.join(tempdir, 'shortfile.bin') 164*d289c2baSAndroid Build Coastguard Worker shutil.copy2(pik_cert, shortfile) 165*d289c2baSAndroid Build Coastguard Worker with open(shortfile, 'ab') as f: 166*d289c2baSAndroid Build Coastguard Worker f.seek(-1, os.SEEK_END) 167*d289c2baSAndroid Build Coastguard Worker f.truncate() 168*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 169*d289c2baSAndroid Build Coastguard Worker creds = UnlockCredentials( 170*d289c2baSAndroid Build Coastguard Worker intermediate_cert_file=shortfile, 171*d289c2baSAndroid Build Coastguard Worker unlock_cert_file=DATA_FILE_PUK_CERTIFICATE, 172*d289c2baSAndroid Build Coastguard Worker unlock_key_file=DATA_FILE_PUK_KEY) 173*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 174*d289c2baSAndroid Build Coastguard Worker creds = UnlockCredentials( 175*d289c2baSAndroid Build Coastguard Worker intermediate_cert_file=DATA_FILE_PIK_CERTIFICATE, 176*d289c2baSAndroid Build Coastguard Worker unlock_cert_file=shortfile, 177*d289c2baSAndroid Build Coastguard Worker unlock_key_file=DATA_FILE_PUK_KEY) 178*d289c2baSAndroid Build Coastguard Worker 179*d289c2baSAndroid Build Coastguard Worker # Copy a valid cert and append an arbitrary byte on the end to create a 180*d289c2baSAndroid Build Coastguard Worker # too-long cert. 181*d289c2baSAndroid Build Coastguard Worker longfile = os.path.join(tempdir, 'longfile.bin') 182*d289c2baSAndroid Build Coastguard Worker shutil.copy2(pik_cert, longfile) 183*d289c2baSAndroid Build Coastguard Worker with open(longfile, 'ab') as f: 184*d289c2baSAndroid Build Coastguard Worker f.write(b'\0') 185*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 186*d289c2baSAndroid Build Coastguard Worker creds = UnlockCredentials( 187*d289c2baSAndroid Build Coastguard Worker intermediate_cert_file=longfile, 188*d289c2baSAndroid Build Coastguard Worker unlock_cert_file=DATA_FILE_PUK_CERTIFICATE, 189*d289c2baSAndroid Build Coastguard Worker unlock_key_file=DATA_FILE_PUK_KEY) 190*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 191*d289c2baSAndroid Build Coastguard Worker creds = UnlockCredentials( 192*d289c2baSAndroid Build Coastguard Worker intermediate_cert_file=DATA_FILE_PIK_CERTIFICATE, 193*d289c2baSAndroid Build Coastguard Worker unlock_cert_file=longfile, 194*d289c2baSAndroid Build Coastguard Worker unlock_key_file=DATA_FILE_PUK_KEY) 195*d289c2baSAndroid Build Coastguard Worker finally: 196*d289c2baSAndroid Build Coastguard Worker shutil.rmtree(tempdir) 197*d289c2baSAndroid Build Coastguard Worker 198*d289c2baSAndroid Build Coastguard Worker 199*d289c2baSAndroid Build Coastguard Workerdef writeFullUnlockChallenge(out_file, product_id_hash=None): 200*d289c2baSAndroid Build Coastguard Worker """Helper function to create a file with a full AvbCertUnlockChallenge struct. 201*d289c2baSAndroid Build Coastguard Worker 202*d289c2baSAndroid Build Coastguard Worker Arguments: 203*d289c2baSAndroid Build Coastguard Worker product_id_hash: [optional] 32 byte value to include in the challenge as the 204*d289c2baSAndroid Build Coastguard Worker SHA256 hash of the product ID. If not provided, will default to the 205*d289c2baSAndroid Build Coastguard Worker product ID hash from the subject of DATA_FILE_PUK_CERTIFICATE. 206*d289c2baSAndroid Build Coastguard Worker """ 207*d289c2baSAndroid Build Coastguard Worker if product_id_hash is None: 208*d289c2baSAndroid Build Coastguard Worker with open(DATA_FILE_PUK_CERTIFICATE, 'rb') as f: 209*d289c2baSAndroid Build Coastguard Worker product_id_hash = GetCertCertificateSubject(f.read()) 210*d289c2baSAndroid Build Coastguard Worker assert len(product_id_hash) == 32 211*d289c2baSAndroid Build Coastguard Worker 212*d289c2baSAndroid Build Coastguard Worker with open(out_file, 'wb') as out: 213*d289c2baSAndroid Build Coastguard Worker out.write(struct.pack('<I', 1)) 214*d289c2baSAndroid Build Coastguard Worker out.write(product_id_hash) 215*d289c2baSAndroid Build Coastguard Worker with open(DATA_FILE_UNLOCK_CHALLENGE, 'rb') as f: 216*d289c2baSAndroid Build Coastguard Worker out.write(f.read()) 217*d289c2baSAndroid Build Coastguard Worker 218*d289c2baSAndroid Build Coastguard Worker 219*d289c2baSAndroid Build Coastguard Workerclass MakeCertUnlockCredentialTest(unittest.TestCase): 220*d289c2baSAndroid Build Coastguard Worker 221*d289c2baSAndroid Build Coastguard Worker def testCredentialIsCorrect(self): 222*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as zip: 223*d289c2baSAndroid Build Coastguard Worker creds = UnlockCredentials.from_credential_archive(zip) 224*d289c2baSAndroid Build Coastguard Worker 225*d289c2baSAndroid Build Coastguard Worker tempdir = tempfile.mkdtemp() 226*d289c2baSAndroid Build Coastguard Worker try: 227*d289c2baSAndroid Build Coastguard Worker challenge_file = os.path.join(tempdir, 'challenge') 228*d289c2baSAndroid Build Coastguard Worker writeFullUnlockChallenge(challenge_file) 229*d289c2baSAndroid Build Coastguard Worker challenge = UnlockChallenge(challenge_file) 230*d289c2baSAndroid Build Coastguard Worker out_cred = os.path.join(tempdir, 'credential') 231*d289c2baSAndroid Build Coastguard Worker 232*d289c2baSAndroid Build Coastguard Worker # Compare unlock credential generated by function with one generated 233*d289c2baSAndroid Build Coastguard Worker # using 'avbtool make_cert_unlock_credential', to check correctness. 234*d289c2baSAndroid Build Coastguard Worker MakeCertUnlockCredential(creds, challenge, out_cred) 235*d289c2baSAndroid Build Coastguard Worker self.assertTrue(filecmp.cmp(out_cred, DATA_FILE_UNLOCK_CREDENTIAL)) 236*d289c2baSAndroid Build Coastguard Worker finally: 237*d289c2baSAndroid Build Coastguard Worker shutil.rmtree(tempdir) 238*d289c2baSAndroid Build Coastguard Worker 239*d289c2baSAndroid Build Coastguard Worker def testWrongChallengeSize(self): 240*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as zip: 241*d289c2baSAndroid Build Coastguard Worker creds = UnlockCredentials.from_credential_archive(zip) 242*d289c2baSAndroid Build Coastguard Worker 243*d289c2baSAndroid Build Coastguard Worker tempdir = tempfile.mkdtemp() 244*d289c2baSAndroid Build Coastguard Worker try: 245*d289c2baSAndroid Build Coastguard Worker out_cred = os.path.join(tempdir, 'credential') 246*d289c2baSAndroid Build Coastguard Worker 247*d289c2baSAndroid Build Coastguard Worker # The bundled unlock challenge is just the 16 byte challenge, not the 248*d289c2baSAndroid Build Coastguard Worker # full AvbCertUnlockChallenge like this expects. 249*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 250*d289c2baSAndroid Build Coastguard Worker challenge = UnlockChallenge(DATA_FILE_UNLOCK_CHALLENGE) 251*d289c2baSAndroid Build Coastguard Worker MakeCertUnlockCredential(creds, challenge, out_cred) 252*d289c2baSAndroid Build Coastguard Worker finally: 253*d289c2baSAndroid Build Coastguard Worker shutil.rmtree(tempdir) 254*d289c2baSAndroid Build Coastguard Worker 255*d289c2baSAndroid Build Coastguard Worker 256*d289c2baSAndroid Build Coastguard Workerdef makeFastbootCommandFake(testcase, 257*d289c2baSAndroid Build Coastguard Worker expect_serial=None, 258*d289c2baSAndroid Build Coastguard Worker error_on_command_number=None, 259*d289c2baSAndroid Build Coastguard Worker product_id_hash=None, 260*d289c2baSAndroid Build Coastguard Worker stay_locked=False): 261*d289c2baSAndroid Build Coastguard Worker """Construct a fake fastboot command handler, to be used with unitttest.mock.Mock.side_effect. 262*d289c2baSAndroid Build Coastguard Worker 263*d289c2baSAndroid Build Coastguard Worker This can be used to create a callable that acts as a fake for a real device 264*d289c2baSAndroid Build Coastguard Worker responding to the fastboot commands involved in an authenticated unlock. The 265*d289c2baSAndroid Build Coastguard Worker returned callback is intended to be used with unittest.mock.Mock.side_effect. 266*d289c2baSAndroid Build Coastguard Worker There are a number of optional arguments here that can be used to customize 267*d289c2baSAndroid Build Coastguard Worker the behavior of the fake for a specific test. 268*d289c2baSAndroid Build Coastguard Worker 269*d289c2baSAndroid Build Coastguard Worker Arguments: 270*d289c2baSAndroid Build Coastguard Worker testcase: unittest.TestCase object for the associated test 271*d289c2baSAndroid Build Coastguard Worker expect_serial: [optional] Expect (and assert) that the fastboot command 272*d289c2baSAndroid Build Coastguard Worker specifies a specific device serial to communicate with. 273*d289c2baSAndroid Build Coastguard Worker error_on_command_number: [optional] Return a fastboot error (non-zero exit 274*d289c2baSAndroid Build Coastguard Worker code) on the nth (0-based) command handled. 275*d289c2baSAndroid Build Coastguard Worker stay_locked: [optional] Make the fake report that the device is still locked 276*d289c2baSAndroid Build Coastguard Worker after an otherwise successful unlock attempt. 277*d289c2baSAndroid Build Coastguard Worker """ 278*d289c2baSAndroid Build Coastguard Worker 279*d289c2baSAndroid Build Coastguard Worker def handler(args, *extraArgs, **kwargs): 280*d289c2baSAndroid Build Coastguard Worker if error_on_command_number is not None: 281*d289c2baSAndroid Build Coastguard Worker handler.command_counter += 1 282*d289c2baSAndroid Build Coastguard Worker if handler.command_counter - 1 == error_on_command_number: 283*d289c2baSAndroid Build Coastguard Worker raise subprocess.CalledProcessError( 284*d289c2baSAndroid Build Coastguard Worker returncode=1, cmd=args, output=b'Fake: ERROR') 285*d289c2baSAndroid Build Coastguard Worker 286*d289c2baSAndroid Build Coastguard Worker testcase.assertEqual(args.pop(0), 'fastboot') 287*d289c2baSAndroid Build Coastguard Worker if expect_serial is not None: 288*d289c2baSAndroid Build Coastguard Worker # This is a bit fragile in that, in reality, fastboot allows '-s SERIAL' 289*d289c2baSAndroid Build Coastguard Worker # to not just be the first arguments, but it works for this use case. 290*d289c2baSAndroid Build Coastguard Worker testcase.assertEqual(args.pop(0), '-s') 291*d289c2baSAndroid Build Coastguard Worker testcase.assertEqual(args.pop(0), expect_serial) 292*d289c2baSAndroid Build Coastguard Worker 293*d289c2baSAndroid Build Coastguard Worker if args[0:2] == ['oem', 'at-get-vboot-unlock-challenge']: 294*d289c2baSAndroid Build Coastguard Worker handler.challenge_staged = True 295*d289c2baSAndroid Build Coastguard Worker elif args[0] == 'get_staged': 296*d289c2baSAndroid Build Coastguard Worker if not handler.challenge_staged: 297*d289c2baSAndroid Build Coastguard Worker raise subprocess.CalledProcessError( 298*d289c2baSAndroid Build Coastguard Worker returncode=1, cmd=args, output=b'Fake: No data staged') 299*d289c2baSAndroid Build Coastguard Worker 300*d289c2baSAndroid Build Coastguard Worker writeFullUnlockChallenge(args[1], product_id_hash=product_id_hash) 301*d289c2baSAndroid Build Coastguard Worker handler.challenge_staged = False 302*d289c2baSAndroid Build Coastguard Worker elif args[0] == 'stage': 303*d289c2baSAndroid Build Coastguard Worker handler.staged_file = args[1] 304*d289c2baSAndroid Build Coastguard Worker elif args[0:2] == ['oem', 'at-unlock-vboot']: 305*d289c2baSAndroid Build Coastguard Worker if handler.staged_file is None: 306*d289c2baSAndroid Build Coastguard Worker raise subprocess.CalledProcessError( 307*d289c2baSAndroid Build Coastguard Worker returncode=1, cmd=args, output=b'Fake: No unlock credential staged') 308*d289c2baSAndroid Build Coastguard Worker 309*d289c2baSAndroid Build Coastguard Worker # Validate the unlock credential as if this were a test key locked device, 310*d289c2baSAndroid Build Coastguard Worker # which implies tests that want a successful unlock need to be set up to 311*d289c2baSAndroid Build Coastguard Worker # use DATA_FILE_PUK_KEY to sign the challenge. Credentials generated using 312*d289c2baSAndroid Build Coastguard Worker # other keys will be properly rejected. 313*d289c2baSAndroid Build Coastguard Worker if not filecmp.cmp(handler.staged_file, DATA_FILE_UNLOCK_CREDENTIAL): 314*d289c2baSAndroid Build Coastguard Worker raise subprocess.CalledProcessError( 315*d289c2baSAndroid Build Coastguard Worker returncode=1, cmd=args, output=b'Fake: Incorrect unlock credential') 316*d289c2baSAndroid Build Coastguard Worker 317*d289c2baSAndroid Build Coastguard Worker handler.locked = True if stay_locked else False 318*d289c2baSAndroid Build Coastguard Worker elif args[0:2] == ['getvar', 'at-vboot-state']: 319*d289c2baSAndroid Build Coastguard Worker return b'avb-locked: ' + (b'1' if handler.locked else b'0') 320*d289c2baSAndroid Build Coastguard Worker return b'Fake: OK' 321*d289c2baSAndroid Build Coastguard Worker 322*d289c2baSAndroid Build Coastguard Worker handler.command_counter = 0 323*d289c2baSAndroid Build Coastguard Worker handler.challenge_staged = False 324*d289c2baSAndroid Build Coastguard Worker handler.staged_file = None 325*d289c2baSAndroid Build Coastguard Worker handler.locked = True 326*d289c2baSAndroid Build Coastguard Worker return handler 327*d289c2baSAndroid Build Coastguard Worker 328*d289c2baSAndroid Build Coastguard Worker 329*d289c2baSAndroid Build Coastguard Workerclass AuthenticatedUnlockTest(unittest.TestCase): 330*d289c2baSAndroid Build Coastguard Worker 331*d289c2baSAndroid Build Coastguard Worker @patch('subprocess.check_output') 332*d289c2baSAndroid Build Coastguard Worker def testUnlockWithZipArchive(self, mock_subp_check_output): 333*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as zip: 334*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake(self) 335*d289c2baSAndroid Build Coastguard Worker self.assertEqual(main([zip.name]), 0) 336*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 337*d289c2baSAndroid Build Coastguard Worker 338*d289c2baSAndroid Build Coastguard Worker @patch('subprocess.check_output') 339*d289c2baSAndroid Build Coastguard Worker def testUnlockDeviceBySerial(self, mock_subp_check_output): 340*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as zip: 341*d289c2baSAndroid Build Coastguard Worker SERIAL = 'abcde12345' 342*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake( 343*d289c2baSAndroid Build Coastguard Worker self, expect_serial=SERIAL) 344*d289c2baSAndroid Build Coastguard Worker self.assertEqual(main([zip.name, '-s', SERIAL]), 0) 345*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 346*d289c2baSAndroid Build Coastguard Worker 347*d289c2baSAndroid Build Coastguard Worker @patch('subprocess.check_output') 348*d289c2baSAndroid Build Coastguard Worker def testUnlockWithIndividualFiles(self, mock_subp_check_output): 349*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake(self) 350*d289c2baSAndroid Build Coastguard Worker self.assertEqual( 351*d289c2baSAndroid Build Coastguard Worker main([ 352*d289c2baSAndroid Build Coastguard Worker '--pik_cert', DATA_FILE_PIK_CERTIFICATE, '--puk_cert', 353*d289c2baSAndroid Build Coastguard Worker DATA_FILE_PUK_CERTIFICATE, '--puk', DATA_FILE_PUK_KEY 354*d289c2baSAndroid Build Coastguard Worker ]), 0) 355*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 356*d289c2baSAndroid Build Coastguard Worker 357*d289c2baSAndroid Build Coastguard Worker @patch('subprocess.check_output') 358*d289c2baSAndroid Build Coastguard Worker def testFastbootError(self, mock_subp_check_output): 359*d289c2baSAndroid Build Coastguard Worker """Verify that errors are handled properly if fastboot commands error out.""" 360*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as zip: 361*d289c2baSAndroid Build Coastguard Worker for n in range(5): 362*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.reset_mock() 363*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake( 364*d289c2baSAndroid Build Coastguard Worker self, error_on_command_number=n) 365*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(main([zip.name]), 0) 366*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 367*d289c2baSAndroid Build Coastguard Worker 368*d289c2baSAndroid Build Coastguard Worker @patch('subprocess.check_output') 369*d289c2baSAndroid Build Coastguard Worker def testDoesntActuallyUnlock(self, mock_subp_check_output): 370*d289c2baSAndroid Build Coastguard Worker """Verify fails if fake set to not actually unlock.""" 371*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as zip: 372*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake( 373*d289c2baSAndroid Build Coastguard Worker self, stay_locked=True) 374*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(main([zip.name]), 0) 375*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 376*d289c2baSAndroid Build Coastguard Worker 377*d289c2baSAndroid Build Coastguard Worker @patch('subprocess.check_output') 378*d289c2baSAndroid Build Coastguard Worker def testNoCredentialsMatchDeviceProductID(self, mock_subp_check_output): 379*d289c2baSAndroid Build Coastguard Worker """Test two cases where fake responds with a challenge that has a product ID hash which doesn't match the credentials used.""" 380*d289c2baSAndroid Build Coastguard Worker # Case 1: Change the product ID hash that the fake responds with. 381*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as zip: 382*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake( 383*d289c2baSAndroid Build Coastguard Worker self, product_id_hash=b'\x00' * 32) 384*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(main([zip.name]), 0) 385*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 386*d289c2baSAndroid Build Coastguard Worker 387*d289c2baSAndroid Build Coastguard Worker # Case 2: Use credentials with a different product ID. 388*d289c2baSAndroid Build Coastguard Worker with createTempZip({ 389*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 390*d289c2baSAndroid Build Coastguard Worker # Note: PIK cert used as PUK cert so subject (i.e. product ID hash) is 391*d289c2baSAndroid Build Coastguard Worker # different 392*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 393*d289c2baSAndroid Build Coastguard Worker 'puk_v1.pem': DATA_FILE_PUK_KEY 394*d289c2baSAndroid Build Coastguard Worker }) as zip: 395*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake(self) 396*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(main([zip.name]), 0) 397*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 398*d289c2baSAndroid Build Coastguard Worker 399*d289c2baSAndroid Build Coastguard Worker @patch('subprocess.check_output') 400*d289c2baSAndroid Build Coastguard Worker def testMatchingCredentialSelectedFromZipArchives(self, 401*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output): 402*d289c2baSAndroid Build Coastguard Worker """Test correct credential based on product ID hash used if multiple provided directly through arguments.""" 403*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as correctCreds, createTempZip({ 404*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 405*d289c2baSAndroid Build Coastguard Worker # Note: PIK cert used as PUK cert so subject (i.e. product ID hash) 406*d289c2baSAndroid Build Coastguard Worker # doesn't match 407*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 408*d289c2baSAndroid Build Coastguard Worker 'puk_v1.pem': DATA_FILE_PUK_KEY 409*d289c2baSAndroid Build Coastguard Worker }) as wrongCreds: 410*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake(self) 411*d289c2baSAndroid Build Coastguard Worker self.assertEqual(main([wrongCreds.name, correctCreds.name]), 0) 412*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 413*d289c2baSAndroid Build Coastguard Worker 414*d289c2baSAndroid Build Coastguard Worker @patch('subprocess.check_output') 415*d289c2baSAndroid Build Coastguard Worker def testMatchingCredentialSelectedFromDirectory(self, mock_subp_check_output): 416*d289c2baSAndroid Build Coastguard Worker """Test correct credential based on product ID hash used if multiple provided indirectly through a directory argument.""" 417*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as correctCreds, createTempZip({ 418*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 419*d289c2baSAndroid Build Coastguard Worker # Note: PIK cert used as PUK cert so subject (i.e. product ID hash) 420*d289c2baSAndroid Build Coastguard Worker # doesn't match 421*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 422*d289c2baSAndroid Build Coastguard Worker 'puk_v1.pem': DATA_FILE_PUK_KEY 423*d289c2baSAndroid Build Coastguard Worker }) as wrongCreds: 424*d289c2baSAndroid Build Coastguard Worker tempdir = tempfile.mkdtemp() 425*d289c2baSAndroid Build Coastguard Worker try: 426*d289c2baSAndroid Build Coastguard Worker shutil.copy2(correctCreds.name, tempdir) 427*d289c2baSAndroid Build Coastguard Worker shutil.copy2(wrongCreds.name, tempdir) 428*d289c2baSAndroid Build Coastguard Worker 429*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake(self) 430*d289c2baSAndroid Build Coastguard Worker self.assertEqual(main([tempdir]), 0) 431*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 432*d289c2baSAndroid Build Coastguard Worker finally: 433*d289c2baSAndroid Build Coastguard Worker shutil.rmtree(tempdir) 434*d289c2baSAndroid Build Coastguard Worker 435*d289c2baSAndroid Build Coastguard Worker @patch('subprocess.check_output') 436*d289c2baSAndroid Build Coastguard Worker def testMatchingCredentialSelectedFromEither(self, mock_subp_check_output): 437*d289c2baSAndroid Build Coastguard Worker """Test correct credential based on product ID hash used if arguments give some combination of file and directory arguments.""" 438*d289c2baSAndroid Build Coastguard Worker with validUnlockCredsZip() as correctCreds, createTempZip({ 439*d289c2baSAndroid Build Coastguard Worker 'pik_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 440*d289c2baSAndroid Build Coastguard Worker # Note: PIK cert used as PUK cert so subject (i.e. product ID hash) 441*d289c2baSAndroid Build Coastguard Worker # doesn't match 442*d289c2baSAndroid Build Coastguard Worker 'puk_certificate_v1.bin': DATA_FILE_PIK_CERTIFICATE, 443*d289c2baSAndroid Build Coastguard Worker 'puk_v1.pem': DATA_FILE_PUK_KEY 444*d289c2baSAndroid Build Coastguard Worker }) as wrongCreds: 445*d289c2baSAndroid Build Coastguard Worker # Case 1: Correct creds in directory, wrong in file arg 446*d289c2baSAndroid Build Coastguard Worker tempdir = tempfile.mkdtemp() 447*d289c2baSAndroid Build Coastguard Worker try: 448*d289c2baSAndroid Build Coastguard Worker shutil.copy2(correctCreds.name, tempdir) 449*d289c2baSAndroid Build Coastguard Worker 450*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake(self) 451*d289c2baSAndroid Build Coastguard Worker self.assertEqual(main([wrongCreds.name, tempdir]), 0) 452*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 453*d289c2baSAndroid Build Coastguard Worker 454*d289c2baSAndroid Build Coastguard Worker finally: 455*d289c2baSAndroid Build Coastguard Worker shutil.rmtree(tempdir) 456*d289c2baSAndroid Build Coastguard Worker 457*d289c2baSAndroid Build Coastguard Worker # Case 2: Correct creds in file arg, wrong in directory 458*d289c2baSAndroid Build Coastguard Worker tempdir = tempfile.mkdtemp() 459*d289c2baSAndroid Build Coastguard Worker try: 460*d289c2baSAndroid Build Coastguard Worker shutil.copy2(wrongCreds.name, tempdir) 461*d289c2baSAndroid Build Coastguard Worker 462*d289c2baSAndroid Build Coastguard Worker mock_subp_check_output.side_effect = makeFastbootCommandFake(self) 463*d289c2baSAndroid Build Coastguard Worker self.assertEqual(main([tempdir, correctCreds.name]), 0) 464*d289c2baSAndroid Build Coastguard Worker self.assertNotEqual(mock_subp_check_output.call_count, 0) 465*d289c2baSAndroid Build Coastguard Worker 466*d289c2baSAndroid Build Coastguard Worker # Case 2: Correct creds in file arg, wrong in directory 467*d289c2baSAndroid Build Coastguard Worker finally: 468*d289c2baSAndroid Build Coastguard Worker shutil.rmtree(tempdir) 469*d289c2baSAndroid Build Coastguard Worker 470*d289c2baSAndroid Build Coastguard Worker @patch('argparse.ArgumentParser.error') 471*d289c2baSAndroid Build Coastguard Worker def testArgparseDirectoryWithNoCredentials(self, mock_parser_error): 472*d289c2baSAndroid Build Coastguard Worker """Test """ 473*d289c2baSAndroid Build Coastguard Worker tempdir = tempfile.mkdtemp() 474*d289c2baSAndroid Build Coastguard Worker try: 475*d289c2baSAndroid Build Coastguard Worker # Make sure random files are ignored. 476*d289c2baSAndroid Build Coastguard Worker with open(os.path.join(tempdir, 'so_random'), 'w') as f: 477*d289c2baSAndroid Build Coastguard Worker f.write("I'm a random file") 478*d289c2baSAndroid Build Coastguard Worker 479*d289c2baSAndroid Build Coastguard Worker mock_parser_error.side_effect = ValueError('ArgumentParser.error') 480*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 481*d289c2baSAndroid Build Coastguard Worker main([tempdir]) 482*d289c2baSAndroid Build Coastguard Worker self.assertEqual(mock_parser_error.call_count, 1) 483*d289c2baSAndroid Build Coastguard Worker finally: 484*d289c2baSAndroid Build Coastguard Worker shutil.rmtree(tempdir) 485*d289c2baSAndroid Build Coastguard Worker 486*d289c2baSAndroid Build Coastguard Worker @patch('argparse.ArgumentParser.error') 487*d289c2baSAndroid Build Coastguard Worker def testArgparseMutualExclusionArchiveAndFiles(self, mock_parser_error): 488*d289c2baSAndroid Build Coastguard Worker mock_parser_error.side_effect = ValueError('ArgumentParser.error') 489*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 490*d289c2baSAndroid Build Coastguard Worker main(['dummy.zip', '--pik_cert', DATA_FILE_PIK_CERTIFICATE]) 491*d289c2baSAndroid Build Coastguard Worker self.assertEqual(mock_parser_error.call_count, 1) 492*d289c2baSAndroid Build Coastguard Worker 493*d289c2baSAndroid Build Coastguard Worker @patch('argparse.ArgumentParser.error') 494*d289c2baSAndroid Build Coastguard Worker def testArgparseMutualInclusionOfFileArgs(self, mock_parser_error): 495*d289c2baSAndroid Build Coastguard Worker mock_parser_error.side_effect = ValueError('ArgumentParser.error') 496*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 497*d289c2baSAndroid Build Coastguard Worker main(['--pik_cert', 'pik_cert.bin', '--puk_cert', 'puk_cert.bin']) 498*d289c2baSAndroid Build Coastguard Worker self.assertEqual(mock_parser_error.call_count, 1) 499*d289c2baSAndroid Build Coastguard Worker 500*d289c2baSAndroid Build Coastguard Worker mock_parser_error.reset_mock() 501*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 502*d289c2baSAndroid Build Coastguard Worker main(['--pik_cert', 'pik_cert.bin', '--puk', 'puk.pem']) 503*d289c2baSAndroid Build Coastguard Worker self.assertEqual(mock_parser_error.call_count, 1) 504*d289c2baSAndroid Build Coastguard Worker 505*d289c2baSAndroid Build Coastguard Worker mock_parser_error.reset_mock() 506*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 507*d289c2baSAndroid Build Coastguard Worker main(['--puk_cert', 'puk_cert.bin', '--puk', 'puk.pem']) 508*d289c2baSAndroid Build Coastguard Worker self.assertEqual(mock_parser_error.call_count, 1) 509*d289c2baSAndroid Build Coastguard Worker 510*d289c2baSAndroid Build Coastguard Worker @patch('argparse.ArgumentParser.error') 511*d289c2baSAndroid Build Coastguard Worker def testArgparseMissingBundleAndFiles(self, mock_parser_error): 512*d289c2baSAndroid Build Coastguard Worker mock_parser_error.side_effect = ValueError('ArgumentParser.error') 513*d289c2baSAndroid Build Coastguard Worker with self.assertRaises(ValueError): 514*d289c2baSAndroid Build Coastguard Worker main(['-s', '1234abcd']) 515*d289c2baSAndroid Build Coastguard Worker self.assertEqual(mock_parser_error.call_count, 1) 516*d289c2baSAndroid Build Coastguard Worker 517*d289c2baSAndroid Build Coastguard Worker 518*d289c2baSAndroid Build Coastguard Workerif __name__ == '__main__': 519*d289c2baSAndroid Build Coastguard Worker unittest.main(verbosity=3) 520