Merge pull request #2447 from reaperhulk/encode-decode-point

add support for encoding/decoding elliptic curve points
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 08ac109..2689273 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -16,6 +16,12 @@
   :func:`~cryptography.hazmat.primitives.keywrap.aes_key_wrap` and
   :func:`~cryptography.hazmat.primitives.keywrap.aes_key_unwrap`.
 * Added an ``__hash__`` method to :class:`~cryptography.x509.Name`.
+* Add support for encoding and decoding elliptic curve points to a byte string
+  form using
+  :meth:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicNumbers.encode_point`
+  and
+  :meth:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicNumbers.from_encoded_point`.
+
 
 1.0.2 - 2015-09-27
 ~~~~~~~~~~~~~~~~~~
diff --git a/docs/hazmat/primitives/asymmetric/ec.rst b/docs/hazmat/primitives/asymmetric/ec.rst
index 90e7371..c1619dd 100644
--- a/docs/hazmat/primitives/asymmetric/ec.rst
+++ b/docs/hazmat/primitives/asymmetric/ec.rst
@@ -122,6 +122,37 @@
         :returns: A new instance of a :class:`EllipticCurvePublicKey`
             provider.
 
+    .. method:: encode_point()
+
+        .. versionadded:: 1.1
+
+        Encodes an elliptic curve point to a byte string as described in
+        `SEC 1 v2.0`_ section 2.3.3. This method only supports uncompressed
+        points.
+
+        :return bytes: The encoded point.
+
+    .. classmethod:: from_encoded_point(curve, data)
+
+        .. versionadded:: 1.1
+
+        Decodes a byte string as described in `SEC 1 v2.0`_ section 2.3.3 and
+        returns an :class:`EllipticCurvePublicNumbers`. This method only
+        supports uncompressed points.
+
+        :param curve: An
+            :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve`
+            instance.
+
+        :param bytes data: The serialized point byte string.
+
+        :returns: An :class:`EllipticCurvePublicNumbers` instance.
+
+        :raises ValueError: Raised on invalid point type or data length.
+
+        :raises TypeError: Raised when curve is not an
+            :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurve`.
+
 Elliptic Curve Key Exchange algorithm
 -------------------------------------
 
@@ -478,3 +509,4 @@
 .. _`ECDSA`: https://en.wikipedia.org/wiki/ECDSA
 .. _`EdDSA`: https://en.wikipedia.org/wiki/EdDSA
 .. _`forward secrecy`: https://en.wikipedia.org/wiki/Forward_secrecy
+.. _`SEC 1 v2.0`: http://www.secg.org/sec1-v2.pdf
diff --git a/src/cryptography/hazmat/primitives/asymmetric/ec.py b/src/cryptography/hazmat/primitives/asymmetric/ec.py
index c6f8366..7782133 100644
--- a/src/cryptography/hazmat/primitives/asymmetric/ec.py
+++ b/src/cryptography/hazmat/primitives/asymmetric/ec.py
@@ -259,6 +259,31 @@
     def public_key(self, backend):
         return backend.load_elliptic_curve_public_numbers(self)
 
+    def encode_point(self):
+        # key_size is in bits. Convert to bytes and round up
+        byte_length = (self.curve.key_size + 7) // 8
+        return (
+            b'\x04' + utils.int_to_bytes(self.x, byte_length) +
+            utils.int_to_bytes(self.y, byte_length)
+        )
+
+    @classmethod
+    def from_encoded_point(cls, curve, data):
+        if not isinstance(curve, EllipticCurve):
+            raise TypeError("curve must be an EllipticCurve instance")
+
+        if data.startswith(b'\x04'):
+            # key_size is in bits. Convert to bytes and round up
+            byte_length = (curve.key_size + 7) // 8
+            if len(data) == 2 * byte_length + 1:
+                x = utils.int_from_bytes(data[1:byte_length + 1], 'big')
+                y = utils.int_from_bytes(data[byte_length + 1:], 'big')
+                return cls(x, y, curve)
+            else:
+                raise ValueError('Invalid elliptic curve point data length')
+        else:
+            raise ValueError('Unsupported elliptic curve point type')
+
     curve = utils.read_only_property("_curve")
     x = utils.read_only_property("_x")
     y = utils.read_only_property("_y")
diff --git a/src/cryptography/utils.py b/src/cryptography/utils.py
index dac4046..dbd961f 100644
--- a/src/cryptography/utils.py
+++ b/src/cryptography/utils.py
@@ -48,9 +48,12 @@
         return result
 
 
-def int_to_bytes(integer):
+def int_to_bytes(integer, length=None):
     hex_string = '%x' % integer
-    n = len(hex_string)
+    if length is None:
+        n = len(hex_string)
+    else:
+        n = length * 2
     return binascii.unhexlify(hex_string.zfill(n + (n & 1)))
 
 
diff --git a/tests/hazmat/primitives/test_ec.py b/tests/hazmat/primitives/test_ec.py
index 4c4d5b9..5baaa3c 100644
--- a/tests/hazmat/primitives/test_ec.py
+++ b/tests/hazmat/primitives/test_ec.py
@@ -4,6 +4,7 @@
 
 from __future__ import absolute_import, division, print_function
 
+import binascii
 import itertools
 import os
 
@@ -148,6 +149,72 @@
         )
 
 
+def test_encode_point():
+    # secp256r1 point
+    x = int(
+        '233ea3b0027127084cd2cd336a13aeef69c598d8af61369a36454a17c6c22aec',
+        16
+    )
+    y = int(
+        '3ea2c10a84153862be4ec82940f0543f9ba866af9751a6ee79d38460b35f442e',
+        16
+    )
+    pn = ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1())
+    data = pn.encode_point()
+    assert data == binascii.unhexlify(
+        "04233ea3b0027127084cd2cd336a13aeef69c598d8af61369a36454a17c6c22ae"
+        "c3ea2c10a84153862be4ec82940f0543f9ba866af9751a6ee79d38460b35f442e"
+    )
+
+
+def test_from_encoded_point():
+    # secp256r1 point
+    data = binascii.unhexlify(
+        "04233ea3b0027127084cd2cd336a13aeef69c598d8af61369a36454a17c6c22ae"
+        "c3ea2c10a84153862be4ec82940f0543f9ba866af9751a6ee79d38460b35f442e"
+    )
+    pn = ec.EllipticCurvePublicNumbers.from_encoded_point(
+        ec.SECP256R1(), data
+    )
+    assert pn.x == int(
+        '233ea3b0027127084cd2cd336a13aeef69c598d8af61369a36454a17c6c22aec',
+        16
+    )
+    assert pn.y == int(
+        '3ea2c10a84153862be4ec82940f0543f9ba866af9751a6ee79d38460b35f442e',
+        16
+    )
+
+
+def test_from_encoded_point_invalid_length():
+    bad_data = binascii.unhexlify(
+        "04233ea3b0027127084cd2cd336a13aeef69c598d8af61369a36454a17c6c22ae"
+        "c3ea2c10a84153862be4ec82940f0543f9ba866af9751a6ee79d38460"
+    )
+    with pytest.raises(ValueError):
+        ec.EllipticCurvePublicNumbers.from_encoded_point(
+            ec.SECP384R1(), bad_data
+        )
+
+
+def test_from_encoded_point_unsupported_point_type():
+    # set to point type 2.
+    unsupported_type = binascii.unhexlify(
+        "02233ea3b0027127084cd2cd336a13aeef69c598d8af61369a36454a17c6c22a"
+    )
+    with pytest.raises(ValueError):
+        ec.EllipticCurvePublicNumbers.from_encoded_point(
+            ec.SECP256R1(), unsupported_type
+        )
+
+
+def test_from_encoded_point_not_a_curve():
+    with pytest.raises(TypeError):
+        ec.EllipticCurvePublicNumbers.from_encoded_point(
+            "notacurve", b"\x04data"
+        )
+
+
 @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
 class TestECWithNumbers(object):
     @pytest.mark.parametrize(