def check_password(self, user_id: str, password: str): public_key_hash = bytes.fromhex(user_id.split(":", 1)[0][1:]) #signature_type = password.split(":")[0] signature = bytes.fromhex(password.split(":")[1]) public_key = bytes.fromhex(password.split(":")[2]) public_key_digest = pysodium.crypto_generichash(public_key) if public_key_hash.hex() == public_key_digest.hex(): try: message_digest = pysodium.crypto_generichash( u"login:{}".format(int(time.time()/(5*60))).encode()) pysodium.crypto_sign_verify_detached( signature, message_digest, public_key) if not (yield self.account_handler.check_user_exists(user_id)): self.log.info( "First user login, registering: user=%r", user_id.lower()) yield self.account_handler.register(localpart=public_key_digest.hex()) defer.returnValue(True) except Exception as exception: self.log.info( "Got exception while verifying signature: "+str(exception)) defer.returnValue(False) else: self.log.info( "pubkey hash did not match pubkey") defer.returnValue(False)
def signature_verifier_check (_peer_sign_pub_key, _local_sess_pub_key, _verifier) : log ("[e7943771]", "[crypto][signature]", "verifying `%s`...", _verifier.encode ("b64")) pysodium.crypto_sign_verify_detached (_verifier, _local_sess_pub_key, _peer_sign_pub_key) log ("[37e1f393]", "[crypto][signature]", "verified `%s`;", _local_sess_pub_key.encode ("b64"))
def test_private_key_no_encrypt(self): crypto.input = lambda prompt: 'n' private_key, public_key = crypto.make_keypair() key = crypto.unlock_private_key(private_key) sig = pysodium.crypto_sign_detached('test', key) pysodium.crypto_sign_verify_detached(sig, 'test', base64.b64decode(public_key))
def verify(self, data: bytes, key: PublicKey) -> bool: """Verifies the signature against the provided data and the public key.""" try: crypto_sign_verify_detached(self.value, data, key.value) return True except ValueError: # ValueError is raised if verification fails. return False
def authenticate(): """ This does two things, either validate a pre-existing session token or create a new one from a signed authentication token. """ client_ip = request.environ['REMOTE_ADDR'] repository = request.headers['repository'] if repository not in config['repositories']: return fail(no_such_repo_msg) # == repository_path = config['repositories'][repository]['path'] conn = auth_db_connect(cpjoin(repository_path, 'auth_transient.db')); gc_tokens(conn) gc_tokens(conn) # Allow resume of an existing session if 'session_token' in request.headers: session_token = request.headers['session_token'] conn.execute("delete from session_tokens where expires < ?", (time.time(),)); conn.commit() res = conn.execute("select * from session_tokens where token = ? and ip = ?", (session_token, client_ip)).fetchall() if res != []: return success({'session_token' : session_token}) else: return fail(user_auth_fail_msg) # Create a new session else: user = request.headers['user'] auth_token = request.headers['auth_token'] signiture = request.headers['signature'] try: public_key = config['users'][user]['public_key'] # signature pysodium.crypto_sign_verify_detached(base64.b64decode(signiture), auth_token, base64.b64decode(public_key)) # check token was previously issued by this system and is still valid res = conn.execute("select * from tokens where token = ? and ip = ? ", (auth_token, client_ip)).fetchall() # Validate token matches one we sent if res == [] or len(res) > 1: return fail(user_auth_fail_msg) # Does the user have permission to use this repository? if repository not in config['users'][user]['uses_repositories']: return fail(user_auth_fail_msg) # Everything OK conn.execute("delete from tokens where token = ?", (auth_token,)); conn.commit() # generate a session token and send it to the client session_token = base64.b64encode(pysodium.randombytes(35)) conn.execute("insert into session_tokens (token, expires, ip, username) values (?,?,?, ?)", (session_token, time.time() + extend_session_duration, client_ip, user)) conn.commit() return success({'session_token' : session_token}) except Exception: # pylint: disable=broad-except return fail(user_auth_fail_msg)
def is_valid_event(self, h, ev): try: crypto_sign_verify_detached(ev.s, dumps(ev[:-1]), ev.c) except ValueError: return False return ( crypto_generichash(dumps(ev)) == h and (ev.p == () or (len(ev.p) == 2 and ev.p[0] in self.hg and ev.p[1] in self.hg and self.hg[ev.p[0]].c == ev.c and self.hg[ev.p[1]].c != ev.c)))
def verify(message: bytes, signature: bytes, public_key: bytes) -> bool: """Verify an unsigned message with accompanying signature. :param message: The unsigned message to verify :param signature: The signature to be verified :param public_key: The public key to use during verifying """ try: crypto_sign_verify_detached(signature, message, public_key) except ValueError: return False return True
def validate(data): rc = bencode.bdecode(data) if b'z' not in rc or b'k' not in rc: return False sig = rc[b'z'] rc[b'z'] = b'\x00' * 64 buf = bencode.bencode(rc) try: k = rc[b'k'] pysodium.crypto_sign_verify_detached(sig, buf, k) except: return False else: return not _expired(rc[b't'])
def on_message(ws, data): #print("msg: ", data) msg = json.loads(data) #id = "".join(map(chr, (msg['id']))) id = msg['id'] if id == get_identify_reply_id: global remote_node_id pk = bytes(msg['result'][0]) sg = bytes(msg['result'][1]) remote_node_id = pk hex_id = binascii.hexlify(remote_node_id).decode("utf-8") try: pysodium.crypto_sign_verify_detached(sg, get_identify_reply_tn, pk) print("OK!!! node id: ", hex_id) except: print("error identify node id: ", hex_id) do_send_put(ws) return if id == get_reply_id: print("GET result!!!:\n", msg) return if id == get_fin_reply_id: print("GET finally result!!!:\n", msg) return if msg["method"] == "get_identify": tn = bytes(msg['params'][0]) digest = binascii.unhexlify( b'DAFBABCBC50DF4BB432C72F610878A6990E85734') with_digest = msg['params'][1] sm = tn if with_digest: sm.append(digest) sg = pysodium.crypto_sign_detached(sm, secretkey) ws.send( json.dumps({ "jsonrpc": "2.0", "id": msg['id'], "result": (list(bytearray(publickey)), list(bytearray(sg))) }))
def verify(self, signature: Union[str, bytes], message: Union[str, bytes]) -> None: """Verify signature, raise exception if it is not valid. :param message: sequance of bytes, raw format or hexadecimal notation :param signature: a signature in base58 encoding :raises: ValueError if signature is not valid """ encoded_signature = scrub_input(signature) encoded_message = scrub_input(message) if not self.public_point: raise ValueError("Cannot verify without a public key") if encoded_signature[:3] != b'sig': # not generic if self.curve != encoded_signature[:2]: # "sp", "p2" "ed" raise ValueError("Signature and public key curves mismatch.") decoded_signature = base58_decode(encoded_signature) # Ed25519 if self.curve == b"ed": digest = pysodium.crypto_generichash(encoded_message) try: pysodium.crypto_sign_verify_detached(decoded_signature, digest, self.public_point) except ValueError as exc: raise ValueError('Signature is invalid.') from exc # Secp256k1 elif self.curve == b"sp": pk = secp256k1.PublicKey(self.public_point, raw=True) sig = pk.ecdsa_deserialize_compact(decoded_signature) if not pk.ecdsa_verify(encoded_message, sig, digest=blake2b_32): raise ValueError('Signature is invalid.') # P256 elif self.curve == b"p2": pk = fastecdsa.encoding.sec1.SEC1Encoder.decode_public_key( self.public_point, curve=fastecdsa.curve.P256) r, s = bytes_to_int(decoded_signature[:32]), bytes_to_int( decoded_signature[32:]) if not fastecdsa.ecdsa.verify( sig=(r, s), msg=encoded_message, Q=pk, hashfunc=blake2b_32): raise ValueError('Signature is invalid.') else: raise Exception( f'Unknown elliptic curve {self.curve}') # type: ignore
def check_json_signature(message): message = message.copy() sender = message['sender'] del message['sender'] signature = message['signature'] del message['signature'] canonicalized = json.dumps(message, separators=(',', ':'), sort_keys=True) return pysodium.crypto_sign_verify_detached(base64.b64decode(signature), canonicalized, base64.b64decode(sender))
def verifyEd25519(sig, msg, vk): """ Returns True if signature sig of message msg is verified with verification key vk Otherwise False All of sig, msg, vk are bytes """ try: result = pysodium.crypto_sign_verify_detached(sig, msg, vk) except Exception as ex: return False return (True if result else False)
def auth(s, id, alpha): pk = load_blob(id, 'pub', 32) if pk is None: print('no pubkey found in %s' % id) fail(s) nonce = pysodium.randombytes(32) k = load_blob(id, 'key') if k is not None: try: beta = sphinxlib.respond(alpha, k) except: fail(s) else: beta = b'' s.send(b''.join([beta, nonce])) sig = s.recv(64) try: pysodium.crypto_sign_verify_detached(sig, nonce, pk) except: print('bad sig') fail(s)
def verify(self, signature, message): """ Verify signature, raise exception if it is not valid :param message: sequance of bytes, raw format or hexadecimal notation :param signature: a signature in base58 encoding """ signature = scrub_input(signature) message = scrub_input(message) if not self.public_point: raise ValueError("Cannot verify without a public key") if signature[:3] != b'sig': # not generic if self.curve != signature[:2]: # "sp", "p2" "ed" raise ValueError("Signature and public key curves mismatch.") signature = base58_decode(signature) # Ed25519 if self.curve == b"ed": digest = pysodium.crypto_generichash(message) try: pysodium.crypto_sign_verify_detached(signature, digest, self.public_point) except ValueError: raise ValueError('Signature is invalid.') # Secp256k1 elif self.curve == b"sp": pk = secp256k1.PublicKey(self.public_point, raw=True) sig = pk.ecdsa_deserialize_compact(signature) if not pk.ecdsa_verify(message, sig, digest=blake2b_32): raise ValueError('Signature is invalid.') # P256 elif self.curve == b"p2": pk = SEC1Encoder.decode_public_key(self.public_point, curve=P256) r, s = bytes_to_int(signature[:32]), bytes_to_int(signature[32:]) if not verify(sig=(r, s), msg=message, Q=pk, hashfunc=blake2b_32): raise ValueError('Signature is invalid.') else: assert False
def verify_client(clientid, message): try: data = json.loads(message) idkey_base64 = data["idkey"] idkey = binascii.a2b_base64(idkey_base64) payload_str = data["payload"] payload_bytes = payload_str.encode("utf-8") sig_base64 = data["signature"] sig = binascii.a2b_base64(sig_base64) try: pysodium.crypto_sign_verify_detached(sig, payload_bytes, idkey) except ValueError: print("client", clientid, "bad signature") return False client_info.setdefault(clientid, set()).add(idkey_base64) return True except: print("client", clientid, "malformed message") return False
def verify(token, key): token_header = token[:len(consts.public_header)] token_version = token[:2] if not compare_digest(token_version, consts.version): raise ValueError('not a v2 token') if not compare_digest(token_header, consts.public_header): raise ValueError('not a v2.public token') parts = token.split(b'.') footer = b'' if len(parts) == 4: encoded_footer = parts[-1] footer = b64decode(encoded_footer) decoded = b64decode(parts[2]) message = decoded[:-crypto_sign_BYTES] signature = decoded[-crypto_sign_BYTES:] try: crypto_sign_verify_detached(sig=signature, msg=pre_auth_encode( token_header, message, footer), pk=key) except ValueError as e: raise ValueError('invalid signature') from e return {'message': message, 'footer': footer}
def test_private_key_no_encrypt(self): crypto.raw_input = lambda : 'n' private_key, public_key = crypto.make_keypair() key = crypto.unlock_private_key(private_key) sig = pysodium.crypto_sign_detached('test', key) pysodium.crypto_sign_verify_detached(sig, 'test', base64.b64decode(public_key))
def parse_and_verify_pushtoken_pairing_data(plaintext): """ Parses the decrypted inner layer of a pairing response according to the PushToken Pairing data format :param plaintext: The plaintext reveceived from the decryption of the outer pairing response layer :raises ParameterError: If plaintext has a wrong format :raises ValueError: If signature check failed :return: PushTokenPairingData """ # ----------------------------------------------------------------------- -- # check format boundaries plaintext_min_length = 1 + 4 + 32 + 1 + 1 + 1 + 64 if len(plaintext) < plaintext_min_length: raise ParameterError('Malformed pairing response for type PushToken') # ----------------------------------------------------------------------- -- # get user token id (unique id on the client) # (token type was already processed in # decrypt_pairing_response function) # --------------------------------------------- # fields | token type | user token id | ... | sign | # --------------------------------------------- # size | 1 | 4 | ? | 64 | # --------------------------------------------- user_token_id = struct.unpack('<I', plaintext[1:5])[0] # ----------------------------------------------------------------------- -- # get user public key (next 32 bytes) # ------------------------------------ # fields | ... | user public key | ... | sign | # ------------------------------------ # size | 5 | 32 | ? | 64 | # ------------------------------------ user_public_key = plaintext[5:5+32] # ----------------------------------------------------------------------- -- # get serial, user login and gda # ---------------------------------------------------------- # fields | ... | serial | NUL | user login | NUL | gda | NUL | sign | # ---------------------------------------------------------- # size | 37 | ? | 1 | ? | 1 | ? | 1 | 64 | # ---------------------------------------------------------- # parse token_serial and user identification str_parts = plaintext[5+32:-64].split(b'\x00') # enforce format if not len(str_parts) == 3 + 1: raise ParameterError('Malformed pairing response for type PushToken') serial = str_parts[0].decode('utf8') user_login = str_parts[1].decode('utf8') gda = str_parts[2].decode('utf8') # ----------------------------------------------------------------------- -- # get signature and verify signature = plaintext[-64:] message = plaintext[:-64] try: crypto_sign_verify_detached(signature, message, user_public_key) except ValueError: # original value error is too generic raise ValueError('Invalid signature for pairing response data') # ----------------------------------------------------------------------- -- return PushTokenPairingData(user_public_key, user_token_id, serial, user_login, gda)
def parse_and_verify_pushtoken_pairing_data(plaintext): """ Parses the decrypted inner layer of a pairing response according to the PushToken Pairing data format :param plaintext: The plaintext reveceived from the decryption of the outer pairing response layer :raises ParameterError: If plaintext has a wrong format :raises ValueError: If signature check failed :return: PushTokenPairingData """ # -------------------------------------------------------------------------- # check format boundaries plaintext_min_length = 1 + 4 + 32 + 1 + 1 + 1 + 64 if len(plaintext) < plaintext_min_length: raise ParameterError('Malformed pairing response for type PushToken') # -------------------------------------------------------------------------- # get user token id (unique id on the client) # (token type was already processed in # decrypt_pairing_response function) # --------------------------------------------- # fields | token type | user token id | ... | sign | # --------------------------------------------- # size | 1 | 4 | ? | 64 | # --------------------------------------------- user_token_id = struct.unpack('<I', plaintext[1:5])[0] # -------------------------------------------------------------------------- # get user public key (next 32 bytes) # ------------------------------------ # fields | ... | user public key | ... | sign | # ------------------------------------ # size | 5 | 32 | ? | 64 | # ------------------------------------ user_public_key = plaintext[5:5+32] # -------------------------------------------------------------------------- # get serial, user login and gda # ---------------------------------------------------------- # fields | ... | serial | NUL | user login | NUL | gda | NUL | sign | # ---------------------------------------------------------- # size | 37 | ? | 1 | ? | 1 | ? | 1 | 64 | # ---------------------------------------------------------- # parse token_serial and user identification str_parts = plaintext[5+32:-64].split(b'\x00') # enforce format if not len(str_parts) == 3 + 1: raise ParameterError('Malformed pairing response for type PushToken') serial = str_parts[0].decode('utf8') user_login = str_parts[1].decode('utf8') gda = str_parts[2].decode('utf8') # -------------------------------------------------------------------------- # get signature and verify signature = plaintext[-64:] message = plaintext[:-64] try: crypto_sign_verify_detached(signature, message, user_public_key) except ValueError: # original value error is too generic raise ValueError('Invalid signature for pairing response data') # -------------------------------------------------------------------------- return PushTokenPairingData(user_public_key, user_token_id, serial, user_login, gda)
def verify_blob(msg, pk): sig = msg[-64:] msg = msg[:-64] pysodium.crypto_sign_verify_detached(sig, msg, pk) return msg
def authenticate(request: Request) -> Responce: """ This does two things, either validate a pre-existing session token or create a new one from a signed authentication token. """ client_ip = request.remote_addr repository = request.headers['repository'] if repository not in config['repositories']: return fail(no_such_repo_msg) # == repository_path = config['repositories'][repository]['path'] conn = auth_db_connect(cpjoin(repository_path, 'auth_transient.db')) gc_tokens(conn) gc_tokens(conn) # Allow resume of an existing session if 'session_token' in request.headers: session_token = request.headers['session_token'] conn.execute("delete from session_tokens where expires < ?", (time.time(), )) conn.commit() res = conn.execute( "select * from session_tokens where token = ? and ip = ?", (session_token, client_ip)).fetchall() if res != []: return success({'session_token': session_token}) else: return fail(user_auth_fail_msg) # Create a new session else: user = request.headers['user'] auth_token = request.headers['auth_token'] signiture = request.headers['signature'] try: public_key = config['users'][user]['public_key'] # signature pysodium.crypto_sign_verify_detached(base64.b64decode(signiture), auth_token, base64.b64decode(public_key)) # check token was previously issued by this system and is still valid res = conn.execute( "select * from tokens where token = ? and ip = ? ", (auth_token, client_ip)).fetchall() # Validate token matches one we sent if res == [] or len(res) > 1: return fail(user_auth_fail_msg) # Does the user have permission to use this repository? if repository not in config['users'][user]['uses_repositories']: return fail(user_auth_fail_msg) # Everything OK conn.execute("delete from tokens where token = ?", (auth_token, )) conn.commit() # generate a session token and send it to the client session_token = base64.b64encode(pysodium.randombytes(35)) conn.execute( "insert into session_tokens (token, expires, ip, username) values (?,?,?, ?)", (session_token, time.time() + extend_session_duration, client_ip, user)) conn.commit() return success({'session_token': session_token}) except Exception: # pylint: disable=broad-except return fail(user_auth_fail_msg)
def run_server(): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((server_ip, server_port)) log('[INFO] Listening on '+server_ip+':'+str(server_port)) # NONCE(24) + MAC(16) + MAGIC(8) + SIGNPUB(32) + SIG(64) + COUNTER(14) + LEN(2) + REST PROTO_MIN_SIZE = 160 magic_bin = '42f9708e2f1369d9'.decode('hex') # chosen by fair die server_ip_bin = socket.inet_aton(server_ip) # FIXME: lock state file for duration of the program counter = read_counter() while True: try: client_ip, client_port = (None, None) cdata, (client_ip, client_port) = sock.recvfrom(1024) if len(cdata) < PROTO_MIN_SIZE: # normal: a UDP scan would cause this log('[INFO] small packet', client_ip, client_port) continue pkt_nonce = cdata[:pysodium.crypto_secretbox_NONCEBYTES] try: plain = pysodium.crypto_secretbox_open(cdata[pysodium.crypto_secretbox_NONCEBYTES:], pkt_nonce, server_private_key) except: # normal-ish: any big enough packet to the port will cause # this, but unless knockd happens to run on a common UDP # port there's not much reason to see such a big packet log('[INFO] incorrect ciphertext', client_ip, client_port) continue pkt_magic = plain[:8] plain = plain[8:] if pkt_magic != magic_bin: # unrecognised protocol version log('[WARN] unrecognised version magic', client_ip, client_port, pkt_magic.encode('hex')) continue pkt_sign_pub = plain[:32] plain = plain[32:] if pkt_sign_pub not in client_sign_keys: log('[WARN] unrecognised client sign key', client_ip, client_port, pkt_sign_pub.encode('hex')) continue pkt_sig = plain[:64] plain = plain[64:] pkt_counter_bin = plain[:14] pkt_counter = int(pkt_counter_bin.encode('hex'), 16) plain = plain[14:] # check counter early so we don't need to verify replays if pkt_counter <= counter: log('[POSSIBLE_REPLAY] bad counter', client_ip, client_port) continue try: pysodium.crypto_sign_verify_detached(pkt_sig, (pkt_nonce + magic_bin + pkt_sign_pub + pkt_counter_bin + plain + server_private_key + server_ip_bin), pkt_sign_pub) except: # shenanigans, the data isn't signed by that user log('[SEVERE] bad signature', client_ip, client_port) continue pkt_data_len_bin = plain[:2] pkt_data_len = int(pkt_data_len_bin.encode('hex'), 16) plain = plain[2:] pkt_data = plain[:pkt_data_len] # Cool, write that counter ASAP so the packet can't be used again counter = pkt_counter write_counter(counter) log('[INFO] valid knock', client_ip, client_port) # NB. do *not* trust that client_ip is the originator (beyond # port knocking functionality). A person who can perform # packet captures can race the packet from their own IP subprocess.Popen([knocked_command, client_ip, pkt_data]) except KeyboardInterrupt: break except Exception as e: log('[ERROR] packet exception', client_ip, client_port, e)
def verifyPropose(self, data, addr): propose_tuple = sts_utility.deconstructPropose(data) # 1. validate certificate # 2. validate cert status # 3. Message signature check # 4. Check capability # 1. Certificate Checks if propose_tuple['C']['I'] not in STS.CAcerts.keys(): print('wrong issuer principal') return False for I, _ in STS.CAcerts.items(): if propose_tuple['C']['I'] == I: ca_cert_tuple = STS.CAcerts[I] try: pysodium.crypto_sign_verify_detached( propose_tuple['C']['G'], propose_tuple['C_B'][:-64] + (0).to_bytes(64, byteorder='big'), ca_cert_tuple['K_S']) except: print('wrong certificate signature') return False # CA DEPTH if ca_cert_tuple['C'][0] == 0: print('wrong CA depth') return False if not self.subjectPrincipalCheck(addr, propose_tuple['C']): print('wrong subject principal') return False # 4. NODE CAPABILITIES if not (propose_tuple['C']['C'][1:4] == b'\x00\x00\x1f' or propose_tuple['C']['C'][1:4] == b'\x00\x00?'): print('wrong capabilities') return False # 2. Certificate Status check try: pysodium.crypto_sign_verify_detached( propose_tuple['S']['G'], propose_tuple['S_B'][:-64] + (0).to_bytes(64, byteorder='big'), ca_cert_tuple['K_S']) except: print('wrong status signature') return False if pysodium.crypto_generichash(propose_tuple['C_B'], outlen=64) != propose_tuple['S']['H']: print('wrong status HASH') return False if propose_tuple['S']['S'] != 1: print('wrong cert status validity') return False currTime = int(time.time()) if propose_tuple['S']['F'] + propose_tuple['S']['U'] <= currTime: print('wrong cert status, validity expired') return False # 3. Message signature: try: pysodium.crypto_sign_verify_detached( propose_tuple['G'], data[:-64] + (0).to_bytes(64, byteorder='big'), propose_tuple['C']['K_S']) except: print('wrong propose message signature') return False return True
def test_pysodium(): """ Test all the functions needed from pysodium libarary (libsodium) """ # crypto_sign signatures with Ed25519 keys # create keypair without seed verkey, sigkey = pysodium.crypto_sign_keypair() assert len(verkey) == 32 == pysodium.crypto_sign_PUBLICKEYBYTES assert len(sigkey) == 64 == pysodium.crypto_sign_SECRETKEYBYTES assert 32 == pysodium.crypto_sign_SEEDBYTES sigseed = pysodium.randombytes(pysodium.crypto_sign_SEEDBYTES) assert len(sigseed) == 32 # seed = (b'J\xeb\x06\xf2BA\xd6/T\xe1\xe2\xe2\x838\x8a\x99L\xd9\xb5(\\I\xccRb\xc8\xd5\xc7Y\x1b\xb6\xf0') # Ann's seed sigseed = ( b'PTi\x15\xd5\xd3`\xf1u\x15}^r\x9bfH\x02l\xc6\x1b\x1d\x1c\x0b9\xd7{\xc0_\xf2K\x93`' ) assert len(sigseed) == 32 # try key stretching from 16 bytes using pysodium.crypto_pwhash() assert 16 == pysodium.crypto_pwhash_SALTBYTES salt = pysodium.randombytes(pysodium.crypto_pwhash_SALTBYTES) assert len(salt) == 16 # salt = b'\x19?\xfa\xc7\x8f\x8b\x7f\x8b\xdbS"$\xd7[\x85\x87' # algorithm default is argon2id sigseed = pysodium.crypto_pwhash( outlen=32, passwd="", salt=salt, opslimit=pysodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, memlimit=pysodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, alg=pysodium.crypto_pwhash_ALG_DEFAULT) assert len(sigseed) == 32 # seed = (b'\xa9p\x89\x7f+\x0e\xc4\x9c\xf2\x01r\xafTI\xc0\xfa\xac\xd5\x99\xf8O\x8f=\x843\xa2\xb6e\x9fO\xff\xd0') # creates signing/verification key pair from seed verkey, sigkey = pysodium.crypto_sign_seed_keypair(sigseed) assert len(verkey) == 32 assert len(sigkey) == 64 # sigkey is seed and verkey concatenated. Libsodium does this as an optimization # because the signing scheme needs both the private key (seed) and the public key so # instead of recomputing the public key each time from the secret key it requires # the public key as an input of and instead of two separate inputs, one for the # secret key and one for the public key, it uses a concatenated form. # Essentially crypto_sign_seed_keypair and crypto_sign_keypair return redundant # information in the duple (verkey, sigkey) because sigkey includes verkey # so one could just store sigkey and extract verkey or sigseed when needed # or one could just store verkey and sigseed and reconstruct sigkey when needed. # crypto_sign_detached requires sigkey (sigseed + verkey) # crypto_sign_verify_detached reqires verkey only # https://crypto.stackexchange.com/questions/54353/why-are-nacl-secret-keys-64-bytes-for-signing-but-32-bytes-for-box assert sigseed == sigkey[:32] assert verkey == sigkey[32:] assert sigkey == sigseed + verkey # vk = (b'B\xdd\xbb}8V\xa0\xd6lk\xcf\x15\xad9\x1e\xa7\xa1\xfe\xe0p<\xb6\xbex\xb0s\x8d\xd6\xf5\xa5\xe8Q') # utility function to extract seed from secret sigkey (really just extracting from front half) assert sigseed == pysodium.crypto_sign_sk_to_seed(sigkey) assert 64 == pysodium.crypto_sign_BYTES msg = "The lazy dog jumped over the river" msgb = msg.encode( "utf-8") # must convert unicode string to bytes in order to sign it assert msgb == b'The lazy dog jumped over the river' sig = pysodium.crypto_sign_detached(msgb, sigseed + verkey) # sigkey = seed + verkey assert len(sig) == 64 """ sig = (b"\x99\xd2<9$$0\x9fk\xfb\x18\xa0\x8c@r\x122.k\xb2\xc7\x1fp\x0e'm\x8f@" b'\xaa\xa5\x8c\xc8n\x85\xc8!\xf6q\x91p\xa9\xec\xcf\x92\xaf)\xde\xca' b'\xfc\x7f~\xd7o|\x17\x82\x1d\xd4<o"\x81&\t') """ #siga = pysodium.crypto_sign(msg.encode("utf-8"), sk)[:pysodium.crypto_sign_BYTES] #assert len(siga) == 64 #assert sig == siga try: # verify returns None if valid else raises ValueError result = pysodium.crypto_sign_verify_detached(sig, msgb, verkey) except Exception as ex: assert False assert not result assert result is None sigbad = sig[:-1] sigbad += b'A' try: # verify returns None if valid else raises ValueError result = pysodium.crypto_sign_verify_detached(sigbad, msgb, verkey) except Exception as ex: assert True assert isinstance(ex, ValueError) # crypto_box authentication encryption with X25519 keys apubkey, aprikey = pysodium.crypto_box_keypair() assert len(apubkey) == 32 == pysodium.crypto_box_SECRETKEYBYTES assert len(aprikey) == 32 == pysodium.crypto_box_PUBLICKEYBYTES repubkey = pysodium.crypto_scalarmult_curve25519_base(aprikey) assert repubkey == apubkey assert 32 == pysodium.crypto_box_SEEDBYTES boxseed = pysodium.randombytes(pysodium.crypto_box_SEEDBYTES) assert len(boxseed) == 32 bpubkey, bprikey = pysodium.crypto_box_seed_keypair(boxseed) assert len(bpubkey) == 32 assert len(bprikey) == 32 repubkey = pysodium.crypto_scalarmult_curve25519_base(bprikey) assert repubkey == bpubkey assert 24 == pysodium.crypto_box_NONCEBYTES nonce = pysodium.randombytes(pysodium.crypto_box_NONCEBYTES) assert len(nonce) == 24 # nonce = b'\x11\xfbi<\xf2\xb6k\xa05\x0c\xf9\x86t\x07\x8e\xab\x8a\x97nG\xe8\x87,\x94' atob_tx = "Hi Bob I'm Alice" atob_txb = atob_tx.encode("utf-8") # Detached recomputes shared key every time. # A encrypt to B acrypt, amac = pysodium.crypto_box_detached(atob_txb, nonce, bpubkey, aprikey) amacl = pysodium.crypto_box_MACBYTES assert amacl == 16 # amac = b'\xa1]\xc6ML\xe2\xa9:\xc0\xdc\xab\xa5\xc4\xc7\xf4\xdb' # acrypt = (b'D\n\x17\xb6z\xd8+t)\xcc`y\x1d\x10\x0cTC\x02\xb5@\xe2\xf2\xc9-(\xec*O\xb8~\xe2\x1a\xebO') # when transmitting prepend amac to crypt acipher = pysodium.crypto_box(atob_txb, nonce, bpubkey, aprikey) assert acipher == amac + acrypt atob_rxb = pysodium.crypto_box_open_detached(acrypt, amac, nonce, apubkey, bprikey) atob_rx = atob_rxb.decode("utf-8") assert atob_rx == atob_tx assert atob_rxb == atob_txb atob_rxb = pysodium.crypto_box_open(acipher, nonce, apubkey, bprikey) atob_rx = atob_rxb.decode("utf-8") assert atob_rx == atob_tx assert atob_rxb == atob_txb btoa_tx = "Hello Alice I am Bob" btoa_txb = btoa_tx.encode("utf-8") # B encrypt to A bcrypt, bmac = pysodium.crypto_box_detached(btoa_txb, nonce, apubkey, bprikey) # bmac = b'\x90\xe07=\xd22\x8fh2\xff\xdd\x84tC\x053' # bcrypt = (b'8\xb5\xba\xe7\xcc\xae B\xefx\xe6{U\xf7\xefA\x00\xc7|\xdbu\xcfc\x01$\xa9\xa2P\xa7\x84\xa5\xae\x180') # when transmitting prepend amac to crypt bcipher = pysodium.crypto_box(btoa_txb, nonce, apubkey, bprikey) assert bcipher == bmac + bcrypt btoa_rxb = pysodium.crypto_box_open_detached(bcrypt, bmac, nonce, bpubkey, aprikey) btoa_rx = btoa_rxb.decode("utf-8") assert btoa_rx == btoa_tx assert btoa_rxb == btoa_txb btoa_rxb = pysodium.crypto_box_open(bcipher, nonce, bpubkey, aprikey) btoa_rx = btoa_rxb.decode("utf-8") assert btoa_rx == btoa_tx assert btoa_rxb == btoa_txb # compute shared key asymkey = pysodium.crypto_box_beforenm(bpubkey, aprikey) bsymkey = pysodium.crypto_box_beforenm(apubkey, bprikey) assert asymkey == bsymkey acipher = pysodium.crypto_box_afternm(atob_txb, nonce, asymkey) atob_rxb = pysodium.crypto_box_open_afternm(acipher, nonce, bsymkey) assert atob_rxb == atob_txb bcipher = pysodium.crypto_box_afternm(btoa_txb, nonce, bsymkey) btoa_rxb = pysodium.crypto_box_open_afternm(bcipher, nonce, asymkey) assert btoa_rxb == btoa_txb # crypto_box_seal public key encryption with X25519 keys # uses same X25519 type of keys as crypto_box authenticated encryption # so when converting sign key Ed25519 to X25519 can use for both types of encryption pubkey, prikey = pysodium.crypto_box_keypair() assert len(pubkey) == 32 == pysodium.crypto_box_PUBLICKEYBYTES assert len(prikey) == 32 == pysodium.crypto_box_SECRETKEYBYTES assert 48 == pysodium.crypto_box_SEALBYTES msg_txb = "Catch me if you can.".encode("utf-8") assert msg_txb == b'Catch me if you can.' cipher = pysodium.crypto_box_seal(msg_txb, pubkey) assert len(cipher) == 48 + len(msg_txb) msg_rxb = pysodium.crypto_box_seal_open(cipher, pubkey, prikey) assert msg_rxb == msg_txb # convert Ed25519 key pair to X25519 key pair # https://blog.filippo.io/using-ed25519-keys-for-encryption/ # https://libsodium.gitbook.io/doc/advanced/ed25519-curve25519 # crypto_sign_ed25519_pk_to_curve25519 # crypto_sign_ed25519_sk_to_curve25519 pubkey = pysodium.crypto_sign_pk_to_box_pk(verkey) assert len(pubkey) == pysodium.crypto_box_PUBLICKEYBYTES prikey = pysodium.crypto_sign_sk_to_box_sk(sigkey) assert len(prikey) == pysodium.crypto_box_SECRETKEYBYTES repubkey = pysodium.crypto_scalarmult_curve25519_base(prikey) assert repubkey == pubkey msg_txb = "Encoded using X25519 key converted from Ed25519 key".encode( "utf-8") cipher = pysodium.crypto_box_seal(msg_txb, pubkey) assert len(cipher) == 48 + len(msg_txb) msg_rxb = pysodium.crypto_box_seal_open(cipher, pubkey, prikey) assert msg_rxb == msg_txb """
def decrypt_and_verify_challenge(self, challenge_url, action): """ Decrypts the data packed in the challenge url, verifies its content, returns the parsed data as a dictionary, calculates and returns the signature. The calling method must then send the signature back to the server. (The reason for this control flow is that the challenge data must be checked in different scenarios, e.g. when we have a pairing the data must be checked by the method that simulates the pairing) :param challenge_url: the challenge url as sent by the server :param action: a string identifier for the verification action (at the moment 'ACCEPT' or 'DENY') :returns: (challenge, signature) challenge has the keys * content_type - one of the three values CONTENT_TYPE_SIGNREQ, CONTENT_TYPE_PAIRING or CONTENT_TYPE_LOGIN) (all defined in this module) * transaction_id - used to identify the challenge on the server * callback_url (optional) - the url to which the challenge response should be set * user_token_id - used to identify the token in the user database for which this challenge was created depending on the content type additional keys are present * for CONTENT_TYPE_PAIRING: serial * for CONTENT_TYPE_SIGNREQ: message * for CONTENT_TYPE_LOGIN: login, host signature is the generated user signature used to respond to the challenge """ challenge_data_encoded = challenge_url[len(self.uri + '://chal/'):] challenge_data = decode_base64_urlsafe(challenge_data_encoded) # ------------------------------------------------------------------ -- # parse and verify header information in the # encrypted challenge data header = challenge_data[0:5] version, user_token_id = struct.unpack('<bI', header) self.assertEqual(version, CHALLENGE_URL_VERSION) # ------------------------------------------------------------------ -- # get token from client token database token = self.tokens[user_token_id] server_public_key = token['server_public_key'] # ------------------------------------------------------------------ -- # prepare decryption by seperating R from # ciphertext and server signature R = challenge_data[5:5 + 32] ciphertext = challenge_data[5 + 32:-64] server_signature = challenge_data[-64:] # check signature data = challenge_data[0:-64] crypto_sign_verify_detached(server_signature, data, server_public_key) # ------------------------------------------------------------------ -- # key derivation secret_key_dh = dsa_to_dh_secret(self.secret_key) ss = calc_dh(secret_key_dh, R) U = SHA256.new(ss).digest() sk = U[0:16] nonce = U[16:32] # ------------------------------------------------------------------ -- # decrypt and verify challenge 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) plaintext = cipher.decrypt(ciphertext) # ------------------------------------------------------------------ -- # parse/check plaintext header # 1 - for content type # 8 - for transaction id # 8 - for time stamp offset = 1 + 8 + 8 pt_header = plaintext[0:offset] (content_type, transaction_id, _time_stamp) = struct.unpack('<bQQ', pt_header) transaction_id = u64_to_transaction_id(transaction_id) # ------------------------------------------------------------------ -- # prepare the parsed challenge data challenge = {} challenge['content_type'] = content_type # ------------------------------------------------------------------ -- # retrieve plaintext data depending on content_type if content_type == CONTENT_TYPE_PAIRING: serial, callback_url, __ = plaintext[offset:].split('\x00') challenge['serial'] = serial elif content_type == CONTENT_TYPE_SIGNREQ: message, callback_url, __ = plaintext[offset:].split('\x00') challenge['message'] = message elif content_type == CONTENT_TYPE_LOGIN: login, host, callback_url, __ = plaintext[offset:].split('\x00') challenge['login'] = login challenge['host'] = host # ------------------------------------------------------------------ -- # prepare the parsed challenge data challenge['callback_url'] = callback_url challenge['transaction_id'] = transaction_id challenge['user_token_id'] = user_token_id # calculate signature sig_base = (struct.pack('<b', CHALLENGE_URL_VERSION) + b'%s\0' % action + server_signature + plaintext) sig = crypto_sign_detached(sig_base, self.secret_key) encoded_sig = encode_base64_urlsafe(sig) return challenge, encoded_sig
def decrypt_and_verify_challenge(self, challenge_url, action): """ Decrypts the data packed in the challenge url, verifies its content, returns the parsed data as a dictionary, calculates and returns the signature. The calling method must then send the signature back to the server. (The reason for this control flow is that the challenge data must be checked in different scenarios, e.g. when we have a pairing the data must be checked by the method that simulates the pairing) :param challenge_url: the challenge url as sent by the server :param action: a string identifier for the verification action (at the moment 'ACCEPT' or 'DENY') :returns: (challenge, signature) challenge has the keys * content_type - one of the three values CONTENT_TYPE_SIGNREQ, CONTENT_TYPE_PAIRING or CONTENT_TYPE_LOGIN) (all defined in this module) * transaction_id - used to identify the challenge on the server * callback_url (optional) - the url to which the challenge response should be set * user_token_id - used to identify the token in the user database for which this challenge was created depending on the content type additional keys are present * for CONTENT_TYPE_PAIRING: serial * for CONTENT_TYPE_SIGNREQ: message * for CONTENT_TYPE_LOGIN: login, host signature is the generated user signature used to respond to the challenge """ challenge_data_encoded = challenge_url[len(self.uri + '://chal/'):] challenge_data = decode_base64_urlsafe(challenge_data_encoded) # ------------------------------------------------------------------ -- # parse and verify header information in the # encrypted challenge data header = challenge_data[0:5] version, user_token_id = struct.unpack('<bI', header) self.assertEqual(version, CHALLENGE_URL_VERSION) # ------------------------------------------------------------------ -- # get token from client token database token = self.tokens[user_token_id] server_public_key = token['server_public_key'] # ------------------------------------------------------------------ -- # prepare decryption by seperating R from # ciphertext and server signature R = challenge_data[5:5 + 32] ciphertext = challenge_data[5 + 32:-64] server_signature = challenge_data[-64:] # check signature data = challenge_data[0:-64] crypto_sign_verify_detached(server_signature, data, server_public_key) # ------------------------------------------------------------------ -- # key derivation secret_key_dh = dsa_to_dh_secret(self.secret_key) ss = calc_dh(secret_key_dh, R) U = SHA256.new(ss).digest() sk = U[0:16] nonce = U[16:32] # ------------------------------------------------------------------ -- # decrypt and verify challenge 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) plaintext = cipher.decrypt(ciphertext) # ------------------------------------------------------------------ -- # parse/check plaintext header # 1 - for content type # 8 - for transaction id # 8 - for time stamp offset = 1 + 8 + 8 pt_header = plaintext[0:offset] (content_type, transaction_id, _time_stamp) = struct.unpack('<bQQ', pt_header) transaction_id = u64_to_transaction_id(transaction_id) # ------------------------------------------------------------------ -- # prepare the parsed challenge data challenge = {} challenge['content_type'] = content_type # ------------------------------------------------------------------ -- # retrieve plaintext data depending on content_type if content_type == CONTENT_TYPE_PAIRING: serial, callback_url, __ = plaintext[offset:].split('\x00') challenge['serial'] = serial elif content_type == CONTENT_TYPE_SIGNREQ: message, callback_url, __ = plaintext[offset:].split('\x00') challenge['message'] = message elif content_type == CONTENT_TYPE_LOGIN: login, host, callback_url, __ = plaintext[offset:].split('\x00') challenge['login'] = login challenge['host'] = host # ------------------------------------------------------------------ -- # prepare the parsed challenge data challenge['callback_url'] = callback_url challenge['transaction_id'] = transaction_id challenge['user_token_id'] = user_token_id # calculate signature sig_base = ( struct.pack('<b', CHALLENGE_URL_VERSION) + b'%s\0' % action + server_signature + plaintext) sig = crypto_sign_detached(sig_base, self.secret_key) encoded_sig = encode_base64_urlsafe(sig) return challenge, encoded_sig