summaryrefslogtreecommitdiffhomepage
path: root/tests/pkey.py
blob: d1dbe0e5509467626337fcefed772417347c1aa4 (plain)
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
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_:
    # 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"))