Esempio n. 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
Esempio n. 2
0
def get_manifest_aliases(options, manifestInput):
    aliases = []
    for alias in manifestGet(manifestInput,'resource.resource.manifest.aliases') or []:
        # Ensure all required options are defined
        if not all(k in alias for k in ('file', 'uri')):
            LOG.critical('Could not create aliases, as all required keys for alias are not defined ("uri" and "file")')
            sys.exit(1)

        # Read alias file to calculate hash
        fPath = alias['file']
        if not os.path.isabs(fPath):
            fPath = os.path.join(os.path.dirname(options.input_file.name), fPath)
        content = utils.read_file(fPath)
        sha_hash = utils.sha_hash(content)
        LOG.debug('Creating ResourceAlias for file {} with SHA reference {}'.format(fPath, sha_hash))

        aliases.append(ResourceAlias(
            hash = sha_hash,
            uri = alias['uri']
        ))
    return aliases
Esempio n. 3
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
        )
Esempio n. 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
        }
    )
Esempio n. 5
0
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
    }
Esempio n. 6
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)
Esempio n. 7
0
def get_symmetric_signature(options, manifestInput, enc_data):
    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 'psk-master-key' in manifestInput:
            try:
                options.private_key = open(manifestInput['psk-master-key'],
                                           'rb')
            except:
                LOG.critical(
                    'No PSK master key specified and default key ({}) cannot be opened'
                    .format(manifestInput['private-key']))
                sys.exit(1)
        else:
            LOG.critical(
                'Resource is not signed and no PSK master key is provided.')
            sys.exit(1)

    if not symmetricArgsOkay(options):
        LOG.critical('--mac requires:')
        LOG.critical(
            '    --private-key: The master key for generating device PSKs')
        #                                                                                 80 chars ->|
        #             ================================================================================
        LOG.critical(
            '    --psk-table: the output file for PSKs. This argument is optional/ignored'
        )
        LOG.critical('                 when inline encoding is used.')
        LOG.critical(
            '    --psk-table-encoding: the encoding to use for the PSK output file.'
        )
        LOG.critical('    Either:')
        LOG.critical(
            '        --device-urn: The device URN for the target device (Endpoint Client Name)'
        )
        LOG.critical('    OR:')
        LOG.critical(
            '        --filter-id: The filter identifier used to gather device URNs'
        )
        sys.exit(1)

    # Get SHA-256 hash of content and sign it using private key
    sha_content = utils.sha_hash(enc_data)
    # If a hash is provided in the input json, then the encoded content must match the provided hash
    if input_hash and 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)

    # Load a default URN out of the settings file.
    devices = manifestGet(manifestInput, 'deviceURNs') or []
    LOG.info('Loaded device URNs from {!r}'.format(defaults.config))
    for dev in devices:
        LOG.info('    {!r}'.format(dev))

    # Optionally load a URN out of the command-line arguments
    if hasattr(options, 'device_urn') and options.device_urn:
        cmdDevs = [options.device_urn]
        LOG.info('Loaded device URNs from input arguments')
        for dev in cmdDevs:
            LOG.info('    {!r}'.format(dev))
        devices.extend(cmdDevs)
    if hasattr(options, 'filter_id') and options.filter_id:
        LOG.critical('Device Filters not supported yet.')
        sys.exit(1)

    # Use only unique devices
    devices = list(set(devices))

    crypto_mode = get_crypto_mode(options, manifestInput)

    payload_key = b''
    if hasattr(options, 'payload_key') and options.payload_key:
        LOG.debug('Converting payload key ({0}) to binary'.format(
            options.payload_key))
        payload_key = bytes(binascii.a2b_hex(options.payload_key))
    LOG.debug('Payload key length: {0}'.format(len(payload_key)))
    master_key = options.private_key.read()
    vendor_id = manifestGet(manifestInput,
                            'resource.resource.manifest.vendorId', 'vendorId')
    class_id = manifestGet(manifestInput, 'resource.resource.manifest.classId',
                           'classId')
    iv = os.urandom(13)
    deviceSymmetricInfos = {}
    # For each device
    maxIndexSize = 0
    maxRecordSize = 0
    LOG.info('Creating per-device validation codes...')
    for device in devices:
        LOG.info('    {!r}'.format(device))

        hkdf = utils.getDevicePSK_HKDF(
            cryptoMode(crypto_mode).name, master_key,
            uuid.UUID(vendor_id).bytes,
            uuid.UUID(class_id).bytes, b'Authentication')
        psk = hkdf.derive(bytes(device, 'utf-8'))
        maxIndexSize = max(maxIndexSize, len(device))
        # Now encrypt the hash with the selected AE algorithm.
        pskCipherData = getDevicePSKData(crypto_mode, psk, iv, sha_content,
                                         payload_key)
        recordData = der_encoder.encode(
            OctetString(
                hexValue=binascii.b2a_hex(pskCipherData).decode("ascii")))
        maxRecordSize = max(maxRecordSize, len(recordData))
        deviceSymmetricInfos[device] = recordData
    # print (deviceSymmetricInfos)
    def proto_encode(x):
        keytable = keytable_pb2.KeyTable()

        for k, d in x.items():
            entry = keytable.entries.add()
            entry.urn = k
            entry.opaque = d
        return keytable.SerializeToString()

    # Save the symmetric info file
    encodedSymmertricInfos = {
        'json':
        lambda x: json.JSONEncoder(default=binascii.b2a_base64).encode(x),
        'cbor':
        lambda x: None,
        'protobuf':
        proto_encode,
        'text':
        lambda x: '\n'.join([
            ','.join([binascii.b2a_hex(y) for y in (k, d)])
            for k, d in x.items()
        ]) + '\n'
        # 'inline' : lambda x : None
    }.get(options.psk_table_encoding)(deviceSymmetricInfos)
    options.psk_table.write(encodedSymmertricInfos)

    #=========================
    # PSK ID is the subject key identifier (hash) of the master key.
    # The URI of the "certificate" is the location at which to find the key table or key query.
    # PSK is known only to the signer and the device
    # PSK Signature is AE(PSK, hash), but it is not included, since it must be distributed in the key table.
    shaMaster = cryptoHashes.Hash(cryptoHashes.SHA256(),
                                  cryptoBackends.default_backend())
    shaMaster.update(master_key)
    subjectKeyIdentifier = shaMaster.finalize()

    mb = MacBlock(pskID=subjectKeyIdentifier,
                  keyTableIV=iv,
                  keyTableRef='thismessage://1',
                  keyTableVersion=0,
                  keyTableRecordSize=maxRecordSize,
                  keyTableIndexSize=maxIndexSize)
    macs = [mb]
    rs = ResourceSignature(hash=sha_content, signatures=[], macs=macs)
    return rs
Esempio n. 8
0
def create(options):
    key = ec.generate_private_key(ec.SECP256R1(), default_backend())
    options.key_output.write(
        key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.TraditionalOpenSSL,
            encryption_algorithm=serialization.NoEncryption()))
    # Various details about who we are. For a self-signed certificate the
    # subject and issuer are always the same.
    subjectList = []
    try:
        if options.country:
            subjectList += [
                x509.NameAttribute(x509.oid.NameOID.COUNTRY_NAME,
                                   text(options.country))
            ]
        if options.state:
            subjectList += [
                x509.NameAttribute(x509.oid.NameOID.STATE_OR_PROVINCE_NAME,
                                   text(options.state))
            ]
        if options.locality:
            subjectList += [
                x509.NameAttribute(x509.oid.NameOID.LOCALITY_NAME,
                                   text(options.locality))
            ]
        if options.organization:
            subjectList += [
                x509.NameAttribute(x509.oid.NameOID.ORGANIZATION_NAME,
                                   text(options.organization))
            ]
        if options.common_name:
            subjectList += [
                x509.NameAttribute(x509.oid.NameOID.COMMON_NAME,
                                   text(options.common_name))
            ]
        if len(subjectList) == 0:
            subjectList += [
                x509.NameAttribute(x509.oid.NameOID.COMMON_NAME,
                                   text('localhost'))
            ]
        subject = issuer = x509.Name(subjectList)
    except ValueError as e:
        LOG.critical('Error creating certificate: {}'.format(e.message))
        fname = options.output_file.name
        options.output_file.close()
        os.remove(fname)
        return 1

    subjectKey = key.public_key().public_bytes(
        serialization.Encoding.DER,
        serialization.PublicFormat.SubjectPublicKeyInfo)
    subjectKeyIdentifier = utils.sha_hash(
        subjectKey)[:160 // 8]  # Use RFC7093, Method 1

    try:
        cert = x509.CertificateBuilder().subject_name(subject).issuer_name(
            issuer).public_key(key.public_key(
            )).serial_number(x509.random_serial_number()).not_valid_before(
                datetime.datetime.utcnow()).not_valid_after(
                    datetime.datetime.utcnow() +
                    datetime.timedelta(days=options.valid_time)).add_extension(
                        x509.KeyUsage(digital_signature=True,
                                      content_commitment=False,
                                      key_encipherment=False,
                                      data_encipherment=False,
                                      key_agreement=False,
                                      key_cert_sign=False,
                                      crl_sign=False,
                                      encipher_only=False,
                                      decipher_only=False),
                        critical=False).add_extension(
                            x509.SubjectAlternativeName(
                                [x509.DNSName(u"localhost")]),
                            critical=False,
                        ).add_extension(
                            x509.ExtendedKeyUsage(
                                [x509.oid.ExtendedKeyUsageOID.CODE_SIGNING]),
                            critical=False,
                        ).add_extension(
                            x509.SubjectKeyIdentifier(subjectKeyIdentifier),
                            critical=False,
                            # Sign our certificate with our private key
                        ).sign(key, hashes.SHA256(), default_backend())
    except ValueError as e:
        LOG.critical('Error creating certificate: {}'.format(e.message))
        fname = options.output_file.name
        options.output_file.close()
        os.remove(fname)
        return 1
    # Write our certificate out to disk.
    options.output_file.write(cert.public_bytes(serialization.Encoding.DER))
    options.output_file.close()
    print(
        '[\033[1;93mWARNING\033[1;0m]: Certificates generated with this tool are self-signed and for testing only',
        file=sys.stderr)
    if options.valid_time < 10 * 365.25:
        print(
            '[\033[1;93mWARNING\033[1;0m]: This certificate is valid for {} days. For production,'
            'use certificates with at least 10 years validity.'.format(
                options.valid_time),
            file=sys.stderr)

    return 0
Esempio n. 9
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