aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst1
-rw-r--r--docs/hazmat/primitives/asymmetric/serialization.rst13
-rw-r--r--src/cryptography/hazmat/backends/openssl/backend.py55
-rw-r--r--src/cryptography/hazmat/backends/openssl/dsa.py1
-rw-r--r--src/cryptography/hazmat/backends/openssl/ec.py1
-rw-r--r--src/cryptography/hazmat/backends/openssl/rsa.py1
-rw-r--r--src/cryptography/hazmat/primitives/serialization.py37
-rw-r--r--tests/hazmat/primitives/test_dsa.py23
-rw-r--r--tests/hazmat/primitives/test_ec.py28
-rw-r--r--tests/hazmat/primitives/test_rsa.py36
10 files changed, 182 insertions, 14 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 22764e53..f8602e90 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,7 @@ Changelog
* Support for OpenSSL 0.9.8 has been removed. Users on older version of OpenSSL
will need to upgrade.
* Added :class:`~cryptography.hazmat.primitives.kdf.kbkdf.KBKDFHMAC`.
+* Added support for ``OpenSSH`` public key serialization.
1.3.4 - 2016-06-03
~~~~~~~~~~~~~~~~~~
diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst
index b94c0e10..7cef77fd 100644
--- a/docs/hazmat/primitives/asymmetric/serialization.rst
+++ b/docs/hazmat/primitives/asymmetric/serialization.rst
@@ -360,6 +360,13 @@ Serialization Formats
Just the public key elements (without the algorithm identifier). This
format is RSA only, but is used by some older systems.
+ .. attribute:: OpenSSH
+
+ .. versionadded:: 1.4
+
+ The public key format used by OpenSSH (e.g. as found in
+ ``~/.ssh/id_rsa.pub`` or ``~/.ssh/authorized_keys``).
+
Serialization Encodings
~~~~~~~~~~~~~~~~~~~~~~~
@@ -389,6 +396,12 @@ Serialization Encodings
For DER format. This is a binary format.
+ .. attribute:: OpenSSH
+
+ .. versionadded:: 1.4
+
+ The format used by OpenSSH public keys. This is a text format.
+
Serialization Encryption Types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index d8a681e6..126a881a 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -4,6 +4,7 @@
from __future__ import absolute_import, division, print_function
+import base64
import calendar
import collections
import itertools
@@ -1698,11 +1699,23 @@ class Backend(object):
self.openssl_assert(res == 1)
return self._read_mem_bio(bio)
- def _public_key_bytes(self, encoding, format, evp_pkey, cdata):
+ def _public_key_bytes(self, encoding, format, key, evp_pkey, cdata):
if not isinstance(encoding, serialization.Encoding):
raise TypeError("encoding must be an item from the Encoding enum")
- if format is serialization.PublicFormat.SubjectPublicKeyInfo:
+ if (
+ format is serialization.PublicFormat.OpenSSH or
+ encoding is serialization.Encoding.OpenSSH
+ ):
+ if (
+ format is not serialization.PublicFormat.OpenSSH or
+ encoding is not serialization.Encoding.OpenSSH
+ ):
+ raise ValueError(
+ "OpenSSH format must be used with OpenSSH encoding"
+ )
+ return self._openssh_public_key_bytes(key)
+ elif format is serialization.PublicFormat.SubjectPublicKeyInfo:
if encoding is serialization.Encoding.PEM:
write_bio = self._lib.PEM_write_bio_PUBKEY
else:
@@ -1732,6 +1745,44 @@ class Backend(object):
self.openssl_assert(res == 1)
return self._read_mem_bio(bio)
+ def _openssh_public_key_bytes(self, key):
+ if isinstance(key, rsa.RSAPublicKey):
+ public_numbers = key.public_numbers()
+ return b"ssh-rsa " + base64.b64encode(
+ serialization._ssh_write_string(b"ssh-rsa") +
+ serialization._ssh_write_mpint(public_numbers.e) +
+ serialization._ssh_write_mpint(public_numbers.n)
+ )
+ elif isinstance(key, dsa.DSAPublicKey):
+ public_numbers = key.public_numbers()
+ parameter_numbers = public_numbers.parameter_numbers
+ return b"ssh-dss " + base64.b64encode(
+ serialization._ssh_write_string(b"ssh-dss") +
+ serialization._ssh_write_mpint(parameter_numbers.p) +
+ serialization._ssh_write_mpint(parameter_numbers.q) +
+ serialization._ssh_write_mpint(parameter_numbers.g) +
+ serialization._ssh_write_mpint(public_numbers.y)
+ )
+ else:
+ assert isinstance(key, ec.EllipticCurvePublicKey)
+ public_numbers = key.public_numbers()
+ try:
+ curve_name = {
+ ec.SECP256R1: b"nistp256",
+ ec.SECP384R1: b"nistp384",
+ ec.SECP521R1: b"nistp521",
+ }[type(public_numbers.curve)]
+ except KeyError:
+ raise ValueError(
+ "Only SECP256R1, SECP384R1, and SECP521R1 curves are "
+ "supported by the SSH public key format"
+ )
+ return b"ecdsa-sha2-" + curve_name + b" " + base64.b64encode(
+ serialization._ssh_write_string(b"ecdsa-sha2-" + curve_name) +
+ serialization._ssh_write_string(curve_name) +
+ serialization._ssh_write_string(public_numbers.encode_point())
+ )
+
class GetCipherByName(object):
def __init__(self, fmt):
diff --git a/src/cryptography/hazmat/backends/openssl/dsa.py b/src/cryptography/hazmat/backends/openssl/dsa.py
index 5abc3da9..1608df04 100644
--- a/src/cryptography/hazmat/backends/openssl/dsa.py
+++ b/src/cryptography/hazmat/backends/openssl/dsa.py
@@ -297,6 +297,7 @@ class _DSAPublicKey(object):
return self._backend._public_key_bytes(
encoding,
format,
+ self,
self._evp_pkey,
None
)
diff --git a/src/cryptography/hazmat/backends/openssl/ec.py b/src/cryptography/hazmat/backends/openssl/ec.py
index aa4a7a35..2f476031 100644
--- a/src/cryptography/hazmat/backends/openssl/ec.py
+++ b/src/cryptography/hazmat/backends/openssl/ec.py
@@ -299,6 +299,7 @@ class _EllipticCurvePublicKey(object):
return self._backend._public_key_bytes(
encoding,
format,
+ self,
self._evp_pkey,
None
)
diff --git a/src/cryptography/hazmat/backends/openssl/rsa.py b/src/cryptography/hazmat/backends/openssl/rsa.py
index d8458ccc..920bae06 100644
--- a/src/cryptography/hazmat/backends/openssl/rsa.py
+++ b/src/cryptography/hazmat/backends/openssl/rsa.py
@@ -642,6 +642,7 @@ class _RSAPublicKey(object):
return self._backend._public_key_bytes(
encoding,
format,
+ self,
self._evp_pkey,
self._rsa_cdata
)
diff --git a/src/cryptography/hazmat/primitives/serialization.py b/src/cryptography/hazmat/primitives/serialization.py
index d848e5d4..992fd42f 100644
--- a/src/cryptography/hazmat/primitives/serialization.py
+++ b/src/cryptography/hazmat/primitives/serialization.py
@@ -59,7 +59,7 @@ def load_ssh_public_key(data, backend):
except TypeError:
raise ValueError('Key is not in the proper format.')
- inner_key_type, rest = _read_next_string(decoded_data)
+ inner_key_type, rest = _ssh_read_next_string(decoded_data)
if inner_key_type != key_type:
raise ValueError(
@@ -70,8 +70,8 @@ def load_ssh_public_key(data, backend):
def _load_ssh_rsa_public_key(key_type, decoded_data, backend):
- e, rest = _read_next_mpint(decoded_data)
- n, rest = _read_next_mpint(rest)
+ e, rest = _ssh_read_next_mpint(decoded_data)
+ n, rest = _ssh_read_next_mpint(rest)
if rest:
raise ValueError('Key body contains extra bytes.')
@@ -80,10 +80,10 @@ def _load_ssh_rsa_public_key(key_type, decoded_data, backend):
def _load_ssh_dss_public_key(key_type, decoded_data, backend):
- p, rest = _read_next_mpint(decoded_data)
- q, rest = _read_next_mpint(rest)
- g, rest = _read_next_mpint(rest)
- y, rest = _read_next_mpint(rest)
+ p, rest = _ssh_read_next_mpint(decoded_data)
+ q, rest = _ssh_read_next_mpint(rest)
+ g, rest = _ssh_read_next_mpint(rest)
+ y, rest = _ssh_read_next_mpint(rest)
if rest:
raise ValueError('Key body contains extra bytes.')
@@ -95,8 +95,8 @@ def _load_ssh_dss_public_key(key_type, decoded_data, backend):
def _load_ssh_ecdsa_public_key(expected_key_type, decoded_data, backend):
- curve_name, rest = _read_next_string(decoded_data)
- data, rest = _read_next_string(rest)
+ curve_name, rest = _ssh_read_next_string(decoded_data)
+ data, rest = _ssh_read_next_string(rest)
if expected_key_type != b"ecdsa-sha2-" + curve_name:
raise ValueError(
@@ -121,7 +121,7 @@ def _load_ssh_ecdsa_public_key(expected_key_type, decoded_data, backend):
return numbers.public_key(backend)
-def _read_next_string(data):
+def _ssh_read_next_string(data):
"""
Retrieves the next RFC 4251 string value from the data.
@@ -137,22 +137,34 @@ def _read_next_string(data):
return data[4:4 + str_len], data[4 + str_len:]
-def _read_next_mpint(data):
+def _ssh_read_next_mpint(data):
"""
Reads the next mpint from the data.
Currently, all mpints are interpreted as unsigned.
"""
- mpint_data, rest = _read_next_string(data)
+ mpint_data, rest = _ssh_read_next_string(data)
return (
utils.int_from_bytes(mpint_data, byteorder='big', signed=False), rest
)
+def _ssh_write_string(data):
+ return struct.pack(">I", len(data)) + data
+
+
+def _ssh_write_mpint(value):
+ data = utils.int_to_bytes(value)
+ if six.indexbytes(data, 0) & 0x80:
+ data = b"\x00" + data
+ return _ssh_write_string(data)
+
+
class Encoding(Enum):
PEM = "PEM"
DER = "DER"
+ OpenSSH = "OpenSSH"
class PrivateFormat(Enum):
@@ -163,6 +175,7 @@ class PrivateFormat(Enum):
class PublicFormat(Enum):
SubjectPublicKeyInfo = "X.509 subjectPublicKeyInfo with PKCS#1"
PKCS1 = "Raw PKCS#1"
+ OpenSSH = "OpenSSH"
@six.add_metaclass(abc.ABCMeta)
diff --git a/tests/hazmat/primitives/test_dsa.py b/tests/hazmat/primitives/test_dsa.py
index b02cadc8..6ad9762a 100644
--- a/tests/hazmat/primitives/test_dsa.py
+++ b/tests/hazmat/primitives/test_dsa.py
@@ -1018,6 +1018,29 @@ class TestDSAPEMPublicKeySerialization(object):
)
assert serialized == key_bytes
+ def test_public_bytes_openssh(self, backend):
+ key_bytes = load_vectors_from_file(
+ os.path.join("asymmetric", "PKCS8", "unenc-dsa-pkcs8.pub.pem"),
+ lambda pemfile: pemfile.read(), mode="rb"
+ )
+ key = serialization.load_pem_public_key(key_bytes, backend)
+
+ ssh_bytes = key.public_bytes(
+ serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH
+ )
+ assert ssh_bytes == (
+ b"ssh-dss AAAAB3NzaC1kc3MAAACBAKoJMMwUWCUiHK/6KKwolBlqJ4M95ewhJweR"
+ b"aJQgd3Si57I4sNNvGySZosJYUIPrAUMpJEGNhn+qIS3RBx1NzrJ4J5StOTzAik1K"
+ b"2n9o1ug5pfzTS05ALYLLioy0D+wxkRv5vTYLA0yqy0xelHmSVzyekAmcGw8FlAyr"
+ b"5dLeSaFnAAAAFQCtwOhps28KwBOmgf301ImdaYIEUQAAAIEAjGtFia+lOk0QSL/D"
+ b"RtHzhsp1UhzPct2qJRKGiA7hMgH/SIkLv8M9ebrK7HHnp3hQe9XxpmQi45QVvgPn"
+ b"EUG6Mk9bkxMZKRgsiKn6QGKDYGbOvnS1xmkMfRARBsJAq369VOTjMB/Qhs5q2ski"
+ b"+ycTorCIfLoTubxozlz/8kHNMkYAAACAKyYOqX3GoSrpMsZA5989j/BKigWgMk+N"
+ b"Xxsj8V+hcP8/QgYRJO/yWGyxG0moLc3BuQ/GqE+xAQnLZ9tdLalxrq8Xvl43KEVj"
+ b"5MZNnl/ISAJYsxnw3inVTYNQcNnih5FNd9+BSR9EI7YtqYTrP0XrKin86l2uUlrG"
+ b"q2vM4Ev99bY="
+ )
+
def test_public_bytes_invalid_encoding(self, backend):
key = DSA_KEY_2048.private_key(backend).public_key()
with pytest.raises(TypeError):
diff --git a/tests/hazmat/primitives/test_ec.py b/tests/hazmat/primitives/test_ec.py
index 8747ea4f..8705f79c 100644
--- a/tests/hazmat/primitives/test_ec.py
+++ b/tests/hazmat/primitives/test_ec.py
@@ -815,6 +815,34 @@ class TestEllipticCurvePEMPublicKeySerialization(object):
)
assert serialized == key_bytes
+ def test_public_bytes_openssh(self, backend):
+ _skip_curve_unsupported(backend, ec.SECP192R1())
+ _skip_curve_unsupported(backend, ec.SECP256R1())
+
+ key_bytes = load_vectors_from_file(
+ os.path.join(
+ "asymmetric", "PEM_Serialization", "ec_public_key.pem"
+ ),
+ lambda pemfile: pemfile.read(), mode="rb"
+ )
+ key = serialization.load_pem_public_key(key_bytes, backend)
+
+ ssh_bytes = key.public_bytes(
+ serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH
+ )
+ assert ssh_bytes == (
+ b"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy"
+ b"NTYAAABBBCS8827s9rUZyxZTi/um01+oIlWrwLHOjQxRU9CDAndom00zVAw5BRrI"
+ b"KtHB+SWD4P+sVJTARSq1mHt8kOIWrPc="
+ )
+
+ key = ec.generate_private_key(ec.SECP192R1(), backend).public_key()
+ with pytest.raises(ValueError):
+ key.public_bytes(
+ serialization.Encoding.OpenSSH,
+ serialization.PublicFormat.OpenSSH
+ )
+
def test_public_bytes_invalid_encoding(self, backend):
_skip_curve_unsupported(backend, ec.SECP256R1())
key = load_vectors_from_file(
diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py
index 6a8bb95d..320a96e5 100644
--- a/tests/hazmat/primitives/test_rsa.py
+++ b/tests/hazmat/primitives/test_rsa.py
@@ -2066,6 +2066,42 @@ class TestRSAPEMPublicKeySerialization(object):
serialized = key.public_bytes(encoding, format)
assert serialized == key_bytes
+ def test_public_bytes_openssh(self, backend):
+ key_bytes = load_vectors_from_file(
+ os.path.join("asymmetric", "public", "PKCS1", "rsa.pub.pem"),
+ lambda pemfile: pemfile.read(), mode="rb"
+ )
+ key = serialization.load_pem_public_key(key_bytes, backend)
+
+ ssh_bytes = key.public_bytes(
+ serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH
+ )
+ assert ssh_bytes == (
+ b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC7JHoJfg6yNzLMOWet8Z49a4KD"
+ b"0dCspMAYvo2YAMB7/wdEycocujbhJ2n/seONi+5XqTqqFkM5VBl8rmkkFPZk/7x0"
+ b"xmdsTPECSWnHK+HhoaNDFPR3j8jQhVo1laxiqcEhAHegi5cwtFosuJAvSKAFKEvy"
+ b"D43si00DQnXWrYHAEQ=="
+ )
+
+ with pytest.raises(ValueError):
+ key.public_bytes(
+ serialization.Encoding.PEM, serialization.PublicFormat.OpenSSH
+ )
+ with pytest.raises(ValueError):
+ key.public_bytes(
+ serialization.Encoding.DER, serialization.PublicFormat.OpenSSH
+ )
+ with pytest.raises(ValueError):
+ key.public_bytes(
+ serialization.Encoding.OpenSSH,
+ serialization.PublicFormat.PKCS1,
+ )
+ with pytest.raises(ValueError):
+ key.public_bytes(
+ serialization.Encoding.OpenSSH,
+ serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+
def test_public_bytes_invalid_encoding(self, backend):
key = RSA_KEY_2048.private_key(backend).public_key()
with pytest.raises(TypeError):