mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
Migrate from cryptography.io to oscrypto
This commit is contained in:
parent
789d80d712
commit
509f7bfaa8
27
README.rst
27
README.rst
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"))
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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"))
|
||||
|
@ -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")
|
||||
|
@ -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"))
|
||||
|
@ -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,11 +182,12 @@ 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?
|
||||
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
|
||||
|
||||
@ -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
|
||||
|
122
certidude/cli.py
122
certidude/cli.py
@ -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")
|
||||
|
||||
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")
|
||||
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
|
||||
os.system("apt-get install -y libnginx-mod-nchan")
|
||||
if not os.path.exists("/usr/sbin/nginx"):
|
||||
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:
|
||||
|
||||
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()
|
||||
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
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
|
||||
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
||||
|
||||
server {
|
||||
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;
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
|
||||
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.1.1:8080/api/;
|
||||
# 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;
|
||||
|
||||
# Proxy pass to backend
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.1.1:8080/api/;
|
||||
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 %}
|
||||
|
@ -1,3 +1,5 @@
|
||||
click>=6.7
|
||||
configparser>=3.5.0
|
||||
certbuilder
|
||||
crlbuilder
|
||||
oscrypto
|
||||
|
@ -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,10 +1283,14 @@ def test_cli_setup_authority():
|
||||
##################
|
||||
|
||||
os.umask(0022)
|
||||
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")
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user