├── est ├── __init__.py ├── errors.py ├── test │ ├── live.py │ └── server.pem ├── request.py └── client.py ├── pylint_tests.sh ├── contributors ├── pylint_est.sh ├── AUTHORS ├── requirements.txt ├── coverage.sh ├── validate.sh ├── .gitignore ├── setup.py ├── LICENSE └── README.md /est/__init__.py: -------------------------------------------------------------------------------- 1 | """EST Client.""" 2 | -------------------------------------------------------------------------------- /pylint_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pylint -i y tests 4 | -------------------------------------------------------------------------------- /contributors: -------------------------------------------------------------------------------- 1 | Laurent Luce 2 | Raúl Sampedro @rsrdesarrollo 3 | -------------------------------------------------------------------------------- /pylint_est.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pylint -i y est 4 | 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | http://github.com/laurentluce/est-client-python/contributors 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.2.1 2 | pyopenssl>=0.13 3 | asn1crypto>=0.15.1 4 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | coverage run --source=est,tests -m unittest discover 4 | coverage report -m 5 | coverage html 6 | -------------------------------------------------------------------------------- /validate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python -m unittest discover 4 | if [ $? -ne 0 ] 5 | then 6 | echo "Unit tests failed." 7 | exit 1 8 | fi 9 | 10 | bash coverage.sh | grep TOTAL | grep "100%" 11 | if [ $? -ne 0 ] 12 | then 13 | echo "Unit tests coverage less than 100%." 14 | exit 1 15 | fi 16 | 17 | bash pylint_est.sh 18 | if [ $? -ne 0 ] 19 | then 20 | echo "Pylint not 10/10 for est/." 21 | exit 1 22 | fi 23 | 24 | bash pylint_tests.sh 25 | if [ $? -ne 0 ] 26 | then 27 | echo "Pylint not 10/10 for tests/." 28 | exit 1 29 | fi 30 | 31 | echo "OK" 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | EST Client 4 | ========== 5 | """ 6 | import os 7 | from setuptools import setup 8 | 9 | # Utility function to read the README file. 10 | # Used for the long_description. It's nice, because now 1) we have a top level 11 | # README file and 2) it's easier to type in the README file than to put a raw 12 | # string in below ... 13 | def read(fname): 14 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 15 | 16 | setup( 17 | name = "est", 18 | version = "0.2.1", 19 | author = "Laurent Luce", 20 | author_email = "laurentluce49@yahoo.com", 21 | description = ('Client to interact with an EST server - RFC 7030.'), 22 | license = "MIT", 23 | keywords = "Enrollment secure transport", 24 | packages=['est'], 25 | install_requires=[ 26 | 'pyOpenSSL', 27 | 'asn1crypto', 28 | 'requests' 29 | ], 30 | long_description=read('README.md'), 31 | classifiers=[ 32 | "Development Status :: 5 - Production/Stable", 33 | "Topic :: Utilities", 34 | "License :: OSI Approved :: MIT License", 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Laurent Luce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | est-client-python 2 | ================= 3 | 4 | EST client - RFC 7030 - Enrollment over Secure Transport 5 | 6 | ```python 7 | import est.client 8 | 9 | host = 'testrfc7030.cisco.com' 10 | port = 8443 11 | implicit_trust_anchor_cert_path = 'server.pem' 12 | 13 | client = est.client.Client(host, port, implicit_trust_anchor_cert_path) 14 | 15 | # Get CSR attributes from EST server as an OrderedDict. 16 | csr_attrs = client.csrattrs() 17 | 18 | # Get EST server CA certs. 19 | ca_certs = client.cacerts() 20 | 21 | username = 'estuser' 22 | password = 'estpwd' 23 | client.set_basic_auth(username, password) 24 | 25 | # Create CSR and get private key used to sign the CSR. 26 | common_name = 'test' 27 | country = 'US' 28 | state = 'Massachusetts' 29 | city = 'Boston' 30 | organization = 'Cisco Systems' 31 | organizational_unit = 'ENG' 32 | email_address = 'test@cisco.com' 33 | priv, csr = client.create_csr(common_name, country, state, city, 34 | organization, organizational_unit, 35 | email_address) 36 | 37 | # Enroll: get cert signed by the EST server. 38 | client_cert = client.simpleenroll(csr) 39 | 40 | # Re-Enroll: Renew cert. The previous cert/key can be passed for auth if needed. 41 | client_cert = client.simplereenroll(csr) 42 | ``` 43 | 44 | Out of Scope: 45 | 46 | - §3.3.3 - Certificate-less TLS Mutual Authentication. 47 | - §3.5 - Linking Identity and PoP information. 48 | - §4.3 - CMC. 49 | - §4.4 - Server-side key generation. 50 | -------------------------------------------------------------------------------- /est/errors.py: -------------------------------------------------------------------------------- 1 | """Errors definitions.""" 2 | 3 | 4 | class Error(Exception): 5 | """Top error class. All errors should derive this class. 6 | 7 | Attributes: 8 | message (str): Error message. 9 | """ 10 | 11 | def __init__(self, message): 12 | Exception.__init__(self, message) 13 | 14 | 15 | class RequestError(Error): 16 | """Server request error. 17 | 18 | Attributes: 19 | status (int): Error http status code. 20 | message (str): Error message. 21 | """ 22 | 23 | status = None 24 | message = None 25 | 26 | def __init__(self, status, message): 27 | Error.__init__(self, message) 28 | self.status = status 29 | self.message = message 30 | 31 | def __str__(self): 32 | return "RequestError: status=%s, message=%s" % ( 33 | self.status, self.message) 34 | 35 | def __repr__(self): 36 | return "RequestError(status=%r, message=%r)" % ( 37 | self.status, self.message) 38 | 39 | 40 | class TryLater(Error): 41 | """Server try later response 42 | 43 | Attributes: 44 | seconds (int): number of seconds to wait for the next try 45 | message (str): Error message. 46 | """ 47 | 48 | def __init__(self, seconds, message): 49 | Error.__init__(self, message) 50 | self.seconds = seconds 51 | self.message = message 52 | 53 | def __str__(self): 54 | return "TryLater(seconds=%r, message=%r)" % ( 55 | self.seconds, self.message 56 | ) 57 | 58 | def __repr__(self): 59 | return "TryLater(seconds=%r, message=%r)" % ( 60 | self.seconds, self.message 61 | ) -------------------------------------------------------------------------------- /est/test/live.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import OpenSSL.crypto 4 | 5 | import est.client 6 | 7 | class Test(unittest.TestCase): 8 | 9 | def setUp(self): 10 | host = 'testrfc7030.cisco.com' 11 | port = 8443 12 | implicit_trust_anchor_cert_path = 'server.pem' 13 | self.client = est.client.Client(host, port, 14 | implicit_trust_anchor_cert_path) 15 | 16 | def set_auth(self): 17 | username = 'estuser' 18 | password = 'estpwd' 19 | self.client.set_basic_auth(username, password) 20 | 21 | def create_csr(self): 22 | common_name = 'test' 23 | organization = 'Cisco Systems' 24 | email_address = 'test@cisco.com' 25 | key, csr = self.client.create_csr(common_name, 26 | organization=organization, email_address=email_address) 27 | return csr 28 | 29 | def test_cacerts(self): 30 | ca_certs = self.client.cacerts() 31 | lines = ca_certs.split('\n') 32 | self.assertEqual(lines[0], 'subject=/CN=estExampleCA') 33 | 34 | def test_simpleenroll(self): 35 | self.set_auth() 36 | csr = self.create_csr() 37 | client_cert = self.client.simpleenroll(csr) 38 | x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, 39 | client_cert) 40 | self.assertEqual(x509.get_subject().CN, 'test') 41 | self.assertEqual(x509.get_issuer().CN, 'estExampleCA') 42 | 43 | def test_simplereenroll(self): 44 | self.set_auth() 45 | csr = self.create_csr() 46 | client_cert = self.client.simplereenroll(csr) 47 | x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, 48 | client_cert) 49 | self.assertEqual(x509.get_subject().CN, 'test') 50 | self.assertEqual(x509.get_issuer().CN, 'estExampleCA') 51 | 52 | def test_csrattrs(self): 53 | self.client.csrattrs() 54 | 55 | 56 | if __name__ == '__main__': 57 | unittest.main() 58 | 59 | -------------------------------------------------------------------------------- /est/test/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | 8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /est/request.py: -------------------------------------------------------------------------------- 1 | """HTTP requests to server.""" 2 | 3 | import base64 4 | import time 5 | 6 | import requests 7 | import requests.auth 8 | import requests.exceptions 9 | 10 | import est.errors 11 | 12 | def get(url, params=None, headers=None, retries=3, timeout=10, verify=False, 13 | cert=False): 14 | """GET from server. 15 | 16 | Args: 17 | url (str): Request URL. 18 | 19 | Kwargs: 20 | params (dict): Request parameters. 21 | 22 | headers (dict): Request headers. 23 | 24 | Returns: 25 | str: HTTP response. 26 | """ 27 | request_params = params 28 | if request_params is None: 29 | request_params = {} 30 | 31 | 32 | res = send(requests.get, url, params=request_params, headers=headers, 33 | retries=retries, timeout=timeout, verify=verify, cert=cert) 34 | 35 | return res 36 | 37 | def post(url, data, headers=None, auth=None, retries=3, timeout=10, 38 | verify=False, cert=False): 39 | """POST to server. 40 | 41 | Args: 42 | url (str): Request URL. 43 | 44 | data: POST data. 45 | 46 | auth (tuple): Authentication username and password. 47 | 48 | Kwargs: 49 | headers (dict): Request headers. 50 | 51 | Returns: 52 | str: Server response. 53 | """ 54 | return send(requests.post, url, data=data, headers=headers, auth=auth, 55 | retries=retries, timeout=timeout, verify=verify, cert=cert) 56 | 57 | def send(method, url, params=None, data=None, headers=None, auth=None, 58 | retries=3, timeout=10, verify=False, cert=False): 59 | """Send request to server. 60 | 61 | Args: 62 | method (method): Requests library method to call. 63 | 64 | url (str): Request URL. 65 | 66 | auth (tuple): Authentication username and password. 67 | 68 | Kwargs: 69 | params (dict): Request parameters. 70 | 71 | data (str): Request body data. 72 | 73 | headers (dict): Request headers. 74 | 75 | Returns: 76 | str: Server response. 77 | """ 78 | request_params = params 79 | if request_params is None: 80 | request_params = {} 81 | 82 | request_data = data 83 | if request_data is None: 84 | request_data = {} 85 | 86 | if headers: 87 | request_headers = headers 88 | else: 89 | request_headers = {} 90 | 91 | if auth: 92 | auth = requests.auth.HTTPBasicAuth(*auth) 93 | 94 | message = None 95 | while retries: 96 | try: 97 | res = method(url, params=request_params, data=request_data, 98 | headers=request_headers, 99 | timeout=timeout, verify=verify, auth=auth, cert=cert) 100 | message = res.text 101 | if res.status_code == 200: 102 | try: 103 | if (res.headers['Content-Transfer-Encoding'] == 'base64' 104 | and not res.content.startswith(b'-----BEGIN')): 105 | return base64.b64decode(res.content) 106 | except KeyError: 107 | pass 108 | return res.content 109 | elif res.status_code in (400, 401, 403, 404, 413, 202): 110 | break 111 | except (requests.exceptions.RequestException) as exception: 112 | message = str(exception) 113 | res = None 114 | 115 | time.sleep(1) 116 | retries -= 1 117 | 118 | raise_request_error(res, message) 119 | 120 | def raise_request_error(res, message): 121 | """Raise a RequestError exception. 122 | 123 | Args: 124 | res (Requests response): HTTP response. 125 | 126 | message (str): Request error message. 127 | 128 | Raises: 129 | est.errors.RequestError 130 | """ 131 | if res is not None: 132 | status = res.status_code 133 | else: 134 | status = None 135 | 136 | if status == 202: 137 | raise est.errors.TryLater(int(res.headers['retry-after']), message) 138 | else: 139 | raise est.errors.RequestError(status, message) 140 | -------------------------------------------------------------------------------- /est/client.py: -------------------------------------------------------------------------------- 1 | """EST Client. 2 | 3 | This is the first object to instantiate to interact with the API. 4 | """ 5 | 6 | import base64 7 | import ssl 8 | import subprocess 9 | 10 | import OpenSSL.crypto 11 | 12 | import asn1crypto.core 13 | 14 | import est.errors 15 | import est.request 16 | 17 | class Client(object): 18 | """API client. 19 | 20 | Attributes: 21 | uri (str): URI prefix to use for requests. 22 | 23 | url_prefix (str): URL prefix to use for requests. scheme://host:port 24 | """ 25 | url_prefix = None 26 | username = None 27 | password = None 28 | implicit_trust_anchor_cert_path = None 29 | 30 | def __init__(self, host, port, implicit_trust_anchor_cert_path): 31 | """Initialize the client to interact with the EST server. 32 | 33 | Args: 34 | host (str): EST server hostname. 35 | 36 | port (int): EST server port number. 37 | 38 | implicit_trust_anchor_cert_path (str): 39 | EST server implicit trust anchor certificate path. 40 | """ 41 | self.url_prefix = 'https://%s:%s/.well-known/est' % (host, port) 42 | self.implicit_trust_anchor_cert_path = implicit_trust_anchor_cert_path 43 | 44 | def cacerts(self): 45 | """EST /cacerts request. 46 | 47 | Args: 48 | None 49 | 50 | Returns: 51 | str. CA certificates (PEM). 52 | 53 | Raises: 54 | est.errors.RequestError 55 | """ 56 | url = self.url_prefix + '/cacerts' 57 | content = est.request.get(url, 58 | verify=self.implicit_trust_anchor_cert_path) 59 | 60 | pem = self.pkcs7_to_pem(content) 61 | 62 | return pem 63 | 64 | def simpleenroll(self, csr): 65 | """EST /simpleenroll request. 66 | 67 | Args: 68 | csr (str): Certificate signing request (PEM). 69 | 70 | Returns: 71 | str. Signed certificate (PEM). 72 | 73 | Raises: 74 | est.errors.RequestError 75 | """ 76 | url = self.url_prefix + '/simpleenroll' 77 | auth = (self.username, self.password) 78 | headers = {'Content-Type': 'application/pkcs10'} 79 | content = est.request.post(url, csr, auth=auth, headers=headers, 80 | verify=self.implicit_trust_anchor_cert_path) 81 | pem = self.pkcs7_to_pem(content) 82 | 83 | return pem 84 | 85 | def simplereenroll(self, csr, cert=False): 86 | """EST /simplereenroll request. 87 | 88 | Args: 89 | csr (str): Certificate signing request (PEM). 90 | 91 | cert (tuple): Client cert path and private key path. 92 | 93 | Returns: 94 | str. Signed certificate (PEM). 95 | 96 | Raises: 97 | est.errors.RequestError 98 | """ 99 | url = self.url_prefix + '/simplereenroll' 100 | auth = (self.username, self.password) 101 | headers = {'Content-Type': 'application/pkcs10'} 102 | content = est.request.post(url, csr, auth=auth, headers=headers, 103 | verify=self.implicit_trust_anchor_cert_path, 104 | cert=cert) 105 | pem = self.pkcs7_to_pem(content) 106 | 107 | return pem 108 | 109 | def csrattrs(self): 110 | """EST /csrattrs request. 111 | 112 | Returns: 113 | OrderedDict. Example: 114 | OrderedDict([(u'0', u'1.3.6.1.1.1.1.22'), 115 | (u'1', u'1.2.840.113549.1.9.1'), 116 | (u'2', u'1.3.132.0.34'), 117 | (u'3', u'2.16.840.1.101.3.4.2.2')]) 118 | 119 | Raises: 120 | est.errors.RequestError 121 | """ 122 | url = self.url_prefix + '/csrattrs' 123 | content = est.request.get(url, 124 | verify=self.implicit_trust_anchor_cert_path) 125 | 126 | parsed = asn1crypto.core.Sequence.load(content) 127 | return parsed.native 128 | 129 | def set_basic_auth(self, username, password): 130 | """Set up HTTP Basic authentication. 131 | 132 | Args: 133 | username (str). 134 | 135 | password (str). 136 | """ 137 | self.username = username 138 | self.password = password 139 | 140 | def create_csr(self, common_name, country=None, state=None, city=None, 141 | organization=None, organizational_unit=None, 142 | email_address=None, subject_alt_name=None): 143 | """ 144 | Args: 145 | common_name (str). 146 | 147 | country (str). 148 | 149 | state (str). 150 | 151 | city (str). 152 | 153 | organization (str). 154 | 155 | organizational_unit (str). 156 | 157 | email_address (str). 158 | 159 | subject_alt_name (str). 160 | 161 | Returns: 162 | (str, str). Tuple containing private key and certificate 163 | signing request (PEM). 164 | """ 165 | key = OpenSSL.crypto.PKey() 166 | key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048) 167 | 168 | req = OpenSSL.crypto.X509Req() 169 | req.get_subject().CN = common_name 170 | if country: 171 | req.get_subject().C = country 172 | if state: 173 | req.get_subject().ST = state 174 | if city: 175 | req.get_subject().L = city 176 | if organization: 177 | req.get_subject().O = organization 178 | if organizational_unit: 179 | req.get_subject().OU = organizational_unit 180 | if email_address: 181 | req.get_subject().emailAddress = email_address 182 | if subject_alt_name: 183 | altName = OpenSSL.crypto.X509Extension('subjectAltName', False, subject_alt_name) 184 | req.add_extensions([altName]) 185 | 186 | req.set_pubkey(key) 187 | req.sign(key, 'sha256') 188 | 189 | private_key = OpenSSL.crypto.dump_privatekey( 190 | OpenSSL.crypto.FILETYPE_PEM, key) 191 | 192 | csr = OpenSSL.crypto.dump_certificate_request( 193 | OpenSSL.crypto.FILETYPE_PEM, req) 194 | 195 | return private_key, csr 196 | 197 | def pkcs7_to_pem(self, pkcs7): 198 | inform = None 199 | for filetype in (OpenSSL.crypto.FILETYPE_PEM, 200 | OpenSSL.crypto.FILETYPE_ASN1): 201 | try: 202 | OpenSSL.crypto.load_pkcs7_data(filetype, pkcs7) 203 | if filetype == OpenSSL.crypto.FILETYPE_PEM: 204 | inform = 'PEM' 205 | else: 206 | inform = 'DER' 207 | break 208 | except OpenSSL.crypto.Error: 209 | pass 210 | 211 | if not inform: 212 | raise est.errors.Error('Invalid PKCS7 data type') 213 | 214 | stdout, stderr = subprocess.Popen( 215 | ['openssl', 'pkcs7', '-inform', inform, '-outform', 'PEM', 216 | '-print_certs'], 217 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, 218 | stdin=subprocess.PIPE 219 | ).communicate(pkcs7) 220 | 221 | return stdout.decode("utf-8") 222 | --------------------------------------------------------------------------------