diff --git a/clouder_cert_authority/README.rst b/clouder_cert_authority/README.rst new file mode 100644 index 0000000..09e4376 --- /dev/null +++ b/clouder_cert_authority/README.rst @@ -0,0 +1,52 @@ +.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +============================= +Clouder Cert Authority +============================= + +This module provides a Cert Authority using Clouder and CFSSL, + + +Configuration +============= + +Clouder configuration instructions are available at https://clouder.readthedocs.io/ + +Usage +===== + +To use this module, you need to: + +#. Create a CFSSL Service in the Clouder Control Panel + +Known issues / Roadmap +====================== + +* Add more Signature Profile options - https://github.com/cloudflare/cfssl/blob/86ecfbe5750ebf05565e4c80104d0a7919792fee/doc/cmd/cfssl.txt#L113 +* Need to add a hook so that services dependent on revoked certs are refreshed +* Don't run as root + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Dave Lasley + +Maintainer +---------- + +This module is maintained by Clouder Community. + +To contribute to this module, please visit https://github.com/clouder-community/clouder diff --git a/clouder_cert_authority/__init__.py b/clouder_cert_authority/__init__.py new file mode 100644 index 0000000..ebef902 --- /dev/null +++ b/clouder_cert_authority/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from . import models +from . import wizards diff --git a/clouder_cert_authority/__manifest__.py b/clouder_cert_authority/__manifest__.py new file mode 100644 index 0000000..973b9d7 --- /dev/null +++ b/clouder_cert_authority/__manifest__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +{ + 'name': 'Clouder Cert Authority', + 'version': '10.0.10.0.0', + 'category': 'Clouder', + 'depends': [ + 'clouder', + 'clouder_template_proxy', + ], + 'author': 'LasLabs Inc.', + 'license': 'LGPL-3', + 'website': 'https://github.com/clouder-community/clouder', + 'data': [ + 'data/image_template.xml', + 'data/image.xml', + 'data/image_port.xml', + 'data/image_volume.xml', + 'data/application_tag.xml', + 'data/application_type.xml', + 'data/application_template.xml', + 'data/application.xml', + 'data/cert_policy_use.xml', + 'views/cert_authority.xml', + 'views/menu.xml', + 'wizards/cert_authority_scan.xml', + 'wizards/cert_authority_sign.xml', + 'wizards/cert_authority_revoke.xml', + ], + 'installable': True, + 'application': False, +} diff --git a/clouder_cert_authority/api.py b/clouder_cert_authority/api.py new file mode 100644 index 0000000..41c0867 --- /dev/null +++ b/clouder_cert_authority/api.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +import logging + +from contextlib import contextmanager + +from odoo import api + +_logger = logging.getLogger(__name__) + +try: + import cfssl +except ImportError: + _logger.info('CFSSL Python library is not installed.') + + +class API(object): + """ It provides a base for all Models requiring API functionality """ + + cfssl = cfssl + + @contextmanager + @api.model_cr_context + def get_api(self, cert_authority=None): + """ It returns a :obj:`cfssl.CFSSL` for the cert authority. + + Args: + cert_authority (:type:`clouder.CertAuthority`): + The cert authority record singleton representing the + remote API. The CA does not have to be initialized yet. Use + :type:`None` if ``self`` is the CA that should be connected + to. + """ + try: + # @TODO: Figure out how the hell to get this host from the base + host = '000.000.000.000' + port = cert_authority.port_id.local_port + api = cfssl.CFSSL(host, port, ssl=True) + yield api + finally: + pass diff --git a/clouder_cert_authority/data/application.xml b/clouder_cert_authority/data/application.xml new file mode 100644 index 0000000..ea082fc --- /dev/null +++ b/clouder_cert_authority/data/application.xml @@ -0,0 +1,60 @@ + + + + + + + OpenSSL + exec + + + + 1 + + auto + + + + CFSSL Data + data + + + + 1 + + + + + CFSSL Exec + exec + + + + 2 + + auto + + + + + CFSSL + cfssl + + 1 + + + + + + + + diff --git a/clouder_cert_authority/data/application_tag.xml b/clouder_cert_authority/data/application_tag.xml new file mode 100644 index 0000000..62e887a --- /dev/null +++ b/clouder_cert_authority/data/application_tag.xml @@ -0,0 +1,11 @@ + + + + + + + cert_authority + + + diff --git a/clouder_cert_authority/data/application_template.xml b/clouder_cert_authority/data/application_template.xml new file mode 100644 index 0000000..fb6c5d9 --- /dev/null +++ b/clouder_cert_authority/data/application_template.xml @@ -0,0 +1,19 @@ + + + + + + + CFSSL + + + + OpenSSL + + + diff --git a/clouder_cert_authority/data/application_type.xml b/clouder_cert_authority/data/application_type.xml new file mode 100644 index 0000000..b19d5c4 --- /dev/null +++ b/clouder_cert_authority/data/application_type.xml @@ -0,0 +1,24 @@ + + + + + + + cfssl + root + + + + + openssl + root + + + diff --git a/clouder_cert_authority/data/cert_policy_use.xml b/clouder_cert_authority/data/cert_policy_use.xml new file mode 100644 index 0000000..c62e284 --- /dev/null +++ b/clouder_cert_authority/data/cert_policy_use.xml @@ -0,0 +1,162 @@ + + + + + + + Cert Signing + cert sign + + + + Signing + signing + + + + S/MIME + s/mime + + + + Server Authentication + server auth + + + + Client Authentication + client auth + + + + Digital Signatures + digital signature + + + + Email Protection + email protection + + + + Key Encipherment + key encipherment + + + + Content Commitment + content commitment + + + + Key Agreement + key agreement + + + + CRL Signing + crl sign + + + + Encipher Only + encipher only + + + + Decipher Only + decipher only + + + + Any + any + + + + Code Signing + code signing + + + + IPSEC End System + ipsec end system + + + + IPSEC Tunnel + ipsec tunnel + + + + IPSEC User + ipsec user + + + + Timestamping + timestamping + + + + OCSP Signing + ocsp signing + + + + Microsoft SGC + microsoft sgc + + + + Netscape SGC + netscape sgc + + + diff --git a/clouder_cert_authority/data/image.xml b/clouder_cert_authority/data/image.xml new file mode 100644 index 0000000..49ae3f5 --- /dev/null +++ b/clouder_cert_authority/data/image.xml @@ -0,0 +1,32 @@ + + + + + + + image_cfssl_data + + clouder/base:3.4 + + + + image_cfssl_exec + + laslabs/clouder-cffsl-exec:latest + data + + + + image_openssl_exec + + laslabs/clouder-openssl-exec:latest + + + diff --git a/clouder_cert_authority/data/image_port.xml b/clouder_cert_authority/data/image_port.xml new file mode 100644 index 0000000..0a90d3a --- /dev/null +++ b/clouder_cert_authority/data/image_port.xml @@ -0,0 +1,15 @@ + + + + + + + + http + 8080 + + + diff --git a/clouder_cert_authority/data/image_template.xml b/clouder_cert_authority/data/image_template.xml new file mode 100644 index 0000000..5c32650 --- /dev/null +++ b/clouder_cert_authority/data/image_template.xml @@ -0,0 +1,25 @@ + + + + + + + image_template_cfssl_data + + + + image_template_cfssl_exec + + + + image_template_openssl_exec + + + diff --git a/clouder_cert_authority/data/image_volume.xml b/clouder_cert_authority/data/image_volume.xml new file mode 100644 index 0000000..e882e1f --- /dev/null +++ b/clouder_cert_authority/data/image_volume.xml @@ -0,0 +1,16 @@ + + + + + + + + cert_store + /var/pki + root + + + diff --git a/clouder_cert_authority/models/__init__.py b/clouder_cert_authority/models/__init__.py new file mode 100644 index 0000000..dc20286 --- /dev/null +++ b/clouder_cert_authority/models/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +# Abstract +from . import cert_abstract +from . import config_cert_abstract +from . import key_abstract + +# CA +from . import cert_authority + +# Cert parts +from . import cert_host +from . import cert_name + +# Cert Types +from . import cert_request +from . import cert_x509 + +# Key Types +from . import key_private +from . import key_public + +# Policies +from . import cert_policy_auth +from . import cert_policy_sign +from . import cert_policy_use + +# Configs +from . import config_cert_client +from . import config_cert_server diff --git a/clouder_cert_authority/models/cert_abstract.py b/clouder_cert_authority/models/cert_abstract.py new file mode 100644 index 0000000..7a7bd6e --- /dev/null +++ b/clouder_cert_authority/models/cert_abstract.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class ClouderCertAbstract(models.AbstractModel): + """ It provides attributes and methods related to all cert files. """ + + _name = 'clouder.cert.abstract' + _description = 'Clouder Cert Abstract' + + name = fields.Char( + required=True, + help='SHA-1 Sum of Cert', + ) + description = fields.Char() + mime_sub_type = fields.Selection( + selection=lambda s: s._get_mime_sub_types(), + deault='x-pkcs12', + ) + mime_type = fields.Char( + readonly=True, + computed='_compute_mime_type', + ) + attachment_id = fields.Many2one( + string='Key', + comodel_name='ir.attachment', + context="""{ + 'default_type': 'binary', + 'default_res_model': _name, + 'default_res_field': 'attachment_id', + 'default_res_id': id, + 'default_name': name, + 'default_description': description, + 'default_mime_type': mime_type, + }""", + ) + data = fields.Binary( + related='attachment_id.datas', + ) + request_id = fields.Many2one( + string='CSR', + comodel_name='clouder.cert.request', + ) + + @api.model + def _get_mime_sub_types(self): + return [ + ('pkcs8', 'PKCS-8'), + ('pkcs10', 'PKCS-10'), + ('x-pkcs12', 'PKCS-12'), + ('x-pem-file', 'PEM'), + ('pkcs7-mime', 'PKCS-7 MIME'), + ('x-x509-ca-cert', 'X.509 CA'), + ('x-x509-user-cert', 'X.509 User'), + ] + + @api.multi + def _compute_mime_type(self): + for record in self: + record.mime_type = 'application/%s' % record.mime_sub_type diff --git a/clouder_cert_authority/models/cert_authority.py b/clouder_cert_authority/models/cert_authority.py new file mode 100644 index 0000000..d06d190 --- /dev/null +++ b/clouder_cert_authority/models/cert_authority.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +import pickle + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + +from ..api import API + +class ClouderCertAuthority(models.Model, API): + """ It provides an interface for controlling a Cert Authority. """ + + _name = 'clouder.cert.authority' + _description = 'Clouder Cert Authority' + _inherits = {'clouder.service': 'service_id'} + + service_id = fields.Many2one( + string='Cert Authority', + comodel_name='clouder.service', + required=True, + ondelete='cascade', + domain=lambda s: "[('application_id.tag_ids', 'in', %s)]" % ( + s.env.ref("clouder_cert_authority.tag_cert_authority").ids, + ) + ) + environment_id = fields.Many2one( + string='Environment', + comodel_name='clouder.environment', + related='service_id.environment_id', + ) + api_port_id = fields.Many2one( + string='API Port', + comodel_name='clouder.image.port', + compute='_compute_api_port_id', + ) + private_key_ids = fields.One2many( + string='Private Keys', + comodel_name='clouder.key.private', + related='cert_request_id.private_key_ids', + ) + cert_ids = fields.One2many( + string='Certs', + comodel_name='clouder.key.public', + related='cert_request_id.public_key_ids', + ) + cert_request_id = fields.Many2one( + string='CSR', + comodel_name='clouder.cert.request', + context="""{'default_host_ids': [(4, node_id)], + 'default_authority_id': id, + 'default_public_key_id': cert_id, + }""", + help='Cert Signing Request.', + ) + config_id = fields.Many2one( + string='Configuration', + comodel_name='clouder.config.cert.server', + help='Cert Authority Configuration.', + ) + is_initialized = fields.Boolean( + string='Initialized', + readonly=True, + help='Has this CA server been initialized yet?', + ) + openssl_service_id = fields.Many2one( + string='OpenSSL Service', + comodel_name='clouder.service', + compute='_compute_openssl_service_id', + ) + + @api.multi + @api.depends('service_id') + def _compute_api_port_id(self): + for record in self: + ports = record.service_id.port_ids.filtered( + lambda r: r.name == 'https' + ) + record.api_port_id = ports[0] + + @api.multi + @api.depends('sign_default_id', + 'sign_profile_ids', + ) + def _compute_auth_policy_ids(self): + for record in self: + policies = sum((record.sign_default_id, + record.sign_profile_ids, + )) + record.auth_policy_ids = policies.mapped('auth_policy_id') + + @api.multi + @api.depends('service_id') + def _compute_openssl_service_id(self): + for record in self: + # @TODO + raise NotImplementedError() + + @api.multi + @api.constrains('environment_id', 'service_id') + def _check_environment_id(self): + for record in self: + res = self.search( + [('environment_id', '=', record.enviroment_id.id)], + ) + if len(res) > 1: + raise ValidationError(_( + 'The Environment "%s" already has an active Certificate ' + 'Authority.', + )) + + @api.multi + def name_get(self): + names = [] + for record in self: + name = record.name + if record.cert_request_id: + name += ': %s' % record.cert_request_id.name + names.append((record.id, name)) + return names + + @api.multi + def init_ca(self): + """ It initializes a new cert authority if needed. """ + for record in self.fitered(lambda r: not r.is_initialized): + if not record.cert_request_id: + raise ValidationError(_( + 'You must assign a cert request before ' + 'initializing a Cert Authority.', + )) + with record.get_api() as api: + csr = record.cert_request_id + response = api.init_ca( + cert_request=csr.api_object, + ca=record.config_id.api_object, + ) + cert = record.cert_request_id._new_cert( + response['cert'], + ) + private_key = record.cert_request_id._new_key( + response['private_key'], + public=False, + ) + record.write({ + 'is_initialized': True, + }) + + @api.multi + def scan(self, host, ip=None): + """ It scans servers to determine the quality of their TLS setup. + + Args: + host (ClouderCertHost): The host to scan. + ip (str): The IP Address to override DNS lookup of host. + """ + self.ensure_one() + with self.get_api() as api: + results = api.scan(host.api_object, ip) + if results['error']: + raise UserError(_(results['error'])) + return { + 'grade': results['grade'], + 'output': results['output'], + } + + @api.multi + def sign(self, cert_request, hosts=None, + subject=None, serial_sequence=None): + """ It signs a cert request. """ + self.ensure_one() + if hosts is None: + hosts = [] + with self.get_api() as api: + results = api.sign( + certifcate_request=cert_request.api_object, + profile=self.config_id.api_object, + hosts=[host.api_object for host in hosts], + subject=subject, + serial_sequence=serial_sequence, + ) + return cert_request._new_cert(results) + + @api.multi + def revoke(self, cert): + """ It revokes a cert. """ + self.ensure_one() + with self.get_api() as api: + api.revoke(cert.name, cert.authority_key) + + @api.model + def _get_cert_info(self, cert): + """ It returns information about a signed cert. + + Args: + cert (str): PEM encoded cert. + Returns: + dict: A dictionary with the following keys: + * extensions (:class:`dict`): X.509 extensions. Some valid + keys are: + * authorityInfoAccess + * authorityKeyIdentifier + * basicConstraints + * cRLDistributionPoints + * certPolicies + * extendedKeyUsage + * issuerAltName + * keyUsage + * subjectAltName + * subjectKeyIdentifier + * fingerprint (:class:`str`): Cert fingerprint. + * signature (:class:`str`): Cert signature. + * not_valid_before (:class:`datetime`): Validity start + date for the cert. + * not_valid_after (:class:`datetime`): Validity end date + for the cert. + * public_key (:class:`str`): Public key associated with + the cert. + """ + self.ensure_one() + res = self.openssl_service_id.execute(['parse_cert', cert]) + return pickle.loads(res.decode('base64')) diff --git a/clouder_cert_authority/models/cert_host.py b/clouder_cert_authority/models/cert_host.py new file mode 100644 index 0000000..7d4a38a --- /dev/null +++ b/clouder_cert_authority/models/cert_host.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + +from ..api import API + + +class ClouderCertHost(models.Model, API): + """ It provides the concept of a cert's CommonName """ + + _name = 'clouder.cert.host' + _description = 'Clouder Cert Host' + + name = fields.Char( + required=True, + ) + host = fields.Char( + required=True, + ) + port = fields.Integer() + api_object = fields.Binary( + compute="_compute_api_object", + ) + + @api.multi + def _compute_api_object(self): + """ It computes the keys required for the JSON request """ + for record in self: + record.api_object = self.cfssl.Host( + name=record.name, + host=record.host, + port=record.port, + ) diff --git a/clouder_cert_authority/models/cert_name.py b/clouder_cert_authority/models/cert_name.py new file mode 100644 index 0000000..2e97d2b --- /dev/null +++ b/clouder_cert_authority/models/cert_name.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + +from ..api import API + + +class ClouderCertName(models.Model, API): + """ It provides the concept of a cert's SubjectInfo """ + + _name = 'clouder.cert.name' + _description = 'Clouder Cert Name' + + country_id = fields.Many2one( + string='Country', + comodel_name='res.country', + required=True, + default=lambda s: s.env.user.country_id, + ) + state_id = fields.Many2one( + string='State', + comodel_name='res.country.state', + domain='[(country_id, "=", country_id)]', + default=lambda s: s.env.user.state_id, + ) + city = fields.Char( + default=lambda s: s.env.user.city, + ) + company_id = fields.Many2one( + string='Organization', + comodel_name='res.company', + required=True, + domain='[(company_id, "=", company_id)]', + default=lambda s: s.env.user.company_id, + ) + organization_unit = fields.Char( + required=True, + ) + api_object = fields.Binary( + compute="_compute_api_object", + ) + + @api.multi + def _compute_api_object(self): + """ It computes the keys required for the JSON request """ + for record in self: + record.api_object = self.cfssl.SubjectInfo( + record.company_id.name, + record.organizational_unit, + record.city, + record.state_id.name, + record.country_id.code, + ) diff --git a/clouder_cert_authority/models/cert_policy_auth.py b/clouder_cert_authority/models/cert_policy_auth.py new file mode 100644 index 0000000..1120506 --- /dev/null +++ b/clouder_cert_authority/models/cert_policy_auth.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + +from odoo.addons.clouder.tools import generate_random_password + + +class ClouderCertPolicyAuth(models.Model): + """ It provides a CA Signature Auth Policy """ + + _name = 'clouder.cert.policy.auth' + _description = 'Clouder Cert Auth Policy' + + name = fields.Char( + required=True, + ) + key = fields.Char( + required=True, + default=lambda s: s._default_key(), + ) + key_type = fields.Selection([ + ('standard', 'Standard'), + ], + default='standard', + required=True, + ) + api_object = fields.Binary( + compute="_compute_api_object", + ) + + _sql_constraints = [ + ('name_uniq', 'UNIQUE(name)', 'Name must be unique.'), + ('key_uniq', 'UNIQUE(key)', 'Key must be unique.'), + ] + + @api.model + def _default_key(self): + passwd = generate_random_password() + return passwd.encode('hex')[:16] + + @api.multi + def _compute_api_object(self): + """ It computes the keys required for the JSON request """ + for record in self: + record.api_object = self.cfssl.PolicyAuth({ + 'name': record.name, + 'key': record.key, + 'key_type': record.key_type, + }) diff --git a/clouder_cert_authority/models/cert_policy_sign.py b/clouder_cert_authority/models/cert_policy_sign.py new file mode 100644 index 0000000..096787b --- /dev/null +++ b/clouder_cert_authority/models/cert_policy_sign.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class ClouderCertPolicySign(models.Model): + """ It provides a CA Signature Policy """ + + _name = 'clouder.cert.policy.sign' + _description = 'Clouder Cert Signing Policy' + + name = fields.Char( + required=True, + ) + usage_ids = fields.Many2many( + string='Usages', + comodel_name='clouder.cert.policy.use', + required=True, + ) + auth_policy_id = fields.Many2one( + string='Auth Key', + comodel_name='clouder.cert.policy.auth', + required=True, + ) + expire_hours = fields.Integer( + required=True, + default=(365 * 24), + ) + api_object = fields.Binary( + compute="_compute_api_object", + ) + + @api.multi + def _compute_api_object(self): + """ It computes the keys required for the JSON request """ + for record in self: + record.api_object = self.cfssl.PolicySign({ + 'name': record.name, + 'usage_policies': record.usage_ids.mapped('api_object'), + 'auth_policy': record.auth_policy_id.api_object, + }) diff --git a/clouder_cert_authority/models/cert_policy_use.py b/clouder_cert_authority/models/cert_policy_use.py new file mode 100644 index 0000000..1d502fc --- /dev/null +++ b/clouder_cert_authority/models/cert_policy_use.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class ClouderCertPolicyUse(models.Model): + """ It provides a CA Signature Use Policy """ + + _name = 'clouder.cert.policy.use' + _description = 'Clouder Cert Usage Policy' + _order = 'sequence, name' + + name = fields.Char( + required=True, + ) + code = fields.Char( + required=True, + ) + sequence = fields.Integer( + required=True, + default=5, + ) + api_object = fields.Binary( + compute="_compute_api_object", + ) + + _sql_constraints = [ + ('code_uniq', 'UNIQUE(code)', 'Code must be unique.'), + ('name_uniq', 'UNIQUE(name)', 'Name must be unique.'), + ] + + @api.multi + def _compute_api_object(self): + """ It computes the keys required for the JSON request """ + for record in self: + record.api_object = self.cfssl.PolicyUse( + name=record.name, + code=record.code, + ) diff --git a/clouder_cert_authority/models/cert_request.py b/clouder_cert_authority/models/cert_request.py new file mode 100644 index 0000000..d7a758e --- /dev/null +++ b/clouder_cert_authority/models/cert_request.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from hashlib import sha1 + +from odoo import api, fields, models + +from ..api import API + + +class ClouderCertRequest(models.Model, API): + """ It provides the concept of a Cert Request """ + + _name = 'clouder.cert.request' + _inherit = ['clouder.cert.abstract', + 'clouder.key.abstract', + ] + _description = 'Clouder Cert Request' + + name = fields.Char( + string='Common Name', + required=True, + ) + authority_id = fields.Many2one( + string='Cert Authority', + comodel_name='clouder.cert.authority', + required=True, + ondelete='cascade', + ) + host_ids = fields.Many2one( + string='Hosts', + comodel_name='clouder.cert.host', + ) + subject_info_ids = fields.Many2many( + string='Names', + comodel_name='clouder.cert.name', + required=True, + ) + public_key_ids = fields.One2many( + string='Public Key', + comodel_name='clouder.key.public', + inverse_name='request_id', + ) + private_key_ids = fields.One2many( + string='Private Key', + comodel_name='clouder.key.private', + inverse_name='request_id', + ) + cert_ids = fields.One2many( + string='Signed Cert', + comodel_name='clouder.cert.x509', + inverse_name='request_id', + ) + api_object = fields.Binary( + compute="_compute_api_object", + ) + + @api.multi + def create_cert(self): + for record in self: + with record.get_api(record.authority_id) as api: + response = api.new_cert( + request=record.to_api(), + ) + cert = record._new_cert( + response['cert'], + ) + private_key = record._new_key( + response['private_key'], + public=True, + ) + attachment = record.attachment_id.create({ + 'datas': response['cert_request'], + }) + record.write({ + 'attachment_id': attachment.id, + }) + + @api.multi + def _new_key(self, key_data, public=False, mime='x-pem-file'): + self.ensure_one() + model = 'public' if public else 'private' + key = self.env['clouder.key.%s' % model].create({ + 'name': sha1(key_data).hexdigest(), + 'strength': self.strength, + 'algorithm': self.algorithm, + 'mime_sub_type': mime, + 'request_id': self.id, + }) + attachment = key.attachment_id.create({ + 'datas': key_data, + }) + key.attachment_id = attachment.id + return key + + @api.multi + def _new_cert(self, cert_data): + self.ensure_one() + cert_info = self.authority_id._get_cert_info(cert_data) + public_key = self._new_key( + cert['public_key'], + public=True, + ) + auth_key = cert_info['authorityKeyIdentifier']['key_identifier'] + usages = self.env['clouder.policy.use'] + for usage_str in cert_info['keyUsage']: + usage = usages.search([ + ('code', '=', usage_str.replace(' ', '_')), + ]) + if not usage: + continue + usages += usage + cert = self.env['clouder.cert.x509'].create({ + 'subject_key': cert_info['subjectKeyIdentifier']['digest'], + 'authority_key': auth_key, + 'public_key_id': public_key.id, + 'request_id': self.id, + 'usage_ids': usages.ids, + }) + attachment = key.attachment_id.create({ + 'datas': cert_data, + }) + cert.attachment_id = attachment.id + + @api.multi + def _compute_api_object(self): + """ It computes the keys required for the JSON request """ + for record in self: + record.api_object = self.cfssl.CertRequest( + common_name=record.name, + names=record.subject_info_ids.mapped('api_object'), + hosts=record.host_ids.mapped('api_object'), + key=self.cfssl.ConfigKey( + algorithm=record.algorithm, + strength=record.strength, + ), + ) diff --git a/clouder_cert_authority/models/cert_x509.py b/clouder_cert_authority/models/cert_x509.py new file mode 100644 index 0000000..f3e4ea0 --- /dev/null +++ b/clouder_cert_authority/models/cert_x509.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + +from ..api import API + + +class ClouderCertX509(models.Model, API): + """ It provides the concept of a signed cert. """ + + _name = 'clouder.cert.x509' + _inherit = 'clouder.cert.abstract' + _description = 'Clouder Cert X.509' + + subject_key = fields.Char( + required=True, + help='X509v3 Subject Key Identifier.', + ) + authority_key = fields.Char( + required=True, + help='X509v3 Authority Key Identifier.', + ) + public_key_id = fields.Many2one( + string='Public Key', + comodel_name='clouder.key.public', + required=True, + context="{'default_request_id': request_id}", + ) + request_id = fields.Many2one( + string='CSR', + comodel_name='clouder.cert.request', + required=True, + ) + use_policy_ids = fields.Many2many( + string='Use Policies', + comodel_name='clouder.cert.policy.use', + ) diff --git a/clouder_cert_authority/models/config_cert_abstract.py b/clouder_cert_authority/models/config_cert_abstract.py new file mode 100644 index 0000000..35a38de --- /dev/null +++ b/clouder_cert_authority/models/config_cert_abstract.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + +from ..api import API + + +class ClouderConfigCertAbstract(models.AbstractModel, API): + """ It provides data handling for cert client + server configs. """ + + _name = 'clouder.config.cert.abstract' + _description = 'Clouder Config Cert Abstract' + + sign_default_id = fields.Many2one( + string='Default Signing Policy', + comodel_name='clouder.cert.policy.sign', + required=True, + ) + sign_profile_ids = fields.Many2many( + string='Signing Policy Profiles', + comodel_name='clouder.cert.policy.sign', + ) + auth_policy_ids = fields.Many2many( + string='Auth Policies', + comodel_name='clouder.cert.policy.auth', + compute='_compute_auth_policy_ids', + ) + api_object = fields.Binary( + compute="_compute_api_object", + ) + + @api.multi + def _compute_api_object(self): + """ It computes the keys required for the JSON request. """ + for record in self: + profiles = record.sign_profile_ids.mapped('api_object') + auth_keys = record.auth_policy_ids.mapped('api_object') + record.api_object = self.cfssl.ConfigMixer( + sign_policy=record.sign_default_id.api_object, + sign_policies=profiles, + auth_policies=auth_keys, + ) diff --git a/clouder_cert_authority/models/config_cert_client.py b/clouder_cert_authority/models/config_cert_client.py new file mode 100644 index 0000000..57c5c4a --- /dev/null +++ b/clouder_cert_authority/models/config_cert_client.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + +from ..api import API + + +class ClouderConfigCertClient(models.Model, API): + """ It provides data handling for cert client configs. """ + + _name = 'clouder.config.cert.client' + _inherit = 'clouder.config.cert.abstract' + _description = 'Clouder Config Cert Client' + + remote_ids = fields.Many2many( + string='Remotes', + comodel_name='clouder.cert.host', + required=True, + ) + + @api.multi + def _compute_api_object(self): + """ It computes the keys required for the JSON request. """ + for record in self: + super(ClouderConfigCertClient, record)._compute_api_object() + remotes = { + remote.name: remote.api_object for remote in record.remote_ids + } + record.api_object = self.cfssl.ConfigClient( + sign_policy_default=record.api_object.sign_policy, + sign_policies_add=record.api_object.sign_policies, + auth_policies=record.api_object.auth_policies, + remotes=remotes, + ) diff --git a/clouder_cert_authority/models/config_cert_server.py b/clouder_cert_authority/models/config_cert_server.py new file mode 100644 index 0000000..958dcb4 --- /dev/null +++ b/clouder_cert_authority/models/config_cert_server.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, models + +from ..api import API + + +class ClouderConfigCertServer(models.Model, API): + """ It provides data handling for cert server configs. """ + + _name = 'clouder.config.cert.server' + _inherit = 'clouder.config.cert.abstract' + _description = 'Clouder Config Cert Server' + + @api.multi + def _compute_api_object(self): + """ It computes the keys required for the JSON request. """ + for record in self: + super(ClouderConfigCertServer, record)._compute_api_object() + record.api_object = self.cfssl.ConfigServer( + sign_policy_default=record.api_object.sign_policy, + sign_policies_add=record.api_object.sign_policies, + auth_policies=record.api_object.auth_policies, + ) diff --git a/clouder_cert_authority/models/key_abstract.py b/clouder_cert_authority/models/key_abstract.py new file mode 100644 index 0000000..4c916bd --- /dev/null +++ b/clouder_cert_authority/models/key_abstract.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class ClouderKeyAbstract(models.AbstractModel): + """ It provides attributes and methods related to all keys. """ + + _name = 'clouder.key.abstract' + _description = 'Clouder Key Abstract' + + strength = fields.Integer( + default=4096, + ) + algorithm = fields.Selection( + default='rsa', + selection=lambda s: s._get_algorithms(), + ) + is_private = fields.Boolean() + active = fields.Boolean( + default=True, + ) + mime_sub_type = fields.Selection( + selection=lambda s: s._get_mime_sub_types(), + deault='x-pkcs12', + ) + mime_type = fields.Char( + readonly=True, + computed='_compute_mime_type', + ) + attachment_id = fields.Many2one( + string='Key', + comodel_name='ir.attachment', + context="""{ + 'default_type': 'binary', + 'default_res_model': _name, + 'default_res_field': 'attachment_id', + 'default_res_id': id, + 'default_name': name, + 'default_description': description, + 'default_mime_type': mime_type, + }""", + ) + data = fields.Binary( + related='attachment_id.datas', + ) + request_id = fields.Many2one( + string='CSR', + comodel_name='clouder.cert.request', + ) + + @api.model + def _get_algorithms(self): + return [ + ('rsa', 'RSA'), + ('ecdsa', 'ECDSA'), + ] + + @api.model + def _get_mime_sub_types(self): + return self.env['clouder.cert.abstract']._get_mime_sub_types() + + @api.multi + def _compute_mime_type(self): + for record in self: + record.mime_type = 'application/%s' % record.mime_sub_type diff --git a/clouder_cert_authority/models/key_private.py b/clouder_cert_authority/models/key_private.py new file mode 100644 index 0000000..5c8e214 --- /dev/null +++ b/clouder_cert_authority/models/key_private.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class ClouderKeyPrivate(models.Model): + """ It provides the concept of a Private Key """ + + _name = 'clouder.key.private' + _inherit = 'clouder.key.abstract' + _description = 'Clouder Key Private' + + public_key_id = fields.Many2one( + string='Public Key', + comodel_name='clouder.key.public', + ) + + @api.model + def create(self, vals): + vals['is_private'] = True + return super(ClouderKeyPrivate, self).create(vals) diff --git a/clouder_cert_authority/models/key_public.py b/clouder_cert_authority/models/key_public.py new file mode 100644 index 0000000..e40713d --- /dev/null +++ b/clouder_cert_authority/models/key_public.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class ClouderKeyPublic(models.Model): + """ It provides attributes and methods related to handling public keys """ + + _name = 'clouder.key.public' + _inherit = 'clouder.key.abstract' + _description = 'Clouder Key Abstract' diff --git a/clouder_cert_authority/views/cert_authority.xml b/clouder_cert_authority/views/cert_authority.xml new file mode 100644 index 0000000..de80295 --- /dev/null +++ b/clouder_cert_authority/views/cert_authority.xml @@ -0,0 +1,103 @@ + + + + + + + Cert Authority Tree + clouder.cert.authority + + + + + + + + + + + + Cert Authority Form + clouder.cert.authority + +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + Cert Authority Search + clouder.cert.authority + + + + + + + + + + + + + Cert Authorities + clouder.cert.authority + form + tree,form + + + diff --git a/clouder_cert_authority/views/menu.xml b/clouder_cert_authority/views/menu.xml new file mode 100644 index 0000000..6e77df9 --- /dev/null +++ b/clouder_cert_authority/views/menu.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/clouder_cert_authority/wizards/__init__.py b/clouder_cert_authority/wizards/__init__.py new file mode 100644 index 0000000..0c953a6 --- /dev/null +++ b/clouder_cert_authority/wizards/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from . import cert_authority_revoke +from . import cert_authority_scan +from . import cert_authority_sign diff --git a/clouder_cert_authority/wizards/cert_authority_revoke.py b/clouder_cert_authority/wizards/cert_authority_revoke.py new file mode 100644 index 0000000..857b712 --- /dev/null +++ b/clouder_cert_authority/wizards/cert_authority_revoke.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class ClouderCertAuthorityRevoke(models.TransientModel): + """ It revokes a Cert Request. """ + + _name = 'clouder.cert.authority.revoke' + _description = 'Clouder Cert Authority Revoke' + + cert_authority_id = fields.Many2one( + string='Cert Authority', + comodel_name='clouder.cert.authority', + required=True, + readonly=True, + ) + cert_id = fields.Many2one( + string='Signed Cert', + comodel_name='clouder.cert.x509', + required=True, + ) + + @api.multi + def action_revoke(self): + self.ensure_one() + self.cert_authority_id.revoke( + self.cert_id, + ) + return True diff --git a/clouder_cert_authority/wizards/cert_authority_revoke.xml b/clouder_cert_authority/wizards/cert_authority_revoke.xml new file mode 100644 index 0000000..e1c3657 --- /dev/null +++ b/clouder_cert_authority/wizards/cert_authority_revoke.xml @@ -0,0 +1,42 @@ + + + + + + + Cert Authority Revoke Form + clouder.cert.authority.revoke + +
+ +

+ +

+ + + +
+
+
+
+
+
+ + + Revoke A Cert + clouder.cert.authority + clouder.cert.authority.revoke + form + form + client_action_multi + new + + +
diff --git a/clouder_cert_authority/wizards/cert_authority_scan.py b/clouder_cert_authority/wizards/cert_authority_scan.py new file mode 100644 index 0000000..51bf139 --- /dev/null +++ b/clouder_cert_authority/wizards/cert_authority_scan.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class ClouderCertAuthorityScan(models.TransientModel): + """ It scans a host and displays the results. """ + + _name = 'clouder.cert.authority.scan' + _description = 'Clouder Cert Authority Scan' + + cert_authority_id = fields.Many2one( + string='Cert Authority', + comodel_name='clouder.cert.authority', + required=True, + readonly=True, + ) + host_id = fields.Many2one( + string='Host', + comodel_name='clouder.cert.host', + required=True, + help='The host to scan.' + ) + ip = fields.Char( + string='IP', + help='IP Address to override DNS lookup of host.', + ) + state = fields.Selection([ + ('New', 'Not Scanned'), + ('Good', 'Good'), + ('Warning', 'Warning'), + ('Bad', 'Bad'), + ('Skipped', 'Skipped'), + ], + default='New', + readonly=True, + required=True, + ) + error = fields.Text() + results = fields.Text() + + @api.multi + def action_scan(self): + self.ensure_one() + results = self.cert_authority_id.scan( + self.host_id, self.ip, + ) + self.write({ + 'state': results['grade'], + 'error': results['error'], + 'results': results['output'], + }) + return True diff --git a/clouder_cert_authority/wizards/cert_authority_scan.xml b/clouder_cert_authority/wizards/cert_authority_scan.xml new file mode 100644 index 0000000..056ea36 --- /dev/null +++ b/clouder_cert_authority/wizards/cert_authority_scan.xml @@ -0,0 +1,52 @@ + + + + + + + Cert Authority Scan Form + clouder.cert.authority.scan + +
+
+ +
+ +

+ +

+ + + + + + + + +
+
+
+
+
+
+ + + Scan TLS/SSL Setup + clouder.cert.authority + clouder.cert.authority.scan + form + form + client_action_multi + new + + +
diff --git a/clouder_cert_authority/wizards/cert_authority_sign.py b/clouder_cert_authority/wizards/cert_authority_sign.py new file mode 100644 index 0000000..97e77f0 --- /dev/null +++ b/clouder_cert_authority/wizards/cert_authority_sign.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from odoo import api, fields, models + + +class ClouderCertAuthoritySign(models.TransientModel): + """ It signs a Cert Request. """ + + _name = 'clouder.cert.authority.sign' + _description = 'Clouder Cert Authority Sign' + + cert_authority_id = fields.Many2one( + string='Cert Authority', + comodel_name='clouder.cert.authority', + required=True, + readonly=True, + ) + cert_request_id = fields.Many2one( + string='Cert Request', + comodel_name='clouder.cert.request', + required=True, + ) + host_ids = fields.Many2many( + string='Hosts', + comodel_name='clouder.cert.host', + help='Hosts to use as overrides for the CSR.', + ) + subject = fields.Char( + help='Subject to use as override for the CSR.', + ) + serial_sequence = fields.Char( + help='Prefix for the generated cert\'s serial number.', + ) + cert_id = fields.Many2one( + string='Signed Cert', + comodel_name='clouder.cert.x509', + ) + + @api.multi + def action_sign(self): + self.ensure_one() + self.write({ + 'cert_id': self.cert_authority_id.sign( + cert_request=self.cert_request_id, + hosts=self.host_ids, + subject=self.subject, + serial_sequence=self.serial_sequence, + ) + }) + return True diff --git a/clouder_cert_authority/wizards/cert_authority_sign.xml b/clouder_cert_authority/wizards/cert_authority_sign.xml new file mode 100644 index 0000000..f36eec9 --- /dev/null +++ b/clouder_cert_authority/wizards/cert_authority_sign.xml @@ -0,0 +1,53 @@ + + + + + + + Cert Authority Sign Form + clouder.cert.authority.sign + +
+ +

+ +

+ + + + + + + + + + + + +
+
+
+
+
+
+ + + Sign A Cert + clouder.cert.authority + clouder.cert.authority.sign + form + form + client_action_multi + new + + +
diff --git a/requirements.txt b/requirements.txt index 0d68b60..978f55e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ apache-libcloud +cfssl==0.0.2b215 +cryptography erppeek paramiko