from pathlib import Path from unittest.mock import patch, call from pytest import raises from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey from paramiko import ( PKey, Ed25519Key, RSAKey, UnknownKeyType, Message, PublicBlob, ) from ._util import _support class PKey_: class from_type_string: def loads_from_type_and_bytes(self, keys): obj = PKey.from_type_string(keys.full_type, keys.pkey.asbytes()) assert obj == keys.pkey class from_path: def loads_from_Path(self, keys): obj = PKey.from_path(keys.path) assert obj == keys.pkey def loads_from_str(self): key = PKey.from_path(str(_support("rsa.key"))) assert isinstance(key, RSAKey) @patch("paramiko.pkey.Path") def expands_user(self, mPath): # real key for guts that want a real key format mykey = Path(_support("rsa.key")) pathy = mPath.return_value.expanduser.return_value # read_bytes for cryptography.io's loaders pathy.read_bytes.return_value = mykey.read_bytes() # open() for our own class loader pathy.open.return_value = mykey.open() # fake out exists() to avoid attempts to load cert pathy.exists.return_value = False PKey.from_path("whatever") # we're not testing expanduser itself # Both key and cert paths mPath.return_value.expanduser.assert_has_calls([call(), call()]) def raises_UnknownKeyType_for_unknown_types(self): # I.e. a real, becomes a useful object via cryptography.io, key # class that we do NOT support. Chose Ed448 randomly as OpenSSH # doesn't seem to support it either, going by ssh-keygen... keypath = _support("ed448.key") with raises(UnknownKeyType) as exc: PKey.from_path(keypath) assert issubclass(exc.value.key_type, Ed448PrivateKey) with open(keypath, "rb") as fd: assert exc.value.key_bytes == fd.read() def leaves_cryptography_exceptions_untouched(self): # a Python file is not a private key! with raises(ValueError): PKey.from_path(__file__) class automatically_loads_certificates: def existing_cert_loaded_when_given_key_path(self): key = PKey.from_path(_support("rsa.key")) # Public blob exists despite no .load_certificate call assert key.public_blob is not None assert ( key.public_blob.key_type == "ssh-rsa-cert-v01@openssh.com" ) # And it's definitely the one we expected assert key.public_blob == PublicBlob.from_file( _support("rsa.key-cert.pub") ) def can_be_given_cert_path_instead(self): key = PKey.from_path(_support("rsa.key-cert.pub")) # It's still a key, not a PublicBlob assert isinstance(key, RSAKey) # Public blob exists despite no .load_certificate call assert key.public_blob is not None assert ( key.public_blob.key_type == "ssh-rsa-cert-v01@openssh.com" ) # And it's definitely the one we expected assert key.public_blob == PublicBlob.from_file( _support("rsa.key-cert.pub") ) def no_cert_load_if_no_cert(self): # This key exists (it's a copy of the regular one) but has no # matching -cert.pub key = PKey.from_path(_support("rsa-lonely.key")) assert key.public_blob is None def excepts_usefully_if_no_key_only_cert(self): # TODO: is that truly an error condition? the cert is ~the # pubkey and we still require the privkey for signing, yea? # This cert exists (it's a copy of the regular one) but there's # no rsa-missing.key to load. with raises(FileNotFoundError) as info: PKey.from_path(_support("rsa-missing.key-cert.pub")) assert info.value.filename.endswith("rsa-missing.key") class load_certificate: def rsa_public_cert_blobs(self): # Data to test signing with (arbitrary) data = b"ice weasels" # Load key w/o cert at first (so avoiding .from_path) key = RSAKey.from_private_key_file(_support("rsa.key")) assert key.public_blob is None # Sign regular-style (using, arbitrarily, SHA2) msg = key.sign_ssh_data(data, "rsa-sha2-256") msg.rewind() assert "rsa-sha2-256" == msg.get_text() signed = msg.get_binary() # for comparison later # Load cert and inspect its internals key.load_certificate(_support("rsa.key-cert.pub")) assert key.public_blob is not None assert key.public_blob.key_type == "ssh-rsa-cert-v01@openssh.com" assert key.public_blob.comment == "test_rsa.key.pub" msg = Message(key.public_blob.key_blob) # cert type assert msg.get_text() == "ssh-rsa-cert-v01@openssh.com" # nonce msg.get_string() # public numbers assert msg.get_mpint() == key.public_numbers.e assert msg.get_mpint() == key.public_numbers.n # serial number assert msg.get_int64() == 1234 # TODO: whoever wrote the OG tests didn't care about the remaining # fields from # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys # so neither do I, for now... # Sign cert-style (still SHA256 - so this actually does almost # exactly the same thing under the hood as the previous sign) msg = key.sign_ssh_data(data, "rsa-sha2-256-cert-v01@openssh.com") msg.rewind() assert "rsa-sha2-256" == msg.get_text() assert signed == msg.get_binary() # same signature as above msg.rewind() assert key.verify_ssh_sig(b"ice weasels", msg) # our data verified def loading_cert_of_different_type_from_key_raises_ValueError(self): edkey = Ed25519Key.from_private_key_file(_support("ed25519.key")) err = "PublicBlob type ssh-rsa-cert-v01@openssh.com incompatible with key type ssh-ed25519" # noqa with raises(ValueError, match=err): edkey.load_certificate(_support("rsa.key-cert.pub"))