mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
api: Preliminary OCSP support
This commit is contained in:
parent
5ae872e1ea
commit
5d48abe973
@ -69,6 +69,7 @@ Common:
|
||||
|
||||
* Standard request, sign, revoke workflow via web interface.
|
||||
* Kerberos and basic auth based web interface authentication.
|
||||
* Preliminary `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support.
|
||||
* PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind.
|
||||
* POSIX groups and Active Directory (LDAP) group membership based authorization.
|
||||
* Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``.
|
||||
@ -94,8 +95,6 @@ HTTPS:
|
||||
TODO
|
||||
----
|
||||
|
||||
* `OCSP <https://tools.ietf.org/html/rfc4557>`_ support, needs a bit hacking since OpenSSL wrappers are not exposing the functionality.
|
||||
* `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support, a client implementation available `here <https://github.com/certnanny/sscep>`_. Not sure if we can implement server-side events within current standard.
|
||||
* WebCrypto support, meanwhile check out `hwcrypto.js <https://github.com/open-eid/hwcrypto.js>`_.
|
||||
* Use `pki.js <https://pkijs.org/>`_ for generating keypair in the browser when claiming a token.
|
||||
* Signer process logging.
|
||||
|
@ -215,6 +215,10 @@ def certidude_app(log_handlers=[]):
|
||||
from .scep import SCEPResource
|
||||
app.add_route("/api/scep/", SCEPResource())
|
||||
|
||||
if config.OCSP_SUBNETS:
|
||||
from .ocsp import OCSPResource
|
||||
app.add_route("/api/ocsp/", OCSPResource())
|
||||
|
||||
# Add sink for serving static files
|
||||
app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static")))
|
||||
|
||||
|
97
certidude/api/ocsp.py
Normal file
97
certidude/api/ocsp.py
Normal file
@ -0,0 +1,97 @@
|
||||
from __future__ import unicode_literals, division, absolute_import, print_function
|
||||
import click
|
||||
import hashlib
|
||||
import os
|
||||
from asn1crypto.util import timezone
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from asn1crypto import cms, algos, x509, ocsp
|
||||
from base64 import b64decode, b64encode
|
||||
from certbuilder import pem_armor_certificate
|
||||
from certidude import authority, push, config
|
||||
from certidude.firewall import whitelist_subnets
|
||||
from oscrypto import keys, asymmetric, symmetric
|
||||
from oscrypto.errors import SignatureError
|
||||
|
||||
# openssl ocsp -issuer /var/lib/certidude/ca2.koodur.lan/ca_crt.pem -cert /var/lib/certidude/ca2.koodur.lan/signed/lauri-c720p.pem -text -url http://ca2.koodur.lan/api/ocsp/
|
||||
|
||||
class OCSPResource(object):
|
||||
def on_post(self, req, resp):
|
||||
fh = open(config.AUTHORITY_CERTIFICATE_PATH)
|
||||
server_certificate = asymmetric.load_certificate(fh.read())
|
||||
fh.close()
|
||||
|
||||
ocsp_req = ocsp.OCSPRequest.load(req.stream.read())
|
||||
print(ocsp_req["tbs_request"].native)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
response_extensions = []
|
||||
|
||||
for ext in ocsp_req["tbs_request"]["request_extensions"]:
|
||||
if ext["extn_id"] == "nonce":
|
||||
response_extensions.append(
|
||||
ocsp.ResponseDataExtension({
|
||||
'extn_id': "nonce",
|
||||
'critical': False,
|
||||
'extn_value': ext["extn_value"]
|
||||
})
|
||||
)
|
||||
|
||||
responses = []
|
||||
for item in ocsp_req["tbs_request"]["request_list"]:
|
||||
serial = item["req_cert"]["serial_number"].native
|
||||
|
||||
try:
|
||||
link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % serial))
|
||||
assert link_target.startswith("../")
|
||||
assert link_target.endswith(".pem")
|
||||
path, buf, cert = authority.get_signed(link_target[3:-4])
|
||||
if serial != cert.serial:
|
||||
raise EnvironmentError("integrity check failed")
|
||||
status = ocsp.CertStatus(name='good', value=None)
|
||||
except EnvironmentError:
|
||||
try:
|
||||
path, buf, cert, revoked = authority.get_revoked(serial)
|
||||
status = ocsp.CertStatus(
|
||||
name='revoked',
|
||||
value={
|
||||
'revocation_time': revoked,
|
||||
'revocation_reason': "key_compromise",
|
||||
})
|
||||
except EnvironmentError:
|
||||
status = ocsp.CertStatus(name="unknown", value=None)
|
||||
|
||||
responses.append({
|
||||
'cert_id': {
|
||||
'hash_algorithm': {
|
||||
'algorithm': "sha1"
|
||||
},
|
||||
'issuer_name_hash': server_certificate.asn1.subject.sha1,
|
||||
'issuer_key_hash': server_certificate.public_key.asn1.sha1,
|
||||
'serial_number': serial,
|
||||
},
|
||||
'cert_status': status,
|
||||
'this_update': now,
|
||||
'single_extensions': []
|
||||
})
|
||||
|
||||
response_data = ocsp.ResponseData({
|
||||
'responder_id': ocsp.ResponderId(name='by_key', value=server_certificate.public_key.asn1.sha1),
|
||||
'produced_at': now,
|
||||
'responses': responses,
|
||||
'response_extensions': response_extensions
|
||||
})
|
||||
|
||||
resp.body = ocsp.OCSPResponse({
|
||||
'response_status': "successful",
|
||||
'response_bytes': {
|
||||
'response_type': 'basic_ocsp_response',
|
||||
'response': {
|
||||
'tbs_response_data': response_data,
|
||||
'signature_algorithm': {'algorithm': "sha1_rsa"},
|
||||
'signature': b64decode(authority.signer_exec("sign-pkcs7", b64encode(response_data.dump()))),
|
||||
'certs': [server_certificate.asn1]
|
||||
}
|
||||
}
|
||||
}).dump()
|
||||
|
@ -139,7 +139,7 @@ class RequestListResource(object):
|
||||
|
||||
# Attempt to save the request otherwise
|
||||
try:
|
||||
csr = authority.store_request(body)
|
||||
csr = authority.store_request(body.decode("ascii"))
|
||||
except errors.RequestExists:
|
||||
reasons.append("Same request already uploaded exists")
|
||||
# We should still redirect client to long poll URL below
|
||||
|
@ -50,10 +50,11 @@ def get_signed(common_name):
|
||||
return path, buf, x509.load_pem_x509_certificate(buf, default_backend())
|
||||
|
||||
def get_revoked(serial):
|
||||
path = os.path.join(config.REVOKED_DIR, serial + ".pem")
|
||||
path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial)
|
||||
with open(path) as fh:
|
||||
buf = fh.read()
|
||||
return path, buf, x509.load_pem_x509_certificate(buf, default_backend())
|
||||
return path, buf, x509.load_pem_x509_certificate(buf, default_backend()), \
|
||||
datetime.utcfromtimestamp(os.stat(path).st_ctime)
|
||||
|
||||
|
||||
def get_attributes(cn):
|
||||
@ -83,7 +84,7 @@ def store_request(buf, overwrite=False):
|
||||
raise ValueError("No signing request supplied")
|
||||
|
||||
if isinstance(buf, unicode):
|
||||
csr = x509.load_pem_x509_csr(buf, backend=default_backend())
|
||||
csr = x509.load_pem_x509_csr(buf.encode("ascii"), backend=default_backend())
|
||||
elif isinstance(buf, str):
|
||||
csr = x509.load_der_x509_csr(buf, backend=default_backend())
|
||||
buf = csr.public_bytes(Encoding.PEM)
|
||||
@ -134,10 +135,11 @@ def revoke(common_name):
|
||||
"""
|
||||
Revoke valid certificate
|
||||
"""
|
||||
path, buf, cert = get_signed(common_name)
|
||||
signed_path, buf, cert = get_signed(common_name)
|
||||
revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial)
|
||||
signed_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name)
|
||||
os.rename(signed_path, revoked_path)
|
||||
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial))
|
||||
|
||||
push.publish("certificate-revoked", common_name)
|
||||
|
||||
# Publish CRL for long polls
|
||||
@ -362,6 +364,11 @@ def _sign(csr, buf, overwrite=False):
|
||||
attachments.append((cert_buf, "application/x-pem-file", common_name.value + ".crt"))
|
||||
cert_serial_hex = "%x" % cert.serial
|
||||
|
||||
# Create symlink
|
||||
os.symlink(
|
||||
"../%s.pem" % common_name.value,
|
||||
os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial))
|
||||
|
||||
# Copy filesystem attributes to newly signed certificate
|
||||
if revoked_path:
|
||||
for key in listxattr(revoked_path):
|
||||
|
@ -736,9 +736,8 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
||||
pip("gssapi falcon cryptography humanize ipaddress simplepam humanize requests pyopenssl")
|
||||
click.echo("Software dependencies installed")
|
||||
|
||||
if not os.path.exists("/etc/apt/sources.list.d/nginx-stable-trusty.list"):
|
||||
os.system("add-apt-repository -y ppa:nginx/stable")
|
||||
os.system("apt-get update")
|
||||
os.system("add-apt-repository -y ppa:nginx/stable")
|
||||
os.system("apt-get update")
|
||||
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
|
||||
os.system("apt-get install -y libnginx-mod-nchan")
|
||||
if not os.path.exists("/usr/sbin/nginx"):
|
||||
@ -773,6 +772,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
||||
# Expand variables
|
||||
ca_key = os.path.join(directory, "ca_key.pem")
|
||||
ca_crt = os.path.join(directory, "ca_crt.pem")
|
||||
sqlite_path = os.path.join(directory, "meta", "db.sqlite")
|
||||
|
||||
try:
|
||||
pwd.getpwnam("certidude")
|
||||
@ -858,11 +858,29 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
||||
fh.write(env.get_template("server/server.conf").render(vars()))
|
||||
click.echo("Generated %s" % const.CONFIG_PATH)
|
||||
|
||||
if os.path.lexists(directory):
|
||||
click.echo("CA directory %s already exists, remove to regenerate" % directory)
|
||||
else:
|
||||
click.echo("CA configuration files are saved to: {}".format(directory))
|
||||
# Create directories with 770 permissions
|
||||
os.umask(0o027)
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
# Create subdirectories with 770 permissions
|
||||
os.umask(0o007)
|
||||
for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta"):
|
||||
path = os.path.join(directory, subdir)
|
||||
if not os.path.exists(path):
|
||||
click.echo("Creating directory %s" % path)
|
||||
os.mkdir(path)
|
||||
else:
|
||||
click.echo("Directory already exists %s" % path)
|
||||
|
||||
# Create SQLite database file with correct permissions
|
||||
if not os.path.exists(sqlite_path):
|
||||
os.umask(0o117)
|
||||
with open(sqlite_path, "wb") as fh:
|
||||
pass
|
||||
|
||||
# Generate and sign CA key
|
||||
if not os.path.exists(ca_key):
|
||||
click.echo("Generating %d-bit RSA key..." % const.KEY_SIZE)
|
||||
|
||||
key = rsa.generate_private_key(
|
||||
@ -920,22 +938,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
||||
|
||||
click.echo("Signing %s..." % cert.subject)
|
||||
|
||||
# Create directories with 770 permissions
|
||||
os.umask(0o027)
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
# Create subdirectories with 770 permissions
|
||||
os.umask(0o007)
|
||||
for subdir in ("signed", "requests", "revoked", "expired", "meta"):
|
||||
if not os.path.exists(os.path.join(directory, subdir)):
|
||||
os.mkdir(os.path.join(directory, subdir))
|
||||
|
||||
# Create SQLite database file with correct permissions
|
||||
os.umask(0o117)
|
||||
with open(os.path.join(directory, "meta", "db.sqlite"), "wb") as fh:
|
||||
pass
|
||||
|
||||
# Set permission bits to 640
|
||||
os.umask(0o137)
|
||||
with open(ca_crt, "wb") as fh:
|
||||
@ -1106,6 +1108,13 @@ def certidude_serve(port, listen, fork, exit_handler):
|
||||
|
||||
from certidude import config
|
||||
|
||||
# Rebuild reverse mapping
|
||||
for cn, path, buf, cert, server in authority.list_signed():
|
||||
by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial)
|
||||
if not os.path.exists(by_serial):
|
||||
click.echo("Linking %s to ../%s.pem" % (by_serial, cn))
|
||||
os.symlink("../%s.pem" % cn, by_serial)
|
||||
|
||||
# Process directories
|
||||
if not os.path.exists(const.RUN_DIR):
|
||||
click.echo("Creating: %s" % const.RUN_DIR)
|
||||
|
@ -34,12 +34,15 @@ REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS)
|
||||
SCEP_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
cp.get("authorization", "scep subnets").split(" ") if j])
|
||||
OCSP_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
cp.get("authorization", "ocsp subnets").split(" ") if j])
|
||||
|
||||
AUTHORITY_DIR = "/var/lib/certidude"
|
||||
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
|
||||
AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path")
|
||||
REQUESTS_DIR = cp.get("authority", "requests dir")
|
||||
SIGNED_DIR = cp.get("authority", "signed dir")
|
||||
SIGNED_BY_SERIAL_DIR = os.path.join(SIGNED_DIR, "by-serial")
|
||||
REVOKED_DIR = cp.get("authority", "revoked dir")
|
||||
EXPIRED_DIR = cp.get("authority", "expired dir")
|
||||
|
||||
|
@ -59,6 +59,9 @@ autosign subnets = 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
||||
# Simple Certificate Enrollment Protocol enabled subnets
|
||||
scep subnets =
|
||||
|
||||
# Online Certificate Status Protocol enabled subnets
|
||||
ocsp subnets = 0.0.0.0/0
|
||||
|
||||
[logging]
|
||||
# Disable logging
|
||||
;backend =
|
||||
|
Loading…
Reference in New Issue
Block a user