mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 17:39:12 +00:00 
			
		
		
		
	Merge branch 'master' of github.com:laurivosandi/certidude
This commit is contained in:
		| @@ -44,7 +44,6 @@ class SessionResource(object): | |||||||
|     @login_required |     @login_required | ||||||
|     @event_source |     @event_source | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|  |  | ||||||
|         return dict( |         return dict( | ||||||
|             user = dict( |             user = dict( | ||||||
|                 name=req.context.get("user").name, |                 name=req.context.get("user").name, | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ KEYWORDS = ( | |||||||
|     (u"iPhone", u"iphone"), |     (u"iPhone", u"iphone"), | ||||||
|     (u"iPad", u"ipad"), |     (u"iPad", u"ipad"), | ||||||
|     (u"Ubuntu", u"ubuntu"), |     (u"Ubuntu", u"ubuntu"), | ||||||
|  |     (u"Fedora", u"fedora"), | ||||||
|  |     (u"Linux", u"linux"), | ||||||
| ) | ) | ||||||
|  |  | ||||||
| class BundleResource(object): | class BundleResource(object): | ||||||
| @@ -29,8 +31,15 @@ class BundleResource(object): | |||||||
|                 hashlib.sha256(req.user_agent).hexdigest()[:8]) |                 hashlib.sha256(req.user_agent).hexdigest()[:8]) | ||||||
|  |  | ||||||
|         logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user")) |         logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user")) | ||||||
|  |         if config.BUNDLE_FORMAT == "p12": | ||||||
|             resp.set_header("Content-Type", "application/x-pkcs12") |             resp.set_header("Content-Type", "application/x-pkcs12") | ||||||
|             resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii")) |             resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii")) | ||||||
|             resp.body, cert = authority.generate_pkcs12_bundle(common_name, |             resp.body, cert = authority.generate_pkcs12_bundle(common_name, | ||||||
|                 owner=req.context.get("user")) |                 owner=req.context.get("user")) | ||||||
|  |         elif config.BUNDLE_FORMAT == "ovpn": | ||||||
|  |             resp.set_header("Content-Type", "application/x-openvpn") | ||||||
|  |             resp.set_header("Content-Disposition", "attachment; filename=%s.ovpn" % common_name.encode("ascii")) | ||||||
|  |             resp.body, cert = authority.generate_ovpn_bundle(common_name, | ||||||
|  |                 owner=req.context.get("user")) | ||||||
|  |         else: | ||||||
|  |             raise ValueError("Unknown bundle format %s" % config.BUNDLE_FORMAT) | ||||||
|   | |||||||
| @@ -122,9 +122,9 @@ def authenticate(optional=False): | |||||||
|             import ldap |             import ldap | ||||||
|  |  | ||||||
|             if not req.auth: |             if not req.auth: | ||||||
|                 resp.append_header("WWW-Authenticate", "Basic") |                 raise falcon.HTTPUnauthorized("Unauthorized", | ||||||
|                 raise falcon.HTTPUnauthorized("Forbidden", |                     "No authentication header provided", | ||||||
|                     "Please authenticate with %s domain account or supply UPN" % const.DOMAIN) |                     ("Basic",)) | ||||||
|  |  | ||||||
|             if not req.auth.startswith("Basic "): |             if not req.auth.startswith("Basic "): | ||||||
|                 raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth) |                 raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth) | ||||||
| @@ -133,26 +133,35 @@ def authenticate(optional=False): | |||||||
|             basic, token = req.auth.split(" ", 1) |             basic, token = req.auth.split(" ", 1) | ||||||
|             user, passwd = b64decode(token).split(":", 1) |             user, passwd = b64decode(token).split(":", 1) | ||||||
|  |  | ||||||
|             for server in config.LDAP_SERVERS: |             click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, user)) | ||||||
|                 click.echo("Connecting to %s as %s" % (server, user)) |             conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI) | ||||||
|                 conn = ldap.initialize(server) |  | ||||||
|             conn.set_option(ldap.OPT_REFERRALS, 0) |             conn.set_option(ldap.OPT_REFERRALS, 0) | ||||||
|  |  | ||||||
|  |             if "@" not in user: | ||||||
|  |                 user = "%s@%s" % (user, const.DOMAIN) | ||||||
|  |                 logger.debug("Expanded username to %s", user) | ||||||
|  |  | ||||||
|             try: |             try: | ||||||
|                     conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, const.DOMAIN), passwd) |                 conn.simple_bind_s(user, passwd) | ||||||
|                 except ldap.LDAPError, e: |             except ldap.STRONG_AUTH_REQUIRED: | ||||||
|                     resp.append_header("WWW-Authenticate", "Basic") |                 logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://") | ||||||
|  |                 raise | ||||||
|  |             except ldap.SERVER_DOWN: | ||||||
|  |                 logger.critical("Failed to connect LDAP server at %s, are you sure LDAP server's CA certificate has been copied to this machine?", | ||||||
|  |                     config.LDAP_AUTHENTICATION_URI) | ||||||
|  |                 raise | ||||||
|  |             except ldap.INVALID_CREDENTIALS: | ||||||
|                 logger.critical(u"LDAP bind authentication failed for user %s from  %s", |                 logger.critical(u"LDAP bind authentication failed for user %s from  %s", | ||||||
|                     repr(user), req.context.get("remote_addr")) |                     repr(user), req.context.get("remote_addr")) | ||||||
|                 raise falcon.HTTPUnauthorized("Forbidden", |                 raise falcon.HTTPUnauthorized("Forbidden", | ||||||
|                         "Please authenticate with %s domain account or supply UPN" % const.DOMAIN) |                     "Please authenticate with %s domain account or supply UPN" % const.DOMAIN, | ||||||
|  | 		("Basic",)) | ||||||
|  |  | ||||||
|             req.context["ldap_conn"] = conn |             req.context["ldap_conn"] = conn | ||||||
|                 break |  | ||||||
|             else: |  | ||||||
|                 raise ValueError("No LDAP servers!") |  | ||||||
|  |  | ||||||
|             req.context["user"] = User.objects.get(user) |             req.context["user"] = User.objects.get(user) | ||||||
|             return func(resource, req, resp, *args, **kwargs) |             retval = func(resource, req, resp, *args, **kwargs) | ||||||
|  |             conn.unbind_s() | ||||||
|  |             return retval | ||||||
|  |  | ||||||
|  |  | ||||||
|         def pam_authenticate(resource, req, resp, *args, **kwargs): |         def pam_authenticate(resource, req, resp, *args, **kwargs): | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ from cryptography.hazmat.primitives import hashes, serialization | |||||||
| from certidude import config, push, mailer, const | from certidude import config, push, mailer, const | ||||||
| from certidude.wrappers import Certificate, Request | from certidude.wrappers import Certificate, Request | ||||||
| from certidude import errors | from certidude import errors | ||||||
|  | from jinja2 import Template | ||||||
|  |  | ||||||
| RE_HOSTNAME =  "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$" | RE_HOSTNAME =  "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$" | ||||||
|  |  | ||||||
| @@ -21,7 +22,6 @@ RE_HOSTNAME =  "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z | |||||||
| # https://jamielinux.com/docs/openssl-certificate-authority/ | # https://jamielinux.com/docs/openssl-certificate-authority/ | ||||||
| # http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py | # http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py | ||||||
|  |  | ||||||
|  |  | ||||||
| # Cache CA certificate | # Cache CA certificate | ||||||
| certificate = Certificate(open(config.AUTHORITY_CERTIFICATE_PATH)) | certificate = Certificate(open(config.AUTHORITY_CERTIFICATE_PATH)) | ||||||
|  |  | ||||||
| @@ -185,6 +185,39 @@ def delete_request(common_name): | |||||||
|     requests.delete(config.PUSH_PUBLISH % request.fingerprint(), |     requests.delete(config.PUSH_PUBLISH % request.fingerprint(), | ||||||
|         headers={"User-Agent": "Certidude API"}) |         headers={"User-Agent": "Certidude API"}) | ||||||
|  |  | ||||||
|  | def generate_ovpn_bundle(common_name, owner=None): | ||||||
|  |     # Construct private key | ||||||
|  |     click.echo("Generating 4096-bit RSA key...") | ||||||
|  |  | ||||||
|  |     key = rsa.generate_private_key( | ||||||
|  |         public_exponent=65537, | ||||||
|  |         key_size=4096, | ||||||
|  |         backend=default_backend() | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ | ||||||
|  |         x509.NameAttribute(k, v) for k, v in ( | ||||||
|  |             (NameOID.COMMON_NAME, common_name), | ||||||
|  |             (NameOID.GIVEN_NAME, owner and owner.given_name), | ||||||
|  |             (NameOID.SURNAME, owner and owner.surname), | ||||||
|  |         ) if v | ||||||
|  |     ])) | ||||||
|  |  | ||||||
|  |     # Sign CSR | ||||||
|  |     cert = sign(Request( | ||||||
|  |         csr.sign(key, hashes.SHA512(), default_backend()).public_bytes(serialization.Encoding.PEM)), overwrite=True) | ||||||
|  |  | ||||||
|  |     bundle = Template(open(config.OPENVPN_BUNDLE_TEMPLATE).read()).render( | ||||||
|  |         ca = certificate.dump(), | ||||||
|  |         key = key.private_bytes( | ||||||
|  |             encoding=serialization.Encoding.PEM, | ||||||
|  |             format=serialization.PrivateFormat.TraditionalOpenSSL, | ||||||
|  |             encryption_algorithm=serialization.NoEncryption() | ||||||
|  |         ), | ||||||
|  |         cert = cert.dump(), | ||||||
|  |         crl=export_crl(), | ||||||
|  |     ) | ||||||
|  |     return bundle, cert | ||||||
|  |  | ||||||
| def generate_pkcs12_bundle(common_name, key_size=4096, owner=None): | def generate_pkcs12_bundle(common_name, key_size=4096, owner=None): | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -18,8 +18,10 @@ AUTHENTICATION_BACKENDS = set([j for j in | |||||||
| AUTHORIZATION_BACKEND = cp.get("authorization", "backend")  # whitelist, ldap, posix | AUTHORIZATION_BACKEND = cp.get("authorization", "backend")  # whitelist, ldap, posix | ||||||
| ACCOUNTS_BACKEND = cp.get("accounts", "backend")             # posix, ldap | ACCOUNTS_BACKEND = cp.get("accounts", "backend")             # posix, ldap | ||||||
|  |  | ||||||
| if ACCOUNTS_BACKEND == "ldap": | LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri") | ||||||
| LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache") | LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache") | ||||||
|  | LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri") | ||||||
|  | LDAP_BASE = cp.get("accounts", "ldap base") | ||||||
|  |  | ||||||
| USER_SUBNETS = set([ipaddress.ip_network(j) for j in | USER_SUBNETS = set([ipaddress.ip_network(j) for j in | ||||||
|     cp.get("authorization", "user subnets").split(" ") if j]) |     cp.get("authorization", "user subnets").split(" ") if j]) | ||||||
| @@ -38,6 +40,9 @@ SIGNED_DIR = cp.get("authority", "signed dir") | |||||||
| REVOKED_DIR = cp.get("authority", "revoked dir") | REVOKED_DIR = cp.get("authority", "revoked dir") | ||||||
| OUTBOX = cp.get("authority", "outbox") | OUTBOX = cp.get("authority", "outbox") | ||||||
|  |  | ||||||
|  | BUNDLE_FORMAT = cp.get("authority", "bundle format") | ||||||
|  | OPENVPN_BUNDLE_TEMPLATE = cp.get("authority", "openvpn bundle template") | ||||||
|  |  | ||||||
| USER_CERTIFICATE_ENROLLMENT = { | USER_CERTIFICATE_ENROLLMENT = { | ||||||
|     "forbidden": False, "single allowed": True, "multiple allowed": True }[ |     "forbidden": False, "single allowed": True, "multiple allowed": True }[ | ||||||
|     cp.get("authority", "user certificate enrollment")] |     cp.get("authority", "user certificate enrollment")] | ||||||
| @@ -78,17 +83,4 @@ elif "ldap" == AUTHORIZATION_BACKEND: | |||||||
| else: | else: | ||||||
|     raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND) |     raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND) | ||||||
|  |  | ||||||
| for line in open("/etc/ldap/ldap.conf"): |  | ||||||
|     line = line.strip().lower() |  | ||||||
|     if "#" in line: |  | ||||||
|         line, _ = line.split("#", 1) |  | ||||||
|     if not " " in line: |  | ||||||
|         continue |  | ||||||
|     key, value = line.split(" ", 1) |  | ||||||
|     if key == "uri": |  | ||||||
|         LDAP_SERVERS = set([j for j in value.split(" ") if j]) |  | ||||||
|         click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS)) |  | ||||||
|     elif key == "base": |  | ||||||
|         LDAP_BASE = value |  | ||||||
|  |  | ||||||
| # TODO: Check if we don't have base or servers | # TODO: Check if we don't have base or servers | ||||||
|   | |||||||
| @@ -9,11 +9,12 @@ backends = pam | |||||||
| ;backends = ldap | ;backends = ldap | ||||||
| ;backends = kerberos ldap | ;backends = kerberos ldap | ||||||
| ;backends = kerberos pam | ;backends = kerberos pam | ||||||
|  | ldap uri = ldaps://dc1.example.com | ||||||
|  |  | ||||||
| [accounts] | [accounts] | ||||||
| # The accounts backend specifies how the user's given name, surname and e-mail | # The accounts backend specifies how the user's given name, surname and e-mail | ||||||
| # address are looked up. In case of 'posix' basically 'getent passwd' is performed, | # address are looked up. In case of 'posix' basically 'getent passwd' is performed, | ||||||
| # in case of 'ldap' a search is performed on LDAP server specified in /etc/ldap/ldap.conf | # in case of 'ldap' a search is performed on LDAP server specified by ldap uri | ||||||
| # with Kerberos credential cache initialized at path specified by environment variable KRB5CCNAME | # with Kerberos credential cache initialized at path specified by environment variable KRB5CCNAME | ||||||
| # If certidude setup authority was performed correctly the credential cache should be | # If certidude setup authority was performed correctly the credential cache should be | ||||||
| # updated automatically by /etc/cron.hourly/certidude | # updated automatically by /etc/cron.hourly/certidude | ||||||
| @@ -21,6 +22,8 @@ backends = pam | |||||||
| backend = posix | backend = posix | ||||||
| ;backend = ldap | ;backend = ldap | ||||||
| ldap gssapi credential cache = /run/certidude/krb5cc | ldap gssapi credential cache = /run/certidude/krb5cc | ||||||
|  | ldap uri = ldap://dc1.example.com | ||||||
|  | ldap base = {% if base %}{{ base }}{% else %}dc=example,dc=com{% endif %} | ||||||
|  |  | ||||||
| [authorization] | [authorization] | ||||||
| # The authorization backend specifies how the users are authorized. | # The authorization backend specifies how the users are authorized. | ||||||
| @@ -92,3 +95,8 @@ revoked dir = {{ directory }}/revoked/ | |||||||
| expired dir = {{ directory }}/expired/ | expired dir = {{ directory }}/expired/ | ||||||
| outbox = {{ outbox }} | outbox = {{ outbox }} | ||||||
|  |  | ||||||
|  | bundle format = p12 | ||||||
|  | ;bundle format = ovpn | ||||||
|  |  | ||||||
|  | openvpn bundle template = /etc/certidude/template.ovpn | ||||||
|  |  | ||||||
|   | |||||||
| @@ -70,17 +70,15 @@ class DirectoryConnection(object): | |||||||
|             raise ValueError("Ticket cache at %s not initialized, unable to " |             raise ValueError("Ticket cache at %s not initialized, unable to " | ||||||
|                 "authenticate with computer account against LDAP server!" % config.LDAP_GSSAPI_CRED_CACHE) |                 "authenticate with computer account against LDAP server!" % config.LDAP_GSSAPI_CRED_CACHE) | ||||||
|         os.environ["KRB5CCNAME"] = config.LDAP_GSSAPI_CRED_CACHE |         os.environ["KRB5CCNAME"] = config.LDAP_GSSAPI_CRED_CACHE | ||||||
|         for server in config.LDAP_SERVERS: |         self.conn = ldap.initialize(config.LDAP_ACCOUNTS_URI) | ||||||
|             self.conn = ldap.initialize(server) |  | ||||||
|         self.conn.set_option(ldap.OPT_REFERRALS, 0) |         self.conn.set_option(ldap.OPT_REFERRALS, 0) | ||||||
|         click.echo("Connecing to %s using Kerberos ticket cache from %s" % |         click.echo("Connecing to %s using Kerberos ticket cache from %s" % | ||||||
|                 (server, config.LDAP_GSSAPI_CRED_CACHE)) |             (config.LDAP_ACCOUNTS_URI, config.LDAP_GSSAPI_CRED_CACHE)) | ||||||
|         self.conn.sasl_interactive_bind_s('', ldap.sasl.gssapi()) |         self.conn.sasl_interactive_bind_s('', ldap.sasl.gssapi()) | ||||||
|         return self.conn |         return self.conn | ||||||
|         raise ValueError("No LDAP servers specified!") |  | ||||||
|  |  | ||||||
|     def __exit__(self, type, value, traceback): |     def __exit__(self, type, value, traceback): | ||||||
|         self.conn.unbind_s |         self.conn.unbind_s() | ||||||
|  |  | ||||||
|  |  | ||||||
| class ActiveDirectoryUserManager(object): | class ActiveDirectoryUserManager(object): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user