def wrapped(self, req, resp, cn, *args, **kwargs): from ipaddress import ip_address from certidude import authority from xattr import getxattr try: path, buf, cert, signed, expires = authority.get_signed(cn) except IOError: raise falcon.HTTPNotFound() else: # First attempt to authenticate client with certificate buf = req.get_header("X-SSL-CERT") if buf: header, _, der_bytes = pem.unarmor( buf.replace("\t", "").encode("ascii")) origin_cert = x509.Certificate.load(der_bytes) if origin_cert.native == cert.native: logger.debug("Subject authenticated using certificates") return func(self, req, resp, cn, *args, **kwargs) # For backwards compatibility check source IP address # TODO: make it disableable try: inner_address = getxattr( path, "user.lease.inner_address").decode("ascii") except IOError: raise falcon.HTTPForbidden( "Forbidden", "Remote address %s not whitelisted" % req.context.get("remote_addr")) else: if req.context.get("remote_addr") != ip_address(inner_address): raise falcon.HTTPForbidden( "Forbidden", "Remote address %s mismatch" % req.context.get("remote_addr")) else: return func(self, req, resp, cn, *args, **kwargs)
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_get(self, req, resp, cn): try: path, buf, cert = authority.get_signed(cn) return dict(last_seen=xattr.getxattr(path, "user.lease.last_seen"), inner_address=xattr.getxattr( path, "user.lease.inner_address").decode("ascii"), outer_address=xattr.getxattr( path, "user.lease.outer_address").decode("ascii")) except EnvironmentError: # Certificate or attribute not found raise falcon.HTTPNotFound()
def on_get(self, req, resp, cn): path, buf, cert = authority.get_signed(cn) tags = [] try: for tag in getxattr(path, "user.xdg.tags").split(","): if "=" in tag: k, v = tag.split("=", 1) else: k, v = "other", tag tags.append(dict(id=tag, key=k, value=v)) except IOError: # No user.xdg.tags attribute pass return tags
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 on_get(self, req, resp, cn): # Compensate for NTP lag # from time import sleep # sleep(5) try: cert = authority.get_signed(cn) except EnvironmentError: logger.warning(u"Failed to serve non-existant certificate %s to %s", cn, req.context.get("remote_addr")) resp.body = "No certificate CN=%s found" % cn raise falcon.HTTPNotFound() else: logger.debug(u"Served certificate %s to %s", cn, req.context.get("remote_addr")) return cert
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_get(self, req, resp, cn): preferred_type = req.client_prefers( ("application/json", "application/x-pem-file")) try: path, buf, cert = authority.get_signed(cn) except EnvironmentError: logger.warning( u"Failed to serve non-existant certificate %s to %s", cn, req.context.get("remote_addr")) raise falcon.HTTPNotFound() if preferred_type == "application/x-pem-file": resp.set_header("Content-Type", "application/x-pem-file") resp.set_header("Content-Disposition", ("attachment; filename=%s.pem" % cn)) resp.body = buf logger.debug( u"Served certificate %s to %s as application/x-pem-file", cn, req.context.get("remote_addr")) elif preferred_type == "application/json": resp.set_header("Content-Type", "application/json") resp.set_header("Content-Disposition", ("attachment; filename=%s.json" % cn)) resp.body = json.dumps( dict(common_name=cn, serial_number="%x" % cert.serial_number, signed=cert["tbs_certificate"]["validity"]["not_before"]. native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", expires=cert["tbs_certificate"]["validity"]["not_after"]. native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", sha256sum=hashlib.sha256(buf).hexdigest())) logger.debug(u"Served certificate %s to %s as application/json", cn, req.context.get("remote_addr")) else: logger.debug( u"Client did not accept application/json or application/x-pem-file" ) raise falcon.HTTPUnsupportedMediaType( "Client did not accept application/json or application/x-pem-file" )
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 wrapped(self, req, resp, cn, *args, **kwargs): from ipaddress import ip_address from certidude import authority from xattr import getxattr try: path, buf, cert = authority.get_signed(cn) except IOError: raise falcon.HTTPNotFound() else: try: inner_address = getxattr( path, "user.lease.inner_address").decode("ascii") except IOError: raise falcon.HTTPForbidden( "Forbidden", "Remote address %s not whitelisted" % req.context.get("remote_addr")) else: if req.context.get("remote_addr") != ip_address(inner_address): raise falcon.HTTPForbidden( "Forbidden", "Remote address %s mismatch" % req.context.get("remote_addr")) else: return func(self, req, resp, cn, *args, **kwargs)
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).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