def create_pairing_response_by_serial(self, user_token_id, gda=None): """ Creates a base64-encoded pairing response that identifies the token by its serial :param user_token_id: the token id (primary key for the user token db) :returns base64 encoded pairing response """ if not gda: gda = self.gda token_serial = self.tokens[user_token_id]['serial'] server_public_key = self.tokens[user_token_id]['server_public_key'] partition = self.tokens[user_token_id]['partition'] # ------------------------------------------------------------------ -- # assemble header and plaintext header = struct.pack('<bI', PAIR_RESPONSE_VERSION, partition) pairing_response = b'' pairing_response += struct.pack('<bI', TYPE_PUSHTOKEN, user_token_id) pairing_response += self.public_key pairing_response += token_serial.encode('utf8') + b'\x00\x00' pairing_response += gda.encode('utf-8') + b'\x00' signature = crypto_sign_detached(pairing_response, self.secret_key) pairing_response += signature # ------------------------------------------------------------------ -- # create public diffie hellman component # (used to decrypt and verify the reponse) r = os.urandom(32) R = calc_dh_base(r) # ------------------------------------------------------------------ -- # derive encryption key and nonce server_public_key_dh = dsa_to_dh_public(server_public_key) ss = calc_dh(r, server_public_key_dh) U = SHA256.new(ss).digest() encryption_key = U[0:16] nonce = U[16:32] # ------------------------------------------------------------------ -- # encrypt in EAX mode cipher = AES.new(encryption_key, AES.MODE_EAX, nonce) cipher.update(header) ciphertext, tag = cipher.encrypt_and_digest(pairing_response) return encode_base64_urlsafe(header + R + ciphertext + tag)
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 send_pairing_response(pairing_url): user_token_id = parse_pairing_url(pairing_url) serial = token_db[user_token_id]['serial'] # ---------------------------------------------------------------------- server_public_key = token_db[user_token_id]['server_public_key'] pairing_response = b'' pairing_response += struct.pack('<bbI', RESPONSE_VERSION, TYPE_QRTOKEN, user_token_id) pairing_response += public_key pairing_response += serial.encode('utf8') + b'\x00\x00' # ---------------------------------------------------------------------- # create public diffie hellman component # (used to decrypt and verify the reponse) r = os.urandom(32) R = calc_dh_base(r) # ---------------------------------------------------------------------- # derive encryption key and nonce ss = calc_dh(r, server_public_key) U = SHA256.new(ss).digest() encryption_key = U[0:16] nonce = U[16:32] # ---------------------------------------------------------------------- # encrypt in EAX mode cipher = AES.new(encryption_key, AES.MODE_EAX, nonce) ciphertext, tag = cipher.encrypt_and_digest(pairing_response) pairing_response = encode_base64_urlsafe(R + ciphertext + tag) params = { 'session': SESSION, 'pairing_response': pairing_response, 'type': 'qr' } r = requests.request('get', 'http://localhost:5001/admin/init', params=params, cookies=cookies) print((r.status_code)) print('----------------------------------') print((r.content))
def send_pairing_response(pairing_url): user_token_id = parse_pairing_url(pairing_url) serial = token_db[user_token_id]['serial'] # ---------------------------------------------------------------------- server_public_key = token_db[user_token_id]['server_public_key'] pairing_response = b'' pairing_response += struct.pack('<bbI', RESPONSE_VERSION, TYPE_QRTOKEN, user_token_id) pairing_response += public_key pairing_response += serial.encode('utf8') + b'\x00\x00' # ---------------------------------------------------------------------- # create public diffie hellman component # (used to decrypt and verify the reponse) r = os.urandom(32) R = calc_dh_base(r) # ---------------------------------------------------------------------- # derive encryption key and nonce ss = calc_dh(r, server_public_key) U = SHA256.new(ss).digest() encryption_key = U[0:16] nonce = U[16:32] # ---------------------------------------------------------------------- # encrypt in EAX mode cipher = AES.new(encryption_key, AES.MODE_EAX, nonce) ciphertext, tag = cipher.encrypt_and_digest(pairing_response) pairing_response = encode_base64_urlsafe(R + ciphertext + tag) params = {'session': SESSION, 'pairing_response': pairing_response, 'type': 'qr' } r = requests.request('get', 'http://localhost:5001/admin/init', params=params, cookies=cookies) print(r.status_code) print('----------------------------------') print(r.content)
def create_pairing_response(public_key, token_info, token_id=1): """ Creates a base64-encoded pairing response that identifies the token by its serial :param user_token_id: the token id (primary key for the user token db) :returns base64 encoded pairing response """ token_serial = token_info['serial'] server_public_key = token_info['server_public_key'] partition = token_info['partition'] header = struct.pack('<bI', PAIR_RESPONSE_VERSION, partition) pairing_response = b'' pairing_response += struct.pack('<bI', TYPE_QRTOKEN, token_id) pairing_response += public_key pairing_response += token_serial.encode('utf8') + b'\x00\x00' # ------------------------------------------------------------------- -- # create public diffie hellman component # (used to decrypt and verify the reponse) r = os.urandom(32) R = calc_dh_base(r) # ------------------------------------------------------------------- -- # derive encryption key and nonce ss = calc_dh(r, server_public_key) U = SHA256.new(ss).digest() encryption_key = U[0:16] nonce = U[16:32] # ------------------------------------------------------------------- -- # encrypt in EAX mode cipher = AES.new(encryption_key, AES.MODE_EAX, nonce) cipher.update(header) ciphertext, tag = cipher.encrypt_and_digest(pairing_response) return encode_base64_urlsafe(header + R + ciphertext + tag)
def init_qrtoken_secret_key(config): """ create an elliptic curve secret + public key pair and store it in the linotp config """ import linotp.lib.config secret_key = os.urandom(32) secret_key_entry = 'qrtokensk:' + base64.b64encode(secret_key) linotp.lib.config.storeConfig(key='QrTokenSecretKey', val=secret_key_entry, typ='password') public_key = calc_dh_base(secret_key) public_key_entry = 'qrtokenpk:' + base64.b64encode(public_key) linotp.lib.config.storeConfig(key='QrTokenPublicKey', val=public_key_entry) return
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)
def create_challenge_url(self, transaction_id, content_type, message, callback_url, callback_sms_number, use_compression=False, reset_url=False): """ creates a challenge url (looking like lseqr://chal/<base64string>) from a challenge dictionary as provided by Challanges.create_challenge in lib.challenge the version identifier of the challenge url is currently hardcoded to 1. """ serial = self.getSerial() if content_type is None: content_type = CONTENT_TYPE_FREE # ---------------------------------------------------------------------- # sanity/format checks if content_type not in [CONTENT_TYPE_PAIRING, CONTENT_TYPE_AUTH, CONTENT_TYPE_FREE]: raise InvalidFunctionParameter('content_type', 'content_type must ' 'be CONTENT_TYPE_PAIRING, ' 'CONTENT_TYPE_AUTH or ' 'CONTENT_TYPE_FREE.') if content_type == CONTENT_TYPE_PAIRING and \ message != serial: raise InvalidFunctionParameter('message', 'message must be equal ' 'to serial in pairing mode') if content_type == CONTENT_TYPE_AUTH: if '@' not in message: raise InvalidFunctionParameter('message', 'For content type ' 'auth, message must have format ' '<login>@<server>') # ---------------------------------------------------------------------- # after the lseqr://chal/ prefix the following data is encoded # in urlsafe base64: # --------------------------------------------------- # fields | version | user token id | R | ciphertext | MAC | # --------------------------------------------------- # | header | | EAX enc data | # --------------------------------------------------- # size | 1 | 4 | 32 | ? | 16 | # --------------------------------------------------- # r = urandom(32) R = calc_dh_base(r) user_token_id = self.getFromTokenInfo('user_token_id') data_header = struct.pack('<bI', QRTOKEN_VERSION, user_token_id) # the user public key is saved as base64 in # the token info since the byte format is # incompatible with the json backend. b64_user_public_key = self.getFromTokenInfo('user_public_key') user_public_key = b64decode(b64_user_public_key) ss = calc_dh(r, user_public_key) U1 = SHA256.new(ss).digest() U2 = SHA256.new(U1).digest() zerome(ss) skA = U1[0:16] skB = U2[0:16] nonce = U2[16:32] zerome(U1) zerome(U2) # ---------------------------------------------------------------------- # create plaintext section # ---------------------------------------------------------------------- # create the bitmap for flags flags = 0 if use_compression: flags |= CHALLENGE_HAS_COMPRESSION # FIXME: sizecheck for message, callback url, sms number # wiki specs are utf-8 byte length (without \0) if callback_url is not None: flags |= CHALLENGE_HAS_URL if callback_sms_number is not None: flags |= CHALLENGE_HAS_SMS_NUMBER if (content_type == CONTENT_TYPE_PAIRING): flags |= CHALLENGE_HAS_SIGNATURE if reset_url: flags |= CHALLENGE_SHOULD_RESET_URL flags |= CHALLENGE_HAS_SIGNATURE #---------------------------------------------------------------------- # generate plaintext header # ---------------------------------------------- # fields | content_type | flags | transaction_id | ... | # ---------------------------------------------- # size | 1 | 1 | 8 | ? | # ---------------------------------------------- transaction_id = transaction_id_to_u64(transaction_id) pt_header = struct.pack('<bbQ', content_type, flags, transaction_id) plaintext = pt_header #---------------------------------------------------------------------- # create data package # ------------------------------- # fields | header | message | NUL | ... | # ------------------------------- # size | 10 | ? | 1 | ? | # ------------------------------- data_package = b'' utf8_message = message.encode('utf8') # enforce max sizes specified by protocol if content_type == CONTENT_TYPE_FREE and len(utf8_message) > 511: raise ParameterError('message (encoded as utf8) can only be 511 ' 'characters long') elif content_type == CONTENT_TYPE_PAIRING and len(utf8_message) > 63: raise InvalidFunctionParameter('message', 'max string length ' '(encoded as utf8) is 511 for ' 'content type PAIRING') elif content_type == CONTENT_TYPE_AUTH and len(utf8_message) > 511: raise InvalidFunctionParameter('message', 'max string length ' '(encoded as utf8) is 511 for ' 'content type AUTH') data_package += utf8_message + b'\x00' # ---------------------------------------------------------------------- # depending on function parameters add callback url # and/or callback sms number # ----------------------------------------------------- # fields | ... | callback url | NUL | callback sms | NUL | ... | # ----------------------------------------------------- # size | ? | ? | 1 | ? | 1 | ? | # ----------------------------------------------------- # ---------------------------------------------------------------------- if callback_url is not None: utf8_callback_url = callback_url.encode('utf8') # enforce max url length as specified in protocol if len(utf8_callback_url) > 511: raise InvalidFunctionParameter('callback_url', 'max string ' 'length (encoded as utf8) is ' '511') data_package += utf8_callback_url + b'\x00' # ---------------------------------------------------------------------- if callback_sms_number is not None: utf8_callback_sms_number = callback_sms_number.encode('utf8') if len(utf8_callback_sms_number) > 31: raise InvalidFunctionParameter('callback_sms_number', 'max string length (encoded ' 'as utf8) is 31') data_package += utf8_callback_sms_number + b'\x00' # ---------------------------------------------------------------------- if use_compression: maybe_compressed_data_package = zlib.compress(data_package, 9) else: maybe_compressed_data_package = data_package # ---------------------------------------------------------------------- # when content type is pairing the protocol specifies that # the server must send a hmac based signature with the # response sig = '' if flags & CHALLENGE_HAS_SIGNATURE: hmac_message = nonce + pt_header + maybe_compressed_data_package sig = HMAC.new(self.server_hmac_secret, hmac_message, digestmod=SHA256).digest() plaintext += sig # ---------------------------------------------------------------------- plaintext += maybe_compressed_data_package # ---------------------------------------------------------------------- user_message = nonce + pt_header + sig + data_package user_sig = HMAC.new(skB, user_message, digestmod=SHA256).digest() # the user sig will be given as urlsafe base64 in the # challenge response. for this reasons (and because we # need to serialize it into json) we convert the user_sig # into this format. user_sig = encode_base64_urlsafe(user_sig) # ---------------------------------------------------------------------- cipher = AES.new(skA, AES.MODE_EAX, nonce) cipher.update(data_header) ciphertext, tag = cipher.encrypt_and_digest(plaintext) raw_data = data_header + R + ciphertext + tag url = 'lseqr://chal/' + encode_base64_urlsafe(raw_data) return url, user_sig
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
def create_keys(): secret_key = os.urandom(32) public_key = calc_dh_base(secret_key) return secret_key, public_key
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)
def create_pairing_response(public_key: bytes, secret_key: bytes, token_info: Dict, gda: str = 'DEADBEEF') -> str: """Creates a base64-encoded pairing response. :param public_key: the public key in bytes :param secret_key: the secret key in bytes :param token_info: the token_info dict :param user_token_id: the token id :param gda: the mobile device gda :returns base64 encoded pairing response """ token_serial = token_info['serial'] token_id = token_info.get('token_id', 1) server_public_key = token_info['server_public_key'] partition = token_info['partition'] # ------------------------------------------------------------------ -- # assemble header and plaintext header = struct.pack('<bI', PAIR_RESPONSE_VERSION, partition) pairing_response = b'' pairing_response += struct.pack('<bI', TYPE_PUSHTOKEN, token_id) pairing_response += public_key pairing_response += token_serial.encode('utf8') + b'\x00\x00' pairing_response += gda.encode('utf-8') + b'\x00' signature = crypto_sign_detached(pairing_response, secret_key) pairing_response += signature # ------------------------------------------------------------------ -- # create public diffie hellman component # (used to decrypt and verify the reponse) r = os.urandom(32) R = calc_dh_base(r) # ------------------------------------------------------------------ -- # derive encryption key and nonce server_public_key_dh = dsa_to_dh_public(server_public_key) ss = calc_dh(r, server_public_key_dh) U = SHA256.new(ss).digest() encryption_key = U[0:16] nonce = U[16:32] # ------------------------------------------------------------------ -- # encrypt in EAX mode cipher = AES.new(encryption_key, AES.MODE_EAX, nonce) cipher.update(header) ciphertext, tag = cipher.encrypt_and_digest(pairing_response) return encode_base64_urlsafe(header + R + ciphertext + tag)