Пример #1
0
 def __init__(self, address, **ssl_args):
     """Create a new client for the syncapi API at `address`."""
     self.address = address
     ssl_args.setdefault('dhparams', dhparams['skip2048'])
     ssl_args.setdefault('ciphers', 'ADH+AES')
     self.ssl_args = ssl_args
     self.connection = None
     logger = logging.getLogger(__name__)
     self.logger = ContextLogger(logger)
     self.crypto = CryptoProvider()
Пример #2
0
 def setup_class(cls):
     super(TestLocator, cls).setup_class()
     sources = platform.get_location_sources()
     if not sources:
         raise SkipTest('No location sources avaialble')
     cls.locator = Locator()
     cls.locator.add_source(sources[0]())
     cls.crypto = CryptoProvider()
Пример #3
0
 def __init__(self, address, **ssl_args):
     """Create a new client for the syncapi API at `address`."""
     self.address = address
     ssl_args.setdefault('dhparams', dhparams['skip2048'])
     ssl_args.setdefault('ciphers', 'ADH+AES')
     self.ssl_args = ssl_args
     self.connection = None
     logger = logging.getLogger(__name__)
     self.logger = ContextLogger(logger)
     self.crypto = CryptoProvider()
Пример #4
0
 def __init__(self, connection):
     """Create a new keyring. Requires a python-tdbus dispatcher as an argument."""
     self.connection = connection
     self.logger = logging.getLogger(
         'bluepass.platform.freedesktop.keyring')
     self.crypto = CryptoProvider()
Пример #5
0
class SecretsKeyring(Keyring):
    """A Keyring interface into the freedesktop "secrets" service.

    This interface is available on Freedesktop platforms (GNOME, KDE).
    """
    def __init__(self, connection):
        """Create a new keyring. Requires a python-tdbus dispatcher as an argument."""
        self.connection = connection
        self.logger = logging.getLogger(
            'bluepass.platform.freedesktop.keyring')
        self.crypto = CryptoProvider()

    def _call_svc(self, path, method, interface, format=None, args=None):
        """INTERNAL: call into the secrets service."""
        try:
            result = self.connection.call_method(path,
                                                 method,
                                                 interface=interface,
                                                 format=format,
                                                 args=args,
                                                 destination=CONN_SERVICE)
        except tdbus.Error as e:
            raise KeyringError('D-BUS error for method "%s": %s' %
                               (method, str(e)))
        return result

    def isavailable(self):
        """Return whether or not we can store a key. This requires the secrets
        service to be available and the login keyring to be unlocked."""
        try:
            reply = self._call_svc(PATH_LOGIN_COLLECTION, 'Get', IFACE_PROPS,
                                   'ss', (IFACE_COLLECTION, 'Locked'))
        except KeyringError:
            self.logger.debug('could not access secrets service')
            return False
        value = reply.get_args()[0]
        if value[0] != 'b':
            raise KeyringError('expecting type "b" for "Locked" property')
        self.logger.debug('login keyring is locked: %s', value[1])
        return not value[1]

    def _open_session(self):
        """INTERNAL: open a session."""
        algo = 'dh-ietf1024-sha256-aes128-cbc-pkcs7'
        params = dhparams['ietf1024']
        keypair = self.crypto.dh_genkey(params)
        reply = self._call_svc(PATH_SERVICE, 'OpenSession', IFACE_SERVICE,
                               'sv', (algo, ('ay', keypair[1])))
        if reply.get_signature() != 'vo':
            raise KeyringError(
                'expecting "vo" reply signature for "OpenSession"')
        output, path = reply.get_args()
        if output[0] != 'ay':
            raise KeyringError(
                'expecting "ay" type for output argument of "OpenSession"')
        pubkey = output[1]
        if not self.crypto.dh_checkkey(params, pubkey):
            raise KeyringError('insecure public key returned by "OpenSession"')
        secret = self.crypto.dh_compute(params, keypair[0], pubkey)
        symkey = self.crypto.hkdf(secret, None, '', 16, 'sha256')
        return path, symkey

    def store(self, key, value):
        """Store a secret in the keyring."""
        session, symkey = self._open_session()
        try:
            attrib = {'application': 'bluepass', 'bluepass-key-id': key}
            props = {
                'org.freedesktop.Secret.Item.Label':
                ('s', 'Bluepass Key: %s' % key),
                'org.freedesktop.Secret.Item.Attributes': ('a{ss}', attrib)
            }
            iv = self.crypto.random(16)
            encrypted = self.crypto.aes_encrypt(value, symkey, iv, 'cbc-pkcs7')
            secret = (session, iv, encrypted, 'text/plain')
            reply = self._call_svc(PATH_LOGIN_COLLECTION, 'CreateItem',
                                   IFACE_COLLECTION, 'a{sv}(oayays)b',
                                   (props, secret, True))
            item, prompt = reply.get_args()
            if item == '/':
                raise KeyringError('not expecting a prompt for "CreateItem"')
            return item
        finally:
            self._call_svc(session, 'Close', IFACE_SESSION)

    def retrieve(self, key):
        """Retrieve a secret from the keyring."""
        session, symkey = self._open_session()
        try:
            attrib = {'application': 'bluepass', 'bluepass-key-id': key}
            reply = self._call_svc(PATH_LOGIN_COLLECTION, 'SearchItems',
                                   IFACE_COLLECTION, 'a{ss}', (attrib, ))
            paths = reply.get_args()[0]
            if len(paths) > 1:
                self.logger.error(
                    'SearchItems returned %d entries for key "%s"' %
                    (len(paths), key))
                return
            elif len(paths) == 0:
                return
            item = paths[0]
            reply = self._call_svc(item, 'GetSecret', IFACE_ITEM, 'o',
                                   (session, ))
            secret = reply.get_args()[0]
            decrypted = self.crypto.aes_decrypt(secret[2], symkey, secret[1],
                                                'cbc-pkcs7')
            return decrypted
        finally:
            self._call_svc(session, 'Close', IFACE_SESSION)
Пример #6
0
 def __init__(self, connection):
     """Create a new keyring. Requires a python-tdbus dispatcher as an argument."""
     self.connection = connection
     self.logger = logging.getLogger('bluepass.platform.freedesktop.keyring')
     self.crypto = CryptoProvider()
Пример #7
0
class SecretsKeyring(Keyring):
    """A Keyring interface into the freedesktop "secrets" service.

    This interface is available on Freedesktop platforms (GNOME, KDE).
    """

    def __init__(self, connection):
        """Create a new keyring. Requires a python-tdbus dispatcher as an argument."""
        self.connection = connection
        self.logger = logging.getLogger('bluepass.platform.freedesktop.keyring')
        self.crypto = CryptoProvider()

    def _call_svc(self, path, method, interface, format=None, args=None):
        """INTERNAL: call into the secrets service."""
        try:
            result = self.connection.call_method(path, method, interface=interface,
                            format=format, args=args, destination=CONN_SERVICE)
        except tdbus.Error as e:
            raise KeyringError('D-BUS error for method "%s": %s' % (method, str(e)))
        return result

    def isavailable(self):
        """Return whether or not we can store a key. This requires the secrets
        service to be available and the login keyring to be unlocked."""
        try:
            reply = self._call_svc(PATH_LOGIN_COLLECTION, 'Get', IFACE_PROPS,
                                   'ss', (IFACE_COLLECTION, 'Locked'))
        except KeyringError:
            self.logger.debug('could not access secrets service')
            return False
        value = reply.get_args()[0]
        if value[0] != 'b':
            raise KeyringError('expecting type "b" for "Locked" property')
        self.logger.debug('login keyring is locked: %s', value[1])
        return not value[1]

    def _open_session(self):
        """INTERNAL: open a session."""
        algo = 'dh-ietf1024-sha256-aes128-cbc-pkcs7'
        params = dhparams['ietf1024']
        keypair = self.crypto.dh_genkey(params)
        reply = self._call_svc(PATH_SERVICE, 'OpenSession', IFACE_SERVICE,
                               'sv', (algo, ('ay', keypair[1])))
        if reply.get_signature() != 'vo':
            raise KeyringError('expecting "vo" reply signature for "OpenSession"')
        output, path = reply.get_args()
        if output[0] != 'ay':
            raise KeyringError('expecting "ay" type for output argument of "OpenSession"')
        pubkey = output[1]
        if not self.crypto.dh_checkkey(params, pubkey):
            raise KeyringError('insecure public key returned by "OpenSession"')
        secret = self.crypto.dh_compute(params, keypair[0], pubkey)
        symkey = self.crypto.hkdf(secret, None, '', 16, 'sha256')
        return path, symkey

    def store(self, key, value):
        """Store a secret in the keyring."""
        session, symkey = self._open_session()
        try:
            attrib = { 'application': 'bluepass', 'bluepass-key-id': key }
            props = { 'org.freedesktop.Secret.Item.Label': ('s', 'Bluepass Key: %s' % key),
                      'org.freedesktop.Secret.Item.Attributes': ('a{ss}', attrib) }
            iv = self.crypto.random(16)
            encrypted = self.crypto.aes_encrypt(value, symkey, iv, 'cbc-pkcs7')
            secret = (session, iv, encrypted, 'text/plain')
            reply = self._call_svc(PATH_LOGIN_COLLECTION, 'CreateItem', IFACE_COLLECTION,
                                   'a{sv}(oayays)b', (props, secret, True))
            item, prompt = reply.get_args()
            if item == '/':
                raise KeyringError('not expecting a prompt for "CreateItem"')
            return item
        finally:
            self._call_svc(session, 'Close', IFACE_SESSION)

    def retrieve(self, key):
        """Retrieve a secret from the keyring."""
        session, symkey = self._open_session()
        try:
            attrib = { 'application': 'bluepass', 'bluepass-key-id': key }
            reply = self._call_svc(PATH_LOGIN_COLLECTION, 'SearchItems', IFACE_COLLECTION,
                                   'a{ss}', (attrib,))
            paths = reply.get_args()[0]
            if len(paths) > 1:
                self.logger.error('SearchItems returned %d entries for key "%s"' % (len(paths), key))
                return
            elif len(paths) == 0:
                return
            item = paths[0]
            reply = self._call_svc(item, 'GetSecret', IFACE_ITEM, 'o', (session,))
            secret = reply.get_args()[0]
            decrypted = self.crypto.aes_decrypt(secret[2], symkey, secret[1], 'cbc-pkcs7')
            return decrypted
        finally:
            self._call_svc(session, 'Close', IFACE_SESSION)
Пример #8
0
 def setup_class(cls):
     super(CryptoTest, cls).setup_class()
     cls.provider = CryptoProvider()
Пример #9
0
 def __init__(self):
     super(SyncAPIApplication, self).__init__()
     self.crypto = CryptoProvider()
     self.allow_pairing = False
     self.key_exchanges = {}
Пример #10
0
class SyncAPIApplication(WSGIApplication):
    """A WSGI application that implements our SyncAPI."""

    def __init__(self):
        super(SyncAPIApplication, self).__init__()
        self.crypto = CryptoProvider()
        self.allow_pairing = False
        self.key_exchanges = {}

    def _do_auth_hmac_cb(self, uuid):
        """Perform mutual HMAC_CB authentication."""
        wwwauth = create_option_header('HMAC_CB', realm=uuid)
        headers = [('WWW-Authenticate', wwwauth)]
        auth = self.environ.get('HTTP_AUTHORIZATION')
        if auth is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        try:
            method, options = parse_option_header(auth)
        except ValueError:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if method != 'HMAC_CB':
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'name' in options:
            # pair step 1 - ask user for permission to pair
            name = options['name']
            if not self.allow_pairing:
                raise HTTPReturn('403 Pairing Disabled')
            bus = instance(MessageBusServer)
            kxid = self.crypto.random(16).encode('hex')
            pin = '%06d' % (self.crypto.randint(bits=31) % 1000000)
            approved = bus.call_method(None, 'get_pairing_approval',
                                       name, uuid, pin, kxid, timeout=60)
            if not approved:
                raise HTTPReturn('403 Approval Denied')
            restrictions = {}
            self.key_exchanges[kxid] = (time.time(), restrictions, pin)
            wwwauth = create_option_header('HMAC_CB', kxid=kxid)
            headers = [('WWW-Authenticate', wwwauth)]
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        elif 'kxid' in options:
            # pair step 2 - check auth and do the actual pairing
            kxid = options['kxid']
            if kxid not in self.key_exchanges:
                raise HTTPReturn(http.FORBIDDEN)
            starttime, restrictions, pin = self.key_exchanges.pop(kxid)
            signature = base64.try_decode(options.get('signature', ''))
            if not signature:
                raise HTTPReturn(http.FORBIDDEN)
            now = time.time()
            if now - starttime > 60:
                raise HTTPReturn('403 Request Timeout')
            cb = self.environ['SSL_CHANNEL_BINDING_TLS_UNIQUE']
            check = self.crypto.hmac(adjust_pin(pin, +1), cb, 'sha1')
            if check != signature:
                raise HTTPReturn('403 Invalid PIN')
            bus = instance(MessageBusServer)
            bus.send_signal(None, 'PairingComplete', kxid)
            # Prove to the other side we also know the PIN
            signature = self.crypto.hmac(adjust_pin(pin, -1), cb, 'sha1')
            signature = base64.encode(signature)
            authinfo = create_option_header('HMAC_CB', kxid=kxid,
                                            signature=signature)
            self.headers.append(('Authentication-Info', authinfo))
        else:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
 
    def _do_auth_rsa_cb(self, uuid):
        """Perform mutual RSA_CB authentication."""
        wwwauth = create_option_header('RSA_CB', realm=uuid)
        headers = [('WWW-Authenticate', wwwauth)]
        auth = self.environ.get('HTTP_AUTHORIZATION')
        if auth  is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        try:
            method, opts = parse_option_header(auth)
        except ValueError:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if method != 'RSA_CB':
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'node' not in opts or not check_uuid4(opts['node']):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'signature' not in opts or not base64.check(opts['signature']):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        model = instance(Model)
        cert = model.get_certificate(uuid, opts['node'])
        if cert is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        signature = base64.decode(opts['signature'])
        pubkey = base64.decode(cert['payload']['keys']['auth']['key'])
        cb = self.environ['SSL_CHANNEL_BINDING_TLS_UNIQUE']
        if not self.crypto.rsa_verify(cb, signature, pubkey, 'pss-sha1'):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        # The peer was authenticated. Authenticate ourselves as well.
        privkey = model.get_auth_key(uuid)
        vault = model.get_vault(uuid)
        node = vault['node']
        signature = self.crypto.rsa_sign(cb, privkey, 'pss-sha1')
        signature = base64.encode(signature)
        auth = create_option_header('RSA_CB', node=node, signature=signature)
        self.headers.append(('Authentication-Info', auth))

    @expose('/api/vaults/:vault/pair', method='POST')
    def pair(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if not vault:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_hmac_cb(uuid)
        # Sign the certificate request that was sent to tus
        certinfo = self.entity
        if not certinfo or not isinstance(certinfo, dict):
            raise HTTPReturn(http.BAD_REQUEST)
        model.add_certificate(uuid, certinfo)
        # And send our own certificate request in return
        certinfo = { 'node': vault['node'], 'name': socket.gethostname() }
        certkeys = certinfo['keys'] = {}
        for key in vault['keys']:
            certkeys[key] = { 'key': vault['keys'][key]['public'],
                              'keytype': vault['keys'][key]['keytype'] }
        return certinfo

    @expose('/api/vaults/:vault/items', method='GET')
    def sync_outbound(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if vault is None:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_rsa_cb(uuid)
        args = parse_qs(env.get('QUERY_STRING', ''))
        vector = args.get('vector', [''])[0]
        if vector:
            try:
                vector = parse_vector(vector)
            except ValueError:
                raise HTTPReturn(http.BAD_REQUEST)
        items = model.get_items(uuid, vector)
        myvector = model.get_vector(uuid)
        self.headers.append(('X-Vector', dump_vector(myvector)))
        return items

    @expose('/api/vaults/:vault/items', method='POST')
    def sync_inbound(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if vault is None:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_rsa_cb(uuid)
        items = self.entity
        if items is None or not isinstance(items, list):
            raise HTTPReturn(http.BAD_REQUEST)
        model.import_items(uuid, items)
Пример #11
0
class SyncAPIClient(object):
    """
    SyncAPI client.

    This classs implements a client to the Bluepass HTTP based synchronization
    API. The two main functions are pairing (pair_step1() and pair_step2())
    and synchronization (sync()).
    """

    def __init__(self, address, **ssl_args):
        """Create a new client for the syncapi API at `address`."""
        self.address = address
        ssl_args.setdefault('dhparams', dhparams['skip2048'])
        ssl_args.setdefault('ciphers', 'ADH+AES')
        self.ssl_args = ssl_args
        self.connection = None
        logger = logging.getLogger(__name__)
        self.logger = ContextLogger(logger)
        self.crypto = CryptoProvider()

    def _make_request(self, method, url, headers=None, body=None):
        """Make an HTTP request to the API.
        
        This returns the HTTPResponse object on success, or None on failure.
        """
        logger = self.logger
        if headers is None:
            headers = []
        headers.append(('User-Agent', 'Bluepass/%s' % _version.version))
        headers.append(('Accept', 'text/json'))
        if body is None:
            body = ''
        else:
            body = json.dumps(body)
            headers.append(('Content-Type', 'text/json'))
        connection = self.connection
        assert connection is not None
        try:
            logger.debug('client request: %s %s', method, url)
            connection.request(method, url, body, dict(headers))
            response = connection.getresponse()
            headers = response.getheaders()
            body = response.read()
        except (socket.error, HTTPException) as e:
            logger.error('error when making HTTP request: %s', str(e))
            return
        ctype = response.getheader('Content-Type')
        if ctype == 'text/json':
            parsed = json.try_loads(body)
            if parsed is None:
                logger.error('response body contains invalid JSON')
                return
            response.entity = parsed
            logger.debug('parsed "%s" request body (%d bytes)', ctype, len(body))
        else:
            response.entity = None
        return response

    def connect(self):
        """Connect to the remote syncapi."""
        ssl_args = { 'dhparams': dhparams['skip2048'], 'ciphers': 'ADH+AES' }
        # Support both dict style addresses for arbitrary address families,
        # as well as (host, port) tuples for IPv4.
        if isinstance(self.address, dict):
            host = self.address['host']; port = None
            sockinfo = self.address
        else:
            host, port = self.address
            sockinfo = None
        connection = HTTPSConnection(host, port, sockinfo=sockinfo, **ssl_args)
        try:
            connection.connect()
        except socket.error as e:
            self.logger.error('could not connect to %s:%d' % self.address)
            raise SyncAPIError('RemoteError', 'Could not connect')
        self.connection = connection

    def close(self):
        """Close the connection."""
        if self.connection is not None:
            try:
                conection.close()
            except Exception:
                pass
        self.connection = None

    def _get_hmac_cb_auth(self, kxid, pin):
        """Return the headers for a client to server HMAC_CB auth."""
        cb = self.connection.sock.get_channel_binding('tls-unique')
        signature = self.crypto.hmac(adjust_pin(pin, +1), cb, 'sha1')
        signature = base64.encode(signature)
        auth = create_option_header('HMAC_CB', kxid=kxid, signature=signature)
        headers = [('Authorization', auth)]
        return headers

    def _check_hmac_cb_auth(self, response, pin):
        """Check a server to client HMAC_CB auth."""
        logger = self.logger
        authinfo = response.getheader('Authentication-Info', '')
        try:
            method, options = parse_option_header(authinfo)
        except ValueError:
            logger.error('illegal Authentication-Info header: %s', authinfo)
            return False
        if 'signature' not in options or not base64.check(options['signature']):
            logger.error('illegal Authentication-Info header: %s', authinfo)
            return False
        signature = base64.decode(options['signature'])
        cb = self.connection.sock.get_channel_binding('tls-unique')
        check = self.crypto.hmac(adjust_pin(pin, -1), cb, 'sha1')
        if check != signature:
            logger.error('HMAC_CB signature did not match')
            return False
        return True

    def pair_step1(self, uuid, name):
        """Perform step 1 in a pairing exchange.
        
        If succesful, this returns a key exchange ID. On error, a SyncAPIError
        exception is raised.
        """
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('pair step #1')
        url = '/api/vaults/%s/pair' % uuid
        headers = [('Authorization', 'HMAC_CB name=%s' % name)]
        response = self._make_request('POST', url, headers)
        if response is None:
            raise SyncAPIError('RemoteError', 'Could not make HTTP request')
        status = response.status
        if status != 401:
            logger.error('expecting HTTP status 401 (got: %s)', status)
            raise SyncAPIError('RemoteError', response.reason)
        wwwauth = response.getheader('WWW-Authenticate', '')
        try:
            method, options = parse_option_header(wwwauth)
        except ValueError:
            raise SyncAPIError('RemoteError', 'Illegal response')
        if method != 'HMAC_CB' or 'kxid' not in options:
            logger.error('illegal WWW-Authenticate header: %s', wwwauth)
            raise SyncAPIError('RemoteError', 'Illegal response')
        return options['kxid']

    def pair_step2(self, uuid, kxid, pin, certinfo):
        """Perform step 2 in pairing exchange.
        
        If successfull, this returns the peer certificate. On error, a
        SyncAPIError is raised.
        """
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('pair step #2')
        url = '/api/vaults/%s/pair' % uuid
        headers = self._get_hmac_cb_auth(kxid, pin)
        response = self._make_request('POST', url, headers, certinfo)
        if response is None:
            raise SyncAPIError('RemoteError', 'Could not make syncapi request')
        status = response.status
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', response.reason)
        if not self._check_hmac_cb_auth(response, pin):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        peercert = response.entity
        if peercert is None or not isinstance(peercert, dict):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        return peercert

    def _get_rsa_cb_auth(self, uuid, model):
        """Return the headers for RSA_CB authentication."""
        cb = self.connection.sock.get_channel_binding('tls-unique')
        privkey = model.get_auth_key(uuid)
        assert privkey is not None
        signature = self.crypto.rsa_sign(cb, privkey, 'pss-sha1')
        signature = base64.encode(signature)
        vault = model.get_vault(uuid)
        auth = create_option_header('RSA_CB', node=vault['node'], signature=signature)
        headers = [('Authorization', auth)]
        return headers

    def _check_rsa_cb_auth(self, uuid, response, model):
        """Verify RSA_CB authentication."""
        logger = self.logger
        authinfo = response.getheader('Authentication-Info', '')
        try:
            method, options = parse_option_header(authinfo)
        except ValueError:
            logger.error('illegal Authentication-Info header')
            return False
        if 'signature' not in options or 'node' not in options \
                or not base64.check(options['signature']) \
                or not check_uuid4(options['node']):
            logger.error('illegal Authentication-Info header')
            return False
        cb = self.connection.sock.get_channel_binding('tls-unique')
        signature = base64.decode(options['signature'])
        cert = model.get_certificate(uuid, options['node'])
        if cert is None:
            logger.error('unknown node in RSA_CB authentication', node)
            return False
        pubkey = base64.decode(cert['payload']['keys']['auth']['key'])
        try:
            status = self.crypto.rsa_verify(cb, signature, pubkey, 'pss-sha1')
        except CryptoError:
            logger.error('corrupt RSA_CB signature')
            return False
        if not status:
            logger.error('RSA_CB signature did not match')
        return status

    def sync(self, uuid, model, notify=True):
        """Synchronize vault `uuid` with the remote peer."""
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('sync')
        vault = model.get_vault(uuid)
        if vault is None:
            raise SyncAPIError('NotFound', 'Vault not found')
        vector = model.get_vector(uuid)
        vector = dump_vector(vector)
        url = '/api/vaults/%s/items?vector=%s' % (vault['id'], vector)
        headers = self._get_rsa_cb_auth(uuid, model)
        response = self._make_request('GET', url, headers)
        if not response:
            raise SyncAPIError('RemoteError', 'Could not make HTTP request')
        status = response.status
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if not self._check_rsa_cb_auth(uuid, response, model):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        initems = response.entity
        if initems is None or not isinstance(initems, list):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        nitems = model.import_items(uuid, initems, notify=notify)
        logger.debug('imported %d items into model', nitems)
        vector = response.getheader('X-Vector', '')
        try:
            vector = parse_vector(vector)
        except ValueError as e:
            logger.error('illegal X-Vector header: %s (%s)', vector, str(e))
            raise SyncAPIError('RemoteError', 'Invalid response')
        outitems = model.get_items(uuid, vector)
        url = '/api/vaults/%s/items' % uuid
        response = self._make_request('POST', url, headers, outitems)
        if not response:
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if not self._check_rsa_cb_auth(uuid, response, model):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        logger.debug('succesfully retrieved %d items from peer', len(initems))
        logger.debug('succesfully pushed %d items to peer', len(outitems))
        return len(initems) + len(outitems)
Пример #12
0
 def __init__(self):
     super(SyncAPIApplication, self).__init__()
     self.crypto = CryptoProvider()
     self.allow_pairing = False
     self.key_exchanges = {}
Пример #13
0
class SyncAPIApplication(WSGIApplication):
    """A WSGI application that implements our SyncAPI."""
    def __init__(self):
        super(SyncAPIApplication, self).__init__()
        self.crypto = CryptoProvider()
        self.allow_pairing = False
        self.key_exchanges = {}

    def _do_auth_hmac_cb(self, uuid):
        """Perform mutual HMAC_CB authentication."""
        wwwauth = create_option_header('HMAC_CB', realm=uuid)
        headers = [('WWW-Authenticate', wwwauth)]
        auth = self.environ.get('HTTP_AUTHORIZATION')
        if auth is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        try:
            method, options = parse_option_header(auth)
        except ValueError:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if method != 'HMAC_CB':
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'name' in options:
            # pair step 1 - ask user for permission to pair
            name = options['name']
            if not self.allow_pairing:
                raise HTTPReturn('403 Pairing Disabled')
            bus = instance(MessageBusServer)
            kxid = self.crypto.random(16).encode('hex')
            pin = '%06d' % (self.crypto.randint(bits=31) % 1000000)
            approved = bus.call_method(None,
                                       'get_pairing_approval',
                                       name,
                                       uuid,
                                       pin,
                                       kxid,
                                       timeout=60)
            if not approved:
                raise HTTPReturn('403 Approval Denied')
            restrictions = {}
            self.key_exchanges[kxid] = (time.time(), restrictions, pin)
            wwwauth = create_option_header('HMAC_CB', kxid=kxid)
            headers = [('WWW-Authenticate', wwwauth)]
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        elif 'kxid' in options:
            # pair step 2 - check auth and do the actual pairing
            kxid = options['kxid']
            if kxid not in self.key_exchanges:
                raise HTTPReturn(http.FORBIDDEN)
            starttime, restrictions, pin = self.key_exchanges.pop(kxid)
            signature = base64.try_decode(options.get('signature', ''))
            if not signature:
                raise HTTPReturn(http.FORBIDDEN)
            now = time.time()
            if now - starttime > 60:
                raise HTTPReturn('403 Request Timeout')
            cb = self.environ['SSL_CHANNEL_BINDING_TLS_UNIQUE']
            check = self.crypto.hmac(adjust_pin(pin, +1), cb, 'sha1')
            if check != signature:
                raise HTTPReturn('403 Invalid PIN')
            bus = instance(MessageBusServer)
            bus.send_signal(None, 'PairingComplete', kxid)
            # Prove to the other side we also know the PIN
            signature = self.crypto.hmac(adjust_pin(pin, -1), cb, 'sha1')
            signature = base64.encode(signature)
            authinfo = create_option_header('HMAC_CB',
                                            kxid=kxid,
                                            signature=signature)
            self.headers.append(('Authentication-Info', authinfo))
        else:
            raise HTTPReturn(http.UNAUTHORIZED, headers)

    def _do_auth_rsa_cb(self, uuid):
        """Perform mutual RSA_CB authentication."""
        wwwauth = create_option_header('RSA_CB', realm=uuid)
        headers = [('WWW-Authenticate', wwwauth)]
        auth = self.environ.get('HTTP_AUTHORIZATION')
        if auth is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        try:
            method, opts = parse_option_header(auth)
        except ValueError:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if method != 'RSA_CB':
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'node' not in opts or not check_uuid4(opts['node']):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'signature' not in opts or not base64.check(opts['signature']):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        model = instance(Model)
        cert = model.get_certificate(uuid, opts['node'])
        if cert is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        signature = base64.decode(opts['signature'])
        pubkey = base64.decode(cert['payload']['keys']['auth']['key'])
        cb = self.environ['SSL_CHANNEL_BINDING_TLS_UNIQUE']
        if not self.crypto.rsa_verify(cb, signature, pubkey, 'pss-sha1'):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        # The peer was authenticated. Authenticate ourselves as well.
        privkey = model.get_auth_key(uuid)
        vault = model.get_vault(uuid)
        node = vault['node']
        signature = self.crypto.rsa_sign(cb, privkey, 'pss-sha1')
        signature = base64.encode(signature)
        auth = create_option_header('RSA_CB', node=node, signature=signature)
        self.headers.append(('Authentication-Info', auth))

    @expose('/api/vaults/:vault/pair', method='POST')
    def pair(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if not vault:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_hmac_cb(uuid)
        # Sign the certificate request that was sent to tus
        certinfo = self.entity
        if not certinfo or not isinstance(certinfo, dict):
            raise HTTPReturn(http.BAD_REQUEST)
        model.add_certificate(uuid, certinfo)
        # And send our own certificate request in return
        certinfo = {'node': vault['node'], 'name': socket.gethostname()}
        certkeys = certinfo['keys'] = {}
        for key in vault['keys']:
            certkeys[key] = {
                'key': vault['keys'][key]['public'],
                'keytype': vault['keys'][key]['keytype']
            }
        return certinfo

    @expose('/api/vaults/:vault/items', method='GET')
    def sync_outbound(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if vault is None:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_rsa_cb(uuid)
        args = parse_qs(env.get('QUERY_STRING', ''))
        vector = args.get('vector', [''])[0]
        if vector:
            try:
                vector = parse_vector(vector)
            except ValueError:
                raise HTTPReturn(http.BAD_REQUEST)
        items = model.get_items(uuid, vector)
        myvector = model.get_vector(uuid)
        self.headers.append(('X-Vector', dump_vector(myvector)))
        return items

    @expose('/api/vaults/:vault/items', method='POST')
    def sync_inbound(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if vault is None:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_rsa_cb(uuid)
        items = self.entity
        if items is None or not isinstance(items, list):
            raise HTTPReturn(http.BAD_REQUEST)
        model.import_items(uuid, items)
Пример #14
0
class SyncAPIClient(object):
    """
    SyncAPI client.

    This classs implements a client to the Bluepass HTTP based synchronization
    API. The two main functions are pairing (pair_step1() and pair_step2())
    and synchronization (sync()).
    """
    def __init__(self, address, **ssl_args):
        """Create a new client for the syncapi API at `address`."""
        self.address = address
        ssl_args.setdefault('dhparams', dhparams['skip2048'])
        ssl_args.setdefault('ciphers', 'ADH+AES')
        self.ssl_args = ssl_args
        self.connection = None
        logger = logging.getLogger(__name__)
        self.logger = ContextLogger(logger)
        self.crypto = CryptoProvider()

    def _make_request(self, method, url, headers=None, body=None):
        """Make an HTTP request to the API.
        
        This returns the HTTPResponse object on success, or None on failure.
        """
        logger = self.logger
        if headers is None:
            headers = []
        headers.append(('User-Agent', 'Bluepass/%s' % _version.version))
        headers.append(('Accept', 'text/json'))
        if body is None:
            body = ''
        else:
            body = json.dumps(body)
            headers.append(('Content-Type', 'text/json'))
        connection = self.connection
        assert connection is not None
        try:
            logger.debug('client request: %s %s', method, url)
            connection.request(method, url, body, dict(headers))
            response = connection.getresponse()
            headers = response.getheaders()
            body = response.read()
        except (socket.error, HTTPException) as e:
            logger.error('error when making HTTP request: %s', str(e))
            return
        ctype = response.getheader('Content-Type')
        if ctype == 'text/json':
            parsed = json.try_loads(body)
            if parsed is None:
                logger.error('response body contains invalid JSON')
                return
            response.entity = parsed
            logger.debug('parsed "%s" request body (%d bytes)', ctype,
                         len(body))
        else:
            response.entity = None
        return response

    def connect(self):
        """Connect to the remote syncapi."""
        ssl_args = {'dhparams': dhparams['skip2048'], 'ciphers': 'ADH+AES'}
        # Support both dict style addresses for arbitrary address families,
        # as well as (host, port) tuples for IPv4.
        if isinstance(self.address, dict):
            host = self.address['host']
            port = None
            sockinfo = self.address
        else:
            host, port = self.address
            sockinfo = None
        connection = HTTPSConnection(host, port, sockinfo=sockinfo, **ssl_args)
        try:
            connection.connect()
        except socket.error as e:
            self.logger.error('could not connect to %s:%d' % self.address)
            raise SyncAPIError('RemoteError', 'Could not connect')
        self.connection = connection

    def close(self):
        """Close the connection."""
        if self.connection is not None:
            try:
                conection.close()
            except Exception:
                pass
        self.connection = None

    def _get_hmac_cb_auth(self, kxid, pin):
        """Return the headers for a client to server HMAC_CB auth."""
        cb = self.connection.sock.get_channel_binding('tls-unique')
        signature = self.crypto.hmac(adjust_pin(pin, +1), cb, 'sha1')
        signature = base64.encode(signature)
        auth = create_option_header('HMAC_CB', kxid=kxid, signature=signature)
        headers = [('Authorization', auth)]
        return headers

    def _check_hmac_cb_auth(self, response, pin):
        """Check a server to client HMAC_CB auth."""
        logger = self.logger
        authinfo = response.getheader('Authentication-Info', '')
        try:
            method, options = parse_option_header(authinfo)
        except ValueError:
            logger.error('illegal Authentication-Info header: %s', authinfo)
            return False
        if 'signature' not in options or not base64.check(
                options['signature']):
            logger.error('illegal Authentication-Info header: %s', authinfo)
            return False
        signature = base64.decode(options['signature'])
        cb = self.connection.sock.get_channel_binding('tls-unique')
        check = self.crypto.hmac(adjust_pin(pin, -1), cb, 'sha1')
        if check != signature:
            logger.error('HMAC_CB signature did not match')
            return False
        return True

    def pair_step1(self, uuid, name):
        """Perform step 1 in a pairing exchange.
        
        If succesful, this returns a key exchange ID. On error, a SyncAPIError
        exception is raised.
        """
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('pair step #1')
        url = '/api/vaults/%s/pair' % uuid
        headers = [('Authorization', 'HMAC_CB name=%s' % name)]
        response = self._make_request('POST', url, headers)
        if response is None:
            raise SyncAPIError('RemoteError', 'Could not make HTTP request')
        status = response.status
        if status != 401:
            logger.error('expecting HTTP status 401 (got: %s)', status)
            raise SyncAPIError('RemoteError', response.reason)
        wwwauth = response.getheader('WWW-Authenticate', '')
        try:
            method, options = parse_option_header(wwwauth)
        except ValueError:
            raise SyncAPIError('RemoteError', 'Illegal response')
        if method != 'HMAC_CB' or 'kxid' not in options:
            logger.error('illegal WWW-Authenticate header: %s', wwwauth)
            raise SyncAPIError('RemoteError', 'Illegal response')
        return options['kxid']

    def pair_step2(self, uuid, kxid, pin, certinfo):
        """Perform step 2 in pairing exchange.
        
        If successfull, this returns the peer certificate. On error, a
        SyncAPIError is raised.
        """
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('pair step #2')
        url = '/api/vaults/%s/pair' % uuid
        headers = self._get_hmac_cb_auth(kxid, pin)
        response = self._make_request('POST', url, headers, certinfo)
        if response is None:
            raise SyncAPIError('RemoteError', 'Could not make syncapi request')
        status = response.status
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', response.reason)
        if not self._check_hmac_cb_auth(response, pin):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        peercert = response.entity
        if peercert is None or not isinstance(peercert, dict):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        return peercert

    def _get_rsa_cb_auth(self, uuid, model):
        """Return the headers for RSA_CB authentication."""
        cb = self.connection.sock.get_channel_binding('tls-unique')
        privkey = model.get_auth_key(uuid)
        assert privkey is not None
        signature = self.crypto.rsa_sign(cb, privkey, 'pss-sha1')
        signature = base64.encode(signature)
        vault = model.get_vault(uuid)
        auth = create_option_header('RSA_CB',
                                    node=vault['node'],
                                    signature=signature)
        headers = [('Authorization', auth)]
        return headers

    def _check_rsa_cb_auth(self, uuid, response, model):
        """Verify RSA_CB authentication."""
        logger = self.logger
        authinfo = response.getheader('Authentication-Info', '')
        try:
            method, options = parse_option_header(authinfo)
        except ValueError:
            logger.error('illegal Authentication-Info header')
            return False
        if 'signature' not in options or 'node' not in options \
                or not base64.check(options['signature']) \
                or not check_uuid4(options['node']):
            logger.error('illegal Authentication-Info header')
            return False
        cb = self.connection.sock.get_channel_binding('tls-unique')
        signature = base64.decode(options['signature'])
        cert = model.get_certificate(uuid, options['node'])
        if cert is None:
            logger.error('unknown node in RSA_CB authentication', node)
            return False
        pubkey = base64.decode(cert['payload']['keys']['auth']['key'])
        try:
            status = self.crypto.rsa_verify(cb, signature, pubkey, 'pss-sha1')
        except CryptoError:
            logger.error('corrupt RSA_CB signature')
            return False
        if not status:
            logger.error('RSA_CB signature did not match')
        return status

    def sync(self, uuid, model, notify=True):
        """Synchronize vault `uuid` with the remote peer."""
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('sync')
        vault = model.get_vault(uuid)
        if vault is None:
            raise SyncAPIError('NotFound', 'Vault not found')
        vector = model.get_vector(uuid)
        vector = dump_vector(vector)
        url = '/api/vaults/%s/items?vector=%s' % (vault['id'], vector)
        headers = self._get_rsa_cb_auth(uuid, model)
        response = self._make_request('GET', url, headers)
        if not response:
            raise SyncAPIError('RemoteError', 'Could not make HTTP request')
        status = response.status
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if not self._check_rsa_cb_auth(uuid, response, model):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        initems = response.entity
        if initems is None or not isinstance(initems, list):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        nitems = model.import_items(uuid, initems, notify=notify)
        logger.debug('imported %d items into model', nitems)
        vector = response.getheader('X-Vector', '')
        try:
            vector = parse_vector(vector)
        except ValueError as e:
            logger.error('illegal X-Vector header: %s (%s)', vector, str(e))
            raise SyncAPIError('RemoteError', 'Invalid response')
        outitems = model.get_items(uuid, vector)
        url = '/api/vaults/%s/items' % uuid
        response = self._make_request('POST', url, headers, outitems)
        if not response:
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if not self._check_rsa_cb_auth(uuid, response, model):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        logger.debug('succesfully retrieved %d items from peer', len(initems))
        logger.debug('succesfully pushed %d items to peer', len(outitems))
        return len(initems) + len(outitems)