mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-30 17:09:19 +00:00 
			
		
		
		
	Major refactoring, CA is associated with it's hostname now
This commit is contained in:
		
							
								
								
									
										38
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								README.rst
									
									
									
									
									
								
							| @@ -83,11 +83,20 @@ Create a system user for ``certidude``: | ||||
| Setting up CA | ||||
| -------------- | ||||
|  | ||||
| Certidude can set up CA relatively easily: | ||||
| First make sure the machine used for CA has fully qualified | ||||
| domain name set up properly. | ||||
| You can check it with: | ||||
|  | ||||
|   hostname -f | ||||
|  | ||||
| The command should return ca.example.co | ||||
|  | ||||
| Certidude can set up CA relatively easily, following will set up | ||||
| CA in /var/lib/certidude/hostname.domain: | ||||
|  | ||||
| .. code:: bash | ||||
|  | ||||
|     certidude setup authority /path/to/directory | ||||
|     certidude setup authority | ||||
|  | ||||
| Tweak command-line options until you meet your requirements and | ||||
| then insert generated section to your /etc/ssl/openssl.cnf | ||||
| @@ -112,7 +121,7 @@ Use following command to request a certificate on a machine: | ||||
|  | ||||
| .. code:: | ||||
|  | ||||
|     certidude setup client http://certidude-hostname-or-ip:perhaps-port/api/ca-name/ | ||||
|     certidude setup client ca.example.com | ||||
|  | ||||
| Use following to list signing requests, certificates and revoked certificates: | ||||
|  | ||||
| @@ -185,20 +194,26 @@ configure the site in /etc/nginx/sites-available.d/certidude: | ||||
|         listen 80 default_server; | ||||
|         listen [::]:80 default_server ipv6only=on; | ||||
|  | ||||
|         location ~ /event/publish/(.*) { | ||||
|             allow 127.0.0.1; # Allow publishing only from this IP address | ||||
|         location /pub { | ||||
|             allow 127.0.0.1; # Allow publishing only from CA machine | ||||
|             push_stream_publisher admin; | ||||
|             push_stream_channels_path $1; | ||||
|             push_stream_channels_path $arg_id; | ||||
|         } | ||||
|  | ||||
|         location ~ /event/subscribe/(.*) { | ||||
|         location ~ "^/lp/(.*)" { | ||||
|             push_stream_channels_path $1; | ||||
|             push_stream_subscriber long-polling; | ||||
|         } | ||||
|  | ||||
|         location ~ "^/ev/(.*)" { | ||||
|             push_stream_channels_path $1; | ||||
|             push_stream_subscriber eventsource; | ||||
|         } | ||||
|  | ||||
|         location / { | ||||
|             include uwsgi_params; | ||||
|             uwsgi_pass certidude_api; | ||||
|             proxy_pass       http://ca.koodur.com/; | ||||
|             proxy_set_header Host      $host; | ||||
|             proxy_set_header X-Real-IP $remote_addr; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -239,8 +254,7 @@ Also adjust ``/etc/nginx/nginx.conf``: | ||||
|  | ||||
| In your CA ssl.cnf make sure Certidude is aware of your nginx setup: | ||||
|  | ||||
|     publish_certificate_url = http://push.example.com/event/publish/%(request_sha1sum)s | ||||
|     subscribe_certificate_url = http://push.example.com/event/subscribe/%(request_sha1sum)s | ||||
|     push_server = http://push.example.com/ | ||||
|  | ||||
| Restart the services: | ||||
|  | ||||
| @@ -338,7 +352,7 @@ Create ``/etc/NetworkManager/dispatcher.d/certidude`` with following content: | ||||
|  | ||||
|     case "$2" in | ||||
|         up) | ||||
|             LANG=C.UTF-8 /usr/local/bin/certidude setup strongswan networkmanager http://ca.example.org/api/laptops/ gateway.example.org | ||||
|             LANG=C.UTF-8 /usr/local/bin/certidude setup strongswan networkmanager ca.example.com gateway.example.com | ||||
|         ;; | ||||
|     esac | ||||
|  | ||||
|   | ||||
							
								
								
									
										251
									
								
								certidude/api.py
									
									
									
									
									
								
							
							
						
						
									
										251
									
								
								certidude/api.py
									
									
									
									
									
								
							| @@ -21,22 +21,45 @@ env = Environment(loader=PackageLoader("certidude", "templates")) | ||||
|  | ||||
| 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])$" | ||||
|  | ||||
|  | ||||
| OIDS = { | ||||
|     (2, 5, 4,  3) : 'CN',   # common name | ||||
|     (2, 5, 4,  6) : 'C',    # country | ||||
|     (2, 5, 4,  7) : 'L',    # locality | ||||
|     (2, 5, 4,  8) : 'ST',   # stateOrProvince | ||||
|     (2, 5, 4, 10) : 'O',    # organization | ||||
|     (2, 5, 4, 11) : 'OU',   # organizationalUnit | ||||
| } | ||||
|  | ||||
| def parse_dn(data): | ||||
|     chunks, remainder = decoder.decode(data) | ||||
|     dn = "" | ||||
|     if remainder: | ||||
|         raise ValueError() | ||||
|     # TODO: Check for duplicate entries? | ||||
|     def generate(): | ||||
|         for chunk in chunks: | ||||
|             for chunkette in chunk: | ||||
|                 key, value = chunkette | ||||
|                 yield str(OIDS[key] + "=" + value) | ||||
|     return ", ".join(generate()) | ||||
|  | ||||
| def omit(**kwargs): | ||||
|     return dict([(key,value) for (key, value) in kwargs.items() if value]) | ||||
|  | ||||
| def event_source(func): | ||||
|     def wrapped(self, req, resp, ca, *args, **kwargs): | ||||
|     def wrapped(self, req, resp, *args, **kwargs): | ||||
|         if req.get_header("Accept") == "text/event-stream": | ||||
|             resp.status = falcon.HTTP_SEE_OTHER | ||||
|             resp.location = ca.push_server + "/ev/" + ca.uuid | ||||
|             resp.location = req.context.get("ca").push_server + "/ev/" + req.context.get("ca").uuid | ||||
|             resp.body = "Redirecting to:" + resp.location | ||||
|             print("Delegating EventSource handling to:", resp.location) | ||||
|         return func(self, req, resp, ca, *args, **kwargs) | ||||
|         return func(self, req, resp, *args, **kwargs) | ||||
|     return wrapped | ||||
|  | ||||
| def authorize_admin(func): | ||||
|     def wrapped(self, req, resp, *args, **kwargs): | ||||
|         authority = kwargs.get("ca") | ||||
|         authority = req.context.get("ca") | ||||
|  | ||||
|         # Parse remote IPv4/IPv6 address | ||||
|         remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"]) | ||||
| @@ -50,19 +73,19 @@ def authorize_admin(func): | ||||
|             raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr) | ||||
|  | ||||
|         # Check for username whitelist | ||||
|         kerberos_username, kerberos_realm = kwargs.get("user") | ||||
|         kerberos_username, kerberos_realm = req.context.get("user") | ||||
|         if kerberos_username not in authority.admin_users: | ||||
|             raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % kerberos_username) | ||||
|  | ||||
|         # Retain username, TODO: Better abstraction with username, e-mail, sn, gn? | ||||
|         kwargs["user"] = kerberos_username | ||||
|  | ||||
|         return func(self, req, resp, *args, **kwargs) | ||||
|     return wrapped | ||||
|  | ||||
|  | ||||
| def pop_certificate_authority(func): | ||||
|     def wrapped(self, req, resp, *args, **kwargs): | ||||
|         kwargs["ca"] = self.config.instantiate_authority(kwargs["ca"]) | ||||
|         req.context["ca"] = self.config.instantiate_authority(req.env["HTTP_HOST"]) | ||||
|         return func(self, req, resp, *args, **kwargs) | ||||
|     return wrapped | ||||
|  | ||||
| @@ -111,7 +134,7 @@ class MyEncoder(json.JSONEncoder): | ||||
|         if isinstance(obj, CertificateAuthority): | ||||
|             return dict( | ||||
|                 event_channel = obj.push_server + "/ev/" + obj.uuid, | ||||
|                 slug = obj.slug, | ||||
|                 common_name = obj.common_name, | ||||
|                 certificate = obj.certificate, | ||||
|                 admin_users = obj.admin_users, | ||||
|                 autosign_subnets = obj.autosign_subnets, | ||||
| @@ -137,12 +160,12 @@ def serialize(func): | ||||
|         resp.set_header("Expires", "0"); | ||||
|         r = func(instance, req, resp, **kwargs) | ||||
|         if resp.body is None: | ||||
|             if not req.client_accepts_json: | ||||
|                 raise falcon.HTTPUnsupportedMediaType( | ||||
|                     "This API only supports the JSON media type.", | ||||
|                     href="http://docs.examples.com/api/json") | ||||
|             resp.set_header("Content-Type", "application/json") | ||||
|             resp.body = json.dumps(r, cls=MyEncoder) | ||||
|             if req.get_header("Accept").split(",")[0] == "application/json": | ||||
|                 resp.set_header("Content-Type", "application/json") | ||||
|                 resp.append_header("Content-Disposition", "inline") | ||||
|                 resp.body = json.dumps(r, cls=MyEncoder) | ||||
|             else: | ||||
|                 resp.body = repr(r) | ||||
|         return r | ||||
|     return wrapped | ||||
|  | ||||
| @@ -162,7 +185,6 @@ def templatize(path): | ||||
|                     resp.set_header("Content-Type", "application/json") | ||||
|                     r.pop("req") | ||||
|                     r.pop("resp") | ||||
|                     r.pop("user") | ||||
|                     resp.body = json.dumps(r, cls=MyEncoder) | ||||
|                     return r | ||||
|                 else: | ||||
| @@ -180,59 +202,39 @@ class CertificateAuthorityBase(object): | ||||
|  | ||||
| class RevocationListResource(CertificateAuthorityBase): | ||||
|     @pop_certificate_authority | ||||
|     def on_get(self, req, resp, ca): | ||||
|     def on_get(self, req, resp): | ||||
|         resp.set_header("Content-Type", "application/x-pkcs7-crl") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=%s.crl" % ca.slug) | ||||
|         resp.body = ca.export_crl() | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=%s.crl" % req.context.get("ca").common_name) | ||||
|         resp.body = req.context.get("ca").export_crl() | ||||
|  | ||||
|  | ||||
| class SignedCertificateDetailResource(CertificateAuthorityBase): | ||||
|     @serialize | ||||
|     @pop_certificate_authority | ||||
|     @validate_common_name | ||||
|     def on_get(self, req, resp, ca, cn): | ||||
|         path = os.path.join(ca.signed_dir, cn + ".pem") | ||||
|     def on_get(self, req, resp, cn): | ||||
|         path = os.path.join(req.context.get("ca").signed_dir, cn + ".pem") | ||||
|         if not os.path.exists(path): | ||||
|             raise falcon.HTTPNotFound() | ||||
|         resp.stream = open(path, "rb") | ||||
|  | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % cn) | ||||
|         return Certificate(open(path)) | ||||
|  | ||||
|     @login_required | ||||
|     @pop_certificate_authority | ||||
|     @authorize_admin | ||||
|     @validate_common_name | ||||
|     def on_delete(self, req, resp, ca, cn, user): | ||||
|         ca.revoke(cn) | ||||
|     def on_delete(self, req, resp, cn): | ||||
|         req.context.get("ca").revoke(cn) | ||||
|  | ||||
| class LeaseResource(CertificateAuthorityBase): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @pop_certificate_authority | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp, ca, user): | ||||
|     def on_get(self, req, resp): | ||||
|         from ipaddress import ip_address | ||||
|  | ||||
|         OIDS = { | ||||
|             (2, 5, 4,  3) : 'CN',   # common name | ||||
|             (2, 5, 4,  6) : 'C',    # country | ||||
|             (2, 5, 4,  7) : 'L',    # locality | ||||
|             (2, 5, 4,  8) : 'ST',   # stateOrProvince | ||||
|             (2, 5, 4, 10) : 'O',    # organization | ||||
|             (2, 5, 4, 11) : 'OU',   # organizationalUnit | ||||
|         } | ||||
|  | ||||
|         def parse_dn(data): | ||||
|             chunks, remainder = decoder.decode(data) | ||||
|             dn = "" | ||||
|             if remainder: | ||||
|                 raise ValueError() | ||||
|             # TODO: Check for duplicate entries? | ||||
|             def generate(): | ||||
|                 for chunk in chunks: | ||||
|                     for chunkette in chunk: | ||||
|                         key, value = chunkette | ||||
|                         yield str(OIDS[key] + "=" + value) | ||||
|             return ", ".join(generate()) | ||||
|  | ||||
|         # BUGBUG | ||||
|         SQL_LEASES = """ | ||||
|             SELECT | ||||
| @@ -249,17 +251,18 @@ class LeaseResource(CertificateAuthorityBase): | ||||
|             WHERE | ||||
|                 addresses.released <> 1 | ||||
|         """ | ||||
|         cnx = ca.database.get_connection() | ||||
|         cursor = cnx.cursor(dictionary=True) | ||||
|         cnx = req.context.get("ca").database.get_connection() | ||||
|         cursor = cnx.cursor() | ||||
|         query = (SQL_LEASES) | ||||
|         cursor.execute(query) | ||||
|  | ||||
|         for row in cursor: | ||||
|             row["acquired"] = datetime.utcfromtimestamp(row["acquired"]) | ||||
|             row["released"] = datetime.utcfromtimestamp(row["released"]) if row["released"] else None | ||||
|             row["address"] = ip_address(bytes(row["address"])) | ||||
|             row["identity"] = parse_dn(bytes(row["identity"])) | ||||
|             yield row | ||||
|         for acquired, released, address, identity in cursor: | ||||
|             yield { | ||||
|                 "acquired": datetime.utcfromtimestamp(acquired), | ||||
|                 "released": datetime.utcfromtimestamp(released) if released else None, | ||||
|                 "address":  ip_address(bytes(address)), | ||||
|                 "identity": parse_dn(bytes(identity)) | ||||
|             } | ||||
|  | ||||
|  | ||||
| class SignedCertificateListResource(CertificateAuthorityBase): | ||||
| @@ -267,7 +270,7 @@ class SignedCertificateListResource(CertificateAuthorityBase): | ||||
|     @pop_certificate_authority | ||||
|     @authorize_admin | ||||
|     @validate_common_name | ||||
|     def on_get(self, req, resp, ca): | ||||
|     def on_get(self, req, resp): | ||||
|         for j in authority.get_signed(): | ||||
|             yield omit( | ||||
|                 key_type=j.key_type, | ||||
| @@ -283,29 +286,31 @@ class SignedCertificateListResource(CertificateAuthorityBase): | ||||
|  | ||||
|  | ||||
| class RequestDetailResource(CertificateAuthorityBase): | ||||
|     @serialize | ||||
|     @pop_certificate_authority | ||||
|     @validate_common_name | ||||
|     def on_get(self, req, resp, ca, cn): | ||||
|     def on_get(self, req, resp, cn): | ||||
|         """ | ||||
|         Fetch certificate signing request as PEM | ||||
|         """ | ||||
|         path = os.path.join(ca.request_dir, cn + ".pem") | ||||
|         path = os.path.join(req.context.get("ca").request_dir, cn + ".pem") | ||||
|         if not os.path.exists(path): | ||||
|             raise falcon.HTTPNotFound() | ||||
|         resp.stream = open(path, "rb") | ||||
|  | ||||
|         resp.append_header("Content-Type", "application/x-x509-user-cert") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=%s.csr" % cn) | ||||
|         return Request(open(path)) | ||||
|  | ||||
|     @login_required | ||||
|     @pop_certificate_authority | ||||
|     @authorize_admin | ||||
|     @validate_common_name | ||||
|     def on_patch(self, req, resp, ca, cn, user): | ||||
|     def on_patch(self, req, resp, cn): | ||||
|         """ | ||||
|         Sign a certificate signing request | ||||
|         """ | ||||
|         csr = ca.get_request(cn) | ||||
|         cert = ca.sign(csr, overwrite=True, delete=True) | ||||
|         csr = req.context.get("ca").get_request(cn) | ||||
|         cert = req.context.get("ca").sign(csr, overwrite=True, delete=True) | ||||
|         os.unlink(csr.path) | ||||
|         resp.body = "Certificate successfully signed" | ||||
|         resp.status = falcon.HTTP_201 | ||||
| @@ -314,15 +319,16 @@ class RequestDetailResource(CertificateAuthorityBase): | ||||
|     @login_required | ||||
|     @pop_certificate_authority | ||||
|     @authorize_admin | ||||
|     def on_delete(self, req, resp, ca, cn, user): | ||||
|         ca.delete_request(cn) | ||||
|     def on_delete(self, req, resp, cn): | ||||
|         req.context.get("ca").delete_request(cn) | ||||
|  | ||||
|  | ||||
| class RequestListResource(CertificateAuthorityBase): | ||||
|     @serialize | ||||
|     @pop_certificate_authority | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp, ca): | ||||
|         for j in ca.get_requests(): | ||||
|     def on_get(self, req, resp): | ||||
|         for j in req.context.get("ca").get_requests(): | ||||
|             yield omit( | ||||
|                 key_type=j.key_type, | ||||
|                 key_length=j.key_length, | ||||
| @@ -336,12 +342,13 @@ class RequestListResource(CertificateAuthorityBase): | ||||
|                 fingerprint=j.fingerprint()) | ||||
|  | ||||
|     @pop_certificate_authority | ||||
|     def on_post(self, req, resp, ca): | ||||
|     def on_post(self, req, resp): | ||||
|         """ | ||||
|         Submit certificate signing request (CSR) in PEM format | ||||
|         """ | ||||
|         # Parse remote IPv4/IPv6 address | ||||
|         remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"]) | ||||
|         ca = req.context.get("ca") | ||||
|  | ||||
|         # Check for CSR submission whitelist | ||||
|         if ca.request_subnets: | ||||
| @@ -415,12 +422,11 @@ class RequestListResource(CertificateAuthorityBase): | ||||
|             resp.status = falcon.HTTP_202 | ||||
|  | ||||
|  | ||||
|  | ||||
| class CertificateStatusResource(CertificateAuthorityBase): | ||||
|     """ | ||||
|     openssl ocsp -issuer CAcert_class1.pem -serial 0x<serial no in hex> -url http://localhost -CAfile cacert_both.pem | ||||
|     """ | ||||
|     def on_post(self, req, resp, ca): | ||||
|     def on_post(self, req, resp): | ||||
|         ocsp_request = req.stream.read(req.content_length) | ||||
|         for component in decoder.decode(ocsp_request): | ||||
|             click.echo(component) | ||||
| @@ -430,10 +436,10 @@ class CertificateStatusResource(CertificateAuthorityBase): | ||||
|  | ||||
| class CertificateAuthorityResource(CertificateAuthorityBase): | ||||
|     @pop_certificate_authority | ||||
|     def on_get(self, req, resp, ca): | ||||
|         path = os.path.join(ca.certificate.path) | ||||
|     def on_get(self, req, resp): | ||||
|         path = os.path.join(req.context.get("ca").certificate.path) | ||||
|         resp.stream = open(path, "rb") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % ca.slug) | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % req.context.get("ca").common_name) | ||||
|  | ||||
| class IndexResource(CertificateAuthorityBase): | ||||
|     @serialize | ||||
| @@ -441,45 +447,95 @@ class IndexResource(CertificateAuthorityBase): | ||||
|     @pop_certificate_authority | ||||
|     @authorize_admin | ||||
|     @event_source | ||||
|     def on_get(self, req, resp, ca, user): | ||||
|         return ca | ||||
|     def on_get(self, req, resp): | ||||
|         return req.context.get("ca") | ||||
|  | ||||
| class AuthorityListResource(CertificateAuthorityBase): | ||||
| class SessionResource(CertificateAuthorityBase): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     def on_get(self, req, resp, user): | ||||
|     def on_get(self, req, resp): | ||||
|         return dict( | ||||
|             authorities=(self.config.ca_list), # TODO: Check if user is CA admin | ||||
|             username=user[0] | ||||
|             username=req.context.get("user")[0] | ||||
|         ) | ||||
|  | ||||
| def address_to_identity(cnx, addr): | ||||
|     """ | ||||
|     Translate currently online client's IP-address to distinguished name | ||||
|     """ | ||||
|  | ||||
|     SQL_LEASES = """ | ||||
|         SELECT | ||||
|             acquired, | ||||
|             released, | ||||
|             identities.data as identity | ||||
|         FROM | ||||
|             addresses | ||||
|         RIGHT JOIN | ||||
|             identities | ||||
|         ON | ||||
|             identities.id = addresses.identity | ||||
|         WHERE | ||||
|             address = %s AND | ||||
|             released IS NOT NULL | ||||
|     """ | ||||
|  | ||||
|     cursor = cnx.cursor() | ||||
|     query = (SQL_LEASES) | ||||
|     import struct | ||||
|     cursor.execute(query, (struct.pack("!L", int(addr)),)) | ||||
|  | ||||
|     for acquired, released, identity in cursor: | ||||
|         return { | ||||
|             "acquired": datetime.utcfromtimestamp(acquired), | ||||
|             "identity": parse_dn(bytes(identity)) | ||||
|         } | ||||
|     return None | ||||
|  | ||||
|  | ||||
| class WhoisResource(CertificateAuthorityBase): | ||||
|     @serialize | ||||
|     @pop_certificate_authority | ||||
|     def on_get(self, req, resp): | ||||
|         identity = address_to_identity( | ||||
|             req.context.get("ca").database.get_connection(), | ||||
|             ipaddress.ip_address(req.get_param("address") or req.env["REMOTE_ADDR"]) | ||||
|         ) | ||||
|  | ||||
|         if identity: | ||||
|             return identity | ||||
|         else: | ||||
|             resp.status = falcon.HTTP_403 | ||||
|             resp.body = "Failed to look up node %s" % req.env["REMOTE_ADDR"] | ||||
|  | ||||
|  | ||||
| class ApplicationConfigurationResource(CertificateAuthorityBase): | ||||
|     @pop_certificate_authority | ||||
|     @validate_common_name | ||||
|     def on_get(self, req, resp, ca, cn): | ||||
|     def on_get(self, req, resp, cn): | ||||
|         ctx = dict( | ||||
|             cn = cn, | ||||
|             certificate = ca.get_certificate(cn), | ||||
|             ca_certificate = open(ca.certificate.path, "r").read()) | ||||
|             certificate = req.context.get("ca").get_certificate(cn), | ||||
|             ca_certificate = open(req.context.get("ca").certificate.path, "r").read()) | ||||
|         resp.append_header("Content-Type", "application/ovpn") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=%s.ovpn" % cn) | ||||
|         resp.body = Template(open("/etc/openvpn/%s.template" % ca.slug).read()).render(ctx) | ||||
|         resp.body = Template(open("/etc/openvpn/%s.template" % req.context.get("ca").common_name).read()).render(ctx) | ||||
|  | ||||
|     @login_required | ||||
|     @pop_certificate_authority | ||||
|     @authorize_admin | ||||
|     @validate_common_name | ||||
|     def on_put(self, req, resp, user, ca, cn=None): | ||||
|         pkey_buf, req_buf, cert_buf = ca.create_bundle(cn) | ||||
|     def on_put(self, req, resp, cn=None): | ||||
|         pkey_buf, req_buf, cert_buf = req.context.get("ca").create_bundle(cn) | ||||
|  | ||||
|         ctx = dict( | ||||
|             private_key = pkey_buf, | ||||
|             certificate = cert_buf, | ||||
|             ca_certificate = ca.certificate.dump()) | ||||
|             ca_certificate = req.context.get("ca").certificate.dump()) | ||||
|  | ||||
|         resp.append_header("Content-Type", "application/ovpn") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=%s.ovpn" % cn) | ||||
|         resp.body = Template(open("/etc/openvpn/%s.template" % ca.slug).read()).render(ctx) | ||||
|         resp.body = Template(open("/etc/openvpn/%s.template" % req.context.get("ca").common_name).read()).render(ctx) | ||||
|  | ||||
|  | ||||
| class StaticResource(object): | ||||
| @@ -513,15 +569,20 @@ def certidude_app(): | ||||
|     config = CertificateAuthorityConfig() | ||||
|  | ||||
|     app = falcon.API() | ||||
|     app.add_route("/api/ca/{ca}/ocsp/", CertificateStatusResource(config)) | ||||
|     app.add_route("/api/ca/{ca}/signed/{cn}/openvpn", ApplicationConfigurationResource(config)) | ||||
|     app.add_route("/api/ca/{ca}/certificate/", CertificateAuthorityResource(config)) | ||||
|     app.add_route("/api/ca/{ca}/revoked/", RevocationListResource(config)) | ||||
|     app.add_route("/api/ca/{ca}/signed/{cn}/", SignedCertificateDetailResource(config)) | ||||
|     app.add_route("/api/ca/{ca}/signed/", SignedCertificateListResource(config)) | ||||
|     app.add_route("/api/ca/{ca}/request/{cn}/", RequestDetailResource(config)) | ||||
|     app.add_route("/api/ca/{ca}/request/", RequestListResource(config)) | ||||
|     app.add_route("/api/ca/{ca}/lease/", LeaseResource(config)) | ||||
|     app.add_route("/api/ca/{ca}/", IndexResource(config)) | ||||
|     app.add_route("/api/ca/", AuthorityListResource(config)) | ||||
|  | ||||
|     # Certificate authority API calls | ||||
|     app.add_route("/api/ocsp/", CertificateStatusResource(config)) | ||||
|     app.add_route("/api/signed/{cn}/openvpn", ApplicationConfigurationResource(config)) | ||||
|     app.add_route("/api/certificate/", CertificateAuthorityResource(config)) | ||||
|     app.add_route("/api/revoked/", RevocationListResource(config)) | ||||
|     app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(config)) | ||||
|     app.add_route("/api/signed/", SignedCertificateListResource(config)) | ||||
|     app.add_route("/api/request/{cn}/", RequestDetailResource(config)) | ||||
|     app.add_route("/api/request/", RequestListResource(config)) | ||||
|     app.add_route("/api/", IndexResource(config)) | ||||
|     app.add_route("/api/session/", SessionResource(config)) | ||||
|  | ||||
|     # Gateway API calls, should this be moved to separate project? | ||||
|     app.add_route("/api/lease/", LeaseResource(config)) | ||||
|     app.add_route("/api/whois/", WhoisResource(config)) | ||||
|     return app | ||||
|   | ||||
| @@ -34,35 +34,35 @@ def login_required(func): | ||||
|  | ||||
|         if not authorization: | ||||
|             resp.append_header("WWW-Authenticate", "Negotiate") | ||||
|             raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered?") | ||||
|             raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered, are you sure you've logged in with domain user account?") | ||||
|  | ||||
|         token = ''.join(authorization.split()[1:]) | ||||
|  | ||||
|         try: | ||||
|             result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) | ||||
|         except kerberos.GSSError as ex: | ||||
|             raise falcon.HTTPForbidden("Forbidden", "Authentication System Failure: %s(%s)" % (ex[0][0], ex[1][0],)) | ||||
|             raise falcon.HTTPForbidden("Forbidden", "Authentication System Failure: %s(%s)" % (ex.args[0][0], ex.args[1][0],)) | ||||
|  | ||||
|         try: | ||||
|             result = kerberos.authGSSServerStep(context, token) | ||||
|         except kerberos.GSSError as ex: | ||||
|             s = str(dir(ex)) | ||||
|             kerberos.authGSSServerClean(context) | ||||
|             raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s(%s)" % (ex[0][0], ex[1][0],)) | ||||
|             raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s (%s)" % (ex.args[0][0], ex.args[1][0])) | ||||
|         except kerberos.KrbError as ex: | ||||
|             kerberos.authGSSServerClean(context) | ||||
|             raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s" % (ex[0],)) | ||||
|             raise falcon.HTTPForbidden("Forbidden", "Bad credentials: %s" % (ex.args[0],)) | ||||
|  | ||||
|         kerberos_user = kerberos.authGSSServerUserName(context).split("@") | ||||
|         req.context["user"] = kerberos.authGSSServerUserName(context).split("@") | ||||
|  | ||||
|         try: | ||||
|             # BUGBUG: https://github.com/02strich/pykerberos/issues/6 | ||||
|             #kerberos.authGSSServerClean(context) | ||||
|             pass | ||||
|         except kerberos.GSSError as ex: | ||||
|             raise error.LoginFailed('Authentication System Failure %s(%s)' % (ex[0][0], ex[1][0],)) | ||||
|             raise error.LoginFailed('Authentication System Failure %s(%s)' % (ex.args[0][0], ex.args[1][0],)) | ||||
|  | ||||
|         if result == kerberos.AUTH_GSS_COMPLETE: | ||||
|             kwargs["user"] = kerberos_user | ||||
|             return func(resource, req, resp, *args, **kwargs) | ||||
|         elif result == kerberos.AUTH_GSS_CONTINUE: | ||||
|             raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI") | ||||
|   | ||||
| @@ -52,7 +52,7 @@ SURNAME = None | ||||
| EMAIL = None | ||||
|  | ||||
| if USERNAME: | ||||
|     EMAIL = USERNAME + "@" + HOSTNAME | ||||
|     EMAIL = USERNAME + "@" + FQDN | ||||
|  | ||||
| if os.getuid() >= 1000: | ||||
|     _, _, _, _, gecos, _, _ = pwd.getpwnam(USERNAME) | ||||
| @@ -106,14 +106,14 @@ def certidude_spawn(kill, no_interaction): | ||||
|     ca_loaded = False | ||||
|     config = load_config() | ||||
|     for ca in config.all_authorities(): | ||||
|         socket_path = os.path.join(signer_dir, ca.slug + ".sock") | ||||
|         pidfile_path = os.path.join(signer_dir, ca.slug + ".pid") | ||||
|         socket_path = os.path.join(signer_dir, ca.common_name + ".sock") | ||||
|         pidfile_path = os.path.join(signer_dir, ca.common_name + ".pid") | ||||
|  | ||||
|         try: | ||||
|             with open(pidfile_path) as fh: | ||||
|                 pid = int(fh.readline()) | ||||
|                 os.kill(pid, 0) | ||||
|                 click.echo("Found process with PID %d for %s" % (pid, ca.slug)) | ||||
|                 click.echo("Found process with PID %d for %s" % (pid, ca.common_name)) | ||||
|         except (ValueError, ProcessLookupError, FileNotFoundError): | ||||
|             pid = 0 | ||||
|  | ||||
| @@ -138,9 +138,9 @@ def certidude_spawn(kill, no_interaction): | ||||
|             with open(pidfile_path, "w") as fh: | ||||
|                 fh.write("%d\n" % os.getpid()) | ||||
|  | ||||
|             setproctitle("%s spawn %s" % (sys.argv[0], ca.slug)) | ||||
|             setproctitle("%s spawn %s" % (sys.argv[0], ca.common_name)) | ||||
|             logging.basicConfig( | ||||
|                 filename="/var/log/certidude-%s.log" % ca.slug, | ||||
|                 filename="/var/log/certidude-%s.log" % ca.common_name, | ||||
|                 level=logging.INFO) | ||||
|             server = SignServer(socket_path, ca.private_key, ca.certificate.path, | ||||
|                 ca.certificate_lifetime, ca.basic_constraints, ca.key_usage, | ||||
| @@ -524,15 +524,15 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw | ||||
|  | ||||
| @click.command("authority", help="Set up Certificate Authority in a directory") | ||||
| @click.option("--parent", "-p", help="Parent CA, none by default") | ||||
| @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, hostname by default") | ||||
| @click.option("--country", "-c", default="ee", help="Country, Estonia by default") | ||||
| @click.option("--state", "-s", default="Harjumaa", help="State or country, Harjumaa by default") | ||||
| @click.option("--locality", "-l", default="Tallinn", help="City or locality, Tallinn by default") | ||||
| @click.option("--common-name", "-cn", default=FQDN, help="Common name, fully qualified hostname by default") | ||||
| @click.option("--country", "-c", default=None, help="Country, none by default") | ||||
| @click.option("--state", "-s", default=None, help="State or country, none by default") | ||||
| @click.option("--locality", "-l", default=None, help="City or locality, none by default") | ||||
| @click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 7300 days (20 years) by default") | ||||
| @click.option("--certificate-lifetime", default=5*365, help="Certificate lifetime in days, 1825 days (5 years) by default") | ||||
| @click.option("--revocation-list-lifetime", default=1, help="Revocation list lifetime in days, 1 day by default") | ||||
| @click.option("--organization", "-o", default="Example LLC", help="Company or organization name") | ||||
| @click.option("--organizational-unit", "-ou", default="Certification Department") | ||||
| @click.option("--organization", "-o", default=None, help="Company or organization name") | ||||
| @click.option("--organizational-unit", "-ou", default=None) | ||||
| @click.option("--pkcs11", default=False, is_flag=True, help="Use PKCS#11 token instead of files") | ||||
| @click.option("--crl-distribution-url", default=None, help="CRL distribution URL") | ||||
| @click.option("--ocsp-responder-url", default=None, help="OCSP responder URL") | ||||
| @@ -540,17 +540,14 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw | ||||
| @click.option("--inbox", default="imap://user:pass@host:port/INBOX", help="Inbound e-mail server") | ||||
| @click.option("--outbox", default="smtp://localhost", help="Outbound e-mail server") | ||||
| @click.option("--push-server", default="", help="Streaming nginx push server") | ||||
| @click.argument("directory") | ||||
| @click.option("--directory", default=None, help="Directory for authority files, /var/lib/certidude/<common-name>/ by default") | ||||
| def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, crl_distribution_url, ocsp_responder_url, email_address, inbox, outbox, push_server): | ||||
|  | ||||
|     publish_certificate_url = push_server + "/publish/%(request_sha1sum)s" | ||||
|     subscribe_certificate_url = push_server + "/subscribe/%(request_sha1sum)s" | ||||
|     if not directory: | ||||
|         directory = os.path.join("/var/lib/certidude", common_name) | ||||
|  | ||||
|     slug = os.path.basename(directory[:-1] if directory.endswith('/') else directory) | ||||
|     if not slug: | ||||
|         raise click.ClickException("Please supply proper target path") | ||||
|     # Make sure slug is valid | ||||
|     if not re.match(r"^[_a-zA-Z0-9]+$", slug): | ||||
|     # Make sure common_name is valid | ||||
|     if not re.match(r"^[\._a-zA-Z0-9]+$", common_name): | ||||
|         raise click.ClickException("CA name can contain only alphanumeric and '_' characters") | ||||
|  | ||||
|     if os.path.lexists(directory): | ||||
| @@ -567,7 +564,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | ||||
|         key.generate_key(crypto.TYPE_RSA, 4096) | ||||
|  | ||||
|     if not crl_distribution_url: | ||||
|         crl_distribution_url = "http://%s/api/%s/revoked/" % (common_name, slug) | ||||
|         crl_distribution_url = "http://%s/api/revoked/" % common_name | ||||
|  | ||||
|     # File paths | ||||
|     ca_key = os.path.join(directory, "ca_key.pem") | ||||
| @@ -579,11 +576,18 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | ||||
|     ca.set_version(2) # This corresponds to X.509v3 | ||||
|     ca.set_serial_number(1) | ||||
|     ca.get_subject().CN = common_name | ||||
|     ca.get_subject().C = country | ||||
|     ca.get_subject().ST = state | ||||
|     ca.get_subject().L = locality | ||||
|     ca.get_subject().O = organization | ||||
|     ca.get_subject().OU = organizational_unit | ||||
|  | ||||
|     if country: | ||||
|         ca.get_subject().C = country | ||||
|     if state: | ||||
|         ca.get_subject().ST = state | ||||
|     if locality: | ||||
|         ca.get_subject().L = locality | ||||
|     if organization: | ||||
|         ca.get_subject().O = organization | ||||
|     if organizational_unit: | ||||
|         ca.get_subject().OU = organizational_unit | ||||
|  | ||||
|     ca.gmtime_adj_notBefore(0) | ||||
|     ca.gmtime_adj_notAfter(authority_lifetime * 24 * 60 * 60) | ||||
|     ca.set_issuer(ca.get_subject()) | ||||
| @@ -621,7 +625,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | ||||
|         raise NotImplementedError() | ||||
|  | ||||
|     """ | ||||
|         ocsp_responder_url = "http://%s/api/%s/ocsp/" % (common_name, slug) | ||||
|         ocsp_responder_url = "http://%s/api/ocsp/" % common_name | ||||
|         authority_info_access = "OCSP;URI:%s" % ocsp_responder_url | ||||
|         ca.add_extensions([ | ||||
|             crypto.X509Extension( | ||||
| @@ -661,18 +665,19 @@ def certidude_setup_authority(parent, country, state, locality, organization, or | ||||
|     with open(ca_key, "wb") as fh: | ||||
|         fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) | ||||
|  | ||||
|     with open(os.path.join(directory, "openssl.cnf.example"), "w") as fh: | ||||
|     ssl_cnf_example = os.path.join(directory, "openssl.cnf.example") | ||||
|     with open(ssl_cnf_example, "w") as fh: | ||||
|         fh.write(env.get_template("openssl.cnf").render(locals())) | ||||
|  | ||||
|     click.echo("You need to copy the contents of the 'openssl.cnf.example'") | ||||
|     click.echo("You need to copy the contents of the '%s'" % ssl_cnf_example) | ||||
|     click.echo("to system-wide OpenSSL configuration file, usually located") | ||||
|     click.echo("at /etc/ssl/openssl.cnf") | ||||
|  | ||||
|     click.echo() | ||||
|     click.echo("Use following commands to inspect the newly created files:") | ||||
|     click.echo() | ||||
|     click.echo("  openssl crl -inform PEM -text -noout -in %s" % ca_crl) | ||||
|     click.echo("  openssl x509 -text -noout -in %s" % ca_crt) | ||||
|     click.echo("  openssl crl -inform PEM -text -noout -in %s | less" % ca_crl) | ||||
|     click.echo("  openssl x509 -text -noout -in %s | less" % ca_crt) | ||||
|     click.echo("  openssl rsa -check -in %s" % ca_key) | ||||
|     click.echo("  openssl verify -CAfile %s %s" % (ca_crt, ca_crt)) | ||||
|     click.echo() | ||||
| @@ -746,7 +751,7 @@ def certidude_list(ca, verbose, show_key_type, show_extensions, show_path, show_ | ||||
|         if not hide_requests: | ||||
|             for j in ca.get_requests(): | ||||
|                 if not verbose: | ||||
|                     click.echo("s " + j.path + " " + j.distinguished_name) | ||||
|                     click.echo("s " + j.path + " " + j.identity) | ||||
|                     continue | ||||
|                 click.echo(click.style(j.common_name, fg="blue")) | ||||
|                 click.echo("=" * len(j.common_name)) | ||||
| @@ -777,11 +782,11 @@ def certidude_list(ca, verbose, show_key_type, show_extensions, show_path, show_ | ||||
|             for j in ca.get_signed(): | ||||
|                 if not verbose: | ||||
|                     if j.signed < NOW and j.expires > NOW: | ||||
|                         click.echo("v " + j.path + " " + j.distinguished_name) | ||||
|                         click.echo("v " + j.path + " " + j.identity) | ||||
|                     elif NOW > j.expires: | ||||
|                         click.echo("e " + j.path + " " + j.distinguished_name) | ||||
|                         click.echo("e " + j.path + " " + j.identity) | ||||
|                     else: | ||||
|                         click.echo("y " + j.path + " " + j.distinguished_name) | ||||
|                         click.echo("y " + j.path + " " + j.identity) | ||||
|                     continue | ||||
|  | ||||
|                 click.echo(click.style(j.common_name, fg="blue") + " " + click.style(j.serial_number_hex, fg="white")) | ||||
| @@ -803,7 +808,7 @@ def certidude_list(ca, verbose, show_key_type, show_extensions, show_path, show_ | ||||
|         if show_revoked: | ||||
|             for j in ca.get_revoked(): | ||||
|                 if not verbose: | ||||
|                     click.echo("r " + j.path + " " + j.distinguished_name) | ||||
|                     click.echo("r " + j.path + " " + j.identity) | ||||
|                     continue | ||||
|                 click.echo(click.style(j.common_name, fg="blue") + " " + click.style(j.serial_number_hex, fg="white")) | ||||
|                 click.echo("="*(len(j.common_name)+60)) | ||||
| @@ -869,7 +874,7 @@ def certidude_sign(common_name, overwrite, lifetime): | ||||
|             # Sign directly using private key | ||||
|             cert = ca.sign2(request, overwrite, True, lifetime) | ||||
|  | ||||
|         click.echo("Signed %s" % cert.distinguished_name) | ||||
|         click.echo("Signed %s" % cert.identity) | ||||
|         for key, value, data in cert.extensions: | ||||
|             click.echo("Added extension %s: %s" % (key, value)) | ||||
|         click.echo() | ||||
|   | ||||
| @@ -40,10 +40,15 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, | ||||
|     # Set up URL-s | ||||
|     request_params = set() | ||||
|     if autosign: | ||||
|         request_params.add("autosign=yes") | ||||
|         request_params.add("autosign=true") | ||||
|     if wait: | ||||
|         request_params.add("wait=forever") | ||||
|  | ||||
|     # Expand ca.example.com to http://ca.example.com/api/ | ||||
|     if not "/" in url: | ||||
|         url += "/api/" | ||||
|     if "//" not in url: | ||||
|         url = "http://" + url | ||||
|     if not url.endswith("/"): | ||||
|         url = url + "/" | ||||
|  | ||||
|   | ||||
| @@ -50,10 +50,14 @@ def raw_sign(private_key, ca_cert, request, basic_constraints, lifetime, key_usa | ||||
| #            raise ValueError("Country mismatch!") | ||||
|  | ||||
|         # Copy attributes from CA | ||||
|         cert.get_subject().C  = ca_cert.get_subject().C | ||||
|         cert.get_subject().ST  = ca_cert.get_subject().ST | ||||
|         cert.get_subject().L  = ca_cert.get_subject().L | ||||
|         cert.get_subject().O  = ca_cert.get_subject().O | ||||
|         if ca_cert.get_subject().C: | ||||
|             cert.get_subject().C  = ca_cert.get_subject().C | ||||
|         if ca_cert.get_subject().ST: | ||||
|             cert.get_subject().ST  = ca_cert.get_subject().ST | ||||
|         if ca_cert.get_subject().L: | ||||
|             cert.get_subject().L  = ca_cert.get_subject().L | ||||
|         if ca_cert.get_subject().O: | ||||
|             cert.get_subject().O  = ca_cert.get_subject().O | ||||
|  | ||||
|         # Copy attributes from request | ||||
|         cert.get_subject().CN = request.get_subject().CN | ||||
| @@ -198,7 +202,7 @@ class SignServer(asyncore.dispatcher): | ||||
|  | ||||
|         # Dropping privileges | ||||
|         _, _, uid, gid, gecos, root, shell = pwd.getpwnam("nobody") | ||||
|         os.chroot("/run/certidude/signer/jail") | ||||
|         #os.chroot("/run/certidude/signer/jail") | ||||
|         os.setgid(gid) | ||||
|         os.setuid(uid) | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <h1>{{authority.slug}} management</h1> | ||||
| <h1>{{authority.common_name}} management</h1> | ||||
|  | ||||
| <p>Hi {{session.username}},</p> | ||||
|  | ||||
| @@ -9,51 +9,26 @@ | ||||
|  | ||||
| {% set s = authority.certificate.identity %} | ||||
|  | ||||
|  | ||||
| <input id="search" class="icon search" type="search" placeholder="hostname, IP-address, etc"/> | ||||
|  | ||||
| <h1>Pending requests</h1> | ||||
|  | ||||
| <ul> | ||||
|     {% for j in authority.requests %} | ||||
| <ul id="pending_requests"> | ||||
|     {% for request in authority.requests %} | ||||
|          {% include "request.html" %} | ||||
|     {% else %} | ||||
|         <li>Great job! No certificate signing requests to sign.</li> | ||||
| 	{% endfor %} | ||||
|     <li class="notify"> | ||||
|         <p>No certificate signing requests to sign! You can  submit a certificate signing request by:</p> | ||||
|         <pre>certidude setup client {{authority.common_name}}</pre> | ||||
|     </li> | ||||
| </ul> | ||||
|  | ||||
| <h1>Signed certificates</h1> | ||||
|  | ||||
| <ul id="signed_certificates"> | ||||
|     {% for j in authority.signed | sort | reverse %} | ||||
|         <li id="certificate_{{ j.sha256sum }}" data-dn="{{ j.identity }}"> | ||||
|             <a class="button icon download" href="/api/ca/{{authority.slug}}/signed/{{j.common_name}}/">Fetch</a> | ||||
|             <button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/ca/{{authority.slug}}/signed/{{j.common_name}}/',type:'delete'});">Revoke</button> | ||||
|  | ||||
|             <div class="monospace"> | ||||
|             {% include 'img/iconmonstr-certificate-15-icon.svg' %} | ||||
|             {{j.identity}} | ||||
|             </div> | ||||
|  | ||||
|             {% if j.email_address %} | ||||
|             <div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ j.email_address }}</div> | ||||
|             {% endif %} | ||||
|  | ||||
|             <div class="monospace"> | ||||
|             {% include 'img/iconmonstr-key-2-icon.svg' %} | ||||
|             <span title="SHA-256 of public key"> | ||||
|             {{ j.sha256sum }} | ||||
|             </span> | ||||
|             {{ j.key_length }}-bit | ||||
|             {{ j.key_type }} | ||||
|             </div> | ||||
|  | ||||
|             <div> | ||||
|             {% include 'img/iconmonstr-flag-3-icon.svg' %} | ||||
|             {{j.key_usage}} | ||||
|             </div> | ||||
|  | ||||
|             <div class="status"> | ||||
|             {% include 'status.html' %} | ||||
|             </div> | ||||
|         </li> | ||||
|     {% for certificate in authority.signed | sort | reverse %} | ||||
|         {% include "signed.html" %} | ||||
| 	{% endfor %} | ||||
| </ul> | ||||
|  | ||||
| @@ -61,7 +36,7 @@ | ||||
|  | ||||
| <p>To fetch certificate revocation list:</p> | ||||
| <pre> | ||||
| curl {{request.url}}/revoked/ | openssl crl -text -noout | ||||
| curl {{window.location.href}}api/revoked/ | openssl crl -text -noout | ||||
| </pre> | ||||
| <!-- | ||||
| <p>To perform online certificate status request</p> | ||||
|   | ||||
| @@ -35,18 +35,36 @@ ul { | ||||
|     padding: 0; | ||||
| } | ||||
|  | ||||
| #pending_requests .notify { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| #pending_requests .notify:only-child { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
|  | ||||
| button, .button, input[type='search'], input[type='text'] { | ||||
|     border: 1pt solid #ccc; | ||||
|     border-radius: 6px; | ||||
| } | ||||
|  | ||||
| button, .button { | ||||
|     color: #000; | ||||
|     float: right; | ||||
|     border: 1pt solid #ccc; | ||||
|     background-color: #eee; | ||||
|     border-radius: 6px; | ||||
|     margin: 2px; | ||||
|     padding: 6px 12px; | ||||
|     background-position: 6px; | ||||
|     box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| input[type='search'], input[type='text'] { | ||||
|     padding: 4px 4px 4px 36px; | ||||
|     background-position: 6px; | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| button:disabled, .button:disabled { | ||||
|     color: #888; | ||||
| } | ||||
| @@ -108,7 +126,7 @@ h2 svg { | ||||
|     top: 16px; | ||||
| } | ||||
|  | ||||
| p, td, footer, li, button { | ||||
| p, td, footer, li, button, input { | ||||
|     font-family: 'PT Sans Narrow'; | ||||
|     font-size: 14pt; | ||||
| } | ||||
| @@ -155,6 +173,7 @@ li { | ||||
| .icon.revoke { background-image: url("../img/iconmonstr-x-mark-5-icon.svg"); } | ||||
| .icon.download { background-image: url("../img/iconmonstr-download-12-icon.svg"); } | ||||
| .icon.sign { background-image: url("../img/iconmonstr-pen-10-icon.svg"); } | ||||
| .icon.search { background-image: url("../img/iconmonstr-magnifier-4-icon.svg"); } | ||||
|  | ||||
| /* Make sure this is the last one */ | ||||
| .icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");} | ||||
|   | ||||
							
								
								
									
										2
									
								
								certidude/static/error.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								certidude/static/error.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| <h1>{{ message.title }}</h1> | ||||
| <p>{{ message.description }}</p> | ||||
							
								
								
									
										27
									
								
								certidude/static/img/iconmonstr-magnifier-4-icon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								certidude/static/img/iconmonstr-magnifier-4-icon.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
|  | ||||
|  | ||||
| <!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.  | ||||
| You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com --> | ||||
|  | ||||
|  | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
|  | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
|  | ||||
| 	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"> | ||||
|  | ||||
| <path id="magnifier-4-icon" d="M448.225,394.243l-85.387-85.385c16.55-26.081,26.146-56.986,26.146-90.094 | ||||
|  | ||||
| 	c0-92.989-75.652-168.641-168.643-168.641c-92.989,0-168.641,75.652-168.641,168.641s75.651,168.641,168.641,168.641 | ||||
|  | ||||
| 	c31.465,0,60.939-8.67,86.175-23.735l86.14,86.142C429.411,486.566,485.011,431.029,448.225,394.243z M103.992,218.764 | ||||
|  | ||||
| 	c0-64.156,52.192-116.352,116.35-116.352s116.353,52.195,116.353,116.352s-52.195,116.352-116.353,116.352 | ||||
|  | ||||
| 	S103.992,282.92,103.992,218.764z M138.455,188.504c34.057-78.9,148.668-69.752,170.248,12.862 | ||||
|  | ||||
| 	C265.221,150.329,188.719,144.834,138.455,188.504z"/> | ||||
|  | ||||
| </svg> | ||||
|  | ||||
| After Width: | Height: | Size: 1.2 KiB | 
| @@ -8,6 +8,7 @@ | ||||
|     <script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script> | ||||
|     <script type="text/javascript" src="/js/nunjucks.min.js"></script> | ||||
|     <script type="text/javascript" src="/js/certidude.js"></script> | ||||
|     <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon"> | ||||
| </head> | ||||
| <body> | ||||
|     <div id="container"> | ||||
|   | ||||
| @@ -3,8 +3,16 @@ $(document).ready(function() { | ||||
|  | ||||
|     $.ajax({ | ||||
|         method: "GET", | ||||
|         url: "/api/ca/", | ||||
|         url: "/api/session/", | ||||
|         dataType: "json", | ||||
|         error: function(response) { | ||||
|             if (response.responseJSON) { | ||||
|                 var msg = response.responseJSON | ||||
|             } else { | ||||
|                 var msg = { title: "Error " + response.status, description: response.statusText } | ||||
|             } | ||||
|             $("#container").html(nunjucks.render('error.html', { message: msg })); | ||||
|         }, | ||||
|         success: function(session, status, xhr) { | ||||
|             console.info("Loaded CA list:", session); | ||||
|  | ||||
| @@ -15,7 +23,7 @@ $(document).ready(function() { | ||||
|  | ||||
|             $.ajax({ | ||||
|                 method: "GET", | ||||
|                 url: "/api/ca/" + session.authorities[0], | ||||
|                 url: "/api/", | ||||
|                 dataType: "json", | ||||
|                 success: function(authority, status, xhr) { | ||||
|                     console.info("Got CA:", authority); | ||||
| @@ -61,12 +69,33 @@ $(document).ready(function() { | ||||
|  | ||||
|                     source.addEventListener("request_submitted", function(e) { | ||||
|                         console.log("Request submitted:", e.data); | ||||
|                         $.ajax({ | ||||
|                             method: "GET", | ||||
|                             url: "/api/request/lauri-c720p/", | ||||
|                             dataType: "json", | ||||
|                             success: function(request, status, xhr) { | ||||
|                                 console.info(request); | ||||
|                                 $("#pending_requests").prepend( | ||||
|                                     nunjucks.render('request.html', { request: request })); | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                     }); | ||||
|  | ||||
|                     source.addEventListener("request_signed", function(e) { | ||||
|                         console.log("Request signed:", e.data); | ||||
|                         $("#request_" + e.data).slideUp("normal", function() { $(this).remove(); }); | ||||
|                         // TODO: Insert <li> to signed certs list | ||||
|  | ||||
|                         $.ajax({ | ||||
|                             method: "GET", | ||||
|                             url: "/api/signed/lauri-c720p/", | ||||
|                             dataType: "json", | ||||
|                             success: function(certificate, status, xhr) { | ||||
|                                 console.info(certificate); | ||||
|                                 $("#signed_certificates").prepend( | ||||
|                                     nunjucks.render('signed.html', { certificate: certificate })); | ||||
|                             } | ||||
|                         }); | ||||
|                     }); | ||||
|  | ||||
|                     source.addEventListener("certificate_revoked", function(e) { | ||||
| @@ -74,11 +103,11 @@ $(document).ready(function() { | ||||
|                         $("#certificate_" + e.data).slideUp("normal", function() { $(this).remove(); }); | ||||
|                     }); | ||||
|  | ||||
|                     $("#container").html(nunjucks.render('authority.html', { authority: authority, session: session })); | ||||
|                     $("#container").html(nunjucks.render('authority.html', { authority: authority, session: session, window: window })); | ||||
|  | ||||
|                     $.ajax({ | ||||
|                         method: "GET", | ||||
|                         url: "/api/ca/" + authority.slug + "/lease/", | ||||
|                         url: "/api/lease/", | ||||
|                         dataType: "json", | ||||
|                         success: function(leases, status, xhr) { | ||||
|                             console.info("Got leases:", leases); | ||||
| @@ -96,6 +125,18 @@ $(document).ready(function() { | ||||
|                                         released: leases[j].released ? new Date(leases[j].released).toLocaleString() : null | ||||
|                                     }})); | ||||
|                             } | ||||
|  | ||||
|                             /* Set up search box */ | ||||
|                             $("#search").on("keyup", function() { | ||||
|                                 var q = $("#search").val().toLowerCase(); | ||||
|                                 $(".filterable").each(function(i, e) { | ||||
|                                     if ($(e).attr("data-dn").toLowerCase().indexOf(q) >= 0) { | ||||
|                                         $(e).show(); | ||||
|                                     } else { | ||||
|                                         $(e).hide(); | ||||
|                                     } | ||||
|                                 }); | ||||
|                             }); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|   | ||||
| @@ -1,37 +1,37 @@ | ||||
| <li id="request_{{ j.md5sum }}"> | ||||
| <li id="request_{{ request.sha256sum }}" class="filterable"> | ||||
|  | ||||
| <a class="button icon download" href="/api/ca/{{authority.slug}}/request/{{j.common_name}}/">Fetch</a> | ||||
| {% if j.signable %} | ||||
| <button class="icon sign" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/ca/{{authority.slug}}/request/{{j.common_name}}/',type:'patch'});">Sign</button> | ||||
| <a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a> | ||||
| {% if request.signable %} | ||||
| <button class="icon sign" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/',type:'patch'});">Sign</button> | ||||
| {% else %} | ||||
| <button title="Please use certidude command-line utility to sign unusual requests" disabled>Sign</button> | ||||
| {% endif %} | ||||
| <button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/ca/{{authority.slug}}/request/{{j.common_name}}/',type:'delete'});">Delete</button> | ||||
| <button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/',type:'delete'});">Delete</button> | ||||
|  | ||||
|  | ||||
| <div class="monospace"> | ||||
| {% include 'img/iconmonstr-certificate-15-icon.svg' %} | ||||
| {{j.identity}} | ||||
| {{request.identity}} | ||||
| </div> | ||||
|  | ||||
| {% if j.email_address %} | ||||
| <div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ j.email_address }}</div> | ||||
| {% if request.email_address %} | ||||
| <div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ request.email_address }}</div> | ||||
| {% endif %} | ||||
|  | ||||
| <div class="monospace"> | ||||
| {% include 'img/iconmonstr-key-2-icon.svg' %} | ||||
| <span title="SHA-1 of public key"> | ||||
| {{ j.sha256sum }} | ||||
| {{ request.sha256sum }} | ||||
| </span> | ||||
| {{ j.key_length }}-bit | ||||
| {{ j.key_type }} | ||||
| {{ request.key_length }}-bit | ||||
| {{ request.key_type }} | ||||
| </div> | ||||
|  | ||||
| {% set key_usage = j.key_usage %} | ||||
| {% set key_usage = request.key_usage %} | ||||
| {% if key_usage %} | ||||
| <div> | ||||
| {% include 'img/iconmonstr-flag-3-icon.svg' %} | ||||
| {{j.key_usage}} | ||||
| {{request.key_usage}} | ||||
| </div> | ||||
| {% endif %} | ||||
|  | ||||
|   | ||||
							
								
								
									
										31
									
								
								certidude/static/signed.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								certidude/static/signed.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| <li id="certificate_{{ certificate.sha256sum }}" data-dn="{{ certificate.identity }}" class="filterable"> | ||||
|     <a class="button icon download" href="/api/signed/{{certificate.common_name}}/">Fetch</a> | ||||
|     <button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button> | ||||
|  | ||||
|     <div class="monospace"> | ||||
|     {% include 'img/iconmonstr-certificate-15-icon.svg' %} | ||||
|     {{certificate.identity}} | ||||
|     </div> | ||||
|  | ||||
|     {% if certificate.email_address %} | ||||
|     <div class="email">{% include 'img/iconmonstr-email-2-icon.svg' %} {{ certificate.email_address }}</div> | ||||
|     {% endif %} | ||||
|  | ||||
|     <div class="monospace"> | ||||
|     {% include 'img/iconmonstr-key-2-icon.svg' %} | ||||
|     <span title="SHA-256 of public key"> | ||||
|     {{ certificate.sha256sum }} | ||||
|     </span> | ||||
|     {{ certificate.key_length }}-bit | ||||
|     {{ certificate.key_type }} | ||||
|     </div> | ||||
|  | ||||
|     <div> | ||||
|     {% include 'img/iconmonstr-flag-3-icon.svg' %} | ||||
|     {{certificate.key_usage}} | ||||
|     </div> | ||||
|  | ||||
|     <div class="status"> | ||||
|     {% include 'status.html' %} | ||||
|     </div> | ||||
| </li> | ||||
| @@ -1,35 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
|  | ||||
| <!-- License Agreement at http://iconmonstr.com/license/ --> | ||||
|  | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
| 	 width="32px" height="32px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> | ||||
| <path id="certificate-15" d="M374.021,384.08c-4.527,29.103-16.648,55.725-36.043,77.92c-1.125-7.912-4.359-15.591-7.428-21.727 | ||||
| 	c-7.023,3.705-15.439,5.666-22.799,5.666c-1.559,0-3.102-0.084-4.543-0.268c20.586-21.459,30.746-43.688,33.729-73.294 | ||||
| 	c4.828,1.341,10.697,2.046,18.072,2.046C362.119,379.285,364.918,382.319,374.021,384.08z M457.709,445.672 | ||||
| 	c-20.553-21.425-30.596-43.755-33.596-73.327c-4.861,1.358-10.73,2.079-18.207,2.079c-7.107,4.895-10.074,7.93-18.994,9.639 | ||||
| 	c4.527,29.12,16.648,55.742,36.027,77.938c1.123-7.912,4.359-15.591,7.426-21.727C439.133,444.9,449.795,446.678,457.709,445.672z | ||||
| 	 M372.01,362.789c-12.088-8.482-9.473-7.678-24.426-7.628c-0.018,0-0.018,0-0.033,0c-6.221,0-11.752-3.872-13.631-9.572 | ||||
| 	c-4.576-13.68-3.018-11.551-15.088-19.95c-5.18-3.57-7.174-9.907-5.264-15.456c4.695-13.612,4.695-10.997,0-24.677 | ||||
| 	c-1.877-5.499,0.033-11.869,5.264-15.457c12.07-8.383,10.496-6.27,15.088-19.958c1.879-5.717,7.41-9.564,13.631-9.564 | ||||
| 	c0.016,0,0.016,0,0.033,0c14.938,0.042,12.322,0.888,24.426-7.628c2.514-1.76,5.465-2.649,8.449-2.649s5.934,0.889,8.449,2.649 | ||||
| 	c12.086,8.491,9.471,7.678,24.426,7.628c0.016,0,0.016,0,0.016,0c6.236,0,11.77,3.847,13.68,9.564 | ||||
| 	c4.561,13.654,2.951,11.542,15.055,19.958c3.822,2.632,5.969,6.822,5.969,11.165c0,1.425-0.234,2.884-0.721,4.292 | ||||
| 	c-4.678,13.612-4.678,10.997,0,24.677c1.91,5.432,0,11.835-5.248,15.456c-12.104,8.399-10.494,6.287-15.055,19.95 | ||||
| 	c-3.52,10.562-11.266,9.522-20.25,9.522c-7.947,0-7.98,0.721-17.871,7.678C383.879,366.326,377.039,366.326,372.01,362.789z | ||||
| 	 M380.459,331.641c18.676,0,33.797-15.154,33.797-33.797c0-18.676-15.121-33.797-33.797-33.797s-33.797,15.121-33.797,33.797 | ||||
| 	C346.662,316.486,361.783,331.641,380.459,331.641z M300.225,354.508c-28.76,18.172-61.131,38.574-67.837,42.799 | ||||
| 	c-0.737-13.261-5.649-25.6-14.216-35.792c-0.998-1.257-99.79-127.031-123.981-157.987c-19.044-24.358-1.039-50.352,21.106-50.352 | ||||
| 	c29.078,0,40.662,37.887,15.348,54.3l19.967,25.515l138.247-78.122c23.975-17.712,30.73-50.436,15.691-76.119 | ||||
| 	C294.156,61.014,274.91,50,254.348,50c-8.155,0-16.068,1.677-23.57,5.013L88.918,127.577C66.58,138.281,54.292,159.27,54.292,181.6 | ||||
| 	c0,14.015,4.836,28.55,15.062,41.408c24.786,31.165,124.643,158.859,125.641,160.133c14.794,19.682,0.293,47.259-23.621,47.259 | ||||
| 	c-16.974,0-26.019-12.104-28.608-22.447c-3.018-12.104,1.19-24.157,13.269-31.903l-19.58-25.028 | ||||
| 	c-14.686,10.327-24.032,26.001-25.876,43.521C106.646,431.857,136.386,462,171.633,462c10.821,0,21.542-2.984,31.014-8.617 | ||||
| 	l94.158-59.379C301.33,386.896,305.891,369.461,300.225,354.508z M243.25,84.057c3.487-1.635,7.401-2.49,11.315-2.49 | ||||
| 	c9.909,0,18.577,5.23,23.161,14.007c5.801,11.073,4.191,27.3-10.193,35.548l-91.114,51.609c0-20.453-9.975-39.212-26.957-50.67 | ||||
| 	L243.25,84.057z M277.35,191.642c5.139,6.32,16.891,20.729,29.613,36.336c5.969-9.019,14.736-15.817,25.062-19.245 | ||||
| 	c-11.549-14.166-21.775-26.739-26.805-32.883L277.35,191.642z M227.81,329.729l49.288-27.963l-10.863-14.149l-49.145,28.5 | ||||
| 	L227.81,329.729z M259.428,209.772l-86.042,50.52l10.712,13.596l86.288-50.662L259.428,209.772z M281.516,237.182l-86.429,50.905 | ||||
| 	l10.713,13.597l86.679-51.048L281.516,237.182z"/> | ||||
| </svg> | ||||
| Before Width: | Height: | Size: 3.5 KiB | 
| @@ -1,21 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
|  | ||||
|  | ||||
| <!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.  | ||||
| You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com --> | ||||
|  | ||||
|  | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
|  | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
|  | ||||
| 	 width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"> | ||||
|  | ||||
| <path id="email-2-icon" d="M49.744,103.407v305.186H50.1h411.156h1V103.407H49.744z M415.533,138.407L255.947,260.465 | ||||
|  | ||||
| 	L96.473,138.407H415.533z M84.744,173.506l85.504,65.441L84.744,324.45V173.506z M85.1,373.593l113.186-113.186l57.654,44.127 | ||||
|  | ||||
| 	l57.375-43.882l112.941,112.94H85.1z M427.256,325.097l-85.896-85.896l85.896-65.695V325.097z"/> | ||||
|  | ||||
| </svg> | ||||
|  | ||||
| Before Width: | Height: | Size: 982 B | 
| @@ -1,11 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
|  | ||||
| <!-- License Agreement at http://iconmonstr.com/license/ --> | ||||
|  | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
| 	 width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"> | ||||
| <path id="flag-3-icon" d="M120.204,462H74.085V50h46.119V462z M437.915,80.746c0,0-29.079,25.642-67.324,25.642 | ||||
| 	c-60.271,0-61.627-51.923-131.596-51.923c-37.832,0-73.106,17.577-88.045,30.381c0,12.64,0,216.762,0,216.762 | ||||
| 	c21.204-14.696,53.426-30.144,88.286-30.144c66.08,0,75.343,49.388,134.242,49.388c38.042,0,64.437-24.369,64.437-24.369V80.746z"/> | ||||
| </svg> | ||||
| Before Width: | Height: | Size: 786 B | 
| @@ -1,15 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
|  | ||||
| <!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.  | ||||
| You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com --> | ||||
|  | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
| 	 width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"> | ||||
| <path id="key-2-icon" stroke="#000000" stroke-miterlimit="10" d="M286.529,325.486l-45.314,45.314h-43.873l0.002,43.872 | ||||
| 	l-45.746-0.001v41.345l-100.004-0.001l150.078-150.076c-4.578-4.686-10.061-11.391-13.691-17.423L50,426.498v-40.939 | ||||
| 	l145.736-145.736C212.174,278.996,244.713,310.705,286.529,325.486z M425.646,92.339c48.473,48.473,48.471,127.064-0.002,175.535 | ||||
| 	c-48.477,48.476-127.061,48.476-175.537,0.001c-48.473-48.472-48.475-127.062,0-175.537 | ||||
| 	C298.58,43.865,377.172,43.865,425.646,92.339z M400.73,117.165c-12.023-12.021-31.516-12.021-43.537,0 | ||||
| 	c-12.021,12.022-12.021,31.517,0,43.538s31.514,12.021,43.537-0.001C412.754,148.68,412.75,129.188,400.73,117.165z"/> | ||||
| </svg> | ||||
| Before Width: | Height: | Size: 1.3 KiB | 
| @@ -1,18 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
|  | ||||
| <!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.  | ||||
| You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com --> | ||||
|  | ||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | ||||
| <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | ||||
| 	 width="32px" height="32px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"> | ||||
| <path id="time-13-icon" d="M361.629,172.206c15.555-19.627,24.121-44.229,24.121-69.273V50h-259.5v52.933 | ||||
| 	c0,25.044,8.566,49.646,24.121,69.273l50.056,63.166c9.206,11.617,9.271,27.895,0.159,39.584l-50.768,65.13 | ||||
| 	c-15.198,19.497-23.568,43.85-23.568,68.571V462h259.5v-53.343c0-24.722-8.37-49.073-23.567-68.571l-50.769-65.13 | ||||
| 	c-9.112-11.689-9.047-27.967,0.159-39.584L361.629,172.206z M330.634,364.678c11.412,14.64,15.116,29.947,15.116,47.321h-11.096 | ||||
| 	c-4.586-17.886-31.131-30.642-62.559-47.586c-6.907-3.724-6.096-10.373-6.096-15.205h-20c0,4.18,1.03,11.365-6.106,15.202 | ||||
| 	c-32.073,17.249-58.274,29.705-62.701,47.589H166.25c0-17.261,3.645-32.605,15.115-47.321l50.769-65.13 | ||||
| 	c7.109-9.12,11.723-19.484,13.866-30.22v13.38h20V269.33c2.144,10.734,6.758,21.098,13.866,30.218L330.634,364.678z | ||||
| 	 M197.966,167.862l-16.245-20.5c-11.538-14.56-15.471-30.096-15.471-47.361h179.5c0,17.149-3.872,32.727-15.471,47.361l-16.245,20.5 | ||||
| 	H197.966z M246,294.458h20v15h-20V294.458z M246,321.958h20v15h-20V321.958z"/> | ||||
| </svg> | ||||
| Before Width: | Height: | Size: 1.6 KiB | 
| @@ -1,199 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="utf-8"/> | ||||
|     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> | ||||
|     <title>Certidude server</title> | ||||
|     <link href="/css/style.css" rel="stylesheet" type="text/css"/> | ||||
|     <link href="//fonts.googleapis.com/css?family=Ubuntu+Mono" rel="stylesheet" type="text/css"/> | ||||
|     <link href="//fonts.googleapis.com/css?family=Gentium" rel="stylesheet" type="text/css"/> | ||||
|     <link href="//fonts.googleapis.com/css?family=PT+Sans+Narrow" rel="stylesheet" type="text/css"/> | ||||
|     <script type="text/javascript" src="/js/jquery-2.1.4.min.js"></script> | ||||
|     <script type="text/javascript" src="/js/certidude.js"></script> | ||||
| </head> | ||||
| <body> | ||||
| <div id="container"> | ||||
|  | ||||
| <h1>Submit signing request</h1> | ||||
|  | ||||
| <p>Hi, {{user}}</p> | ||||
|  | ||||
| <p>Request submission is allowed from: {% if ca.request_subnets %}{% for i in ca.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}</p> | ||||
| <p>Autosign is allowed from: {% if ca.autosign_subnets %}{% for i in ca.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}</p> | ||||
| <p>Authority administration is allowed from: {% if ca.admin_subnets %}{% for i in ca.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %} | ||||
| <p>Authority administration allowed for: {% for i in ca.admin_users %}{{ i }} {% endfor %}</p> | ||||
|  | ||||
| <h2>IPsec gateway on OpenWrt</h2> | ||||
|  | ||||
| {% set s = ca.certificate.subject %} | ||||
|  | ||||
| <pre> | ||||
| opkg update | ||||
| opkg install strongswan-default curl openssl-util | ||||
| modprobe authenc | ||||
| </pre> | ||||
|  | ||||
| <p>Generate key and submit using standard shell tools:</p> | ||||
|  | ||||
| <pre> | ||||
| CN=$(cat /proc/sys/kernel/hostname) | ||||
| curl {{request.url}}/certificate/ > /etc/ipsec.d/cacerts/ca.pem | ||||
| openssl genrsa -out /etc/ipsec.d/private/$CN.pem 4096 | ||||
| chmod 0600 /etc/ipsec.d/private/$CN.pem | ||||
| openssl req -new -sha256 -key /etc/ipsec.d/private/$CN.pem -out /etc/ipsec.d/reqs/$CN.pem -subj "{% if s.C %}/C={{s.C}}{% endif %}{% if s.ST %}/ST={{s.ST}}{% endif %}{% if s.L %}/L={{s.L}}{% endif %}{% if s.O %}/O={{s.O}}{% endif %}{% if s.OU %}/OU={{s.OU}}{% endif %}/CN=$CN" | ||||
| curl -L -H "Content-Type: application/pkcs10" --data-binary @/etc/ipsec.d/reqs/$CN.pem {{request.uri}}/request/?autosign=yes\&wait=30 > /etc/ipsec.d/certs/$CN.pem.part | ||||
| if [ $? -eq 0 ]; then mv /etc/ipsec.d/certs/$CN.pem.part /etc/ipsec.d/certs/$CN.pem; fi | ||||
| openssl verify -CAfile /etc/ipsec.d/cacerts/ca.pem /etc/ipsec.d/certs/$CN.pem | ||||
| </pre> | ||||
|  | ||||
| <p> | ||||
| Inspect newly created files: | ||||
| </p> | ||||
|  | ||||
| <pre> | ||||
| openssl x509 -text -noout -in /etc/ipsec.d/cacerts/ca.pem | ||||
| openssl x509 -text -noout -in /etc/ipsec.d/certs/$CN.pem | ||||
| openssl rsa -check -in /etc/ipsec.d/private/$CN.pem | ||||
| </pre> | ||||
|  | ||||
| <p>Assuming you have Certidude installed</p> | ||||
|  | ||||
| <pre> | ||||
| certidude setup client {{request.url}} | ||||
| </pre> | ||||
|  | ||||
| <p>To set up OpenVPN server</p> | ||||
| <pre> | ||||
| certidude setup openvpn server {{request.url}} | ||||
| </pre> | ||||
|  | ||||
| <p>Or to set up OpenVPN client</p> | ||||
| <pre> | ||||
| certidude setup openvpn client {{request.url}} | ||||
| </pre> | ||||
|  | ||||
| <h1>Pending requests</h1> | ||||
|  | ||||
| <ul> | ||||
|     {% for j in ca.get_requests() %} | ||||
|         <li id="request_{{ j.fingerprint() }}"> | ||||
|             <a class="button" href="/api/{{ca.slug}}/request/{{j.common_name}}/">Fetch</a> | ||||
|             {% if j.signable %} | ||||
|             <button onClick="javascript:$.ajax({url:'/api/{{ca.slug}}/request/{{j.common_name}}/',type:'patch'});">Sign</button> | ||||
|             {% else %} | ||||
|             <button title="Please use certidude command-line utility to sign unusual requests" disabled>Sign</button> | ||||
|             {% endif %} | ||||
|             <button onClick="javascript:$.ajax({url:'/api/{{ca.slug}}/request/{{j.common_name}}/',type:'delete'});">Delete</button> | ||||
|  | ||||
|  | ||||
|             <div class="monospace"> | ||||
|             {% include 'iconmonstr-certificate-15-icon.svg' %} | ||||
|             {{j.distinguished_name}} | ||||
|             </div> | ||||
|  | ||||
|             {% if j.email_address %} | ||||
|             <div class="email">{% include 'iconmonstr-email-2-icon.svg' %} {{ j.email_address }}</div> | ||||
|             {% endif %} | ||||
|  | ||||
|             <div class="monospace"> | ||||
|             {% include 'iconmonstr-key-2-icon.svg' %} | ||||
|             <span title="SHA-1 of public key"> | ||||
|             {{ j.fingerprint() }} | ||||
|             </span> | ||||
|             {{ j.key_length }}-bit | ||||
|             {{ j.key_type }} | ||||
|             </div> | ||||
|  | ||||
|             {% set key_usage = j.key_usage %} | ||||
|             {% if key_usage %} | ||||
|             <div> | ||||
|             {% include 'iconmonstr-flag-3-icon.svg' %} | ||||
|             {{j.key_usage}} | ||||
|             </div> | ||||
|             {% endif %} | ||||
|  | ||||
|         </li> | ||||
|     {% else %} | ||||
|         <li>Great job! No certificate signing requests to sign.</li> | ||||
| 	{% endfor %} | ||||
| </ul> | ||||
|  | ||||
| <h1>Signed certificates</h1> | ||||
|  | ||||
|  | ||||
|  | ||||
| <p>You can fetch a certificate by <i>common name</i> signing the request</p> | ||||
|  | ||||
| <pre> | ||||
| curl -f {{request.url}}/signed/$CN > $CN.crt | ||||
| </pre> | ||||
|  | ||||
| <ul> | ||||
|     {% for j in ca.get_signed() | sort | reverse %} | ||||
|         <li id="certificate_{{ j.fingerprint() }}"> | ||||
|             <a class="button" href="/api/{{ca.slug}}/signed/{{j.subject.CN}}/">Fetch</a> | ||||
|             <button onClick="javascript:$.ajax({url:'/api/{{ca.slug}}/signed/{{j.subject.CN}}/',type:'delete'});">Revoke</button> | ||||
|  | ||||
|             <div class="monospace"> | ||||
|             {% include 'iconmonstr-certificate-15-icon.svg' %} | ||||
|             {{j.distinguished_name}} | ||||
|             </div> | ||||
|  | ||||
|             {% if j.email_address %} | ||||
|             <div class="email">{% include 'iconmonstr-email-2-icon.svg' %} {{ j.email_address }}</div> | ||||
|             {% endif %} | ||||
|  | ||||
|             <div class="monospace"> | ||||
|             {% include 'iconmonstr-key-2-icon.svg' %} | ||||
|             <span title="SHA-1 of public key"> | ||||
|             {{ j.fingerprint() }} | ||||
|             </span> | ||||
|             {{ j.key_length }}-bit | ||||
|             {{ j.key_type }} | ||||
|             </div> | ||||
|  | ||||
|             <div> | ||||
|             {% include 'iconmonstr-flag-3-icon.svg' %} | ||||
|             {{j.key_usage}} | ||||
|             </div> | ||||
|  | ||||
|  | ||||
|         </li> | ||||
| 	{% endfor %} | ||||
| </ul> | ||||
|  | ||||
| <h1>Revoked certificates</h1> | ||||
|  | ||||
| <p>To fetch certificate revocation list:</p> | ||||
| <pre> | ||||
| curl {{request.url}}/revoked/ | openssl crl -text -noout | ||||
| </pre> | ||||
| <!-- | ||||
| <p>To perform online certificate status request</p> | ||||
|  | ||||
| <pre> | ||||
| curl {{request.url}}/certificate/ > ca.pem | ||||
| openssl ocsp -issuer ca.pem -CAfile ca.pem -url {{request.url}}/ocsp/ -serial 0x | ||||
| </pre> | ||||
| --> | ||||
| <ul> | ||||
|     {% for j in ca.get_revoked() %} | ||||
|         <li id="certificate_{{ j.fingerprint() }}"> | ||||
|             {{j.changed}} | ||||
|             {{j.serial_number}} <span class="monospace">{{j.distinguished_name}}</span> | ||||
|         </li> | ||||
|     {% else %} | ||||
|         <li>Great job! No certificate signing requests to sign.</li> | ||||
| 	{% endfor %} | ||||
| </ul> | ||||
|  | ||||
| </div> | ||||
| </body> | ||||
|  | ||||
| <footer> | ||||
|     <a href="http://github.com/laurivosandi/certidude">Certidude</a> by | ||||
|     <a href="http://github.com/laurivosandi/">Lauri Võsandi</a> | ||||
| </footer> | ||||
|  | ||||
| </html> | ||||
|  | ||||
| @@ -1,7 +1,7 @@ | ||||
| # You have to copy the settings to the system-wide | ||||
| # OpenSSL configuration (usually /etc/ssl/openssl.cnf | ||||
|  | ||||
| [CA_{{slug}}] | ||||
| [CA_{{common_name}}] | ||||
| default_crl_days = {{revocation_list_lifetime}} | ||||
| default_days = {{certificate_lifetime}} | ||||
| dir = {{directory}} | ||||
| @@ -18,8 +18,8 @@ crlDistributionPoints = {{crl_distribution_points}} | ||||
| {% if email_address %} | ||||
| emailAddress = {{email_address}} | ||||
| {% endif %} | ||||
| x509_extensions = {{slug}}_cert | ||||
| policy = policy_{{slug}} | ||||
| x509_extensions = {{common_name}}_cert | ||||
| policy = policy_{{common_name}} | ||||
|  | ||||
| # Certidude specific stuff, TODO: move to separate section? | ||||
| request_subnets = 10.0.0.0/8 192.168.0.0/16 172.168.0.0/16 | ||||
| @@ -28,10 +28,9 @@ admin_subnets = 127.0.0.0/8 | ||||
| admin_users = | ||||
| inbox = {{inbox}} | ||||
| outbox = {{outbox}} | ||||
| publish_certificate_url = {{publish_certificate_url}} | ||||
| subscribe_certificate_url = {{subscribe_certificate_url}} | ||||
| push_server = {{push_server}} | ||||
|  | ||||
| [policy_{{slug}}] | ||||
| [policy_{{common_name}}] | ||||
| countryName = match | ||||
| stateOrProvinceName = match | ||||
| organizationName = match | ||||
| @@ -39,7 +38,7 @@ organizationalUnitName = optional | ||||
| commonName = supplied | ||||
| emailAddress = optional | ||||
|  | ||||
| [{{slug}}_cert] | ||||
| [{{common_name}}_cert] | ||||
| basicConstraints = CA:FALSE | ||||
| keyUsage = nonRepudiation,digitalSignature,keyEncipherment | ||||
| extendedKeyUsage = clientAuth | ||||
|   | ||||
| @@ -84,8 +84,8 @@ class CertificateAuthorityConfig(object): | ||||
|         else: | ||||
|             return default | ||||
|  | ||||
|     def instantiate_authority(self, slug): | ||||
|         section = "CA_" + slug | ||||
|     def instantiate_authority(self, common_name): | ||||
|         section = "CA_" + common_name | ||||
|  | ||||
|         dirs = dict([(key, self.get(section, key)) | ||||
|             for key in ("dir", "certificate", "crl", "certs", "new_certs_dir", "private_key", "revoked_certs_dir", "request_subnets", "autosign_subnets", "admin_subnets", "admin_users", "push_server", "database", "inbox", "outbox")]) | ||||
| @@ -105,7 +105,7 @@ class CertificateAuthorityConfig(object): | ||||
|             dirs["basic_constraints"] = self.get(extensions_section, "basicConstraints") | ||||
|             dirs["key_usage"] = self.get(extensions_section, "keyUsage") | ||||
|             dirs["extended_key_usage"] = self.get(extensions_section, "extendedKeyUsage") | ||||
|         authority = CertificateAuthority(slug, **dirs) | ||||
|         authority = CertificateAuthority(common_name, **dirs) | ||||
|         return authority | ||||
|  | ||||
|  | ||||
| @@ -129,13 +129,16 @@ class CertificateAuthorityConfig(object): | ||||
|     def pop_certificate_authority(self): | ||||
|         def wrapper(func): | ||||
|             def wrapped(*args, **kwargs): | ||||
|                 slug = kwargs.pop("ca") | ||||
|                 kwargs["ca"] = self.instantiate_authority(slug) | ||||
|                 common_name = kwargs.pop("ca") | ||||
|                 kwargs["ca"] = self.instantiate_authority(common_name) | ||||
|                 return func(*args, **kwargs) | ||||
|             return wrapped | ||||
|         return wrapper | ||||
|  | ||||
| class CertificateBase: | ||||
|     def __repr__(self): | ||||
|         return self.buf | ||||
|  | ||||
|     @property | ||||
|     def given_name(self): | ||||
|         return self.subject.GN | ||||
| @@ -433,15 +436,14 @@ class Certificate(CertificateBase): | ||||
|         return self.signed <= other.signed | ||||
|  | ||||
| class CertificateAuthority(object): | ||||
|     def __init__(self, slug, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, private_key=None, autosign_subnets=None, request_subnets=None, admin_subnets=None, admin_users=None, email_address=None, inbox=None, outbox=None, basic_constraints="CA:FALSE", key_usage="digitalSignature,keyEncipherment", extended_key_usage="clientAuth", certificate_lifetime=5*365, revocation_list_lifetime=1, push_server=None, database=None): | ||||
|     def __init__(self, common_name, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, private_key=None, autosign_subnets=None, request_subnets=None, admin_subnets=None, admin_users=None, email_address=None, inbox=None, outbox=None, basic_constraints="CA:FALSE", key_usage="digitalSignature,keyEncipherment", extended_key_usage="clientAuth", certificate_lifetime=5*365, revocation_list_lifetime=1, push_server=None, database=None): | ||||
|  | ||||
|         import hashlib | ||||
|         m = hashlib.sha512() | ||||
|         m.update(slug.encode("ascii")) | ||||
|         m.update(common_name.encode("ascii")) | ||||
|         m.update(b"TODO:server-secret-goes-here") | ||||
|         self.uuid = m.hexdigest() | ||||
|  | ||||
|         self.slug = slug | ||||
|         self.revocation_list = crl | ||||
|         self.signed_dir = certs | ||||
|         self.request_dir = new_certs_dir | ||||
| @@ -476,6 +478,10 @@ class CertificateAuthority(object): | ||||
|             else: | ||||
|                 self.admin_users = set([j for j in admin_users.split(" ") if j]) | ||||
|  | ||||
|     @property | ||||
|     def common_name(self): | ||||
|         return self.certificate.common_name | ||||
|  | ||||
|     @property | ||||
|     def database(self): | ||||
|         from urllib.parse import urlparse | ||||
| @@ -530,14 +536,14 @@ class CertificateAuthority(object): | ||||
|         return buf | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return "CertificateAuthority(slug=%s)" % repr(self.slug) | ||||
|         return "CertificateAuthority(common_name=%s)" % repr(self.common_name) | ||||
|  | ||||
|     def get_certificate(self, cn): | ||||
|         return open(os.path.join(self.signed_dir, cn + ".pem")).read() | ||||
|  | ||||
|     def connect_signer(self): | ||||
|         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||||
|         sock.connect("/run/certidude/signer/%s.sock" % self.slug) | ||||
|         sock.connect("/run/certidude/signer/%s.sock" % self.common_name) | ||||
|         return sock | ||||
|  | ||||
|     def revoke(self, cn): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user