mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-23 00:25:18 +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.
|
* 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.
|
||||||
|
@ -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
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
|
# 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
|
||||||
|
@ -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):
|
||||||
|
@ -736,7 +736,6 @@ 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"):
|
||||||
@ -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)
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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 =
|
||||||
|
Loading…
Reference in New Issue
Block a user