Пример #1
0
    def create_user_token_by_pairing_url(self, pairing_url, pin='1234'):
        """
        parses the pairing url and saves the extracted data in
        the fake token database of this test class.

        :param pairing_url: the pairing url received from the server
        :param pin: the pin of the token (default: '1234')

        :returns: user_token_id of newly created token
        """

        # extract metadata and the public key

        data_encoded = pairing_url[len(self.uri + '://pair/'):]
        data = decode_base64_urlsafe(data_encoded)
        version, token_type, flags = struct.unpack('<bbI', data[0:6])
        partition = struct.unpack('<I', data[6:10])[0]

        server_public_key = data[10:10 + 32]

        # validate protocol versions and type id

        self.assertEqual(token_type, TYPE_PUSHTOKEN)
        self.assertEqual(version, PAIRING_URL_VERSION)

        # ------------------------------------------------------------------ --

        # extract custom data that may or may not be present
        # (depending on flags)

        custom_data = data[10 + 32:]

        token_serial = None
        if flags & FLAG_PAIR_SERIAL:
            token_serial, __, custom_data = custom_data.partition(b'\x00')

        callback_url = None
        if flags & FLAG_PAIR_CBURL:
            callback_url, __, custom_data = custom_data.partition(b'\x00')
        else:
            raise NotImplementedError(
                'Callback URL is mandatory for PushToken')

        # ------------------------------------------------------------------ --

        # save token data for later use

        user_token_id = len(self.tokens)
        self.tokens[user_token_id] = {
            'serial': token_serial,
            'server_public_key': server_public_key,
            'partition': partition,
            'callback_url': callback_url,
            'pin': pin
        }

        # ------------------------------------------------------------------ --

        return user_token_id
Пример #2
0
    def create_user_token_by_pairing_url(self, pairing_url, pin='1234'):

        """
        parses the pairing url and saves the extracted data in
        the fake token database of this test class.

        :param pairing_url: the pairing url received from the server
        :param pin: the pin of the token (default: '1234')

        :returns: user_token_id of newly created token
        """

        # extract metadata and the public key

        data_encoded = pairing_url[len(self.uri + '://pair/'):]
        data = decode_base64_urlsafe(data_encoded)
        version, token_type, flags = struct.unpack('<bbI', data[0:6])
        partition = struct.unpack('<I', data[6:10])[0]

        server_public_key = data[10:10 + 32]

        # validate protocol versions and type id

        self.assertEqual(token_type, TYPE_PUSHTOKEN)
        self.assertEqual(version, PAIRING_URL_VERSION)

        # ------------------------------------------------------------------ --

        # extract custom data that may or may not be present
        # (depending on flags)

        custom_data = data[10 + 32:]

        token_serial = None
        if flags & FLAG_PAIR_SERIAL:
            token_serial, __, custom_data = custom_data.partition(b'\x00')

        callback_url = None
        if flags & FLAG_PAIR_CBURL:
            callback_url, __, custom_data = custom_data.partition(b'\x00')
        else:
            raise NotImplementedError(
                                    'Callback URL is mandatory for PushToken')

        # ------------------------------------------------------------------ --

        # save token data for later use

        user_token_id = len(self.tokens)
        self.tokens[user_token_id] = {'serial': token_serial,
                                      'server_public_key': server_public_key,
                                      'partition': partition,
                                      'callback_url': callback_url,
                                      'pin': pin}

        # ------------------------------------------------------------------ --

        return user_token_id
Пример #3
0
    def checkOtp(self, passwd, counter, window, options=None):

        """
        checks if the supplied challenge response is correct.

        :param passwd: The challenge response

        :param options: A dictionary of parameters passed by the upper
            layer (used for transaction_id in this context)

        :param counter: legacy API (unused)

        :param window: legacy API (unused)

        :raises TokenStateError: If token state is not 'active' or
            'pairing_challenge_sent'

        :returns: -1 for failure, 1 for success
        """

        valid_states = ['pairing_challenge_sent',
                        'active']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------- --

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('PushMaxChallenges', '3'))

        # ------------------------------------------------------------------- --

        if 'transactionid' in options:

            # --------------------------------------------------------------- --

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transaction_id')

            challenges = Challenges.lookup_challenges(serial, transaction_id)

            # --------------------------------------------------------------- --

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

        # ------------------------------------------------------------------- --

        if not filtered_challenges:
            return -1

        for challenge in filtered_challenges:

            # client verifies the challenge by signing the challenge
            # plaintext. we retrieve the original plaintext (saved
            # in createChallenge) and check for a match

            b64_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
            user_dsa_public_key = b64decode(b64_dsa_public_key)

            data = challenge.getData()
            sig_base = data['sig_base']

            passwd_as_bytes = decode_base64_urlsafe(passwd)
            sig_base_as_bytes = b64decode(sig_base)
            try:
                verify_sig(passwd_as_bytes,
                           sig_base_as_bytes,
                           user_dsa_public_key)
                return 1
            except ValueError:  # signature mismatch
                return -1

        return -1
Пример #4
0
    def decrypt_and_verify_challenge(self, challenge_url, action):
        """
        Decrypts the data packed in the challenge url, verifies
        its content, returns the parsed data as a dictionary,
        calculates and returns the signature.

        The calling method must then send the signature
        back to the server. (The reason for this control flow
        is that the challenge data must be checked in different
        scenarios, e.g. when we have a pairing the data must be
        checked by the method that simulates the pairing)

        :param challenge_url: the challenge url as sent by the server
        :param action: a string identifier for the verification action
            (at the moment 'ACCEPT' or 'DENY')

        :returns: (challenge, signature)

            challenge has the keys

                * content_type - one of the three values CONTENT_TYPE_SIGNREQ,
                    CONTENT_TYPE_PAIRING or CONTENT_TYPE_LOGIN)
                    (all defined in this module)
                * transaction_id - used to identify the challenge
                    on the server
                * callback_url (optional) - the url to which the challenge
                    response should be set
                * user_token_id - used to identify the token in the
                    user database for which this challenge was created

            depending on the content type additional keys are present

                * for CONTENT_TYPE_PAIRING: serial
                * for CONTENT_TYPE_SIGNREQ: message
                * for CONTENT_TYPE_LOGIN: login, host

            signature is the generated user signature used to
            respond to the challenge
        """

        challenge_data_encoded = challenge_url[len(self.uri + '://chal/'):]
        challenge_data = decode_base64_urlsafe(challenge_data_encoded)

        # ------------------------------------------------------------------ --

        # parse and verify header information in the
        # encrypted challenge data

        header = challenge_data[0:5]
        version, user_token_id = struct.unpack('<bI', header)
        self.assertEqual(version, CHALLENGE_URL_VERSION)

        # ------------------------------------------------------------------ --

        # get token from client token database

        token = self.tokens[user_token_id]
        server_public_key = token['server_public_key']

        # ------------------------------------------------------------------ --

        # prepare decryption by seperating R from
        # ciphertext and server signature

        R = challenge_data[5:5 + 32]
        ciphertext = challenge_data[5 + 32:-64]
        server_signature = challenge_data[-64:]

        # check signature

        data = challenge_data[0:-64]
        crypto_sign_verify_detached(server_signature, data, server_public_key)

        # ------------------------------------------------------------------ --

        # key derivation

        secret_key_dh = dsa_to_dh_secret(self.secret_key)
        ss = calc_dh(secret_key_dh, R)
        U = SHA256.new(ss).digest()

        sk = U[0:16]
        nonce = U[16:32]

        # ------------------------------------------------------------------ --

        # decrypt and verify challenge

        nonce_as_int = int_from_bytes(nonce, byteorder='big')
        ctr = Counter.new(128, initial_value=nonce_as_int)
        cipher = AES.new(sk, AES.MODE_CTR, counter=ctr)
        plaintext = cipher.decrypt(ciphertext)

        # ------------------------------------------------------------------ --

        # parse/check plaintext header

        # 1 - for content type
        # 8 - for transaction id
        # 8 - for time stamp
        offset = 1 + 8 + 8

        pt_header = plaintext[0:offset]
        (content_type, transaction_id,
         _time_stamp) = struct.unpack('<bQQ', pt_header)

        transaction_id = u64_to_transaction_id(transaction_id)

        # ------------------------------------------------------------------ --

        # prepare the parsed challenge data

        challenge = {}
        challenge['content_type'] = content_type

        # ------------------------------------------------------------------ --

        # retrieve plaintext data depending on content_type

        if content_type == CONTENT_TYPE_PAIRING:

            serial, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['serial'] = serial

        elif content_type == CONTENT_TYPE_SIGNREQ:

            message, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['message'] = message

        elif content_type == CONTENT_TYPE_LOGIN:

            login, host, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['login'] = login
            challenge['host'] = host

        # ------------------------------------------------------------------ --

        # prepare the parsed challenge data

        challenge['callback_url'] = callback_url
        challenge['transaction_id'] = transaction_id
        challenge['user_token_id'] = user_token_id

        # calculate signature

        sig_base = (struct.pack('<b', CHALLENGE_URL_VERSION) +
                    b'%s\0' % action + server_signature + plaintext)

        sig = crypto_sign_detached(sig_base, self.secret_key)
        encoded_sig = encode_base64_urlsafe(sig)

        return challenge, encoded_sig
Пример #5
0
    def checkOtp(self, passwd, counter, window, options=None):

        valid_states = ['pairing_challenge_sent', 'pairing_complete']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------- --

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('QRMaxChallenges', '3'))

        # ------------------------------------------------------------------- --

        # TODO: from which point is checkOtp called, when there
        # is no challenge response in the request?

        if 'transactionid' in options:

            # --------------------------------------------------------------- --

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transaction_id')

            challenges = Challenges.lookup_challenges(serial, transaction_id)

            # --------------------------------------------------------------- --

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

            # --------------------------------------------------------------- --

        if not filtered_challenges:
            return -1

        for challenge in filtered_challenges:

            data = challenge.getData()
            correct_passwd = data['user_sig']

            # compare values with python's native constant
            # time comparison

            if compare_digest(correct_passwd, passwd):

                return 1

            else:

                # maybe we got a tan instead of a signature

                correct_passwd_as_bytes = decode_base64_urlsafe(correct_passwd)
                tan_length = 8  # TODO fetch from policy
                correct_tan = extract_tan(correct_passwd_as_bytes, tan_length)

                # TODO PYLONS-HACK pylons silently converts integers in
                # incoming json to unicode. since extract_tan returns
                # an integer, we have to convert it here
                correct_tan = unicode(correct_tan)

                if compare_digest(correct_tan, passwd):
                    return 1

        return -1  # TODO: ??? semantics of this ret val?
Пример #6
0
    def checkOtp(self, passwd, counter, window, options=None):
        """
        checks if the supplied challenge response is correct.

        :param passwd: The challenge response

        :param options: A dictionary of parameters passed by the upper
            layer (used for transaction_id in this context)

        :param counter: legacy API (unused)

        :param window: legacy API (unused)

        :raises TokenStateError: If token state is not 'active' or
            'pairing_challenge_sent'

        :returns: -1 for failure, 1 for success
        """

        valid_states = ['pairing_challenge_sent', 'active']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------ --

        # new pushtoken protocoll supports the keyword based accept or deny.
        # the old 'passwd' argument is not supported anymore

        try:

            signature_accept = passwd.get('accept', None)
            signature_reject = passwd.get('reject', None)

        except AttributeError:  # will be raised with a get() on a str object

            raise Exception('Pushtoken version %r requires "accept" or'
                            ' "reject" as parameter' % CHALLENGE_URL_VERSION)

        if signature_accept is not None and signature_reject is not None:

            raise Exception('Pushtoken version %r requires "accept" or'
                            ' "reject" as parameter' % CHALLENGE_URL_VERSION)

        # ------------------------------------------------------------------ --

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('PushMaxChallenges', '3'))

        # ------------------------------------------------------------------ --

        if 'transactionid' in options:

            # -------------------------------------------------------------- --

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transaction_id')

            challenges = Challenges.lookup_challenges(serial, transaction_id)

            # -------------------------------------------------------------- --

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

        # ------------------------------------------------------------------ --

        if not filtered_challenges:
            return -1

        for challenge in filtered_challenges:

            # client verifies the challenge by signing the challenge
            # plaintext. we retrieve the original plaintext (saved
            # in createChallenge) and check for a match

            data = challenge.getData()
            data_to_verify = b64decode(data['sig_base'])

            b64_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
            user_dsa_public_key = b64decode(b64_dsa_public_key)

            # -------------------------------------------------------------- --

            # handle the accept case

            if signature_accept is not None:

                accept_signature_as_bytes = decode_base64_urlsafe(
                    signature_accept)

                accept_data_to_verify_as_bytes = (
                    struct.pack('<b', CHALLENGE_URL_VERSION) + b'ACCEPT\0' +
                    data_to_verify)

                try:
                    verify_sig(accept_signature_as_bytes,
                               accept_data_to_verify_as_bytes,
                               user_dsa_public_key)

                    challenge.add_session_info({'accept': True})

                    return 1
                except ValueError:  # accept signature mismatch

                    challenge.add_session_info({'accept': False})
                    log.error("accept signature mismatch!")

                    return -1

            # -------------------------------------------------------------- --

            # handle the reject case

            elif signature_reject is not None:

                reject_signature_as_bytes = decode_base64_urlsafe(
                    signature_reject)

                reject_data_to_verify_as_bytes = (
                    struct.pack('<b', CHALLENGE_URL_VERSION) + b'DENY\0' +
                    data_to_verify)

                try:
                    verify_sig(reject_signature_as_bytes,
                               reject_data_to_verify_as_bytes,
                               user_dsa_public_key)

                    challenge.add_session_info({'reject': True})

                    return 1

                except ValueError:  # reject signature mismatch

                    challenge.add_session_info({'reject': False})
                    log.error("reject signature mismatch!")

                    return -1

        return -1
Пример #7
0
    def checkOtp(self, passwd, counter, window, options=None):

        """
        checks if the supplied challenge response is correct.

        :param passwd: The challenge response

        :param options: A dictionary of parameters passed by the upper
            layer (used for transaction_id in this context)

        :param counter: legacy API (unused)

        :param window: legacy API (unused)

        :raises TokenStateError: If token state is not 'active' or
            'pairing_challenge_sent'

        :returns: -1 for failure, 1 for success
        """

        valid_states = ['pairing_challenge_sent',
                        'active']

        self.ensure_state_is_in(valid_states)

        # ------------------------------------------------------------------ --

        # new pushtoken protocoll supports the keyword based accept or deny.
        # the old 'passwd' argument is not supported anymore

        try:

            signature_accept = passwd.get('accept', None)
            signature_reject = passwd.get('reject', None)

        except AttributeError:  # will be raised with a get() on a str object

            raise Exception('Pushtoken version %r requires "accept" or'
                            ' "reject" as parameter' % CHALLENGE_URL_VERSION)

        if signature_accept is not None and signature_reject is not None:

            raise Exception('Pushtoken version %r requires "accept" or'
                            ' "reject" as parameter' % CHALLENGE_URL_VERSION)

        # ------------------------------------------------------------------ --

        filtered_challenges = []
        serial = self.getSerial()

        if options is None:
            options = {}

        max_fail = int(getFromConfig('PushMaxChallenges', '3'))

        # ------------------------------------------------------------------ --

        if 'transactionid' in options:

            # -------------------------------------------------------------- --

            # fetch all challenges that match the transaction id or serial

            transaction_id = options.get('transactionid')

            challenges = Challenges.lookup_challenges(serial=serial,
                transid=transaction_id, filter_open=True)

            # -------------------------------------------------------------- --

            # filter into filtered_challenges

            for challenge in challenges:

                (received_tan, tan_is_valid) = challenge.getTanStatus()
                fail_counter = challenge.getTanCount()

                # if we iterate over matching challenges (that is: challenges
                # with the correct transaction id) we either find a fresh
                # challenge, that didn't receive a TAN at all (first case)
                # or a challenge, that already received a number of wrong
                # TANs but still has tries left (second case).

                if not received_tan:
                    filtered_challenges.append(challenge)
                elif not tan_is_valid and fail_counter <= max_fail:
                    filtered_challenges.append(challenge)

        # ------------------------------------------------------------------ --

        if not filtered_challenges:
            return -1

        if len(filtered_challenges) > 1:
            log.error('multiple challenges for one transaction and for one'
                      ' token found!')
            return -1

        # for the serial and the transaction id there could always be only
        # at max one challenge matching. This is even true for sub transactions

        challenge = filtered_challenges[0]

        # client verifies the challenge by signing the challenge
        # plaintext. we retrieve the original plaintext (saved
        # in createChallenge) and check for a match

        data = challenge.getData()
        data_to_verify = b64decode(data['sig_base'])

        b64_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
        user_dsa_public_key = b64decode(b64_dsa_public_key)

        # -------------------------------------------------------------- --

        # handle the accept case

        if signature_accept is not None:

            accept_signature_as_bytes = decode_base64_urlsafe(
                                                    signature_accept)

            accept_data_to_verify_as_bytes = (
                            struct.pack('<b', CHALLENGE_URL_VERSION) +
                            b'ACCEPT\0' +
                            data_to_verify)

            try:
                verify_sig(accept_signature_as_bytes,
                           accept_data_to_verify_as_bytes,
                           user_dsa_public_key)

                challenge.add_session_info({'accept': True})

                return 1

            except ValueError:

                challenge.add_session_info({'accept': False})
                log.error("accept signature mismatch!")

                return -1

        # -------------------------------------------------------------- --

        # handle the reject case

        elif signature_reject is not None:

            reject_signature_as_bytes = decode_base64_urlsafe(
                                                        signature_reject)

            reject_data_to_verify_as_bytes = (
                            struct.pack('<b', CHALLENGE_URL_VERSION) +
                            b'DENY\0' +
                            data_to_verify)

            try:
                verify_sig(reject_signature_as_bytes,
                           reject_data_to_verify_as_bytes,
                           user_dsa_public_key)

                challenge.add_session_info({'reject': True})

                return 1

            except ValueError:

                challenge.add_session_info({'reject': False})
                log.error("reject signature mismatch!")

                return -1

        return -1
Пример #8
0
    def decrypt_and_verify_challenge(self, challenge_url, action):

        """
        Decrypts the data packed in the challenge url, verifies
        its content, returns the parsed data as a dictionary,
        calculates and returns the signature.

        The calling method must then send the signature
        back to the server. (The reason for this control flow
        is that the challenge data must be checked in different
        scenarios, e.g. when we have a pairing the data must be
        checked by the method that simulates the pairing)

        :param challenge_url: the challenge url as sent by the server
        :param action: a string identifier for the verification action
            (at the moment 'ACCEPT' or 'DENY')

        :returns: (challenge, signature)

            challenge has the keys

                * content_type - one of the three values CONTENT_TYPE_SIGNREQ,
                    CONTENT_TYPE_PAIRING or CONTENT_TYPE_LOGIN)
                    (all defined in this module)
                * transaction_id - used to identify the challenge
                    on the server
                * callback_url (optional) - the url to which the challenge
                    response should be set
                * user_token_id - used to identify the token in the
                    user database for which this challenge was created

            depending on the content type additional keys are present

                * for CONTENT_TYPE_PAIRING: serial
                * for CONTENT_TYPE_SIGNREQ: message
                * for CONTENT_TYPE_LOGIN: login, host

            signature is the generated user signature used to
            respond to the challenge
        """

        challenge_data_encoded = challenge_url[len(self.uri + '://chal/'):]
        challenge_data = decode_base64_urlsafe(challenge_data_encoded)

        # ------------------------------------------------------------------ --

        # parse and verify header information in the
        # encrypted challenge data

        header = challenge_data[0:5]
        version, user_token_id = struct.unpack('<bI', header)
        self.assertEqual(version, CHALLENGE_URL_VERSION)

        # ------------------------------------------------------------------ --

        # get token from client token database

        token = self.tokens[user_token_id]
        server_public_key = token['server_public_key']

        # ------------------------------------------------------------------ --

        # prepare decryption by seperating R from
        # ciphertext and server signature

        R = challenge_data[5:5 + 32]
        ciphertext = challenge_data[5 + 32:-64]
        server_signature = challenge_data[-64:]

        # check signature

        data = challenge_data[0:-64]
        crypto_sign_verify_detached(server_signature, data, server_public_key)

        # ------------------------------------------------------------------ --

        # key derivation

        secret_key_dh = dsa_to_dh_secret(self.secret_key)
        ss = calc_dh(secret_key_dh, R)
        U = SHA256.new(ss).digest()

        sk = U[0:16]
        nonce = U[16:32]

        # ------------------------------------------------------------------ --

        # decrypt and verify challenge

        nonce_as_int = int_from_bytes(nonce, byteorder='big')
        ctr = Counter.new(128, initial_value=nonce_as_int)
        cipher = AES.new(sk, AES.MODE_CTR, counter=ctr)
        plaintext = cipher.decrypt(ciphertext)

        # ------------------------------------------------------------------ --

        # parse/check plaintext header

        # 1 - for content type
        # 8 - for transaction id
        # 8 - for time stamp
        offset = 1 + 8 + 8

        pt_header = plaintext[0:offset]
        (content_type,
         transaction_id,
         _time_stamp) = struct.unpack('<bQQ', pt_header)

        transaction_id = u64_to_transaction_id(transaction_id)

        # ------------------------------------------------------------------ --

        # prepare the parsed challenge data

        challenge = {}
        challenge['content_type'] = content_type

        # ------------------------------------------------------------------ --

        # retrieve plaintext data depending on content_type

        if content_type == CONTENT_TYPE_PAIRING:

            serial, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['serial'] = serial

        elif content_type == CONTENT_TYPE_SIGNREQ:

            message, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['message'] = message

        elif content_type == CONTENT_TYPE_LOGIN:

            login, host, callback_url, __ = plaintext[offset:].split('\x00')
            challenge['login'] = login
            challenge['host'] = host

        # ------------------------------------------------------------------ --

        # prepare the parsed challenge data

        challenge['callback_url'] = callback_url
        challenge['transaction_id'] = transaction_id
        challenge['user_token_id'] = user_token_id

        # calculate signature

        sig_base = (
            struct.pack('<b', CHALLENGE_URL_VERSION) +
            b'%s\0' % action +
            server_signature + plaintext)

        sig = crypto_sign_detached(sig_base, self.secret_key)
        encoded_sig = encode_base64_urlsafe(sig)

        return challenge, encoded_sig
Пример #9
0
def decrypt_pairing_response(enc_pairing_response):
    """
    Parses and decrypts a pairing response into a named tuple PairingResponse
    consisting of

    * user_public_key - the user's public key
    * user_token_id   - an id for the client to uniquely identify the token.
                        this id is necessary, because the client could
                        communicate with more than one linotp, so serials
                        could overlap.
    * serial - the serial identifying the token in linotp
    * user_login - the user login name

    It is possible that either user_login or serial is None. Both
    being None is a valid response according to this function but
    will be considered an error in the calling method.

    The following parameters are needed:

    :param enc_pairing_response:
        The urlsafe-base64 encoded string received from the client

    The following exceptions can be raised:

    :raises ParameterError:
        If the pairing response has an invalid format

    :raises ValueError:
        If the pairing response has a different version
        than this implementation (currently hardcoded)

    :raises ValueError:
        If the pairing response indicates a different
        token type than QRToken (also hardcoded)

    :raises ValueError:
        If the pairing response field "partition" is not
        identical to the field "token_type"
        ("partition" is currently used for the token
        type id. It is reserved for multiple key usage
        in a future implementation.)

    :raises ValueError:
        If the MAC of the response didn't match

    :return:
        Parsed/decrypted PairingReponse
    """

    data = decode_base64_urlsafe(enc_pairing_response)

    # ---------------------------------------------------------------------- --

    #            ------------------------------------------- --
    #  fields   | version | partition | R  | ciphertext | MAC |
    #            ------------------------------------------- --
    #  size     |    1    |     4     | 32 |      ?     | 16  |
    #            ------------------------------------------- --

    if len(data) < 1 + 4 + 32 + 16:
        raise ParameterError('Malformed pairing response')

    # ---------------------------------------------------------------------- --

    # parse header

    header = data[0:5]
    version, partition = struct.unpack('<bI', header)

    if version != PAIR_RESPONSE_VERSION:
        raise ValueError('Unexpected pair-response version, '
                         'expected: %d, got: %d' %
                         (PAIR_RESPONSE_VERSION, version))

    # ---------------------------------------------------------------------- --

    R = data[5:32 + 5]
    ciphertext = data[32 + 5:-16]
    mac = data[-16:]

    # ---------------------------------------------------------------------- --

    # calculate the shared secret

    # - --

    secret_key = get_dh_secret_key(partition)
    ss = calc_dh(secret_key, R)

    # derive encryption key and nonce from the shared secret
    # zero the values from memory when they are not longer needed
    U = SHA256.new(ss).digest()
    zerome(ss)
    encryption_key = U[0:16]
    nonce = U[16:32]
    zerome(U)

    # decrypt response
    cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
    cipher.update(header)
    plaintext = cipher.decrypt_and_verify(ciphertext, mac)
    zerome(encryption_key)

    # ---------------------------------------------------------------------- --

    # check format boundaries for type peaking
    # (token type specific length boundaries are checked
    #  in the appropriate functions)

    plaintext_min_length = 1
    if len(data) < plaintext_min_length:
        raise ParameterError('Malformed pairing response')

    # ---------------------------------------------------------------------- --

    # get token type and parse decrypted response

    #            -------------------- --
    #  fields   | token type |   ...   |
    #            -------------------- --
    #  size     |     1      |    ?    |
    #            -------------------- --

    token_type = struct.unpack('<b', plaintext[0])[0]

    if token_type not in SUPPORTED_TOKEN_TYPES:
        raise ValueError('unsupported token type %d, supported types '
                         'are %s' % (token_type, SUPPORTED_TOKEN_TYPES))

    # ---------------------------------------------------------------------- --

    # delegate the data parsing of the plaintext
    # to the appropriate function and return the result

    data_parser = get_pairing_data_parser(token_type)
    pairing_data = data_parser(plaintext)
    zerome(plaintext)

    # get the appropriate high level type

    try:
        token_type_as_str = INV_TOKEN_TYPES[token_type]
    except KeyError:
        raise ProgrammingError(
            'token_type %d is in SUPPORTED_TOKEN_TYPES',
            'however an appropriate mapping entry in '
            'TOKEN_TYPES is missing' % token_type)

    return PairingResponse(token_type_as_str, pairing_data)
Пример #10
0
def decrypt_pairing_response(enc_pairing_response):

    """
    Parses and decrypts a pairing response into a named tuple PairingResponse
    consisting of

    * user_public_key - the user's public key
    * user_token_id   - an id for the client to uniquely identify the token.
                        this id is necessary, because the client could
                        communicate with more than one linotp, so serials
                        could overlap.
    * serial - the serial identifying the token in linotp
    * user_login - the user login name

    It is possible that either user_login or serial is None. Both
    being None is a valid response according to this function but
    will be considered an error in the calling method.

    The following parameters are needed:

    :param enc_pairing_response:
        The urlsafe-base64 encoded string received from the client

    The following exceptions can be raised:

    :raises ParameterError:
        If the pairing response has an invalid format

    :raises ValueError:
        If the pairing response has a different version
        than this implementation (currently hardcoded)

    :raises ValueError:
        If the pairing response indicates a different
        token type than QRToken (also hardcoded)

    :raises ValueError:
        If the pairing response field "partition" is not
        identical to the field "token_type"
        ("partition" is currently used for the token
        type id. It is reserved for multiple key usage
        in a future implementation.)

    :raises ValueError:
        If the MAC of the response didn't match

    :return:
        Parsed/decrypted PairingReponse
    """

    data = decode_base64_urlsafe(enc_pairing_response)

    # ---------------------------------------------------------------------- --

    #            ------------------------------------------- --
    #  fields   | version | partition | R  | ciphertext | MAC |
    #            ------------------------------------------- --
    #  size     |    1    |     4     | 32 |      ?     | 16  |
    #            ------------------------------------------- --

    if len(data) < 1 + 4 + 32 + 16:
        raise ParameterError('Malformed pairing response')

    # ---------------------------------------------------------------------- --

    # parse header

    header = data[0:5]
    version, partition = struct.unpack('<bI', header)

    if version != PAIR_RESPONSE_VERSION:
        raise ValueError('Unexpected pair-response version, '
                         'expected: %d, got: %d' %
                         (PAIR_RESPONSE_VERSION, version))

    # ---------------------------------------------------------------------- --

    R = data[5:32+5]
    ciphertext = data[32+5:-16]
    mac = data[-16:]

    # ---------------------------------------------------------------------- --

    # calculate the shared secret

    # - --

    secret_key = get_dh_secret_key(partition)
    ss = calc_dh(secret_key, R)

    # derive encryption key and nonce from the shared secret
    # zero the values from memory when they are not longer needed
    U = SHA256.new(ss).digest()
    zerome(ss)
    encryption_key = U[0:16]
    nonce = U[16:32]
    zerome(U)

    # decrypt response
    cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
    cipher.update(header)
    plaintext = cipher.decrypt_and_verify(ciphertext, mac)
    zerome(encryption_key)

    # ---------------------------------------------------------------------- --

    # check format boundaries for type peaking
    # (token type specific length boundaries are checked
    #  in the appropriate functions)

    plaintext_min_length = 1
    if len(data) < plaintext_min_length:
        raise ParameterError('Malformed pairing response')

    # ---------------------------------------------------------------------- --

    # get token type and parse decrypted response

    #            -------------------- --
    #  fields   | token type |   ...   |
    #            -------------------- --
    #  size     |     1      |    ?    |
    #            -------------------- --

    token_type = struct.unpack('<b', plaintext[0])[0]

    if token_type not in SUPPORTED_TOKEN_TYPES:
        raise ValueError('unsupported token type %d, supported types '
                         'are %s' % (token_type, SUPPORTED_TOKEN_TYPES))

    # ---------------------------------------------------------------------- --

    # delegate the data parsing of the plaintext
    # to the appropriate function and return the result

    data_parser = get_pairing_data_parser(token_type)
    pairing_data = data_parser(plaintext)
    zerome(plaintext)

    # get the appropriate high level type

    try:
        token_type_as_str = INV_TOKEN_TYPES[token_type]
    except KeyError:
        raise ProgrammingError('token_type %d is in SUPPORTED_TOKEN_TYPES',
                               'however an appropriate mapping entry in '
                               'TOKEN_TYPES is missing' % token_type)

    return PairingResponse(token_type_as_str, pairing_data)