[lxc-devel] [pylxd/master] Client certificates

rockstar on Github lxc-bot at linuxcontainers.org
Thu Jun 9 00:26:12 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 712 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160609/51ef4171/attachment.bin>
-------------- next part --------------
From d91c7cbead0f3579bbdbabc218de027aaccd071d Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Wed, 8 Jun 2016 17:46:22 -0600
Subject: [PATCH 1/5] Add support for certificate authentication

---
 pylxd/certificate.py            | 79 ++++++++++++++++++++++++++++++++++++++
 pylxd/client.py                 | 29 +++++++++++---
 pylxd/managers.py               |  4 ++
 pylxd/tests/lxd.crt             | 19 ++++++++++
 pylxd/tests/lxd.key             | 28 ++++++++++++++
 pylxd/tests/mock_lxd.py         | 50 ++++++++++++++++++++++++
 pylxd/tests/test_certificate.py | 62 ++++++++++++++++++++++++++++++
 pylxd/tests/test_client.py      | 84 +++++++++++++++++++++++++++++++++++++----
 requirements.txt                |  1 +
 9 files changed, 343 insertions(+), 13 deletions(-)
 create mode 100644 pylxd/certificate.py
 create mode 100644 pylxd/tests/lxd.crt
 create mode 100644 pylxd/tests/lxd.key
 create mode 100644 pylxd/tests/test_certificate.py

diff --git a/pylxd/certificate.py b/pylxd/certificate.py
new file mode 100644
index 0000000..c7c2400
--- /dev/null
+++ b/pylxd/certificate.py
@@ -0,0 +1,79 @@
+# Copyright (c) 2016 Canonical Ltd
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+import binascii
+
+from cryptography import x509
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.serialization import Encoding
+import six
+
+
+class Certificate(object):
+    """A LXD certificate."""
+
+    __slots__ = [
+        '_client',
+        'certificate', 'fingerprint', 'type',
+    ]
+
+    @classmethod
+    def get(cls, client, fingerprint):
+        """Get a certificate by fingerprint."""
+        response = client.api.certificates[fingerprint].get()
+
+        return cls(_client=client, **response.json()['metadata'])
+
+    @classmethod
+    def all(cls, client):
+        """Get all certificates."""
+        response = client.api.certificates.get()
+
+        certs = []
+        for cert in response.json()['metadata']:
+            fingerprint = cert.split('/')[-1]
+            certs.append(cls(_client=client, fingerprint=fingerprint))
+        return certs
+
+    @classmethod
+    def create(cls, client, password, cert_data):
+        """Create a new certificate."""
+        cert = x509.load_pem_x509_certificate(cert_data, default_backend())
+        data = {
+            'type': 'client',
+            'certificate': cert.public_bytes(Encoding.PEM).decode('utf-8'),
+            'password': password,
+        }
+        client.api.certificates.post(json=data)
+
+        # XXX: rockstar (08 Jun 2016) - Please see the open lxd bug here:
+        # https://github.com/lxc/lxd/issues/2092
+        fingerprint = binascii.hexlify(cert.fingerprint(hashes.SHA256())).decode('utf-8')
+        return cls.get(client, fingerprint)
+
+    def __init__(self, **kwargs):
+        super(Certificate, self).__init__()
+        for key, value in six.iteritems(kwargs):
+            setattr(self, key, value)
+
+    def delete(self):
+        """Delete the certificate."""
+        self._client.api.certificates[self.fingerprint].delete()
+
+    def fetch(self):
+        """Fetch an updated representation of the certificate."""
+        response = self._client.api.certificates[self.fingerprint].get()
+
+        for key, value in six.iteritems(response.json()['metadata']):
+            setattr(self, key, value)
diff --git a/pylxd/client.py b/pylxd/client.py
index b9f214d..a9df1d3 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -115,6 +115,7 @@ class _WebsocketClient(WebSocketBaseClient):
     all json messages to a messages attribute, which can
     then be read are parsed.
     """
+
     def handshake_ok(self):
         self.messages = []
 
@@ -124,8 +125,7 @@ def received_message(self, message):
 
 
 class Client(object):
-    """
-    Client class for LXD REST API.
+    """Client class for LXD REST API.
 
     This client wraps all the functionality required to interact with
     LXD, and is meant to be the sole entry point.
@@ -163,9 +163,11 @@ class Client(object):
             >>> print response.status_code, response.json()
             # /containers/test/
             >>> print api.containers['test'].get().json()
+
     """
 
     def __init__(self, endpoint=None, version='1.0', cert=None, verify=True):
+        self.cert = cert
         if endpoint is not None:
             self.api = _APINode(endpoint, cert=cert, verify=verify)
         else:
@@ -183,21 +185,36 @@ def __init__(self, endpoint=None, version='1.0', cert=None, verify=True):
             response = self.api.get()
             if response.status_code != 200:
                 raise exceptions.ClientConnectionFailed()
-            auth = response.json()['metadata']['auth']
-            if auth != "trusted":
-                raise exceptions.ClientAuthenticationFailed()
-
             self.host_info = response.json()['metadata']
+
         except (requests.exceptions.ConnectionError,
                 requests.exceptions.InvalidURL):
             raise exceptions.ClientConnectionFailed()
 
+        self.certificates = managers.CertificateManager(self)
         self.containers = managers.ContainerManager(self)
         self.images = managers.ImageManager(self)
         self.operations = managers.OperationManager(self)
         self.profiles = managers.ProfileManager(self)
 
     @property
+    def authenticated(self):
+        return self.host_info['auth'] == 'trusted'
+
+    def authenticate(self, password):
+        if self.authenticated:
+            return
+        # This is naive. There might be a library that can parse this
+        # better, but we basically just want to trim off BEGIN/END
+        # certificate lines.
+        cert = open(self.cert[0]).read().encode('utf-8')
+        self.certificates.create(password, cert)
+
+        # Refresh the host info
+        response = self.api.get()
+        self.host_info = response.json()['metadata']
+
+    @property
     def websocket_url(self):
         parsed = parse.urlparse(self.api._api_endpoint)
         if parsed.scheme in ('http', 'https'):
diff --git a/pylxd/managers.py b/pylxd/managers.py
index ef82569..ddb6214 100644
--- a/pylxd/managers.py
+++ b/pylxd/managers.py
@@ -25,6 +25,10 @@ def __init__(self, *args, **kwargs):
         return super(BaseManager, self).__init__()
 
 
+class CertificateManager(BaseManager):
+    manager_for = 'pylxd.certificate.Certificate'
+
+
 class ContainerManager(BaseManager):
     manager_for = 'pylxd.container.Container'
 
diff --git a/pylxd/tests/lxd.crt b/pylxd/tests/lxd.crt
new file mode 100644
index 0000000..838e56d
--- /dev/null
+++ b/pylxd/tests/lxd.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDBjCCAe4CCQCo5Wv+umHW/TANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
+VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
+cyBQdHkgTHRkMB4XDTE2MDYwNzIzMjQxNVoXDTE5MDMwNDIzMjQxNVowRTELMAkG
+A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
+IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+ALcAkbyCEn+AACxtADutfB4UeOTPtdCFb8EZgytc2KPC2Ch8SObINoj3J44lC0eQ
+NGb/qpyBBBkATRPxJqO6xTHxrWXZmTq+IEgFsVQ1K788Us+YEbHQVcKSYcdWouBk
+C8bo34yJ3HyeO32VRzTunjMK3/gCN1pEB53OKiA3Usfs1QTxe8CMT5dVfAGnSdMc
+QZupKEvu3xEdGRBOPPJ466J1SSnrD+p3ZCjsVqfXe7WF2ilMoU4arBXPAZOVkH+V
+6Rg4qvgKxZXU38EuYBcULWegSsfmr7cBjspmdPJ/SHKbHOYkXZjRFHfUQG7SdvVD
+aQx51agTCPS7nOQ6eAdW8ZMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYf0ByG0g
+JmpsphNk2MEB0gtxy1DesLwOqKAAzUgzmeU0FtEPFNiRqpvjR7W2ZyPN9dvovrY1
+kLrXaRTeLUC5tl3uoAXaMkb3q8ZTrEZ+f70L+5v5LpJ2l6i/v06BIT8tH511IihP
+vX/q0YdD1Pyab/dJcHpLMWwTY3kOHEoA6xUzRSN9CT4LdNg/EIjpt7LxrbWqOxLD
+WsfSbG6NKRoSzHtTHLNcD9+E0xc0/OHR8JAw3I7J39VrAphvqc8dwOTuU4nlq2RF
+xduGTfVb09Zo/yEaocQYsI/yIEyTAfO8mMaexq/ZNjYIzs8JZQds4Zx9HRu3UrcQ
+DVvz7DMNIzu3yQ==
+-----END CERTIFICATE-----
diff --git a/pylxd/tests/lxd.key b/pylxd/tests/lxd.key
new file mode 100644
index 0000000..6101c51
--- /dev/null
+++ b/pylxd/tests/lxd.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3AJG8ghJ/gAAs
+bQA7rXweFHjkz7XQhW/BGYMrXNijwtgofEjmyDaI9yeOJQtHkDRm/6qcgQQZAE0T
+8SajusUx8a1l2Zk6viBIBbFUNSu/PFLPmBGx0FXCkmHHVqLgZAvG6N+Midx8njt9
+lUc07p4zCt/4AjdaRAedziogN1LH7NUE8XvAjE+XVXwBp0nTHEGbqShL7t8RHRkQ
+TjzyeOuidUkp6w/qd2Qo7Fan13u1hdopTKFOGqwVzwGTlZB/lekYOKr4CsWV1N/B
+LmAXFC1noErH5q+3AY7KZnTyf0hymxzmJF2Y0RR31EBu0nb1Q2kMedWoEwj0u5zk
+OngHVvGTAgMBAAECggEBAKBUxVpM83v1XzGNBilC431PHmQJfxeD8NdTTNKO89b1
+/H/r88sOGomBUIx+9BTsyJx83rNjbX2h/+W638mO9vm87dhP/qmyrYGsSyKluwA/
+D6aFautIxfpEWZpV0zmZLaBFoqX0mtIrp59tTAeaD8xUeMlG18wj0jB10f6LueEh
+povKfcO0kFTDqDO5rTNdP9H4P9wUfHDaBzHR90aq6nP2vWvQ/XkKSF9IDYSFzYvD
+rhEDkQhV2JllEnSOfIGPP+/J25Sn9Dq9S6fq/cs4/oi98/0seZZ8A8vKOHpk8aqS
+IFurWs0x4XhYmlLlW/3TpJrN2HT02g9JX2W4tDDt2MECgYEA70He73UIMgSVoIUn
+7K0p9E62qO8DODvDPPa+hXNbcyiF8f83vANW/UqNLvisAo8GjnhvKP4Ohr+YjZAo
+Nl9m6HD0TRoK8jsyuQgvu1v4lZH651SHaesCKm1CLo2tm0Ho/EEtf4ccqSOeN4fe
+Zd0Snz95E4NV9Y4ID7VvBRSw1EMCgYEAw87tcjPpQLEPADx9ylL5xOictQrPhmMc
+xxU/0KPMj6OQOZPW+zj1paof7fGWTuvMtuE7oAMLb+5v+AqFXGiFGVuiKL72CiV0
+QyUTV12Qc8r/NPFyznktffTo+a11xKBdeUNZupXxGM82dbXdybnXO3bxPvhMi/xr
+4cq8Y3OdwHECgYAXN/VCl8Dr2bYLleCB/2wK4XiofEl7s5EG4YsruD4vtscI7ROj
+k09l1U5OOKO4u9iPCvD+sWkHeqB7XHoKjMeX1x5ePSDC0Svi+QBo1kwRd9E5keJy
+TPQw2dmKWwV2A7dwg4K+1YXahDJegTj7+bBM9APz+NLmuZnerGTRwWhHsQKBgE/V
+8ghrVAJVbtlY0K0Ksd3wPdyvILgZdyVQ66kE8CXsuaRQPApIShgWylf49aEOEXTL
+VsVCGIq1vB91IrTvxLz3GKHmYmj2pnWuCznG41vi+7U5cObwj3TYw5jxeaAHBrWn
+mVEzS48jBYBu+5QBWtlbALf9AzDcZZw1TiR6gmpxAoGBAOYsPhNd0KpfQkuZWTtF
+P59lKCIBRYo3uJ2F3vIKyic+LfDzDD1bD63+jCcL0KcqzaIlLctXorsNikXJB97C
+XJjSbr0HMmACRo1Rf+fqLly/y+lCF1azOYM0g4x6O+0F0Xd8/NwYeY5p5WuD41of
+/jgzLEJI/GauStHGpqCQo6FI
+-----END PRIVATE KEY-----
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 0e3a9b3..9c39cf8 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -75,6 +75,56 @@ def profile_GET(request, context):
     },
 
 
+    # Certificates
+    {
+        'text': json.dumps({
+            'type': 'sync',
+            'metadata': [
+                'http://pylxd.test/1.0/certificates/an-certificate',
+            ]}),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/certificates$',
+    },
+    {
+        'method': 'POST',
+        'url': r'^http://pylxd.test/1.0/certificates$',
+    },
+    {
+        'text': json.dumps({
+            'type': 'sync',
+            'metadata': {
+                'certificate': 'certificate-content',
+                'fingerprint': 'eaf55b72fc23aa516d709271df9b0116064bf8cfa009cf34c67c33ad32c2320c',
+                'type': 'client',
+            }}),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/certificates/eaf55b72fc23aa516d709271df9b0116064bf8cfa009cf34c67c33ad32c2320c$',  # NOQA
+    },
+    {
+        'text': json.dumps({
+            'type': 'sync',
+            'metadata': {
+                'certificate': 'certificate-content',
+                'fingerprint': 'an-certificate',
+                'type': 'client',
+            }}),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/certificates/an-certificate$',
+    },
+
+
+
+
+
+
+
+    {
+        'status_code': 202,
+        'method': 'DELETE',
+        'url': r'^http://pylxd.test/1.0/certificates/an-certificate$',
+    },
+
+
     # Containers
     {
         'text': json.dumps({
diff --git a/pylxd/tests/test_certificate.py b/pylxd/tests/test_certificate.py
new file mode 100644
index 0000000..467bb0b
--- /dev/null
+++ b/pylxd/tests/test_certificate.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2016 Canonical Ltd
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+import os
+
+from pylxd import certificate
+from pylxd.tests import testing
+
+
+class TestCertificate(testing.PyLXDTestCase):
+    """Tests for pylxd.certificate.Certificate."""
+
+    def test_get(self):
+        """A certificate is retrieved."""
+        cert = self.client.certificates.get('an-certificate')
+
+        self.assertEqual('certificate-content', cert.certificate)
+
+    def test_all(self):
+        """A certificates are returned."""
+        certs = self.client.certificates.all()
+
+        self.assertIn('an-certificate', [c.fingerprint for c in certs])
+
+    def test_create(self):
+        """A certificate is created."""
+        cert_data = open(
+            os.path.join(os.path.dirname(__file__), 'lxd.crt')).read().encode('utf-8')
+        an_certificate = self.client.certificates.create(
+            'test-password', cert_data)
+
+        self.assertEqual(
+            'eaf55b72fc23aa516d709271df9b0116064bf8cfa009cf34c67c33ad32c2320c',
+            an_certificate.fingerprint)
+
+    def test_fetch(self):
+        """A partial object is fully fetched."""
+        an_certificate = certificate.Certificate(
+            _client=self.client, fingerprint='an-certificate')
+
+        an_certificate.fetch()
+
+        self.assertEqual('certificate-content', an_certificate.certificate)
+
+    def test_delete(self):
+        """A certificate is deleted."""
+        # XXX: rockstar (08 Jun 2016) - This just executes a code path. An
+        # assertion should be added.
+        an_certificate = certificate.Certificate(
+            _client=self.client, fingerprint='an-certificate')
+
+        an_certificate.delete()
diff --git a/pylxd/tests/test_client.py b/pylxd/tests/test_client.py
index f4e4294..43eb041 100644
--- a/pylxd/tests/test_client.py
+++ b/pylxd/tests/test_client.py
@@ -1,3 +1,16 @@
+# Copyright (c) 2016 Canonical Ltd
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
 import json
 import os
 import unittest
@@ -13,8 +26,11 @@ class TestClient(unittest.TestCase):
     """Tests for pylxd.client.Client."""
 
     def setUp(self):
-        self.patcher = mock.patch('pylxd.client._APINode.get')
-        self.get = self.patcher.start()
+        self.get_patcher = mock.patch('pylxd.client._APINode.get')
+        self.get = self.get_patcher.start()
+
+        self.post_patcher = mock.patch('pylxd.client._APINode.post')
+        self.post = self.post_patcher.start()
 
         response = mock.MagicMock(status_code=200)
         response.json.return_value = {'metadata': {
@@ -23,8 +39,12 @@ def setUp(self):
         }}
         self.get.return_value = response
 
+        post_response = mock.MagicMock(status_code=200)
+        self.post.return_value = post_response
+
     def tearDown(self):
-        self.patcher.stop()
+        self.get_patcher.stop()
+        self.post_patcher.stop()
 
     def test_create(self):
         """Client creation sets default API endpoint."""
@@ -68,16 +88,66 @@ def raise_exception():
 
         self.assertRaises(exceptions.ClientConnectionFailed, client.Client)
 
-    def test_authentication_failed(self):
-        """If the authentication fails, an exception is raised."""
+    def test_connection_untrusted(self):
+        """Client.authenticated is False when certs are untrusted."""
         response = mock.MagicMock(status_code=200)
         response.json.return_value = {'metadata': {'auth': 'untrusted'}}
         self.get.return_value = response
 
-        self.assertRaises(exceptions.ClientAuthenticationFailed, client.Client)
+        an_client = client.Client()
+
+        self.assertFalse(an_client.authenticated)
+
+    def test_connection_trusted(self):
+        """Client.authenticated is True when certs are untrusted."""
+        response = mock.MagicMock(status_code=200)
+        response.json.return_value = {'metadata': {'auth': 'trusted'}}
+        self.get.return_value = response
+
+        an_client = client.Client()
+
+        self.assertTrue(an_client.authenticated)
+
+    def test_authenticate(self):
+        """A client is authenticated."""
+        response = mock.MagicMock(status_code=200)
+        response.json.return_value = {'metadata': {'auth': 'untrusted'}}
+        self.get.return_value = response
+
+        certs = (
+            os.path.join(os.path.dirname(__file__), 'lxd.crt'),
+            os.path.join(os.path.dirname(__file__), 'lxd.key'))
+        an_client = client.Client(cert=certs)
+
+        get_count = []
+
+        def _get(*args, **kwargs):
+            if len(get_count) == 0:
+                get_count.append(None)
+                return {'metadata': {
+                    'type': 'client',
+                    'fingerprint': 'eaf55b72fc23aa516d709271df9b0116064bf8cfa009cf34c67c33ad32c2320c',  # NOQA
+                }}
+            else:
+                return {'metadata': {'auth': 'trusted'}}
+        response = mock.MagicMock(status_code=200)
+        response.json.side_effect = _get
+        self.get.return_value = response
+
+        an_client.authenticate('test-password')
+
+        self.assertTrue(an_client.authenticated)
+
+    def test_authenticate_already_authenticated(self):
+        """If the client is already authenticated, nothing happens."""
+        an_client = client.Client()
+
+        an_client.authenticate('test-password')
+
+        self.assertTrue(an_client.authenticated)
 
     def test_host_info(self):
-        """Perform a host query """
+        """Perform a host query."""
         an_client = client.Client()
         self.assertEqual('zfs', an_client.host_info['environment']['storage'])
 
diff --git a/requirements.txt b/requirements.txt
index 60bd558..2c17149 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,4 @@ six>=1.9.0
 ws4py>=0.3.4
 requests!=2.8.0,>=2.5.2
 requests-unixsocket==0.1.4
+cryptography>=1.4

From 9ae4d43c415023f5ac0cd3227aadcdc1d33cbf58 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Wed, 8 Jun 2016 17:46:48 -0600
Subject: [PATCH 2/5] Whitespace...

---
 pylxd/tests/mock_lxd.py | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 9c39cf8..2c593cb 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -111,13 +111,6 @@ def profile_GET(request, context):
         'method': 'GET',
         'url': r'^http://pylxd.test/1.0/certificates/an-certificate$',
     },
-
-
-
-
-
-
-
     {
         'status_code': 202,
         'method': 'DELETE',

From 129ea843c63972f510593dbaa387e50ba79e00cc Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Wed, 8 Jun 2016 17:55:08 -0600
Subject: [PATCH 3/5] Fix lint

---
 pylxd/certificate.py            | 3 ++-
 pylxd/tests/mock_lxd.py         | 2 +-
 pylxd/tests/test_certificate.py | 4 ++--
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/pylxd/certificate.py b/pylxd/certificate.py
index c7c2400..d116b63 100644
--- a/pylxd/certificate.py
+++ b/pylxd/certificate.py
@@ -59,7 +59,8 @@ def create(cls, client, password, cert_data):
 
         # XXX: rockstar (08 Jun 2016) - Please see the open lxd bug here:
         # https://github.com/lxc/lxd/issues/2092
-        fingerprint = binascii.hexlify(cert.fingerprint(hashes.SHA256())).decode('utf-8')
+        fingerprint = binascii.hexlify(
+            cert.fingerprint(hashes.SHA256())).decode('utf-8')
         return cls.get(client, fingerprint)
 
     def __init__(self, **kwargs):
diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
index 2c593cb..15ade82 100644
--- a/pylxd/tests/mock_lxd.py
+++ b/pylxd/tests/mock_lxd.py
@@ -94,7 +94,7 @@ def profile_GET(request, context):
             'type': 'sync',
             'metadata': {
                 'certificate': 'certificate-content',
-                'fingerprint': 'eaf55b72fc23aa516d709271df9b0116064bf8cfa009cf34c67c33ad32c2320c',
+                'fingerprint': 'eaf55b72fc23aa516d709271df9b0116064bf8cfa009cf34c67c33ad32c2320c',  # NOQA
                 'type': 'client',
             }}),
         'method': 'GET',
diff --git a/pylxd/tests/test_certificate.py b/pylxd/tests/test_certificate.py
index 467bb0b..34a85eb 100644
--- a/pylxd/tests/test_certificate.py
+++ b/pylxd/tests/test_certificate.py
@@ -34,8 +34,8 @@ def test_all(self):
 
     def test_create(self):
         """A certificate is created."""
-        cert_data = open(
-            os.path.join(os.path.dirname(__file__), 'lxd.crt')).read().encode('utf-8')
+        cert_data = open(os.path.join(
+            os.path.dirname(__file__), 'lxd.crt')).read().encode('utf-8')
         an_certificate = self.client.certificates.create(
             'test-password', cert_data)
 

From 3f8071c8f563bce942ae13e06b4cf0128b2018a9 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Wed, 8 Jun 2016 18:14:57 -0600
Subject: [PATCH 4/5] Move Client.authenticated to Client.trusted

---
 pylxd/client.py            |  4 ++--
 pylxd/tests/test_client.py | 12 ++++++------
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/pylxd/client.py b/pylxd/client.py
index a9df1d3..e63bd1c 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -198,11 +198,11 @@ def __init__(self, endpoint=None, version='1.0', cert=None, verify=True):
         self.profiles = managers.ProfileManager(self)
 
     @property
-    def authenticated(self):
+    def trusted(self):
         return self.host_info['auth'] == 'trusted'
 
     def authenticate(self, password):
-        if self.authenticated:
+        if self.trusted:
             return
         # This is naive. There might be a library that can parse this
         # better, but we basically just want to trim off BEGIN/END
diff --git a/pylxd/tests/test_client.py b/pylxd/tests/test_client.py
index 43eb041..3dbc65e 100644
--- a/pylxd/tests/test_client.py
+++ b/pylxd/tests/test_client.py
@@ -89,24 +89,24 @@ def raise_exception():
         self.assertRaises(exceptions.ClientConnectionFailed, client.Client)
 
     def test_connection_untrusted(self):
-        """Client.authenticated is False when certs are untrusted."""
+        """Client.trusted is False when certs are untrusted."""
         response = mock.MagicMock(status_code=200)
         response.json.return_value = {'metadata': {'auth': 'untrusted'}}
         self.get.return_value = response
 
         an_client = client.Client()
 
-        self.assertFalse(an_client.authenticated)
+        self.assertFalse(an_client.trusted)
 
     def test_connection_trusted(self):
-        """Client.authenticated is True when certs are untrusted."""
+        """Client.trusted is True when certs are untrusted."""
         response = mock.MagicMock(status_code=200)
         response.json.return_value = {'metadata': {'auth': 'trusted'}}
         self.get.return_value = response
 
         an_client = client.Client()
 
-        self.assertTrue(an_client.authenticated)
+        self.assertTrue(an_client.trusted)
 
     def test_authenticate(self):
         """A client is authenticated."""
@@ -136,7 +136,7 @@ def _get(*args, **kwargs):
 
         an_client.authenticate('test-password')
 
-        self.assertTrue(an_client.authenticated)
+        self.assertTrue(an_client.trusted)
 
     def test_authenticate_already_authenticated(self):
         """If the client is already authenticated, nothing happens."""
@@ -144,7 +144,7 @@ def test_authenticate_already_authenticated(self):
 
         an_client.authenticate('test-password')
 
-        self.assertTrue(an_client.authenticated)
+        self.assertTrue(an_client.trusted)
 
     def test_host_info(self):
         """Perform a host query."""

From ede7220cf682fcf1816997e08935b4f1e4e02a46 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Wed, 8 Jun 2016 18:22:30 -0600
Subject: [PATCH 5/5] Add docs for how authentication works with LXD

---
 doc/source/authentication.rst | 44 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 44 insertions(+)
 create mode 100644 doc/source/authentication.rst

diff --git a/doc/source/authentication.rst b/doc/source/authentication.rst
new file mode 100644
index 0000000..cd12e7c
--- /dev/null
+++ b/doc/source/authentication.rst
@@ -0,0 +1,44 @@
+==============
+Authentication
+==============
+
+When using LXD over https, LXD uses an asymmetric keypair for authentication.
+The keypairs are added to the authentication database after entering the LXD
+instance's "trust password".
+
+
+Generate a certificate
+======================
+
+To generate a keypair, you should use the `openssl` command. As an example::
+
+    openssl req -newkey rsa:2048 -nodes -keyout lxd.key -out lxd.csr
+    openssl x509 -signkey lxd.key -in lxd.csr -req -days 365 -out lxd.crt
+
+For more detail on the commands, or to customize the keys, please see the
+documentation for the `openssl` command.
+
+
+Authenticate a new keypair
+==========================
+
+If a client is created using this keypair, it would originally be "untrusted",
+essentially meaning that the authentication has not yet occurred.
+
+.. code-block:: python
+
+    >>> from pylxd import Client
+    >>> client = Client(
+    ...     endpoint='http://10.0.0.1:8443',
+    ...     cert=('lxd.crt', 'lxd.key'))
+    >>> client.trusted
+    False
+
+In order to authenticate the client, pass the lxd instance's trust
+password to `Client.authenticate`
+
+.. code-block:: python
+
+    >>> client.authenticate('a-secret-trust-password')
+    >>> client.trusted
+    >>> True


More information about the lxc-devel mailing list