mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	Refactor wrappers
Completely remove wrapper class for CA, use certidude.authority module instead.
This commit is contained in:
		
							
								
								
									
										98
									
								
								certidude/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								certidude/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| import falcon | ||||
| import mimetypes | ||||
| import os | ||||
| import click | ||||
| from time import sleep | ||||
| from certidude import authority | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.decorators import serialize, event_source | ||||
| from certidude.wrappers import Request, Certificate | ||||
| from certidude import config | ||||
|  | ||||
| class CertificateStatusResource(object): | ||||
|     """ | ||||
|     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): | ||||
|         ocsp_request = req.stream.read(req.content_length) | ||||
|         for component in decoder.decode(ocsp_request): | ||||
|             click.echo(component) | ||||
|         resp.append_header("Content-Type", "application/ocsp-response") | ||||
|         resp.status = falcon.HTTP_200 | ||||
|         raise NotImplementedError() | ||||
|  | ||||
|  | ||||
| class CertificateAuthorityResource(object): | ||||
|     def on_get(self, req, resp): | ||||
|         resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=ca.crt") | ||||
|  | ||||
|  | ||||
| class SessionResource(object): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     @event_source | ||||
|     def on_get(self, req, resp): | ||||
|         return dict( | ||||
|             username=req.context.get("user")[0], | ||||
|             event_channel = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN, | ||||
|             autosign_subnets = config.AUTOSIGN_SUBNETS, | ||||
|             request_subnets = config.REQUEST_SUBNETS, | ||||
|             admin_subnets=config.ADMIN_SUBNETS, | ||||
|             admin_users=config.ADMIN_USERS, | ||||
|             requests=authority.list_requests(), | ||||
|             signed=authority.list_signed(), | ||||
|             revoked=authority.list_revoked()) | ||||
|  | ||||
|  | ||||
| class StaticResource(object): | ||||
|     def __init__(self, root): | ||||
|         self.root = os.path.realpath(root) | ||||
|  | ||||
|     def __call__(self, req, resp): | ||||
|  | ||||
|         path = os.path.realpath(os.path.join(self.root, req.path[1:])) | ||||
|         if not path.startswith(self.root): | ||||
|             raise falcon.HTTPForbidden | ||||
|  | ||||
|         if os.path.isdir(path): | ||||
|             path = os.path.join(path, "index.html") | ||||
|         print("Serving:", path) | ||||
|  | ||||
|         if os.path.exists(path): | ||||
|             content_type, content_encoding = mimetypes.guess_type(path) | ||||
|             if content_type: | ||||
|                 resp.append_header("Content-Type", content_type) | ||||
|             if content_encoding: | ||||
|                 resp.append_header("Content-Encoding", content_encoding) | ||||
|             resp.stream = open(path, "rb") | ||||
|         else: | ||||
|             resp.status = falcon.HTTP_404 | ||||
|             resp.body = "File '%s' not found" % req.path | ||||
|  | ||||
|  | ||||
| def certidude_app(): | ||||
|     from .revoked import RevocationListResource | ||||
|     from .signed import SignedCertificateListResource, SignedCertificateDetailResource | ||||
|     from .request import RequestListResource, RequestDetailResource | ||||
|     from .lease import LeaseResource | ||||
|     from .whois import WhoisResource | ||||
|  | ||||
|     app = falcon.API() | ||||
|  | ||||
|     # Certificate authority API calls | ||||
|     app.add_route("/api/ocsp/", CertificateStatusResource()) | ||||
|     app.add_route("/api/certificate/", CertificateAuthorityResource()) | ||||
|     app.add_route("/api/revoked/", RevocationListResource()) | ||||
|     app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource()) | ||||
|     app.add_route("/api/signed/", SignedCertificateListResource()) | ||||
|     app.add_route("/api/request/{cn}/", RequestDetailResource()) | ||||
|     app.add_route("/api/request/", RequestListResource()) | ||||
|     app.add_route("/api/", SessionResource()) | ||||
|  | ||||
|     # Gateway API calls, should this be moved to separate project? | ||||
|     app.add_route("/api/lease/", LeaseResource()) | ||||
|     app.add_route("/api/whois/", WhoisResource()) | ||||
|  | ||||
|     return app | ||||
							
								
								
									
										65
									
								
								certidude/api/lease.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								certidude/api/lease.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
|  | ||||
| from datetime import datetime | ||||
| from pyasn1.codec.der import decoder | ||||
| from certidude import config | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.decorators import serialize | ||||
|  | ||||
| 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()) | ||||
|  | ||||
|  | ||||
| class LeaseResource(object): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp): | ||||
|         from ipaddress import ip_address | ||||
|  | ||||
|         # BUGBUG | ||||
|         SQL_LEASES = """ | ||||
|             SELECT | ||||
|                 acquired, | ||||
|                 released, | ||||
|                 address, | ||||
|                 identities.data as identity | ||||
|             FROM | ||||
|                 addresses | ||||
|             RIGHT JOIN | ||||
|                 identities | ||||
|             ON | ||||
|                 identities.id = addresses.identity | ||||
|             WHERE | ||||
|                 addresses.released <> 1 | ||||
|         """ | ||||
|         cnx = config.DATABASE_POOL.get_connection() | ||||
|         cursor = cnx.cursor() | ||||
|         cursor.execute(SQL_LEASES) | ||||
|  | ||||
|         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)) | ||||
|             } | ||||
|  | ||||
							
								
								
									
										119
									
								
								certidude/api/request.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								certidude/api/request.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
|  | ||||
| import click | ||||
| import falcon | ||||
| import ipaddress | ||||
| import os | ||||
| from certidude import config, authority, helpers, push | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.decorators import serialize | ||||
| from certidude.wrappers import Request, Certificate | ||||
|  | ||||
| class RequestListResource(object): | ||||
|     @serialize | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp): | ||||
|         return helpers.list_requests() | ||||
|  | ||||
|     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"]) | ||||
|  | ||||
|         # Check for CSR submission whitelist | ||||
|         if config.REQUEST_SUBNETS: | ||||
|             for subnet in config.REQUEST_SUBNETS: | ||||
|                 if subnet.overlaps(remote_addr): | ||||
|                     break | ||||
|             else: | ||||
|                raise falcon.HTTPForbidden("Forbidden", "IP address %s not whitelisted" % remote_addr) | ||||
|  | ||||
|         if req.get_header("Content-Type") != "application/pkcs10": | ||||
|             raise falcon.HTTPUnsupportedMediaType( | ||||
|                 "This API call accepts only application/pkcs10 content type") | ||||
|  | ||||
|         body = req.stream.read(req.content_length) | ||||
|         csr = Request(body) | ||||
|  | ||||
|         # Check if this request has been already signed and return corresponding certificte if it has been signed | ||||
|         try: | ||||
|             cert = authority.get_signed(csr.common_name) | ||||
|         except FileNotFoundError: | ||||
|             pass | ||||
|         else: | ||||
|             if cert.pubkey == csr.pubkey: | ||||
|                 resp.status = falcon.HTTP_SEE_OTHER | ||||
|                 resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", csr.common_name) | ||||
|                 return | ||||
|  | ||||
|         # TODO: check for revoked certificates and return HTTP 410 Gone | ||||
|  | ||||
|         # Process automatic signing if the IP address is whitelisted and autosigning was requested | ||||
|         if req.get_param_as_bool("autosign"): | ||||
|             for subnet in config.AUTOSIGN_SUBNETS: | ||||
|                 if subnet.overlaps(remote_addr): | ||||
|                     try: | ||||
|                         resp.set_header("Content-Type", "application/x-x509-user-cert") | ||||
|                         resp.body = authority.sign(csr).dump() | ||||
|                         return | ||||
|                     except FileExistsError: # Certificate already exists, try to save the request | ||||
|                         pass | ||||
|                     break | ||||
|  | ||||
|         # Attempt to save the request otherwise | ||||
|         try: | ||||
|             csr = authority.store_request(body) | ||||
|         except FileExistsError: | ||||
|             raise falcon.HTTPConflict( | ||||
|                 "CSR with such CN already exists", | ||||
|                 "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") | ||||
|         push.publish("request_submitted", csr.common_name) | ||||
|  | ||||
|         # Wait the certificate to be signed if waiting is requested | ||||
|         if req.get_param("wait"): | ||||
|             # Redirect to nginx pub/sub | ||||
|             url = config.PUSH_LONG_POLL % csr.fingerprint() | ||||
|             click.echo("Redirecting to: %s"  % url) | ||||
|             resp.status = falcon.HTTP_SEE_OTHER | ||||
|             resp.set_header("Location", url) | ||||
|         else: | ||||
|             # Request was accepted, but not processed | ||||
|             resp.status = falcon.HTTP_202 | ||||
|  | ||||
|  | ||||
| class RequestDetailResource(object): | ||||
|     @serialize | ||||
|     def on_get(self, req, resp, cn): | ||||
|         """ | ||||
|         Fetch certificate signing request as PEM | ||||
|         """ | ||||
|         csr = authority.get_request(cn) | ||||
| #        if not os.path.exists(path): | ||||
| #            raise falcon.HTTPNotFound() | ||||
|  | ||||
|         resp.set_header("Content-Type", "application/pkcs10") | ||||
|         resp.set_header("Content-Disposition", "attachment; filename=%s.csr" % csr.common_name) | ||||
|         return csr | ||||
|  | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_patch(self, req, resp, cn): | ||||
|         """ | ||||
|         Sign a certificate signing request | ||||
|         """ | ||||
|         csr = authority.get_request(cn) | ||||
|         cert = authority.sign(csr, overwrite=True, delete=True) | ||||
|         os.unlink(csr.path) | ||||
|         resp.body = "Certificate successfully signed" | ||||
|         resp.status = falcon.HTTP_201 | ||||
|         resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn) | ||||
|  | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_delete(self, req, resp, cn): | ||||
|         try: | ||||
|             authority.delete_request(cn) | ||||
|         except FileNotFoundError: | ||||
|             resp.body = "No certificate CN=%s found" % cn | ||||
|             raise falcon.HTTPNotFound() | ||||
							
								
								
									
										9
									
								
								certidude/api/revoked.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								certidude/api/revoked.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
|  | ||||
| from certidude.authority import export_crl | ||||
|  | ||||
| class RevocationListResource(object): | ||||
|     def on_get(self, req, resp): | ||||
|         resp.set_header("Content-Type", "application/x-pkcs7-crl") | ||||
|         resp.append_header("Content-Disposition", "attachment; filename=ca.crl") | ||||
|         resp.body = export_crl() | ||||
|  | ||||
							
								
								
									
										38
									
								
								certidude/api/signed.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								certidude/api/signed.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
|  | ||||
| import falcon | ||||
| from certidude import authority | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.decorators import serialize | ||||
|  | ||||
| class SignedCertificateListResource(object): | ||||
|     @serialize | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp): | ||||
|         for j in authority.list_signed(): | ||||
|             yield omit( | ||||
|                 key_type=j.key_type, | ||||
|                 key_length=j.key_length, | ||||
|                 identity=j.identity, | ||||
|                 cn=j.common_name, | ||||
|                 c=j.country_code, | ||||
|                 st=j.state_or_county, | ||||
|                 l=j.city, | ||||
|                 o=j.organization, | ||||
|                 ou=j.organizational_unit, | ||||
|                 fingerprint=j.fingerprint()) | ||||
|  | ||||
|  | ||||
| class SignedCertificateDetailResource(object): | ||||
|     @serialize | ||||
|     def on_get(self, req, resp, cn): | ||||
|         try: | ||||
|             return authority.get_signed(cn) | ||||
|         except FileNotFoundError: | ||||
|             resp.body = "No certificate CN=%s found" % cn | ||||
|             raise falcon.HTTPNotFound() | ||||
|  | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_delete(self, req, resp, cn): | ||||
|         authority.revoke_certificate(cn) | ||||
|  | ||||
							
								
								
									
										52
									
								
								certidude/api/whois.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								certidude/api/whois.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
|  | ||||
| import falcon | ||||
| import ipaddress | ||||
| from certidude import config | ||||
| from certidude.decorators import serialize | ||||
|  | ||||
| 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() | ||||
|     import struct | ||||
|     cursor.execute(SQL_LEASES, (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(object): | ||||
|     @serialize | ||||
|     def on_get(self, req, resp): | ||||
|         identity = address_to_identity( | ||||
|             config.DATABASE_POOL.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"] | ||||
		Reference in New Issue
	
	Block a user