예제 #1
0
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
예제 #2
0
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
        )
예제 #3
0
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)
예제 #4
0
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
        }
    )
예제 #5
0
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