""" .. module: lemur.certificates.models :platform: Unix :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ import os import datetime from cryptography import x509 from cryptography.hazmat.backends import default_backend from flask import current_app from sqlalchemy.orm import relationship from sqlalchemy import Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean from sqlalchemy_utils import EncryptedType from lemur.database import db from lemur.domains.models import Domain from lemur.users import service as user_service from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE, NONSTANDARD_NAMING_TEMPLATE from lemur.models import certificate_associations, certificate_account_associations def cert_get_cn(cert): """ Attempts to get a sane common name from a given certificate. :param cert: :return: Common name or None """ try: return cert.subject.get_attributes_for_oid( x509.OID_COMMON_NAME )[0].value.strip() except Exception as e: current_app.logger.error("Unable to get CN! {0}".format(e)) def cert_get_domains(cert): """ Attempts to get an domains listed in a certificate. If 'subjectAltName' extension is not available we simply return the common name. :param cert: :return: List of domains """ domains = [] try: ext = cert.extensions.get_extension_for_oid(x509.OID_SUBJECT_ALTERNATIVE_NAME) entries = ext.get_values_for(x509.DNSName) for entry in entries: domains.append(entry.split(":")[1].strip(", ")) except Exception as e: current_app.logger.warning("Failed to get SubjectAltName: {0}".format(e)) domains.append(cert_get_cn(cert)) return domains def cert_get_serial(cert): """ Fetch the serial number from the certificate. :param cert: :return: serial number """ return cert.serial def cert_is_san(cert): """ Determines if a given certificate is a SAN certificate. SAN certificates are simply certificates that cover multiple domains. :param cert: :return: Bool """ domains = cert_get_domains(cert) if len(domains) > 1: return True return False def cert_is_wildcard(cert): """ Determines if certificate is a wildcard certificate. :param cert: :return: Bool """ domains = cert_get_domains(cert) if len(domains) == 1 and domains[0][0:1] == "*": return True return False def cert_get_bitstrength(cert): """ Calculates a certificates public key bit length. :param cert: :return: Integer """ return cert.public_key().key_size * 8 def cert_get_issuer(cert): """ Gets a sane issuer from a given certificate. :param cert: :return: Issuer """ try: return cert.subject.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value except Exception as e: current_app.logger.error("Unable to get issuer! {0}".format(e)) def cert_is_internal(cert): """ Uses an internal resource in order to determine if a given certificate was issued by an 'internal' certificate authority. :param cert: :return: Bool """ if cert_get_issuer(cert) in current_app.config.get('INTERNAL_CA', []): return True return False def cert_get_not_before(cert): """ Gets the naive datetime of the certificates 'not_before' field. This field denotes the first date in time which the given certificate is valid. :param cert: :return: Datetime """ return cert.not_valid_before def cert_get_not_after(cert): """ Gets the naive datetime of the certificates 'not_after' field. This field denotes the last date in time which the given certificate is valid. :param cert: :return: Datetime """ return cert.not_valid_after def get_name_from_arn(arn): """ Extract the certificate name from an arn. :param arn: IAM SSL arn :return: name of the certificate as uploaded to AWS """ return arn.split("/", 1)[1] def get_account_number(arn): """ Extract the account number from an arn. :param arn: IAM SSL arn :return: account number associated with ARN """ return arn.split(":")[4] class Certificate(db.Model): __tablename__ = 'certificates' id = Column(Integer, primary_key=True) owner = Column(String(128)) body = Column(Text()) private_key = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY'))) challenge = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY'))) csr_config = Column(Text()) status = Column(String(128)) deleted = Column(Boolean, index=True) name = Column(String(128)) chain = Column(Text()) bits = Column(Integer()) issuer = Column(String(128)) serial = Column(String(128)) cn = Column(String(128)) description = Column(String(1024)) active = Column(Boolean, default=True) san = Column(String(1024)) not_before = Column(DateTime) not_after = Column(DateTime) date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) user_id = Column(Integer, ForeignKey('users.id')) authority_id = Column(Integer, ForeignKey('authorities.id')) accounts = relationship("Account", secondary=certificate_account_associations, backref='certificate') domains = relationship("Domain", secondary=certificate_associations, backref="certificate") elb_listeners = relationship("Listener", lazy='dynamic', backref='certificate') def __init__(self, body, private_key=None, challenge=None, chain=None, csr_config=None): self.body = body # We encrypt the private_key on creation self.private_key = private_key self.chain = chain self.csr_config = csr_config self.challenge = challenge cert = x509.load_pem_x509_certificate(str(self.body), default_backend()) self.bits = cert_get_bitstrength(cert) self.issuer = cert_get_issuer(cert) self.serial = cert_get_serial(cert) self.cn = cert_get_cn(cert) self.san = cert_is_san(cert) self.not_before = cert_get_not_before(cert) self.not_after = cert_get_not_after(cert) self.name = self.create_name for domain in cert_get_domains(cert): self.domains.append(Domain(name=domain)) @property def create_name(self): """ Create a name for our certificate. A naming standard is based on a series of templates. The name includes useful information such as Common Name, Validation dates, and Issuer. :rtype : str :return: """ # aws doesn't allow special chars if self.cn: subject = self.cn.replace('*', "WILDCARD") if self.san: t = SAN_NAMING_TEMPLATE else: t = DEFAULT_NAMING_TEMPLATE temp = t.format( subject=subject, issuer=self.issuer, not_before=self.not_before.strftime('%Y%m%d'), not_after=self.not_after.strftime('%Y%m%d') ) else: t = NONSTANDARD_NAMING_TEMPLATE temp = t.format( issuer=self.issuer, not_before=self.not_before.strftime('%Y%m%d'), not_after=self.not_after.strftime('%Y%m%d') ) return temp @property def is_expired(self): if self.not_after < datetime.datetime.now(): return True @property def is_unused(self): if self.elb_listeners.count() == 0: return True @property def is_revoked(self): # we might not yet know the condition of the cert if self.status: if 'revoked' in self.status: return True def get_arn(self, account_number): """ Generate a valid AWS IAM arn :rtype : str :param account_number: :return: """ return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name) def as_dict(self): return {c.name: getattr(self, c.name) for c in self.__table__.columns} def serialize(self): blob = self.as_dict() # TODO this should be done with relationships user = user_service.get(self.user_id) if user: blob['creator'] = user.username return blob