summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--paramiko/__init__.py4
-rw-r--r--paramiko/auth_handler.py18
-rw-r--r--paramiko/dsskey.py1
-rw-r--r--paramiko/ecdsakey.py1
-rw-r--r--paramiko/ed25519key.py1
-rw-r--r--paramiko/pkey.py69
-rw-r--r--paramiko/rsakey.py1
-rw-r--r--tests/test_pkey.py24
-rw-r--r--tests/test_rsa.key-cert.pub1
-rw-r--r--tests/test_rsa.key.pub1
10 files changed, 115 insertions, 6 deletions
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index d67ad62f..5d3a10fc 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -30,7 +30,7 @@ __license__ = "GNU Lesser General Public License (LGPL)"
from paramiko.transport import SecurityOptions, Transport
from paramiko.client import (
- SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy,
+ SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy,
WarningPolicy,
)
from paramiko.auth_handler import AuthHandler
@@ -57,7 +57,7 @@ from paramiko.message import Message
from paramiko.packet import Packetizer
from paramiko.file import BufferedFile
from paramiko.agent import Agent, AgentKey
-from paramiko.pkey import PKey
+from paramiko.pkey import PKey, PublicBlob
from paramiko.hostkeys import HostKeys
from paramiko.config import SSHConfig
from paramiko.proxy import ProxyCommand
diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py
index ae88179e..0b13722c 100644
--- a/paramiko/auth_handler.py
+++ b/paramiko/auth_handler.py
@@ -186,8 +186,13 @@ class AuthHandler (object):
m.add_string(service)
m.add_string('publickey')
m.add_boolean(True)
- m.add_string(key.get_name())
- m.add_string(key)
+ # Use certificate contents, if available, plain pubkey otherwise
+ if key.public_blob:
+ m.add_string(key.public_blob.key_type)
+ m.add_string(key.public_blob.key_blob)
+ else:
+ m.add_string(key.get_name())
+ m.add_string(key)
return m.asbytes()
def wait_for_response(self, event):
@@ -244,8 +249,13 @@ class AuthHandler (object):
m.add_string(password)
elif self.auth_method == 'publickey':
m.add_boolean(True)
- m.add_string(self.private_key.get_name())
- m.add_string(self.private_key)
+ # Use certificate contents, if available, plain pubkey otherwise
+ if self.private_key.public_blob:
+ m.add_string(self.private_key.public_blob.key_type)
+ m.add_string(self.private_key.public_blob.key_blob)
+ else:
+ m.add_string(self.private_key.get_name())
+ m.add_string(self.private_key)
blob = self._get_session_blob(
self.private_key, 'ssh-connection', self.username)
sig = self.private_key.sign_ssh_data(blob)
diff --git a/paramiko/dsskey.py b/paramiko/dsskey.py
index 9af5d0c1..b3197f62 100644
--- a/paramiko/dsskey.py
+++ b/paramiko/dsskey.py
@@ -49,6 +49,7 @@ class DSSKey(PKey):
self.g = None
self.y = None
self.x = None
+ self.public_blob = None
if file_obj is not None:
self._from_private_key(file_obj, password)
return
diff --git a/paramiko/ecdsakey.py b/paramiko/ecdsakey.py
index fa850c2e..805d6bc0 100644
--- a/paramiko/ecdsakey.py
+++ b/paramiko/ecdsakey.py
@@ -46,6 +46,7 @@ class _ECDSACurve(object):
def __init__(self, curve_class, nist_name):
self.nist_name = nist_name
self.key_length = curve_class.key_size
+ self.public_blob = None
# Defined in RFC 5656 6.2
self.key_format_identifier = "ecdsa-sha2-" + self.nist_name
diff --git a/paramiko/ed25519key.py b/paramiko/ed25519key.py
index a50d68bc..d904f1ac 100644
--- a/paramiko/ed25519key.py
+++ b/paramiko/ed25519key.py
@@ -63,6 +63,7 @@ class Ed25519Key(PKey):
self._signing_key = signing_key
self._verifying_key = verifying_key
+ self.public_blob = None
def _parse_signing_key_data(self, data, password):
from paramiko.transport import Transport
diff --git a/paramiko/pkey.py b/paramiko/pkey.py
index 35a26fc7..3a872491 100644
--- a/paramiko/pkey.py
+++ b/paramiko/pkey.py
@@ -33,6 +33,7 @@ from paramiko import util
from paramiko.common import o600
from paramiko.py3compat import u, encodebytes, decodebytes, b
from paramiko.ssh_exception import SSHException, PasswordRequiredException
+from paramiko.message import Message
class PKey(object):
@@ -363,3 +364,71 @@ class PKey(object):
format,
encryption
).decode())
+
+ def load_certificate(self, **kwargs):
+ """
+ Supplement the private key contents with data loaded from
+ an OpenSSH public key (.pub) or certificate (-cert.pub) file.
+
+ The .pub contents adds no real value, since the private key
+ file includes sufficient information to derive the public
+ key info. For certificates, however, this can be used on
+ the client side to offer authentication requests to the server
+ based on certificate instead of raw public key.
+
+ See: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys
+
+ Note: very little effort is made to validate the certificate contents,
+ that is for the server to decide if it is good enough to authenticate
+ successfully.
+ """
+ blob = PublicBlob(**kwargs)
+ if not blob.key_type.startswith(self.get_name()):
+ raise ValueError('PublicBlob type %s incompatible with key type %s' %
+ (blob.key_type, self.get_name()))
+ self.public_blob = blob
+
+
+# General construct for an OpenSSH style Public Key blob
+# readable from a one-line file of the format:
+# <key-name> <base64-blob> [<comment>]
+# Of little value in the case of standard public keys
+# {ssh-rsa, ssh-dss, ssh-ecdsa, ssh-ed25519}, but should
+# provide rudimentary support for {*-cert.v01}
+class PublicBlob(object):
+ '''
+ OpenSSH plain public key or OpenSSH signed public key (certificate)
+ A mostly dumb container
+ '''
+ def __init__(self, pubkey_filename=None, pubkey_string=None):
+ '''
+ Can read from a file or string.
+ '''
+ if pubkey_filename:
+ with open(pubkey_filename) as f:
+ fields = f.read().split(None, 2)
+ elif pubkey_string:
+ fields = pubkey_string.split(None, 2)
+ else:
+ raise ValueError('PublicBlob() requires either a pubkey_filename or pubkey_string')
+ if len(fields) < 2:
+ raise ValueError('PublicBlob() not enough fields %s', fields)
+ self.key_type = fields[0]
+ self.key_blob = decodebytes(fields[1])
+ try:
+ self.comment = fields[2].strip()
+ except IndexError:
+ self.comment = None
+ # Verify that the blob message first (string) field matches the key_type
+ m = Message(self.key_blob)
+ blob_type = m.get_string()
+ if blob_type != self.key_type:
+ raise ValueError(
+ 'Invalid PublicBlob contents. Key type [%s], expected [%s]' %
+ (blob_type, self.key_type))
+
+ def __str__(self):
+ if self.comment:
+ return '%s public key/certificate - %s' % (self.key_type, self.comment)
+ else:
+ return '%s public key/certificate' % self.key_type
diff --git a/paramiko/rsakey.py b/paramiko/rsakey.py
index b5107515..7abcfa28 100644
--- a/paramiko/rsakey.py
+++ b/paramiko/rsakey.py
@@ -40,6 +40,7 @@ class RSAKey(PKey):
def __init__(self, msg=None, data=None, filename=None, password=None,
key=None, file_obj=None):
self.key = None
+ self.public_blob = None
if file_obj is not None:
self._from_private_key(file_obj, password)
return
diff --git a/tests/test_pkey.py b/tests/test_pkey.py
index 9bb3c44c..034331a2 100644
--- a/tests/test_pkey.py
+++ b/tests/test_pkey.py
@@ -480,3 +480,27 @@ class KeyTest(unittest.TestCase):
self.assert_keyfile_is_encrypted(newfile)
finally:
os.remove(newfile)
+
+ def test_certificates(self):
+ # PKey.load_certificate
+ key = RSAKey.from_private_key_file(test_path('test_rsa.key'))
+ self.assertTrue(key.public_blob is None)
+ key.load_certificate(pubkey_filename=test_path('test_rsa.key-cert.pub'))
+ self.assertTrue(key.public_blob is not None)
+ self.assertEqual(key.public_blob.key_type, 'ssh-rsa-cert-v01@openssh.com')
+ self.assertEqual(key.public_blob.comment, 'test_rsa.key.pub')
+ # Delve into blob contents, for test purposes
+ msg = Message(key.public_blob.key_blob)
+ self.assertEqual(msg.get_string(), 'ssh-rsa-cert-v01@openssh.com')
+ nonce = msg.get_string()
+ e = msg.get_mpint()
+ n = msg.get_mpint()
+ self.assertEqual(e, key.public_numbers.e)
+ self.assertEqual(n, key.public_numbers.n)
+ # Serial number
+ self.assertEqual(msg.get_int64(), 1234)
+
+ # Prevented from loading certificate that doesn't match
+ key1 = Ed25519Key.from_private_key_file(test_path('test_ed25519.key'))
+ self.assertRaises(ValueError, key1.load_certificate,
+ pubkey_filename=test_path('test_rsa.key-cert.pub'))
diff --git a/tests/test_rsa.key-cert.pub b/tests/test_rsa.key-cert.pub
new file mode 100644
index 00000000..7487ab66
--- /dev/null
+++ b/tests/test_rsa.key-cert.pub
@@ -0,0 +1 @@
+ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgsZlXTd5NE4uzGAn6TyAqQj+IPbsTEFGap2x5pTRwQR8AAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cAAAAAAAAE0gAAAAEAAAAmU2FtcGxlIHNlbGYtc2lnbmVkIE9wZW5TU0ggY2VydGlmaWNhdGUAAAASAAAABXVzZXIxAAAABXVzZXIyAAAAAAAAAAD//////////wAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAACVAAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4cAAACPAAAAB3NzaC1yc2EAAACATFHFsARDgQevc6YLxNnDNjsFtZ08KPMyYVx0w5xm95IVZHVWSOc5w+ccjqN9HRwxV3kP7IvL91qx0Uc3MJdB9g/O6HkAP+rpxTVoTb2EAMekwp5+i8nQJW4CN2BSsbQY1M6r7OBZ5nmF4hOW/5Pu4l22lXe2ydy8kEXOEuRpUeQ= test_rsa.key.pub
diff --git a/tests/test_rsa.key.pub b/tests/test_rsa.key.pub
new file mode 100644
index 00000000..bfa1e150
--- /dev/null
+++ b/tests/test_rsa.key.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA049W6geFpmsljTwfvI1UmKWWJPNFI74+vNKTk4dmzkQY2yAMs6FhlvhlI8ysU4oj71ZsRYMecHbBbxdN79+JRFVYTKaLqjwGENeTd+yv4q+V2PvZv3fLnzApI3l7EJCqhWwJUHJ1jAkZzqDx0tyOL4uoZpww3nmE0kb3y21tH4c=