[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