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.
* 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.

View File

@ -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
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
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

View File

@ -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):

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")
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)

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)
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")

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