예제 #1
0
    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)
예제 #2
0
 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)
예제 #3
0
파일: lease.py 프로젝트: kmteras/certidude
 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()
예제 #4
0
 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
예제 #5
0
 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)
예제 #6
0
    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
예제 #7
0
 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)
예제 #8
0
파일: signed.py 프로젝트: kmteras/certidude
    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"
            )
예제 #9
0
 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)
예제 #10
0
파일: lease.py 프로젝트: kmteras/certidude
    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)
예제 #11
0
 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)
예제 #12
0
    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"))
예제 #13
0
    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