1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-23 00:25:18 +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, user_mutliple_certificates=config.USER_MULTIPLE_CERTIFICATES,
outbox = config.OUTBOX, outbox = config.OUTBOX,
certificate = authority.certificate, certificate = authority.certificate,
events = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN, events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
requests=authority.list_requests(), requests=authority.list_requests(),
signed=authority.list_signed(), signed=authority.list_signed(),
revoked=authority.list_revoked(), revoked=authority.list_revoked(),

View File

@ -52,11 +52,11 @@ class RequestListResource(object):
raise falcon.HTTPBadRequest( raise falcon.HTTPBadRequest(
"Bad request", "Bad request",
"Common name %s differs from Kerberos credential %s!" % (csr.common_name, machine)) "Common name %s differs from Kerberos credential %s!" % (csr.common_name, machine))
if csr.signable:
# Automatic enroll with Kerberos machine cerdentials # Automatic enroll with Kerberos machine cerdentials
resp.set_header("Content-Type", "application/x-x509-user-cert") resp.set_header("Content-Type", "application/x-x509-user-cert")
resp.body = authority.sign(csr, overwrite=True).dump() resp.body = authority.sign(csr, overwrite=True).dump()
return return
# Check if this request has been already signed and return corresponding certificte if it has been signed # 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 # 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 # 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: for subnet in config.AUTOSIGN_SUBNETS:
if req.context.get("remote_addr") in subnet: if req.context.get("remote_addr") in subnet:
try: try:
@ -103,7 +103,7 @@ class RequestListResource(object):
# Wait the certificate to be signed if waiting is requested # Wait the certificate to be signed if waiting is requested
if req.get_param("wait"): if req.get_param("wait"):
# Redirect to nginx pub/sub # 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) click.echo("Redirecting to: %s" % url)
resp.status = falcon.HTTP_SEE_OTHER resp.status = falcon.HTTP_SEE_OTHER
resp.set_header("Location", url.encode("ascii")) resp.set_header("Location", url.encode("ascii"))

View File

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

View File

@ -305,8 +305,6 @@ def certidude_request(fork):
@click.command("client", help="Setup X.509 certificates for application") @click.command("client", help="Setup X.509 certificates for application")
@click.argument("server") @click.argument("server")
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, '%s' by default" % const.HOSTNAME) @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("--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("--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") @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.command("server", help="Set up OpenVPN server")
@click.argument("authority") @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("--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("--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") @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", default="/etc/openvpn/site-to-client.conf",
type=click.File(mode="w", atomic=True, lazy=True), type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file") 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: Make dirs
# TODO: Intelligent way of getting last IP address in the subnet # 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.command("nginx", help="Set up nginx as HTTPS server")
@click.argument("server") @click.argument("server")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN) @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", @click.option("--tls-config",
default="/etc/nginx/conf.d/tls.conf", default="/etc/nginx/conf.d/tls.conf",
type=click.File(mode="w", atomic=True, lazy=True), 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("authority")
@click.argument("remote") @click.argument("remote")
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default") @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", @click.option("--config", "-o",
default="/etc/openvpn/client-to-site.conf", default="/etc/openvpn/client-to-site.conf",
type=click.File(mode="w", atomic=True, lazy=True), 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.command("server", help="Set up strongSwan server")
@click.argument("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("--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("--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") @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: if "." not in common_name:
raise ValueError("Hostname has to be fully qualified!") raise ValueError("Hostname has to be fully qualified!")
if not local: 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.command("client", help="Set up strongSwan client")
@click.argument("server") @click.argument("server")
@click.argument("remote") @click.argument("remote")
@click.option("--org-unit", "-ou", help="Organizational unit")
def certidude_setup_strongswan_client(authority, config, org_unit, remote, dpdaction): def certidude_setup_strongswan_client(authority, config, org_unit, remote, dpdaction):
# Create corresponding section in /etc/certidude/client.conf # Create corresponding section in /etc/certidude/client.conf
client_config = ConfigParser() 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.command("networkmanager", help="Set up strongSwan client via NetworkManager")
@click.argument("server") # Certidude server @click.argument("server") # Certidude server
@click.argument("remote") # StrongSwan gateway @click.argument("remote") # StrongSwan gateway
@click.option("--org-unit", "-ou", help="Organizational unit")
def certidude_setup_strongswan_networkmanager(server,remote, org_unit): def certidude_setup_strongswan_networkmanager(server,remote, org_unit):
endpoint = "IPSec to %s" % remote 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("server") # Certidude server
@click.argument("remote") # OpenVPN gateway @click.argument("remote") # OpenVPN gateway
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME) @click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit") def certidude_setup_openvpn_networkmanager(authority, org_unit, remote):
@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):
# Create corresponding section in /etc/certidude/client.conf # Create corresponding section in /etc/certidude/client.conf
client_config = ConfigParser() client_config = ConfigParser()
if os.path.exists(const.CLIENT_CONFIG_PATH): 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("--revoked-url", default=None, help="CRL distribution URL")
@click.option("--certificate-url", default=None, help="Authority certificate 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("--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("--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("--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) @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 not directory:
if os.getuid(): if os.getuid():
@ -833,10 +820,11 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf
name = cp.get("global", "netbios name") name = cp.get("global", "netbios name")
base = ",".join(["dc=" + j for j in domain.split(".")]) base = ",".join(["dc=" + j for j in domain.split(".")])
with open("/etc/cron.hourly/certidude", "w") as fh: if not os.path.exists("/etc/cron.hourly/certidude"):
fh.write(env.get_template("ldap-ticket-renewal.sh").render(vars())) with open("/etc/cron.hourly/certidude", "w") as fh:
os.chmod("/etc/cron.hourly/certidude", 0o755) fh.write(env.get_template("ldap-ticket-renewal.sh").render(vars()))
click.echo("Created /etc/cron.hourly/certidude for automatic LDAP service ticket renewal, inspect and adjust accordingly") 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") os.system("/etc/cron.hourly/certidude")
else: else:
click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured") 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( ).not_valid_after(
datetime.utcnow() + timedelta(days=authority_lifetime) datetime.utcnow() + timedelta(days=authority_lifetime)
).serial_number(1 ).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( ).add_extension(x509.KeyUsage(
digital_signature=True, digital_signature=server_flags,
key_encipherment=False, key_encipherment=server_flags,
content_commitment=False, content_commitment=False,
data_encipherment=False, data_encipherment=False,
key_agreement=False, key_agreement=False,
@ -930,12 +918,6 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf
crl_sign=True, crl_sign=True,
encipher_only=False, encipher_only=False,
decipher_only=False), critical=True, decipher_only=False), critical=True,
).add_extension(
x509.SubjectAlternativeName([
x509.DNSName(common_name),
x509.RFC822Name(email_address)
]),
critical=False,
).add_extension( ).add_extension(
x509.SubjectKeyIdentifier.from_public_key(key.public_key()), x509.SubjectKeyIdentifier.from_public_key(key.public_key()),
critical=False 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): def certidude_sign(common_name, overwrite, lifetime):
from certidude import authority, config from certidude import authority, config
request = authority.get_request(common_name) request = authority.get_request(common_name)
cert = authority.sign(request)
# 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)
@click.command("serve", help="Run server") @click.command("serve", help="Run server")
@ -1292,9 +1247,9 @@ def certidude_serve(port, listen):
elif config.LOGGING_BACKEND: elif config.LOGGING_BACKEND:
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND) raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)
if config.PUSH_PUBLISH: if config.EVENT_SOURCE_PUBLISH:
from certidude.push import PushLogHandler from certidude.push import EventSourceLogHandler
log_handlers.append(PushLogHandler()) log_handlers.append(EventSourceLogHandler())
for facility in "api", "cli": for facility in "api", "cli":
logger = logging.getLogger(facility) logger = logging.getLogger(facility)
@ -1310,7 +1265,6 @@ def certidude_serve(port, listen):
atexit.register(exit_handler) atexit.register(exit_handler)
logging.getLogger("cli").debug("Started Certidude at %s", const.FQDN) logging.getLogger("cli").debug("Started Certidude at %s", const.FQDN)
print "Ready"
httpd.serve_forever() httpd.serve_forever()
@click.group("strongswan", help="strongSwan helpers") @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") REQUESTS_DIR = cp.get("authority", "requests dir")
SIGNED_DIR = cp.get("authority", "signed dir") SIGNED_DIR = cp.get("authority", "signed dir")
REVOKED_DIR = cp.get("authority", "revoked 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") BUNDLE_FORMAT = cp.get("authority", "bundle format")
OPENVPN_BUNDLE_TEMPLATE = cp.get("authority", "openvpn bundle template") 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") REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime")
PUSH_TOKEN = cp.get("push", "token") EVENT_SOURCE_TOKEN = cp.get("push", "event source token")
PUSH_EVENT_SOURCE = cp.get("push", "event source") EVENT_SOURCE_PUBLISH = cp.get("push", "event source publish")
PUSH_LONG_POLL = cp.get("push", "long poll") EVENT_SOURCE_SUBSCRIBE = cp.get("push", "event source subscribe")
PUSH_PUBLISH = cp.get("push", "publish") LONG_POLL_PUBLISH = cp.get("push", "long poll publish")
LONG_POLL_SUBSCRIBE = cp.get("push", "long poll subscribe")
TAGGING_BACKEND = cp.get("tagging", "backend") TAGGING_BACKEND = cp.get("tagging", "backend")
LOGGING_BACKEND = cp.get("logging", "backend") LOGGING_BACKEND = cp.get("logging", "backend")

View File

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

View File

@ -15,7 +15,7 @@ from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInforma
from configparser import ConfigParser from configparser import ConfigParser
from OpenSSL import crypto 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 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 # Set subject name attributes
names = [x509.NameAttribute(NameOID.COMMON_NAME, common_name.decode("utf-8"))] 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 # Construct CSR
csr = x509.CertificateSigningRequestBuilder( csr = x509.CertificateSigningRequestBuilder(
).subject_name(x509.Name(names)) ).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 # Sign & dump CSR
os.umask(0o022) os.umask(0o022)
with open(request_path + ".part", "wb") as f: 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 = MIMEMultipart("alternative")
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = authority.certificate.email_address msg["From"] = "%s <%s>" % (config.OUTBOX_NAME, config.OUTBOX_MAIL)
msg["To"] = recipients msg["To"] = recipients
part1 = MIMEText(text, "plain") part1 = MIMEText(text, "plain")
@ -93,4 +93,4 @@ def send(template, to=None, attachments=(), **context):
if username and password: if username and password:
conn.login(username, 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): 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 # Push server disabled
return return
@ -19,7 +19,7 @@ def publish(event_type, event_data):
from certidude.decorators import MyEncoder from certidude.decorators import MyEncoder
event_data = json.dumps(event_data, cls=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)) click.echo("Publishing %s event '%s' on %s" % (event_type, event_data, url))
try: try:
@ -38,12 +38,11 @@ def publish(event_type, event_data):
click.echo("Failed to submit event to push server, connection error") 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 To be used with Python log handling framework for publishing log entries
""" """
def emit(self, record): def emit(self, record):
from certidude.push import publish
publish("log-entry", dict( publish("log-entry", dict(
created = datetime.utcfromtimestamp(record.created), created = datetime.utcfromtimestamp(record.created),
message = record.msg % record.args, message = record.msg % record.args,

View File

@ -61,34 +61,67 @@ class SignHandler(asynchat.async_chat):
NotImplemented # TODO: Implement OCSP NotImplemented # TODO: Implement OCSP
elif cmd == "sign-request": elif cmd == "sign-request":
# Only common name and public key are used from request
request = x509.load_pem_x509_csr(body, default_backend()) 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( cert = x509.CertificateBuilder(
).subject_name(subject ).subject_name(
x509.Name([common_name])
).serial_number(random.randint( ).serial_number(random.randint(
0x1000000000000000000000000000000000000000, 0x1000000000000000000000000000000000000000,
0xffffffffffffffffffffffffffffffffffffffff) 0xffffffffffffffffffffffffffffffffffffffff)
).issuer_name(self.server.certificate.issuer ).issuer_name(
).public_key(request.public_key() self.server.certificate.issuer
).not_valid_before(now - timedelta(hours=1) ).public_key(
).not_valid_after(now + timedelta(days=config.CERTIFICATE_LIFETIME) request.public_key()
).add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True, ).not_valid_before(
).add_extension(x509.KeyUsage( now - timedelta(hours=1)
digital_signature=True, ).not_valid_after(
key_encipherment=True, now + timedelta(days=config.CERTIFICATE_LIFETIME)
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,
).add_extension( ).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 critical=False
).add_extension( ).add_extension(
x509.AuthorityInformationAccess([ x509.AuthorityInformationAccess([

View File

@ -1,11 +1,7 @@
<li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable"> <li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable">
<a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a> <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> <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> <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 autosign subnets = 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
[logging] [logging]
backend = sql backend =
;backend = sql
database = sqlite://{{ directory }}/db.sqlite database = sqlite://{{ directory }}/db.sqlite
[tagging] [tagging]
backend = sql backend =
;backend = sql
database = sqlite://{{ directory }}/db.sqlite database = sqlite://{{ directory }}/db.sqlite
[leases] [leases]
backend = backend =
;backend = sql ;backend = sql
;schema = strongswan schema = strongswan
;database = sqlite://{{ directory }}/db.sqlite database = sqlite://{{ directory }}/db.sqlite
# Following was used on an OpenWrt router # Following was used on an OpenWrt router
# uci set openvpn.s2c.status=/www/status.log # uci set openvpn.s2c.status=/www/status.log
@ -80,10 +84,11 @@ certificate url = {{ certificate_url }}
revoked url = {{ revoked_url }} revoked url = {{ revoked_url }}
[push] [push]
token = {{ push_token }} event source token = {{ push_token }}
event source = {{ push_server }}/ev/%s event source subscribe = {{ push_server }}/ev/sub/%s
long poll = {{ push_server }}/lp/%s event source publish = {{ push_server }}/ev/pub/%s
publish = {{ push_server }}/pub?id=%s long poll subscribe = {{ push_server }}/lp/sub/%s
long poll publish = {{ push_server }}/lp/pub/%s
[authority] [authority]
# User certificate enrollment specifies whether logged in users are allowed to # User certificate enrollment specifies whether logged in users are allowed to
@ -102,7 +107,10 @@ requests dir = {{ directory }}/requests/
signed dir = {{ directory }}/signed/ signed dir = {{ directory }}/signed/
revoked dir = {{ directory }}/revoked/ revoked dir = {{ directory }}/revoked/
expired dir = {{ directory }}/expired/ 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 = p12
;bundle format = ovpn ;bundle format = ovpn

View File

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

View File

@ -22,22 +22,6 @@ class CertificateBase:
def __repr__(self): def __repr__(self):
return self.buf 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 @property
def common_name(self): def common_name(self):
return self.subject.CN return self.subject.CN
@ -46,46 +30,6 @@ class CertificateBase:
def common_name(self, value): def common_name(self, value):
self.subject.CN = 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 @property
def key_usage(self): def key_usage(self):
def iterate(): def iterate():
@ -139,13 +83,6 @@ class CertificateBase:
critical, critical,
value.encode("ascii")) for (key,value,critical) in extensions]) 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 @property
def fqdn(self): def fqdn(self):
for bit in self.subject_alt_name.split(", "): for bit in self.subject_alt_name.split(", "):
@ -153,17 +90,6 @@ class CertificateBase:
return bit[4:] return bit[4:]
return "" 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 @property
def pubkey(self): def pubkey(self):
from Crypto.Util import asn1 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())) assert not self.buf or self.buf == self.dump(), "%s is not %s" % (repr(self.buf), repr(self.dump()))
@property @property
def signable(self): def is_server(self):
for key, value, data in self.extensions: return "." in self.common_name
if key not in const.EXTENSION_WHITELIST:
return False @property
return True def is_client(self):
return not self.is_server
def dump(self): def dump(self):
return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self._obj).decode("ascii") return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self._obj).decode("ascii")