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
|
Introduction
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Certidude is a novel X.509 Certificate Authority management tool
|
Certidude is a minimalist X.509 Certificate Authority management tool
|
||||||
with privilege isolation mechanism and Kerberos authentication
|
with Kerberos authentication mainly designed for OpenVPN gateway operators to make
|
||||||
mainly designed for OpenVPN gateway operators to make
|
|
||||||
VPN client setup on laptops, desktops and mobile devices as painless as possible.
|
VPN client setup on laptops, desktops and mobile devices as painless as possible.
|
||||||
|
|
||||||
.. figure:: doc/certidude.png
|
.. 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
|
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.
|
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
|
Features
|
||||||
--------
|
--------
|
||||||
@ -68,16 +60,14 @@ Features
|
|||||||
Common:
|
Common:
|
||||||
|
|
||||||
* Standard request, sign, revoke workflow via web interface.
|
* Standard request, sign, revoke workflow via web interface.
|
||||||
* Kerberos and basic auth based web interface authentication.
|
* `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support.
|
||||||
* Preliminary `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support.
|
|
||||||
* PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind.
|
* PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind.
|
||||||
* POSIX groups and Active Directory (LDAP) group membership based authorization.
|
* POSIX groups and Active Directory (LDAP) group membership based authorization.
|
||||||
* Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``.
|
* Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``.
|
||||||
* 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.
|
* Certificate serial numbers are intentionally randomized to avoid leaking information about business practices.
|
||||||
* Server-side events support via `nchan <https://nchan.slact.net/>`_.
|
* 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:
|
Virtual private networking:
|
||||||
|
|
||||||
@ -95,9 +85,7 @@ HTTPS:
|
|||||||
TODO
|
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.
|
* Use `pki.js <https://pkijs.org/>`_ for generating keypair in the browser when claiming a token.
|
||||||
* Signer process logging.
|
|
||||||
|
|
||||||
|
|
||||||
Install
|
Install
|
||||||
@ -110,7 +98,8 @@ System dependencies for Ubuntu 16.04:
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
apt install -y python python-cffi python-click python-configparser \
|
apt install -y
|
||||||
|
python-click python-configparser \
|
||||||
python-humanize \
|
python-humanize \
|
||||||
python-ipaddress python-jinja2 python-ldap python-markdown \
|
python-ipaddress python-jinja2 python-ldap python-markdown \
|
||||||
python-mimeparse python-mysql.connector python-openssl python-pip \
|
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
|
yum install redhat-rpm-config python-devel openssl-devel openldap-devel
|
||||||
|
|
||||||
At the moment package at PyPI is rather outdated.
|
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
|
Setting up authority
|
||||||
|
@ -13,7 +13,6 @@ from certidude import authority, mailer
|
|||||||
from certidude.auth import login_required, authorize_admin
|
from certidude.auth import login_required, authorize_admin
|
||||||
from certidude.user import User
|
from certidude.user import User
|
||||||
from certidude.decorators import serialize, csrf_protection
|
from certidude.decorators import serialize, csrf_protection
|
||||||
from cryptography.x509.oid import NameOID
|
|
||||||
from certidude import const, config
|
from certidude import const, config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -82,8 +81,8 @@ class SessionResource(object):
|
|||||||
common_name = common_name,
|
common_name = common_name,
|
||||||
server = server,
|
server = server,
|
||||||
# TODO: key type, key length, key exponent, key modulo
|
# TODO: key type, key length, key exponent, key modulo
|
||||||
signed = obj.not_valid_before,
|
signed = obj["tbs_certificate"]["validity"]["not_before"].native,
|
||||||
expires = obj.not_valid_after,
|
expires = obj["tbs_certificate"]["validity"]["not_after"].native,
|
||||||
sha256sum = hashlib.sha256(buf).hexdigest(),
|
sha256sum = hashlib.sha256(buf).hexdigest(),
|
||||||
lease = lease,
|
lease = lease,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
@ -108,8 +107,7 @@ class SessionResource(object):
|
|||||||
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
|
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
|
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(
|
common_name = authority.certificate.subject.native["common_name"],
|
||||||
NameOID.COMMON_NAME)[0].value,
|
|
||||||
mailer = dict(
|
mailer = dict(
|
||||||
name = config.MAILER_NAME,
|
name = config.MAILER_NAME,
|
||||||
address = config.MAILER_ADDRESS
|
address = config.MAILER_ADDRESS
|
||||||
|
@ -33,7 +33,7 @@ class LeaseResource(object):
|
|||||||
# TODO: verify signature
|
# TODO: verify signature
|
||||||
common_name = req.get_param("client", required=True)
|
common_name = req.get_param("client", required=True)
|
||||||
path, buf, cert = authority.get_signed(common_name) # TODO: catch exceptions
|
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")
|
raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied")
|
||||||
|
|
||||||
xattr.setxattr(path, "user.lease.outer_address", req.get_param("outer_address", required=True).encode("ascii"))
|
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.startswith("../")
|
||||||
assert link_target.endswith(".pem")
|
assert link_target.endswith(".pem")
|
||||||
path, buf, cert = authority.get_signed(link_target[3:-4])
|
path, buf, cert = authority.get_signed(link_target[3:-4])
|
||||||
if serial != cert.serial:
|
if serial != cert.serial_number:
|
||||||
raise EnvironmentError("integrity check failed")
|
raise EnvironmentError("integrity check failed")
|
||||||
status = ocsp.CertStatus(name='good', value=None)
|
status = ocsp.CertStatus(name='good', value=None)
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
@ -94,9 +94,13 @@ class OCSPResource(object):
|
|||||||
'response_type': u"basic_ocsp_response",
|
'response_type': u"basic_ocsp_response",
|
||||||
'response': {
|
'response': {
|
||||||
'tbs_response_data': response_data,
|
'tbs_response_data': response_data,
|
||||||
|
'certs': [server_certificate.asn1],
|
||||||
'signature_algorithm': {'algorithm': u"sha1_rsa"},
|
'signature_algorithm': {'algorithm': u"sha1_rsa"},
|
||||||
'signature': b64decode(authority.signer_exec("sign-pkcs7", b64encode(response_data.dump()))),
|
'signature': asymmetric.rsa_pkcs1v15_sign(
|
||||||
'certs': [server_certificate.asn1]
|
authority.private_key,
|
||||||
|
response_data.dump(),
|
||||||
|
"sha1"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).dump()
|
}).dump()
|
||||||
|
@ -6,18 +6,16 @@ import ipaddress
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from asn1crypto import pem
|
||||||
|
from asn1crypto.csr import CertificationRequest
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from certidude import config, authority, push, errors
|
from certidude import config, authority, push, errors
|
||||||
from certidude.auth import login_required, login_optional, authorize_admin
|
from certidude.auth import login_required, login_optional, authorize_admin
|
||||||
from certidude.decorators import serialize, csrf_protection
|
from certidude.decorators import serialize, csrf_protection
|
||||||
from certidude.firewall import whitelist_subnets, whitelist_content_types
|
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 datetime import datetime
|
||||||
|
from oscrypto import asymmetric
|
||||||
|
from oscrypto.errors import SignatureError
|
||||||
from xattr import getxattr
|
from xattr import getxattr
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -35,19 +33,14 @@ class RequestListResource(object):
|
|||||||
@whitelist_content_types("application/pkcs10")
|
@whitelist_content_types("application/pkcs10")
|
||||||
def on_post(self, req, resp):
|
def on_post(self, req, resp):
|
||||||
"""
|
"""
|
||||||
Validate and parse certificate signing request
|
Validate and parse certificate signing request, the RESTful way
|
||||||
"""
|
"""
|
||||||
reasons = []
|
reasons = []
|
||||||
body = req.stream.read(req.content_length)
|
body = req.stream.read(req.content_length).encode("ascii")
|
||||||
csr = x509.load_pem_x509_csr(body, default_backend())
|
|
||||||
try:
|
header, _, der_bytes = pem.unarmor(body)
|
||||||
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
csr = CertificationRequest.load(der_bytes)
|
||||||
except: # ValueError?
|
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
||||||
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!")
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Handle domain computer automatic enrollment
|
Handle domain computer automatic enrollment
|
||||||
@ -55,10 +48,10 @@ class RequestListResource(object):
|
|||||||
machine = req.context.get("machine")
|
machine = req.context.get("machine")
|
||||||
if machine:
|
if machine:
|
||||||
if config.MACHINE_ENROLLMENT_ALLOWED:
|
if config.MACHINE_ENROLLMENT_ALLOWED:
|
||||||
if common_name.value != machine:
|
if common_name != machine:
|
||||||
raise falcon.HTTPBadRequest(
|
raise falcon.HTTPBadRequest(
|
||||||
"Bad request",
|
"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
|
# Automatic enroll with Kerberos machine cerdentials
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
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
|
Attempt to renew certificate using currently valid key pair
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
path, buf, cert = authority.get_signed(common_name.value)
|
path, buf, cert = authority.get_signed(common_name)
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
pass
|
pass # No currently valid certificate for this common name
|
||||||
else:
|
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")
|
renewal_header = req.get_header("X-Renewal-Signature")
|
||||||
|
|
||||||
if not renewal_header:
|
if not renewal_header:
|
||||||
# No header supplied, redirect to signed API call
|
# No header supplied, redirect to signed API call
|
||||||
resp.status = falcon.HTTP_SEE_OTHER
|
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
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
renewal_signature = b64decode(renewal_header)
|
renewal_signature = b64decode(renewal_header)
|
||||||
except TypeError, ValueError:
|
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")
|
reasons.append("Renewal failed, bad signature supplied")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
verifier = cert.public_key().verifier(
|
asymmetric.rsa_pss_verify(
|
||||||
renewal_signature,
|
asymmetric.load_certificate(cert),
|
||||||
padding.PSS(
|
renewal_signature, buf + body, "sha512")
|
||||||
mgf=padding.MGF1(hashes.SHA512()),
|
except SignatureError:
|
||||||
salt_length=padding.PSS.MAX_LENGTH
|
logger.error(u"Renewal failed, invalid signature supplied for %s", common_name)
|
||||||
),
|
|
||||||
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)
|
|
||||||
reasons.append("Renewal failed, invalid signature supplied")
|
reasons.append("Renewal failed, invalid signature supplied")
|
||||||
else:
|
else:
|
||||||
# At this point renewal signature was valid but we need to perform some extra checks
|
# At this point renewal signature was valid but we need to perform some extra checks
|
||||||
if datetime.utcnow() > cert.not_valid_after:
|
if datetime.utcnow() > expires:
|
||||||
logger.error(u"Renewal failed, current certificate for %s has expired", common_name.value)
|
logger.error(u"Renewal failed, current certificate for %s has expired", common_name)
|
||||||
reasons.append("Renewal failed, current certificate expired")
|
reasons.append("Renewal failed, current certificate expired")
|
||||||
elif not config.CERTIFICATE_RENEWAL_ALLOWED:
|
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")
|
reasons.append("Renewal requested, but not allowed by authority settings")
|
||||||
else:
|
else:
|
||||||
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
||||||
_, resp.body = authority._sign(csr, body, overwrite=True)
|
_, 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
|
return
|
||||||
|
|
||||||
|
|
||||||
@ -127,17 +116,17 @@ class RequestListResource(object):
|
|||||||
autosigning was requested and certificate can be automatically signed
|
autosigning was requested and certificate can be automatically signed
|
||||||
"""
|
"""
|
||||||
if req.get_param_as_bool("autosign"):
|
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:
|
for subnet in config.AUTOSIGN_SUBNETS:
|
||||||
if req.context.get("remote_addr") in subnet:
|
if req.context.get("remote_addr") in subnet:
|
||||||
try:
|
try:
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
_, resp.body = authority._sign(csr, body)
|
_, 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
|
return
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
logger.info(u"Autosign for %s from %s failed, signed certificate already exists",
|
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")
|
reasons.append("Autosign failed, signed certificate already exists")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -147,7 +136,7 @@ class RequestListResource(object):
|
|||||||
|
|
||||||
# Attempt to save the request otherwise
|
# Attempt to save the request otherwise
|
||||||
try:
|
try:
|
||||||
request_path, _, _ = authority.store_request(body.decode("ascii"),
|
request_path, _, _ = authority.store_request(body,
|
||||||
address=str(req.context.get("remote_addr")))
|
address=str(req.context.get("remote_addr")))
|
||||||
except errors.RequestExists:
|
except errors.RequestExists:
|
||||||
reasons.append("Same request already uploaded exists")
|
reasons.append("Same request already uploaded exists")
|
||||||
@ -160,10 +149,10 @@ class RequestListResource(object):
|
|||||||
"CSR with such CN already exists",
|
"CSR with such CN already exists",
|
||||||
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
|
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
|
||||||
else:
|
else:
|
||||||
push.publish("request-submitted", common_name.value)
|
push.publish("request-submitted", common_name)
|
||||||
|
|
||||||
# Wait the certificate to be signed if waiting is requested
|
# 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"):
|
if req.get_param("wait"):
|
||||||
# Redirect to nginx pub/sub
|
# Redirect to nginx pub/sub
|
||||||
url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest()
|
url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest()
|
||||||
@ -221,7 +210,7 @@ class RequestDetailResource(object):
|
|||||||
@csrf_protection
|
@csrf_protection
|
||||||
@login_required
|
@login_required
|
||||||
@authorize_admin
|
@authorize_admin
|
||||||
def on_patch(self, req, resp, cn):
|
def on_post(self, req, resp, cn):
|
||||||
"""
|
"""
|
||||||
Sign a certificate signing request
|
Sign a certificate signing request
|
||||||
"""
|
"""
|
||||||
|
@ -6,9 +6,6 @@ import logging
|
|||||||
from certidude import const, config
|
from certidude import const, config
|
||||||
from certidude.authority import export_crl, list_revoked
|
from certidude.authority import export_crl, list_revoked
|
||||||
from certidude.firewall import whitelist_subnets
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -23,9 +20,8 @@ class RevocationListResource(object):
|
|||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
("attachment; filename=%s.crl" % const.HOSTNAME).encode("ascii"))
|
("attachment; filename=%s.crl" % const.HOSTNAME).encode("ascii"))
|
||||||
# Convert PEM to DER
|
# Convert PEM to DER
|
||||||
logger.debug(u"Serving revocation list to %s in DER format", req.context.get("remote_addr"))
|
logger.debug(u"Serving revocation list (DER) to %s", req.context.get("remote_addr"))
|
||||||
resp.body = x509.load_pem_x509_crl(export_crl(),
|
resp.body = export_crl(pem=False)
|
||||||
default_backend()).public_bytes(Encoding.DER)
|
|
||||||
elif req.client_accepts("application/x-pem-file"):
|
elif req.client_accepts("application/x-pem-file"):
|
||||||
if req.get_param_as_bool("wait"):
|
if req.get_param_as_bool("wait"):
|
||||||
url = config.LONG_POLL_SUBSCRIBE % "crl"
|
url = config.LONG_POLL_SUBSCRIBE % "crl"
|
||||||
@ -38,7 +34,7 @@ class RevocationListResource(object):
|
|||||||
resp.append_header(
|
resp.append_header(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
("attachment; filename=%s-crl.pem" % const.HOSTNAME).encode("ascii"))
|
("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()
|
resp.body = export_crl()
|
||||||
else:
|
else:
|
||||||
logger.debug(u"Client %s asked revocation list in unsupported format" % req.context.get("remote_addr"))
|
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):
|
def on_get(self, req, resp):
|
||||||
operation = req.get_param("operation")
|
operation = req.get_param("operation")
|
||||||
if operation.lower() == "getcacert":
|
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")
|
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -125,14 +125,16 @@ class SCEPResource(object):
|
|||||||
encrypted_content = encrypted_content_info['encrypted_content'].native
|
encrypted_content = encrypted_content_info['encrypted_content'].native
|
||||||
recipient, = encrypted_envelope['recipient_infos']
|
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()
|
raise SCEPBadCertId()
|
||||||
|
|
||||||
# Since CA private key is not directly readable here, we'll redirect it to signer socket
|
# 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
|
if len(key) == 8: key = key * 3 # Convert DES to 3DES
|
||||||
buf = symmetric.tripledes_cbc_pkcs5_decrypt(key, encrypted_content, iv)
|
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)
|
cert, buf = authority.sign(common_name, overwrite=True)
|
||||||
signed_certificate = asymmetric.load_certificate(buf)
|
signed_certificate = asymmetric.load_certificate(buf)
|
||||||
content = signed_certificate.asn1.dump()
|
content = signed_certificate.asn1.dump()
|
||||||
@ -251,7 +253,11 @@ class SCEPResource(object):
|
|||||||
}),
|
}),
|
||||||
'digest_algorithm': algos.DigestAlgorithm({'algorithm': u"sha1"}),
|
'digest_algorithm': algos.DigestAlgorithm({'algorithm': u"sha1"}),
|
||||||
'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': u"rsassa_pkcs1v15"}),
|
'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")
|
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.set_header("Content-Disposition", ("attachment; filename=%s.json" % cn))
|
||||||
resp.body = json.dumps(dict(
|
resp.body = json.dumps(dict(
|
||||||
common_name = cn,
|
common_name = cn,
|
||||||
serial_number = "%x" % cert.serial,
|
serial_number = "%x" % cert.serial_number,
|
||||||
signed = cert.not_valid_before.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
signed = cert["tbs_certificate"]["validity"]["not_before"].native.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",
|
expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||||
sha256sum = hashlib.sha256(buf).hexdigest()))
|
sha256sum = hashlib.sha256(buf).hexdigest()))
|
||||||
logger.debug(u"Served certificate %s to %s as application/json",
|
logger.debug(u"Served certificate %s to %s as application/json",
|
||||||
cn, req.context.get("remote_addr"))
|
cn, req.context.get("remote_addr"))
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import click
|
import click
|
||||||
import os
|
import os
|
||||||
import random
|
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
import hashlib
|
import hashlib
|
||||||
import socket
|
import socket
|
||||||
from datetime import datetime, timedelta
|
from oscrypto import asymmetric
|
||||||
from cryptography.hazmat.backends import default_backend
|
from asn1crypto import pem, x509
|
||||||
from cryptography import x509
|
from asn1crypto.csr import CertificationRequest
|
||||||
from cryptography.x509.oid import NameOID, ExtensionOID, ExtendedKeyUsageOID
|
from certbuilder import CertificateBuilder
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
||||||
from cryptography.hazmat.primitives import hashes, serialization
|
|
||||||
from cryptography.hazmat.primitives.serialization import Encoding
|
|
||||||
from certidude import config, push, mailer, const
|
from certidude import config, push, mailer, const
|
||||||
from certidude import errors
|
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 jinja2 import Template
|
||||||
|
from random import SystemRandom
|
||||||
from xattr import getxattr, listxattr, setxattr
|
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]))?$"
|
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/
|
# 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
|
# Cache CA certificate
|
||||||
|
|
||||||
with open(config.AUTHORITY_CERTIFICATE_PATH) as fh:
|
with open(config.AUTHORITY_CERTIFICATE_PATH) as fh:
|
||||||
ca_buf = fh.read()
|
certificate_buf = fh.read()
|
||||||
ca_cert = x509.load_pem_x509_certificate(ca_buf, default_backend())
|
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):
|
def get_request(common_name):
|
||||||
if not re.match(RE_HOSTNAME, common_name):
|
if not re.match(RE_HOSTNAME, common_name):
|
||||||
@ -37,7 +45,8 @@ def get_request(common_name):
|
|||||||
try:
|
try:
|
||||||
with open(path) as fh:
|
with open(path) as fh:
|
||||||
buf = fh.read()
|
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:
|
except EnvironmentError:
|
||||||
raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path)
|
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")
|
path = os.path.join(config.SIGNED_DIR, common_name + ".pem")
|
||||||
with open(path) as fh:
|
with open(path) as fh:
|
||||||
buf = fh.read()
|
buf = fh.read()
|
||||||
return path, buf, x509.load_pem_x509_certificate(buf, default_backend())
|
header, _, der_bytes = pem.unarmor(buf)
|
||||||
|
return path, buf, x509.Certificate.load(der_bytes)
|
||||||
|
|
||||||
def get_revoked(serial):
|
def get_revoked(serial):
|
||||||
path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial)
|
path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial)
|
||||||
with open(path) as fh:
|
with open(path) as fh:
|
||||||
buf = fh.read()
|
buf = fh.read()
|
||||||
return path, buf, x509.load_pem_x509_certificate(buf, default_backend()), \
|
header, _, der_bytes = pem.unarmor(buf)
|
||||||
|
return path, buf, x509.Certificate.load(der_bytes), \
|
||||||
datetime.utcfromtimestamp(os.stat(path).st_ctime)
|
datetime.utcfromtimestamp(os.stat(path).st_ctime)
|
||||||
|
|
||||||
|
|
||||||
@ -85,20 +96,19 @@ def store_request(buf, overwrite=False, address="", user=""):
|
|||||||
if not buf:
|
if not buf:
|
||||||
raise ValueError("No signing request supplied")
|
raise ValueError("No signing request supplied")
|
||||||
|
|
||||||
if isinstance(buf, unicode):
|
if pem.detect(buf):
|
||||||
csr = x509.load_pem_x509_csr(buf.encode("ascii"), backend=default_backend())
|
header, _, der_bytes = pem.unarmor(buf)
|
||||||
elif isinstance(buf, str):
|
csr = CertificationRequest.load(der_bytes)
|
||||||
csr = x509.load_der_x509_csr(buf, backend=default_backend())
|
|
||||||
buf = csr.public_bytes(Encoding.PEM)
|
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid type, expected str for PEM and bytes for DER")
|
csr = CertificationRequest.load(buf)
|
||||||
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
buf = pem_armor_csr(csr)
|
||||||
# TODO: validate common name again
|
|
||||||
|
|
||||||
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")
|
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
|
# 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)
|
fh.write(buf)
|
||||||
os.rename(request_path + ".part", request_path)
|
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",
|
mailer.send("request-stored.md",
|
||||||
attachments=(attach_csr,),
|
attachments=(attach_csr,),
|
||||||
common_name=common_name.value)
|
common_name=common_name)
|
||||||
setxattr(request_path, "user.request.address", address)
|
setxattr(request_path, "user.request.address", address)
|
||||||
setxattr(request_path, "user.request.user", user)
|
setxattr(request_path, "user.request.user", user)
|
||||||
return request_path, csr, common_name.value
|
return request_path, csr, common_name
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def revoke(common_name):
|
def revoke(common_name):
|
||||||
@ -140,9 +136,9 @@ def revoke(common_name):
|
|||||||
Revoke valid certificate
|
Revoke valid certificate
|
||||||
"""
|
"""
|
||||||
signed_path, buf, cert = get_signed(common_name)
|
signed_path, buf, cert = get_signed(common_name)
|
||||||
revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial)
|
revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial_number)
|
||||||
os.rename(signed_path, revoked_path)
|
os.rename(signed_path, revoked_path)
|
||||||
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial))
|
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number))
|
||||||
|
|
||||||
push.publish("certificate-revoked", common_name)
|
push.publish("certificate-revoked", common_name)
|
||||||
|
|
||||||
@ -155,7 +151,7 @@ def revoke(common_name):
|
|||||||
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
|
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
|
||||||
mailer.send("certificate-revoked.md",
|
mailer.send("certificate-revoked.md",
|
||||||
attachments=(attach_cert,),
|
attachments=(attach_cert,),
|
||||||
serial_hex="%x" % cert.serial,
|
serial_hex="%x" % cert.serial_number,
|
||||||
common_name=common_name)
|
common_name=common_name)
|
||||||
return revoked_path
|
return revoked_path
|
||||||
|
|
||||||
@ -186,12 +182,13 @@ def _list_certificates(directory):
|
|||||||
path = os.path.join(directory, filename)
|
path = os.path.join(directory, filename)
|
||||||
with open(path) as fh:
|
with open(path) as fh:
|
||||||
buf = fh.read()
|
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
|
server = False
|
||||||
extension = cert.extensions.get_extension_for_oid(ExtensionOID.EXTENDED_KEY_USAGE)
|
for extension in cert["tbs_certificate"]["extensions"]:
|
||||||
for usage in extension.value:
|
if extension["extn_id"].native == u"extended_key_usage":
|
||||||
if usage == ExtendedKeyUsageOID.SERVER_AUTH: # TODO: IKE intermediate?
|
if u"server_auth" in extension["extn_value"].native:
|
||||||
server = True
|
server = True
|
||||||
yield common_name, path, buf, cert, server
|
yield common_name, path, buf, cert, server
|
||||||
|
|
||||||
def list_signed():
|
def list_signed():
|
||||||
@ -203,10 +200,13 @@ def list_revoked():
|
|||||||
def list_server_names():
|
def list_server_names():
|
||||||
return [cn for cn, path, buf, cert, server in list_signed() if server]
|
return [cn for cn, path, buf, cert, server in list_signed() if server]
|
||||||
|
|
||||||
def export_crl():
|
def export_crl(pem=True):
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
builder = CertificateListBuilder(
|
||||||
sock.connect(const.SIGNER_SOCKET_PATH)
|
config.AUTHORITY_CRL_URL,
|
||||||
sock.send(b"export-crl\n")
|
certificate,
|
||||||
|
1 # TODO: monotonically increasing
|
||||||
|
)
|
||||||
|
|
||||||
for filename in os.listdir(config.REVOKED_DIR):
|
for filename in os.listdir(config.REVOKED_DIR):
|
||||||
if not filename.endswith(".pem"):
|
if not filename.endswith(".pem"):
|
||||||
continue
|
continue
|
||||||
@ -215,9 +215,15 @@ def export_crl():
|
|||||||
revoked_path = os.path.join(config.REVOKED_DIR, filename)
|
revoked_path = os.path.join(config.REVOKED_DIR, filename)
|
||||||
# TODO: Skip expired certificates
|
# TODO: Skip expired certificates
|
||||||
s = os.stat(revoked_path)
|
s = os.stat(revoked_path)
|
||||||
sock.send(("%s:%d\n" % (serial_number, s.st_ctime)).encode("ascii"))
|
builder.add_certificate(
|
||||||
sock.sendall(b"\n")
|
int(filename[:-4], 16),
|
||||||
return sock.recv(32*1024*1024)
|
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):
|
def delete_request(common_name):
|
||||||
@ -236,88 +242,17 @@ def delete_request(common_name):
|
|||||||
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
|
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
|
||||||
headers={"User-Agent": "Certidude API"})
|
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):
|
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")
|
req_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
||||||
with open(req_path) as fh:
|
with open(req_path) as fh:
|
||||||
csr_buf = fh.read()
|
csr_buf = fh.read()
|
||||||
csr = x509.load_pem_x509_csr(csr_buf, backend=default_backend())
|
header, _, der_bytes = pem.unarmor(csr_buf)
|
||||||
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
csr = CertificationRequest.load(der_bytes)
|
||||||
|
|
||||||
|
|
||||||
# Sign with function below
|
# Sign with function below
|
||||||
cert, buf = _sign(csr, csr_buf, overwrite)
|
cert, buf = _sign(csr, csr_buf, overwrite)
|
||||||
@ -326,15 +261,17 @@ def sign(common_name, overwrite=False):
|
|||||||
return cert, buf
|
return cert, buf
|
||||||
|
|
||||||
def _sign(csr, buf, overwrite=False):
|
def _sign(csr, buf, overwrite=False):
|
||||||
assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n")
|
# TODO: CRLDistributionPoints, OCSP URL, Certificate URL
|
||||||
assert isinstance(csr, x509.CertificateSigningRequest)
|
|
||||||
|
|
||||||
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n")
|
||||||
cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value)
|
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
|
renew = False
|
||||||
|
|
||||||
attachments = [
|
attachments = [
|
||||||
(buf, "application/x-pem-file", common_name.value + ".csr"),
|
(buf, "application/x-pem-file", common_name + ".csr"),
|
||||||
]
|
]
|
||||||
|
|
||||||
revoked_path = None
|
revoked_path = None
|
||||||
@ -344,13 +281,18 @@ def _sign(csr, buf, overwrite=False):
|
|||||||
if os.path.exists(cert_path):
|
if os.path.exists(cert_path):
|
||||||
with open(cert_path) as fh:
|
with open(cert_path) as fh:
|
||||||
prev_buf = fh.read()
|
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?
|
# 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:
|
if overwrite:
|
||||||
# TODO: is this the best approach?
|
# 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)
|
revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex)
|
||||||
os.rename(cert_path, revoked_path)
|
os.rename(cert_path, revoked_path)
|
||||||
attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")]
|
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")
|
raise EnvironmentError("Will not overwrite existing certificate")
|
||||||
|
|
||||||
# Sign via signer process
|
# Sign via signer process
|
||||||
cert_buf = signer_exec("sign-request", buf)
|
builder = CertificateBuilder({u'common_name': common_name }, csr_pubkey)
|
||||||
cert = x509.load_pem_x509_certificate(cert_buf, default_backend())
|
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:
|
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)
|
os.rename(cert_path + ".part", cert_path)
|
||||||
attachments.append((cert_buf, "application/x-pem-file", common_name.value + ".crt"))
|
attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt"))
|
||||||
cert_serial_hex = "%x" % cert.serial
|
cert_serial_hex = "%x" % end_entity_cert.serial_number
|
||||||
|
|
||||||
# Create symlink
|
# Create symlink
|
||||||
os.symlink(
|
link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % end_entity_cert.serial_number)
|
||||||
"../%s.pem" % common_name.value,
|
assert not os.path.exists(link_name), "Certificate with same serial number already exists: %s" % link_name
|
||||||
os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial))
|
os.symlink("../%s.pem" % common_name, link_name)
|
||||||
|
|
||||||
# Copy filesystem attributes to newly signed certificate
|
# Copy filesystem attributes to newly signed certificate
|
||||||
if revoked_path:
|
if revoked_path:
|
||||||
@ -387,8 +351,8 @@ def _sign(csr, buf, overwrite=False):
|
|||||||
|
|
||||||
url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest()
|
url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest()
|
||||||
click.echo("Publishing certificate at %s ..." % url)
|
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"})
|
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
|
||||||
|
|
||||||
push.publish("request-signed", common_name.value)
|
push.publish("request-signed", common_name)
|
||||||
return cert, cert_buf
|
return end_entity_cert, end_entity_cert_buf
|
||||||
|
128
certidude/cli.py
128
certidude/cli.py
@ -12,6 +12,7 @@ import socket
|
|||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from asn1crypto.util import timezone
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from configparser import ConfigParser, NoOptionError, NoSectionError
|
from configparser import ConfigParser, NoOptionError, NoSectionError
|
||||||
from certidude.common import ip_address, ip_network, apt, rpm, pip, drop_privileges, selinux_fixup
|
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
|
# keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_client_config.html
|
||||||
# strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA
|
# strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA
|
||||||
|
|
||||||
# Parse command-line argument defaults from environment
|
NOW = datetime.utcnow()
|
||||||
|
|
||||||
NOW = datetime.utcnow().replace(tzinfo=None)
|
|
||||||
|
|
||||||
def fqdn_required(func):
|
def fqdn_required(func):
|
||||||
def wrapped(**args):
|
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:
|
with open(certificate_path, "rb") as ch, open(request_path, "rb") as rh, open(key_path, "rb") as kh:
|
||||||
cert_buf = ch.read()
|
cert_buf = ch.read()
|
||||||
cert = asymmetric.load_certificate(cert_buf)
|
cert = asymmetric.load_certificate(cert_buf)
|
||||||
expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native
|
expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
|
||||||
if renewal_overlap and datetime.now() > expires - timedelta(days=renewal_overlap):
|
if renewal_overlap and NOW > expires - timedelta(days=renewal_overlap):
|
||||||
click.echo("Certificate will expire %s, will attempt to renew" % expires)
|
click.echo("Certificate will expire %s, will attempt to renew" % expires)
|
||||||
renew = True
|
renew = True
|
||||||
headers["X-Renewal-Signature"] = b64encode(
|
headers["X-Renewal-Signature"] = b64encode(
|
||||||
@ -931,15 +930,13 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
|
|||||||
@fqdn_required
|
@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):
|
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
|
# Install only rarely changing stuff from OS package management
|
||||||
apt("python-setproctitle cython python-dev libkrb5-dev libffi-dev libssl-dev")
|
apt("cython python-dev python-mimeparse python-markdown python-xattr python-jinja2 python-cffi python-ldap software-properties-common libsasl2-modules-gssapi-mit")
|
||||||
apt("python-mimeparse python-markdown python-xattr python-jinja2 python-cffi")
|
pip("gssapi falcon humanize ipaddress simplepam humanize requests")
|
||||||
apt("python-ldap software-properties-common libsasl2-modules-gssapi-mit")
|
|
||||||
pip("gssapi falcon humanize ipaddress simplepam humanize requests pyopenssl")
|
|
||||||
click.echo("Software dependencies installed")
|
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"):
|
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")
|
os.system("apt-get install -y libnginx-mod-nchan")
|
||||||
if not os.path.exists("/usr/sbin/nginx"):
|
if not os.path.exists("/usr/sbin/nginx"):
|
||||||
os.system("apt-get install -y 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(
|
builder.serial_number = random.randint(
|
||||||
0x100000000000000000000000000000000000000,
|
0x100000000000000000000000000000000000000,
|
||||||
0xfffffffffffffffffffffffffffffffffffffff)
|
0xfffffffffffffffffffffffffffffffffffffff)
|
||||||
now = datetime.utcnow()
|
|
||||||
builder.begin_date = now - timedelta(minutes=5)
|
builder.begin_date = NOW - timedelta(minutes=5)
|
||||||
builder.end_date = now + timedelta(days=authority_lifetime)
|
builder.end_date = NOW + timedelta(days=authority_lifetime)
|
||||||
|
|
||||||
if server_flags:
|
if server_flags:
|
||||||
builder.key_usage(set(['digital_signature', 'key_encipherment', 'key_cert_sign', 'crl_sign']))
|
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.extended_key_usage = set(['server_auth', "1.3.6.1.5.5.8.2.2"])
|
||||||
|
|
||||||
certificate = builder.build(private_key)
|
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("sha1sum: %s" % hashlib.sha1(buf).hexdigest())
|
||||||
click.echo("sha256sum: %s" % hashlib.sha256(buf).hexdigest())
|
click.echo("sha256sum: %s" % hashlib.sha256(buf).hexdigest())
|
||||||
click.echo()
|
click.echo()
|
||||||
for ext in cert.extensions:
|
|
||||||
print " -", ext.value
|
|
||||||
click.echo()
|
|
||||||
|
|
||||||
if not hide_requests:
|
if not hide_requests:
|
||||||
for common_name, path, buf, csr, server in authority.list_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:
|
if not verbose:
|
||||||
click.echo("s " + path)
|
click.echo("s " + path)
|
||||||
continue
|
continue
|
||||||
|
click.echo()
|
||||||
click.echo(click.style(common_name, fg="blue"))
|
click.echo(click.style(common_name, fg="blue"))
|
||||||
click.echo("=" * len(common_name))
|
click.echo("=" * len(common_name))
|
||||||
click.echo("State: ? " + click.style("submitted", fg="yellow") + " " + naturaltime(created) + click.style(", %s" %created, fg="white"))
|
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:
|
if show_signed:
|
||||||
for common_name, path, buf, cert, server in authority.list_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 not verbose:
|
||||||
if cert.not_valid_before < NOW and cert.not_valid_after > NOW:
|
if signed < NOW and NOW < expires:
|
||||||
click.echo("v " + path)
|
click.echo("v " + path)
|
||||||
elif NOW > cert.not_valid_after:
|
elif expires < NOW:
|
||||||
click.echo("e " + path)
|
click.echo("e " + path)
|
||||||
else:
|
else:
|
||||||
click.echo("y " + path)
|
click.echo("y " + path)
|
||||||
continue
|
continue
|
||||||
|
click.echo()
|
||||||
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial, fg="white"))
|
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white"))
|
||||||
click.echo("="*(len(common_name)+60))
|
click.echo("="*(len(common_name)+60))
|
||||||
expires = 0 # TODO
|
|
||||||
if cert.not_valid_before < NOW and cert.not_valid_after > NOW:
|
if signed < NOW and NOW < expires:
|
||||||
click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(cert.not_valid_after) + click.style(", %s" % cert.not_valid_after, fg="white"))
|
click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(expires) + click.style(", %s" % expires, fg="white"))
|
||||||
elif NOW > cert.not_valid_after:
|
elif NOW > expires:
|
||||||
click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" %expires, fg="white"))
|
click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" % expires, fg="white"))
|
||||||
else:
|
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()
|
||||||
click.echo("openssl x509 -in %s -text -noout" % path)
|
click.echo("openssl x509 -in %s -text -noout" % path)
|
||||||
dump_common(common_name, path, cert)
|
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:
|
if show_revoked:
|
||||||
for common_name, path, buf, cert, server in authority.list_revoked():
|
for common_name, path, buf, cert, server in authority.list_revoked():
|
||||||
if not verbose:
|
if not verbose:
|
||||||
click.echo("r " + path)
|
click.echo("r " + path)
|
||||||
continue
|
continue
|
||||||
|
click.echo()
|
||||||
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial, fg="white"))
|
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white"))
|
||||||
click.echo("="*(len(common_name)+60))
|
click.echo("="*(len(common_name)+60))
|
||||||
|
|
||||||
_, _, _, _, _, _, _, _, mtime, _ = os.stat(path)
|
_, _, _, _, _, _, _, _, 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("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)
|
click.echo("openssl x509 -in %s -text -noout" % path)
|
||||||
dump_common(common_name, path, cert)
|
dump_common(common_name, path, cert)
|
||||||
|
for ext in cert["tbs_certificate"]["extensions"]:
|
||||||
click.echo()
|
print " - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native))
|
||||||
|
|
||||||
|
|
||||||
@click.command("sign", help="Sign certificate")
|
@click.command("sign", help="Sign certificate")
|
||||||
@click.argument("common_name")
|
@click.argument("common_name")
|
||||||
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
|
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
|
||||||
def certidude_sign(common_name, overwrite):
|
def certidude_sign(common_name, overwrite):
|
||||||
drop_privileges()
|
|
||||||
from certidude import authority
|
from certidude import authority
|
||||||
|
drop_privileges()
|
||||||
cert = authority.sign(common_name, overwrite)
|
cert = authority.sign(common_name, overwrite)
|
||||||
|
|
||||||
|
|
||||||
@click.command("revoke", help="Revoke certificate")
|
@click.command("revoke", help="Revoke certificate")
|
||||||
@click.argument("common_name")
|
@click.argument("common_name")
|
||||||
def certidude_revoke(common_name):
|
def certidude_revoke(common_name):
|
||||||
drop_privileges()
|
|
||||||
from certidude import authority
|
from certidude import authority
|
||||||
|
drop_privileges()
|
||||||
authority.revoke(common_name)
|
authority.revoke(common_name)
|
||||||
|
|
||||||
|
|
||||||
@ -1242,10 +1241,10 @@ def certidude_revoke(common_name):
|
|||||||
def certidude_cron():
|
def certidude_cron():
|
||||||
import itertools
|
import itertools
|
||||||
from certidude import authority, config
|
from certidude import authority, config
|
||||||
now = datetime.now()
|
|
||||||
for cn, path, buf, cert, server in itertools.chain(authority.list_signed(), authority.list_revoked()):
|
for cn, path, buf, cert, server in itertools.chain(authority.list_signed(), authority.list_revoked()):
|
||||||
if cert.not_valid_after < now:
|
expires = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
|
||||||
expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial)
|
if expires < NOW:
|
||||||
|
expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial_number)
|
||||||
assert not os.path.exists(expired_path)
|
assert not os.path.exists(expired_path)
|
||||||
os.rename(path, expired_path)
|
os.rename(path, expired_path)
|
||||||
click.echo("Moved %s to %s" % (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):
|
def certidude_serve(port, listen, fork):
|
||||||
import pwd
|
import pwd
|
||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
from certidude.signer import SignServer
|
|
||||||
from certidude import authority, const, push
|
from certidude import authority, const, push
|
||||||
|
|
||||||
if port == 80:
|
if port == 80:
|
||||||
@ -1272,7 +1270,7 @@ def certidude_serve(port, listen, fork):
|
|||||||
|
|
||||||
# Rebuild reverse mapping
|
# Rebuild reverse mapping
|
||||||
for cn, path, buf, cert, server in authority.list_signed():
|
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):
|
if not os.path.exists(by_serial):
|
||||||
click.echo("Linking %s to ../%s.pem" % (by_serial, cn))
|
click.echo("Linking %s to ../%s.pem" % (by_serial, cn))
|
||||||
os.symlink("../%s.pem" % cn, by_serial)
|
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"))
|
rh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
|
||||||
log_handlers.append(rh)
|
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" %
|
click.echo("Users subnets: %s" %
|
||||||
", ".join([str(j) for j in config.USER_SUBNETS]))
|
", ".join([str(j) for j in config.USER_SUBNETS]))
|
||||||
click.echo("Administrative subnets: %s" %
|
click.echo("Administrative subnets: %s" %
|
||||||
@ -1392,7 +1341,6 @@ def certidude_serve(port, listen, fork):
|
|||||||
def cleanup_handler(*args):
|
def cleanup_handler(*args):
|
||||||
push.publish("server-stopped")
|
push.publish("server-stopped")
|
||||||
logger.debug(u"Shutting down Certidude")
|
logger.debug(u"Shutting down Certidude")
|
||||||
assert authority.signer_exec("exit") == "ok"
|
|
||||||
sys.exit(0) # TODO: use another code, needs test refactor
|
sys.exit(0) # TODO: use another code, needs test refactor
|
||||||
|
|
||||||
import signal
|
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")
|
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
|
||||||
SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid")
|
SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid")
|
||||||
SERVER_LOG_PATH = "/var/log/certidude-server.log"
|
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/"
|
STORAGE_PATH = "/var/lib/certidude/"
|
||||||
|
|
||||||
try:
|
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">
|
<li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable">
|
||||||
|
|
||||||
<a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a>
|
<a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a>
|
||||||
<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>
|
<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 }}.
|
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
|
The new certificate is valid from {{ builder.begin_date }} until
|
||||||
{{ cert.not_valid_after }}.
|
{{ builder.end_date }}.
|
||||||
|
|
||||||
Services making use of those certificates should continue working as expected.
|
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 }}
|
with serial number {{ cert_serial_hex }}
|
||||||
was signed{% if signer %} by {{ signer }}{% endif %}.
|
was signed{% if signer %} by {{ signer }}{% endif %}.
|
||||||
|
|
||||||
The certificate is valid from {{ cert.not_valid_before }} until
|
The certificate is valid from {{ builder.begin_date }} until
|
||||||
{{ cert.not_valid_after }}.
|
{{ builder.end_date }}.
|
||||||
|
|
||||||
{% if overwritten %}
|
{% if overwritten %}
|
||||||
By doing so existing certificate with the same common name
|
By doing so existing certificate with the same common name
|
||||||
|
@ -1,69 +1,136 @@
|
|||||||
# To set up SSL certificates using Let's Encrypt run:
|
# 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
|
# 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_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
|
||||||
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
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 {
|
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 }};
|
server_name {{ common_name }};
|
||||||
listen 80 default_server;
|
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/ {
|
location /api/ {
|
||||||
proxy_pass http://127.0.1.1:8080/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;
|
limit_req zone=api burst=5;
|
||||||
}
|
}
|
||||||
|
|
||||||
# This is for Let's Encrypt
|
# Path to static files
|
||||||
location /.well-known/ {
|
root {{static_path}};
|
||||||
alias /var/www/html/.well-known/;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Rewrite /cgi-bin/pkiclient.exe to /api/scep for SCEP protocol
|
# Rewrite /cgi-bin/pkiclient.exe to /api/scep for SCEP protocol
|
||||||
location /cgi-bin/pkiclient.exe {
|
location /cgi-bin/pkiclient.exe {
|
||||||
rewrite /cgi-bin/pkiclient.exe /api/scep/ last;
|
rewrite /cgi-bin/pkiclient.exe /api/scep/ last;
|
||||||
}
|
}
|
||||||
|
|
||||||
{% if not push_server %}
|
# Long poll for CSR submission
|
||||||
# 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/
|
|
||||||
|
|
||||||
location ~ "^/lp/sub/(.*)" {
|
location ~ "^/lp/sub/(.*)" {
|
||||||
nchan_channel_id $1;
|
nchan_channel_id $1;
|
||||||
nchan_subscriber longpoll;
|
nchan_subscriber longpoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Comment everything below in this server definition if you're using HTTPS
|
||||||
|
|
||||||
|
# Event source for web interface
|
||||||
location ~ "^/ev/sub/(.*)" {
|
location ~ "^/ev/sub/(.*)" {
|
||||||
nchan_channel_id $1;
|
nchan_channel_id $1;
|
||||||
nchan_subscriber eventsource;
|
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 %}
|
{% if not push_server %}
|
||||||
@ -75,13 +142,11 @@ server {
|
|||||||
location ~ "^/lp/pub/(.*)" {
|
location ~ "^/lp/pub/(.*)" {
|
||||||
nchan_publisher;
|
nchan_publisher;
|
||||||
nchan_channel_id $1;
|
nchan_channel_id $1;
|
||||||
nchan_message_buffer_length 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ "^/ev/pub/(.*)" {
|
location ~ "^/ev/pub/(.*)" {
|
||||||
nchan_publisher;
|
nchan_publisher;
|
||||||
nchan_channel_id $1;
|
nchan_channel_id $1;
|
||||||
nchan_message_buffer_length 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
click>=6.7
|
click>=6.7
|
||||||
configparser>=3.5.0
|
configparser>=3.5.0
|
||||||
certbuilder
|
certbuilder
|
||||||
|
crlbuilder
|
||||||
|
oscrypto
|
||||||
|
@ -80,12 +80,6 @@ def clean_client():
|
|||||||
|
|
||||||
|
|
||||||
def clean_server():
|
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"):
|
if os.path.exists("/run/certidude/server.pid"):
|
||||||
with open("/run/certidude/server.pid") as fh:
|
with open("/run/certidude/server.pid") as fh:
|
||||||
try:
|
try:
|
||||||
@ -239,16 +233,18 @@ def test_cli_setup_authority():
|
|||||||
os.setgid(0) # Restore GID
|
os.setgid(0) # Restore GID
|
||||||
os.umask(0022)
|
os.umask(0022)
|
||||||
|
|
||||||
|
# Make sure nginx is running
|
||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert os.getuid() == 0 and os.getgid() == 0, "Serve dropped permissions incorrectly!"
|
assert os.getuid() == 0 and os.getgid() == 0, "Serve dropped permissions incorrectly!"
|
||||||
assert os.system("nginx -t") == 0, "invalid nginx configuration"
|
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"
|
assert os.path.exists("/run/nginx.pid"), "nginx wasn't started up properly"
|
||||||
|
|
||||||
from certidude import config, authority, auth, user
|
from certidude import config, authority, auth, user
|
||||||
assert authority.ca_cert.serial_number >= 0x100000000000000000000000000000000000000
|
assert authority.certificate.serial_number >= 0x100000000000000000000000000000000000000
|
||||||
assert authority.ca_cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff
|
assert authority.certificate.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff
|
||||||
assert authority.ca_cert.not_valid_before < datetime.now()
|
assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
|
||||||
assert authority.ca_cert.not_valid_after > datetime.now() + timedelta(days=7000)
|
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("lauri@fedora-123") == False
|
||||||
assert authority.server_flags("fedora-123") == False
|
assert authority.server_flags("fedora-123") == False
|
||||||
assert authority.server_flags("vpn.example.lan") == True
|
assert authority.server_flags("vpn.example.lan") == True
|
||||||
@ -412,12 +408,12 @@ def test_cli_setup_authority():
|
|||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
|
|
||||||
# Test sign API call
|
# 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
|
assert r.status_code == 401, r.text
|
||||||
r = client().simulate_patch("/api/request/test/",
|
r = client().simulate_post("/api/request/test/",
|
||||||
headers={"Authorization":usertoken})
|
headers={"Authorization":usertoken})
|
||||||
assert r.status_code == 403, r.text
|
assert r.status_code == 403, r.text
|
||||||
r = client().simulate_patch("/api/request/test/",
|
r = client().simulate_post("/api/request/test/",
|
||||||
headers={"Authorization":admintoken})
|
headers={"Authorization":admintoken})
|
||||||
assert r.status_code == 201, r.text
|
assert r.status_code == 201, r.text
|
||||||
assert "Signed " in inbox.pop(), inbox
|
assert "Signed " in inbox.pop(), inbox
|
||||||
@ -476,7 +472,7 @@ def test_cli_setup_authority():
|
|||||||
# Test revocations API call
|
# Test revocations API call
|
||||||
r = client().simulate_get("/api/revoked/",
|
r = client().simulate_get("/api/revoked/",
|
||||||
headers={"Accept":"application/x-pem-file"})
|
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"
|
assert r.headers.get('content-type') == "application/x-pem-file"
|
||||||
|
|
||||||
r = client().simulate_get("/api/revoked/")
|
r = client().simulate_get("/api/revoked/")
|
||||||
@ -672,29 +668,11 @@ def test_cli_setup_authority():
|
|||||||
assert "/ev/sub/" in r.text, r.text
|
assert "/ev/sub/" in r.text, r.text
|
||||||
assert r.json, r.text
|
assert r.json, r.text
|
||||||
assert r.json.get("authority"), r.text
|
assert r.json.get("authority"), r.text
|
||||||
assert r.json.get("authority").get("events"), r.text
|
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
|
||||||
### Subscribe to event source ###
|
assert ev_url.startswith("http://ca.example.lan/ev/sub/")
|
||||||
#################################
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
@ -704,6 +682,7 @@ def test_cli_setup_authority():
|
|||||||
r = client().simulate_post("/api/token/")
|
r = client().simulate_post("/api/token/")
|
||||||
assert r.status_code == 404, r.text
|
assert r.status_code == 404, r.text
|
||||||
|
|
||||||
|
"""
|
||||||
config.BUNDLE_FORMAT = "ovpn"
|
config.BUNDLE_FORMAT = "ovpn"
|
||||||
config.USER_ENROLLMENT_ALLOWED = True
|
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.status_code == 200 # token consumed by anyone on unknown device
|
||||||
assert r2.headers.get('content-type') == "application/x-pkcs12"
|
assert r2.headers.get('content-type') == "application/x-pkcs12"
|
||||||
assert "Signed " in inbox.pop(), inbox
|
assert "Signed " in inbox.pop(), inbox
|
||||||
|
"""
|
||||||
|
|
||||||
# Beyond this point don't use client()
|
# Beyond this point don't use client()
|
||||||
const.STORAGE_PATH = "/tmp/"
|
const.STORAGE_PATH = "/tmp/"
|
||||||
@ -765,12 +745,13 @@ def test_cli_setup_authority():
|
|||||||
result = runner.invoke(cli, ["request", "--no-wait"])
|
result = runner.invoke(cli, ["request", "--no-wait"])
|
||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), 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()
|
child_pid = os.fork()
|
||||||
if not child_pid:
|
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 not result.exception, result.output
|
||||||
|
assert "Publishing request-signed event 'www.example.lan' on http://localhost/ev/pub/" in result.output, result.output
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
os.waitpid(child_pid, 0)
|
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 not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||||
#assert "Writing certificate to:" in result.output, result.output
|
#assert "Writing certificate to:" in result.output, result.output
|
||||||
assert "Attached renewal signature" 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
|
# Test nginx setup
|
||||||
assert os.system("nginx -t") == 0, "Generated nginx config was invalid"
|
assert os.system("nginx -t") == 0, "Generated nginx config was invalid"
|
||||||
|
|
||||||
# TODO: test client verification with curl
|
|
||||||
|
|
||||||
|
|
||||||
###############
|
###############
|
||||||
### OpenVPN ###
|
### OpenVPN ###
|
||||||
@ -818,13 +796,17 @@ def test_cli_setup_authority():
|
|||||||
|
|
||||||
result = runner.invoke(cli, ["request", "--no-wait"])
|
result = runner.invoke(cli, ["request", "--no-wait"])
|
||||||
assert not result.exception, result.output
|
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("/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()
|
child_pid = os.fork()
|
||||||
if not child_pid:
|
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 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
|
return
|
||||||
else:
|
else:
|
||||||
os.waitpid(child_pid, 0)
|
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: Check that tunnel interfaces came up, perhaps try to ping?
|
||||||
# TODO: assert key, req, cert paths were included correctly in OpenVPN config
|
# 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 ###
|
### IPSec ###
|
||||||
###############
|
#############
|
||||||
|
|
||||||
|
# Setup gateway
|
||||||
|
|
||||||
clean_client()
|
clean_client()
|
||||||
|
|
||||||
@ -882,11 +1018,15 @@ def test_cli_setup_authority():
|
|||||||
result = runner.invoke(cli, ["request", "--no-wait"])
|
result = runner.invoke(cli, ["request", "--no-wait"])
|
||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), 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()
|
child_pid = os.fork()
|
||||||
if not child_pid:
|
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 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
|
return
|
||||||
else:
|
else:
|
||||||
os.waitpid(child_pid, 0)
|
os.waitpid(child_pid, 0)
|
||||||
@ -898,7 +1038,8 @@ def test_cli_setup_authority():
|
|||||||
assert "Writing certificate to:" in result.output, result.output
|
assert "Writing certificate to:" in result.output, result.output
|
||||||
assert os.path.exists("/tmp/ca.example.lan/server_cert.pem")
|
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/client.conf")
|
||||||
os.unlink("/etc/certidude/services.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
|
assert "Writing certificate to:" in result.output, result.output
|
||||||
|
|
||||||
|
# IPSec using NetworkManager
|
||||||
######################
|
|
||||||
### 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
|
|
||||||
|
|
||||||
clean_client()
|
clean_client()
|
||||||
|
|
||||||
@ -1158,11 +1283,15 @@ def test_cli_setup_authority():
|
|||||||
##################
|
##################
|
||||||
|
|
||||||
os.umask(0022)
|
os.umask(0022)
|
||||||
assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep")
|
if not os.path.exists("/tmp/sscep"):
|
||||||
assert not os.system("cd /tmp/sscep && ./Configure && make sscep_dyn")
|
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("/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")
|
if not os.path.exists("/tmp/key.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("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")
|
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
|
# TODO: test e-mails at this point
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user