Migrate from cryptography.io to oscrypto

This commit is contained in:
Lauri Võsandi 2017-08-16 20:25:16 +00:00
parent 789d80d712
commit 509f7bfaa8
18 changed files with 533 additions and 681 deletions

View File

@ -11,9 +11,8 @@ Certidude
Introduction
------------
Certidude is a novel X.509 Certificate Authority management tool
with privilege isolation mechanism and Kerberos authentication
mainly designed for OpenVPN gateway operators to make
Certidude is a minimalist X.509 Certificate Authority management tool
with Kerberos authentication mainly designed for OpenVPN gateway operators to make
VPN client setup on laptops, desktops and mobile devices as painless as possible.
.. figure:: doc/certidude.png
@ -54,13 +53,6 @@ Following usecases are covered:
The user logs in using domain account in the web interface and can automatically
retrieve a P12 bundle which can be installed on her Android device.
Future usecases:
* I want to store the private key of my CA on a SmartCard.
I want to make use of it while I log in to my CA web interface.
When I am asked to sign a certificate I have to enter PIN code to unlock the
SmartCard.
Features
--------
@ -68,16 +60,14 @@ Features
Common:
* Standard request, sign, revoke workflow via web interface.
* Kerberos and basic auth based web interface authentication.
* Preliminary `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support.
* `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support.
* PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind.
* POSIX groups and Active Directory (LDAP) group membership based authorization.
* Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``.
* Privilege isolation, separate signer process is spawned per private key isolating
private key use from the the web interface.
* Certificate serial numbers are intentionally randomized to avoid leaking information about business practices.
* Server-side events support via `nchan <https://nchan.slact.net/>`_.
* E-mail notifications about pending, signed, revoked, renewed and overwritten certificates
* E-mail notifications about pending, signed, revoked, renewed and overwritten certificates.
* Built using compilation-free `oscrypto <https://github.com/wbond/oscrypto>`_ library.
Virtual private networking:
@ -95,9 +85,7 @@ HTTPS:
TODO
----
* WebCrypto support, meanwhile check out `hwcrypto.js <https://github.com/open-eid/hwcrypto.js>`_.
* Use `pki.js <https://pkijs.org/>`_ for generating keypair in the browser when claiming a token.
* Signer process logging.
Install
@ -110,7 +98,8 @@ System dependencies for Ubuntu 16.04:
.. code:: bash
apt install -y python python-cffi python-click python-configparser \
apt install -y
python-click python-configparser \
python-humanize \
python-ipaddress python-jinja2 python-ldap python-markdown \
python-mimeparse python-mysql.connector python-openssl python-pip \
@ -124,7 +113,7 @@ System dependencies for Fedora 25+:
yum install redhat-rpm-config python-devel openssl-devel openldap-devel
At the moment package at PyPI is rather outdated.
Please proceed down to Development section to install Certidude from source.
Please proceed down to `Development <#development>`_ section to install Certidude from source.
Setting up authority

View File

@ -13,7 +13,6 @@ from certidude import authority, mailer
from certidude.auth import login_required, authorize_admin
from certidude.user import User
from certidude.decorators import serialize, csrf_protection
from cryptography.x509.oid import NameOID
from certidude import const, config
logger = logging.getLogger(__name__)
@ -82,8 +81,8 @@ class SessionResource(object):
common_name = common_name,
server = server,
# TODO: key type, key length, key exponent, key modulo
signed = obj.not_valid_before,
expires = obj.not_valid_after,
signed = obj["tbs_certificate"]["validity"]["not_before"].native,
expires = obj["tbs_certificate"]["validity"]["not_after"].native,
sha256sum = hashlib.sha256(buf).hexdigest(),
lease = lease,
tags = tags,
@ -108,8 +107,7 @@ class SessionResource(object):
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded
),
common_name = authority.ca_cert.subject.get_attributes_for_oid(
NameOID.COMMON_NAME)[0].value,
common_name = authority.certificate.subject.native["common_name"],
mailer = dict(
name = config.MAILER_NAME,
address = config.MAILER_ADDRESS

View File

@ -33,7 +33,7 @@ class LeaseResource(object):
# TODO: verify signature
common_name = req.get_param("client", required=True)
path, buf, cert = authority.get_signed(common_name) # TODO: catch exceptions
if req.get_param("serial") and cert.serial != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan
if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan
raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied")
xattr.setxattr(path, "user.lease.outer_address", req.get_param("outer_address", required=True).encode("ascii"))

View File

@ -52,7 +52,7 @@ class OCSPResource(object):
assert link_target.startswith("../")
assert link_target.endswith(".pem")
path, buf, cert = authority.get_signed(link_target[3:-4])
if serial != cert.serial:
if serial != cert.serial_number:
raise EnvironmentError("integrity check failed")
status = ocsp.CertStatus(name='good', value=None)
except EnvironmentError:
@ -94,9 +94,13 @@ class OCSPResource(object):
'response_type': u"basic_ocsp_response",
'response': {
'tbs_response_data': response_data,
'certs': [server_certificate.asn1],
'signature_algorithm': {'algorithm': u"sha1_rsa"},
'signature': b64decode(authority.signer_exec("sign-pkcs7", b64encode(response_data.dump()))),
'certs': [server_certificate.asn1]
'signature': asymmetric.rsa_pkcs1v15_sign(
authority.private_key,
response_data.dump(),
"sha1"
)
}
}
}).dump()

View File

@ -6,18 +6,16 @@ import ipaddress
import json
import os
import hashlib
from asn1crypto import pem
from asn1crypto.csr import CertificationRequest
from base64 import b64decode
from certidude import config, authority, push, errors
from certidude.auth import login_required, login_optional, authorize_admin
from certidude.decorators import serialize, csrf_protection
from certidude.firewall import whitelist_subnets, whitelist_content_types
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature
from cryptography.x509.oid import NameOID
from datetime import datetime
from oscrypto import asymmetric
from oscrypto.errors import SignatureError
from xattr import getxattr
logger = logging.getLogger(__name__)
@ -35,19 +33,14 @@ class RequestListResource(object):
@whitelist_content_types("application/pkcs10")
def on_post(self, req, resp):
"""
Validate and parse certificate signing request
Validate and parse certificate signing request, the RESTful way
"""
reasons = []
body = req.stream.read(req.content_length)
csr = x509.load_pem_x509_csr(body, default_backend())
try:
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
except: # ValueError?
logger.warning(u"Rejected signing request without common name from %s",
req.context.get("remote_addr"))
raise falcon.HTTPBadRequest(
"Bad request",
"No common name specified!")
body = req.stream.read(req.content_length).encode("ascii")
header, _, der_bytes = pem.unarmor(body)
csr = CertificationRequest.load(der_bytes)
common_name = csr["certification_request_info"]["subject"].native["common_name"]
"""
Handle domain computer automatic enrollment
@ -55,10 +48,10 @@ class RequestListResource(object):
machine = req.context.get("machine")
if machine:
if config.MACHINE_ENROLLMENT_ALLOWED:
if common_name.value != machine:
if common_name != machine:
raise falcon.HTTPBadRequest(
"Bad request",
"Common name %s differs from Kerberos credential %s!" % (common_name.value, machine))
"Common name %s differs from Kerberos credential %s!" % (common_name, machine))
# Automatic enroll with Kerberos machine cerdentials
resp.set_header("Content-Type", "application/x-pem-file")
@ -73,52 +66,48 @@ class RequestListResource(object):
Attempt to renew certificate using currently valid key pair
"""
try:
path, buf, cert = authority.get_signed(common_name.value)
path, buf, cert = authority.get_signed(common_name)
except EnvironmentError:
pass
pass # No currently valid certificate for this common name
else:
if cert.public_key().public_numbers() == csr.public_key().public_numbers():
cert_pk = cert["tbs_certificate"]["subject_public_key_info"].native
csr_pk = csr["certification_request_info"]["subject_pk_info"].native
if cert_pk == csr_pk: # Same public key, assume renewal
expires = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
renewal_header = req.get_header("X-Renewal-Signature")
if not renewal_header:
# No header supplied, redirect to signed API call
resp.status = falcon.HTTP_SEE_OTHER
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name.value)
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name)
return
try:
renewal_signature = b64decode(renewal_header)
except TypeError, ValueError:
logger.error(u"Renewal failed, bad signature supplied for %s", common_name.value)
logger.error(u"Renewal failed, bad signature supplied for %s", common_name)
reasons.append("Renewal failed, bad signature supplied")
else:
try:
verifier = cert.public_key().verifier(
renewal_signature,
padding.PSS(
mgf=padding.MGF1(hashes.SHA512()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA512()
)
verifier.update(buf)
verifier.update(body)
verifier.verify()
except InvalidSignature:
logger.error(u"Renewal failed, invalid signature supplied for %s", common_name.value)
asymmetric.rsa_pss_verify(
asymmetric.load_certificate(cert),
renewal_signature, buf + body, "sha512")
except SignatureError:
logger.error(u"Renewal failed, invalid signature supplied for %s", common_name)
reasons.append("Renewal failed, invalid signature supplied")
else:
# At this point renewal signature was valid but we need to perform some extra checks
if datetime.utcnow() > cert.not_valid_after:
logger.error(u"Renewal failed, current certificate for %s has expired", common_name.value)
if datetime.utcnow() > expires:
logger.error(u"Renewal failed, current certificate for %s has expired", common_name)
reasons.append("Renewal failed, current certificate expired")
elif not config.CERTIFICATE_RENEWAL_ALLOWED:
logger.error(u"Renewal requested for %s, but not allowed by authority settings", common_name.value)
logger.error(u"Renewal requested for %s, but not allowed by authority settings", common_name)
reasons.append("Renewal requested, but not allowed by authority settings")
else:
resp.set_header("Content-Type", "application/x-x509-user-cert")
_, resp.body = authority._sign(csr, body, overwrite=True)
logger.info(u"Renewed certificate for %s", common_name.value)
logger.info(u"Renewed certificate for %s", common_name)
return
@ -127,17 +116,17 @@ class RequestListResource(object):
autosigning was requested and certificate can be automatically signed
"""
if req.get_param_as_bool("autosign"):
if "." not in common_name.value:
if not authority.server_flags(common_name):
for subnet in config.AUTOSIGN_SUBNETS:
if req.context.get("remote_addr") in subnet:
try:
resp.set_header("Content-Type", "application/x-pem-file")
_, resp.body = authority._sign(csr, body)
logger.info(u"Autosigned %s as %s is whitelisted", common_name.value, req.context.get("remote_addr"))
logger.info(u"Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
return
except EnvironmentError:
logger.info(u"Autosign for %s from %s failed, signed certificate already exists",
common_name.value, req.context.get("remote_addr"))
common_name, req.context.get("remote_addr"))
reasons.append("Autosign failed, signed certificate already exists")
break
else:
@ -147,7 +136,7 @@ class RequestListResource(object):
# Attempt to save the request otherwise
try:
request_path, _, _ = authority.store_request(body.decode("ascii"),
request_path, _, _ = authority.store_request(body,
address=str(req.context.get("remote_addr")))
except errors.RequestExists:
reasons.append("Same request already uploaded exists")
@ -160,10 +149,10 @@ class RequestListResource(object):
"CSR with such CN already exists",
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
else:
push.publish("request-submitted", common_name.value)
push.publish("request-submitted", common_name)
# Wait the certificate to be signed if waiting is requested
logger.info(u"Signing request %s from %s stored", common_name.value, req.context.get("remote_addr"))
logger.info(u"Stored signing request %s from %s", common_name, req.context.get("remote_addr"))
if req.get_param("wait"):
# Redirect to nginx pub/sub
url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest()
@ -221,7 +210,7 @@ class RequestDetailResource(object):
@csrf_protection
@login_required
@authorize_admin
def on_patch(self, req, resp, cn):
def on_post(self, req, resp, cn):
"""
Sign a certificate signing request
"""

View File

@ -6,9 +6,6 @@ import logging
from certidude import const, config
from certidude.authority import export_crl, list_revoked
from certidude.firewall import whitelist_subnets
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import Encoding
logger = logging.getLogger(__name__)
@ -23,9 +20,8 @@ class RevocationListResource(object):
"Content-Disposition",
("attachment; filename=%s.crl" % const.HOSTNAME).encode("ascii"))
# Convert PEM to DER
logger.debug(u"Serving revocation list to %s in DER format", req.context.get("remote_addr"))
resp.body = x509.load_pem_x509_crl(export_crl(),
default_backend()).public_bytes(Encoding.DER)
logger.debug(u"Serving revocation list (DER) to %s", req.context.get("remote_addr"))
resp.body = export_crl(pem=False)
elif req.client_accepts("application/x-pem-file"):
if req.get_param_as_bool("wait"):
url = config.LONG_POLL_SUBSCRIBE % "crl"
@ -38,7 +34,7 @@ class RevocationListResource(object):
resp.append_header(
"Content-Disposition",
("attachment; filename=%s-crl.pem" % const.HOSTNAME).encode("ascii"))
logger.debug(u"Serving revocation list to %s in PEM format", req.context.get("remote_addr"))
logger.debug(u"Serving revocation list (PEM) to %s", req.context.get("remote_addr"))
resp.body = export_crl()
else:
logger.debug(u"Client %s asked revocation list in unsupported format" % req.context.get("remote_addr"))

View File

@ -41,7 +41,7 @@ class SCEPResource(object):
def on_get(self, req, resp):
operation = req.get_param("operation")
if operation.lower() == "getcacert":
resp.stream = keys.parse_certificate(authority.ca_buf).dump()
resp.stream = keys.parse_certificate(authority.certificate_buf).dump()
resp.append_header("Content-Type", "application/x-x509-ca-cert")
return
@ -125,14 +125,16 @@ class SCEPResource(object):
encrypted_content = encrypted_content_info['encrypted_content'].native
recipient, = encrypted_envelope['recipient_infos']
if recipient.native["rid"]["serial_number"] != authority.ca_cert.serial:
if recipient.native["rid"]["serial_number"] != authority.certificate.serial_number:
raise SCEPBadCertId()
# Since CA private key is not directly readable here, we'll redirect it to signer socket
key = b64decode(authority.signer_exec("decrypt-pkcs7", b64encode(recipient.native["encrypted_key"])))
key = asymmetric.rsa_pkcs1v15_decrypt(
authority.private_key,
recipient.native["encrypted_key"])
if len(key) == 8: key = key * 3 # Convert DES to 3DES
buf = symmetric.tripledes_cbc_pkcs5_decrypt(key, encrypted_content, iv)
_, common_name = authority.store_request(buf, overwrite=True)
_, _, common_name = authority.store_request(buf, overwrite=True)
cert, buf = authority.sign(common_name, overwrite=True)
signed_certificate = asymmetric.load_certificate(buf)
content = signed_certificate.asn1.dump()
@ -251,7 +253,11 @@ class SCEPResource(object):
}),
'digest_algorithm': algos.DigestAlgorithm({'algorithm': u"sha1"}),
'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': u"rsassa_pkcs1v15"}),
'signature': b64decode(authority.signer_exec("sign-pkcs7", b64encode(b"\x31" + attrs.dump()[1:])))
'signature': asymmetric.rsa_pkcs1v15_sign(
authority.private_key,
b"\x31" + attrs.dump()[1:],
"sha1"
)
})
resp.append_header("Content-Type", "application/x-pki-message")

View File

@ -31,9 +31,9 @@ class SignedCertificateDetailResource(object):
resp.set_header("Content-Disposition", ("attachment; filename=%s.json" % cn))
resp.body = json.dumps(dict(
common_name = cn,
serial_number = "%x" % cert.serial,
signed = cert.not_valid_before.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
expires = cert.not_valid_after.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
serial_number = "%x" % cert.serial_number,
signed = cert["tbs_certificate"]["validity"]["not_before"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
sha256sum = hashlib.sha256(buf).hexdigest()))
logger.debug(u"Served certificate %s to %s as application/json",
cn, req.context.get("remote_addr"))

View File

@ -1,23 +1,25 @@
from __future__ import division, absolute_import, print_function
import click
import os
import random
import re
import requests
import hashlib
import socket
from datetime import datetime, timedelta
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID, ExtendedKeyUsageOID
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import Encoding
from oscrypto import asymmetric
from asn1crypto import pem, x509
from asn1crypto.csr import CertificationRequest
from certbuilder import CertificateBuilder
from certidude import config, push, mailer, const
from certidude import errors
from crlbuilder import CertificateListBuilder, pem_armor_crl
from csrbuilder import CSRBuilder, pem_armor_csr
from datetime import datetime, timedelta
from jinja2 import Template
from random import SystemRandom
from xattr import getxattr, listxattr, setxattr
random = SystemRandom()
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$"
# https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/
@ -27,8 +29,14 @@ RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z
# Cache CA certificate
with open(config.AUTHORITY_CERTIFICATE_PATH) as fh:
ca_buf = fh.read()
ca_cert = x509.load_pem_x509_certificate(ca_buf, default_backend())
certificate_buf = fh.read()
header, _, certificate_der_bytes = pem.unarmor(certificate_buf)
certificate = x509.Certificate.load(certificate_der_bytes)
public_key = asymmetric.load_public_key(certificate["tbs_certificate"]["subject_public_key_info"])
with open(config.AUTHORITY_PRIVATE_KEY_PATH) as fh:
key_buf = fh.read()
header, _, key_der_bytes = pem.unarmor(key_buf)
private_key = asymmetric.load_private_key(key_der_bytes)
def get_request(common_name):
if not re.match(RE_HOSTNAME, common_name):
@ -37,7 +45,8 @@ def get_request(common_name):
try:
with open(path) as fh:
buf = fh.read()
return path, buf, x509.load_pem_x509_csr(buf, default_backend())
header, _, der_bytes = pem.unarmor(buf)
return path, buf, CertificationRequest.load(der_bytes)
except EnvironmentError:
raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path)
@ -47,13 +56,15 @@ def get_signed(common_name):
path = os.path.join(config.SIGNED_DIR, common_name + ".pem")
with open(path) as fh:
buf = fh.read()
return path, buf, x509.load_pem_x509_certificate(buf, default_backend())
header, _, der_bytes = pem.unarmor(buf)
return path, buf, x509.Certificate.load(der_bytes)
def get_revoked(serial):
path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial)
with open(path) as fh:
buf = fh.read()
return path, buf, x509.load_pem_x509_certificate(buf, default_backend()), \
header, _, der_bytes = pem.unarmor(buf)
return path, buf, x509.Certificate.load(der_bytes), \
datetime.utcfromtimestamp(os.stat(path).st_ctime)
@ -85,20 +96,19 @@ def store_request(buf, overwrite=False, address="", user=""):
if not buf:
raise ValueError("No signing request supplied")
if isinstance(buf, unicode):
csr = x509.load_pem_x509_csr(buf.encode("ascii"), backend=default_backend())
elif isinstance(buf, str):
csr = x509.load_der_x509_csr(buf, backend=default_backend())
buf = csr.public_bytes(Encoding.PEM)
if pem.detect(buf):
header, _, der_bytes = pem.unarmor(buf)
csr = CertificationRequest.load(der_bytes)
else:
raise ValueError("Invalid type, expected str for PEM and bytes for DER")
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
# TODO: validate common name again
csr = CertificationRequest.load(buf)
buf = pem_armor_csr(csr)
if not re.match(RE_HOSTNAME, common_name.value):
common_name = csr["certification_request_info"]["subject"].native["common_name"]
if not re.match(RE_HOSTNAME, common_name):
raise ValueError("Invalid common name")
request_path = os.path.join(config.REQUESTS_DIR, common_name.value + ".pem")
request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
# If there is cert, check if it's the same
@ -112,27 +122,13 @@ def store_request(buf, overwrite=False, address="", user=""):
fh.write(buf)
os.rename(request_path + ".part", request_path)
attach_csr = buf, "application/x-pem-file", common_name.value + ".csr"
attach_csr = buf, "application/x-pem-file", common_name + ".csr"
mailer.send("request-stored.md",
attachments=(attach_csr,),
common_name=common_name.value)
common_name=common_name)
setxattr(request_path, "user.request.address", address)
setxattr(request_path, "user.request.user", user)
return request_path, csr, common_name.value
def signer_exec(cmd, *bits):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(const.SIGNER_SOCKET_PATH)
sock.send(cmd.encode("ascii"))
sock.send(b"\n")
for bit in bits:
sock.send(bit.encode("ascii"))
sock.sendall(b"\n\n")
buf = sock.recv(8192)
if not buf:
raise Exception("Connection lost")
return buf
return request_path, csr, common_name
def revoke(common_name):
@ -140,9 +136,9 @@ def revoke(common_name):
Revoke valid certificate
"""
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_number)
os.rename(signed_path, revoked_path)
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial))
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number))
push.publish("certificate-revoked", common_name)
@ -155,7 +151,7 @@ def revoke(common_name):
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
mailer.send("certificate-revoked.md",
attachments=(attach_cert,),
serial_hex="%x" % cert.serial,
serial_hex="%x" % cert.serial_number,
common_name=common_name)
return revoked_path
@ -186,12 +182,13 @@ def _list_certificates(directory):
path = os.path.join(directory, filename)
with open(path) as fh:
buf = fh.read()
cert = x509.load_pem_x509_certificate(buf, default_backend())
header, _, der_bytes = pem.unarmor(buf)
cert = x509.Certificate.load(der_bytes)
server = False
extension = cert.extensions.get_extension_for_oid(ExtensionOID.EXTENDED_KEY_USAGE)
for usage in extension.value:
if usage == ExtendedKeyUsageOID.SERVER_AUTH: # TODO: IKE intermediate?
server = True
for extension in cert["tbs_certificate"]["extensions"]:
if extension["extn_id"].native == u"extended_key_usage":
if u"server_auth" in extension["extn_value"].native:
server = True
yield common_name, path, buf, cert, server
def list_signed():
@ -203,10 +200,13 @@ def list_revoked():
def list_server_names():
return [cn for cn, path, buf, cert, server in list_signed() if server]
def export_crl():
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(const.SIGNER_SOCKET_PATH)
sock.send(b"export-crl\n")
def export_crl(pem=True):
builder = CertificateListBuilder(
config.AUTHORITY_CRL_URL,
certificate,
1 # TODO: monotonically increasing
)
for filename in os.listdir(config.REVOKED_DIR):
if not filename.endswith(".pem"):
continue
@ -215,9 +215,15 @@ def export_crl():
revoked_path = os.path.join(config.REVOKED_DIR, filename)
# TODO: Skip expired certificates
s = os.stat(revoked_path)
sock.send(("%s:%d\n" % (serial_number, s.st_ctime)).encode("ascii"))
sock.sendall(b"\n")
return sock.recv(32*1024*1024)
builder.add_certificate(
int(filename[:-4], 16),
datetime.utcfromtimestamp(s.st_ctime),
u"key_compromise")
certificate_list = builder.build(private_key)
if pem:
return pem_armor_crl(certificate_list)
return certificate_list.dump()
def delete_request(common_name):
@ -236,88 +242,17 @@ def delete_request(common_name):
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
headers={"User-Agent": "Certidude API"})
def generate_ovpn_bundle(common_name, owner=None):
# Construct private key
click.echo("Generating %d-bit RSA key for OpenVPN profile..." % const.KEY_SIZE)
key = rsa.generate_private_key(
public_exponent=65537,
key_size=const.KEY_SIZE,
backend=default_backend()
)
key_buf = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
)
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
x509.NameAttribute(k, v) for k, v in (
(NameOID.COMMON_NAME, common_name),
) if v
])).sign(key, hashes.SHA512(), default_backend())
buf = csr.public_bytes(serialization.Encoding.PEM)
# Sign CSR
cert, cert_buf = _sign(csr, buf, overwrite=True)
bundle = Template(open(config.OPENVPN_PROFILE_TEMPLATE).read()).render(
ca = ca_buf, key = key_buf, cert = cert_buf, crl=export_crl(),
servers = list_server_names())
return bundle, cert
def generate_pkcs12_bundle(common_name, owner=None):
"""
Generate private key, sign certificate and return PKCS#12 bundle
"""
# Construct private key
click.echo("Generating %d-bit RSA key for PKCS#12 bundle..." % const.KEY_SIZE)
key = rsa.generate_private_key(
public_exponent=65537,
key_size=const.KEY_SIZE,
backend=default_backend()
)
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, common_name)
])).sign(key, hashes.SHA512(), default_backend())
buf = csr.public_bytes(serialization.Encoding.PEM)
# Sign CSR
cert, cert_buf = _sign(csr, buf, 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())))
p12.set_certificate(
crypto.load_certificate(crypto.FILETYPE_PEM, cert_buf))
p12.set_ca_certificates([
crypto.load_certificate(crypto.FILETYPE_PEM, ca_buf)])
return p12.export("1234"), cert
def sign(common_name, overwrite=False):
"""
Sign certificate signing request via signer process
Sign certificate signing request by it's common name
"""
req_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
with open(req_path) as fh:
csr_buf = fh.read()
csr = x509.load_pem_x509_csr(csr_buf, backend=default_backend())
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
header, _, der_bytes = pem.unarmor(csr_buf)
csr = CertificationRequest.load(der_bytes)
# Sign with function below
cert, buf = _sign(csr, csr_buf, overwrite)
@ -326,15 +261,17 @@ def sign(common_name, overwrite=False):
return cert, buf
def _sign(csr, buf, overwrite=False):
assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n")
assert isinstance(csr, x509.CertificateSigningRequest)
# TODO: CRLDistributionPoints, OCSP URL, Certificate URL
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value)
assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n")
assert isinstance(csr, CertificationRequest)
csr_pubkey = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"])
common_name = csr["certification_request_info"]["subject"].native["common_name"]
cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name)
renew = False
attachments = [
(buf, "application/x-pem-file", common_name.value + ".csr"),
(buf, "application/x-pem-file", common_name + ".csr"),
]
revoked_path = None
@ -344,13 +281,18 @@ def _sign(csr, buf, overwrite=False):
if os.path.exists(cert_path):
with open(cert_path) as fh:
prev_buf = fh.read()
prev = x509.load_pem_x509_certificate(prev_buf, default_backend())
header, _, der_bytes = pem.unarmor(prev_buf)
prev = x509.Certificate.load(der_bytes)
# TODO: assert validity here again?
renew = prev.public_key().public_numbers() == csr.public_key().public_numbers()
renew = \
asymmetric.load_public_key(prev["tbs_certificate"]["subject_public_key_info"]) == \
csr_pubkey
# BUGBUG: is this enough?
if overwrite:
# TODO: is this the best approach?
prev_serial_hex = "%x" % prev.serial
prev_serial_hex = "%x" % prev.serial_number
revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex)
os.rename(cert_path, revoked_path)
attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")]
@ -359,18 +301,40 @@ def _sign(csr, buf, overwrite=False):
raise EnvironmentError("Will not overwrite existing certificate")
# Sign via signer process
cert_buf = signer_exec("sign-request", buf)
cert = x509.load_pem_x509_certificate(cert_buf, default_backend())
builder = CertificateBuilder({u'common_name': common_name }, csr_pubkey)
builder.serial_number = random.randint(
0x1000000000000000000000000000000000000000,
0xffffffffffffffffffffffffffffffffffffffff)
now = datetime.utcnow()
builder.begin_date = now - timedelta(minutes=5)
builder.end_date = now + timedelta(days=config.SERVER_CERTIFICATE_LIFETIME
if server_flags(common_name)
else config.CLIENT_CERTIFICATE_LIFETIME)
builder.issuer = certificate
builder.ca = False
builder.key_usage = set([u"digital_signature", u"key_encipherment"])
# OpenVPN uses CN while StrongSwan uses SAN
if server_flags(common_name):
builder.subject_alt_domains = [common_name]
builder.extended_key_usage = set([u"server_auth", u"1.3.6.1.5.5.8.2.2", u"client_auth"])
else:
builder.extended_key_usage = set([u"client_auth"])
end_entity_cert = builder.build(private_key)
end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert)
with open(cert_path + ".part", "wb") as fh:
fh.write(cert_buf)
fh.write(end_entity_cert_buf)
os.rename(cert_path + ".part", cert_path)
attachments.append((cert_buf, "application/x-pem-file", common_name.value + ".crt"))
cert_serial_hex = "%x" % cert.serial
attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt"))
cert_serial_hex = "%x" % end_entity_cert.serial_number
# Create symlink
os.symlink(
"../%s.pem" % common_name.value,
os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial))
link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % end_entity_cert.serial_number)
assert not os.path.exists(link_name), "Certificate with same serial number already exists: %s" % link_name
os.symlink("../%s.pem" % common_name, link_name)
# Copy filesystem attributes to newly signed certificate
if revoked_path:
@ -387,8 +351,8 @@ def _sign(csr, buf, overwrite=False):
url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest()
click.echo("Publishing certificate at %s ..." % url)
requests.post(url, data=cert_buf,
requests.post(url, data=end_entity_cert_buf,
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
push.publish("request-signed", common_name.value)
return cert, cert_buf
push.publish("request-signed", common_name)
return end_entity_cert, end_entity_cert_buf

View File

@ -12,6 +12,7 @@ import socket
import string
import subprocess
import sys
from asn1crypto.util import timezone
from base64 import b64encode
from configparser import ConfigParser, NoOptionError, NoSectionError
from certidude.common import ip_address, ip_network, apt, rpm, pip, drop_privileges, selinux_fixup
@ -26,9 +27,7 @@ logger = logging.getLogger(__name__)
# keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_client_config.html
# strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA
# Parse command-line argument defaults from environment
NOW = datetime.utcnow().replace(tzinfo=None)
NOW = datetime.utcnow()
def fqdn_required(func):
def wrapped(**args):
@ -321,8 +320,8 @@ def certidude_request(fork, renew, no_wait, kerberos):
with open(certificate_path, "rb") as ch, open(request_path, "rb") as rh, open(key_path, "rb") as kh:
cert_buf = ch.read()
cert = asymmetric.load_certificate(cert_buf)
expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native
if renewal_overlap and datetime.now() > expires - timedelta(days=renewal_overlap):
expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
if renewal_overlap and NOW > expires - timedelta(days=renewal_overlap):
click.echo("Certificate will expire %s, will attempt to renew" % expires)
renew = True
headers["X-Renewal-Signature"] = b64encode(
@ -931,15 +930,13 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
@fqdn_required
def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags):
# Install only rarely changing stuff from OS package management
apt("python-setproctitle cython python-dev libkrb5-dev libffi-dev libssl-dev")
apt("python-mimeparse python-markdown python-xattr python-jinja2 python-cffi")
apt("python-ldap software-properties-common libsasl2-modules-gssapi-mit")
pip("gssapi falcon humanize ipaddress simplepam humanize requests pyopenssl")
apt("cython python-dev python-mimeparse python-markdown python-xattr python-jinja2 python-cffi python-ldap software-properties-common libsasl2-modules-gssapi-mit")
pip("gssapi falcon humanize ipaddress simplepam humanize requests")
click.echo("Software dependencies installed")
os.system("add-apt-repository -y ppa:nginx/stable")
os.system("apt-get update")
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
os.system("add-apt-repository -y ppa:nginx/stable")
os.system("apt-get update")
os.system("apt-get install -y libnginx-mod-nchan")
if not os.path.exists("/usr/sbin/nginx"):
os.system("apt-get install -y nginx")
@ -1091,13 +1088,13 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
builder.serial_number = random.randint(
0x100000000000000000000000000000000000000,
0xfffffffffffffffffffffffffffffffffffffff)
now = datetime.utcnow()
builder.begin_date = now - timedelta(minutes=5)
builder.end_date = now + timedelta(days=authority_lifetime)
builder.begin_date = NOW - timedelta(minutes=5)
builder.end_date = NOW + timedelta(days=authority_lifetime)
if server_flags:
builder.key_usage(set(['digital_signature', 'key_encipherment', 'key_cert_sign', 'crl_sign']))
builder.extended_key_usage(['server_auth', "1.3.6.1.5.5.8.2.2"])
builder.key_usage = set(['digital_signature', 'key_encipherment', 'key_cert_sign', 'crl_sign'])
builder.extended_key_usage = set(['server_auth', "1.3.6.1.5.5.8.2.2"])
certificate = builder.build(private_key)
@ -1162,9 +1159,6 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
click.echo("sha1sum: %s" % hashlib.sha1(buf).hexdigest())
click.echo("sha256sum: %s" % hashlib.sha256(buf).hexdigest())
click.echo()
for ext in cert.extensions:
print " -", ext.value
click.echo()
if not hide_requests:
for common_name, path, buf, csr, server in authority.list_requests():
@ -1172,6 +1166,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
if not verbose:
click.echo("s " + path)
continue
click.echo()
click.echo(click.style(common_name, fg="blue"))
click.echo("=" * len(common_name))
click.echo("State: ? " + click.style("submitted", fg="yellow") + " " + naturaltime(created) + click.style(", %s" %created, fg="white"))
@ -1181,35 +1176,39 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
if show_signed:
for common_name, path, buf, cert, server in authority.list_signed():
signed = cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None)
expires = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
if not verbose:
if cert.not_valid_before < NOW and cert.not_valid_after > NOW:
if signed < NOW and NOW < expires:
click.echo("v " + path)
elif NOW > cert.not_valid_after:
elif expires < NOW:
click.echo("e " + path)
else:
click.echo("y " + path)
continue
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial, fg="white"))
click.echo()
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white"))
click.echo("="*(len(common_name)+60))
expires = 0 # TODO
if cert.not_valid_before < NOW and cert.not_valid_after > NOW:
click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(cert.not_valid_after) + click.style(", %s" % cert.not_valid_after, fg="white"))
elif NOW > cert.not_valid_after:
click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" %expires, fg="white"))
if signed < NOW and NOW < expires:
click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(expires) + click.style(", %s" % expires, fg="white"))
elif NOW > expires:
click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" % expires, fg="white"))
else:
click.echo("Status: " + click.style("not valid yet", fg="red") + click.style(", %s" %expires, fg="white"))
click.echo("Status: " + click.style("not valid yet", fg="red") + click.style(", %s" % expires, fg="white"))
click.echo()
click.echo("openssl x509 -in %s -text -noout" % path)
dump_common(common_name, path, cert)
for ext in cert["tbs_certificate"]["extensions"]:
print " - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native))
if show_revoked:
for common_name, path, buf, cert, server in authority.list_revoked():
if not verbose:
click.echo("r " + path)
continue
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial, fg="white"))
click.echo()
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white"))
click.echo("="*(len(common_name)+60))
_, _, _, _, _, _, _, _, mtime, _ = os.stat(path)
@ -1217,24 +1216,24 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
click.echo("Status: " + click.style("revoked", fg="red") + " %s%s" % (naturaltime(NOW-changed), click.style(", %s" % changed, fg="white")))
click.echo("openssl x509 -in %s -text -noout" % path)
dump_common(common_name, path, cert)
click.echo()
for ext in cert["tbs_certificate"]["extensions"]:
print " - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native))
@click.command("sign", help="Sign certificate")
@click.argument("common_name")
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
def certidude_sign(common_name, overwrite):
drop_privileges()
from certidude import authority
drop_privileges()
cert = authority.sign(common_name, overwrite)
@click.command("revoke", help="Revoke certificate")
@click.argument("common_name")
def certidude_revoke(common_name):
drop_privileges()
from certidude import authority
drop_privileges()
authority.revoke(common_name)
@ -1242,10 +1241,10 @@ def certidude_revoke(common_name):
def certidude_cron():
import itertools
from certidude import authority, config
now = datetime.now()
for cn, path, buf, cert, server in itertools.chain(authority.list_signed(), authority.list_revoked()):
if cert.not_valid_after < now:
expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial)
expires = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
if expires < NOW:
expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial_number)
assert not os.path.exists(expired_path)
os.rename(path, expired_path)
click.echo("Moved %s to %s" % (path, expired_path))
@ -1258,7 +1257,6 @@ def certidude_cron():
def certidude_serve(port, listen, fork):
import pwd
from setproctitle import setproctitle
from certidude.signer import SignServer
from certidude import authority, const, push
if port == 80:
@ -1272,7 +1270,7 @@ def certidude_serve(port, listen, fork):
# 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)
by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number)
if not os.path.exists(by_serial):
click.echo("Linking %s to ../%s.pem" % (by_serial, cn))
os.symlink("../%s.pem" % cn, by_serial)
@ -1291,55 +1289,6 @@ def certidude_serve(port, listen, fork):
rh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
log_handlers.append(rh)
"""
Spawn signer process
"""
if os.path.exists(const.SIGNER_SOCKET_PATH):
os.unlink(const.SIGNER_SOCKET_PATH)
signer_pid = os.fork()
if not signer_pid:
click.echo("Signer process spawned with PID %d at %s" % (os.getpid(), const.SIGNER_SOCKET_PATH))
setproctitle("[signer]")
with open(const.SIGNER_PID_PATH, "w") as fh:
fh.write("%d\n" % os.getpid())
logging.basicConfig(
filename=const.SIGNER_LOG_PATH,
level=logging.INFO)
os.umask(0o007)
server = SignServer()
# Drop privileges
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
os.chown(const.SIGNER_SOCKET_PATH, uid, gid)
os.chmod(const.SIGNER_SOCKET_PATH, 0770)
click.echo("Dropping privileges of signer")
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("nobody")
os.setgroups([])
os.setgid(gid)
os.setuid(uid)
try:
asyncore.loop()
except asyncore.ExitNow:
pass
click.echo("Signer was shut down")
return
click.echo("Waiting for signer to start up")
time_left = 2.0
delay = 0.1
while not os.path.exists(const.SIGNER_SOCKET_PATH) and time_left > 0:
sleep(delay)
time_left -= delay
assert authority.signer_exec("ping") == "pong"
click.echo("Signer alive")
click.echo("Users subnets: %s" %
", ".join([str(j) for j in config.USER_SUBNETS]))
click.echo("Administrative subnets: %s" %
@ -1392,7 +1341,6 @@ def certidude_serve(port, listen, fork):
def cleanup_handler(*args):
push.publish("server-stopped")
logger.debug(u"Shutting down Certidude")
assert authority.signer_exec("exit") == "ok"
sys.exit(0) # TODO: use another code, needs test refactor
import signal

View File

@ -12,9 +12,6 @@ CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid")
SERVER_LOG_PATH = "/var/log/certidude-server.log"
SIGNER_SOCKET_PATH = "/run/certidude/signer.sock"
SIGNER_PID_PATH = os.path.join(RUN_DIR, "signer.pid")
SIGNER_LOG_PATH = "/var/log/certidude-signer.log"
STORAGE_PATH = "/var/lib/certidude/"
try:

View File

@ -1,235 +0,0 @@
import random
import socket
import os
import asyncore
import asynchat
from base64 import b64decode, b64encode
from certidude import const, config
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, padding, serialization
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives.asymmetric import padding
from datetime import datetime, timedelta
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInformationAccessOID
import random
class SignHandler(asynchat.async_chat):
def __init__(self, sock, server):
asynchat.async_chat.__init__(self, sock=sock)
self.buffer = []
self.set_terminator(b"\n\n")
self.server = server
def parse_command(self, cmd, body=""):
now = datetime.utcnow()
if cmd == "export-crl":
"""
Generate CRL object based on certificate serial number and revocation timestamp
"""
builder = x509.CertificateRevocationListBuilder(
).last_update(
now - timedelta(minutes=5)
).next_update(
now + timedelta(seconds=config.REVOCATION_LIST_LIFETIME)
).issuer_name(self.server.certificate.issuer
).add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(
self.server.certificate.public_key()), False)
if body:
for line in body.split("\n"):
serial_number, timestamp = line.split(":")
revocation = x509.RevokedCertificateBuilder(
).serial_number(int(serial_number, 16)
).revocation_date(datetime.utcfromtimestamp(int(timestamp))
).add_extension(x509.CRLReason(x509.ReasonFlags.key_compromise), False
).build(default_backend())
builder = builder.add_revoked_certificate(revocation)
crl = builder.sign(
self.server.private_key,
hashes.SHA512(),
default_backend())
self.send(crl.public_bytes(Encoding.PEM))
elif cmd == "ping":
self.send("pong")
self.close()
elif cmd == "exit":
self.send("ok")
self.close()
raise asyncore.ExitNow()
elif cmd == "sign-pkcs7":
signer = self.server.private_key.signer(
padding.PKCS1v15(),
hashes.SHA1()
)
signer.update(b64decode(body))
self.send(b64encode(signer.finalize()))
elif cmd == "decrypt-pkcs7":
self.send(b64encode(self.server.private_key.decrypt(b64decode(body), padding.PKCS1v15())))
self.close()
elif cmd == "sign-request":
# Only common name and public key are used from request
request = x509.load_pem_x509_csr(body, default_backend())
common_name, = request.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
# 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)
aia = [
x509.AccessDescription(
AuthorityInformationAccessOID.CA_ISSUERS,
x509.UniformResourceIdentifier(config.AUTHORITY_CERTIFICATE_URL))
]
if config.AUTHORITY_OCSP_URL:
aia.append(
x509.AccessDescription(
AuthorityInformationAccessOID.OCSP,
x509.UniformResourceIdentifier(config.AUTHORITY_OCSP_URL)))
builder = x509.CertificateBuilder(
).subject_name(
x509.Name([common_name])
).serial_number(random.randint(
0x1000000000000000000000000000000000000000,
0x7fffffffffffffffffffffffffffffffffffffff)
).issuer_name(
self.server.certificate.issuer
).public_key(
request.public_key()
).not_valid_before(
now - timedelta(minutes=5)
).not_valid_after(
now + timedelta(days=
config.SERVER_CERTIFICATE_LIFETIME
if server_flags
else config.CLIENT_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(
extended_key_usage_flags),
critical=True,
).add_extension(
x509.SubjectKeyIdentifier.from_public_key(
request.public_key()),
critical=False
).add_extension(
x509.AuthorityInformationAccess(aia),
critical=False
).add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(
self.server.certificate.public_key()),
critical=False
)
if config.AUTHORITY_CRL_URL:
builder = builder.add_extension(
x509.CRLDistributionPoints([
x509.DistributionPoint(
full_name=[
x509.UniformResourceIdentifier(
config.AUTHORITY_CRL_URL)],
relative_name=None,
crl_issuer=None,
reasons=None)
]),
critical=False
)
# OpenVPN uses CN while StrongSwan uses SAN
if server_flags:
builder = builder.add_extension(
x509.SubjectAlternativeName(
[x509.DNSName(common_name.value)]
),
critical=False
)
cert = builder.sign(self.server.private_key, hashes.SHA512(), default_backend())
self.send(cert.public_bytes(serialization.Encoding.PEM))
else:
raise NotImplementedError("Unknown command: %s" % cmd)
self.close_when_done()
def found_terminator(self):
args = (b"".join(self.buffer)).split("\n", 1)
self.parse_command(*args)
self.buffer = []
def collect_incoming_data(self, data):
self.buffer.append(data)
import signal
import click
class SignServer(asyncore.dispatcher):
def __init__(self):
asyncore.dispatcher.__init__(self)
if os.path.exists(const.SIGNER_SOCKET_PATH):
os.unlink(const.SIGNER_SOCKET_PATH)
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.bind(const.SIGNER_SOCKET_PATH)
self.listen(5)
# Load CA private key and certificate
click.echo("Signer reading private key from %s" % config.AUTHORITY_PRIVATE_KEY_PATH)
self.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())
click.echo("Signer reading certificate from %s" % config.AUTHORITY_CERTIFICATE_PATH)
self.certificate = x509.load_pem_x509_certificate(
open(config.AUTHORITY_CERTIFICATE_PATH).read(),
backend=default_backend())
def handle_accept(self):
pair = self.accept()
if pair is not None:
sock, addr = pair
handler = SignHandler(sock, self)

View File

@ -1,7 +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>
<button class="icon sign" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}',type:'patch'});">Sign</button>
<button class="icon sign" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}',type:'post'});">Sign</button>
<button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}',type:'delete'});">Delete</button>

View File

@ -1,9 +1,9 @@
Renewed {{ common_name.value }} ({{ cert_serial_hex }})
Renewed {{ common_name }} ({{ cert_serial_hex }})
This is simply to notify that certificate for {{ common_name.value }}
This is simply to notify that certificate for {{ common_name }}
was renewed and the serial number of the new certificate is {{ cert_serial_hex }}.
The new certificate is valid from {{ cert.not_valid_before }} until
{{ cert.not_valid_after }}.
The new certificate is valid from {{ builder.begin_date }} until
{{ builder.end_date }}.
Services making use of those certificates should continue working as expected.

View File

@ -1,11 +1,11 @@
Signed {{ common_name.value }} ({{ cert_serial_hex }})
Signed {{ common_name }} ({{ cert_serial_hex }})
This is simply to notify that certificate {{ common_name.value }}
This is simply to notify that certificate {{ common_name }}
with serial number {{ cert_serial_hex }}
was signed{% if signer %} by {{ signer }}{% endif %}.
The certificate is valid from {{ cert.not_valid_before }} until
{{ cert.not_valid_after }}.
The certificate is valid from {{ builder.begin_date }} until
{{ builder.end_date }}.
{% if overwritten %}
By doing so existing certificate with the same common name

View File

@ -1,69 +1,136 @@
# To set up SSL certificates using Let's Encrypt run:
#
# apt install letsencrypt
# certbot certonly -d {{common_name}} --webroot /var/www/html/
#
# Also uncomment URL rewriting and SSL configuration below
# Basic DoS prevention measures
limit_conn addr 10;
client_body_timeout 5s;
client_header_timeout 5s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_conn_zone $binary_remote_addr zone=addr:10m;
# Backend configuration
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-SSL-CERT $ssl_client_cert;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
proxy_set_header Host $host;
send_timeout 600;
# Don't buffer any messages
nchan_message_buffer_length 0;
# To use CA-s own certificate for HTTPS
ssl_certificate /var/lib/certidude/{{common_name}}/ca_crt.pem;
ssl_certificate_key /var/lib/certidude/{{common_name}}/ca_key.pem;
# To use Let's Encrypt certificates
#ssl_certificate /etc/letsencrypt/live/{{common_name}}/fullchain.pem;
#ssl_certificate_key /etc/letsencrypt/live/{{common_name}}/privkey.pem;
# Also run the following to set up Let's Encrypt certificates:
#
# apt install letsencrypt
# certbot certonly -d {{common_name}} --webroot /var/www/html/
server {
# Section for serving insecure HTTP, note that this is suitable for
# OCSP, SCEP, CRL-s etc which is already covered by PKI protection mechanisms.
# This also solves the chicken-and-egg problem of deploying the certificates
server_name {{ common_name }};
listen 80 default_server;
# rewrite ^ https://$server_name$request_uri? permanent;
#}
#server {
# server_name {{ common_name }};
# listen 443 ssl http2 default_server;
# add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;";
# ssl_certificate /etc/letsencrypt/live/{{common_name}}/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/{{common_name}}/privkey.pem;
root {{static_path}};
# Basic DoS prevention measures
limit_conn addr 10;
client_body_timeout 5s;
client_header_timeout 5s;
# Proxy pass to backend
location /api/ {
proxy_pass http://127.0.1.1:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
limit_req zone=api burst=5;
}
# This is for Let's Encrypt
location /.well-known/ {
alias /var/www/html/.well-known/;
}
# Path to static files
root {{static_path}};
# Rewrite /cgi-bin/pkiclient.exe to /api/scep for SCEP protocol
location /cgi-bin/pkiclient.exe {
rewrite /cgi-bin/pkiclient.exe /api/scep/ last;
}
{% if not push_server %}
# This only works with nchan, for Debian 9 just apt install libnginx-mod-nchan
# For Ubuntu and older Debian releases install nchan from https://nchan.io/
# Long poll for CSR submission
location ~ "^/lp/sub/(.*)" {
nchan_channel_id $1;
nchan_subscriber longpoll;
}
# Comment everything below in this server definition if you're using HTTPS
# Event source for web interface
location ~ "^/ev/sub/(.*)" {
nchan_channel_id $1;
nchan_subscriber eventsource;
}
{% endif %}
# Uncomment following to enable HTTPS
#rewrite ^/$ https://$server_name$request_uri? permanent;
}
server {
# Section for accessing web interface over HTTPS
listen 443 ssl http2 default_server;
server_name {{ common_name }};
# HSTS header below should make sure web interface will be accessed over HTTPS only
# once it has been configured
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;";
# Proxy pass to backend
location /api/ {
proxy_pass http://127.0.1.1:8080/api/;
limit_req zone=api burst=5;
}
# Path to static files
root {{static_path}};
# This is for Let's Encrypt enroll/renewal
location /.well-known/ {
alias /var/www/html/.well-known/;
}
# Event stream for pushinge events to web browsers
location ~ "^/ev/sub/(.*)" {
nchan_channel_id $1;
nchan_subscriber eventsource;
}
}
server {
# Section for certificate authenticated HTTPS clients,
# for submitting information to CA eg. leases
# and for delivering scripts to clients
server_name {{ common_name }};
listen 8443 ssl http2;
# Require client authentication with certificate
ssl_verify_client on;
ssl_client_certificate /var/lib/certidude/{{ common_name }}/ca_crt.pem;
# Proxy pass to backend
location /api/ {
proxy_pass http://127.0.1.1:8080/api/;
limit_req zone=api burst=5;
}
# Long poll
location ~ "^/lp/sub/(.*)" {
nchan_channel_id $1;
nchan_subscriber longpoll;
}
}
{% if not push_server %}
@ -75,13 +142,11 @@ server {
location ~ "^/lp/pub/(.*)" {
nchan_publisher;
nchan_channel_id $1;
nchan_message_buffer_length 0;
}
location ~ "^/ev/pub/(.*)" {
nchan_publisher;
nchan_channel_id $1;
nchan_message_buffer_length 0;
}
}
{% endif %}

View File

@ -1,3 +1,5 @@
click>=6.7
configparser>=3.5.0
certbuilder
crlbuilder
oscrypto

View File

@ -80,12 +80,6 @@ def clean_client():
def clean_server():
if os.path.exists("/run/certidude/signer.pid"):
with open("/run/certidude/signer.pid") as fh:
try:
os.kill(int(fh.read()), 15)
except OSError:
pass
if os.path.exists("/run/certidude/server.pid"):
with open("/run/certidude/server.pid") as fh:
try:
@ -239,16 +233,18 @@ def test_cli_setup_authority():
os.setgid(0) # Restore GID
os.umask(0022)
# Make sure nginx is running
assert not result.exception, result.output
assert os.getuid() == 0 and os.getgid() == 0, "Serve dropped permissions incorrectly!"
assert os.system("nginx -t") == 0, "invalid nginx configuration"
os.system("service nginx restart")
assert os.path.exists("/run/nginx.pid"), "nginx wasn't started up properly"
from certidude import config, authority, auth, user
assert authority.ca_cert.serial_number >= 0x100000000000000000000000000000000000000
assert authority.ca_cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff
assert authority.ca_cert.not_valid_before < datetime.now()
assert authority.ca_cert.not_valid_after > datetime.now() + timedelta(days=7000)
assert authority.certificate.serial_number >= 0x100000000000000000000000000000000000000
assert authority.certificate.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff
assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
assert authority.certificate["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=7000)
assert authority.server_flags("lauri@fedora-123") == False
assert authority.server_flags("fedora-123") == False
assert authority.server_flags("vpn.example.lan") == True
@ -412,12 +408,12 @@ def test_cli_setup_authority():
assert not result.exception, result.output
# Test sign API call
r = client().simulate_patch("/api/request/test/")
r = client().simulate_post("/api/request/test/")
assert r.status_code == 401, r.text
r = client().simulate_patch("/api/request/test/",
r = client().simulate_post("/api/request/test/",
headers={"Authorization":usertoken})
assert r.status_code == 403, r.text
r = client().simulate_patch("/api/request/test/",
r = client().simulate_post("/api/request/test/",
headers={"Authorization":admintoken})
assert r.status_code == 201, r.text
assert "Signed " in inbox.pop(), inbox
@ -476,7 +472,7 @@ def test_cli_setup_authority():
# Test revocations API call
r = client().simulate_get("/api/revoked/",
headers={"Accept":"application/x-pem-file"})
assert r.status_code == 200, r.text # if this breaks certidude serve has no access to signer socket
assert r.status_code == 200, r.text
assert r.headers.get('content-type') == "application/x-pem-file"
r = client().simulate_get("/api/revoked/")
@ -672,29 +668,11 @@ def test_cli_setup_authority():
assert "/ev/sub/" in r.text, r.text
assert r.json, r.text
assert r.json.get("authority"), r.text
assert r.json.get("authority").get("events"), r.text
#################################
### Subscribe to event source ###
#################################
ev_pid = os.fork()
if not ev_pid:
url = r.json.get("authority").get("events")
if url.startswith("/"): # Expand URL
url = "http://ca.example.lan" + url
r = requests.get(url, headers={"Accept": "text/event-stream"}, stream=True)
lines = ["data: userbot@fedora-15417dc5", "event: request-signed"] # In reverse order!
assert r.status_code == 200, r.text
for line in r.iter_lines():
if not line or line.startswith("id:") or line.startswith(":"):
continue
assert line == lines.pop(), line
if not lines:
return
assert False, r.text # This should not happen
return
ev_url = r.json.get("authority").get("events")
assert ev_url, r.text
if ev_url.startswith("/"): # Expand URL
ev_url = "http://ca.example.lan" + ev_url
assert ev_url.startswith("http://ca.example.lan/ev/sub/")
#######################
@ -704,6 +682,7 @@ def test_cli_setup_authority():
r = client().simulate_post("/api/token/")
assert r.status_code == 404, r.text
"""
config.BUNDLE_FORMAT = "ovpn"
config.USER_ENROLLMENT_ALLOWED = True
@ -734,6 +713,7 @@ def test_cli_setup_authority():
assert r2.status_code == 200 # token consumed by anyone on unknown device
assert r2.headers.get('content-type') == "application/x-pkcs12"
assert "Signed " in inbox.pop(), inbox
"""
# Beyond this point don't use client()
const.STORAGE_PATH = "/tmp/"
@ -765,12 +745,13 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
assert "refused to sign" in result.output, result.output
assert "(Autosign failed, only client certificates allowed to be signed automatically)" in result.output, result.output
child_pid = os.fork()
if not child_pid:
result = runner.invoke(cli, ['sign', 'www.example.lan'])
result = runner.invoke(cli, ["sign", "www.example.lan"])
assert not result.exception, result.output
assert "Publishing request-signed event 'www.example.lan' on http://localhost/ev/pub/" in result.output, result.output
return
else:
os.waitpid(child_pid, 0)
@ -785,13 +766,10 @@ def test_cli_setup_authority():
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
#assert "Writing certificate to:" in result.output, result.output
assert "Attached renewal signature" in result.output, result.output
#assert "refused to sign immideately" not in result.output, result.output
# Test nginx setup
assert os.system("nginx -t") == 0, "Generated nginx config was invalid"
# TODO: test client verification with curl
###############
### OpenVPN ###
@ -818,13 +796,17 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
assert "(Autosign failed, only client certificates allowed to be signed automatically)" in result.output, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/vpn.example.lan.pem")
child_pid = os.fork()
if not child_pid:
result = runner.invoke(cli, ['sign', 'vpn.example.lan'])
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/vpn.example.lan.pem")
result = runner.invoke(cli, ["sign", "vpn.example.lan"])
assert not result.exception, result.output
assert "overwrit" not in result.output, result.output
assert "Publishing request-signed event 'vpn.example.lan' on http://localhost/ev/pub/" in result.output, result.output
return
else:
os.waitpid(child_pid, 0)
@ -859,10 +841,164 @@ def test_cli_setup_authority():
# TODO: Check that tunnel interfaces came up, perhaps try to ping?
# TODO: assert key, req, cert paths were included correctly in OpenVPN config
clean_client()
###############
result = runner.invoke(cli, ['setup', 'openvpn', 'networkmanager', "-cn", "roadwarrior3", "ca.example.lan", "vpn.example.lan"])
assert not result.exception, result.output
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
assert "Writing certificate to:" in result.output, result.output
#################################
### Subscribe to event source ###
#################################
ev_pid = os.fork()
if not ev_pid:
r = requests.get(ev_url, headers={"Accept": "text/event-stream"}, stream=True)
assert r.status_code == 200, r.text
i = r.iter_lines()
assert i.next() == ": hi"
assert not i.next()
# IPSec gateway below
assert i.next() == "event: log-entry", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Served CA certificate ')
assert not i.next()
assert i.next() == "event: log-entry", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Serving revocation list (PEM)')
assert not i.next()
assert i.next() == "event: log-entry", i.next() # FIXME
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Serving revocation list (PEM)')
assert not i.next()
assert i.next() == "event: request-submitted", "%s; %s" % (i.next(), i.next())
assert i.next().startswith("id:")
assert i.next() == "data: ipsec.example.lan"
assert not i.next()
assert i.next() == "event: log-entry", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Stored signing request ipsec.example.lan ')
assert not i.next()
assert i.next() == "event: log-entry", i.next() # FIXME
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Stored signing request ipsec.example.lan ')
assert not i.next()
assert i.next() == "event: request-signed"
assert i.next().startswith("id:")
assert i.next().startswith('data: ipsec.example.lan')
assert not i.next()
assert i.next() == "event: log-entry", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Serving revocation list (PEM)')
assert not i.next()
assert i.next() == "event: log-entry", i.next() # FIXME
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Serving revocation list (PEM)')
assert not i.next()
assert i.next() == "event: log-entry", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Served certificate ipsec.example.lan')
assert not i.next()
assert i.next() == "event: log-entry", i.next() # FIXME
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Served certificate ipsec.example.lan')
assert not i.next()
# IPsec client as service enroll
assert i.next() == "event: log-entry", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Serving revocation list (PEM)')
assert not i.next()
assert i.next() == "event: log-entry", i.next() # FIXME
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Serving revocation list (PEM)')
assert not i.next()
assert i.next() == "event: request-signed", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: roadwarrior2')
assert not i.next()
assert i.next() == "event: log-entry", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Autosigned roadwarrior2')
assert not i.next()
assert i.next() == "event: log-entry", i.next() # FIXME
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Autosigned roadwarrior2')
assert not i.next()
# IPSec client using Networkmanger enroll
assert i.next() == "event: log-entry", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Served CA certificate ')
assert not i.next()
assert i.next() == "event: log-entry", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Serving revocation list (PEM)')
assert not i.next()
assert i.next() == "event: log-entry", i.next() # FIXME
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Serving revocation list (PEM)')
assert not i.next()
assert i.next() == "event: request-signed", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: roadwarrior4')
assert not i.next()
assert i.next() == "event: log-entry", i.next()
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Autosigned roadwarrior4')
assert not i.next()
assert i.next() == "event: log-entry", i.next() # FIXME
assert i.next().startswith("id:")
assert i.next().startswith('data: {"message": "Autosigned roadwarrior4')
assert not i.next()
# Revoke
assert i.next() == "event: certificate-revoked", i.next() # why?!
assert i.next().startswith("id:")
assert i.next().startswith('data: roadwarrior4')
assert not i.next()
return
#############
### IPSec ###
###############
#############
# Setup gateway
clean_client()
@ -882,11 +1018,15 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem")
child_pid = os.fork()
if not child_pid:
result = runner.invoke(cli, ['sign', 'ipsec.example.lan'])
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem")
result = runner.invoke(cli, ["sign", "ipsec.example.lan"])
assert not result.exception, result.output
assert "overwrit" not in result.output, result.output
assert "Publishing request-signed event 'ipsec.example.lan' on http://localhost/ev/pub/" in result.output, result.output
return
else:
os.waitpid(child_pid, 0)
@ -898,7 +1038,8 @@ def test_cli_setup_authority():
assert "Writing certificate to:" in result.output, result.output
assert os.path.exists("/tmp/ca.example.lan/server_cert.pem")
# Reset config
# IPSec client as service
os.unlink("/etc/certidude/client.conf")
os.unlink("/etc/certidude/services.conf")
@ -917,23 +1058,7 @@ def test_cli_setup_authority():
assert "Writing certificate to:" in result.output, result.output
######################
### NetworkManager ###
######################
clean_client()
result = runner.invoke(cli, ['setup', 'openvpn', 'networkmanager', "-cn", "roadwarrior3", "ca.example.lan", "vpn.example.lan"])
assert not result.exception, result.output
with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n")
result = runner.invoke(cli, ["request", "--no-wait"])
assert not result.exception, result.output
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
assert "Writing certificate to:" in result.output, result.output
# IPSec using NetworkManager
clean_client()
@ -1158,11 +1283,15 @@ def test_cli_setup_authority():
##################
os.umask(0022)
assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep")
assert not os.system("cd /tmp/sscep && ./Configure && make sscep_dyn")
if not os.path.exists("/tmp/sscep"):
assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep")
if not os.path.exists("/tmp/sscep/sscep_dyn"):
assert not os.system("cd /tmp/sscep && ./Configure && make sscep_dyn")
assert not os.system("/tmp/sscep/sscep_dyn getca -c /tmp/sscep/ca.pem -u http://ca.example.lan/cgi-bin/pkiclient.exe")
assert not os.system("openssl genrsa -out /tmp/key.pem 1024")
assert not os.system("echo '.\n.\n.\n.\n.\ntest8\n\n\n\n' | openssl req -new -sha256 -key /tmp/key.pem -out /tmp/req.pem")
if not os.path.exists("/tmp/key.pem"):
assert not os.system("openssl genrsa -out /tmp/key.pem 1024")
if not os.path.exists("/tmp/req.pem"):
assert not os.system("echo '.\n.\n.\n.\n.\ntest8\n\n\n\n' | openssl req -new -sha256 -key /tmp/key.pem -out /tmp/req.pem")
assert not os.system("/tmp/sscep/sscep_dyn enroll -c /tmp/sscep/ca.pem -u http://ca.example.lan/cgi-bin/pkiclient.exe -k /tmp/key.pem -r /tmp/req.pem -l /tmp/cert.pem")
# TODO: test e-mails at this point