def verifyManifestV1(options, data, signedResource, resource, manifest): if manifest['manifestVersion'] != 'v1': LOG.critical('Not a version 1 manifest') return None LOG.info('Manifest version {}: OK'.format(manifest['manifestVersion'])) # Extract the crypto mode if 'oid' in manifest['encryptionMode']: LOG.critical('Cannot verify Object ID encryptionMode') return None if not 'enum' in manifest['encryptionMode']: LOG.critical('No encryptionMode specified') return None emode = manifest['encryptionMode']['enum'] modes = verify_manifest_minimal.Manifest().getComponentType( )['encryptionMode'].getType().getComponentType()['enum'].getType( ).getNamedValues() if modes.getValue(emode) != None and emode != modes.getName(0): LOG.info('Encryption mode {}: OK'.format(emode)) else: LOG.critical('Encryption mode {} not supported'.format(emode)) return None # NOTE: Currently only modes 1-3 are supported and these modes only allow SHA256. # This means that the manifest is hashed with SHA256. # Load defaults defaultConfig = {} if os.path.exists(defaults.config): with open(defaults.config) as f: defaultConfig = json.load(f) # Verify UUIDs for i in ['vendorId', 'classId', 'deviceId']: if i in manifest: try: LOG.debug('Found {}: {}'.format(i, manifest[i])) manifest_id = uuid.UUID(manifest[i]) except: #if len(uuid) != 0 and len(uuid) != 128/8: LOG.critical('UUIDs ({}) must be 0 or 128 bits'.format(i)) return None expect_id = None if hasattr(options, i) and getattr(options, i): expect_id = uuid.UUID(getattr(options, i)) else: expect_id = defaultConfig.get(i, None) if expect_id: expect_id = uuid.UUID(expect_id) LOG.debug('Using default {} from {}'.format( i, defaults.config)) if expect_id: if manifest_id != expect_id: LOG.critical('UUID mismatch for {}\n' 'Expected: {}\n' 'Actual: {}'.format(i, expect_id, manifest_id)) return None LOG.info('UUID {}: OK'.format(i)) else: LOG.warning('UUID {} not specified; ignoring'.format(i)) # Verify the manifest hash if not 'signature' in signedResource: LOG.critical( 'Signature missing, but encryption mode {} requires a signature'. format(emode)) return None c_hash = utils.sha_hash(signedResource['resource']) if not 'hash' in signedResource['signature']: LOG.critical('Manifest does not contain a hash') return None e_hash = binascii.a2b_hex(signedResource['signature']['hash']) if c_hash != e_hash: LOG.critical('Hash mismatch\nExpected: {}\nActual: {}'.format( binascii.b2a_hex(e_hash), binascii.b2a_hex(c_hash))) return None LOG.debug('Manifest hash: {}'.format( binascii.b2a_hex(c_hash).decode('utf-8'))) LOG.info('Maninfest hash: OK') if not options.certificateQuery: LOG.warning('No certificateQuery provided, will not verify signatures') if (emode == 'none-ecc-secp256r1-sha256' or emode == 'aes-128-ctr-ecc-secp256r1-sha256' ) and options.certificateQuery: if not 'signatures' in signedResource['signature']: LOG.critical( 'Signature missing, but encryption mode {} requires a signature' .format(emode)) return None for signature in signedResource['signature']['signatures']: if not 'certificates' in signature: LOG.critical( 'A certificate reference is mandatory in a signature') return None if len(signature['certificates']) == 0: LOG.critical( 'At least one certificate reference is mandatory in a signature' ) return None if not 'fingerprint' in signature['certificates'][0] or len( signature['certificates'][0]['fingerprint']) == 0: LOG.critical('A fingerprint is mandatory in a certificate') return None LOG.debug('Verifying signature by {}'.format( signature['certificates'][0]['fingerprint'])) fingerprints = [ x['fingerprint'] for x in signature['certificates'] ] URLs = [ x['uri'] if 'uri' in x else '' for x in signature['certificates'] ] certfile = options.certificateQuery(options, fingerprints, URLs) if certfile == None: # TODO: Option for mandatory certificate verification if options.mandatory_signature: LOG.critical( 'Could not find certificate chain matching {}'.format( fingerprints[0])) return None LOG.warning( 'Could not find certificate chain matching {}'.format( fingerprints[0])) else: #if not options.ecdsaVerify(cert, signature): # ok = False # vk = ecdsa.VerifyingKey.from_pem(keypem) # ok = vk.verify(binascii.a2b_hex(signature['signature']), # signedResource['resource'], # hashfunc=hashlib.sha256, # sigdecode=ecdsa.util.sigdecode_der) LOG.debug('Opening {} ...'.format(certfile)) with open(certfile, 'rb') as cert: ok = options.ecdsaVerify(cert, signature['signature'], e_hash) if not ok: LOG.critical('Signature verification failed') return None LOG.info('Signature by {}: OK'.format(fingerprints[0])) # Decode the full manifest. full_manifest = { "der": lambda d: codec.bin2obj(data, manifest_definition.SignedResource(), der_decoder), }[options.encoding](data) LOG.debug('Parsed whole manifest from {}-encoded binary object'.format( options.encoding)) full_manifest = full_manifest['resource']['resource']['manifest'] # TODO: Measure nonce entropy if not 'nonce' in full_manifest: LOG.critical('A nonce is required in the manifest') return None else: nonce = binascii.a2b_hex(full_manifest['nonce']) if len(nonce) * 8 != 128: LOG.critical('Nonce must be 128 bits. Got {}: {}'.format( len(nonce) * 8, binascii.b2a_hex(nonce))) return None LOG.info('nonce: {} OK'.format(full_manifest['nonce'])) # Call vendor-supplied Vendor Info Validator if hasattr(options, 'validateVendorInfo'): if options.validateVendorInfo(options, full_manifest['vendorInfo']): LOG.critical('Vendor Info Validation failed') return None LOG.info('VendorInfo: OK') # Extract apply period applyPeriod = full_manifest.get('applyPeriod') # must have either a payload or a dependency if not 'payload' in full_manifest and ( not 'dependencies' in full_manifest or len(full_manifest['dependencies']) == 0): LOG.critical('Manifest must contain either a dependency or a payload') sys.exit(1) # Verify the payload if 'payload' in full_manifest: payload = full_manifest['payload'] LOG.debug('Manifest contains a payload') # Verify the payload format if 'enum' in payload['format']: enum = payload['format']['enum'] payloadFormats = manifest_definition.PayloadDescription( ).getComponentType()['format'].getType().getComponentType( )['enum'].getType().getNamedValues() if payloadFormats.getValue( enum) == None or enum == payloadFormats.getName(0): LOG.critical('Payload format not recognized') return None LOG.info('Payload format {}: OK'.format(enum)) elif 'objectId' in payload['format']: LOG.warning('Cannot verify Object ID payload format') else: LOG.critical('Payload does not contain a format') return None if emode == 'aes-128-ctr-ecc-secp256r1-sha256': if not 'encryptionInfo' in payload: LOG.critical( 'Encryption info must be present for encrypted payload distribution' ) return None cryptinfo = payload['encryptionInfo'] # Validate the encryption information # Validate the init vector: if not 'initVector' in cryptinfo or len( binascii.a2b_hex(cryptinfo['initVector'])) != 128 / 8: LOG.critical( 'When using aes-128-ctr-ecc-secp256r1-sha256, a 128-bit AES initialization vector is mandatory' ) if 'initVector' in cryptinfo: iv = binascii.a2b_hex(cryptinfo['initVector']) LOG.critical('Expected 128 bits, got {}: {}'.format( len(iv) * 8, binascii.b2a_hex(iv))) return None # TODO: Verify the entropy of the init vector # Determine the key mode kmode = 0 # Options: if 'key' in cryptinfo['id'] and 'cipherKey' in cryptinfo['key']: if len(cryptinfo['key']['cipherKey']) == 0: # Select a preshared local key kmode = 1 LOG.debug('Using local preshared key for decryption') else: # Select a preshared local key & decrypt a session key kmode = 2 LOG.debug( 'Using local preshared key to decrypt the payload key') # Select certificate & decrypt a session key (Single-device only) elif 'certificate' in cryptinfo['id'] and 'cipherKey' in cryptinfo[ 'key']: kmode = 3 LOG.debug('Using ECDH to decrypt the device key') # Select a certificate & a keytable elif 'certificate' in cryptinfo['id'] and 'keyTable' in cryptinfo[ 'key']: kmode = 4 LOG.debug( 'Using ECDH to decrypt the payload key from the key table') if kmode == 0: LOG.critical('Unrecognized key distribution mode') return None # NOTE: It is not possible to verify payload without a device key when it is encrypted. # Verify the storage identifier if not 'storageIdentifier' in payload or len( payload['storageIdentifier']) == 0: LOG.critical('storageIdentifier must be provided') return None # Verify the resource reference if not 'reference' in payload: LOG.critical( 'A resource reference is mandatory in a payload-bearing manifest' ) return None if not 'hash' in payload['reference'] or len( payload['reference']['hash']) == 0: LOG.critical('A resource hash is mandatory in a payload reference') return None if not 'size' in payload['reference'] or payload['reference'][ 'size'] == 0: LOG.critical('Zero-size resources are not permitted') return None if 'uri' in payload['reference']: LOG.debug('Payload refers to URI: {}'.format( payload['reference']['uri'])) # TODO: verify payload URI # Do not verify the version string; it is for presentation only. # TODO: Validate aliases # TODO: Validate dependencies return { 'timestamp': full_manifest['timestamp'], 'applyPeriod': applyPeriod }
def verifyManifest(options): data = options.input_file.read() LOG.debug('Read {} bytes of encoded data. Will try to decode...'.format( len(data))) # TODO: Verify the DER structure's encoding # Extract the signed resource. signed_resource_data = { "der": lambda d: codec.bin2obj(d, verify_signed_resource.SignedResource(), der_decoder), }[options.encoding](data) LOG.debug('Decoded SignedResource from {} encoded binary'.format( options.encoding)) # Verify that the content *is* a manifest. # NOTE: This requires a parsing pass of the resource. resource_data = { "der": lambda d: codec.bin2obj(d, verify_resource.Resource(), der_decoder), }[options.encoding](signed_resource_data['resource']) LOG.debug('Decoded Resource from {} encoded binary'.format( options.encoding)) if resource_data['resourceType'] != 'manifest': LOG.critical('The supplied file does not contain a manifest') return 1 # Extract some relevant manifest information: manifest_data = { "der": lambda d: codec.bin2obj(d, verify_manifest_minimal.Manifest(), der_decoder), }[options.encoding](resource_data['resource']) LOG.debug('Decoded Manifest from {} encoded binary'.format( options.encoding)) # Select a manifest format decoder based on the manifest version. def noVersionError(options, data, signedResource, resource, manifest): LOG.critical('Unsupported manifest version: {}'.format( manifest_data['manifestVersion'])) None manifest_verification_data = { 'v1': verifyManifestV1 }.get(manifest_data['manifestVersion'], noVersionError)(options, data, signed_resource_data, resource_data, manifest_data) if manifest_verification_data == None: return 1 # Verify the manifest timestamp is sane # The manifest timestamp should not be in the future, nor before the first release of this tool. # manifest_timestamp = decoded_data['resource']['resource']['manifest']['timestamp'] manifest_timestamp = manifest_verification_data['timestamp'] systime = int(time.time()) if manifest_timestamp > systime: LOG.critical('Manifests MUST not be timestamped in the future.\n' 'Expected timestamp < {} ({})\n' 'Actual timestamp: {} ({})'.format( systime, time.ctime(systime), manifest_timestamp, time.ctime(manifest_timestamp))) release_time = time.mktime( time.strptime('2016-11-22T00:00:00', "%Y-%m-%dT%H:%M:%S")) if manifest_timestamp < int(release_time): LOG.critical('Manifests MUST not be timestamped before 2017.\n' 'Expected timestamp > {} ({})\n' 'Actual timestamp: {} ({})'.format( int(release_time), time.ctime(int(release_time)), manifest_timestamp, time.ctime(manifest_timestamp))) LOG.info('Timestamp {} < {} < {}: OK'.format(int(release_time), manifest_timestamp, systime)) # Verify applyPeriod if 'applyPeriod' in manifest_verification_data and manifest_verification_data[ 'applyPeriod']: applyPeriod = manifest_verification_data['applyPeriod'] if release_time > applyPeriod['validFrom'] or \ applyPeriod['validFrom'] > applyPeriod['validTo'] or \ applyPeriod['validTo'] > systime: LOG.critical( 'Apply perion outside expected bounds: Expected:\n{} <= {} <= {} <= {}' .format(release_time, applyPeriod['validFrom'], applyPeriod['validTo'], systime)) return 1 LOG.info('Apply period {} to {} OK'.format(applyPeriod['validFrom'], applyPeriod['validTo'])) # Verify each URL # Fetch each file from each listed URL (may not be possible, depending on network architecture) # Validate the hash of each file from each listed URL # Write to output buffer/file # indent = None if not options.pretty_json else 2 # options.output_file.write(str.encode(json.dumps(decoded_data, indent = indent))) # If we're writing to TTY, add a helpful newline # if options.output_file.isatty(): # options.output_file.write(b'\n') return 0