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 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 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, cn): namespace = ("user.%s." % self.namespace).encode("ascii") try: path, buf, cert, signed, expires = self.authority.get_signed(cn) except IOError: raise falcon.HTTPNotFound() else: for key in req.params: if not re.match("[a-z0-9_\.]+$", key): raise falcon.HTTPBadRequest("Invalid key %s" % key) valid = set() modified = False for key, value in req.params.items(): identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii") try: if getxattr(path, identifier).decode("utf-8") != value: modified = True except OSError: # no such attribute pass setxattr(path, identifier, value.encode("utf-8")) valid.add(identifier) for key in listxattr(path): if not key.startswith(namespace): continue if key not in valid: modified = True removexattr(path, key) if modified: push.publish("attribute-update", cn)
def on_post(self, req, resp): client_common_name = req.get_param("client", required=True) m = re.match("CN=(.+?),", client_common_name) # It's actually DN, resolve it to CN if m: client_common_name, = m.groups() path, buf, cert, signed, expires = self.authority.get_signed( client_common_name) # TODO: catch exceptions if req.get_param( "serial") and cert.serial_number != req.get_param_as_int( "serial" ): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied") now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" xattr.setxattr( path, "user.lease.outer_address", req.get_param("outer_address", required=True).encode("ascii")) xattr.setxattr( path, "user.lease.inner_address", req.get_param("inner_address", required=True).encode("ascii")) xattr.setxattr(path, "user.lease.last_seen", now) push.publish("lease-update", client_common_name) server_common_name = req.context.get("machine") path = os.path.join(config.SIGNED_DIR, server_common_name + ".pem") xattr.setxattr(path, "user.lease.outer_address", "") xattr.setxattr(path, "user.lease.inner_address", "%s" % req.context.get("remote_addr")) xattr.setxattr(path, "user.lease.last_seen", now) push.publish("lease-update", server_common_name)
def on_put(self, req, resp, identifier): from certidude import push args = req.get_param("value"), identifier self.sql_execute("tag_update.sql", *args) logger.debug(u"Tag %s updated, value set to %s", identifier, req.get_param("value")) push.publish("tag-updated", identifier)
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 on_delete(self, req, resp, cn, tag): path, buf, cert = authority.get_signed(cn) tags = set(getxattr(path, "user.xdg.tags").split(",")) tags.remove(tag) if not tags: removexattr(path, "user.xdg.tags") else: setxattr(path, "user.xdg.tags", ",".join(tags)) logger.debug(u"Tag %s removed for %s" % (tag, cn)) push.publish("tag-update", cn)
def on_delete(self, req, resp, cn, tag): path, buf, cert, signed, expires = self.authority.get_signed(cn) tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(",")) tags.remove(tag) if not tags: removexattr(path, "user.xdg.tags") else: setxattr(path, "user.xdg.tags", ",".join(tags)) logger.info("Tag %s removed for %s by %s" % (tag, cn, req.context.get("user"))) push.publish("tag-update", cn)
def on_post(self, req, resp, cn): path, buf, cert = authority.get_signed(cn) key, value = req.get_param("key", required=True), req.get_param("value", required=True) try: tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(",")) except IOError: tags = set() if key == "other": tags.add(value) else: tags.add("%s=%s" % (key,value)) setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8")) logger.debug(u"Tag %s=%s set for %s" % (key, value, cn)) push.publish("tag-update", cn)
def delete_request(common_name): # Validate CN if not re.match(RE_HOSTNAME, common_name): raise ValueError("Invalid common name") path, buf, csr = get_request(common_name) os.unlink(path) # Publish event at CA channel push.publish("request-deleted", common_name) # Write empty certificate to long-polling URL requests.delete(config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), headers={"User-Agent": "Certidude API"})
def delete_request(common_name): # Validate CN if not re.match(RE_HOSTNAME, common_name): raise ValueError("Invalid common name") path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") request = Request(open(path)) os.unlink(path) # Publish event at CA channel push.publish("request-deleted", request.common_name) # Write empty certificate to long-polling URL requests.delete(config.PUSH_PUBLISH % request.fingerprint(), headers={"User-Agent": "Certidude API"})
def delete_request(common_name, user="******"): # Validate CN if not re.match(const.RE_COMMON_NAME, common_name): raise ValueError("Invalid common name") path, buf, csr, submitted = get_request(common_name) os.unlink(path) logger.info("Rejected signing request %s by %s" % (common_name, user)) # Publish event at CA channel push.publish("request-deleted", common_name) # Write empty certificate to long-polling URL requests.delete(config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), headers={"User-Agent": "Certidude API"})
def on_put(self, req, resp, cn, tag): path, buf, cert = authority.get_signed(cn) value = req.get_param("value", required=True) try: tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(",")) except IOError: tags = set() try: tags.remove(tag) except KeyError: pass if "=" in tag: tags.add("%s=%s" % (tag.split("=")[0], value)) else: tags.add(value) setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8")) logger.debug(u"Tag %s set to %s for %s" % (tag, value, cn)) push.publish("tag-update", cn)
def on_post(self, req, resp, cn): try: path, buf, cert = authority.get_signed(cn) except IOError: raise falcon.HTTPNotFound() else: for key in req.params: if not re.match("[a-z0-9_\.]+$", key): raise falcon.HTTPBadRequest("Invalid key") valid = set() for key, value in req.params.items(): identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii") setxattr(path, identifier, value.encode("utf-8")) valid.add(identifier) for key in listxattr(path): if not key.startswith("user.%s." % self.namespace): continue if key not in valid: removexattr(path, key) push.publish("attribute-update", cn)
def on_post(self, req, resp): # TODO: verify signature common_name = req.get_param("client", required=True) path, buf, cert = authority.get_signed( common_name) # TODO: catch exceptions if req.get_param( "serial") and cert.serial_number != req.get_param_as_int( "serial" ): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied") xattr.setxattr( path, "user.lease.outer_address", req.get_param("outer_address", required=True).encode("ascii")) xattr.setxattr( path, "user.lease.inner_address", req.get_param("inner_address", required=True).encode("ascii")) xattr.setxattr( path, "user.lease.last_seen", datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z") push.publish("lease-update", common_name)
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 on_post(self, req, resp): from certidude import push args = req.get_param("cn"), req.get_param("key"), req.get_param("value") rowid = self.sql_execute("tag_insert.sql", *args) push.publish("tag-added", str(rowid)) logger.debug(u"Tag cn=%s, key=%s, value=%s added" % args)
class RequestListResource(object): @login_optional @whitelist_subnets(config.REQUEST_SUBNETS) @whitelist_content_types("application/pkcs10") def on_post(self, req, resp): """ Validate and parse certificate signing request, the RESTful way """ reasons = [] body = req.stream.read(req.content_length).encode("ascii") header, _, der_bytes = pem.unarmor(body) csr = CertificationRequest.load(der_bytes) common_name = csr["certification_request_info"]["subject"].native["common_name"] """ Handle domain computer automatic enrollment """ machine = req.context.get("machine") if machine: if config.MACHINE_ENROLLMENT_ALLOWED: if common_name != machine: raise falcon.HTTPBadRequest( "Bad request", "Common name %s differs from Kerberos credential %s!" % (common_name, machine)) # Automatic enroll with Kerberos machine cerdentials resp.set_header("Content-Type", "application/x-pem-file") cert, resp.body = authority._sign(csr, body, overwrite=True) logger.info(u"Automatically enrolled Kerberos authenticated machine %s from %s", machine, req.context.get("remote_addr")) return else: reasons.append("Machine enrollment not allowed") """ Attempt to renew certificate using currently valid key pair """ try: path, buf, cert = authority.get_signed(common_name) except EnvironmentError: pass # No currently valid certificate for this common name else: cert_pk = cert["tbs_certificate"]["subject_public_key_info"].native csr_pk = csr["certification_request_info"]["subject_pk_info"].native if cert_pk == csr_pk: # Same public key, assume renewal expires = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) renewal_header = req.get_header("X-Renewal-Signature") if not renewal_header: # No header supplied, redirect to signed API call resp.status = falcon.HTTP_SEE_OTHER resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name) return try: renewal_signature = b64decode(renewal_header) except TypeError, ValueError: logger.error(u"Renewal failed, bad signature supplied for %s", common_name) reasons.append("Renewal failed, bad signature supplied") else: try: asymmetric.rsa_pss_verify( asymmetric.load_certificate(cert), renewal_signature, buf + body, "sha512") except SignatureError: logger.error(u"Renewal failed, invalid signature supplied for %s", common_name) reasons.append("Renewal failed, invalid signature supplied") else: # At this point renewal signature was valid but we need to perform some extra checks if datetime.utcnow() > expires: logger.error(u"Renewal failed, current certificate for %s has expired", common_name) reasons.append("Renewal failed, current certificate expired") elif not config.CERTIFICATE_RENEWAL_ALLOWED: logger.error(u"Renewal requested for %s, but not allowed by authority settings", common_name) reasons.append("Renewal requested, but not allowed by authority settings") else: resp.set_header("Content-Type", "application/x-x509-user-cert") _, resp.body = authority._sign(csr, body, overwrite=True) logger.info(u"Renewed certificate for %s", common_name) return """ Process automatic signing if the IP address is whitelisted, autosigning was requested and certificate can be automatically signed """ if req.get_param_as_bool("autosign"): if not authority.server_flags(common_name): for subnet in config.AUTOSIGN_SUBNETS: if req.context.get("remote_addr") in subnet: try: resp.set_header("Content-Type", "application/x-pem-file") _, resp.body = authority._sign(csr, body) logger.info(u"Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr")) return except EnvironmentError: logger.info(u"Autosign for %s from %s failed, signed certificate already exists", common_name, req.context.get("remote_addr")) reasons.append("Autosign failed, signed certificate already exists") break else: reasons.append("Autosign failed, IP address not whitelisted") else: reasons.append("Autosign failed, only client certificates allowed to be signed automatically") # Attempt to save the request otherwise try: request_path, _, _ = authority.store_request(body, address=str(req.context.get("remote_addr"))) except errors.RequestExists: reasons.append("Same request already uploaded exists") # We should still redirect client to long poll URL below except errors.DuplicateCommonNameError: # TODO: Certificate renewal logger.warning(u"Rejected signing request with overlapping common name from %s", req.context.get("remote_addr")) raise falcon.HTTPConflict( "CSR with such CN already exists", "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") else: push.publish("request-submitted", common_name) # Wait the certificate to be signed if waiting is requested logger.info(u"Stored signing request %s from %s", common_name, req.context.get("remote_addr")) if req.get_param("wait"): # Redirect to nginx pub/sub url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest() click.echo("Redirecting to: %s" % url) resp.status = falcon.HTTP_SEE_OTHER resp.set_header("Location", url.encode("ascii")) logger.debug(u"Redirecting signing request from %s to %s", req.context.get("remote_addr"), url) else: # Request was accepted, but not processed resp.status = falcon.HTTP_202 resp.body = ". ".join(reasons)
def emit(self, record): from certidude.push import publish publish("log-entry", dict( created = datetime.utcfromtimestamp(record.created), message = record.msg % record.args, severity = record.levelname.lower()))
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, 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
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 on_delete(self, req, resp, identifier): from certidude import push self.sql_execute("tag_delete.sql", identifier) push.publish("tag-removed", identifier) logger.debug(u"Tag %s removed" % identifier)
def on_post(self, req, resp): """ Submit certificate signing request (CSR) in PEM format """ body = req.stream.read(req.content_length) # Normalize body, TODO: newlines if not body.endswith("\n"): body += "\n" csr = Request(body) if not csr.common_name: logger.warning(u"Rejected signing request without common name from %s", req.context.get("remote_addr")) raise falcon.HTTPBadRequest( "Bad request", "No common name specified!") machine = req.context.get("machine") if machine: if csr.common_name != machine: raise falcon.HTTPBadRequest( "Bad request", "Common name %s differs from Kerberos credential %s!" % (csr.common_name, machine)) if csr.signable: # Automatic enroll with Kerberos machine cerdentials resp.set_header("Content-Type", "application/x-x509-user-cert") resp.body = authority.sign(csr, overwrite=True).dump() return # Check if this request has been already signed and return corresponding certificte if it has been signed try: cert = authority.get_signed(csr.common_name) except EnvironmentError: pass else: if cert.pubkey == csr.pubkey: resp.status = falcon.HTTP_SEE_OTHER resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", csr.common_name) return # TODO: check for revoked certificates and return HTTP 410 Gone # Process automatic signing if the IP address is whitelisted, autosigning was requested and certificate can be automatically signed if req.get_param_as_bool("autosign") and csr.signable: for subnet in config.AUTOSIGN_SUBNETS: if req.context.get("remote_addr") in subnet: try: resp.set_header("Content-Type", "application/x-x509-user-cert") resp.body = authority.sign(csr).dump() return except EnvironmentError: # Certificate already exists, try to save the request pass break # Attempt to save the request otherwise try: csr = authority.store_request(body) except errors.RequestExists: # We should stil redirect client to long poll URL below pass except errors.DuplicateCommonNameError: # TODO: Certificate renewal logger.warning(u"Rejected signing request with overlapping common name from %s", req.context.get("remote_addr")) raise falcon.HTTPConflict( "CSR with such CN already exists", "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") else: push.publish("request-submitted", csr.common_name) # Wait the certificate to be signed if waiting is requested if req.get_param("wait"): # Redirect to nginx pub/sub url = config.PUSH_LONG_POLL % csr.fingerprint() click.echo("Redirecting to: %s" % url) resp.status = falcon.HTTP_SEE_OTHER resp.set_header("Location", url.encode("ascii")) logger.debug(u"Redirecting signing request from %s to %s", req.context.get("remote_addr"), url) else: # Request was accepted, but not processed resp.status = falcon.HTTP_202 logger.info(u"Signing request from %s stored", req.context.get("remote_addr"))
def on_post(self, req, resp): """ Validate and parse certificate signing request, the RESTful way """ reasons = [] body = req.stream.read(req.content_length) try: header, _, der_bytes = pem.unarmor(body) csr = CertificationRequest.load(der_bytes) except ValueError: logger.info( "Malformed certificate signing request submission from %s blocked", req.context.get("remote_addr")) raise falcon.HTTPBadRequest( "Bad request", "Malformed certificate signing request") else: req_public_key = asymmetric.load_public_key( csr["certification_request_info"]["subject_pk_info"]) if self.authority.public_key.algorithm != req_public_key.algorithm: logger.info( "Attempt to submit %s based request from %s blocked, only %s allowed" % (req_public_key.algorithm.upper(), req.context.get("remote_addr"), self.authority.public_key.algorithm.upper())) raise falcon.HTTPBadRequest( "Bad request", "Incompatible asymmetric key algorithms") common_name = csr["certification_request_info"]["subject"].native[ "common_name"] """ Determine whether autosign is allowed to overwrite already issued certificates automatically """ overwrite_allowed = False for subnet in config.OVERWRITE_SUBNETS: if req.context.get("remote_addr") in subnet: overwrite_allowed = True break """ Handle domain computer automatic enrollment """ machine = req.context.get("machine") if machine: reasons.append("machine enrollment not allowed from %s" % req.context.get("remote_addr")) for subnet in config.MACHINE_ENROLLMENT_SUBNETS: if req.context.get("remote_addr") in subnet: if common_name != machine: raise falcon.HTTPBadRequest( "Bad request", "Common name %s differs from Kerberos credential %s!" % (common_name, machine)) # Automatic enroll with Kerberos machine cerdentials resp.set_header("Content-Type", "application/x-pem-file") cert, resp.body = self.authority._sign( csr, body, profile=config.PROFILES["rw"], overwrite=overwrite_allowed) logger.info( "Automatically enrolled Kerberos authenticated machine %s from %s", machine, req.context.get("remote_addr")) return """ Attempt to renew certificate using currently valid key pair """ try: path, buf, cert, signed, expires = self.authority.get_signed( common_name) except EnvironmentError: pass # No currently valid certificate for this common name else: cert_pk = cert["tbs_certificate"]["subject_public_key_info"].native csr_pk = csr["certification_request_info"][ "subject_pk_info"].native # Same public key if cert_pk == csr_pk: buf = req.get_header("X-SSL-CERT") if buf: # Used mutually authenticated TLS handshake, assume renewal header, _, der_bytes = pem.unarmor( buf.replace("\t", "\n").replace("\n\n", "\n").encode("ascii")) handshake_cert = x509.Certificate.load(der_bytes) if handshake_cert.native == cert.native: for subnet in config.RENEWAL_SUBNETS: if req.context.get("remote_addr") in subnet: resp.set_header( "Content-Type", "application/x-x509-user-cert") setxattr(path, "user.revocation.reason", "superseded") _, resp.body = self.authority._sign( csr, body, overwrite=True, profile=SignatureProfile.from_cert(cert)) logger.info( "Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr")) return reasons.append("renewal failed") else: # No renewal requested, redirect to signed API call resp.status = falcon.HTTP_SEE_OTHER resp.location = os.path.join( os.path.dirname(req.relative_uri), "signed", common_name) return """ Process automatic signing if the IP address is whitelisted, autosigning was requested and certificate can be automatically signed """ if req.get_param_as_bool("autosign"): for subnet in config.AUTOSIGN_SUBNETS: if req.context.get("remote_addr") in subnet: try: resp.set_header("Content-Type", "application/x-pem-file") _, resp.body = self.authority._sign( csr, body, overwrite=overwrite_allowed, profile=config.PROFILES["rw"]) logger.info( "Signed %s as %s is whitelisted for autosign", common_name, req.context.get("remote_addr")) return except EnvironmentError: logger.info( "Autosign for %s from %s failed, signed certificate already exists", common_name, req.context.get("remote_addr")) reasons.append( "autosign failed, signed certificate already exists" ) break else: reasons.append("IP address not whitelisted for autosign") else: reasons.append("autosign not requested") # Attempt to save the request otherwise try: request_path, _, _ = self.authority.store_request( body, address=str(req.context.get("remote_addr"))) except errors.RequestExists: reasons.append("same request already uploaded exists") # We should still redirect client to long poll URL below except errors.DuplicateCommonNameError: # TODO: Certificate renewal logger.warning( "rejected signing request with overlapping common name from %s", req.context.get("remote_addr")) raise falcon.HTTPConflict( "CSR with such CN already exists", "Will not overwrite existing certificate signing request, explicitly delete CSR and try again" ) else: push.publish("request-submitted", common_name) # Wait the certificate to be signed if waiting is requested logger.info("Signing request %s from %s put on hold, %s", common_name, req.context.get("remote_addr"), ", ".join(reasons)) if req.get_param("wait"): # Redirect to nginx pub/sub url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest() click.echo("Redirecting to: %s" % url) resp.status = falcon.HTTP_SEE_OTHER resp.set_header("Location", url) else: # Request was accepted, but not processed resp.status = falcon.HTTP_202 resp.body = ". ".join(reasons) if req.client_accepts("application/json"): resp.body = json.dumps( { "title": "Accepted", "description": resp.body }, cls=MyEncoder)