Example #1
0
    def test_request_options(self):
        o = PublicKeyCredentialRequestOptions(
            b"request_challenge",
            10000,
            "example.com",
            [{
                "type": "public-key",
                "id": b"credential_id"
            }],
            "discouraged",
        )
        self.assertEqual(o.challenge, b"request_challenge")
        self.assertEqual(o.rp_id, "example.com")
        self.assertEqual(o.timeout, 10000)
        self.assertIsNone(o.extensions)

        o = PublicKeyCredentialRequestOptions(b"request_challenge")
        self.assertIsNone(o.timeout)
        self.assertIsNone(o.rp_id)
        self.assertIsNone(o.allow_credentials)
        self.assertIsNone(o.user_verification)

        self.assertIsNone(
            PublicKeyCredentialRequestOptions(
                b"request_challenge",
                user_verification="invalid").user_verification)
Example #2
0
 def auth_func():
     nonlocal response
     nonlocal rq_options
     attempt = 0
     while attempt < 2:
         attempt += 1
         try:
             rs = client.get_assertion(rq_options, event=evt)
             response = rs.get_response(0)
             break
         except ClientError as err:
             if isinstance(err.cause, CtapError) and attempt == 1:
                 if err.cause.code == CtapError.ERR.NO_CREDENTIALS:
                     print(
                         '\n\nKeeper Security stopped supporting U2F security keys starting February 2022.\n'
                         'If you registered your security key prior to this date please re-register it within the Web Vault.\n'
                         'For information on using security keys with Keeper see the documentation: \n'
                         'https://docs.keeper.io/enterprise-guide/two-factor-authentication#security-keys-fido-webauthn\n'
                         'Commander will use the fallback security key authentication method.\n\n'
                         'To use your Yubikey with Commander, please touch the flashing Security key one more time.\n'
                     )
                     rq_options = PublicKeyCredentialRequestOptions(
                         utils.base64_url_decode(options['challenge']),
                         rp_id=origin,
                         user_verification='discouraged',
                         allow_credentials=credentials)
                     continue
             raise err
Example #3
0
 def work(self, client):
     try:
         pin = None
         if client.info.options.get("clientPin"):
             # Prompt for PIN if needed
             pin = getpass("Please enter PIN: ")
         request_options = PublicKeyCredentialRequestOptions(
             challenge=utils.websafe_decode(self._challenge),
             rp_id=self._rp['id'],
             allow_credentials=self._allow_list)
         self._assertions, self._client_data = client.get_assertion(
             request_options,
             on_keepalive=self.on_keepalive,
             event=self._cancel,
             pin=pin)
     except ClientError as e:
         if e.code == ClientError.ERR.DEVICE_INELIGIBLE:
             self.ui.info(
                 'Security key is ineligible')  # TODO extract key info
             return
         elif e.code != ClientError.ERR.TIMEOUT:
             raise
         else:
             return
     self._cancel.set()
Example #4
0
def get_secret(client, credential_id, password, host="localhost", silent=False):
    credential_id = binascii.a2b_hex(credential_id)

    allow_list = [{"type": "public-key", "id": credential_id}]

    challenge = secrets.token_bytes(32)

    h = hashlib.sha256()
    h.update(password)
    salt = h.digest()

    options = PublicKeyCredentialRequestOptions(
        challenge,
        30000,
        host,
        allow_list,
        extensions={"hmacGetSecret": {"salt1": salt}}
    )

    if not silent:
      print("Please press security key to get assertion")

    response = client.get_assertion(options).get_response(0)

    return response.extension_results["hmacGetSecret"]["output1"]
Example #5
0
    def _verify(self, client):
        try:
            user_verification = self._get_user_verification_requirement_from_client(
                client)
            options = PublicKeyCredentialRequestOptions(
                challenge=self._challenge,
                rp_id=self._rp['id'],
                allow_credentials=self._allow_list,
                timeout=self._timeout_ms,
                user_verification=user_verification)

            pin = self._get_pin_from_client(client)
            assertion_selection = client.get_assertion(
                options,
                event=self._event,
                on_keepalive=self.on_keepalive,
                pin=pin)
            self._assertions = assertion_selection.get_assertions()
            assert len(self._assertions) >= 0

            assertion_res = assertion_selection.get_response(0)
            self._client_data = assertion_res.client_data
            self._event.set()
        except ClientError as e:
            if e.code == ClientError.ERR.DEVICE_INELIGIBLE:
                self.ui.info(
                    'Security key is ineligible')  # TODO extract key info
                return

            elif e.code != ClientError.ERR.TIMEOUT:
                raise

            else:
                return
Example #6
0
    def test_webauthn_success(self):
        webauthn = factor.WebauthnFactor("foobar")

        # Mock out the webauthn device
        mock_device = mock.MagicMock(name="mock_device")
        webauthn._get_devices = mock.MagicMock(name="_get_devices")
        webauthn._get_devices.return_value = [mock_device].__iter__()

        # Mock out webauthn client
        mock_client = mock.MagicMock(name="mock_client")
        webauthn._get_client = mock.MagicMock(name="_get_client")
        webauthn._get_client.return_value = mock_client

        assertions = [dotdict({"auth_data": b"foo", "signature": b"bar"})]
        client_data = b"baz"
        mock_client.get_assertion.return_value = (assertions, client_data)

        # Mock call to Okta API
        webauthn._request = mock.MagicMock(name="_request")
        webauthn._request.side_effect = [
            CHALLENGE_RESPONSE,
            SUCCESS_RESPONSE,
        ]

        # Run code
        ret = webauthn.verify("123", "XXXTOKENXXX", 0.1)

        # Check results
        self.assertEqual(ret, SUCCESS_RESPONSE)

        webauthn._get_client.assert_called_once_with(
            mock_device, "https://foobar"
            ".okta.com")

        allow_list = [{"type": "public-key", "id": CREDENTIAL_ID_STR}]

        options = PublicKeyCredentialRequestOptions(
            challenge=NONCE_STR,
            rp_id="foobar.okta.com",
            allow_credentials=allow_list)

        mock_client.get_assertion.assert_called_once_with(options)

        calls = [
            mock.call("/authn/factors/123/verify", {
                "fid": "123",
                "stateToken": "XXXTOKENXXX"
            }),
            mock.call(
                "/authn/factors/123/verify",
                {
                    "stateToken": "XXXTOKENXXX",
                    "clientData": "YmF6",
                    "signatureData": "YmFy",
                    "authenticatorData": "Zm9v",
                },
            ),
        ]
        webauthn._request.assert_has_calls(calls)
Example #7
0
    def test_webauthn_success(self):
        webauthn = factor.WebauthnFactor('foobar')

        # Mock out the webauthn device
        mock_device = mock.MagicMock(name='mock_device')
        webauthn._get_devices = mock.MagicMock(name='_get_devices')
        webauthn._get_devices.return_value = [mock_device].__iter__()

        # Mock out webauthn client
        mock_client = mock.MagicMock(name='mock_client')
        webauthn._get_client = mock.MagicMock(name='_get_client')
        webauthn._get_client.return_value = mock_client

        assertions = [dotdict({'auth_data': b'foo', 'signature': b'bar'})]
        client_data = b'baz'
        mock_client.get_assertion.return_value = (assertions, client_data)

        # Mock call to Okta API
        webauthn._request = mock.MagicMock(name='_request')
        webauthn._request.side_effect = [
            CHALLENGE_RESPONSE,
            SUCCESS_RESPONSE,
        ]

        # Run code
        ret = webauthn.verify('123', 'XXXTOKENXXX', 0.1)

        # Check results
        self.assertEqual(ret, SUCCESS_RESPONSE)

        webauthn._get_client.assert_called_once_with(
            mock_device, 'https://foobar'
            '.okta.com')

        allow_list = [{'type': 'public-key', 'id': CREDENTIAL_ID_STR}]

        options = PublicKeyCredentialRequestOptions(
            challenge=NONCE_STR,
            rp_id='foobar.okta.com',
            allow_credentials=allow_list)

        mock_client.get_assertion.assert_called_once_with(options)

        calls = [
            mock.call('/authn/factors/123/verify', {
                'fid': '123',
                'stateToken': 'XXXTOKENXXX'
            }),
            mock.call(
                '/authn/factors/123/verify', {
                    'stateToken': 'XXXTOKENXXX',
                    'clientData': 'YmF6',
                    'signatureData': 'YmFy',
                    'authenticatorData': 'Zm9v'
                })
        ]
        webauthn._request.assert_has_calls(calls)
Example #8
0
def simple_secret(
    credential_id,
    secret_input,
    host="solokeys.dev",
    user_id="they",
    serial=None,
    pin=None,
    prompt="Touch your authenticator to generate a response...",
    output=True,
    udp=False,
):
    user_id = user_id.encode()

    client = solo.client.find(solo_serial=serial, udp=udp).client
    hmac_ext = HmacSecretExtension(client.ctap2)

    # rp = {"id": host, "name": "Example RP"}
    client.host = host
    client.origin = f"https://{client.host}"
    client.user_id = user_id
    # user = {"id": user_id, "name": "A. User"}
    credential_id = binascii.a2b_hex(credential_id)

    allow_list = [{"type": "public-key", "id": credential_id}]

    challenge = secrets.token_bytes(32)

    h = hashlib.sha256()
    h.update(secret_input.encode())
    salt = h.digest()

    if prompt:
        print(prompt)

    options = PublicKeyCredentialRequestOptions(
        challenge, 30000, host, allow_list, extensions=hmac_ext.get_dict(salt)
    )
    assertions, client_data = client.get_assertion(options, pin=pin)

    assertion = assertions[0]  # Only one cred in allowList, only one response.
    response = hmac_ext.results_for(assertion.auth_data)[0]
    if output:
        print(response.hex())

    return response
Example #9
0
def yubikey_authenticate(request):  # type: (dict) -> Optional[dict]
    auth_func = None  # type: Optional[Callable[[], Union[AuthenticatorAssertionResponse, dict, None]]]
    evt = threading.Event()
    response = None  # type: Optional[str]

    if 'authenticateRequests' in request:  # U2F

        options = request['authenticateRequests']
        origin = options[0].get('appId') or ''
        challenge = options[0]['challenge']
        keys = [{
            'version': x.get('version') or '',
            'keyHandle': x['keyHandle']
        } for x in options if 'keyHandle' in x]

        dev = next(CtapHidDevice.list_devices(), None)
        if not dev:
            logging.warning("No Security Key detected")
            return
        client = U2fClient(dev, origin)

        def auth_func():
            nonlocal response
            response = client.sign(origin, challenge, keys, event=evt)

    elif 'publicKeyCredentialRequestOptions' in request:  # WebAuthN
        origin = ''
        options = request['publicKeyCredentialRequestOptions']
        if 'extensions' in options:
            extensions = options['extensions']
            origin = extensions.get('appid') or ''

        credentials = options.get('allowCredentials') or []
        for c in credentials:
            if isinstance(c.get('id'), str):
                c['id'] = utils.base64_url_decode(c['id'])

        rq_options = PublicKeyCredentialRequestOptions(
            utils.base64_url_decode(options['challenge']),
            rp_id=options['rpId'],
            user_verification='discouraged',
            allow_credentials=credentials)

        if WindowsClient.is_available():
            client = WindowsClient(origin, verify=verify_rp_id_none)
        else:
            dev = next(CtapHidDevice.list_devices(), None)
            if not dev:
                logging.warning("No Security Key detected")
                return
            client = Fido2Client(dev, origin, verify=verify_rp_id_none)

        def auth_func():
            nonlocal response
            nonlocal rq_options
            attempt = 0
            while attempt < 2:
                attempt += 1
                try:
                    rs = client.get_assertion(rq_options, event=evt)
                    response = rs.get_response(0)
                    break
                except ClientError as err:
                    if isinstance(err.cause, CtapError) and attempt == 1:
                        if err.cause.code == CtapError.ERR.NO_CREDENTIALS:
                            print(
                                '\n\nKeeper Security stopped supporting U2F security keys starting February 2022.\n'
                                'If you registered your security key prior to this date please re-register it within the Web Vault.\n'
                                'For information on using security keys with Keeper see the documentation: \n'
                                'https://docs.keeper.io/enterprise-guide/two-factor-authentication#security-keys-fido-webauthn\n'
                                'Commander will use the fallback security key authentication method.\n\n'
                                'To use your Yubikey with Commander, please touch the flashing Security key one more time.\n'
                            )
                            rq_options = PublicKeyCredentialRequestOptions(
                                utils.base64_url_decode(options['challenge']),
                                rp_id=origin,
                                user_verification='discouraged',
                                allow_credentials=credentials)
                            continue
                    raise err
    else:
        logging.warning('Invalid Security Key request')
        return

    prompt_session = None

    def func():
        nonlocal prompt_session
        nonlocal evt
        try:
            time.sleep(0.1)
            auth_func()
        except:
            pass
        if prompt_session:
            evt = None
            prompt_session.app.exit()
        elif evt:
            print('\npress Enter to resume...')

    th = threading.Thread(target=func)
    th.start()
    try:
        prompt = 'Touch the flashing Security key to authenticate or press Enter to resume with the primary two factor authentication...'
        if os.isatty(0) and os.isatty(1):
            prompt_session = PromptSession(multiline=False,
                                           complete_while_typing=False)
            prompt_session.prompt(prompt)
            prompt_session = None
        else:
            input(prompt)
    except KeyboardInterrupt:
        prompt_session = None
    if evt:
        evt.set()
        evt = None
    th.join()

    return response
Example #10
0
def sign_request(public_key, authn_select):
    """Signs a WebAuthn challenge and returns the data.

    :param public_key dict containing `rpId` the relying party and `challenge` the received challenge
    :param authn_select string, that contains the allowed public key of the user

    :return dict containing clientDataJSON, authenticatorData, signature, credentialId and userHandle if available. 
    """

    use_prompt = False
    pin = None
    uv = "discouraged"

    if WindowsClient.is_available(
    ) and not ctypes.windll.shell32.IsUserAnAdmin():
        # Use the Windows WebAuthn API if available, and we're not running as admin
        client = WindowsClient("https://example.com")
    else:
        dev = next(CtapHidDevice.list_devices(), None)
        if dev is not None:
            print("Use USB HID channel.")
            use_prompt = True
        else:
            try:
                from fido2.pcsc import CtapPcscDevice

                dev = next(CtapPcscDevice.list_devices(), None)
                print("Use NFC channel.")
            except Exception as e:
                print("NFC channel search error:", e)

        if not dev:
            print("No FIDO device found")
            sys.exit(1)

        client = Fido2Client(dev,
                             "http://localhost:8080",
                             verify=lambda x, y: True)

        # Prefer UV if supported
        if client.info.options.get("uv"):
            uv = "preferred"
            print("Authenticator supports User Verification")
        elif client.info.options.get("clientPin"):
            # Prompt for PIN if needed
            pin = getpass("Please enter PIN: ")
        else:
            print("PIN not set, won't use")

    # the base64 library does not work when padding is missing, so append some
    allowed_key = base64.urlsafe_b64decode(authn_select + '===')

    pubKey = PublicKeyCredentialRequestOptions(
        public_key['challenge'],
        rp_id=public_key['rpId'],
        allow_credentials=[
            PublicKeyCredentialDescriptor(PublicKeyCredentialType.PUBLIC_KEY,
                                          allowed_key)
        ])

    # Authenticate the credential
    if use_prompt:
        print("\nTouch your authenticator device now...\n")

    # Only one cred in allowCredentials, only one response.
    result = client.get_assertion(pubKey, pin=pin).get_response(0)

    data = {
        "clientDataJSON": b64encode(result.client_data),
        "authenticatorData": b64encode(result.authenticator_data),
        "signature": b64encode(result.signature),
        "credentialId": b64encode(result.credential_id),
    }

    if result.user_handle:
        data['userHandle'] = b64encode(result.user_handle)

    return data
Example #11
0
    def verify(self, fid, state_token, sleep):
        '''Validates user with Okta using user's webauthn hardware device.

        This method is meant to be called by self.auth() if a Login session
        requires MFA, and the users profile supports webauthn.

        We wait for a webauthn device to be plugged into USB, request a
        challenge nonce from Okta, pass the challenge to the webauthn device,
        and finally send back the challenge response to Okta.  If its accepted,
        we write out our SessionToken.

        Args:
            fid: Okta Factor ID used to trigger the push
            state_token: State Token allowing us to trigger the push
            sleep: amount of seconds to wait between checking for webauthn
                   hardware keep to be plugged in.
        '''
        path = self.verify_path.format(fid=fid)

        # Wait for webauthn device to be plugged in before continuing on
        dev = None
        while True:
            dev = next(self._get_devices(), None)
            if dev:
                break
            log.info('Waiting for FIDO webauthn device to be plugged in.')
            time.sleep(sleep)

        # Request webauthn nonce/challenge from Okta
        log.info('Requesting webauthn challenge nonce from Okta')
        data = {'fid': fid,
                'stateToken': state_token}
        ret = self._request(path, data)

        if ret['status'] != 'MFA_CHALLENGE':
            raise FactorVerificationFailed('Expected MFA challenge')

        # Get webauthn device to sign nonce/challenge
        appId = self.base_url
        client = self._get_client(dev, appId)
        nonce = (ret['_embedded']
                    ['factor']
                    ['_embedded']
                    ['challenge']
                    ['challenge'])
        credential_id = ret['_embedded']['factor']['profile']['credentialId']
        # Add extra padding to credential_id as it may not have enough
        credential_id = base64.urlsafe_b64decode(credential_id + "====")

        allow_list = [{
            'type': 'public-key',
            'id': credential_id
        }]
        rp_id = urlparse(appId).hostname

        log.warning('Touch your authenticator device now...')
        try:
            challenge = websafe_decode(nonce)
            options = PublicKeyCredentialRequestOptions(
                challenge=challenge,
                rp_id=rp_id,
                allow_credentials=allow_list
            )

            assertions, client_data = client.get_assertion(options)
        except ClientError:
            raise FactorVerificationFailed('webauthn devices failed to '
                                           'sign request. Have you '
                                           'registered it with "{}"?'
                                           .format(appId))

        # Send challenge response back to Okta
        assert(len(assertions) == 1)
        ad = base64.b64encode(assertions[0].auth_data)
        cd = base64.b64encode(client_data)
        sig = base64.b64encode(assertions[0].signature)

        data = {'stateToken': state_token,
                'clientData': cd.decode(),
                'authenticatorData': ad.decode(),
                'signatureData': sig.decode()}

        ret = self._request(path, data)

        if ret.get('status') != 'SUCCESS':
            raise FactorVerificationFailed()

        return ret
Example #12
0
    def verify(self, fid, state_token, sleep):
        """Validates user with Okta using user's webauthn hardware device.

        This method is meant to be called by self.auth() if a Login session
        requires MFA, and the users profile supports webauthn.

        We wait for a webauthn device to be plugged into USB, request a
        challenge nonce from Okta, pass the challenge to the webauthn device,
        and finally send back the challenge response to Okta.  If its accepted,
        we write out our SessionToken.

        Args:
            fid: Okta Factor ID used to trigger the push
            state_token: State Token allowing us to trigger the push
            sleep: amount of seconds to wait between checking for webauthn
                   hardware keep to be plugged in.
        """
        path = self.verify_path.format(fid=fid)

        # Wait for webauthn device to be plugged in before continuing on
        dev = None
        while True:
            dev = next(self._get_devices(), None)
            if dev:
                break
            log.info("Waiting for FIDO webauthn device to be plugged in.")
            time.sleep(sleep)

        # Request webauthn nonce/challenge from Okta
        log.info("Requesting webauthn challenge nonce from Okta")
        data = {"fid": fid, "stateToken": state_token}
        ret = self._request(path, data)

        if ret["status"] != "MFA_CHALLENGE":
            raise FactorVerificationFailed("Expected MFA challenge")

        # Get webauthn device to sign nonce/challenge
        appId = self.base_url
        client = self._get_client(dev, appId)
        nonce = ret["_embedded"]["factor"]["_embedded"]["challenge"][
            "challenge"]
        credential_id = ret["_embedded"]["factor"]["profile"]["credentialId"]
        # Add extra padding to credential_id as it may not have enough
        credential_id = base64.urlsafe_b64decode(credential_id + "====")

        allow_list = [{"type": "public-key", "id": credential_id}]
        rp_id = urlparse(appId).hostname

        log.warning("Touch your authenticator device now...")
        try:
            challenge = websafe_decode(nonce)
            options = PublicKeyCredentialRequestOptions(
                challenge=challenge, rp_id=rp_id, allow_credentials=allow_list)

            assertions, client_data = client.get_assertion(options)
        except ClientError:
            raise FactorVerificationFailed(
                "webauthn devices failed to "
                "sign request. Have you "
                'registered it with "{}"?'.format(appId))

        # Send challenge response back to Okta
        assert len(assertions) == 1
        ad = base64.b64encode(assertions[0].auth_data)
        cd = base64.b64encode(client_data)
        sig = base64.b64encode(assertions[0].signature)

        data = {
            "stateToken": state_token,
            "clientData": cd.decode(),
            "authenticatorData": ad.decode(),
            "signatureData": sig.decode(),
        }

        ret = self._request(path, data)

        if ret.get("status") != "SUCCESS":
            raise FactorVerificationFailed()

        return ret