1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
|
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 (
ECDSAKey,
Ed25519Key,
Message,
PKey,
PublicBlob,
RSAKey,
UnknownKeyType,
)
from ._util import _support
class PKey_:
# NOTE: this is incidentally tested by a number of other tests, such as the
# agent.py test suite
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
# TODO: exceptions
#
# TODO: passphrase? OTOH since this is aimed at the agent...irrelephant
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__)
# TODO: passphrase support tested
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"))
def fingerprint(self, keys):
# NOTE: Hardcoded fingerprint expectation stored in fixture.
assert keys.pkey.fingerprint == keys.expected_fp
def algorithm_name(self, keys):
key = keys.pkey
if isinstance(key, RSAKey):
assert key.algorithm_name == "RSA"
elif isinstance(key, ECDSAKey):
assert key.algorithm_name == "ECDSA"
elif isinstance(key, Ed25519Key):
assert key.algorithm_name == "ED25519"
# TODO: corner case: AgentKey, whose .name can be cert-y (due to the
# value of the name field passed via agent protocol) and thus
# algorithm_name is eg "RSA-CERT" - keys loaded directly from disk will
# never look this way, even if they have a .public_blob attached.
class equality_and_hashing:
def same_key_is_equal_to_itself(self, keys):
assert keys.pkey == keys.pkey2
def same_key_same_hash(self, keys):
# NOTE: this isn't a great test due to hashseed randomization under
# Python 3 preventing use of static values, but it does still prove
# that __hash__ is implemented/doesn't explode & works across
# instances
assert hash(keys.pkey) == hash(keys.pkey2)
def keys_are_not_equal_to_other_types(self, keys):
for value in [None, True, ""]:
assert keys.pkey != value
class identifiers_classmethods:
def default_is_class_name_attribute(self):
# NOTE: not all classes _have_ this, only the ones that don't
# customize identifiers().
class MyKey(PKey):
name = "it me"
assert MyKey.identifiers() == ["it me"]
def rsa_is_all_combos_of_cert_and_sha_type(self):
assert RSAKey.identifiers() == [
"ssh-rsa",
"ssh-rsa-cert-v01@openssh.com",
"rsa-sha2-256",
"rsa-sha2-256-cert-v01@openssh.com",
"rsa-sha2-512",
"rsa-sha2-512-cert-v01@openssh.com",
]
def ed25519_is_protocol_name(self):
assert Ed25519Key.identifiers() == [
"ssh-ed25519",
"ssh-ed25519-cert-v01@openssh.com",
]
def ecdsa_is_all_curve_names(self):
assert ECDSAKey.identifiers() == [
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
]
|