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_put(self, req, resp): # Consume token now = time() timestamp = req.get_param_as_int("t", required=True) username = req.get_param("u", required=True) user = User.objects.get(username) csum = hashlib.sha256() csum.update(config.TOKEN_SECRET) csum.update(username.encode("ascii")) csum.update(str(timestamp).encode("ascii")) margin = 300 # Tolerate 5 minute clock skew as Kerberos does if csum.hexdigest() != req.get_param("c", required=True): raise falcon.HTTPForbidden("Forbidden", "Invalid token supplied, did you copy-paste link correctly?") if now < timestamp - margin: raise falcon.HTTPForbidden("Forbidden", "Token not valid yet, are you sure server clock is correct?") if now > timestamp + margin + config.TOKEN_LIFETIME: raise falcon.HTTPForbidden("Forbidden", "Token expired") # At this point consider token to be legitimate body = req.stream.read(req.content_length) header, _, der_bytes = pem.unarmor(body) csr = CertificationRequest.load(der_bytes) common_name = csr["certification_request_info"]["subject"].native["common_name"] assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name try: _, resp.body = self.authority._sign(csr, body, profile="default", overwrite=config.TOKEN_OVERWRITE_PERMITTED) resp.set_header("Content-Type", "application/x-pem-file") logger.info("Autosigned %s as proven by token ownership", common_name) except FileExistsError: logger.info("Won't autosign duplicate %s", common_name) raise falcon.HTTPConflict( "Certificate with such common name (CN) already exists", "Will not overwrite existing certificate signing request, explicitly delete existing one and try again")
def get_request(common_name): if not re.match(const.RE_COMMON_NAME, common_name): raise ValueError("Invalid common name %s" % repr(common_name)) path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") try: with open(path, "rb") as fh: buf = fh.read() header, _, der_bytes = pem.unarmor(buf) return path, buf, CertificationRequest.load(der_bytes), \ datetime.utcfromtimestamp(os.stat(path).st_ctime) except EnvironmentError: raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path)
def sign(common_name, overwrite=False): """ Sign certificate signing request by it's common name """ req_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") with open(req_path) as fh: csr_buf = fh.read() header, _, der_bytes = pem.unarmor(csr_buf) csr = CertificationRequest.load(der_bytes) # Sign with function below cert, buf = _sign(csr, csr_buf, overwrite) os.unlink(req_path) return cert, buf
def on_put(self, req, resp): try: username, mail, created, expires, profile = self.manager.consume(req.get_param("token", required=True)) except RelationalMixin.DoesNotExist: raise falcon.HTTPForbidden("Forbidden", "No such token or token expired") body = req.stream.read(req.content_length) header, _, der_bytes = pem.unarmor(body) csr = CertificationRequest.load(der_bytes) common_name = csr["certification_request_info"]["subject"].native["common_name"] if not common_name.startswith(username + "@"): raise falcon.HTTPBadRequest("Bad requst", "Invalid common name %s" % common_name) try: _, resp.body = self.authority._sign(csr, body, profile=config.PROFILES.get(profile), overwrite=config.TOKEN_OVERWRITE_PERMITTED) resp.set_header("Content-Type", "application/x-pem-file") logger.info("Autosigned %s as proven by token ownership", common_name) except FileExistsError: logger.info("Won't autosign duplicate %s", common_name) raise falcon.HTTPConflict( "Certificate with such common name (CN) already exists", "Will not overwrite existing certificate signing request, explicitly delete existing one and try again")
def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None): """ Sign certificate signing request by it's common name """ req_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") with open(req_path, "rb") as fh: csr_buf = fh.read() header, _, der_bytes = pem.unarmor(csr_buf) csr = CertificationRequest.load(der_bytes) # Sign with function below cert, buf = _sign(csr, csr_buf, skip_notify, skip_push, overwrite, profile, signer) os.unlink(req_path) return cert, buf
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)
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
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: raise falcon.HTTPBadRequest( "Bad request", "Malformed certificate signing request") 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 = self.authority._sign(csr, body, overwrite=True) logger.info( "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, 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 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( "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( "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( "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( "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 = self.authority._sign(csr, body, overwrite=True) logger.info("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 self.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 = self.authority._sign(csr, body) logger.info("Autosigned %s as %s is whitelisted", 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( "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, _, _ = 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("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) logger.debug("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) if req.client_accepts("application/json"): resp.body = json.dumps( { "title": "Accepted", "description": resp.body }, cls=MyEncoder)