Esempio n. 1
0
    def create_pairing_response_by_serial(self, user_token_id, gda=None):
        """
        Creates a base64-encoded pairing response that identifies
        the token by its serial

        :param user_token_id: the token id (primary key for the user token db)
        :returns base64 encoded pairing response
        """

        if not gda:
            gda = self.gda

        token_serial = self.tokens[user_token_id]['serial']
        server_public_key = self.tokens[user_token_id]['server_public_key']
        partition = self.tokens[user_token_id]['partition']

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

        # assemble header and plaintext

        header = struct.pack('<bI', PAIR_RESPONSE_VERSION, partition)

        pairing_response = b''
        pairing_response += struct.pack('<bI', TYPE_PUSHTOKEN, user_token_id)

        pairing_response += self.public_key

        pairing_response += token_serial.encode('utf8') + b'\x00\x00'
        pairing_response += gda.encode('utf-8') + b'\x00'

        signature = crypto_sign_detached(pairing_response, self.secret_key)
        pairing_response += signature

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

        # create public diffie hellman component
        # (used to decrypt and verify the reponse)

        r = os.urandom(32)
        R = calc_dh_base(r)

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

        # derive encryption key and nonce

        server_public_key_dh = dsa_to_dh_public(server_public_key)
        ss = calc_dh(r, server_public_key_dh)
        U = SHA256.new(ss).digest()
        encryption_key = U[0:16]
        nonce = U[16:32]

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

        # encrypt in EAX mode

        cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
        cipher.update(header)
        ciphertext, tag = cipher.encrypt_and_digest(pairing_response)

        return encode_base64_urlsafe(header + R + ciphertext + tag)
Esempio n. 2
0
    def create_pairing_response_by_serial(self, user_token_id):

        """
        Creates a base64-encoded pairing response that identifies
        the token by its serial

        :param user_token_id: the token id (primary key for the user token db)
        :returns base64 encoded pairing response
        """

        token_serial = self.tokens[user_token_id]['serial']
        server_public_key = self.tokens[user_token_id]['server_public_key']
        partition = self.tokens[user_token_id]['partition']

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

        # assemble header and plaintext

        header = struct.pack('<bI', PAIR_RESPONSE_VERSION, partition)

        pairing_response = b''
        pairing_response += struct.pack('<bI', TYPE_PUSHTOKEN, user_token_id)

        pairing_response += self.public_key

        pairing_response += token_serial.encode('utf8') + b'\x00\x00'
        pairing_response += self.gda + b'\x00'

        signature = crypto_sign_detached(pairing_response, self.secret_key)
        pairing_response += signature

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

        # create public diffie hellman component
        # (used to decrypt and verify the reponse)

        r = os.urandom(32)
        R = calc_dh_base(r)

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

        # derive encryption key and nonce

        server_public_key_dh = dsa_to_dh_public(server_public_key)
        ss = calc_dh(r, server_public_key_dh)
        U = SHA256.new(ss).digest()
        encryption_key = U[0:16]
        nonce = U[16:32]

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

        # encrypt in EAX mode

        cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
        cipher.update(header)
        ciphertext, tag = cipher.encrypt_and_digest(pairing_response)

        return encode_base64_urlsafe(header + R + ciphertext + tag)
Esempio n. 3
0
def send_pairing_response(pairing_url):

    user_token_id = parse_pairing_url(pairing_url)
    serial = token_db[user_token_id]['serial']

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

    server_public_key = token_db[user_token_id]['server_public_key']

    pairing_response = b''
    pairing_response += struct.pack('<bbI', RESPONSE_VERSION, TYPE_QRTOKEN,
                                    user_token_id)

    pairing_response += public_key

    pairing_response += serial.encode('utf8') + b'\x00\x00'

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

    # create public diffie hellman component
    # (used to decrypt and verify the reponse)

    r = os.urandom(32)
    R = calc_dh_base(r)

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

    # derive encryption key and nonce

    ss = calc_dh(r, server_public_key)
    U = SHA256.new(ss).digest()
    encryption_key = U[0:16]
    nonce = U[16:32]

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

    # encrypt in EAX mode

    cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
    ciphertext, tag = cipher.encrypt_and_digest(pairing_response)

    pairing_response = encode_base64_urlsafe(R + ciphertext + tag)

    params = {
        'session': SESSION,
        'pairing_response': pairing_response,
        'type': 'qr'
    }

    r = requests.request('get',
                         'http://localhost:5001/admin/init',
                         params=params,
                         cookies=cookies)

    print((r.status_code))
    print('----------------------------------')
    print((r.content))
def send_pairing_response(pairing_url):

    user_token_id = parse_pairing_url(pairing_url)
    serial = token_db[user_token_id]['serial']

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

    server_public_key = token_db[user_token_id]['server_public_key']

    pairing_response = b''
    pairing_response += struct.pack('<bbI', RESPONSE_VERSION,
                                    TYPE_QRTOKEN, user_token_id)

    pairing_response += public_key

    pairing_response += serial.encode('utf8') + b'\x00\x00'

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

    # create public diffie hellman component
    # (used to decrypt and verify the reponse)

    r = os.urandom(32)
    R = calc_dh_base(r)

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

    # derive encryption key and nonce

    ss = calc_dh(r, server_public_key)
    U = SHA256.new(ss).digest()
    encryption_key = U[0:16]
    nonce = U[16:32]

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

    # encrypt in EAX mode

    cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
    ciphertext, tag = cipher.encrypt_and_digest(pairing_response)

    pairing_response = encode_base64_urlsafe(R + ciphertext + tag)


    params = {'session': SESSION, 'pairing_response': pairing_response,
              'type': 'qr' }

    r = requests.request('get', 'http://localhost:5001/admin/init',
                         params=params, cookies=cookies)

    print(r.status_code)
    print('----------------------------------')
    print(r.content)
Esempio n. 5
0
    def server_hmac_secret(self):
        """ the server hmac secret for this specific token """

        server_secret_key = get_qrtoken_dh_secret_key()

        # user public key is saved base64 encoded

        b64_user_public_key = self.getFromTokenInfo('user_public_key')
        user_public_key = b64decode(b64_user_public_key)

        hmac_secret = calc_dh(server_secret_key, user_public_key)
        zerome(server_secret_key)

        return hmac_secret
Esempio n. 6
0
    def server_hmac_secret(self):
        """ the server hmac secret for this specific token """

        server_secret_key = get_qrtoken_dh_secret_key()

        # user public key is saved base64 encoded

        b64_user_public_key = self.getFromTokenInfo('user_public_key')
        user_public_key = b64decode(b64_user_public_key)

        hmac_secret = calc_dh(server_secret_key, user_public_key)
        zerome(server_secret_key)

        return hmac_secret
Esempio n. 7
0
    def create_pairing_response(public_key, token_info, token_id=1):
        """
        Creates a base64-encoded pairing response that identifies
        the token by its serial

        :param user_token_id: the token id (primary key for the user token db)
        :returns base64 encoded pairing response
        """

        token_serial = token_info['serial']
        server_public_key = token_info['server_public_key']
        partition = token_info['partition']

        header = struct.pack('<bI', PAIR_RESPONSE_VERSION, partition)

        pairing_response = b''
        pairing_response += struct.pack('<bI', TYPE_QRTOKEN, token_id)

        pairing_response += public_key

        pairing_response += token_serial.encode('utf8') + b'\x00\x00'

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

        # create public diffie hellman component
        # (used to decrypt and verify the reponse)

        r = os.urandom(32)
        R = calc_dh_base(r)

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

        # derive encryption key and nonce

        ss = calc_dh(r, server_public_key)
        U = SHA256.new(ss).digest()
        encryption_key = U[0:16]
        nonce = U[16:32]

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

        # encrypt in EAX mode

        cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
        cipher.update(header)
        ciphertext, tag = cipher.encrypt_and_digest(pairing_response)

        return encode_base64_urlsafe(header + R + ciphertext + tag)
Esempio n. 8
0
    def calc_dh(self, partition, data):
        """
        encapsulate the Diffi Helmann calculation

        as the server secret key is a sensitive data, we try to encapsulate
        it and care for the cleanup

        :param partition: the id of the server secret key
        :param :
        """
        server_secret_key = get_dh_secret_key(partition)
        hmac_secret = calc_dh(server_secret_key, data)

        zerome(server_secret_key)

        return hmac_secret
Esempio n. 9
0
    def calc_dh(self, partition, data):
        """
        encapsulate the Diffi Helmann calculation

        as the server secret key is a sensitive data, we try to encapsulate
        it and care for the cleanup

        :param partition: the id of the server secret key
        :param :
        """
        server_secret_key = get_dh_secret_key(partition)
        hmac_secret = calc_dh(server_secret_key, data)

        zerome(server_secret_key)

        return hmac_secret
Esempio n. 10
0
    def decrypt_and_verify_challenge(challenge_url, token_info, secret_key):
        """
        Decrypts the data packed in the challenge url, verifies
        its content, returns the parsed data as a dictionary,
        calculates and returns the signature and TAN.

        The calling method must then send the signature/TAN
        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

        :returns: (challenge, signature, tan)

            challenge has the keys

                * message - the signed message sent from the server
                * content_type - one of the three values QRTOKEN_CT_PAIR,
                    QRTOKEN_CT_FREE or QRTOKEN_CT_AUTH
                    (all defined in this module
                * callback_url (optional) - the url to which the challenge
                    response should be set
                * callback_sms (optional) - the sms number the challenge
                    can be sent to (typicall used as a fallback)
                * transaction_id - used to identify the challenge
                    on the server
                * user_token_id - used to identify the token in the
                    user database for which this challenge was created

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

            tan is the TAN-Number used as a substitute if the signature
            cant' be sent be the server (is generated from signature)
        """

        challenge_data_encoded = challenge_url[len(QR_Token_Validation.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)
        assert version == QRTOKEN_VERSION

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

        # get token from client token database

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

        # prepare decryption by seperating R from
        # ciphertext and tag

        R = challenge_data[5:5 + 32]
        ciphertext = challenge_data[5 + 32:-16]
        tag = challenge_data[-16:]

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

        # key derivation

        ss = calc_dh(secret_key, R)
        U1 = SHA256.new(ss).digest()
        U2 = SHA256.new(U1).digest()

        skA = U1[0:16]
        skB = U2[0:16]
        nonce = U2[16:32]

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

        # decrypt and verify challenge

        cipher = AES.new(skA, AES.MODE_EAX, nonce)
        cipher.update(header)
        plaintext = cipher.decrypt_and_verify(ciphertext, tag)

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

        # parse/check plaintext header

        pt_header = plaintext[0:10]
        content_type, flags, transaction_id = struct.unpack('<bbQ', pt_header)
        transaction_id = QR_Token_Validation.u64_to_transaction_id(transaction_id)

        # make sure a flag for the server signature is
        # present, if the content type is 'pairing'

        if content_type == QRTOKEN_CT_PAIR:
            assert flags & FLAG_QR_SRVSIG

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

        # retrieve plaintext data depending on flags

        if flags & FLAG_QR_SRVSIG:

            # plaintext has a server signature as a header
            # extract it and check if it is correct

            server_signature = plaintext[10:10 + 32]
            data = plaintext[10 + 32:]

            # calculate secret

            server_public_key = token_info['server_public_key']
            secret = calc_dh(secret_key, server_public_key)

            # check hmac

            message = nonce + pt_header + data
            signed = HMAC.new(secret, msg=message, digestmod=SHA256).digest()
            assert server_signature == signed

        else:

            # no server signature found - just remove
            # the plaintext header

            data = plaintext[10:]

            # we have to define an empty server signature in
            # here because we need it later to create the
            # client signature

            server_signature = b''

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

        # extract message and (optional) callback
        # parameters from data

        message, _, suffix = data.partition(b'\x00')

        callback_url = token_info.get('callback_url')
        if flags & FLAG_QR_HAVE_URL:
            callback_url, _, suffix = suffix.partition(b'\x00')

        callback_sms = token_info.get('callback_sms')
        if flags & FLAG_QR_HAVE_SMS:
            callback_sms, _, suffix = suffix.partition(b'\x00')

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

        # prepare the parsed challenge data

        challenge = {}
        challenge['message'] = message.decode('utf-8')
        challenge['content_type'] = content_type
        challenge['transaction_id'] = transaction_id
        challenge['user_token_id'] = user_token_id

        if callback_url:
            challenge['callback_url'] = callback_url.decode('utf-8')
        if callback_sms:
            challenge['callback_sms'] = callback_sms.decode('utf-8')


        # calculate signature and tan

        message_bin = nonce + pt_header + server_signature + data
        sig_hmac = HMAC.new(skB, message_bin, digestmod=SHA256)
        sig = sig_hmac.digest()

        tan = extract_tan(sig, QR_Token_Validation.tan_length)
        encoded_sig = encode_base64_urlsafe(sig)

        return challenge, encoded_sig, tan
Esempio n. 11
0
    def create_challenge_url(self,
                             transaction_id,
                             content_type,
                             callback_url='',
                             message=None,
                             login=None,
                             host=None):

        """
        creates a challenge url (looking like lseqr://push/<base64string>),
        returns the url and the unencrypted challenge data

        :param transaction_id: The transaction id generated by LinOTP

        :param content_type: One of the types CONTENT_TYPE_SIGNREQ,
            CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN

        :param callback_url: callback url (optional), default is
            empty string

        :param message: the transaction message, that should be signed
            by the client. Only for content type CONTENT_TYPE_SIGNREQ

        :param login: the login name of the user. Only for content type
            CONTENT_TYPE_LOGIN

        :param host: hostname of the user. Only for content type
            CONTENT_TYPE_LOGIN

        :returns: tuple (challenge_url, sig_base), with challenge_url being
            the push url and sig_base the message, that is used for
            the client signature
        """

        serial = self.getSerial()

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

        # sanity/format checks

        if content_type not in [CONTENT_TYPE_SIGNREQ,
                                CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN]:
            raise InvalidFunctionParameter('content_type', 'content_type must '
                                           'be CONTENT_TYPE_SIGNREQ, '
                                           'CONTENT_TYPE_PAIRING or '
                                           'CONTENT_TYPE_LOGIN.')

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

        #  after the lseqr://push/ prefix the following data is encoded
        #  in urlsafe base64:

        #            ---------------------------------------------------
        #  fields   | version | user token id |  R  | ciphertext | sign |
        #            ---------------------------------------------------
        #           |          header         |          body           |
        #            ---------------------------------------------------
        #  size     |    1    |       4       |  32 |      ?     |  64  |
        #            ---------------------------------------------------
        #

        # create header

        user_token_id = self.getFromTokenInfo('user_token_id')
        data_header = struct.pack('<bI', CHALLENGE_URL_VERSION, user_token_id)

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

        # create body

        r = urandom(32)
        R = calc_dh_base(r)

        b64_user_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
        user_dsa_public_key = b64decode(b64_user_dsa_public_key)
        user_dh_public_key = dsa_to_dh_public(user_dsa_public_key)

        ss = calc_dh(r, user_dh_public_key)
        U = SHA256.new(ss).digest()
        zerome(ss)

        sk = U[0:16]
        nonce = U[16:32]
        zerome(U)

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

        # create plaintext section

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

        # generate plaintext header

        #            ------------------------------------------------
        #  fields   | content_type  | transaction_id | timestamp | ..
        #            ------------------------------------------------
        #  size     |       1       |        8       |     8     |  ?
        #            -------------------------------------------------

        transaction_id = transaction_id_to_u64(transaction_id)
        plaintext = struct.pack('<bQQ', content_type, transaction_id,
                                int(time.time()))

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

        utf8_callback_url = callback_url.encode('utf8')

        # enforce max url length as specified in protocol

        if len(utf8_callback_url) > 511:
            raise InvalidFunctionParameter('callback_url', 'max string '
                                           'length (encoded as utf8) is '
                                           '511')

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

        # create data package depending on content type

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

        if content_type == CONTENT_TYPE_PAIRING:

            #            -----------------------------------------
            #  fields   | header | serial | NUL | callback | NUL |
            #            -----------------------------------------
            #  size     |   9    |    ?   |  1  |     ?    |  1  |
            #            -----------------------------------------

            utf8_serial = serial.encode('utf8')

            if len(utf8_serial) > 63:
                raise ValueError('serial (encoded as utf8) can only be 63 '
                                 'characters long')

            plaintext += utf8_serial + b'\00' + utf8_callback_url + b'\00'

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

        if content_type == CONTENT_TYPE_SIGNREQ:

            if message is None:
                raise InvalidFunctionParameter('message', 'message must be '
                                               'supplied for content type '
                                               'SIGNREQ')

            #            ------------------------------------------
            #  fields   | header | message | NUL | callback | NUL |
            #            ------------------------------------------
            #  size     |   9    |    ?    |  1  |     ?    |  1  |
            #            ------------------------------------------

            utf8_message = message.encode('utf8')

            # enforce max sizes specified by protocol

            if len(utf8_message) > 511:
                raise InvalidFunctionParameter('message', 'max string '
                                               'length (encoded as utf8) is '
                                               '511')

            plaintext += utf8_message + b'\00' + utf8_callback_url + b'\00'

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

        if content_type == CONTENT_TYPE_LOGIN:

            if login is None:
                raise InvalidFunctionParameter('login', 'login must be '
                                               'supplied for content type '
                                               'LOGIN')
            if host is None:
                raise InvalidFunctionParameter('host', 'host must be '
                                               'supplied for content type '
                                               'LOGIN')

            #            -----------------------------------------------------
            #  fields   | header | login | NUL | host | NUL | callback | NUL |
            #            -----------------------------------------------------
            #  size     |   9    |   ?   |  1  |   ?  |  1  |     ?    |  1  |
            #            -----------------------------------------------------

            utf8_login = login.encode('utf8')
            utf8_host = host.encode('utf8')

            # enforce max sizes specified by protocol

            if len(utf8_login) > 127:
                raise InvalidFunctionParameter('login', 'max string '
                                               'length (encoded as utf8) is '
                                               '127')
            if len(utf8_host) > 255:
                raise InvalidFunctionParameter('host', 'max string '
                                               'length (encoded as utf8) is '
                                               '255')

            plaintext += utf8_login + b'\00'
            plaintext += utf8_host + b'\00'
            plaintext += utf8_callback_url + b'\00'

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

        # encrypt inner layer

        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)
        ciphertext = cipher.encrypt(plaintext)
        unsigned_raw_data = data_header + R + ciphertext

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

        # create signature

        partition = self.getFromTokenInfo('partition')
        secret_key = get_secret_key(partition)
        signature = crypto_sign_detached(unsigned_raw_data, secret_key)
        raw_data = unsigned_raw_data + signature

        url = 'lseqr://push/' + encode_base64_urlsafe(raw_data)

        return url, (signature + plaintext)
Esempio n. 12
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 MAC of the response didn't match

    :return:
        Parsed/encrpted PairingReponse
    """

    data = decode_base64_urlsafe(enc_pairing_response)

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

    #            -----------------------
    #  fields   | R  | ciphertext | MAC |
    #            -----------------------
    #  size     | 32 |      ?     | 16  |
    #            -----------------------

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

    R = data[0:32]
    ciphertext = data[32:-16]
    mac = data[-16:]

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

    # calculate the shared secret

    # ----

    secret_key = get_qrtoken_secret_key()
    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)
    plaintext = cipher.decrypt_and_verify(ciphertext, mac)

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

    # parse decrypted response

    # ----

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

    # Parse Pairing Reponse Header (First 6 Bytes)

    #            -------------------------------------------
    #  fields   | version  | type | user token id |   ...   |
    #            -------------------------------------------
    #  size     |    1     |  1   |       4       |    ?    |
    #            -------------------------------------------

    resp_header = plaintext[0:6]
    version, token_type, user_token_id = struct.unpack('<bbI', resp_header)

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

    if token_type != 2:
        raise ValueError('wrong token type in user response, '
                         'expected: %d, got: %d' % (2, token_type))

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

    # get user public key (next 32 bytes)

    #            -----------------------------
    #  fields   | ... | user public key | ... |
    #            -----------------------------
    #  size     |  6  |       32        |  ?  |
    #            -----------------------------

    user_public_key = plaintext[6:6+32]

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

    # get serial and/or user login

    #            ---------------------------------
    #  fields   | ... | serial | NUL | user login |
    #            ---------------------------------
    #  size     | 38  |   ?    |  1  |     ?      |
    #            ---------------------------------

    # parse token_serial and user identification

    serial_user_data = plaintext[6+32:].split(b'\x00')
    serial = serial_user_data[0].decode('utf8')
    user_login = serial_user_data[1].decode('utf8')

    return PairingResponse(user_public_key, user_token_id, serial, user_login)
Esempio n. 13
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 MAC of the response didn't match

    :return:
        Parsed/encrpted PairingReponse
    """

    data = decode_base64_urlsafe(enc_pairing_response)

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

    #            -----------------------
    #  fields   | R  | ciphertext | MAC |
    #            -----------------------
    #  size     | 32 |      ?     | 16  |
    #            -----------------------

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

    R = data[0:32]
    ciphertext = data[32:-16]
    mac = data[-16:]

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

    # calculate the shared secret

    # ----

    secret_key = get_qrtoken_dh_secret_key()
    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)
    plaintext = cipher.decrypt_and_verify(ciphertext, mac)

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

    # parse decrypted response

    # ----

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

    # Parse Pairing Reponse Header (First 6 Bytes)

    #            -------------------------------------------
    #  fields   | version  | type | user token id |   ...   |
    #            -------------------------------------------
    #  size     |    1     |  1   |       4       |    ?    |
    #            -------------------------------------------

    resp_header = plaintext[0:6]
    version, token_type, user_token_id = struct.unpack('<bbI', resp_header)

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

    if token_type != 2:
        raise ValueError('wrong token type in user response, '
                         'expected: %d, got: %d' % (2, token_type))

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

    # get user public key (next 32 bytes)

    #            -----------------------------
    #  fields   | ... | user public key | ... |
    #            -----------------------------
    #  size     |  6  |       32        |  ?  |
    #            -----------------------------

    user_public_key = plaintext[6:6+32]

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

    # get serial and/or user login

    #            ---------------------------------
    #  fields   | ... | serial | NUL | user login |
    #            ---------------------------------
    #  size     | 38  |   ?    |  1  |     ?      |
    #            ---------------------------------

    # parse token_serial and user identification

    serial_user_data = plaintext[6+32:].split(b'\x00')
    serial = serial_user_data[0].decode('utf8')
    user_login = serial_user_data[1].decode('utf8')

    return PairingResponse(user_public_key, user_token_id, serial, user_login)
Esempio n. 14
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
Esempio n. 15
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)
Esempio n. 16
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)
Esempio n. 17
0
    def create_challenge_url(self,
                             transaction_id,
                             content_type,
                             message,
                             callback_url,
                             callback_sms_number,
                             use_compression=False,
                             reset_url=False):
        """
        creates a challenge url (looking like lseqr://chal/<base64string>)
        from a challenge dictionary as provided by Challanges.create_challenge
        in lib.challenge

        the version identifier of the challenge url is currently hardcoded
        to 1.
        """

        serial = self.getSerial()

        if content_type is None:
            content_type = CONTENT_TYPE_FREE

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

        # sanity/format checks

        if content_type not in [
                CONTENT_TYPE_PAIRING, CONTENT_TYPE_AUTH, CONTENT_TYPE_FREE
        ]:
            raise InvalidFunctionParameter(
                'content_type', 'content_type must '
                'be CONTENT_TYPE_PAIRING, '
                'CONTENT_TYPE_AUTH or '
                'CONTENT_TYPE_FREE.')

        if content_type == CONTENT_TYPE_PAIRING and \
           message != serial:
            raise InvalidFunctionParameter(
                'message', 'message must be equal '
                'to serial in pairing mode')

        if content_type == CONTENT_TYPE_AUTH:
            if '@' not in message:
                raise InvalidFunctionParameter(
                    'message', 'For content type '
                    'auth, message must have format '
                    '<login>@<server>')

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

        #  after the lseqr://chal/ prefix the following data is encoded
        #  in urlsafe base64:

        #            ---------------------------------------------------
        #  fields   | version | user token id |  R  | ciphertext | MAC |
        #            ---------------------------------------------------
        #           |          header         |     |    EAX enc data  |
        #            ---------------------------------------------------
        #  size     |    1    |       4       |  32 |      ?     | 16  |
        #            ---------------------------------------------------
        #

        r = urandom(32)
        R = calc_dh_base(r)

        user_token_id = self.getFromTokenInfo('user_token_id')
        data_header = struct.pack('<bI', QRTOKEN_VERSION, user_token_id)

        # the user public key is saved as base64 in
        # the token info since the byte format is
        # incompatible with the json backend.

        b64_user_public_key = self.getFromTokenInfo('user_public_key')
        user_public_key = b64decode(b64_user_public_key)

        ss = calc_dh(r, user_public_key)
        U1 = sha256(ss).digest()
        U2 = sha256(U1).digest()
        zerome(ss)

        skA = U1[0:16]
        skB = U2[0:16]
        nonce = U2[16:32]
        zerome(U1)
        zerome(U2)

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

        # create plaintext section

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

        # create the bitmap for flags

        flags = 0

        if use_compression:
            flags |= CHALLENGE_HAS_COMPRESSION

        # FIXME: sizecheck for message, callback url, sms number
        # wiki specs are utf-8 byte length (without \0)

        if callback_url is not None:
            flags |= CHALLENGE_HAS_URL

        if callback_sms_number is not None:
            flags |= CHALLENGE_HAS_SMS_NUMBER

        if (content_type == CONTENT_TYPE_PAIRING):
            flags |= CHALLENGE_HAS_SIGNATURE

        if reset_url:
            flags |= CHALLENGE_SHOULD_RESET_URL
            flags |= CHALLENGE_HAS_SIGNATURE

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

        # generate plaintext header

        #            ----------------------------------------------
        #  fields   | content_type  | flags | transaction_id | ... |
        #            ----------------------------------------------
        #  size     |       1       |   1   |        8       |  ?  |
        #            ----------------------------------------------

        transaction_id = transaction_id_to_u64(transaction_id)
        pt_header = struct.pack('<bbQ', content_type, flags, transaction_id)
        plaintext = pt_header

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

        # create data package

        #            -------------------------------
        #  fields   | header  | message | NUL | ... |
        #            -------------------------------
        #  size     |   10    |    ?    |  1  |  ?  |
        #            -------------------------------

        data_package = b''
        utf8_message = message.encode('utf8')

        # enforce max sizes specified by protocol

        if content_type == CONTENT_TYPE_FREE and len(utf8_message) > 511:
            raise ParameterError('message (encoded as utf8) can only be 511 '
                                 'characters long')

        elif content_type == CONTENT_TYPE_PAIRING and len(utf8_message) > 63:
            raise InvalidFunctionParameter(
                'message', 'max string length '
                '(encoded as utf8) is 511 for '
                'content type PAIRING')

        elif content_type == CONTENT_TYPE_AUTH and len(utf8_message) > 511:
            raise InvalidFunctionParameter(
                'message', 'max string length '
                '(encoded as utf8) is 511 for '
                'content type AUTH')

        data_package += utf8_message + b'\x00'

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

        # depending on function parameters add callback url
        # and/or callback sms number

        #            -----------------------------------------------------
        #  fields   | ... | callback url | NUL | callback sms | NUL | ... |
        #            -----------------------------------------------------
        #  size     |  ?  |       ?      |  1  |       ?      |  1  |  ?  |
        #            -----------------------------------------------------

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

        if callback_url is not None:

            utf8_callback_url = callback_url.encode('utf8')

            # enforce max url length as specified in protocol

            if len(utf8_callback_url) > 511:
                raise InvalidFunctionParameter(
                    'callback_url', 'max string '
                    'length (encoded as utf8) is '
                    '511')

            data_package += utf8_callback_url + b'\x00'

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

        if callback_sms_number is not None:

            utf8_callback_sms_number = callback_sms_number.encode('utf8')

            if len(utf8_callback_sms_number) > 31:
                raise InvalidFunctionParameter(
                    'callback_sms_number', 'max string length (encoded '
                    'as utf8) is 31')

            data_package += utf8_callback_sms_number + b'\x00'

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

        if use_compression:
            maybe_compressed_data_package = zlib.compress(data_package, 9)
        else:
            maybe_compressed_data_package = data_package

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

        # when content type is pairing the protocol specifies that
        # the server must send a hmac based signature with the
        # response

        sig = ''
        sec_obj = self._get_secret_object()

        if flags & CHALLENGE_HAS_SIGNATURE:

            hmac_message = nonce + pt_header + maybe_compressed_data_package

            sig = sec_obj.hmac_digest(data_input=hmac_message,
                                      bkey=self.server_hmac_secret,
                                      hash_algo=sha256)

            plaintext += sig

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

        plaintext += maybe_compressed_data_package

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

        user_message = nonce + pt_header + sig + data_package

        user_sig = sec_obj.hmac_digest(data_input=user_message,
                                       bkey=skB,
                                       hash_algo=sha256)

        # the user sig will be given as urlsafe base64 in the
        # challenge response. for this reasons (and because we
        # need to serialize it into json) we convert the user_sig
        # into this format.

        user_sig = encode_base64_urlsafe(user_sig)

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

        cipher = AES.new(skA, AES.MODE_EAX, nonce)
        cipher.update(data_header)
        ciphertext, tag = cipher.encrypt_and_digest(plaintext)

        raw_data = data_header + R + ciphertext + tag
        url = 'lseqr://chal/' + encode_base64_urlsafe(raw_data)

        return url, user_sig
Esempio n. 18
0
    def create_challenge_url(self, transaction_id, content_type, message,
                             callback_url, callback_sms_number,
                             use_compression=False, reset_url=False):
        """
        creates a challenge url (looking like lseqr://chal/<base64string>)
        from a challenge dictionary as provided by Challanges.create_challenge
        in lib.challenge

        the version identifier of the challenge url is currently hardcoded
        to 1.
        """

        serial = self.getSerial()

        if content_type is None:
            content_type = CONTENT_TYPE_FREE

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

        # sanity/format checks

        if content_type not in [CONTENT_TYPE_PAIRING,
                                CONTENT_TYPE_AUTH, CONTENT_TYPE_FREE]:
            raise InvalidFunctionParameter('content_type', 'content_type must '
                                           'be CONTENT_TYPE_PAIRING, '
                                           'CONTENT_TYPE_AUTH or '
                                           'CONTENT_TYPE_FREE.')

        if content_type == CONTENT_TYPE_PAIRING and \
           message != serial:
            raise InvalidFunctionParameter('message', 'message must be equal '
                                           'to serial in pairing mode')

        if content_type == CONTENT_TYPE_AUTH:
            if '@' not in message:
                raise InvalidFunctionParameter('message', 'For content type '
                                               'auth, message must have format '
                                               '<login>@<server>')

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

        #  after the lseqr://chal/ prefix the following data is encoded
        #  in urlsafe base64:

        #            ---------------------------------------------------
        #  fields   | version | user token id |  R  | ciphertext | MAC |
        #            ---------------------------------------------------
        #           |          header         |     |    EAX enc data  |
        #            ---------------------------------------------------
        #  size     |    1    |       4       |  32 |      ?     | 16  |
        #            ---------------------------------------------------
        #

        r = urandom(32)
        R = calc_dh_base(r)

        user_token_id = self.getFromTokenInfo('user_token_id')
        data_header = struct.pack('<bI', QRTOKEN_VERSION, user_token_id)

        # the user public key is saved as base64 in
        # the token info since the byte format is
        # incompatible with the json backend.

        b64_user_public_key = self.getFromTokenInfo('user_public_key')
        user_public_key = b64decode(b64_user_public_key)

        ss = calc_dh(r, user_public_key)
        U1 = SHA256.new(ss).digest()
        U2 = SHA256.new(U1).digest()
        zerome(ss)

        skA = U1[0:16]
        skB = U2[0:16]
        nonce = U2[16:32]
        zerome(U1)
        zerome(U2)

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

        # create plaintext section

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

        # create the bitmap for flags

        flags = 0

        if use_compression:
            flags |= CHALLENGE_HAS_COMPRESSION

        # FIXME: sizecheck for message, callback url, sms number
        # wiki specs are utf-8 byte length (without \0)

        if callback_url is not None:
            flags |= CHALLENGE_HAS_URL

        if callback_sms_number is not None:
            flags |= CHALLENGE_HAS_SMS_NUMBER

        if (content_type == CONTENT_TYPE_PAIRING):
            flags |= CHALLENGE_HAS_SIGNATURE

        if reset_url:
            flags |= CHALLENGE_SHOULD_RESET_URL
            flags |= CHALLENGE_HAS_SIGNATURE

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

        # generate plaintext header

        #            ----------------------------------------------
        #  fields   | content_type  | flags | transaction_id | ... |
        #            ----------------------------------------------
        #  size     |       1       |   1   |        8       |  ?  |
        #            ----------------------------------------------

        transaction_id = transaction_id_to_u64(transaction_id)
        pt_header = struct.pack('<bbQ', content_type, flags, transaction_id)
        plaintext = pt_header

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

        # create data package

        #            -------------------------------
        #  fields   | header  | message | NUL | ... |
        #            -------------------------------
        #  size     |   10    |    ?    |  1  |  ?  |
        #            -------------------------------

        data_package = b''
        utf8_message = message.encode('utf8')

        # enforce max sizes specified by protocol

        if content_type == CONTENT_TYPE_FREE and len(utf8_message) > 511:
            raise ParameterError('message (encoded as utf8) can only be 511 '
                                 'characters long')

        elif content_type == CONTENT_TYPE_PAIRING and len(utf8_message) > 63:
            raise InvalidFunctionParameter('message', 'max string length '
                                           '(encoded as utf8) is 511 for '
                                           'content type PAIRING')

        elif content_type == CONTENT_TYPE_AUTH and len(utf8_message) > 511:
            raise InvalidFunctionParameter('message', 'max string length '
                                           '(encoded as utf8) is 511 for '
                                           'content type AUTH')

        data_package += utf8_message + b'\x00'

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

        # depending on function parameters add callback url
        # and/or callback sms number

        #            -----------------------------------------------------
        #  fields   | ... | callback url | NUL | callback sms | NUL | ... |
        #            -----------------------------------------------------
        #  size     |  ?  |       ?      |  1  |       ?      |  1  |  ?  |
        #            -----------------------------------------------------

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

        if callback_url is not None:

            utf8_callback_url = callback_url.encode('utf8')

            # enforce max url length as specified in protocol

            if len(utf8_callback_url) > 511:
                raise InvalidFunctionParameter('callback_url', 'max string '
                                               'length (encoded as utf8) is '
                                               '511')

            data_package += utf8_callback_url + b'\x00'

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

        if callback_sms_number is not None:

            utf8_callback_sms_number = callback_sms_number.encode('utf8')

            if len(utf8_callback_sms_number) > 31:
                raise InvalidFunctionParameter('callback_sms_number',
                                               'max string length (encoded '
                                               'as utf8) is 31')

            data_package += utf8_callback_sms_number + b'\x00'

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

        if use_compression:
            maybe_compressed_data_package = zlib.compress(data_package, 9)
        else:
            maybe_compressed_data_package = data_package

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

        # when content type is pairing the protocol specifies that
        # the server must send a hmac based signature with the
        # response

        sig = ''

        if flags & CHALLENGE_HAS_SIGNATURE:

            hmac_message = nonce + pt_header + maybe_compressed_data_package

            sig = HMAC.new(self.server_hmac_secret, hmac_message,
                           digestmod=SHA256).digest()

            plaintext += sig

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

        plaintext += maybe_compressed_data_package

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

        user_message = nonce + pt_header + sig + data_package
        user_sig = HMAC.new(skB, user_message, digestmod=SHA256).digest()

        # the user sig will be given as urlsafe base64 in the
        # challenge response. for this reasons (and because we
        # need to serialize it into json) we convert the user_sig
        # into this format.

        user_sig = encode_base64_urlsafe(user_sig)

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

        cipher = AES.new(skA, AES.MODE_EAX, nonce)
        cipher.update(data_header)
        ciphertext, tag = cipher.encrypt_and_digest(plaintext)

        raw_data = data_header + R + ciphertext + tag
        url = 'lseqr://chal/' + encode_base64_urlsafe(raw_data)

        return url, user_sig
Esempio n. 19
0
def parse_challenge_url(challenge_url):
    """ Parses a challenge url and prints its data """

    challenge_data_encoded = challenge_url[len('lseqr://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)
    if not version == QRTOKEN_VERSION:
        raise Exception('wrong qrtoken version')

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

    # get token from client token database

    token = token_db[user_token_id]

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

    # prepare decryption by seperating R from
    # ciphertext and tag

    R = challenge_data[5:5 + 32]
    ciphertext = challenge_data[5 + 32:-16]
    tag = challenge_data[-16:]

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

    # key derivation

    ss = calc_dh(secret_key, R)
    U1 = SHA256.new(ss).digest()
    U2 = SHA256.new(U1).digest()

    skA = U1[0:16]
    skB = U2[0:16]
    nonce = U2[16:32]

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

    # decrypt and verify challenge

    cipher = AES.new(skA, AES.MODE_EAX, nonce)
    cipher.update(header)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)

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

    # parse/check plaintext header

    pt_header = plaintext[0:10]
    content_type, flags, transaction_id = struct.unpack('<bbQ', pt_header)
    transaction_id = u64_to_transaction_id(transaction_id)

    # make sure a flag for the server signature is
    # present, if the content type is 'pairing'

    if content_type == QRTOKEN_CT_PAIR and not flags & FLAG_QR_SRVSIG:
        raise Exception('Ill formatted callenge url')

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

    # retrieve plaintext data depending on flags

    if flags & FLAG_QR_SRVSIG:

        # plaintext has a server signature as a header
        # extract it and check if it is correct

        server_signature = plaintext[10:10 + 32]
        data = plaintext[10 + 32:]

        # calculate secret

        server_public_key = token['server_public_key']
        secret = calc_dh(secret_key, server_public_key)

        # check hmac

        message = nonce + pt_header + data
        signed = HMAC.new(secret, msg=message, digestmod=SHA256).digest()

        if not server_signature == signed:
            raise Exception('HMAC signature check failed')

    else:

        # no server signature found - just remove
        # the plaintext header

        data = plaintext[10:]

        # we have to define an empty server signature in
        # here because we need it later to create the
        # client signature

        server_signature = b''

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

    # extract message and (optional) callback
    # parameters from data

    message, _, suffix = data.partition(b'\x00')

    callback_url = token['callback_url']
    if flags & FLAG_QR_HAVE_URL:
        callback_url, _, suffix = suffix.partition(b'\x00')

    callback_sms = token['callback_sms']
    if flags & FLAG_QR_HAVE_SMS:
        callback_sms, _, suffix = suffix.partition(b'\x00')

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

    # prepare the parsed challenge data

    challenge = {}
    challenge['message'] = message
    challenge['content_type'] = content_type
    challenge['callback_url'] = callback_url
    challenge['callback_sms'] = callback_sms
    challenge['transaction_id'] = transaction_id
    challenge['user_token_id'] = user_token_id

    # calculate signature and tan

    message = nonce + pt_header + data
    sig_hmac = HMAC.new(skB, message, digestmod=SHA256)
    sig = sig_hmac.digest()

    encoded_sig = encode_base64_urlsafe(sig)

    print('Data in URL:')

    for key, value in list(challenge.items()):
        print(('%s\n    %s\n' % (key, value)))

    return challenge, encoded_sig
def parse_challenge_url(challenge_url):

    """ Parses a challenge url and prints its data """

    challenge_data_encoded = challenge_url[len('lseqr://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)
    if not version == QRTOKEN_VERSION:
        raise Exception('wrong qrtoken version')


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

    # get token from client token database

    token = token_db[user_token_id]

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

    # prepare decryption by seperating R from
    # ciphertext and tag

    R = challenge_data[5:5+32]
    ciphertext = challenge_data[5+32:-16]
    tag = challenge_data[-16:]

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

    # key derivation

    ss = calc_dh(secret_key, R)
    U1 = SHA256.new(ss).digest()
    U2 = SHA256.new(U1).digest()

    skA = U1[0:16]
    skB = U2[0:16]
    nonce = U2[16:32]

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

    # decrypt and verify challenge

    cipher = AES.new(skA, AES.MODE_EAX, nonce)
    cipher.update(header)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)

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

    # parse/check plaintext header

    pt_header = plaintext[0:10]
    content_type, flags, transaction_id = struct.unpack('<bbQ', pt_header)
    transaction_id = u64_to_transaction_id(transaction_id)

    # make sure a flag for the server signature is
    # present, if the content type is 'pairing'

    if content_type == QRTOKEN_CT_PAIR and not flags & FLAG_QR_SRVSIG:
        raise Exception('Ill formatted callenge url')

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

    # retrieve plaintext data depending on flags

    if flags & FLAG_QR_SRVSIG:

        # plaintext has a server signature as a header
        # extract it and check if it is correct

        server_signature = plaintext[10:10+32]
        data = plaintext[10+32:]

        # calculate secret

        server_public_key = token['server_public_key']
        secret = calc_dh(secret_key, server_public_key)

        # check hmac

        message = nonce + pt_header + data
        signed = HMAC.new(secret, msg=message, digestmod=SHA256).digest()

        if not server_signature == signed:
            raise Exception('HMAC signature check failed')

    else:

        # no server signature found - just remove
        # the plaintext header

        data = plaintext[10:]

        # we have to define an empty server signature in
        # here because we need it later to create the
        # client signature

        server_signature = b''

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

    # extract message and (optional) callback
    # parameters from data

    message, _, suffix = data.partition(b'\x00')

    callback_url = token['callback_url']
    if flags & FLAG_QR_HAVE_URL:
        callback_url, _, suffix = suffix.partition(b'\x00')

    callback_sms = token['callback_sms']
    if flags & FLAG_QR_HAVE_SMS:
        callback_sms, _, suffix = suffix.partition(b'\x00')

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

    # prepare the parsed challenge data

    challenge = {}
    challenge['message'] = message
    challenge['content_type'] = content_type
    challenge['callback_url'] = callback_url
    challenge['callback_sms'] = callback_sms
    challenge['transaction_id'] = transaction_id
    challenge['user_token_id'] = user_token_id

    # calculate signature and tan

    message = nonce + pt_header + data
    sig_hmac = HMAC.new(skB, message, digestmod=SHA256)
    sig = sig_hmac.digest()

    encoded_sig = encode_base64_urlsafe(sig)

    print('Data in URL:')

    for key, value in challenge.items():
        print('%s\n    %s\n' % (key, value))

    return challenge, encoded_sig
Esempio n. 21
0
    def create_challenge_url(self,
                             transaction_id,
                             content_type,
                             callback_url='',
                             message=None,
                             login=None,
                             host=None):

        """
        creates a challenge url (looking like lseqr://push/<base64string>),
        returns the url and the unencrypted challenge data

        :param transaction_id: The transaction id generated by LinOTP

        :param content_type: One of the types CONTENT_TYPE_SIGNREQ,
            CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN

        :param callback_url: callback url (optional), default is
            empty string

        :param message: the transaction message, that should be signed
            by the client. Only for content type CONTENT_TYPE_SIGNREQ

        :param login: the login name of the user. Only for content type
            CONTENT_TYPE_LOGIN

        :param host: hostname of the user. Only for content type
            CONTENT_TYPE_LOGIN

        :returns: tuple (challenge_url, sig_base), with challenge_url being
            the push url and sig_base the message, that is used for
            the client signature
        """

        serial = self.getSerial()

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

        # sanity/format checks

        if content_type not in [CONTENT_TYPE_SIGNREQ,
                                CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN]:
            raise InvalidFunctionParameter('content_type', 'content_type must '
                                           'be CONTENT_TYPE_SIGNREQ, '
                                           'CONTENT_TYPE_PAIRING or '
                                           'CONTENT_TYPE_LOGIN.')

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

        #  after the lseqr://push/ prefix the following data is encoded
        #  in urlsafe base64:

        #            ---------------------------------------------------
        #  fields   | version | user token id |  R  | ciphertext | sign |
        #            ---------------------------------------------------
        #           |          header         |          body           |
        #            ---------------------------------------------------
        #  size     |    1    |       4       |  32 |      ?     |  64  |
        #            ---------------------------------------------------
        #

        # create header

        user_token_id = self.getFromTokenInfo('user_token_id')
        data_header = struct.pack('<bI', CHALLENGE_URL_VERSION, user_token_id)

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

        # create body

        r = urandom(32)
        R = calc_dh_base(r)

        b64_user_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key')
        user_dsa_public_key = b64decode(b64_user_dsa_public_key)
        user_dh_public_key = dsa_to_dh_public(user_dsa_public_key)

        ss = calc_dh(r, user_dh_public_key)
        U = SHA256.new(ss).digest()
        zerome(ss)

        sk = U[0:16]
        nonce = U[16:32]
        zerome(U)

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

        # create plaintext section

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

        # generate plaintext header

        #            ------------------------------------------------
        #  fields   | content_type  | transaction_id | timestamp | ..
        #            ------------------------------------------------
        #  size     |       1       |        8       |     8     |  ?
        #            -------------------------------------------------

        transaction_id = transaction_id_to_u64(transaction_id)
        plaintext = struct.pack('<bQQ', content_type, transaction_id,
                                int(time.time()))

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

        utf8_callback_url = callback_url.encode('utf8')

        # enforce max url length as specified in protocol

        if len(utf8_callback_url) > 511:
            raise InvalidFunctionParameter('callback_url', 'max string '
                                           'length (encoded as utf8) is '
                                           '511')

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

        # create data package depending on content type

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

        if content_type == CONTENT_TYPE_PAIRING:

            #            -----------------------------------------
            #  fields   | header | serial | NUL | callback | NUL |
            #            -----------------------------------------
            #  size     |   9    |    ?   |  1  |     ?    |  1  |
            #            -----------------------------------------

            utf8_serial = serial.encode('utf8')

            if len(utf8_serial) > 63:
                raise ValueError('serial (encoded as utf8) can only be 63 '
                                 'characters long')

            plaintext += utf8_serial + b'\00' + utf8_callback_url + b'\00'

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

        if content_type == CONTENT_TYPE_SIGNREQ:

            if message is None:
                raise InvalidFunctionParameter('message', 'message must be '
                                               'supplied for content type '
                                               'SIGNREQ')

            #            ------------------------------------------
            #  fields   | header | message | NUL | callback | NUL |
            #            ------------------------------------------
            #  size     |   9    |    ?    |  1  |     ?    |  1  |
            #            ------------------------------------------

            utf8_message = message.encode('utf8')

            # enforce max sizes specified by protocol

            if len(utf8_message) > 511:
                raise InvalidFunctionParameter('message', 'max string '
                                               'length (encoded as utf8) is '
                                               '511')

            plaintext += utf8_message + b'\00' + utf8_callback_url + b'\00'

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

        if content_type == CONTENT_TYPE_LOGIN:

            if login is None:
                raise InvalidFunctionParameter('login', 'login must be '
                                               'supplied for content type '
                                               'LOGIN')
            if host is None:
                raise InvalidFunctionParameter('host', 'host must be '
                                               'supplied for content type '
                                               'LOGIN')

            #            -----------------------------------------------------
            #  fields   | header | login | NUL | host | NUL | callback | NUL |
            #            -----------------------------------------------------
            #  size     |   9    |   ?   |  1  |   ?  |  1  |     ?    |  1  |
            #            -----------------------------------------------------

            utf8_login = login.encode('utf8')
            utf8_host = host.encode('utf8')

            # enforce max sizes specified by protocol

            if len(utf8_login) > 127:
                raise InvalidFunctionParameter('login', 'max string '
                                               'length (encoded as utf8) is '
                                               '127')
            if len(utf8_host) > 255:
                raise InvalidFunctionParameter('host', 'max string '
                                               'length (encoded as utf8) is '
                                               '255')

            plaintext += utf8_login + b'\00'
            plaintext += utf8_host + b'\00'
            plaintext += utf8_callback_url + b'\00'

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

        # encrypt inner layer

        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)
        ciphertext = cipher.encrypt(plaintext)
        unsigned_raw_data = data_header + R + ciphertext

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

        # create signature

        partition = self.getFromTokenInfo('partition')
        secret_key = get_secret_key(partition)
        signature = crypto_sign_detached(unsigned_raw_data, secret_key)
        raw_data = unsigned_raw_data + signature

        protocol_id = config.get('mobile_app_protocol_id', 'lseqr')
        url = protocol_id + '://push/' + encode_base64_urlsafe(raw_data)

        return url, (signature + plaintext)
Esempio n. 22
0
    def create_pairing_response(public_key: bytes,
                                secret_key: bytes,
                                token_info: Dict,
                                gda: str = 'DEADBEEF') -> str:
        """Creates a base64-encoded pairing response.

        :param public_key: the public key in bytes
        :param secret_key: the secret key in bytes
        :param token_info: the token_info dict
        :param user_token_id: the token id
        :param gda: the mobile device gda

        :returns base64 encoded pairing response
        """

        token_serial = token_info['serial']
        token_id = token_info.get('token_id', 1)
        server_public_key = token_info['server_public_key']
        partition = token_info['partition']

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

        # assemble header and plaintext

        header = struct.pack('<bI', PAIR_RESPONSE_VERSION, partition)

        pairing_response = b''
        pairing_response += struct.pack('<bI', TYPE_PUSHTOKEN, token_id)

        pairing_response += public_key

        pairing_response += token_serial.encode('utf8') + b'\x00\x00'
        pairing_response += gda.encode('utf-8') + b'\x00'

        signature = crypto_sign_detached(pairing_response, secret_key)
        pairing_response += signature

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

        # create public diffie hellman component
        # (used to decrypt and verify the reponse)

        r = os.urandom(32)
        R = calc_dh_base(r)

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

        # derive encryption key and nonce

        server_public_key_dh = dsa_to_dh_public(server_public_key)
        ss = calc_dh(r, server_public_key_dh)
        U = SHA256.new(ss).digest()
        encryption_key = U[0:16]
        nonce = U[16:32]

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

        # encrypt in EAX mode

        cipher = AES.new(encryption_key, AES.MODE_EAX, nonce)
        cipher.update(header)
        ciphertext, tag = cipher.encrypt_and_digest(pairing_response)

        return encode_base64_urlsafe(header + R + ciphertext + tag)
Esempio n. 23
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