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 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 certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None): """ Exchange CSR for certificate using Certidude HTTP API server """ # Set up URL-s request_params = set() if autosign: request_params.add("autosign=yes") if wait: request_params.add("wait=forever") if not url.endswith("/"): url = url + "/" authority_url = url + "certificate" request_url = url + "request" if request_params: request_url = request_url + "?" + "&".join(request_params) if os.path.exists(authority_path): click.echo("Found CA certificate in: %s" % authority_path) else: if authority_url: click.echo("Attempting to fetch CA certificate from %s" % authority_url) try: with urllib.request.urlopen(authority_url) as fh: buf = fh.read() try: cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf) except crypto.Error: raise ValueError("Failed to parse PEM: %s" % buf) with open(authority_path + ".part", "wb") as oh: oh.write(buf) click.echo("Writing CA certificate to: %s" % authority_path) os.rename(authority_path + ".part", authority_path) except urllib.error.HTTPError as e: click.echo("Failed to fetch CA certificate, server responded with: %d %s" % (e.code, e.reason), err=True) return 1 else: raise FileNotFoundError("CA certificate not found and no URL specified") try: certificate = Certificate(open(certificate_path)) click.echo("Found certificate: %s" % certificate_path) except FileNotFoundError: try: request = Request(open(request_path)) click.echo("Found signing request: %s" % request_path) except FileNotFoundError: # Construct private key click.echo("Generating 4096-bit RSA key...") key = crypto.PKey() key.generate_key(crypto.TYPE_RSA, 4096) # Dump private key os.umask(0o077) with open(key_path + ".part", "wb") as fh: fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) # Construct CSR csr = crypto.X509Req() csr.set_pubkey(key) request = Request(csr) # Set subject attributes request.common_name = common_name if given_name: request.given_name = given_name if surname: request.surname = surname if org_unit: request.organizational_unit = org_unit # Set extensions extensions = [] if key_usage: extensions.append(("keyUsage", key_usage, True)) if extended_key_usage: extensions.append(("extendedKeyUsage", extended_key_usage, True)) if email_address: extensions.append(("subjectAltName", "email:" + email_address, False)) request.set_extensions(extensions) # Dump CSR os.umask(0o022) with open(request_path + ".part", "w") as fh: fh.write(request.dump()) click.echo("Writing private key to: %s" % key_path) os.rename(key_path + ".part", key_path) click.echo("Writing certificate signing request to: %s" % request_path) os.rename(request_path + ".part", request_path) with open(request_path, "rb") as fh: buf = fh.read() submission = urllib.request.Request(request_url, buf) submission.add_header("User-Agent", "Certidude") submission.add_header("Content-Type", "application/pkcs10") click.echo("Submitting to %s, waiting for response..." % request_url) try: response = urllib.request.urlopen(submission) buf = response.read() cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf) except crypto.Error: raise ValueError("Failed to parse PEM: %s" % buf) except urllib.error.HTTPError as e: if e.code == 409: click.echo("Different signing request with same CN is already present on server, server refuses to overwrite", err=True) return 2 else: click.echo("Failed to fetch certificate, server responded with: %d %s" % (e.code, e.reason), err=True) return 3 else: if response.code == 202: click.echo("Server stored the request for processing (202 Accepted), but waiting was not requested, hence quitting for now", err=True) return 254 os.umask(0o022) with open(certificate_path + ".part", "wb") as gh: gh.write(buf) click.echo("Writing certificate to: %s" % certificate_path) os.rename(certificate_path + ".part", certificate_path)
def on_post(self, req, resp, ca): """ Submit certificate signing request (CSR) in PEM format """ if req.get_header("Content-Type") != "application/pkcs10": raise falcon.HTTPUnsupportedMediaType( "This API call accepts only application/pkcs10 content type") body = req.stream.read(req.content_length) csr = Request(body) # Check if this request has been already signed and return corresponding certificte if it has been signed try: cert_buf = ca.get_certificate(csr.common_name) except FileNotFoundError: pass else: cert = Certificate(cert_buf) if cert.pubkey == csr.pubkey: resp.status = falcon.HTTP_FOUND 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 and autosigning was requested if ca.autosign_allowed(req.env["REMOTE_ADDR"]) and req.get_param("autosign"): try: resp.append_header("Content-Type", "application/x-x509-user-cert") resp.body = ca.sign(csr).dump() return except FileExistsError: # Certificate already exists, try to save the request pass # Attempt to save the request otherwise try: request = ca.store_request(body) except FileExistsError: raise falcon.HTTPConflict( "CSR with such CN already exists", "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") # Wait the certificate to be signed if waiting is requested if req.get_param("wait"): url_template = os.getenv("CERTIDUDE_EVENT_SUBSCRIBE") if url_template: # Redirect to nginx pub/sub url = url_template % dict(channel=request.fingerprint()) click.echo("Redirecting to: %s" % url) resp.status = falcon.HTTP_FOUND resp.append_header("Location", url) else: click.echo("Using dummy streaming mode, please switch to nginx in production!", err=True) # Dummy streaming mode while True: sleep(1) if not ca.request_exists(csr.common_name): resp.append_header("Content-Type", "application/x-x509-user-cert") resp.status = falcon.HTTP_201 # Certificate was created resp.body = ca.get_certificate(csr.common_name) break else: # Request was accepted, but not processed resp.status = falcon.HTTP_202
def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None): """ Exchange CSR for certificate using Certidude HTTP API server """ # Set up URL-s request_params = set() if autosign: request_params.add("autosign=yes") if wait: request_params.add("wait=forever") if not url.endswith("/"): url = url + "/" authority_url = url + "certificate" request_url = url + "request" if request_params: request_url = request_url + "?" + "&".join(request_params) if os.path.exists(authority_path): click.echo("Found CA certificate in: %s" % authority_path) else: if authority_url: click.echo("Attempting to fetch CA certificate from %s" % authority_url) try: with urllib.request.urlopen(authority_url) as fh: buf = fh.read() try: cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf) except crypto.Error: raise ValueError("Failed to parse PEM: %s" % buf) with open(authority_path + ".part", "wb") as oh: oh.write(buf) click.echo("Writing CA certificate to: %s" % authority_path) os.rename(authority_path + ".part", authority_path) except urllib.error.HTTPError as e: click.echo( "Failed to fetch CA certificate, server responded with: %d %s" % (e.code, e.reason), err=True) return 1 else: raise FileNotFoundError( "CA certificate not found and no URL specified") try: certificate = Certificate(open(certificate_path)) click.echo("Found certificate: %s" % certificate_path) except FileNotFoundError: try: request = Request(open(request_path)) click.echo("Found signing request: %s" % request_path) except FileNotFoundError: # Construct private key click.echo("Generating 4096-bit RSA key...") key = crypto.PKey() key.generate_key(crypto.TYPE_RSA, 4096) # Dump private key os.umask(0o077) with open(key_path + ".part", "wb") as fh: fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) # Construct CSR csr = crypto.X509Req() csr.set_pubkey(key) request = Request(csr) # Set subject attributes request.common_name = common_name if given_name: request.given_name = given_name if surname: request.surname = surname if org_unit: request.organizational_unit = org_unit # Set extensions extensions = [] if key_usage: extensions.append(("keyUsage", key_usage, True)) if extended_key_usage: extensions.append( ("extendedKeyUsage", extended_key_usage, True)) if email_address: extensions.append( ("subjectAltName", "email:" + email_address, False)) request.set_extensions(extensions) # Dump CSR os.umask(0o022) with open(request_path + ".part", "w") as fh: fh.write(request.dump()) click.echo("Writing private key to: %s" % key_path) os.rename(key_path + ".part", key_path) click.echo("Writing certificate signing request to: %s" % request_path) os.rename(request_path + ".part", request_path) with open(request_path, "rb") as fh: buf = fh.read() submission = urllib.request.Request(request_url, buf) submission.add_header("User-Agent", "Certidude") submission.add_header("Content-Type", "application/pkcs10") click.echo("Submitting to %s, waiting for response..." % request_url) try: response = urllib.request.urlopen(submission) buf = response.read() cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf) except crypto.Error: raise ValueError("Failed to parse PEM: %s" % buf) except urllib.error.HTTPError as e: if e.code == 409: click.echo( "Different signing request with same CN is already present on server, server refuses to overwrite", err=True) return 2 else: click.echo( "Failed to fetch certificate, server responded with: %d %s" % (e.code, e.reason), err=True) return 3 else: if response.code == 202: click.echo( "Server stored the request for processing (202 Accepted), but waiting was not requested, hence quitting for now", err=True) return 254 os.umask(0o022) with open(certificate_path + ".part", "wb") as gh: gh.write(buf) click.echo("Writing certificate to: %s" % certificate_path) os.rename(certificate_path + ".part", certificate_path)