Beispiel #1
0
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
Beispiel #2
0
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
Beispiel #3
0
    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
Beispiel #4
0
 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)
Beispiel #5
0
    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)
Beispiel #6
0
 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)
Beispiel #7
0
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)
Beispiel #8
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)
Beispiel #9
0
 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)
Beispiel #10
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)
Beispiel #11
0
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"})
Beispiel #12
0
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"})
Beispiel #13
0
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"})
Beispiel #14
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)
Beispiel #15
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)
Beispiel #16
0
    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)
Beispiel #17
0
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
Beispiel #18
0
 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)
Beispiel #19
0
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)
Beispiel #20
0
 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()))
Beispiel #21
0
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
Beispiel #22
0
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
Beispiel #23
0
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
Beispiel #24
0
 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)
Beispiel #25
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"))
Beispiel #26
0
    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)