aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMark Adams <mark@markadams.me>2014-12-12 23:13:12 -0600
committerMark Adams <mark@markadams.me>2014-12-13 08:38:11 -0600
commit78a7d1c4c63737c4eae0c22207a00141a44402d3 (patch)
tree99b63769c2260fd888450e49ec8d25e26c1566d8
parentbe42d096746ca211d0e1b21874017e75765dc40b (diff)
downloadcryptography-78a7d1c4c63737c4eae0c22207a00141a44402d3.tar.gz
cryptography-78a7d1c4c63737c4eae0c22207a00141a44402d3.tar.bz2
cryptography-78a7d1c4c63737c4eae0c22207a00141a44402d3.zip
Added load_ssh_rsa_public_key to hazmat.primitives.serialization to allow for loading of OpenSSH RSA public keys
Also added load_ssh_public_key as a generic method that can be later extended to support more public key algorithms.
-rw-r--r--AUTHORS.rst1
-rw-r--r--CHANGELOG.rst4
-rw-r--r--docs/hazmat/primitives/asymmetric/serialization.rst55
-rw-r--r--src/cryptography/hazmat/primitives/serialization.py47
-rw-r--r--tests/hazmat/primitives/test_serialization.py103
5 files changed, 207 insertions, 3 deletions
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 250b717b..c233bc86 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -18,3 +18,4 @@ PGP key fingerprints are enclosed in parentheses.
* Matthew Iversen <matt@notevencode.com> (2F04 3DCC D6E6 D5AC D262 2E0B C046 E8A8 7452 2973)
* Mohammed Attia <skeuomorf@gmail.com> (854A F9C5 9FF5 6E38 B17D 9587 2D70 E1ED 5290 D357)
* Michael Hart <michael.hart1994@gmail.com>
+* Mark Adams <mark@markadams.me> (A18A 7DD3 283C CF2A B0CE FE0E C7A0 5E3F C972 098C)
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b8a799a2..e0c71a7b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -18,6 +18,10 @@ Changelog
:class:`~cryptography.hazmat.primitives.interfaces.CMACContext`.
* Added support for encoding and decoding :rfc:`6979` signatures in
:doc:`/hazmat/primitives/asymmetric/utils`.
+* Added
+ :func:`~cryptography.hazmat.primitives.serialization.load_ssh_public_key` and
+ :func:`~cryptography.hazmat.primitives.serialization.load_ssh_rsa_public_key`
+ to support the loading of OpenSSH RSA public keys (RFC 4253).
0.6.1 - 2014-10-15
~~~~~~~~~~~~~~~~~~
diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst
index b0b37b80..52960ec0 100644
--- a/docs/hazmat/primitives/asymmetric/serialization.rst
+++ b/docs/hazmat/primitives/asymmetric/serialization.rst
@@ -195,3 +195,58 @@ KEY-----`` or ``-----BEGIN DSA PRIVATE KEY-----``.
:raises UnsupportedAlgorithm: If the serialized key is of a type that
is not supported by the backend or if the key is encrypted with a
symmetric cipher that is not supported by the backend.
+
+OpenSSH Public Key
+~~~~~~~~~~~~~~~~~~
+
+The format used by OpenSSH to store public keys as specified in :rfc:`4253`
+
+Currently, only RSA public keys are supported. Any other type of key will
+result in an exception being thrown.
+
+Example RSA key in OpenSSH format (line breaks added for formatting purposes)::
+
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk
+ FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll
+ PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK
+ vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f
+ sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy
+ ///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX
+ 2MzHvnbv testkey@localhost
+
+.. function:: load_ssh_public_key(data, backend)
+
+ .. versionadded:: 0.7
+
+ Deserialize a public key from OpenSSH (:rfc:`4253`) encoded data to an
+ instance of the public key type for the specified backend.
+
+ :param bytes data: The OpenSSH encoded key data.
+
+ :param backend: A backend provider.
+
+ :returns: A new instance of a public key type.
+
+ :raises ValueError: If the OpenSSH data could not be properly decoded or
+ if the key is not in the proper format.
+
+ :raises UnsupportedAlgorithm: If the serialized key is of a type that is
+ not supported.
+
+.. function:: load_ssh_rsa_public_key(data, backend)
+
+ .. versionadded:: 0.7
+
+ Deserialize a RSA public key from OpenSSH (:rfc:`4253`) encoded data to an
+ instance of the RSA Public Key type for the specified backend.
+
+ :param bytes data: The OpenSSH encoded key data.
+
+ :param backend: A
+ :class:`~cryptography.hazmat.backends.interfaces.RSABackend`
+ provider.
+
+ :returns: A new instance of a public key type.
+
+ :raises ValueError: If the OpenSSH data could not be properly decoded or
+ if the key is not in the proper format.
diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py
index b9cf5967..0f07e41f 100644
--- a/src/cryptography/hazmat/primitives/serialization.py
+++ b/src/cryptography/hazmat/primitives/serialization.py
@@ -4,9 +4,13 @@
from __future__ import absolute_import, division, print_function
+import base64
+import struct
import warnings
from cryptography import utils
+from cryptography.exceptions import UnsupportedAlgorithm
+from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
def load_pem_traditional_openssl_private_key(data, password, backend):
@@ -39,3 +43,46 @@ def load_pem_private_key(data, password, backend):
def load_pem_public_key(data, backend):
return backend.load_pem_public_key(data)
+
+
+def load_ssh_public_key(data, backend):
+ if not data.startswith(b'ssh-'):
+ raise ValueError('SSH-formatted keys must begin with ssh-')
+
+ if not data.startswith(b'ssh-rsa'):
+ raise UnsupportedAlgorithm('Only RSA keys are currently supported.')
+
+ return load_ssh_rsa_public_key(data, backend)
+
+
+def load_ssh_rsa_public_key(data, backend):
+ if not data.startswith(b'ssh-rsa '):
+ raise ValueError('SSH-formatted RSA keys must begin with ssh-rsa')
+
+ parts = data.split(b' ')
+ data = base64.b64decode(parts[1])
+
+ cert_data = []
+
+ while len(data) > 0:
+ str_len = struct.unpack('>I', data[0:4])[0]
+ cert_data.append(data[4:4 + str_len])
+ data = data[4 + str_len:]
+
+ e = _bytes_to_int(cert_data[1])
+ n = _bytes_to_int(cert_data[2])
+ return backend.load_rsa_public_numbers(RSAPublicNumbers(e, n))
+
+
+def _bytes_to_int(data):
+ if len(data) % 4 != 0:
+ # Pad the bytes with 0x00 to a block size of 4
+ data = (b'\x00' * (4 - (len(data) % 4))) + data
+
+ result = 0
+
+ while len(data) > 0:
+ result = (result << 32) + struct.unpack('>I', data[0:4])[0]
+ data = data[4:]
+
+ return result
diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py
index 726e73dd..63ec6c4c 100644
--- a/tests/hazmat/primitives/test_serialization.py
+++ b/tests/hazmat/primitives/test_serialization.py
@@ -9,16 +9,18 @@ import textwrap
import pytest
-from cryptography.exceptions import _Reasons
+from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
from cryptography.hazmat.backends.interfaces import (
EllipticCurveBackend, PEMSerializationBackend, PKCS8SerializationBackend,
- TraditionalOpenSSLSerializationBackend
+ RSABackend, TraditionalOpenSSLSerializationBackend
)
from cryptography.hazmat.primitives import interfaces
from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from cryptography.hazmat.primitives.serialization import (
load_pem_pkcs8_private_key, load_pem_private_key, load_pem_public_key,
- load_pem_traditional_openssl_private_key
+ load_pem_traditional_openssl_private_key, load_ssh_public_key,
+ load_ssh_rsa_public_key
)
@@ -680,3 +682,98 @@ class TestPKCS8Serialization(object):
pemfile.read().encode(), password, backend
)
)
+
+
+@pytest.mark.requires_backend_interface(interface=RSABackend)
+class TestSSHSerialization(object):
+ def test_load_ssh_public_key_unsupported(self, backend):
+ str_key = b'ssh-dss AAAAB3NzaC1kc3MAAACBAO7q0a7VsQZcdRTCqFentQt...'
+
+ with pytest.raises(UnsupportedAlgorithm):
+ load_ssh_public_key(str_key, backend)
+
+ def test_load_ssh_public_key_bad_format(self, backend):
+ str_key = b'not-a-real-key'
+
+ with pytest.raises(ValueError):
+ load_ssh_public_key(str_key, backend)
+
+ def test_load_ssh_public_key(self, backend):
+ str_key = (
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk'
+ 'FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll'
+ 'PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK'
+ 'vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f'
+ 'sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy'
+ '///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX'
+ '2MzHvnbv testkey@localhost').encode()
+
+ key = load_ssh_public_key(str_key, backend)
+
+ assert key is not None
+ assert isinstance(key, interfaces.RSAPublicKey)
+
+ if not isinstance(key, interfaces.RSAPublicKeyWithNumbers):
+ return
+
+ numbers = key.public_numbers()
+
+ expected_e = 0x10001
+ expected_n = int(
+ '00C3BBF5D13F59322BA0A0B77EA0B6CF570241628AE24B5BA454D'
+ '23DCA295652B3523B67752653DFFD69587FAD9578DD6406F23691'
+ 'EA491C3F8B2D391D0312D9653C303B651067ADF887A5241843CEF'
+ '8019680A088E092FEC305FB04EA070340BB9BD0F1635B2AD84142'
+ '61B4E2D010ABD8FC6D2FB768912F78EE6B05A60857532B75B75EF'
+ 'C007601A4EF58BA947B7E75E38F3443CDD87E7C138A1DAD9D9FB3'
+ '19FF69DA43A9F6F6B0CD243F042CD1A5AFAEB286BD46AEB2D922B'
+ 'D01385D6892167074A0907F94A2BF08A54ABB2FFFFC89920861D0'
+ '46F8706AB88DDADBD9E8204D48B87789081E074024C8996783B31'
+ '7076A98ABF0A2D8550EAF2097D8CCC7BE76EF', 16)
+
+ expected = RSAPublicNumbers(expected_e, expected_n)
+
+ assert numbers == expected
+
+ def test_load_ssh_rsa_public_key_bad_format(self, backend):
+ str_key = b'ssh-rsa-not-a-key'
+
+ with pytest.raises(ValueError):
+ load_ssh_rsa_public_key(str_key, backend)
+
+ def test_load_ssh_rsa_public_key(self, backend):
+ str_key = (
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDu/XRP1kyK6Cgt36gts9XAk'
+ 'FiiuJLW6RU0j3KKVZSs1I7Z3UmU9/9aVh/rZV43WQG8jaR6kkcP4stOR0DEtll'
+ 'PDA7ZRBnrfiHpSQYQ874AZaAoIjgkv7DBfsE6gcDQLub0PFjWyrYQUJhtOLQEK'
+ 'vY/G0vt2iRL3juawWmCFdTK3W3XvwAdgGk71i6lHt+deOPNEPN2H58E4odrZ2f'
+ 'sxn/adpDqfb2sM0kPwQs0aWvrrKGvUaustkivQE4XWiSFnB0oJB/lKK/CKVKuy'
+ '///ImSCGHQRvhwariN2tvZ6CBNSLh3iQgeB0AkyJlng7MXB2qYq/Ci2FUOryCX'
+ '2MzHvnbv testkey@localhost').encode()
+
+ key = load_ssh_public_key(str_key, backend)
+
+ assert key is not None
+ assert isinstance(key, interfaces.RSAPublicKey)
+
+ if not isinstance(key, interfaces.RSAPublicKeyWithNumbers):
+ return
+
+ numbers = key.public_numbers()
+
+ expected_e = 0x10001
+ expected_n = int(
+ '00C3BBF5D13F59322BA0A0B77EA0B6CF570241628AE24B5BA454D'
+ '23DCA295652B3523B67752653DFFD69587FAD9578DD6406F23691'
+ 'EA491C3F8B2D391D0312D9653C303B651067ADF887A5241843CEF'
+ '8019680A088E092FEC305FB04EA070340BB9BD0F1635B2AD84142'
+ '61B4E2D010ABD8FC6D2FB768912F78EE6B05A60857532B75B75EF'
+ 'C007601A4EF58BA947B7E75E38F3443CDD87E7C138A1DAD9D9FB3'
+ '19FF69DA43A9F6F6B0CD243F042CD1A5AFAEB286BD46AEB2D922B'
+ 'D01385D6892167074A0907F94A2BF08A54ABB2FFFFC89920861D0'
+ '46F8706AB88DDADBD9E8204D48B87789081E074024C8996783B31'
+ '7076A98ABF0A2D8550EAF2097D8CCC7BE76EF', 16)
+
+ expected = RSAPublicNumbers(expected_e, expected_n)
+
+ assert numbers == expected