1
0
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:
Lauri Võsandi 2017-05-25 22:20:29 +03:00
parent 5ae872e1ea
commit 5d48abe973
8 changed files with 153 additions and 31 deletions

View File

@ -69,6 +69,7 @@ Common:
* Standard request, sign, revoke workflow via web interface. * Standard request, sign, revoke workflow via web interface.
* Kerberos and basic auth based web interface authentication. * 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. * PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind.
* POSIX groups and Active Directory (LDAP) group membership based authorization. * POSIX groups and Active Directory (LDAP) group membership based authorization.
* Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``. * Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``.
@ -94,8 +95,6 @@ HTTPS:
TODO 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>`_. * 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. * Use `pki.js <https://pkijs.org/>`_ for generating keypair in the browser when claiming a token.
* Signer process logging. * Signer process logging.

View File

@ -215,6 +215,10 @@ def certidude_app(log_handlers=[]):
from .scep import SCEPResource from .scep import SCEPResource
app.add_route("/api/scep/", 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 # Add sink for serving static files
app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static")))

97
certidude/api/ocsp.py Normal file
View 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()

View File

@ -139,7 +139,7 @@ class RequestListResource(object):
# Attempt to save the request otherwise # Attempt to save the request otherwise
try: try:
csr = authority.store_request(body) csr = authority.store_request(body.decode("ascii"))
except errors.RequestExists: except errors.RequestExists:
reasons.append("Same request already uploaded exists") reasons.append("Same request already uploaded exists")
# We should still redirect client to long poll URL below # We should still redirect client to long poll URL below

View File

@ -50,10 +50,11 @@ def get_signed(common_name):
return path, buf, x509.load_pem_x509_certificate(buf, default_backend()) return path, buf, x509.load_pem_x509_certificate(buf, default_backend())
def get_revoked(serial): 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: with open(path) as fh:
buf = fh.read() 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): def get_attributes(cn):
@ -83,7 +84,7 @@ def store_request(buf, overwrite=False):
raise ValueError("No signing request supplied") raise ValueError("No signing request supplied")
if isinstance(buf, unicode): 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): elif isinstance(buf, str):
csr = x509.load_der_x509_csr(buf, backend=default_backend()) csr = x509.load_der_x509_csr(buf, backend=default_backend())
buf = csr.public_bytes(Encoding.PEM) buf = csr.public_bytes(Encoding.PEM)
@ -134,10 +135,11 @@ def revoke(common_name):
""" """
Revoke valid certificate 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) 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.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) push.publish("certificate-revoked", common_name)
# Publish CRL for long polls # 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")) attachments.append((cert_buf, "application/x-pem-file", common_name.value + ".crt"))
cert_serial_hex = "%x" % cert.serial 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 # Copy filesystem attributes to newly signed certificate
if revoked_path: if revoked_path:
for key in listxattr(revoked_path): for key in listxattr(revoked_path):

View File

@ -736,9 +736,8 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
pip("gssapi falcon cryptography humanize ipaddress simplepam humanize requests pyopenssl") pip("gssapi falcon cryptography humanize ipaddress simplepam humanize requests pyopenssl")
click.echo("Software dependencies installed") 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("add-apt-repository -y ppa:nginx/stable") os.system("apt-get update")
os.system("apt-get update")
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
os.system("apt-get install -y libnginx-mod-nchan") os.system("apt-get install -y libnginx-mod-nchan")
if not os.path.exists("/usr/sbin/nginx"): if not os.path.exists("/usr/sbin/nginx"):
@ -773,6 +772,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
# Expand variables # Expand variables
ca_key = os.path.join(directory, "ca_key.pem") ca_key = os.path.join(directory, "ca_key.pem")
ca_crt = os.path.join(directory, "ca_crt.pem") ca_crt = os.path.join(directory, "ca_crt.pem")
sqlite_path = os.path.join(directory, "meta", "db.sqlite")
try: try:
pwd.getpwnam("certidude") 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())) fh.write(env.get_template("server/server.conf").render(vars()))
click.echo("Generated %s" % const.CONFIG_PATH) click.echo("Generated %s" % const.CONFIG_PATH)
if os.path.lexists(directory): # Create directories with 770 permissions
click.echo("CA directory %s already exists, remove to regenerate" % directory) os.umask(0o027)
else: if not os.path.exists(directory):
click.echo("CA configuration files are saved to: {}".format(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) click.echo("Generating %d-bit RSA key..." % const.KEY_SIZE)
key = rsa.generate_private_key( 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) 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 # Set permission bits to 640
os.umask(0o137) os.umask(0o137)
with open(ca_crt, "wb") as fh: with open(ca_crt, "wb") as fh:
@ -1106,6 +1108,13 @@ def certidude_serve(port, listen, fork, exit_handler):
from certidude import config 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 # Process directories
if not os.path.exists(const.RUN_DIR): if not os.path.exists(const.RUN_DIR):
click.echo("Creating: %s" % const.RUN_DIR) click.echo("Creating: %s" % const.RUN_DIR)

View File

@ -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) cp.get("authorization", "request subnets").split(" ") if j]).union(AUTOSIGN_SUBNETS)
SCEP_SUBNETS = set([ipaddress.ip_network(j) for j in SCEP_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "scep subnets").split(" ") if j]) 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_DIR = "/var/lib/certidude"
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path") AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path")
REQUESTS_DIR = cp.get("authority", "requests dir") REQUESTS_DIR = cp.get("authority", "requests dir")
SIGNED_DIR = cp.get("authority", "signed 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") REVOKED_DIR = cp.get("authority", "revoked dir")
EXPIRED_DIR = cp.get("authority", "expired dir") EXPIRED_DIR = cp.get("authority", "expired dir")

View File

@ -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 # Simple Certificate Enrollment Protocol enabled subnets
scep subnets = scep subnets =
# Online Certificate Status Protocol enabled subnets
ocsp subnets = 0.0.0.0/0
[logging] [logging]
# Disable logging # Disable logging
;backend = ;backend =