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',