diff --git a/README.rst b/README.rst index ef3ded7..11d01ad 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,16 @@ Python CFSSL Library ==================== -This library allows you to interact with a remote CFSSL using Python. +This library allows you to interact with a remote CFSSL server using Python. + +CFSSL is CloudFlare's open source toolkit for everything TLS/SSL. CFSSL is used by +CloudFlare for their internal Certificate Authority infrastructure and for all of +their TLS certificates. + +* `Read more on the CloudFlare blog + `_. +* `View the CFSSL source + `_. Installation ------------ @@ -14,13 +23,13 @@ Setup Usage ----- -`API Documentation `_ +`Read The API Documentation `_ Known Issues / Road Map ----------------------- - Installation, setup, usage - in ReadMe -- Add a Certificate Request data structure +- Add type checking in datamodels .. |Build Status| image:: https://api.travis-ci.org/laslabs/Python-CFSSL.svg?branch=master :target: https://travis-ci.org/laslabs/Python-CFSSL diff --git a/cfssl/__init__.py b/cfssl/__init__.py index 1edfa7d..ba8d6a5 100644 --- a/cfssl/__init__.py +++ b/cfssl/__init__.py @@ -2,4 +2,20 @@ # Copyright 2016 LasLabs Inc. # License MIT (https://opensource.org/licenses/MIT). +# API from .cfssl import CFSSL + +# Models +from .models.certificate_request import CertificateRequest + +from .models.config_client import ConfigClient +from .models.config_key import ConfigKey +from .models.config_server import ConfigServer + +from .models.host import Host + +from .models.policy_auth import PolicyAuth +from .models.policy_sign import PolicySign +from .models.policy_use import PolicyUse + +from .models.subject_info import SubjectInfo diff --git a/cfssl/cfssl.py b/cfssl/cfssl.py index 9cbd4bd..afb67f6 100644 --- a/cfssl/cfssl.py +++ b/cfssl/cfssl.py @@ -6,6 +6,8 @@ from .exceptions import CFSSLException, CFSSLRemoteException +from .models.config_key import ConfigKey + class CFSSL(object): """ It provides Python bindings to a remote CFSSL server via HTTP(S). @@ -23,8 +25,7 @@ def auth_sign(self, token, request, datetime=None, remote_address=None): Args: token: (str) The authentication token. - request: (mixed) Signing request document (e.g. as - documented in endpoint_sign.txt, but not JSON encoded). + request: (cfssl.CertificateRequest) Signing request document. datetime: (datetime.datetime) Authentication timestamp. remote_address: (str) An address used in making the request. Returns: @@ -33,7 +34,7 @@ def auth_sign(self, token, request, datetime=None, remote_address=None): """ data = self._clean_mapping({ 'token': token, - 'request': request, + 'request': request.to_api(), 'datetime': datetime, 'remote_address': remote_address, }) @@ -65,11 +66,11 @@ def bundle(self, certificate, private_key=None, If only the ``domain`` parameter is present, the following parameter is valid: - + ip: (str) The IP address of the remote host; this will fetch the certificate from the IP, and verify that it is valid for the domain name. - + Returns: (dict) Object repesenting the bundle, with the following keys: * bundle contains the concatenated list of PEM certificates @@ -162,27 +163,31 @@ def init_ca(self, hosts, names, common_name=None, key=None, ca=None): """ It initializes a new certificate authority. Args: - hosts: (list) Of SANs (subject alternative names) for the - requested CA certificate. - names: (list) the certificate subject for the requested CA - certificate. + hosts: (iter of cfssl.Host) Subject Alternative Name(s) for the + requested CA certificate. + names: (iter of cfssl.SubjectInfo) The Subject Info(s) for the + requested CA certificate. common_name: (str) the common name for the certificate subject in the requested CA certificate. - key: the key algorithm and size for the newly generated private key, - default to ECDSA-256. - ca: the CA configuration of the requested CA, including CA pathlen - and CA default expiry. + key: (cfssl.ConfigKey) Cipher and strength to use for certificate. + ca: (cfssl.ConfigServer) the CA configuration of the requested CA, + including CA pathlen and CA default expiry. Returns: (dict) Mapping with two keys: * private key: (str) a PEM-encoded CA private key. * certificate: (str) a PEM-encoded self-signed CA certificate. """ + key = key or ConfigKey() data = self._clean_mapping({ - 'hosts': hosts, - 'names': names, + 'hosts': [ + host.to_api() for host in hosts + ], + 'names': [ + name.to_api() for name in names + ], 'CN': common_name, - 'key': key, - 'ca': ca, + 'key': key.to_api(), + 'ca': ca and ca.to_api() or None, }) return self.call('init_ca', 'POST', data=data) @@ -190,14 +195,13 @@ def new_key(self, hosts, names, common_name=None, key=None, ca=None): """ It generates and returns a new private key + CSR. Args: - hosts: (list) Of SANs (subject alternative names) for the - requested CA certificate. - names: (list) the certificate subject for the requested CA - certificate. + hosts: (iter of cfssl.Host) Subject Alternative Name(s) for the + requested certificate. + names: (iter of cfssl.SubjectInfo) The Subject Info(s) for the + requested certificate. CN: (str) the common name for the certificate subject in the requestedrequested CA certificate. - key: the key algorithm and size for the newly generated private key, - default to ECDSA-256. + key: (cfssl.ConfigKey) Cipher and strength to use for certificate. ca: the CA configuration of the requested CA, including CA pathlen and CA default expiry. Returns: @@ -208,8 +212,12 @@ def new_key(self, hosts, names, common_name=None, key=None, ca=None): certificate request """ data = self._clean_mapping({ - 'hosts': hosts, - 'names': names, + 'hosts': [ + host.to_api() for host in hosts + ], + 'names': [ + name.to_api() for name in names + ], 'CN': common_name, 'key': key, 'ca': ca, @@ -220,7 +228,8 @@ def new_cert(self, request, label=None, profile=None, bundle=None): """ It generates and returns a new private key and certificate. Args: - request: (dict) Specifying the certificate request. + request: (cfssl.CertificateRequest) CSR to be used for + certificate creation. label: (str) Specifying which signer to be appointed to sign the CSR, useful when interacting with cfssl server that stands in front of a remote multi-root CA signer. @@ -238,7 +247,7 @@ def new_cert(self, request, label=None, profile=None, bundle=None): if the bundle parameter was set). """ data = self._clean_mapping({ - 'request': request, + 'request': request.to_api(), 'label': label, 'profile': profile, 'bundle': bundle, @@ -269,12 +278,12 @@ def scan(self, host, ip=None, timeout=None, family=None, scanner=None): """ It scans servers to determine the quality of their TLS setup. Args: - host: the hostname (optionally including port) to scan. - ip: IP Address to override DNS lookup of host. - timeout: The amount of time allotted for the scan to complete + host: (cfssl.Host) The host to scan. + ip: (str) IP Address to override DNS lookup of host. + timeout: (str) The amount of time allotted for the scan to complete (default: 1 minute). - family: regular expression specifying scan famil(ies) to run. - scanner: regular expression specifying scanner(s) to run. + family: (str) regular expression specifying scan famil(ies) to run. + scanner: (str) regular expression specifying scanner(s) to run. Returns: (dict) Mapping with keys for each scan family. Each of these objects contains keys for each scanner run in that family @@ -290,7 +299,7 @@ def scan(self, host, ip=None, timeout=None, family=None, scanner=None): * output: (dict) Arbitrary data retrieved during the scan. """ data = self._clean_mapping({ - 'host': host, + 'host': host.to_api(), 'ip': ip, 'timeout': timeout, 'family': family, @@ -314,8 +323,8 @@ def sign(self, certificate_request, hosts=None, subject=None, """ It signs and returns a certificate. Args: - certificate_request: (str) the CSR bytes to be signed in PEM. - hosts: (iter) of SAN (subject alternative .names) + certificate_request: (str) the CSR bytes to be signed (in PEM). + hosts: (iter of cfssl.Host) of SAN (subject alternative .names) which overrides the ones in the CSR subject: (str) The certificate subject which overrides the ones in the CSR. @@ -324,19 +333,22 @@ def sign(self, certificate_request, hosts=None, subject=None, label: (str) Specifying which signer to be appointed to sign the CSR, useful when interacting with a remote multi-root CA signer. - profile: (str) Specifying the signing profile for the signer, - useful when interacting with a remote multi-root CA signer. + profile: (cfssl.ConfigServer) Specifying the signing profile for + the signer, useful when interacting with a remote multi-root + CA signer. Returns: (str) A PEM-encoded certificate that has been signed by the server. """ data = self._clean_mapping({ - 'certificate_request': certificate_request, - 'hosts': hosts, + 'certificate_request': certificate_request.to_api(), + 'hosts': [ + host.to_api() for host in hosts + ], 'subject': subject, 'serial_sequence': serial_sequence, 'label': label, - 'profile': profile, + 'profile': profile.to_api(), }) result = self.call('sign', 'POST', data=data) return result['certificate'] diff --git a/cfssl/defaults.py b/cfssl/defaults.py new file mode 100644 index 0000000..dd4db07 --- /dev/null +++ b/cfssl/defaults.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +DEFAULT_ALGORITHM = 'rsa' +DEFAULT_STRENGTH = 4096 +DEFAULT_EXPIRE_MINUTES = 365 * 24 * 60 diff --git a/cfssl/models/__init__.py b/cfssl/models/__init__.py new file mode 100644 index 0000000..da36d8a --- /dev/null +++ b/cfssl/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). diff --git a/cfssl/models/certificate_request.py b/cfssl/models/certificate_request.py new file mode 100644 index 0000000..9ea662c --- /dev/null +++ b/cfssl/models/certificate_request.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +from .host import Host +from .config_key import ConfigKey +from .subject_info import SubjectInfo + + +class CertificateRequest(object): + """ It provides a Certificate Request compatible with CFSSL. """ + + def __init__(self, common_name, names=None, hosts=None, key=None): + self.common_name = common_name + self.names = names or [] + self.hosts = hosts or [] + self.key = key or KeyConfig() + + def to_api(self): + """ It returns an object compatible with the API. """ + return { + 'CN': self.common_name, + 'names': [ + name.to_api() for name in self.names + ], + 'hosts': [ + host.to_api() for host in self.hosts + ], + 'key': self.key.to_api(), + } diff --git a/cfssl/models/config_client.py b/cfssl/models/config_client.py new file mode 100644 index 0000000..a3da5b0 --- /dev/null +++ b/cfssl/models/config_client.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +from .config_mixer import ConfigMixer + + +class ConfigClient(ConfigMixer): + """ It provides a Client Config compatible with CFSSL. """ + + def __init__(self, sign_policy_default, + sign_policies_add, auth_policies, remotes): + super(ConfigClient, self).__init__( + sign_policy_default, auth_policies, remotes, + ) + self.remotes = remotes + + def to_api(self): + """ It returns an object compatible with the API. """ + res = super(ConfigClient, self).to_api() + res['remotes'] = { + r.name: r.to_api() for r in self.remotes + } + return res diff --git a/cfssl/models/config_key.py b/cfssl/models/config_key.py new file mode 100644 index 0000000..8767171 --- /dev/null +++ b/cfssl/models/config_key.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +from ..defaults import DEFAULT_ALGORITHM, DEFAULT_STRENGTH + + +class ConfigKey(object): + """ It provides a Key Config compatible with CFSSL. """ + + def __init__(self, algorithm=DEFAULT_ALGORITHM, + strength=DEFAULT_STRENGTH): + self.algorithm = algorithm + self.strength = strength + + def to_api(self): + """ It returns an object compatible with the API. """ + return { + 'algo': self.algorithm, + 'size': self.strength, + } diff --git a/cfssl/models/config_mixer.py b/cfssl/models/config_mixer.py new file mode 100644 index 0000000..625cfed --- /dev/null +++ b/cfssl/models/config_mixer.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + + +class ConfigMixer(object): + """ It provides a mixer for the Client and Server Configs """ + + def __init__(self, sign_policy_default, sign_policies_add, auth_policies): + self.sign_policy = sign_policy_default + self.sign_policies = sign_policies_add + self.auth_policies = auth_policies + + def to_api(self): + """ It returns an object compatible with the API. """ + return { + 'signing': { + 'default': self.sign_policy.to_api(), + 'profiles': { + p.name: p.to_api() for p in self.sign_policies + }, + }, + 'auth_keys': { + k.name: k.to_api() for k in self.auth_policies + }, + } diff --git a/cfssl/models/config_server.py b/cfssl/models/config_server.py new file mode 100644 index 0000000..618cbdd --- /dev/null +++ b/cfssl/models/config_server.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +from .config_mixer import ConfigMixer + + +class ConfigServer(ConfigMixer): + """ It provides a Server Config compatible with CFSSL. """ diff --git a/cfssl/models/host.py b/cfssl/models/host.py new file mode 100644 index 0000000..1091c7c --- /dev/null +++ b/cfssl/models/host.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + + +class Host(object): + """ It provides a Host compatible with CFSSL. """ + + def __init__(self, name, host, port=None): + self.name = name + self.host = host + self.port = port + + def to_api(self): + """ It returns an object compatible with the API. """ + if self.port: + return '%s:%d' % (self.host, self.port) + return self.host diff --git a/cfssl/models/policy_auth.py b/cfssl/models/policy_auth.py new file mode 100644 index 0000000..6803074 --- /dev/null +++ b/cfssl/models/policy_auth.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + + +class PolicyAuth(object): + """ It provides a Certificate Auth policy compatible with CFSSL """ + + def __init__(self, name, key, key_type='standard'): + self.name = name + self.key = key + self.key_type = key_type + + def to_api(self): + """ It returns an object compatible with the API. """ + return { + 'key': self.key, + 'type': self.key_type, + } diff --git a/cfssl/models/policy_sign.py b/cfssl/models/policy_sign.py new file mode 100644 index 0000000..1e6eab2 --- /dev/null +++ b/cfssl/models/policy_sign.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +from ..defaults import DEFAULT_EXPIRE_MINUTES + + +class PolicySign(object): + """ It provides a Certificate Auth policy compatible with CFSSL """ + + def __init__(self, name, usage_policies, auth_policy, + expire_minutes=DEFAULT_EXPIRE_MINUTES): + self.name = name + self.usage_policies = usage_policies + self.auth_policy = auth_policy + self.expire_minutes = expire_minutes + + def to_api(self): + """ It returns an object compatible with the API. """ + return { + 'auth_key': self.auth_policy.name, + 'expiry': '%dm' % self.expire_minutes, + 'usages': [u.to_api() for u in self.usage_policies], + } diff --git a/cfssl/models/policy_use.py b/cfssl/models/policy_use.py new file mode 100644 index 0000000..5c1a25b --- /dev/null +++ b/cfssl/models/policy_use.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + + +class PolicyUse(object): + """ It provides a Certificate Use policy compatible with CFSSL """ + + def __init__(self, name, code): + self.name = name + self.code = code + + def to_api(self): + """ It returns an object compatible with the API. """ + return self.code diff --git a/cfssl/models/subject_info.py b/cfssl/models/subject_info.py new file mode 100644 index 0000000..aed18ac --- /dev/null +++ b/cfssl/models/subject_info.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + + +class SubjectInfo(object): + """ It provides a SubjectInfo (Name) compatible with CFSSL. """ + + def __init__(self, org_name, org_unit, city, state, country): + self.org_name = org_name + self.org_unit = org_unit + self.city = city + self.state = state + self.country = country + + def to_api(self): + """ It returns an object compatible with the API. """ + return { + 'O': self.org_name, + 'OU': self.org_unit, + 'L': self.city, + 'ST': self.state, + 'C': self.country, + } diff --git a/cfssl/tests/test_certificate_request.py b/cfssl/tests/test_certificate_request.py new file mode 100644 index 0000000..ba63e10 --- /dev/null +++ b/cfssl/tests/test_certificate_request.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +import mock +import unittest + +from ..models.certificate_request import CertificateRequest + + +class TestCertificateRequest(unittest.TestCase): + + def setUp(self): + super(TestCertificateRequest, self).setUp() + self.vals = { + 'common_name': 'common_name', + 'names': [mock.MagicMock()], + 'hosts': [mock.MagicMock()], + 'key': mock.MagicMock(), + } + self.model = CertificateRequest(**self.vals) + + def test_to_api(self): + """ It should return the correctly compatible obj """ + res = self.model.to_api() + expect = { + 'CN': self.vals['common_name'], + 'names': [self.vals['names'][0].to_api()], + 'hosts': [self.vals['hosts'][0].to_api()], + 'key': self.vals['key'].to_api(), + } + self.assertDictEqual(res, expect) + + +if __name__ == '__main__': + unittest.main() diff --git a/cfssl/tests/test_cfssl.py b/cfssl/tests/test_cfssl.py index 9d3606b..566005f 100644 --- a/cfssl/tests/test_cfssl.py +++ b/cfssl/tests/test_cfssl.py @@ -28,9 +28,10 @@ def test_auth_sign(self, call): """ It should call with proper args """ expect = { 'token': 'token', - 'request': 'request', + 'request': mock.MagicMock(), } self.cfssl.auth_sign(**expect) + expect['request'] = expect['request'].to_api() call.assert_called_once_with( 'authsign', 'POST', data=expect ) @@ -62,13 +63,19 @@ def test_info(self, call): def test_init_ca(self, call): """ It should call with proper args """ expect = { - 'hosts': 'hosts', - 'names': 'names', - 'common_name': 'cn' + 'hosts': [mock.MagicMock()], + 'names': [mock.MagicMock()], + 'common_name': 'cn', + 'key': mock.MagicMock(), + 'ca': mock.MagicMock(), } self.cfssl.init_ca(**expect) expect['CN'] = 'cn' del expect['common_name'] + expect['hosts'][0]= expect['hosts'][0].to_api() + expect['names'][0] = expect['names'][0].to_api() + expect['key'] = expect['key'].to_api() + expect['ca'] = expect['ca'].to_api() call.assert_called_once_with( 'init_ca', 'POST', data=expect ) @@ -77,13 +84,15 @@ def test_init_ca(self, call): def test_new_key(self, call): """ It should call with proper args """ expect = { - 'hosts': 'hosts', - 'names': 'names', + 'hosts': [mock.MagicMock()], + 'names': [mock.MagicMock()], 'common_name': 'cn' } self.cfssl.new_key(**expect) expect['CN'] = 'cn' del expect['common_name'] + expect['hosts'][0]= expect['hosts'][0].to_api() + expect['names'][0] = expect['names'][0].to_api() call.assert_called_once_with( 'newkey', 'POST', data=expect ) @@ -92,10 +101,11 @@ def test_new_key(self, call): def test_new_cert(self, call): """ It should call with proper args """ expect = { - 'request': 'request', + 'request': mock.MagicMock(), 'label': 'label', } self.cfssl.new_cert(**expect) + expect['request'] = expect['request'].to_api() call.assert_called_once_with( 'newcert', 'POST', data=expect ) @@ -117,9 +127,10 @@ def test_revoke(self, call): def test_scan(self, call): """ It should call with proper args """ expect = { - 'host': 'host', + 'host': mock.MagicMock(), } self.cfssl.scan(**expect) + expect['host'] = expect['host'].to_api() call.assert_called_once_with( 'scan', params=expect ) @@ -134,9 +145,14 @@ def test_scan_info(self, call): def test_sign(self, call): """ It should call with proper args """ expect = { - 'certificate_request': 'certificate_request', + 'certificate_request': mock.MagicMock(), + 'hosts': [mock.MagicMock()], + 'profile': mock.MagicMock(), } self.cfssl.sign(**expect) + expect['certificate_request'] = expect['certificate_request'].to_api() + expect['hosts'][0] = expect['hosts'][0].to_api() + expect['profile'] = expect['profile'].to_api() call.assert_called_once_with( 'sign', 'POST', data=expect ) diff --git a/cfssl/tests/test_config_client.py b/cfssl/tests/test_config_client.py new file mode 100644 index 0000000..27abfce --- /dev/null +++ b/cfssl/tests/test_config_client.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +import mock + +from ..models.config_client import ConfigClient +from .test_config_mixer import TestConfigMixer + +class TestConfigClient(TestConfigMixer): + + def setUp(self): + super(TestConfigClient, self).setUp() + self.vals['remotes'] = [mock.MagicMock()] + + @property + def model(self): + return ConfigClient(**self.vals) + + def test_to_api(self): + """ It should return the correctly compatible obj """ + res = self.model.to_api() + expect = { + self.vals['remotes'][0].name: self.vals['remotes'][0].to_api() + } + self.assertDictEqual(res['remotes'], expect) + + +if __name__ == '__main__': + unittest.main() diff --git a/cfssl/tests/test_config_key.py b/cfssl/tests/test_config_key.py new file mode 100644 index 0000000..b4258e3 --- /dev/null +++ b/cfssl/tests/test_config_key.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +import unittest + +from ..models.config_key import ConfigKey + + +class TestConfigKey(unittest.TestCase): + + def setUp(self): + super(TestConfigKey, self).setUp() + self.vals = { + 'algorithm': 'rsa', + 'strength': 2048, + } + self.model = ConfigKey(**self.vals) + + def test_to_api(self): + """ It should return the correctly compatible obj """ + res = self.model.to_api() + expect = { + 'algo': self.vals['algorithm'], + 'size': self.vals['strength'], + } + self.assertDictEqual(res, expect) + + +if __name__ == '__main__': + unittest.main() diff --git a/cfssl/tests/test_config_mixer.py b/cfssl/tests/test_config_mixer.py new file mode 100644 index 0000000..1927ef8 --- /dev/null +++ b/cfssl/tests/test_config_mixer.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +import mock +import unittest + +from ..models.config_mixer import ConfigMixer + + +class TestConfigMixer(unittest.TestCase): + + def setUp(self): + super(TestConfigMixer, self).setUp() + self.vals = { + 'sign_policy_default': mock.MagicMock(), + 'sign_policies_add': [mock.MagicMock()], + 'auth_policies': [mock.MagicMock()], + } + + @property + def model(self): + return ConfigMixer(**self.vals) + + def test_to_api(self): + """ It should return the correctly compatible obj """ + res = self.model.to_api() + sign_policy = self.vals['sign_policies_add'][0] + auth_policy = self.vals['auth_policies'][0] + expect = { + 'signing': { + 'default': self.vals['sign_policy_default'].to_api(), + 'profiles': { + sign_policy.name: sign_policy.to_api(), + }, + }, + 'auth_keys': { + auth_policy.name: auth_policy.to_api(), + }, + } + self.assertDictEqual(res, expect) + + +if __name__ == '__main__': + unittest.main() diff --git a/cfssl/tests/test_host.py b/cfssl/tests/test_host.py new file mode 100644 index 0000000..8da0de8 --- /dev/null +++ b/cfssl/tests/test_host.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +import unittest + +from ..models.host import Host + + +class TestHost(unittest.TestCase): + + def setUp(self): + super(TestHost, self).setUp() + self.vals = { + 'name': 'name', + 'host': 'host', + 'port': 443, + } + self.model = Host(**self.vals) + + def test_to_api(self): + """ It should return the correctly compatible obj """ + res = self.model.to_api() + self.assertEqual( + res, + '%s:%s' % (self.vals['host'], self.vals['port']), + ) + + def test_to_api_no_port(self): + """ It should return the correctly compatible obj """ + del self.vals['port'] + model = Host(**self.vals) + res = model.to_api() + self.assertEqual(res, 'host') + + +if __name__ == '__main__': + unittest.main() diff --git a/cfssl/tests/test_policy_auth.py b/cfssl/tests/test_policy_auth.py new file mode 100644 index 0000000..b2b9612 --- /dev/null +++ b/cfssl/tests/test_policy_auth.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +import unittest + +from ..models.policy_auth import PolicyAuth + + +class TestPolicyAuth(unittest.TestCase): + + def setUp(self): + super(TestPolicyAuth, self).setUp() + self.vals = { + 'name': 'name', + 'key': 'key', + 'key_type': 'key_type', + } + self.model = PolicyAuth(**self.vals) + + def test_to_api(self): + """ It should return the correctly compatible obj """ + res = self.model.to_api() + expect = { + 'key': self.vals['key'], + 'type': self.vals['key_type'], + } + self.assertDictEqual(res, expect) + + +if __name__ == '__main__': + unittest.main() diff --git a/cfssl/tests/test_policy_sign.py b/cfssl/tests/test_policy_sign.py new file mode 100644 index 0000000..bc78c85 --- /dev/null +++ b/cfssl/tests/test_policy_sign.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +import mock +import unittest + +from ..models.policy_sign import PolicySign + + +class TestPolicySign(unittest.TestCase): + + def setUp(self): + super(TestPolicySign, self).setUp() + self.vals = { + 'name': 'name', + 'usage_policies': [mock.MagicMock()], + 'auth_policy': mock.MagicMock(), + 'expire_minutes': 1234, + } + self.model = PolicySign(**self.vals) + + def test_to_api(self): + """ It should return the correctly compatible obj """ + res = self.model.to_api() + expect = { + 'auth_key': self.vals['auth_policy'].name, + 'expiry': '1234m', + 'usages': [self.vals['usage_policies'][0].to_api()], + } + self.assertDictEqual(res, expect) + + +if __name__ == '__main__': + unittest.main() diff --git a/cfssl/tests/test_policy_use.py b/cfssl/tests/test_policy_use.py new file mode 100644 index 0000000..a2a4317 --- /dev/null +++ b/cfssl/tests/test_policy_use.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +import unittest + +from ..models.policy_use import PolicyUse + + +class TestPolicyUse(unittest.TestCase): + + def setUp(self): + super(TestPolicyUse, self).setUp() + self.vals = { + 'name': 'name', + 'code': 'code', + } + self.model = PolicyUse(**self.vals) + + def test_to_api(self): + """ It should return the correctly compatible obj """ + res = self.model.to_api() + self.assertEqual(res, self.vals['code']) + + +if __name__ == '__main__': + unittest.main() diff --git a/cfssl/tests/test_subject_info.py b/cfssl/tests/test_subject_info.py new file mode 100644 index 0000000..402c6cb --- /dev/null +++ b/cfssl/tests/test_subject_info.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License MIT (https://opensource.org/licenses/MIT). + +import unittest + +from ..models.subject_info import SubjectInfo + + +class TestSubjectInfo(unittest.TestCase): + + def setUp(self): + super(TestSubjectInfo, self).setUp() + self.vals = { + 'org_name': 'org name', + 'org_unit': 'org unit', + 'city': 'city', + 'state': 'state', + 'country': 'country', + } + self.model = SubjectInfo(**self.vals) + + def test_to_api(self): + """ It should return the correctly compatible obj """ + res = self.model.to_api() + expect = { + 'O': self.vals['org_name'], + 'OU': self.vals['org_unit'], + 'L': self.vals['city'], + 'ST': self.vals['state'], + 'C': self.vals['country'], + } + self.assertDictEqual(res, expect) + + +if __name__ == '__main__': + unittest.main() diff --git a/setup.py b/setup.py index aee20c0..83f2b5d 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ 'author_email': 'support@laslabs.com', 'description': 'This library will allow you to interact with CFSSL ' 'using Python.', - 'url': 'https://github.com/laslabs/Python-CFSSL', + 'url': 'https://laslabs.github.io/python-cfssl', + 'download_url': 'https://github.com/LasLabs/python-cfssl', 'license': 'MIT', 'classifiers': [ 'Development Status :: 4 - Beta',