Repository: pyca/pyopenssl
Pull request #733 (2018-02-06 09:12:29)

Add Connection.get_certificate method
This makes it possible to retrieve the local certificate (if any)
for a Connection.

An example where this is useful is when negotiating a DTLS-SRTP
connection, the fingerprint of the local certificate needs to be
communicated to the remote party out-of-band via SDP.

This depends on:
https://github.com/pyca/cryptography/pull/4100

codecov[bot]: # [Codecov](https://codecov.io/gh/pyca/pyopenssl/pull/733?src=pr&el=h1) Report
> Merging [#733](https://codecov.io/gh/pyca/pyopenssl/pull/733?src=pr&el=desc) into [master](https://codecov.io/gh/pyca/pyopenssl/commit/3d231f03ed24f760d1896f5e2be3adf1cbf13127?src=pr&el=desc) will **decrease** coverage by `0.18%`.
> The diff coverage is `42.1%`.

[![Impacted file tree graph](https://codecov.io/gh/pyca/pyopenssl/pull/733/graphs/tree.svg?width=650&src=pr&token=KKfpMZOTgK&height=150)](https://codecov.io/gh/pyca/pyopenssl/pull/733?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #733      +/-   ##
==========================================
- Coverage   97.05%   96.87%   -0.19%     
==========================================
  Files          18       18              
  Lines        5711     5730      +19     
  Branches      395      396       +1     
==========================================
+ Hits         5543     5551       +8     
- Misses        112      123      +11     
  Partials       56       56
```


| [Impacted Files](https://codecov.io/gh/pyca/pyopenssl/pull/733?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/OpenSSL/SSL.py](https://codecov.io/gh/pyca/pyopenssl/pull/733/diff?src=pr&el=tree#diff-c3JjL09wZW5TU0wvU1NMLnB5) | `94.54% <33.33%> (-0.41%)` | :arrow_down: |
| [tests/test\_ssl.py](https://codecov.io/gh/pyca/pyopenssl/pull/733/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9zc2wucHk=) | `98.75% <46.15%> (-0.38%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pyca/pyopenssl/pull/733?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pyca/pyopenssl/pull/733?src=pr&el=footer). Last update [3d231f0...ebf80ce](https://codecov.io/gh/pyca/pyopenssl/pull/733?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

jlaine: As far as I can tell, this pull request is complete : tests, docstring and an entry in api/ssl.rst. All that's needed is for cryptography 2.2 to be released.
reaperhulk: I will be circling around to figure out a rough release date for 2.2 next week.
reaperhulk: Okay, this can be updated to set cryptography 2.2 as minimum version
jlaine: OK, so this requires #742 

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ab28dcb..1a659a0 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -24,8 +24,12 @@ Deprecations:
 Changes:
 ^^^^^^^^
 
+- Added ``Connection.get_certificate`` to retrieve the local certificate.
+  `#733 <https://github.com/pyca/pyopenssl/pull/733>`_
 - ``OpenSSL.SSL.Connection`` now sets ``SSL_MODE_AUTO_RETRY`` by default.
   `#753 <https://github.com/pyca/pyopenssl/pull/753>`_
+- Added ``Context.set_tlsext_use_srtp`` to enable negotiation of SRTP keying material.
+  `#734 <https://github.com/pyca/pyopenssl/pull/734>`_
 
 
 ----
diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py
index 1bf6450..e3eddae 100644
--- a/src/OpenSSL/SSL.py
+++ b/src/OpenSSL/SSL.py
@@ -1371,6 +1371,21 @@ class Context(object):
         _lib.SSL_CTX_set_tlsext_servername_callback(
             self._context, self._tlsext_servername_callback)
 
+    def set_tlsext_use_srtp(self, profiles):
+        """
+        Enable support for negotiating SRTP keying material.
+
+        :param bytes profiles: A colon delimited list of protection profile
+            names, like ``b'SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32'``.
+        :return: None
+        """
+        if not isinstance(profiles, bytes):
+            raise TypeError("profiles must be a byte string.")
+
+        _openssl_assert(
+            _lib.SSL_CTX_set_tlsext_use_srtp(self._context, profiles) == 0
+        )
+
     @_requires_npn
     def set_npn_advertise_callback(self, callback):
         """
@@ -2024,7 +2039,7 @@ class Connection(object):
             result.append(pyname)
         return result
 
-    def makefile(self):
+    def makefile(self, *args, **kwargs):
         """
         The makefile() method is not implemented, since there is no dup
         semantics for SSL connections
@@ -2161,6 +2176,18 @@ class Connection(object):
         """
         return self._socket.shutdown(*args, **kwargs)
 
+    def get_certificate(self):
+        """
+        Retrieve the local certificate (if any)
+
+        :return: The local certificate
+        """
+        cert = _lib.SSL_get_certificate(self._ssl)
+        if cert != _ffi.NULL:
+            _lib.X509_up_ref(cert)
+            return X509._from_raw_x509_ptr(cert)
+        return None
+
     def get_peer_certificate(self):
         """
         Retrieve the other side's certificate (if any)


Test patch:
diff --git a/tests/test_ssl.py b/tests/test_ssl.py
index 87bd18c..0831904 100644
--- a/tests/test_ssl.py
+++ b/tests/test_ssl.py
@@ -1593,6 +1593,35 @@ class TestContext(object):
         store = context.get_cert_store()
         assert isinstance(store, X509Store)
 
+    def test_set_tlsext_use_srtp_not_bytes(self):
+        """
+        `Context.set_tlsext_use_srtp' enables negotiating SRTP keying material.
+
+        It raises a TypeError if the list of profiles is not a byte string.
+        """
+        context = Context(TLSv1_METHOD)
+        with pytest.raises(TypeError):
+            context.set_tlsext_use_srtp(text_type('SRTP_AES128_CM_SHA1_80'))
+
+    def test_set_tlsext_use_srtp_invalid_profile(self):
+        """
+        `Context.set_tlsext_use_srtp' enables negotiating SRTP keying material.
+
+        It raises an Error if the call to OpenSSL fails.
+        """
+        context = Context(TLSv1_METHOD)
+        with pytest.raises(Error):
+            context.set_tlsext_use_srtp(b'SRTP_BOGUS')
+
+    def test_set_tlsext_use_srtp_valid(self):
+        """
+        `Context.set_tlsext_use_srtp' enables negotiating SRTP keying material.
+
+        It does not return anything.
+        """
+        context = Context(TLSv1_METHOD)
+        assert context.set_tlsext_use_srtp(b'SRTP_AES128_CM_SHA1_80') is None
+
 
 class TestServerNameCallback(object):
     """
@@ -2389,6 +2418,31 @@ class TestConnection(object):
         with pytest.raises(NotImplementedError):
             conn.makefile()
 
+    def test_get_certificate(self):
+        """
+        `Connection.get_certificate` returns the local certificate.
+        """
+        chain = _create_certificate_chain()
+        [(cakey, cacert), (ikey, icert), (skey, scert)] = chain
+
+        context = Context(TLSv1_METHOD)
+        context.use_certificate(scert)
+        client = Connection(context, None)
+        cert = client.get_certificate()
+        assert cert is not None
+        assert "Server Certificate" == cert.get_subject().CN
+
+    def test_get_certificate_none(self):
+        """
+        `Connection.get_certificate` returns the local certificate.
+
+        If there is no certificate, it returns None.
+        """
+        context = Context(TLSv1_METHOD)
+        client = Connection(context, None)
+        cert = client.get_certificate()
+        assert cert is None
+
     def test_get_peer_cert_chain(self):
         """
         `Connection.get_peer_cert_chain` returns a list of certificates