1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 16:25:17 +00:00
* Remove given name and surname attributes because of issues with OpenVPN Connect
* Remove e-mail attribute because of no reliable method of deriving usable address
* Remove organizational unit attribute
* Don't overwrite Kerberos cronjob during certidude setup authority
* Enforce path_length=0 for disabling intermediate CA-s
* Remove SAN attributes
* Add configuration options for outbox sender name and address
* Use common name attribute to derive signature flags
* Use distinct pub/sub URL-s for long poll and event source
This commit is contained in:
Lauri Võsandi 2017-02-07 22:07:21 +00:00
parent 703970c1d3
commit 2a8109704a
15 changed files with 160 additions and 265 deletions

View File

@ -59,7 +59,7 @@ class SessionResource(object):
user_mutliple_certificates=config.USER_MULTIPLE_CERTIFICATES,
outbox = config.OUTBOX,
certificate = authority.certificate,
events = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN,
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
requests=authority.list_requests(),
signed=authority.list_signed(),
revoked=authority.list_revoked(),

View File

@ -52,11 +52,11 @@ class RequestListResource(object):
raise falcon.HTTPBadRequest(
"Bad request",
"Common name %s differs from Kerberos credential %s!" % (csr.common_name, machine))
if csr.signable:
# Automatic enroll with Kerberos machine cerdentials
resp.set_header("Content-Type", "application/x-x509-user-cert")
resp.body = authority.sign(csr, overwrite=True).dump()
return
# Automatic enroll with Kerberos machine cerdentials
resp.set_header("Content-Type", "application/x-x509-user-cert")
resp.body = authority.sign(csr, overwrite=True).dump()
return
# Check if this request has been already signed and return corresponding certificte if it has been signed
@ -73,7 +73,7 @@ class RequestListResource(object):
# TODO: check for revoked certificates and return HTTP 410 Gone
# Process automatic signing if the IP address is whitelisted, autosigning was requested and certificate can be automatically signed
if req.get_param_as_bool("autosign") and csr.signable:
if req.get_param_as_bool("autosign") and csr.is_client:
for subnet in config.AUTOSIGN_SUBNETS:
if req.context.get("remote_addr") in subnet:
try:
@ -103,7 +103,7 @@ class RequestListResource(object):
# 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()
url = config.LONG_POLL_SUBSCRIBE % csr.fingerprint()
click.echo("Redirecting to: %s" % url)
resp.status = falcon.HTTP_SEE_OTHER
resp.set_header("Location", url.encode("ascii"))

View File

@ -27,7 +27,7 @@ class RevocationListResource(object):
default_backend()).public_bytes(Encoding.DER)
elif req.client_accepts("application/x-pem-file"):
if req.get_param_as_bool("wait"):
url = config.PUSH_LONG_POLL % "crl"
url = config.LONG_POLL_SUBSCRIBE % "crl"
resp.status = falcon.HTTP_SEE_OTHER
resp.set_header("Location", url.encode("ascii"))
logger.debug(u"Redirecting to CRL request to %s", url)

View File

@ -31,12 +31,7 @@ def publish_certificate(func):
cert = func(csr, *args, **kwargs)
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
if cert.given_name and cert.surname and cert.email_address:
recipient = "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address)
elif cert.email_address:
recipient = cert.email_address
else:
recipient = None
recipient = None
mailer.send(
"certificate-signed.md",
@ -44,8 +39,8 @@ def publish_certificate(func):
attachments=(cert,),
certificate=cert)
if config.PUSH_PUBLISH:
url = config.PUSH_PUBLISH % csr.fingerprint()
if config.LONG_POLL_PUBLISH:
url = config.LONG_POLL_PUBLISH % csr.fingerprint()
click.echo("Publishing certificate at %s ..." % url)
requests.post(url, data=cert.dump(),
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
@ -133,8 +128,8 @@ def revoke_certificate(common_name):
push.publish("certificate-revoked", cert.common_name)
# Publish CRL for long polls
if config.PUSH_PUBLISH:
url = config.PUSH_PUBLISH % "crl"
if config.LONG_POLL_PUBLISH:
url = config.LONG_POLL_PUBLISH % "crl"
click.echo("Publishing CRL at %s ..." % url)
requests.post(url, data=export_crl(),
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"})
@ -190,7 +185,7 @@ def delete_request(common_name):
push.publish("request-deleted", request.common_name)
# Write empty certificate to long-polling URL
requests.delete(config.PUSH_PUBLISH % request.fingerprint(),
requests.delete(config.LONG_POLL_PUBLISH % request.fingerprint(),
headers={"User-Agent": "Certidude API"})
def generate_ovpn_bundle(common_name, owner=None):
@ -206,8 +201,6 @@ def generate_ovpn_bundle(common_name, owner=None):
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
x509.NameAttribute(k, v) for k, v in (
(NameOID.COMMON_NAME, common_name),
(NameOID.GIVEN_NAME, owner and owner.given_name),
(NameOID.SURNAME, owner and owner.surname),
) if v
]))
@ -244,8 +237,6 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
x509.NameAttribute(k, v) for k, v in (
(NameOID.COMMON_NAME, common_name),
(NameOID.GIVEN_NAME, owner and owner.given_name),
(NameOID.SURNAME, owner and owner.surname),
) if v
]))
@ -262,21 +253,26 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
csr.sign(key, hashes.SHA512(), default_backend()).public_bytes(serialization.Encoding.PEM)), overwrite=True)
# Generate P12, currently supported only by PyOpenSSL
from OpenSSL import crypto
p12 = crypto.PKCS12()
p12.set_privatekey(
crypto.load_privatekey(
crypto.FILETYPE_PEM,
key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
try:
from OpenSSL import crypto
except ImportError:
logger.error("For P12 bundles please install pyOpenSSL: pip install pyOpenSSL")
raise
else:
p12 = crypto.PKCS12()
p12.set_privatekey(
crypto.load_privatekey(
crypto.FILETYPE_PEM,
key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
)
)
)
p12.set_certificate( cert._obj )
p12.set_ca_certificates([certificate._obj])
return p12.export(), cert
p12.set_certificate( cert._obj )
p12.set_ca_certificates([certificate._obj])
return p12.export(), cert
@publish_certificate

View File

@ -305,8 +305,6 @@ def certidude_request(fork):
@click.command("client", help="Setup X.509 certificates for application")
@click.argument("server")
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, '%s' by default" % const.HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@click.option("--given-name", "-gn", default=FIRST_NAME, help="Given name of the person associted with the certificate, '%s' by default" % FIRST_NAME)
@click.option("--surname", "-sn", default=SURNAME, help="Surname of the person associted with the certificate, '%s' by default" % SURNAME)
@click.option("--key-usage", "-ku", help="Key usage attributes, none requested by default")
@ -325,8 +323,6 @@ def certidude_setup_client(quiet, **kwargs):
@click.command("server", help="Set up OpenVPN server")
@click.argument("authority")
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default")
@click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces")
@click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default")
@ -336,7 +332,7 @@ def certidude_setup_client(quiet, **kwargs):
default="/etc/openvpn/site-to-client.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file")
def certidude_setup_openvpn_server(authority, config, subnet, route, email_address, org_unit, local, proto, port):
def certidude_setup_openvpn_server(authority, config, subnet, route, org_unit, local, proto, port):
# TODO: Make dirs
# TODO: Intelligent way of getting last IP address in the subnet
@ -428,7 +424,6 @@ def certidude_setup_openvpn_server(authority, config, subnet, route, email_addre
@click.command("nginx", help="Set up nginx as HTTPS server")
@click.argument("server")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--tls-config",
default="/etc/nginx/conf.d/tls.conf",
type=click.File(mode="w", atomic=True, lazy=True),
@ -496,7 +491,6 @@ def certidude_setup_nginx(authority, site_config, tls_config, common_name, org_u
@click.argument("authority")
@click.argument("remote")
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--config", "-o",
default="/etc/openvpn/client-to-site.conf",
type=click.File(mode="w", atomic=True, lazy=True),
@ -568,12 +562,10 @@ def certidude_setup_openvpn_client(authority, remote, config, org_unit, proto):
@click.command("server", help="Set up strongSwan server")
@click.argument("server")
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL)
@click.option("--subnet", "-sn", default=u"192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
@click.option("--local", "-l", type=ip_address, help="IP address associated with the certificate, none by default")
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
def certidude_setup_strongswan_server(authority, config, secrets, subnet, route, email_address, org_unit, local, fqdn):
def certidude_setup_strongswan_server(authority, config, secrets, subnet, route, local, fqdn):
if "." not in common_name:
raise ValueError("Hostname has to be fully qualified!")
if not local:
@ -627,7 +619,6 @@ def certidude_setup_strongswan_server(authority, config, secrets, subnet, route,
@click.command("client", help="Set up strongSwan client")
@click.argument("server")
@click.argument("remote")
@click.option("--org-unit", "-ou", help="Organizational unit")
def certidude_setup_strongswan_client(authority, config, org_unit, remote, dpdaction):
# Create corresponding section in /etc/certidude/client.conf
client_config = ConfigParser()
@ -675,7 +666,6 @@ def certidude_setup_strongswan_client(authority, config, org_unit, remote, dpdac
@click.command("networkmanager", help="Set up strongSwan client via NetworkManager")
@click.argument("server") # Certidude server
@click.argument("remote") # StrongSwan gateway
@click.option("--org-unit", "-ou", help="Organizational unit")
def certidude_setup_strongswan_networkmanager(server,remote, org_unit):
endpoint = "IPSec to %s" % remote
@ -721,9 +711,7 @@ def certidude_setup_strongswan_networkmanager(server,remote, org_unit):
@click.argument("server") # Certidude server
@click.argument("remote") # OpenVPN gateway
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", help="E-mail associated with the request, none by default")
def certidude_setup_openvpn_networkmanager(authority, email_address, org_unit, remote):
def certidude_setup_openvpn_networkmanager(authority, org_unit, remote):
# Create corresponding section in /etc/certidude/client.conf
client_config = ConfigParser()
if os.path.exists(const.CLIENT_CONFIG_PATH):
@ -781,11 +769,10 @@ def certidude_setup_openvpn_networkmanager(authority, email_address, org_unit, r
@click.option("--revoked-url", default=None, help="CRL distribution URL")
@click.option("--certificate-url", default=None, help="Authority certificate URL")
@click.option("--push-server", default="http://" + const.FQDN, help="Push server, by default http://%s" % const.FQDN)
@click.option("--email-address", default="certidude@" + const.FQDN, help="E-mail address of the CA")
@click.option("--directory", help="Directory for authority files")
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_config, parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, revoked_url, certificate_url, push_server, email_address, outbox, server_flags):
def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_config, parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, revoked_url, certificate_url, push_server, outbox, server_flags):
if not directory:
if os.getuid():
@ -833,10 +820,11 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf
name = cp.get("global", "netbios name")
base = ",".join(["dc=" + j for j in domain.split(".")])
with open("/etc/cron.hourly/certidude", "w") as fh:
fh.write(env.get_template("ldap-ticket-renewal.sh").render(vars()))
os.chmod("/etc/cron.hourly/certidude", 0o755)
click.echo("Created /etc/cron.hourly/certidude for automatic LDAP service ticket renewal, inspect and adjust accordingly")
if not os.path.exists("/etc/cron.hourly/certidude"):
with open("/etc/cron.hourly/certidude", "w") as fh:
fh.write(env.get_template("ldap-ticket-renewal.sh").render(vars()))
os.chmod("/etc/cron.hourly/certidude", 0o755)
click.echo("Created /etc/cron.hourly/certidude for automatic LDAP service ticket renewal, inspect and adjust accordingly")
os.system("/etc/cron.hourly/certidude")
else:
click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured")
@ -919,10 +907,10 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf
).not_valid_after(
datetime.utcnow() + timedelta(days=authority_lifetime)
).serial_number(1
).add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True,
).add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True,
).add_extension(x509.KeyUsage(
digital_signature=True,
key_encipherment=False,
digital_signature=server_flags,
key_encipherment=server_flags,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
@ -930,12 +918,6 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf
crl_sign=True,
encipher_only=False,
decipher_only=False), critical=True,
).add_extension(
x509.SubjectAlternativeName([
x509.DNSName(common_name),
x509.RFC822Name(email_address)
]),
critical=False,
).add_extension(
x509.SubjectKeyIdentifier.from_public_key(key.public_key()),
critical=False
@ -1132,34 +1114,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
def certidude_sign(common_name, overwrite, lifetime):
from certidude import authority, config
request = authority.get_request(common_name)
# Use signer if this is regular client CSR
if request.signable:
# Sign via signer process
cert = authority.sign(request)
# Sign directly if it's eg. TLS server CSR
else:
# Load CA private key and certificate
private_key = serialization.load_pem_private_key(
open(config.AUTHORITY_PRIVATE_KEY_PATH).read(),
password=None, # TODO: Ask password for private key?
backend=default_backend())
authority_certificate = x509.load_pem_x509_certificate(
open(config.AUTHORITY_CERTIFICATE_PATH).read(),
backend=default_backend())
# Drop privileges
# to use LDAP service ticket to read usernames of the admins group
# in order to send e-mail
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
os.setgroups([])
os.setgid(gid)
os.setuid(uid)
# Sign directly using private key
cert = authority.sign2(request, private_key, authority_certificate,
overwrite, True, lifetime)
cert = authority.sign(request)
@click.command("serve", help="Run server")
@ -1292,9 +1247,9 @@ def certidude_serve(port, listen):
elif config.LOGGING_BACKEND:
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)
if config.PUSH_PUBLISH:
from certidude.push import PushLogHandler
log_handlers.append(PushLogHandler())
if config.EVENT_SOURCE_PUBLISH:
from certidude.push import EventSourceLogHandler
log_handlers.append(EventSourceLogHandler())
for facility in "api", "cli":
logger = logging.getLogger(facility)
@ -1310,7 +1265,6 @@ def certidude_serve(port, listen):
atexit.register(exit_handler)
logging.getLogger("cli").debug("Started Certidude at %s", const.FQDN)
print "Ready"
httpd.serve_forever()
@click.group("strongswan", help="strongSwan helpers")

View File

@ -38,7 +38,10 @@ AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path")
REQUESTS_DIR = cp.get("authority", "requests dir")
SIGNED_DIR = cp.get("authority", "signed dir")
REVOKED_DIR = cp.get("authority", "revoked dir")
OUTBOX = cp.get("authority", "outbox")
OUTBOX = cp.get("authority", "outbox uri")
OUTBOX_NAME = cp.get("authority", "outbox sender name")
OUTBOX_MAIL = cp.get("authority", "outbox sender address")
BUNDLE_FORMAT = cp.get("authority", "bundle format")
OPENVPN_BUNDLE_TEMPLATE = cp.get("authority", "openvpn bundle template")
@ -59,10 +62,11 @@ CERTIFICATE_CRL_URL = cp.get("signature", "revoked url")
REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime")
PUSH_TOKEN = cp.get("push", "token")
PUSH_EVENT_SOURCE = cp.get("push", "event source")
PUSH_LONG_POLL = cp.get("push", "long poll")
PUSH_PUBLISH = cp.get("push", "publish")
EVENT_SOURCE_TOKEN = cp.get("push", "event source token")
EVENT_SOURCE_PUBLISH = cp.get("push", "event source publish")
EVENT_SOURCE_SUBSCRIBE = cp.get("push", "event source subscribe")
LONG_POLL_PUBLISH = cp.get("push", "long poll publish")
LONG_POLL_SUBSCRIBE = cp.get("push", "long poll subscribe")
TAGGING_BACKEND = cp.get("tagging", "backend")
LOGGING_BACKEND = cp.get("logging", "backend")

View File

@ -52,12 +52,12 @@ def event_source(func):
return wrapped
class MyEncoder(json.JSONEncoder):
REQUEST_ATTRIBUTES = "signable", "identity", "changed", "common_name", \
"organizational_unit", "given_name", "surname", "fqdn", "email_address", \
REQUEST_ATTRIBUTES = "is_client", "identity", "changed", "common_name", \
"organizational_unit", "fqdn", \
"key_type", "key_length", "md5sum", "sha1sum", "sha256sum", "key_usage"
CERTIFICATE_ATTRIBUTES = "revokable", "identity", "common_name", \
"organizational_unit", "given_name", "surname", "fqdn", "email_address", \
"organizational_unit", "fqdn", \
"key_type", "key_length", "sha256sum", "serial_number", "key_usage", \
"signed", "expires"

View File

@ -15,7 +15,7 @@ from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInforma
from configparser import ConfigParser
from OpenSSL import crypto
def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, extended_key_usage_flags=None, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, ip_address=None, dns=None, bundle=False, insecure=False):
def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, autosign=False, wait=False, ip_address=None, bundle=False, insecure=False):
"""
Exchange CSR for certificate using Certidude HTTP API server
"""
@ -127,40 +127,11 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
# Set subject name attributes
names = [x509.NameAttribute(NameOID.COMMON_NAME, common_name.decode("utf-8"))]
if given_name:
names.append(x509.NameAttribute(NameOID.GIVEN_NAME, given_name.decode("utf-8")))
if surname:
names.append(x509.NameAttribute(NameOID.SURNAME, surname.decode("utf-8")))
if org_unit:
names.append(x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT, org_unit.decode("utf-8")))
# Collect subject alternative names
subject_alt_names = set()
if email_address:
subject_alt_names.add(x509.RFC822Name(email_address))
if ip_address:
subject_alt_names.add("IP:%s" % ip_address)
if dns:
subject_alt_names.add(x509.DNSName(dns))
# Construct CSR
csr = x509.CertificateSigningRequestBuilder(
).subject_name(x509.Name(names))
if extended_key_usage_flags:
click.echo("Adding extended key usage extension: %s" % extended_key_usage_flags)
csr = csr.add_extension(x509.ExtendedKeyUsage(
extended_key_usage_flags), critical=True)
if subject_alt_names:
click.echo("Adding subject alternative name extension: %s" % subject_alt_names)
csr = csr.add_extension(
x509.SubjectAlternativeName(subject_alt_names),
critical=False)
# Sign & dump CSR
os.umask(0o022)
with open(request_path + ".part", "wb") as f:

View File

@ -70,7 +70,7 @@ def send(template, to=None, attachments=(), **context):
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = authority.certificate.email_address
msg["From"] = "%s <%s>" % (config.OUTBOX_NAME, config.OUTBOX_MAIL)
msg["To"] = recipients
part1 = MIMEText(text, "plain")
@ -93,4 +93,4 @@ def send(template, to=None, attachments=(), **context):
if username and password:
conn.login(username, password)
conn.sendmail(authority.certificate.email_address, recipients, msg.as_string())
conn.sendmail(config.OUTBOX_MAIL, recipients, msg.as_string())

View File

@ -9,9 +9,9 @@ from certidude import config
def publish(event_type, event_data):
"""
Publish event on push server
Publish event on nchan EventSource publisher
"""
if not config.PUSH_PUBLISH:
if not config.EVENT_SOURCE_PUBLISH:
# Push server disabled
return
@ -19,7 +19,7 @@ def publish(event_type, event_data):
from certidude.decorators import MyEncoder
event_data = json.dumps(event_data, cls=MyEncoder)
url = config.PUSH_PUBLISH % config.PUSH_TOKEN
url = config.EVENT_SOURCE_PUBLISH % config.EVENT_SOURCE_TOKEN
click.echo("Publishing %s event '%s' on %s" % (event_type, event_data, url))
try:
@ -38,12 +38,11 @@ def publish(event_type, event_data):
click.echo("Failed to submit event to push server, connection error")
class PushLogHandler(logging.Handler):
class EventSourceLogHandler(logging.Handler):
"""
To be used with Python log handling framework for publishing log entries
"""
def emit(self, record):
from certidude.push import publish
publish("log-entry", dict(
created = datetime.utcfromtimestamp(record.created),
message = record.msg % record.args,

View File

@ -61,34 +61,67 @@ class SignHandler(asynchat.async_chat):
NotImplemented # TODO: Implement OCSP
elif cmd == "sign-request":
# Only common name and public key are used from request
request = x509.load_pem_x509_csr(body, default_backend())
subject = x509.Name([n for n in request.subject if n.oid in DN_WHITELIST])
common_name, = request.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
#subject = x509.Name([n for n in request.subject if n.oid in DN_WHITELIST])
# If common name is a fully qualified name assume it has to be signed
# with server certificate flags
server_flags = "." in common_name.value
# TODO: For fqdn allow autosign with validation
extended_key_usage_flags = []
if server_flags:
extended_key_usage_flags.append( # IKE intermediate for IPSec
x509.ObjectIdentifier("1.3.6.1.5.5.8.2.2"))
extended_key_usage_flags.append( # OpenVPN server
ExtendedKeyUsageOID.SERVER_AUTH)
else:
extended_key_usage_flags.append( # OpenVPN client
ExtendedKeyUsageOID.CLIENT_AUTH)
cert = x509.CertificateBuilder(
).subject_name(subject
).subject_name(
x509.Name([common_name])
).serial_number(random.randint(
0x1000000000000000000000000000000000000000,
0xffffffffffffffffffffffffffffffffffffffff)
).issuer_name(self.server.certificate.issuer
).public_key(request.public_key()
).not_valid_before(now - timedelta(hours=1)
).not_valid_after(now + timedelta(days=config.CERTIFICATE_LIFETIME)
).add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True,
).add_extension(x509.KeyUsage(
digital_signature=True,
key_encipherment=True,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False), critical=True,
).add_extension(x509.ExtendedKeyUsage(
[ExtendedKeyUsageOID.CLIENT_AUTH]
), critical=True,
).issuer_name(
self.server.certificate.issuer
).public_key(
request.public_key()
).not_valid_before(
now - timedelta(hours=1)
).not_valid_after(
now + timedelta(days=config.CERTIFICATE_LIFETIME)
).add_extension(
x509.SubjectKeyIdentifier.from_public_key(request.public_key()),
x509.BasicConstraints(
ca=False,
path_length=None),
critical=True,
).add_extension(
x509.KeyUsage(
digital_signature=True,
key_encipherment=True,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False),
critical=True,
).add_extension(
x509.ExtendedKeyUsage(
extended_key_usage_flags),
critical=True,
).add_extension(
x509.SubjectKeyIdentifier.from_public_key(
request.public_key()),
critical=False
).add_extension(
x509.AuthorityInformationAccess([

View File

@ -1,11 +1,7 @@
<li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable">
<a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a>
{% if request.signable %}
<button class="icon sign" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/',type:'patch'});">Sign</button>
{% else %}
<button title="Please use certidude command-line utility to sign unusual requests" disabled>Sign</button>
{% endif %}
<button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/',type:'delete'});">Delete</button>

View File

@ -52,19 +52,23 @@ request subnets = 0.0.0.0/0
autosign subnets = 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
[logging]
backend = sql
backend =
;backend = sql
database = sqlite://{{ directory }}/db.sqlite
[tagging]
backend = sql
backend =
;backend = sql
database = sqlite://{{ directory }}/db.sqlite
[leases]
backend =
;backend = sql
;schema = strongswan
;database = sqlite://{{ directory }}/db.sqlite
schema = strongswan
database = sqlite://{{ directory }}/db.sqlite
# Following was used on an OpenWrt router
# uci set openvpn.s2c.status=/www/status.log
@ -80,10 +84,11 @@ certificate url = {{ certificate_url }}
revoked url = {{ revoked_url }}
[push]
token = {{ push_token }}
event source = {{ push_server }}/ev/%s
long poll = {{ push_server }}/lp/%s
publish = {{ push_server }}/pub?id=%s
event source token = {{ push_token }}
event source subscribe = {{ push_server }}/ev/sub/%s
event source publish = {{ push_server }}/ev/pub/%s
long poll subscribe = {{ push_server }}/lp/sub/%s
long poll publish = {{ push_server }}/lp/pub/%s
[authority]
# User certificate enrollment specifies whether logged in users are allowed to
@ -102,7 +107,10 @@ requests dir = {{ directory }}/requests/
signed dir = {{ directory }}/signed/
revoked dir = {{ directory }}/revoked/
expired dir = {{ directory }}/expired/
outbox = {{ outbox }}
outbox uri = {{ outbox }}
outbox sender name = Certificate management
outbox sender address = certificates@example.com
bundle format = p12
;bundle format = ovpn

View File

@ -17,21 +17,28 @@ server {
}
{% if not push_server %}
location /pub {
location ~ "^/lp/pub/(.*)" {
allow 127.0.0.1;
nchan_publisher http;
nchan_store_messages off;
nchan_channel_id $arg_id;
nchan_publisher;
nchan_channel_id $1;
nchan_message_buffer_length 0;
}
location ~ "^/lp/(.*)" {
location ~ "^/ev/pub/(.*)" {
allow 127.0.0.1;
nchan_publisher;
nchan_channel_id $1;
nchan_message_buffer_length 0;
}
location ~ "^/lp/sub/(.*)" {
nchan_channel_id $1;
nchan_subscriber longpoll;
nchan_channel_id $1;
}
location ~ "^/ev/(.*)" {
nchan_subscriber eventsource;
location ~ "^/ev/sub/(.*)" {
nchan_channel_id $1;
nchan_subscriber eventsource;
}
{% endif %}

View File

@ -22,22 +22,6 @@ class CertificateBase:
def __repr__(self):
return self.buf
@property
def given_name(self):
return self.subject.GN
@given_name.setter
def given_name(self, value):
return setattr(self.subject, "GN", value)
@property
def surname(self):
return self.subject.SN
@surname.setter
def surname(self, value):
return setattr(self.subject, "SN", value)
@property
def common_name(self):
return self.subject.CN
@ -46,46 +30,6 @@ class CertificateBase:
def common_name(self, value):
self.subject.CN = value
@property
def country_code(self):
return getattr(self._obj.get_subject(), "C", None)
@property
def state_or_county(self):
return getattr(self._obj.get_subject(), "S", None)
@property
def city(self):
return getattr(self._obj.get_subject(), "L", None)
@property
def organization(self):
return getattr(self._obj.get_subject(), "O", None)
@property
def organizational_unit(self):
return getattr(self._obj.get_subject(), "OU", None)
@country_code.setter
def country_code(self, value):
return setattr(self._obj.get_subject(), "C", value)
@state_or_county.setter
def state_or_county(self, value):
return setattr(self._obj.get_subject(), "S", value)
@city.setter
def city(self, value):
return setattr(self._obj.get_subject(), "L", value)
@organization.setter
def organization(self, value):
return setattr(self._obj.get_subject(), "O", value)
@organizational_unit.setter
def organizational_unit(self, value):
return setattr(self._obj.get_subject(), "OU", value)
@property
def key_usage(self):
def iterate():
@ -139,13 +83,6 @@ class CertificateBase:
critical,
value.encode("ascii")) for (key,value,critical) in extensions])
@property
def email_address(self):
for bit in self.subject_alt_name.split(", "):
if bit.startswith("email:"):
return bit[6:]
return ""
@property
def fqdn(self):
for bit in self.subject_alt_name.split(", "):
@ -153,17 +90,6 @@ class CertificateBase:
return bit[4:]
return ""
@property
def subject_alt_name(self):
for key, value, data in self.extensions:
if key == "subjectAltName":
return value
return ""
@subject_alt_name.setter
def subject_alt_name(self, value):
self.set_extension("subjectAltName", value, False)
@property
def pubkey(self):
from Crypto.Util import asn1
@ -226,11 +152,12 @@ class Request(CertificateBase):
assert not self.buf or self.buf == self.dump(), "%s is not %s" % (repr(self.buf), repr(self.dump()))
@property
def signable(self):
for key, value, data in self.extensions:
if key not in const.EXTENSION_WHITELIST:
return False
return True
def is_server(self):
return "." in self.common_name
@property
def is_client(self):
return not self.is_server
def dump(self):
return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self._obj).decode("ascii")