コード例 #1
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)
コード例 #2
0
ファイル: test_pushtoken.py プロジェクト: alexdutton/LinOTP
    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)
コード例 #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

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

        return url, (signature + plaintext)
コード例 #4
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)