Merge pull request #1331 from michael-hart/public_key_pem

Add support for .PEM public keys, with tests and docs
diff --git a/cryptography/hazmat/backends/interfaces.py b/cryptography/hazmat/backends/interfaces.py
index 3761e25..dc720ad 100644
--- a/cryptography/hazmat/backends/interfaces.py
+++ b/cryptography/hazmat/backends/interfaces.py
@@ -281,6 +281,12 @@
         if the data is encrypted.
         """
 
+    @abc.abstractmethod
+    def load_pem_public_key(self, data):
+        """
+        Loads a public key from PEM encoded data.
+        """
+
 
 @six.add_metaclass(abc.ABCMeta)
 class TraditionalOpenSSLSerializationBackend(object):
@@ -297,6 +303,6 @@
     @abc.abstractmethod
     def load_pkcs8_pem_private_key(self, data, password):
         """
-        Load a private key from PEM encoded data, using password if the data
+        Load a private key from PKCS8 encoded data, using password if the data
         is encrypted.
         """
diff --git a/cryptography/hazmat/backends/multibackend.py b/cryptography/hazmat/backends/multibackend.py
index 221f1a1..163dd0e 100644
--- a/cryptography/hazmat/backends/multibackend.py
+++ b/cryptography/hazmat/backends/multibackend.py
@@ -329,6 +329,15 @@
             _Reasons.UNSUPPORTED_SERIALIZATION
         )
 
+    def load_pem_public_key(self, data):
+        for b in self._filtered_backends(PEMSerializationBackend):
+            return b.load_pem_public_key(data)
+
+        raise UnsupportedAlgorithm(
+            "This backend does not support this key serialization.",
+            _Reasons.UNSUPPORTED_SERIALIZATION
+        )
+
     def load_pkcs8_pem_private_key(self, data, password):
         for b in self._filtered_backends(PKCS8SerializationBackend):
             return b.load_pkcs8_pem_private_key(data, password)
diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py
index cb988ac..0b129d1 100644
--- a/cryptography/hazmat/backends/openssl/backend.py
+++ b/cryptography/hazmat/backends/openssl/backend.py
@@ -483,6 +483,33 @@
         else:
             raise UnsupportedAlgorithm("Unsupported key type.")
 
+    def _evp_pkey_to_public_key(self, evp_pkey):
+        """
+        Return the appropriate type of PublicKey given an evp_pkey cdata
+        pointer.
+        """
+
+        type = evp_pkey.type
+
+        if type == self._lib.EVP_PKEY_RSA:
+            rsa_cdata = self._lib.EVP_PKEY_get1_RSA(evp_pkey)
+            assert rsa_cdata != self._ffi.NULL
+            rsa_cdata = self._ffi.gc(rsa_cdata, self._lib.RSA_free)
+            return _RSAPublicKey(self, rsa_cdata)
+        elif type == self._lib.EVP_PKEY_DSA:
+            dsa_cdata = self._lib.EVP_PKEY_get1_DSA(evp_pkey)
+            assert dsa_cdata != self._ffi.NULL
+            dsa_cdata = self._ffi.gc(dsa_cdata, self._lib.DSA_free)
+            return _DSAPublicKey(self, dsa_cdata)
+        elif self._lib.Cryptography_HAS_EC == 1 \
+                and type == self._lib.EVP_PKEY_EC:
+            ec_cdata = self._lib.EVP_PKEY_get1_EC_KEY(evp_pkey)
+            assert ec_cdata != self._ffi.NULL
+            ec_cdata = self._ffi.gc(ec_cdata, self._lib.EC_KEY_free)
+            return _EllipticCurvePublicKey(self, ec_cdata, None)
+        else:
+            raise UnsupportedAlgorithm("Unsupported key type.")
+
     def _pem_password_cb(self, password):
         """
         Generate a pem_password_cb function pointer that copied the password to
@@ -787,6 +814,14 @@
             password,
         )
 
+    def load_pem_public_key(self, data):
+        return self._load_key(
+            self._lib.PEM_read_bio_PUBKEY,
+            self._evp_pkey_to_public_key,
+            data,
+            None,
+        )
+
     def load_traditional_openssl_pem_private_key(self, data, password):
         warnings.warn(
             "load_traditional_openssl_pem_private_key is deprecated and will "
diff --git a/cryptography/hazmat/primitives/serialization.py b/cryptography/hazmat/primitives/serialization.py
index cf1ca8e..0fb560e 100644
--- a/cryptography/hazmat/primitives/serialization.py
+++ b/cryptography/hazmat/primitives/serialization.py
@@ -44,3 +44,7 @@
 
 def load_pem_private_key(data, password, backend):
     return backend.load_pem_private_key(data, password)
+
+
+def load_pem_public_key(data, backend):
+    return backend.load_pem_public_key(data)
diff --git a/docs/hazmat/backends/interfaces.rst b/docs/hazmat/backends/interfaces.rst
index f8341d1..e8e1bac 100644
--- a/docs/hazmat/backends/interfaces.rst
+++ b/docs/hazmat/backends/interfaces.rst
@@ -595,6 +595,12 @@
         :raises cryptography.exceptions.UnsupportedAlgorithm: If the data is
             encrypted with an unsupported algorithm.
 
+    .. method:: load_pem_public_key(data)
+
+        :param bytes data: PEM data to load.
+        :return: A new instance of the appropriate type of public key serialized data contains.
+        :raises ValueError: If the data could not be deserialized.
+
 .. class:: TraditionalOpenSSLSerializationBackend
 
     .. versionadded:: 0.3
diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst
index 7a953d9..18b89c4 100644
--- a/docs/hazmat/primitives/asymmetric/serialization.rst
+++ b/docs/hazmat/primitives/asymmetric/serialization.rst
@@ -92,6 +92,27 @@
         is not supported by the backend or if the key is encrypted with a
         symmetric cipher that is not supported by the backend.
 
+.. function:: load_pem_public_key(data, backend):
+
+    .. versionadded:: 0.6
+
+    Deserialize a public key from PEM encoded data to one of the supported
+    asymmetric public key types.
+
+    :param bytes data: The PEM encoded key data.
+
+    :param backend: A
+        :class:`~cryptography.hazmat.backends.interfaces.PEMSerializationBackend`
+        provider.
+
+    :returns: A new instance of a public key.
+
+    :raises ValueError: If the PEM data could not be decrypted or if its
+        structure could not be decoded successfully.
+
+    :raises UnsupportedAlgorithm: If the serialized key is of a type that
+        is not supported by the backend.
+
 
 PKCS #8 Format
 ~~~~~~~~~~~~~~
diff --git a/tests/hazmat/backends/test_multibackend.py b/tests/hazmat/backends/test_multibackend.py
index 655acc4..45c12b3 100644
--- a/tests/hazmat/backends/test_multibackend.py
+++ b/tests/hazmat/backends/test_multibackend.py
@@ -217,6 +217,9 @@
     def load_pem_private_key(self, data, password):
         pass
 
+    def load_pem_public_key(self, data):
+        pass
+
 
 class TestMultiBackend(object):
     def test_ciphers(self):
@@ -532,7 +535,10 @@
         backend = MultiBackend([DummyPEMSerializationBackend()])
 
         backend.load_pem_private_key(b"keydata", None)
+        backend.load_pem_public_key(b"keydata")
 
         backend = MultiBackend([])
         with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_SERIALIZATION):
             backend.load_pem_private_key(b"keydata", None)
+        with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_SERIALIZATION):
+            backend.load_pem_public_key(b"keydata")
diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py
index cf70f10..d4c5e2e 100644
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -473,6 +473,8 @@
         key = pretend.stub(type="unsupported")
         with raises_unsupported_algorithm(None):
             backend._evp_pkey_to_private_key(key)
+        with raises_unsupported_algorithm(None):
+            backend._evp_pkey_to_public_key(key)
 
     def test_very_long_pem_serialization_password(self):
         password = "x" * 1024
diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py
index d369e8f..8405f4b 100644
--- a/tests/hazmat/primitives/test_serialization.py
+++ b/tests/hazmat/primitives/test_serialization.py
@@ -24,6 +24,7 @@
 from cryptography.hazmat.primitives.asymmetric import ec
 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
 )
 
@@ -38,7 +39,7 @@
     def test_load_pem_rsa_private_key(self, backend):
         key = load_vectors_from_file(
             os.path.join(
-                "asymmetric", "Traditional_OpenSSL_Serialization", "key1.pem"),
+                "asymmetric", "PEM_Serialization", "rsa_private_key.pem"),
             lambda pemfile: load_pem_private_key(
                 pemfile.read().encode(), b"123456", backend
             )
@@ -49,6 +50,17 @@
         if isinstance(key, interfaces.RSAPrivateKeyWithNumbers):
             _check_rsa_private_numbers(key.private_numbers())
 
+    def test_load_dsa_private_key(self, backend):
+        key = load_vectors_from_file(
+            os.path.join(
+                "asymmetric", "PEM_Serialization", "dsa_private_key.pem"),
+            lambda pemfile: load_pem_private_key(
+                pemfile.read().encode(), b"123456", backend
+            )
+        )
+        assert key
+        assert isinstance(key, interfaces.DSAPrivateKey)
+
     @pytest.mark.parametrize(
         ("key_file", "password"),
         [
@@ -70,6 +82,60 @@
         assert key
         assert isinstance(key, interfaces.EllipticCurvePrivateKey)
 
+    @pytest.mark.parametrize(
+        ("key_file"),
+        [
+            os.path.join("asymmetric", "PKCS8", "unenc-rsa-pkcs8.pub.pem"),
+            os.path.join(
+                "asymmetric", "PEM_Serialization", "rsa_public_key.pem"),
+        ]
+    )
+    def test_load_pem_rsa_public_key(self, key_file, backend):
+        key = load_vectors_from_file(
+            key_file,
+            lambda pemfile: load_pem_public_key(
+                pemfile.read().encode(), backend
+            )
+        )
+        assert key
+        assert isinstance(key, interfaces.RSAPublicKey)
+        if isinstance(key, interfaces.RSAPublicKeyWithNumbers):
+            numbers = key.public_numbers()
+            assert numbers.e == 65537
+
+    @pytest.mark.parametrize(
+        ("key_file"),
+        [
+            os.path.join("asymmetric", "PKCS8", "unenc-dsa-pkcs8.pub.pem"),
+            os.path.join(
+                "asymmetric", "PEM_Serialization",
+                "dsa_public_key.pem"),
+        ]
+    )
+    def test_load_pem_dsa_public_key(self, key_file, backend):
+        key = load_vectors_from_file(
+            key_file,
+            lambda pemfile: load_pem_public_key(
+                pemfile.read().encode(), backend
+            )
+        )
+        assert key
+        assert isinstance(key, interfaces.DSAPublicKey)
+
+    @pytest.mark.elliptic
+    def test_load_ec_public_key(self, backend):
+        _skip_curve_unsupported(backend, ec.SECP256R1())
+        key = load_vectors_from_file(
+            os.path.join(
+                "asymmetric", "PEM_Serialization",
+                "ec_public_key.pem"),
+            lambda pemfile: load_pem_public_key(
+                pemfile.read().encode(), backend
+            )
+        )
+        assert key
+        assert isinstance(key, interfaces.EllipticCurvePublicKey)
+
 
 @pytest.mark.traditional_openssl_serialization
 class TestTraditionalOpenSSLSerialization(object):
diff --git a/vectors/cryptography_vectors/asymmetric/PEM_Serialization/README.txt b/vectors/cryptography_vectors/asymmetric/PEM_Serialization/README.txt
index 6963d2b..14f9151 100644
--- a/vectors/cryptography_vectors/asymmetric/PEM_Serialization/README.txt
+++ b/vectors/cryptography_vectors/asymmetric/PEM_Serialization/README.txt
@@ -3,13 +3,13 @@
 Contains
 
 1. ec_private_key.pem - Contains an Elliptic Curve key generated using OpenSSL, from the curve secp256r1.
-2. ec_private_key_encrypted.pem - Contains the same Elliptic Curve key as ec_private_key.pem, except that 
+2. ec_private_key_encrypted.pem - Contains the same Elliptic Curve key as ec_private_key.pem, except that
    it is encrypted with AES-256 with the password "123456".
 3. ec_public_key.pem - Contains the public key corresponding to ec_private_key.pem, generated using OpenSSL.
 4. rsa_private_key.pem - Contains an RSA 2048 bit key generated using OpenSSL, protected by the secret
    "123456" with DES3 encryption.
 5. rsa_public_key.pem - Contains an RSA 2048 bit public generated using OpenSSL from rsa_private_key.pem.
 6. dsaparam.pem - Contains 2048-bit DSA parameters generated using OpenSSL; contains no keys.
-7. dsa_private_key.pem - Contains a DSA 2048 bit key generated using OpenSSL from the parameters in 
+7. dsa_private_key.pem - Contains a DSA 2048 bit key generated using OpenSSL from the parameters in
    dsaparam.pem, protected by the secret "123456" with DES3 encryption.
-8. dsa_public_key.pem - Contains a DSA 2048 bit key generated using OpenSSL from dsa_private_key.pem.
\ No newline at end of file
+8. dsa_public_key.pem - Contains a DSA 2048 bit key generated using OpenSSL from dsa_private_key.pem.