1
0
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:
2015-12-12 22:34:08 +00:00
parent 5876f61e15
commit b788d701eb
23 changed files with 1165 additions and 1439 deletions

98
certidude/api/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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"]