def revoke(common_name, reason, user="******"): """ Revoke valid certificate """ signed_path, buf, cert, signed, expires = get_signed(common_name) if reason not in ("key_compromise", "ca_compromise", "affiliation_changed", "superseded", "cessation_of_operation", "certificate_hold", "remove_from_crl", "privilege_withdrawn"): raise ValueError("Invalid revocation reason %s" % reason) setxattr(signed_path, "user.revocation.reason", reason) revoked_path = os.path.join(config.REVOKED_DIR, "%040x.pem" % cert.serial_number) logger.info("Revoked certificate %s by %s", common_name, user) os.unlink( os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number)) os.rename(signed_path, revoked_path) push.publish("certificate-revoked", common_name) attach_cert = buf, "application/x-pem-file", common_name + ".crt" mailer.send("certificate-revoked.md", attachments=(attach_cert, ), serial_hex="%x" % cert.serial_number, common_name=common_name) return revoked_path
def issue(self, issuer, subject, subject_mail=None): # Expand variables subject_username = subject.name if not subject_mail: subject_mail = subject.mail # Generate token token = ''.join(random.choice(string.ascii_lowercase + string.ascii_uppercase + string.digits) for _ in range(32)) token_created = datetime.utcnow() token_expires = token_created + config.TOKEN_LIFETIME self.sql_execute("token_issue.sql", token_created, token_expires, token, issuer.name if issuer else None, subject_username, subject_mail, "rw") # Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata try: with open("/etc/timezone") as fh: token_timezone = fh.read().strip() except EnvironmentError: token_timezone = None router = sorted([j[0] for j in authority.list_signed( common_name=config.SERVICE_ROUTERS)])[0] protocols = ",".join(config.SERVICE_PROTOCOLS) url = config.TOKEN_URL % locals() context = globals() context.update(locals()) mailer.send("token.md", to=subject_mail, **context) return token
def revoke(common_name): """ Revoke valid certificate """ signed_path, buf, cert = get_signed(common_name) revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial_number) os.rename(signed_path, revoked_path) os.unlink( os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number)) push.publish("certificate-revoked", common_name) # Publish CRL for long polls url = config.LONG_POLL_PUBLISH % "crl" click.echo("Publishing CRL at %s ..." % url) requests.post(url, data=export_crl(), headers={ "User-Agent": "Certidude API", "Content-Type": "application/x-pem-file" }) attach_cert = buf, "application/x-pem-file", common_name + ".crt" mailer.send("certificate-revoked.md", attachments=(attach_cert, ), serial_hex="%x" % cert.serial_number, common_name=common_name) return revoked_path
def store_request(buf, overwrite=False): """ Store CSR for later processing """ if not buf: return # No certificate supplied csr = x509.load_pem_x509_csr(buf, backend=default_backend()) for name in csr.subject: if name.oid == NameOID.COMMON_NAME: common_name = name.value break else: raise ValueError("No common name in %s" % csr.subject) request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") if not re.match(RE_HOSTNAME, common_name): raise ValueError("Invalid common name") # If there is cert, check if it's the same if os.path.exists(request_path): if open(request_path).read() == buf: raise errors.RequestExists("Request already exists") else: raise errors.DuplicateCommonNameError("Another request with same common name already exists") else: with open(request_path + ".part", "w") as fh: fh.write(buf) os.rename(request_path + ".part", request_path) req = Request(open(request_path)) mailer.send("request-stored.md", attachments=(req,), request=req) return req
def wrapped(csr, *args, **kwargs): cert = func(csr, *args, **kwargs) assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert)) if cert.given_name and cert.surname and cert.email_address: recipient = "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address) elif cert.email_address: recipient = cert.email_address else: recipient = None mailer.send( "certificate-signed.md", to=recipient, attachments=(cert,), certificate=cert) if config.PUSH_PUBLISH: url = config.PUSH_PUBLISH % csr.fingerprint() click.echo("Publishing certificate at %s ..." % url) requests.post(url, data=cert.dump(), headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) # For deleting request in the web view, use pubkey modulo push.publish("request-signed", cert.common_name) return cert
def on_post(self, req, resp): # Generate token issuer = req.context.get("user") username = req.get_param("user", required=True) user = User.objects.get(username) timestamp = int(time()) csum = hashlib.sha256() csum.update(config.TOKEN_SECRET) csum.update(username) csum.update(str(timestamp)) args = "u=%s&t=%d&c=%s" % (username, timestamp, csum.hexdigest()) # Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata token_created = datetime.fromtimestamp(timestamp) token_expires = datetime.fromtimestamp(timestamp + config.TOKEN_LIFETIME) try: with open("/etc/timezone") as fh: token_timezone = fh.read().strip() except EnvironmentError: token_timezone = None context = globals() context.update(locals()) mailer.send("token.md", to=user, **context) resp.body = args
def revoke_certificate(common_name): """ Revoke valid certificate """ cert = get_signed(common_name) revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number) os.rename(cert.path, revoked_filename) push.publish("certificate-revoked", cert.common_name) mailer.send("certificate-revoked.md", attachments=(cert,), certificate=cert)
def store_request(buf, overwrite=False, address="", user=""): """ Store CSR for later processing """ if not buf: raise ValueError("No signing request supplied") if pem.detect(buf): header, _, der_bytes = pem.unarmor(buf) csr = CertificationRequest.load(der_bytes) else: csr = CertificationRequest.load(buf) buf = pem_armor_csr(csr) common_name = csr["certification_request_info"]["subject"].native[ "common_name"] if not re.match(const.RE_COMMON_NAME, common_name): raise ValueError("Invalid common name %s" % repr(common_name)) request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") # If there is cert, check if it's the same if os.path.exists(request_path) and not overwrite: if open(request_path, "rb").read() == buf: raise errors.RequestExists("Request already exists") else: raise errors.DuplicateCommonNameError( "Another request with same common name already exists") else: with open(request_path + ".part", "wb") as fh: fh.write(buf) os.rename(request_path + ".part", request_path) attach_csr = buf, "application/x-pem-file", common_name + ".csr" mailer.send("request-stored.md", attachments=(attach_csr, ), common_name=common_name) setxattr(request_path, "user.request.address", address) setxattr(request_path, "user.request.user", user) try: hostname, aliaslist, ipaddrlist = socket.gethostbyaddr(address) except (socket.herror, OSError): # Failed to resolve hostname or resolved to multiple pass else: setxattr(request_path, "user.request.hostname", hostname) return request_path, csr, common_name
def on_post(self, req, resp): # Generate token issuer = req.context.get("user") username = req.get_param("username") secondary = req.get_param("mail") if username: # Otherwise try to look up user so we can derive their e-mail address user = User.objects.get(username) else: # If no username is specified, assume it's intended for someone outside domain username = "******" % hashlib.sha256( secondary.encode("ascii")).hexdigest()[-8:] if not secondary: raise timestamp = int(time()) csum = hashlib.sha256() csum.update(config.TOKEN_SECRET) csum.update(username.encode("ascii")) csum.update(str(timestamp).encode("ascii")) args = "u=%s&t=%d&c=%s&i=%s" % (username, timestamp, csum.hexdigest(), issuer.name) # Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata token_created = datetime.fromtimestamp(timestamp) token_expires = datetime.fromtimestamp(timestamp + config.TOKEN_LIFETIME) try: with open("/etc/timezone") as fh: token_timezone = fh.read().strip() except EnvironmentError: token_timezone = None url = "%s#%s" % (config.TOKEN_URL, args) context = globals() context.update(locals()) mailer.send("token.md", to=user, **context) return { "token": args, "url": url, }
def revoke(common_name, reason): """ Revoke valid certificate """ signed_path, buf, cert, signed, expires = get_signed(common_name) if reason not in ("key_compromise", "ca_compromise", "affiliation_changed", "superseded", "cessation_of_operation", "certificate_hold", "remove_from_crl", "privilege_withdrawn"): raise ValueError("Invalid revocation reason %s" % reason) setxattr(signed_path, "user.revocation.reason", reason) revoked_path = os.path.join(config.REVOKED_DIR, "%040x.pem" % cert.serial_number) os.unlink( os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number)) os.rename(signed_path, revoked_path) push.publish("certificate-revoked", common_name) # Publish CRL for long polls url = config.LONG_POLL_PUBLISH % "crl" click.echo("Publishing CRL at %s ..." % url) requests.post(url, data=export_crl(), headers={ "User-Agent": "Certidude API", "Content-Type": "application/x-pem-file" }) attach_cert = buf, "application/x-pem-file", common_name + ".crt" mailer.send("certificate-revoked.md", attachments=(attach_cert, ), serial_hex="%040x" % cert.serial_number, common_name=common_name) return revoked_path
def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False, signer=None): # TODO: CRLDistributionPoints, OCSP URL, Certificate URL assert buf.startswith(b"-----BEGIN ") assert isinstance(csr, CertificationRequest) csr_pubkey = asymmetric.load_public_key( csr["certification_request_info"]["subject_pk_info"]) common_name = csr["certification_request_info"]["subject"].native[ "common_name"] cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name) renew = False attachments = [ (buf, "application/x-pem-file", common_name + ".csr"), ] revoked_path = None overwritten = False # Move existing certificate if necessary if os.path.exists(cert_path): with open(cert_path, "rb") as fh: prev_buf = fh.read() header, _, der_bytes = pem.unarmor(prev_buf) prev = x509.Certificate.load(der_bytes) # TODO: assert validity here again? renew = \ asymmetric.load_public_key(prev["tbs_certificate"]["subject_public_key_info"]) == \ csr_pubkey # BUGBUG: is this enough? if overwrite: # TODO: is this the best approach? # TODO: why didn't unittest detect bugs here? prev_serial_hex = "%x" % prev.serial_number revoked_path = os.path.join(config.REVOKED_DIR, "%040x.pem" % prev.serial_number) os.rename(cert_path, revoked_path) attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")] overwritten = True else: raise FileExistsError("Will not overwrite existing certificate") builder = CertificateBuilder( cn_to_dn(common_name, const.FQDN, o=certificate["tbs_certificate"]["subject"].native.get( "organization_name"), ou=profile.ou), csr_pubkey) builder.serial_number = generate_serial() now = datetime.utcnow() builder.begin_date = now - const.CLOCK_SKEW_TOLERANCE builder.end_date = now + timedelta(days=profile.lifetime) builder.issuer = certificate builder.ca = profile.ca builder.key_usage = profile.key_usage builder.extended_key_usage = profile.extended_key_usage builder.subject_alt_domains = [common_name] builder.ocsp_url = profile.responder_url builder.crl_url = profile.revoked_url end_entity_cert = builder.build(private_key) end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) with open(cert_path + ".part", "wb") as fh: fh.write(end_entity_cert_buf) os.rename(cert_path + ".part", cert_path) attachments.append( (end_entity_cert_buf, "application/x-pem-file", common_name + ".crt")) cert_serial_hex = "%x" % end_entity_cert.serial_number # Create symlink link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % end_entity_cert.serial_number) assert not os.path.exists( link_name ), "Certificate with same serial number already exists: %s" % link_name os.symlink("../%s.pem" % common_name, link_name) # Copy filesystem attributes to newly signed certificate if revoked_path: for key in listxattr(revoked_path): if not key.startswith(b"user."): continue setxattr(cert_path, key, getxattr(revoked_path, key)) # Attach signer username if signer: setxattr(cert_path, "user.signature.username", signer) if not skip_notify: # Send mail if renew: # Same keypair mailer.send("certificate-renewed.md", **locals()) else: # New keypair mailer.send("certificate-signed.md", **locals()) if not skip_push: url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() click.echo("Publishing certificate at %s ..." % url) requests.post(url, data=end_entity_cert_buf, headers={ "User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert" }) if renew: # TODO: certificate-renewed event push.publish("certificate-revoked", common_name) push.publish("request-signed", common_name) else: push.publish("request-signed", common_name) return end_entity_cert, end_entity_cert_buf
def _sign(csr, buf, overwrite=False): # TODO: CRLDistributionPoints, OCSP URL, Certificate URL assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n") assert isinstance(csr, CertificationRequest) csr_pubkey = asymmetric.load_public_key( csr["certification_request_info"]["subject_pk_info"]) common_name = csr["certification_request_info"]["subject"].native[ "common_name"] cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name) renew = False attachments = [ (buf, "application/x-pem-file", common_name + ".csr"), ] revoked_path = None overwritten = False # Move existing certificate if necessary if os.path.exists(cert_path): with open(cert_path) as fh: prev_buf = fh.read() header, _, der_bytes = pem.unarmor(prev_buf) prev = x509.Certificate.load(der_bytes) # TODO: assert validity here again? renew = \ asymmetric.load_public_key(prev["tbs_certificate"]["subject_public_key_info"]) == \ csr_pubkey # BUGBUG: is this enough? if overwrite: # TODO: is this the best approach? prev_serial_hex = "%x" % prev.serial_number revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex) os.rename(cert_path, revoked_path) attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")] overwritten = True else: raise EnvironmentError("Will not overwrite existing certificate") # Sign via signer process builder = CertificateBuilder({u'common_name': common_name}, csr_pubkey) builder.serial_number = random.randint( 0x1000000000000000000000000000000000000000, 0xffffffffffffffffffffffffffffffffffffffff) now = datetime.utcnow() builder.begin_date = now - timedelta(minutes=5) builder.end_date = now + timedelta( days=config.SERVER_CERTIFICATE_LIFETIME if server_flags(common_name) else config.CLIENT_CERTIFICATE_LIFETIME) builder.issuer = certificate builder.ca = False builder.key_usage = set([u"digital_signature", u"key_encipherment"]) # OpenVPN uses CN while StrongSwan uses SAN if server_flags(common_name): builder.subject_alt_domains = [common_name] builder.extended_key_usage = set( [u"server_auth", u"1.3.6.1.5.5.8.2.2", u"client_auth"]) else: builder.extended_key_usage = set([u"client_auth"]) end_entity_cert = builder.build(private_key) end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) with open(cert_path + ".part", "wb") as fh: fh.write(end_entity_cert_buf) os.rename(cert_path + ".part", cert_path) attachments.append( (end_entity_cert_buf, "application/x-pem-file", common_name + ".crt")) cert_serial_hex = "%x" % end_entity_cert.serial_number # Create symlink link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % end_entity_cert.serial_number) assert not os.path.exists( link_name ), "Certificate with same serial number already exists: %s" % link_name os.symlink("../%s.pem" % common_name, link_name) # Copy filesystem attributes to newly signed certificate if revoked_path: for key in listxattr(revoked_path): if not key.startswith("user."): continue setxattr(cert_path, key, getxattr(revoked_path, key)) # Send mail if renew: # Same keypair mailer.send("certificate-renewed.md", **locals()) else: # New keypair mailer.send("certificate-signed.md", **locals()) url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() click.echo("Publishing certificate at %s ..." % url) requests.post(url, data=end_entity_cert_buf, headers={ "User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert" }) push.publish("request-signed", common_name) return end_entity_cert, end_entity_cert_buf
def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None): # TODO: CRLDistributionPoints, OCSP URL, Certificate URL if profile not in config.PROFILES: raise ValueError("Invalid profile supplied '%s'" % profile) assert buf.startswith(b"-----BEGIN ") assert isinstance(csr, CertificationRequest) csr_pubkey = asymmetric.load_public_key( csr["certification_request_info"]["subject_pk_info"]) common_name = csr["certification_request_info"]["subject"].native[ "common_name"] cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name) renew = False attachments = [ (buf, "application/x-pem-file", common_name + ".csr"), ] revoked_path = None overwritten = False # Move existing certificate if necessary if os.path.exists(cert_path): with open(cert_path, "rb") as fh: prev_buf = fh.read() header, _, der_bytes = pem.unarmor(prev_buf) prev = x509.Certificate.load(der_bytes) # TODO: assert validity here again? renew = \ asymmetric.load_public_key(prev["tbs_certificate"]["subject_public_key_info"]) == \ csr_pubkey # BUGBUG: is this enough? if overwrite: # TODO: is this the best approach? prev_serial_hex = "%x" % prev.serial_number revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex) os.rename(cert_path, revoked_path) attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")] overwritten = True else: raise FileExistsError("Will not overwrite existing certificate") # Sign via signer process dn = {u'common_name': common_name} profile_server_flags, lifetime, dn[ "organizational_unit_name"], _ = config.PROFILES[profile] lifetime = int(lifetime) builder = CertificateBuilder(dn, csr_pubkey) builder.serial_number = random.randint( 0x1000000000000000000000000000000000000000, 0x7fffffffffffffffffffffffffffffffffffffff) now = datetime.utcnow() builder.begin_date = now - timedelta(minutes=5) builder.end_date = now + timedelta(days=lifetime) builder.issuer = certificate builder.ca = False builder.key_usage = set(["digital_signature", "key_encipherment"]) # If we have FQDN and profile suggests server flags, enable them if server_flags(common_name) and profile_server_flags: builder.subject_alt_domains = [ common_name ] # OpenVPN uses CN while StrongSwan uses SAN to match hostname of the server builder.extended_key_usage = set( ["server_auth", "1.3.6.1.5.5.8.2.2", "client_auth"]) else: builder.subject_alt_domains = [common_name ] # iOS demands SAN also for clients builder.extended_key_usage = set(["client_auth"]) end_entity_cert = builder.build(private_key) end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) with open(cert_path + ".part", "wb") as fh: fh.write(end_entity_cert_buf) os.rename(cert_path + ".part", cert_path) attachments.append( (end_entity_cert_buf, "application/x-pem-file", common_name + ".crt")) cert_serial_hex = "%x" % end_entity_cert.serial_number # Create symlink link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % end_entity_cert.serial_number) assert not os.path.exists( link_name ), "Certificate with same serial number already exists: %s" % link_name os.symlink("../%s.pem" % common_name, link_name) # Copy filesystem attributes to newly signed certificate if revoked_path: for key in listxattr(revoked_path): if not key.startswith(b"user."): continue setxattr(cert_path, key, getxattr(revoked_path, key)) # Attach signer username if signer: setxattr(cert_path, "user.signature.username", signer) if not skip_notify: # Send mail if renew: # Same keypair mailer.send("certificate-renewed.md", **locals()) else: # New keypair mailer.send("certificate-signed.md", **locals()) if not skip_push: url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() click.echo("Publishing certificate at %s ..." % url) requests.post(url, data=end_entity_cert_buf, headers={ "User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert" }) push.publish("request-signed", common_name) return end_entity_cert, end_entity_cert_buf