Exemple #1
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
Exemple #2
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)
Exemple #3
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)