def test_x448(backend, wycheproof): assert set(wycheproof.testgroup.items()) == { ("curve", "curve448"), ("type", "XdhComp"), } private_key = X448PrivateKey.from_private_bytes( binascii.unhexlify(wycheproof.testcase["private"])) public_key_bytes = binascii.unhexlify(wycheproof.testcase["public"]) if len(public_key_bytes) == 57: assert wycheproof.acceptable assert wycheproof.has_flag("NonCanonicalPublic") with pytest.raises(ValueError): X448PublicKey.from_public_bytes(public_key_bytes) return public_key = X448PublicKey.from_public_bytes(public_key_bytes) assert wycheproof.valid or wycheproof.acceptable expected = binascii.unhexlify(wycheproof.testcase["shared"]) if expected == b"\x00" * 56: assert wycheproof.acceptable # OpenSSL returns an error on all zeros shared key with pytest.raises(ValueError): private_key.exchange(public_key) else: assert private_key.exchange(public_key) == expected
def test_x448_unsupported(backend): with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM): X448PublicKey.from_public_bytes(b"0" * 56) with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM): X448PrivateKey.from_private_bytes(b"0" * 56) with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM): X448PrivateKey.generate()
def test_x448_unsupported(backend): with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM): X448PublicKey.from_public_bytes(b"0" * 56) with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM): X448PrivateKey.from_private_bytes(b"0" * 56) with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_EXCHANGE_ALGORITHM): X448PrivateKey.generate()
def update_url_token(url_token_private_key: 'X448PrivateKey', ut_pubkey_hex: str, cached_pk: str, onion_pub_key: bytes, queues: 'QueueDict') -> Tuple[str, str]: """Update URL token for contact. When contact's URL token public key changes, update URL token. """ if ut_pubkey_hex == cached_pk: raise SoftError("URL token public key has not changed.", output=False) try: public_key = bytes.fromhex(ut_pubkey_hex) if len(public_key) != TFC_PUBLIC_KEY_LENGTH or public_key == bytes( TFC_PUBLIC_KEY_LENGTH): raise ValueError shared_secret = url_token_private_key.exchange( X448PublicKey.from_public_bytes(public_key)) url_token = hashlib.blake2b(shared_secret, digest_size=URL_TOKEN_LENGTH).hexdigest() queues[URL_TOKEN_QUEUE].put( (onion_pub_key, url_token)) # Update Flask server's URL token for contact return url_token, ut_pubkey_hex except (TypeError, ValueError): raise SoftError("URL token derivation failed.", output=False)
def from_bytes(cls, pk_bytes): if not isinstance(pk_bytes, bytes): raise TypeError("pk_bytes must be of type: bytes") if not len(pk_bytes) == DHPublicKey.KEY_LEN: raise ValueError("pk_bytes must be 56 bytes") return cls(X448PublicKey.from_public_bytes(pk_bytes))
def deserialize(cls, serialized_pk): if not isinstance(serialized_pk, dict): raise TypeError("serialized_pk must be of type: dict") public_key = X448PublicKey.from_public_bytes( bytes.fromhex(serialized_pk["public_key"])) return cls(public_key)
def shared_secret(private_key: 'CK', public_key: 'CK') -> bytes: """ Compute the shared secret. """ if public_key.crv == X25519: d = X25519PrivateKey.from_private_bytes(private_key.d) x = X25519PublicKey.from_public_bytes(public_key.x) secret = d.exchange(x) elif public_key.crv == X448: d = X448PrivateKey.from_private_bytes(private_key.d) x = X448PublicKey.from_public_bytes(public_key.x) secret = d.exchange(x) elif public_key.crv == P256: d = ec.derive_private_key(int(hexlify(private_key.d), 16), SECP256R1(), default_backend()) x = ec.EllipticCurvePublicNumbers(int(hexlify(public_key.x), 16), int(hexlify(public_key.y), 16), SECP256R1()) x = x.public_key() secret = d.exchange(ec.ECDH(), x) else: raise CoseIllegalCurve(f"{public_key.crv} is unsupported") return secret
def get_shared_secret(client_private_key, server_public_key, cryptographic_group): # x25519 (x00 x1d) if cryptographic_group == b"\x00\x1d": private_key = X25519PrivateKey.from_private_bytes( client_private_key) public_key = X25519PublicKey.from_public_bytes(server_public_key) return private_key.exchange(public_key) # x448 (x00 x1e) elif cryptographic_group == b"\x00\x1e": private_key = X448PrivateKey.from_private_bytes(client_private_key) public_key = X448PublicKey.from_public_bytes(server_public_key) return private_key.exchange(public_key) # secp256r1 (x00 x17) elif cryptographic_group == b"\x00\x17": return Crypto_Helper.get_shared_secret_secpr1( client_private_key, server_public_key, SECP256R1) # secp384r1 (x00 x18) elif cryptographic_group == b"\x00\x18": return Crypto_Helper.get_shared_secret_secpr1( client_private_key, server_public_key, SECP384R1) # secp521r1 (x00 x19) elif cryptographic_group == b"\x00\x19": return Crypto_Helper.get_shared_secret_secpr1( client_private_key, server_public_key, SECP521R1)
def test_rfc7748(self, vector, backend): private = binascii.unhexlify(vector["input_scalar"]) public = binascii.unhexlify(vector["input_u"]) shared_key = binascii.unhexlify(vector["output_u"]) private_key = X448PrivateKey.from_private_bytes(private) public_key = X448PublicKey.from_public_bytes(public) computed_shared_key = private_key.exchange(public_key) assert computed_shared_key == shared_key
def test_rfc7748(self, vector, backend): private = binascii.unhexlify(vector["input_scalar"]) public = binascii.unhexlify(vector["input_u"]) shared_key = binascii.unhexlify(vector["output_u"]) private_key = X448PrivateKey.from_private_bytes(private) public_key = X448PublicKey.from_public_bytes(public) computed_shared_key = private_key.exchange(public_key) assert computed_shared_key == shared_key
def test_rfc7748_1000_iteration(self, backend): old_private = private = public = binascii.unhexlify( b"05000000000000000000000000000000000000000000000000000000" b"00000000000000000000000000000000000000000000000000000000") shared_key = binascii.unhexlify( b"aa3b4749d55b9daf1e5b00288826c467274ce3ebbdd5c17b975e09d4" b"af6c67cf10d087202db88286e2b79fceea3ec353ef54faa26e219f38") private_key = X448PrivateKey.from_private_bytes(private) public_key = X448PublicKey.from_public_bytes(public) for _ in range(1000): computed_shared_key = private_key.exchange(public_key) private_key = X448PrivateKey.from_private_bytes( computed_shared_key) public_key = X448PublicKey.from_public_bytes(old_private) old_private = computed_shared_key assert computed_shared_key == shared_key
def test_rfc7748_1000_iteration(self, backend): old_private = private = public = binascii.unhexlify( b"05000000000000000000000000000000000000000000000000000000" b"00000000000000000000000000000000000000000000000000000000" ) shared_key = binascii.unhexlify( b"aa3b4749d55b9daf1e5b00288826c467274ce3ebbdd5c17b975e09d4" b"af6c67cf10d087202db88286e2b79fceea3ec353ef54faa26e219f38" ) private_key = X448PrivateKey.from_private_bytes(private) public_key = X448PublicKey.from_public_bytes(public) for _ in range(1000): computed_shared_key = private_key.exchange(public_key) private_key = X448PrivateKey.from_private_bytes( computed_shared_key ) public_key = X448PublicKey.from_public_bytes(old_private) old_private = computed_shared_key assert computed_shared_key == shared_key
def test_pub_priv_bytes_raw(self, private_bytes, public_bytes, backend): private_key = X448PrivateKey.from_private_bytes(private_bytes) assert private_key.private_bytes( serialization.Encoding.Raw, serialization.PrivateFormat.Raw, serialization.NoEncryption()) == private_bytes assert private_key.public_key().public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw) == public_bytes public_key = X448PublicKey.from_public_bytes(public_bytes) assert public_key.public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw) == public_bytes
def test_pub_priv_bytes_raw(self, private_bytes, public_bytes, backend): private_key = X448PrivateKey.from_private_bytes(private_bytes) assert private_key.private_bytes( serialization.Encoding.Raw, serialization.PrivateFormat.Raw, serialization.NoEncryption() ) == private_bytes assert private_key.public_key().public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw ) == public_bytes public_key = X448PublicKey.from_public_bytes(public_bytes) assert public_key.public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw ) == public_bytes
def shared_key(private_key: 'X448PrivateKey', public_key: bytes) -> bytes: """Derive the X448 shared key. The pyca/cryptography library validates the length of the public key and verifies that the shared secret is not zero. The X448 shared secret is not a random byte string, but a random point on the curve. Thus, the raw bits of the shared secret might not be uniformly distributed in the keyspace, but have bias towards 0 or 1. To get rid of the bias, the raw shared secret is passed through a computational extractor (BLAKE2b CSPRF) to ensure a uniformly random shared key. While `shared secret` and `shared key` are used synonymously, in TFC we choose to distinguish between the raw shared secret and the BLAKE2b compressed shared secret by calling only the latter the `shared key`. Note that the shared key won't be used directly as a session key. Instead, it will be used as the key parameter in separate BLAKE2b instances where the hash function is used as a KDF to extract unidirectional message/header keys and fingerprints. """ try: shared_secret = private_key.exchange( X448PublicKey.from_public_bytes(public_key)) # type: bytes except ValueError as e: raise CriticalError(str(e)) if not isinstance(shared_secret, bytes): # pragma: no cover raise CriticalError( f"Derived an invalid type ({type(shared_secret)}) shared secret." ) if len(shared_secret) != X448_SHARED_SECRET_LENGTH: # pragma: no cover raise CriticalError( f"Generated an invalid size shared secret ({len(shared_secret)} bytes)." ) return blake2b(shared_secret, digest_size=SYMMETRIC_KEY_LENGTH)
def shared_key(private_key: 'X448PrivateKey', public_key: bytes) -> bytes: """Derive the X448 shared key. Since the shared secret is zero if contact's public key is zero, this function checks the public key is a valid non-zero bytestring. Because the raw bits of the X448 shared secret might not be uniformly distributed in the keyspace (i.e. bits might have bias towards 0 or 1), the raw shared secret is passed through BLAKE2b CSPRF to ensure uniformly random shared key. """ if len(public_key) != TFC_PUBLIC_KEY_LENGTH: raise CriticalError("Invalid public key length.") if public_key == bytes(TFC_PUBLIC_KEY_LENGTH): raise CriticalError("Received zero public key.") shared_secret = private_key.exchange( X448PublicKey.from_public_bytes(public_key)) return blake2b(shared_secret, digest_size=SYMMETRIC_KEY_LENGTH)
def shared_secret(private_key: Key, public_key: Key) -> bytes: """ Compute the shared secret. """ if public_key.crv == CoseEllipticCurves.X25519: d = X25519PrivateKey.from_private_bytes(private_key.d) x = X25519PublicKey.from_public_bytes(public_key.x) elif public_key.crv == CoseEllipticCurves.X448: d = X448PrivateKey.from_private_bytes(private_key.d) x = X448PublicKey.from_public_bytes(public_key.x) elif public_key.crv == CoseEllipticCurves.P_256: d = ec.derive_private_key(int(hexlify(private_key.d), 16), config_cose(public_key.crv).curve[1](), default_backend()) x = ec.EllipticCurvePublicNumbers( int(hexlify(public_key.x), 16), int(hexlify(public_key.y), 16), config_cose(public_key.crv).curve[1]()) else: raise CoseIllegalCurve(f"{public_key.crv} is unsupported") secret = d.exchange(x) return secret
def test_invalid_length_from_public_bytes(self, backend): with pytest.raises(ValueError): X448PublicKey.from_public_bytes(b"a" * 55) with pytest.raises(ValueError): X448PublicKey.from_public_bytes(b"a" * 57)
def exchange_x448(self, public: bytes) -> bytes: assert len(public) == 56, f"x448 public key must be 56 bytes" public_key = X448PublicKey.from_public_bytes(public) return self.private.exchange(public_key)
def client(onion_pub_key: bytes, queues: 'QueueDict', url_token_private_key: X448PrivateKey, tor_port: str, gateway: 'Gateway', onion_addr_user: str, unittest: bool = False) -> None: """Load packets from contact's Onion Service.""" url_token = '' cached_pk = '' short_addr = pub_key_to_short_address(onion_pub_key) onion_addr = pub_key_to_onion_address(onion_pub_key) check_delay = RELAY_CLIENT_MIN_DELAY is_online = False session = requests.session() session.proxies = { 'http': f'socks5h://127.0.0.1:{tor_port}', 'https': f'socks5h://127.0.0.1:{tor_port}' } rp_print(f"Connecting to {short_addr}...", bold=True) # When Transmitter Program sends contact under UNENCRYPTED_ADD_EXISTING_CONTACT, this function # receives user's own Onion address: That way it knows to request the contact to add them: if onion_addr_user: while True: try: reply = session.get( f'http://{onion_addr}.onion/contact_request/{onion_addr_user}', timeout=45).text if reply == "OK": break except requests.exceptions.RequestException: time.sleep(RELAY_CLIENT_MIN_DELAY) while True: with ignored(EOFError, KeyboardInterrupt): time.sleep(check_delay) # Obtain URL token # ---------------- # Load URL token public key from contact's Onion Service root domain try: url_token_public_key_hex = session.get( f'http://{onion_addr}.onion/', timeout=45).text except requests.exceptions.RequestException: url_token_public_key_hex = '' # Manage online status of contact based on availability of URL token's public key if url_token_public_key_hex == '': if check_delay < RELAY_CLIENT_MAX_DELAY: check_delay *= 2 if check_delay > CLIENT_OFFLINE_THRESHOLD and is_online: is_online = False rp_print(f"{short_addr} is now offline", bold=True) continue else: check_delay = RELAY_CLIENT_MIN_DELAY if not is_online: is_online = True rp_print(f"{short_addr} is now online", bold=True) # When contact's URL token public key changes, update URL token if url_token_public_key_hex != cached_pk: try: public_key = bytes.fromhex(url_token_public_key_hex) if len(public_key ) != TFC_PUBLIC_KEY_LENGTH or public_key == bytes( TFC_PUBLIC_KEY_LENGTH): raise ValueError shared_secret = url_token_private_key.exchange( X448PublicKey.from_public_bytes(public_key)) url_token = hashlib.blake2b( shared_secret, digest_size=SYMMETRIC_KEY_LENGTH).hexdigest() except (TypeError, ValueError): continue cached_pk = url_token_public_key_hex # Update client's URL token public key queues[URL_TOKEN_QUEUE].put( (onion_pub_key, url_token)) # Update Flask server's URL token for contact # Load TFC data with URL token # ---------------------------- get_data_loop(onion_addr, url_token, short_addr, onion_pub_key, queues, session, gateway) if unittest: break
def test_public_bytes(self, private_bytes, public_bytes, backend): private_key = X448PrivateKey._from_private_bytes(private_bytes) assert private_key.public_key().public_bytes() == public_bytes public_key = X448PublicKey.from_public_bytes(public_bytes) assert public_key.public_bytes() == public_bytes
def test_invalid_length_from_public_bytes(self, backend): with pytest.raises(ValueError): X448PublicKey.from_public_bytes(b"a" * 55) with pytest.raises(ValueError): X448PublicKey.from_public_bytes(b"a" * 57)
def __init__(self, params: Dict[int, Any]): super().__init__(params) self._public_key: Any = None self._private_key: Any = None self._hash_alg: Any = None self._x = None self._d = None # Validate kty. if params[1] != 1: raise ValueError("kty(1) should be OKP(1).") # Validate crv. if -1 not in params: raise ValueError("crv(-1) not found.") self._crv = params[-1] if not isinstance(self._crv, int): raise ValueError("crv(-1) should be int.") if self._crv not in [4, 5, 6, 7]: raise ValueError( f"Unsupported or unknown crv(-1) for OKP: {self._crv}.") if self._crv in [4, 5]: if not self._alg: raise ValueError("X25519/X448 needs alg explicitly.") if self._alg in [-25, -27]: self._hash_alg = hashes.SHA256 elif self._alg in [-26, -28]: self._hash_alg = hashes.SHA512 else: raise ValueError( f"Unsupported or unknown alg used with X25519/X448: {self._alg}." ) # Validate alg and key_ops. if self._key_ops: if set(self._key_ops) & set([3, 4, 5, 6, 9, 10]): raise ValueError( "Unknown or not permissible key_ops(4) for OKP.") else: if self._crv in [4, 5]: self._key_ops = [7, 8] if -4 in params else [] else: # self._crv in [6, 7] self._key_ops = [1, 2] if -4 in params else [2] if self._alg: if self._alg in COSE_ALGORITHMS_SIG_OKP.values(): if -4 in params: # private key for signing. if not (set(self._key_ops) & set([1, 2])): raise ValueError("Invalid key_ops for signing key.") if set(self._key_ops) & set([7, 8]): raise ValueError( "Signing key should not be used for key derivation." ) else: # public key for signing. if 2 not in self._key_ops or len(self._key_ops) != 1: raise ValueError("Invalid key_ops for public key.") elif self._alg in COSE_ALGORITHMS_CKDM_KEY_AGREEMENT.values(): if -4 in params: # private key for key derivation. if not (set(self._key_ops) & set([7, 8])): raise ValueError("Invalid key_ops for key derivation.") if set(self._key_ops) & set([1, 2]): raise ValueError( "Private key for ECDHE should not be used for signing." ) else: # public key for key derivation. if self._key_ops: raise ValueError( "Public key for ECDHE should not have key_ops.") else: raise ValueError( f"Unsupported or unknown alg(3) for OKP: {self._alg}.") else: if -4 in params: # private key. if set(self._key_ops) & set([1, 2]): # private key for signing. if set(self._key_ops) & set([7, 8]): raise ValueError( "OKP private key should not be used for both signing and key derivation." ) self._alg = -8 # EdDSA else: # public key. if 2 in self._key_ops: if len(self._key_ops) > 1: raise ValueError("Invalid key_ops for public key.") else: raise ValueError("Invalid key_ops for public key.") if self._alg in COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_ES.values(): if -2 not in params: return # Validate x. if -2 not in params: raise ValueError("x(-2) not found.") if not isinstance(params[-2], bytes): raise ValueError("x(-2) should be bytes(bstr).") self._x = params[-2] try: if -4 not in params: if self._crv == 4: # X25519 self._public_key = X25519PublicKey.from_public_bytes( self._x) elif self._crv == 5: # X448 self._public_key = X448PublicKey.from_public_bytes(self._x) elif self._crv == 6: # Ed25519 self._public_key = Ed25519PublicKey.from_public_bytes( self._x) else: # self._crv == 7 (Ed448) self._public_key = Ed448PublicKey.from_public_bytes( self._x) self._key = self._public_key return except ValueError as err: raise ValueError("Invalid key parameter.") from err if not isinstance(params[-4], bytes): raise ValueError("d(-4) should be bytes(bstr).") try: self._d = params[-4] if self._crv == 4: # X25519 self._private_key = X25519PrivateKey.from_private_bytes( self._d) elif self._crv == 5: # X448 self._private_key = X448PrivateKey.from_private_bytes(self._d) elif self._crv == 6: # Ed25519 self._private_key = Ed25519PrivateKey.from_private_bytes( self._d) else: # self._crv == 7 (Ed448) self._private_key = Ed448PrivateKey.from_private_bytes(self._d) self._key = self._private_key except ValueError as err: raise ValueError("Invalid key parameter.") from err return