示例#1
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)
示例#2
0
def generate_pairing_url(token_type,
                         partition=None,
                         serial=None,
                         callback_url=None,
                         callback_sms_number=None,
                         otp_pin_length=None,
                         hash_algorithm=None,
                         use_cert=False):
    """
    Generates a pairing url that should be sent to the client.

    Mandatory parameters:

    :param: token_type The token type for which this url is generated
        as a string (currently supported is only 'qr')

    Optional parameters:

    :param partition: A partition id that should be used during pairing.
        Partitions identitify a subspace of tokens, that share a common
        key pair. This currently defaults to the enum id of the token
        type when set to None and is reserved for future use.

    :param serial: When a token for the client was already enrolled
        (e.g. manually in the manage interface) its serial has to be
        sent to the client. When serial is not specified the client will
        receive a so-called 'anonymous pairing url' with no token data
        inside it. The token will then be created after the server
        received a pairing response from the client.

    :param callback_url: A callback URL that should be used by the client
        to sent back the pairing reponse. Please note, that this url will
        be cached by the client and used in the challenge step, if the
        challenge doesn't provide a custom url

    :param callback_sms_number: A sms number that can be used by the client
        to send back the pairing response. Typically this is used as a
        fallback for offline pairing.
        As with the callback url please note, that the number will be
        cached by the client. If you want a different number in the
        challenge step you have to send it inside the challenge as
        specified in the challenge protocol

    :param otp_pin_length: The number of digits the otp has to consist of.

    :param hash_algorithm: A string value that signifies the hash algorithm
        used in calculating the hmac. Currently the values 'sha1', 'sha256',
        'sha512' are supported. If the parameter is left out the default
        depends on the token type. qrtoken uses sha256 as default, while
        hotp/totp uses sha1.

    :param use_cert: A boolean, if a server certificate should be used
        in the pairing url

    The function can raise several exceptions:

    :raises InvalidFunctionParameter: If the string given in token_type
        doesn't match a supported token type

    :raises InvalidFunctionParameter: If the string given in hash_algorithm
        doesn't match a supported hash algorithm

    :raises InvalidFunctionParameter: If public key has a different size
        than 32 bytes

    :raises InvalidFunctionParameter: If otp_pin_length value is not between
        1 and 127

    :return: Pairing URL string
    """

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

    # check the token type

    try:
        TOKEN_TYPE = TOKEN_TYPES[token_type]
    except KeyError:
        allowed_types = ', '.join(TOKEN_TYPES.keys())
        raise InvalidFunctionParameter(
            'token_type', 'Unsupported token type %s. Supported '
            'types for pairing are: %s' % (token_type, allowed_types))

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

    # initialize the flag bitmap

    flags = 0

    if not use_cert:
        flags |= FLAG_PAIR_PK
    if serial is not None:
        flags |= FLAG_PAIR_SERIAL
    if callback_url is not None:
        flags |= FLAG_PAIR_CBURL
    if callback_sms_number is not None:
        flags |= FLAG_PAIR_CBSMS
    if hash_algorithm is not None:
        flags |= FLAG_PAIR_HMAC
    if otp_pin_length is not None:
        flags |= FLAG_PAIR_DIGITS

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

    #            ---------------------------- --
    #  fields   | version | type | flags | ... |
    #            ---------------------------- --
    #  size     |    1    |  1   |   4   |  ?  |
    #            ---------------------------- --

    data = struct.pack('<bbI', PAIR_URL_VERSION, TOKEN_TYPE, flags)

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

    #            --------------------- --
    #  fields   | ... | partition | ... |
    #            --------------------- --
    #  size     |  6  |     4     |  ?  |
    #            --------------------- --

    data += struct.pack('<I', partition)

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

    #            ------------------------------ --
    #  fields   | .... | server public key | ... |
    #            ------------------------------ --
    #  size     |  10  |        32         |  ?  |
    #            ------------------------------ --

    if flags & FLAG_PAIR_PK:

        server_public_key = get_public_key(partition)

        if len(server_public_key) != 32:
            raise InvalidFunctionParameter('server_public_key',
                                           'Public key must be 32 bytes long')

        data += server_public_key

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

    # Depending on flags additional data may be sent. If serial was provided
    # serial will be sent back. If callback url or callback sms was provided
    # the corresponding data will be added, too

    #            ------------------------------------------------------- --
    #  fields   | .... | serial | NUL | cb url | NUL | cb sms | NUL | ... |
    #            ------------------------------------------------------- --
    #  size     |  42  |   ?    |  1  |   ?    |  1  |   ?    |  1  |  ?  |
    #            ------------------------------------------------------- --

    if flags & FLAG_PAIR_SERIAL:
        data += serial.encode('utf8') + b'\x00'
    if flags & FLAG_PAIR_CBURL:
        data += callback_url.encode('utf8') + b'\x00'
    if flags & FLAG_PAIR_CBSMS:
        data += callback_sms_number.encode('utf8') + b'\x00'

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

    # Other optional values: allowed pin length of otp (number of digits)
    # and custom hash algorithm

    #            ------------------------------------- --
    #  fields   | ... | otp pin length | hash_algorithm |
    #            ------------------------------------- --
    #  size     |  ?  |       1        |       1        |
    #            ------------------------------------- --

    if flags & FLAG_PAIR_DIGITS:
        if not (6 <= otp_pin_length <= 12):
            raise InvalidFunctionParameter(
                'otp_pin_length', 'Pin length must '
                'be in the range 6..12')
        data += struct.pack('<b', otp_pin_length)

    if flags & FLAG_PAIR_HMAC:
        try:
            HASH_ALGO = hash_algorithms[hash_algorithm]
        except KeyError:
            allowed_values = ", ".join(hash_algorithms.keys())
            raise InvalidFunctionParameter(
                'hash_algorithm', 'Unsupported hash algorithm %s, '
                'allowed values are %s' % (hash_algorithm, allowed_values))
        data += struct.pack('<b', HASH_ALGO)

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

    # TODO missing token details for other protocols (hotp, hmac, etc)
    # * counter (u64le)
    # * tstart (u64le)
    # * tstep (u32le)

    if not (flags & FLAG_PAIR_PK):

        secret_key = get_secret_key(partition)
        server_sig = crypto_sign_detached(data, secret_key)
        data += server_sig

    protocol_id = config.get('mobile_app_protocol_id', 'lseqr')
    return protocol_id + '://pair/' + encode_base64_urlsafe(data)
示例#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)
示例#4
0
def generate_pairing_url(token_type,
                         partition=None,
                         serial=None,
                         callback_url=None,
                         callback_sms_number=None,
                         otp_pin_length=None,
                         hash_algorithm=None,
                         use_cert=False):

    """
    Generates a pairing url that should be sent to the client.

    Mandatory parameters:

    :param: token_type The token type for which this url is generated
        as a string (currently supported is only 'qr')

    Optional parameters:

    :param partition: A partition id that should be used during pairing.
        Partitions identitify a subspace of tokens, that share a common
        key pair. This currently defaults to the enum id of the token
        type when set to None and is reserved for future use.

    :param serial: When a token for the client was already enrolled
        (e.g. manually in the manage interface) its serial has to be
        sent to the client. When serial is not specified the client will
        receive a so-called 'anonymous pairing url' with no token data
        inside it. The token will then be created after the server
        received a pairing response from the client.

    :param callback_url: A callback URL that should be used by the client
        to sent back the pairing reponse. Please note, that this url will
        be cached by the client and used in the challenge step, if the
        challenge doesn't provide a custom url

    :param callback_sms_number: A sms number that can be used by the client
        to send back the pairing response. Typically this is used as a
        fallback for offline pairing.
        As with the callback url please note, that the number will be
        cached by the client. If you want a different number in the
        challenge step you have to send it inside the challenge as
        specified in the challenge protocol

    :param otp_pin_length: The number of digits the otp has to consist of.

    :param hash_algorithm: A string value that signifies the hash algorithm
        used in calculating the hmac. Currently the values 'sha1', 'sha256',
        'sha512' are supported. If the parameter is left out the default
        depends on the token type. qrtoken uses sha256 as default, while
        hotp/totp uses sha1.

    :param use_cert: A boolean, if a server certificate should be used
        in the pairing url

    The function can raise several exceptions:

    :raises InvalidFunctionParameter: If the string given in token_type
        doesn't match a supported token type

    :raises InvalidFunctionParameter: If the string given in hash_algorithm
        doesn't match a supported hash algorithm

    :raises InvalidFunctionParameter: If public key has a different size
        than 32 bytes

    :raises InvalidFunctionParameter: If otp_pin_length value is not between
        1 and 127

    :return: Pairing URL string
    """

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

    # check the token type

    try:
        TOKEN_TYPE = TOKEN_TYPES[token_type]
    except KeyError:
        allowed_types = ', '.join(TOKEN_TYPES.keys())
        raise InvalidFunctionParameter('token_type',
                                       'Unsupported token type %s. Supported '
                                       'types for pairing are: %s' %
                                       (token_type, allowed_types))

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

    # initialize the flag bitmap

    flags = 0

    if not use_cert:
        flags |= FLAG_PAIR_PK
    if serial is not None:
        flags |= FLAG_PAIR_SERIAL
    if callback_url is not None:
        flags |= FLAG_PAIR_CBURL
    if callback_sms_number is not None:
        flags |= FLAG_PAIR_CBSMS
    if hash_algorithm is not None:
        flags |= FLAG_PAIR_HMAC
    if otp_pin_length is not None:
        flags |= FLAG_PAIR_DIGITS

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

    #            ---------------------------- --
    #  fields   | version | type | flags | ... |
    #            ---------------------------- --
    #  size     |    1    |  1   |   4   |  ?  |
    #            ---------------------------- --

    data = struct.pack('<bbI', PAIR_URL_VERSION, TOKEN_TYPE, flags)

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

    #            --------------------- --
    #  fields   | ... | partition | ... |
    #            --------------------- --
    #  size     |  6  |     4     |  ?  |
    #            --------------------- --

    data += struct.pack('<I', partition)

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

    #            ------------------------------ --
    #  fields   | .... | server public key | ... |
    #            ------------------------------ --
    #  size     |  10  |        32         |  ?  |
    #            ------------------------------ --

    if flags & FLAG_PAIR_PK:

        server_public_key = get_public_key(partition)

        if len(server_public_key) != 32:
            raise InvalidFunctionParameter('server_public_key',
                                           'Public key must be 32 bytes long')

        data += server_public_key

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

    # Depending on flags additional data may be sent. If serial was provided
    # serial will be sent back. If callback url or callback sms was provided
    # the corresponding data will be added, too

    #            ------------------------------------------------------- --
    #  fields   | .... | serial | NUL | cb url | NUL | cb sms | NUL | ... |
    #            ------------------------------------------------------- --
    #  size     |  42  |   ?    |  1  |   ?    |  1  |   ?    |  1  |  ?  |
    #            ------------------------------------------------------- --

    if flags & FLAG_PAIR_SERIAL:
        data += serial.encode('utf8') + b'\x00'
    if flags & FLAG_PAIR_CBURL:
        data += callback_url.encode('utf8') + b'\x00'
    if flags & FLAG_PAIR_CBSMS:
        data += callback_sms_number.encode('utf8') + b'\x00'

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

    # Other optional values: allowed pin length of otp (number of digits)
    # and custom hash algorithm

    #            ------------------------------------- --
    #  fields   | ... | otp pin length | hash_algorithm |
    #            ------------------------------------- --
    #  size     |  ?  |       1        |       1        |
    #            ------------------------------------- --

    if flags & FLAG_PAIR_DIGITS:
        if not(6 <= otp_pin_length <= 12):
            raise InvalidFunctionParameter('otp_pin_length', 'Pin length must '
                                           'be in the range 6..12')
        data += struct.pack('<b', otp_pin_length)

    if flags & FLAG_PAIR_HMAC:
        try:
            HASH_ALGO = hash_algorithms[hash_algorithm]
        except KeyError:
            allowed_values = ", ".join(hash_algorithms.keys())
            raise InvalidFunctionParameter('hash_algorithm',
                                           'Unsupported hash algorithm %s, '
                                           'allowed values are %s' %
                                           (hash_algorithm, allowed_values))
        data += struct.pack('<b', HASH_ALGO)

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

    # TODO missing token details for other protocols (hotp, hmac, etc)
    # * counter (u64le)
    # * tstart (u64le)
    # * tstep (u32le)

    if not (flags & FLAG_PAIR_PK):

        secret_key = get_secret_key(partition)
        server_sig = crypto_sign_detached(data, secret_key)
        data += server_sig

    protocol_id = config.get('mobile_app_protocol_id', 'lseqr')
    return protocol_id + '://pair/' + encode_base64_urlsafe(data)