def get_manifest_dependencies(options, manifestInput): dependencies = [] for link in manifestGet(manifestInput, 'resource.resource.manifest.dependencies') or []: if not any(k in link for k in ('uri', 'file')): LOG.critical('Manifest link requires either a "uri" or a "file"' 'key - or both. Could only find %r' % link.keys()) sys.exit(1) LOG.debug('Adding manifest link reference (URI: {}, File: {})'.format(link.get('uri', None), link.get('file', None))) # If file isn't provided, we attempt to download the file from provided URI # If the user provides the link as a filepath, just read the file if 'file' in link: linkPath = link['file'] if not os.path.isabs(linkPath): linkPath = os.path.join(os.path.dirname(options.input_file.name), linkPath) content = utils.read_file(linkPath) else: content = utils.download_file(link['uri']) # Calculate the hash and length. manifest_hash = utils.sha_hash(content) manifest_length = len(content) LOG.debug('Linked manifest of {} bytes loaded. Hash: {}'.format(manifest_length, manifest_hash)) link_uri = link['uri'] if 'uri' in link else link['file'] dependencies.append({ 'hash': manifest_hash, 'uri': link_uri, 'size': manifest_length }) return dependencies
def get_signature(options, manifestInput, enc_data): signatures = manifestGet(manifestInput,'signature.signatures') or [] input_hash = manifestGet(manifestInput,'signature.hash') or b'' # There should always be a signing key on create. if not hasattr(options,'private_key') or not options.private_key: if 'private-key' in manifestInput: try: options.private_key = open(manifestInput['private-key'],'r') except: LOG.critical('No private key specified and default key ({}) cannot be opened'.format(manifestInput['private-key'])) sys.exit(1) else: LOG.critical('Resource is not signed and no signing key is provided.') sys.exit(1) # Get SHA-256 hash of content and sign it using private key sha_content = utils.sha_hash(enc_data) if len(signatures): # If a signature is provided in the input json, then the encoded content must match the provided hash # Signature validation is not performed, since this would require certificate acquisition, which may not be # possible if sha_content != binascii.a2b_hex(input_hash): LOG.critical('Manifest hash provided in input file does not match hashed output') LOG.critical('Expected: {0}'.format(input_hash)) LOG.critical('Actual: {0}'.format(binascii.b2a_hex(sha_content))) sys.exit(1) # TODO: perform best-effort signature validation if hasattr(options, 'private_key') and options.private_key: sk = ecdsa.SigningKey.from_pem(options.private_key.read()) sig = sk.sign_digest(sha_content, sigencode=ecdsa.util.sigencode_der) certificates = [] # pick a signature block with no signature in it. inputCerts = manifestGet(manifestInput, 'certificates') or [] # If no certificate was provided in the manifest input or in options, if len(inputCerts) == 0: # then load the default certificate inputCerts = manifestInput.get('default-certificates', []) # If there is still no certificate, if len(inputCerts) == 0: # Search through all signature blocks for one that contains certificates but no signature for idx, sb in enumerate(signatures): if not 'signature' in sb: inputCerts = sb.get('certificates', []) # This signature will be appended later so we must trim it. del signatures[idx] break for idx, cert in enumerate(inputCerts): if not any(k in cert for k in ('file', 'uri')): LOG.critical('Could not find "file" or "uri" property for certificate') sys.exit(1) # If 'file', we just use the content in local file if 'file' in cert: fPath = cert['file'] if not os.path.isabs(fPath): fPath = os.path.join(os.path.dirname(options.input_file.name), cert['file']) content = utils.read_file(fPath) # Else we download the file contents else: content = utils.download_file(cert['uri']) # Figure our which extension the certificate has contentPath = cert['file'] if 'file' in cert else cert['uri'] ext = contentPath.rsplit('.', 1)[1] # Read the certificate file, and get DER encoded data if ext == 'pem': lines = content.replace(" ",'').split() content = binascii.a2b_base64(''.join(lines[1:-1])) # Verify the certificate hash algorithm # Extract subjectPublicKeyInfo field from X.509 certificate (see RFC3280) # fingerprint = utils.sha_hash(content) cPath = cert['file'] if 'file' in cert else cert['uri'] certObj = None try: certObj = x509.load_der_x509_certificate(content, cryptoBackends.default_backend()) except ValueError as e: LOG.critical("X.509 Certificate Error in ({file}): {error}".format(error=e, file=cPath)) sys.exit(1) if not certObj: LOG.critical("({file}) is not a valid certificate".format(file=cPath)) sys.exit(1) if not isinstance(certObj.signature_hash_algorithm, cryptoHashes.SHA256): LOG.critical("In ({file}): Only SHA256 certificates are supported by the mbed Cloud Update client at this time.".format(file=cPath)) sys.exit(1) fingerprint = certObj.fingerprint(cryptoHashes.SHA256()) LOG.debug('Creating certificate reference ({}) from {} with fingerprint {}'.format(idx, contentPath, fingerprint)) uri = '' if 'uri' in cert: uri = cert['uri'] certificates.append(CertificateReference( fingerprint = fingerprint, uri = uri )) LOG.debug('Signed hash ({}) of encoded content ({} bytes) with resulting signature {}'.format( sha_content, len(enc_data), sig)) signatures.append(SignatureBlock(signature = sig, certificates = certificates)) return ResourceSignature( hash = sha_content, signatures = signatures )
def get_signature(options, manifestInput, enc_data): signatures = manifestGet(manifestInput, 'signature.signatures') or [] input_hash = manifestGet(manifestInput, 'signature.hash') or b'' signing_tool = manifestGet(manifestInput, 'signing-tool') or '' if getattr(options, 'signing_tool', None): signing_tool = options.signing_tool # There should always be a signing key or signing tool on create. if not signing_tool and not getattr(options, 'private_key', None): if 'private-key' in manifestInput: try: options.private_key = open(manifestInput['private-key'], 'r') except: LOG.critical( 'No private key specified and default key ({}) cannot be opened' .format(manifestInput['private-key'])) sys.exit(1) else: LOG.critical( 'Resource is not signed and no signing key is provided.') sys.exit(1) # Get SHA-256 hash of content and sign it using private key sha_content = utils.sha_hash(enc_data) if len(signatures): # If a signature is provided in the input json, then the encoded content must match the provided hash # Signature validation is not performed, since this would require certificate acquisition, which may not be # possible if sha_content != binascii.a2b_hex(input_hash): LOG.critical( 'Manifest hash provided in input file does not match hashed output' ) LOG.critical('Expected: {0}'.format(input_hash)) LOG.critical('Actual: {0}'.format(binascii.b2a_hex(sha_content))) sys.exit(1) # TODO: perform best-effort signature validation signature = None if signing_tool: # get the key id key_id = manifestGet(manifestInput, 'signing-key-id') if hasattr(options, 'signing_key_id') and options.signing_key_id: key_id = options.signing_key_id digest_algo = 'sha256' infile = None with tempfile.NamedTemporaryFile(delete=False) as f: infile = f.name f.write(enc_data) f.flush() LOG.debug('Temporary manifest file: {}'.format(infile)) outfile = None with tempfile.NamedTemporaryFile(delete=False) as f: outfile = f.name LOG.debug('Temporary signature file: {}'.format(outfile)) try: cmd = [signing_tool, digest_algo, key_id, infile, outfile] LOG.debug('Running "{}" to sign manifest.'.format(' '.join(cmd))) # This command line is constructed internally, so we ignore bandit # warnings about executing a Popen. See: # https://bandit.readthedocs.io/en/latest/plugins/b603_subprocess_without_shell_equals_true.html p = subprocess.Popen(cmd) #nosec p.wait() if p.returncode != 0: LOG.critical('Signing tool failed.') sys.exit(1) with open(outfile, 'rb') as f: signature = f.read() except: LOG.critical('Failed to execute signing tool.') sys.exit(1) finally: os.unlink(infile) os.unlink(outfile) LOG.debug('Signature: {}'.format( binascii.b2a_hex(signature).decode('utf-8'))) elif hasattr(options, 'private_key') and options.private_key: sk = ecdsa.SigningKey.from_pem(options.private_key.read()) signature = sk.sign_digest(sha_content, sigencode=ecdsa.util.sigencode_der) certificates = [] # pick a signature block with no signature in it. inputCerts = manifestGet(manifestInput, 'certificates') or [] # If no certificate was provided in the manifest input or in options, if len(inputCerts) == 0: # then load the default certificate inputCerts = manifestInput.get('default-certificates', []) # If there is still no certificate, if len(inputCerts) == 0: # Search through all signature blocks for one that contains certificates but no signature for idx, sb in enumerate(signatures): if not 'signature' in sb: inputCerts = sb.get('certificates', []) # This signature will be appended later so we must trim it. del signatures[idx] break for idx, cert in enumerate(inputCerts): if not any(k in cert for k in ('file', 'uri')): LOG.critical( 'Could not find "file" or "uri" property for certificate') sys.exit(1) # If 'file', we just use the content in local file if 'file' in cert: fPath = cert['file'] if not os.path.isabs(fPath): fPath = os.path.join(os.path.dirname(options.input_file.name), cert['file']) content = utils.read_file(fPath) # Else we download the file contents else: content = utils.download_file(cert['uri']) # Figure our which extension the certificate has contentPath = cert['file'] if 'file' in cert else cert['uri'] ext = contentPath.rsplit('.', 1)[1] # Read the certificate file, and get DER encoded data if ext == 'pem': lines = content.replace(" ", '').split() content = binascii.a2b_base64(''.join(lines[1:-1])) # Verify the certificate hash algorithm # Extract subjectPublicKeyInfo field from X.509 certificate (see RFC3280) # fingerprint = utils.sha_hash(content) cPath = cert['file'] if 'file' in cert else cert['uri'] certObj = None try: certObj = x509.load_der_x509_certificate( content, cryptoBackends.default_backend()) except ValueError as e: LOG.critical("X.509 Certificate Error in ({file}): {error}".format( error=e, file=cPath)) sys.exit(1) if not certObj: LOG.critical( "({file}) is not a valid certificate".format(file=cPath)) sys.exit(1) if not isinstance(certObj.signature_hash_algorithm, cryptoHashes.SHA256): LOG.critical( "In ({file}): Only SHA256 certificates are supported by the Device Management Update client at this time." .format(file=cPath)) sys.exit(1) fingerprint = certObj.fingerprint(cryptoHashes.SHA256()) LOG.debug( 'Creating certificate reference ({}) from {} with fingerprint {}'. format(idx, contentPath, fingerprint)) uri = '' if 'uri' in cert: uri = cert['uri'] certificates.append( CertificateReference(fingerprint=fingerprint, uri=uri)) LOG.debug( 'Signed hash ({}) of encoded content ({} bytes) with resulting signature {}' .format(sha_content, len(enc_data), signature)) if signature: signatures.append( SignatureBlock(signature=signature, certificates=certificates)) return ResourceSignature(hash=sha_content, signatures=signatures)
def get_payload_description(options, manifestInput): crypto_mode = manifestGet(manifestInput, 'resource.resource.manifest.encryptionMode.enum', 'encryptionMode') crypto_mode = crypto_mode if crypto_mode in cryptoMode.MODES else cryptoMode.name2enum(crypto_mode) if not crypto_mode: cryptname = '' if hasattr(options, 'encrypt_payload') and options.encrypt_payload: cryptname = 'aes-128-ctr-ecc-secp256r1-sha256' else: cryptname = 'none-ecc-secp256r1-sha256' crypto_mode = cryptoMode.name2enum(cryptname) crypto_name = cryptoMode.MODES.get(crypto_mode).name payload_file = manifestGet(manifestInput, 'resource.resource.manifest.payload.reference.file', 'payloadFile') if hasattr(options, 'payload') and options.payload: payload_file = options.payload.name # payload URI defaults to the payload file path if no payload URI is supplied. payload_uri = payload_file payload_uri = manifestGet(manifestInput, 'resource.resource.manifest.payload.reference.uri', 'payloadUri') if hasattr(options, 'uri') and options.uri: payload_uri = options.uri dependencies = manifestGet(manifestInput, 'resource.resource.manifest.dependencies') if not any((payload_uri, payload_file)) and not dependencies: LOG.critical('No payload was specified and no dependencies were provided.') sys.exit(1) # fwFile/fwUri is optional, so if not provided we just return empty return None payload_hash = manifestGet(manifestInput, 'resource.resource.manifest.payload.reference.hash', 'payloadHash') payload_size = manifestGet(manifestInput, 'resource.resource.manifest.payload.reference.size', 'payloadSize') if payload_hash: LOG.debug('Found hash in input, skipping payload load. Hash: {}'.format(payload_hash)) payload_hash = binascii.a2b_hex(payload_hash) else: if payload_file: payload_filePath = payload_file # If file path is not absolute, then make it relative to input file if os.path.isabs(payload_file) or not options.input_file.isatty(): payload_filePath = os.path.join(os.path.dirname(options.input_file.name), payload_file) content = utils.read_file(payload_filePath) else: content = utils.download_file(payload_uri) # Read payload input, record length and hash it payload_size = len(content) payload_hash = utils.sha_hash(content) LOG.debug('payload of {} bytes loaded. Hash: {}'.format(payload_size, payload_hash)) # Ensure the cryptoMode is valid if not crypto_mode in cryptoMode.MODES: valid_modes = ", ".join((('%s (%d)' % (v.name, k) for k, v in cryptoMode.MODES.items()))) LOG.critical('Could not find specified cryptoMode (%d) in list of valid encryption modes. ' 'Please use on of the following: %r' % (crypto_mode, valid_modes)) sys.exit(1) # Get encryption options for the provided mode should_encrypt = ARM_UC_mmCryptoModeShouldEncrypt(crypto_mode) if hasattr(options, 'encrypt_payload') and options.encrypt_payload and not should_encrypt: LOG.critical('--encrypt-payload specified, but cryptoMode({cryptoMode}) does not support encryption.'.format(**manifestInput)) sys.exit(1) encryptionInfo = None if should_encrypt: LOG.debug('Crypto mode {} ({}) requires encryption. Will ensure ciphers are valid and loaded...'\ .format(crypto_mode, crypto_name)) if not options.encrypt_payload: LOG.critical('Specified crypto mode ({cryptoMode}) requires encryption, ' 'but --encrypt-payload not specified.'.format(**manifestInput)) sys.exit(1) cipherFile = manifestGet(manifestInput, 'resource.resource.manifest.payload.encryptionInfo.file', 'encryptedPayloadFile') if not cipherFile: LOG.critical('"resource.resource.manifest.payload.encryptionInfo.file" must be specified in the JSON input' 'file when --encrypt-payload is specified on the command-line.') sys.exit(1) encryptionInfo = {} cipherModes = { 'aes-psk' : encryptKeyAES } if not options.encrypt_payload in cipherModes: LOG.critical('Specified encryption mode "{mode}" is not supported'.format(mode=options.encrypt_payload)) sys.exit(1) init_vector, cipherKey, cipherpayload = cipherModes.get(options.encrypt_payload)(options, manifestInput, encryptionInfo, content) with open(cipherFile,'wb') as f: f.write(cipherpayload) encryptionInfo["key"] = { "cipherKey": cipherKey } encryptionInfo["initVector"] = init_vector LOG.debug('payload ({} bytes) encrypted. Cipher key: {}, cipher payload ouptut to : {}'.format(len(content), cipherKey, cipherFile)) else: LOG.debug('Will not encrypt payload as crypto mode {} ({}) does not require it'.format(crypto_mode, crypto_name)) return PayloadDescription( **{ "storageIdentifier": manifestGet(manifestInput,'resource.resource.manifest.payload.storageIdentifier', 'storageIdentifier') or "default", "reference": ResourceReference( hash = payload_hash, uri = payload_uri, size = payload_size ), "encryptionInfo": encryptionInfo } )
def sign(options): # Load defaults defaultConfig = {} if os.path.exists(defaults.config): with open(defaults.config) as f: defaultConfig = json.load(f) # Load the existing manifest content = None if hasattr(options, 'manifest') and options.manifest: options.manifest.seek(0) content = options.manifest.read() else: content = utils.download_file(options.url) # Extract the signed resource. signed_resource_data = { "der": lambda d: codec.bin2obj(d, definition.SignedResource(), der_decoder), }[options.encoding](content) if not signed_resource_data: return 1 LOG.debug('Decoded SignedResource from {} encoded binary'.format( options.encoding)) # Sign the resource # Calculate the hash of the content of the existing manifest c_hash = utils.sha_hash(signed_resource_data['resource']) if not 'hash' in signed_resource_data['signature']: LOG.critical('Manifest does not contain a hash') return None # Extract the hash e_hash = binascii.a2b_hex(signed_resource_data['signature']['hash']) # Compare calculated and extracted hashes 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))) # Load a certificate and private key # Private key must be in .manifest_tool.json, or provided on the command-line # 1. Check the command-line if not hasattr(options, 'private_key') or not options.private_key: # 2. Check the default config if 'private-key' in defaultConfig: try: # NOTE: binary is not specified since the key is usually PEM encoded. options.private_key = open(defaultConfig['private-key'], 'r') except: LOG.critical( 'No private key specified and default key ({}) cannot be opened' .format(defaultConfig['private-key'])) return 1 # 3. Fail if the private key is not found if not hasattr(options, 'private_key') or not options.private_key: LOG.critical( 'No private key specified and default key ({}) cannot be opened'. format(defaultConfig['private-key'])) return 1 if not hasattr(options, 'password'): options.password = None # Load the private key privkey = load_pem_private_key(options.private_key.read(), password=options.password, backend=default_backend()) # Make sure that this is an ECDSA key! if not isinstance(privkey, ec.EllipticCurvePrivateKey): LOG.critical('Private key was not an ECC private key') return 1 LOG.debug('Loaded private key') LOG.info('Signing manifest...') # Create signature sig = privkey.sign(signed_resource_data['resource'], ec.ECDSA(hashes.SHA256())) LOG.debug('Signature: {}'.format(binascii.b2a_hex(sig))) # destroy the privkey object privkey = None # Certificate must be in .manifest_tool.json, or provided on the command-line # Load the certificate if not hasattr(options, 'certificate') or not options.certificate: if 'default-certificates' in defaultConfig: options.certificate = open( defaultConfig['default-certificates'][0]['file'], 'rb') if not hasattr(options, 'certificate') or not options.certificate: LOG.critical( 'No certificate specified and default certificate ({}) cannot be opened' .format(defaultConfig['default-certificates'][0]['file'])) return 1 # Load the certificate object from the DER file certObj = None try: certObj = x509.load_der_x509_certificate(options.certificate.read(), default_backend()) except ValueError as e: LOG.critical("X.509 Certificate Error in ({file}): {error}".format( error=e, file=options.certificate.name)) return (1) if not certObj: LOG.critical("({file}) is not a valid certificate".format(file=cPath)) return (1) # Make sure that the certificate is signed with SHA256 if not isinstance(certObj.signature_hash_algorithm, hashes.SHA256): LOG.critical( "In ({file}): Only SHA256 certificates are supported by the update client at this time." .format(file=cPath)) return (1) LOG.info('Verifying signature with supplied certificate...') # Verify the signature with the provided certificate to ensure that the update target will be able to do so try: pubkey = certObj.public_key() pubkey.verify(sig, signed_resource_data['resource'], ec.ECDSA(hashes.SHA256())) except InvalidSignature as e: LOG.critical( 'New signature failed to verify with supplied certificate ({})'. format(options.certificate.name)) return 1 # Store the fingerprint of the certificate fingerprint = certObj.fingerprint(hashes.SHA256()) certificates = [] # for idx in range(len()) # certObj = None # try: # certObj = x509.load_der_x509_certificate(options.certificate.read(), default_backend()) # except ValueError as e: # LOG.critical("X.509 Certificate Error in ({file}): {error}".format(error=e, file=options.certificate.name)) # return(1) # # if not certObj: # LOG.critical("({file}) is not a valid certificate".format(file=cPath)) # return(1) # if not isinstance(certObj.signature_hash_algorithm, hashes.SHA256): # LOG.critical("In ({file}): Only SHA256 certificates are supported by the update client at this time.".format(file=cPath)) # return(1) # LOG.debug('Creating certificate reference ({}) from {} with fingerprint {}'.format(idx, options.certificate.name, fingerprint)) LOG.debug( 'Creating certificate reference from {} with fingerprint {}'.format( options.certificate.name, fingerprint)) uri = '' # TODO: Insert URI for delegation of trust cr = schema.CertificateReference(fingerprint=fingerprint, uri=uri) # Append the certificate reference to the current list # NOTE: Currently, only one certificate reference will exist in the certificates list, but with delegation of trust # there will be more certificate references certificates.append(cr) LOG.debug( 'Signed hash ({}) of encoded content ({} bytes) with resulting signature {}' .format(binascii.b2a_hex(c_hash), len(content), binascii.b2a_hex(sig))) signatures = [] for s in signed_resource_data['signature']['signatures']: signatures.append(sig_from_dict(s)) # encode the signature block signatures.append( schema.SignatureBlock(signature=sig, certificates=certificates)) # encode the resource signature resource_signature = schema.ResourceSignature(hash=c_hash, signatures=signatures) # encode the signed resource signed_resource = schema.SignedResource( resource=signed_resource_data['resource'], signature=resource_signature) # Convert the signed resource into a python dictionary manifest_dict = signed_resource.to_dict() # Encode the Python dictionary as a DER stream output = utils.encode(manifest_dict, options, definition.SignedResource()) # Write the result to the output_file if hasattr(options, 'output_file') and options.output_file: # Write result to output file or stdout buffer. options.output_file.write(output) # Append newline if outputting to TTY if options.output_file.isatty(): options.output_file.write(b'\n') return 0 return 1