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)
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, challenge_data), with challenge_url being the push url and challenge data being the data, that will be used as message in the signing step. """ 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 | ... | # -------------------------------------- # size | 1 | 8 | ? | # -------------------------------------- transaction_id = transaction_id_to_u64(transaction_id) pt_header = struct.pack('<bQ', content_type, transaction_id) plaintext = pt_header # ---------------------------------------------------------------------- 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, plaintext