1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 16:25:17 +00:00

Several updates

* Subnets configuration option for Kerberos machine enrollment
* Configurable script snippets via [service] configuration section
* Preliminary revocation reason support
* Improved signature profile support
* Add domain components to DN to distinguish certificate CN's namespace
* Image builder improvements, add Elliptic Curve support
* Added GetCACaps operation and more digest algorithms for SCEP
* Generate certificate and CRL serial from timestamp (64+32bits) and random bytes (56bits)
* Move client storage pool to /etc/certidude/authority/
* Cleanups & bugfixes
This commit is contained in:
Lauri Võsandi 2018-04-27 07:48:15 +00:00
parent 94e5f72566
commit 5e9251f365
35 changed files with 1192 additions and 580 deletions

4
.gitignore vendored
View File

@ -64,3 +64,7 @@ node_modules/
# Ignore patch # Ignore patch
*.orig *.orig
*.rej *.rej
lextab.py
yacctab.py
.pytest_cache

View File

@ -8,6 +8,7 @@ import hashlib
from datetime import datetime from datetime import datetime
from xattr import listxattr, getxattr from xattr import listxattr, getxattr
from certidude.auth import login_required from certidude.auth import login_required
from certidude.common import cert_to_dn
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 certidude import const, config, authority from certidude import const, config, authority
@ -54,7 +55,7 @@ class SessionResource(AuthorityHandler):
) )
def serialize_revoked(g): def serialize_revoked(g):
for common_name, path, buf, cert, signed, expired, revoked in g(): for common_name, path, buf, cert, signed, expired, revoked, reason in g():
yield dict( yield dict(
serial = "%x" % cert.serial_number, serial = "%x" % cert.serial_number,
common_name = common_name, common_name = common_name,
@ -62,6 +63,7 @@ class SessionResource(AuthorityHandler):
signed = signed, signed = signed,
expired = expired, expired = expired,
revoked = revoked, revoked = revoked,
reason = reason,
sha256sum = hashlib.sha256(buf).hexdigest()) sha256sum = hashlib.sha256(buf).hexdigest())
def serialize_certificates(g): def serialize_certificates(g):
@ -69,7 +71,7 @@ class SessionResource(AuthorityHandler):
# Extract certificate tags from filesystem # Extract certificate tags from filesystem
try: try:
tags = [] tags = []
for tag in getxattr(path, "user.xdg.tags").decode("ascii").split(","): for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","):
if "=" in tag: if "=" in tag:
k, v = tag.split("=", 1) k, v = tag.split("=", 1)
else: else:
@ -116,7 +118,7 @@ class SessionResource(AuthorityHandler):
extensions = dict([ extensions = dict([
(e["extn_id"].native, e["extn_value"].native) (e["extn_id"].native, e["extn_value"].native)
for e in cert["tbs_certificate"]["extensions"] for e in cert["tbs_certificate"]["extensions"]
if e["extn_value"] in ("extended_key_usage",)]) if e["extn_id"].native in ("extended_key_usage",)])
) )
if req.context.get("user").is_admin(): if req.context.get("user").is_admin():
@ -131,6 +133,11 @@ class SessionResource(AuthorityHandler):
mail=req.context.get("user").mail mail=req.context.get("user").mail
), ),
request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED,
service = dict(
protocols = config.SERVICE_PROTOCOLS,
routers = [j[0] for j in authority.list_signed(
common_name=config.SERVICE_ROUTERS)]
),
authority = dict( authority = dict(
builder = dict( builder = dict(
profiles = config.IMAGE_BUILDER_PROFILES profiles = config.IMAGE_BUILDER_PROFILES
@ -143,13 +150,15 @@ class SessionResource(AuthorityHandler):
certificate = dict( certificate = dict(
algorithm = authority.public_key.algorithm, algorithm = authority.public_key.algorithm,
common_name = self.authority.certificate.subject.native["common_name"], common_name = self.authority.certificate.subject.native["common_name"],
distinguished_name = cert_to_dn(self.authority.certificate),
md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(),
blob = self.authority.certificate_buf.decode("ascii"), blob = self.authority.certificate_buf.decode("ascii"),
), ),
mailer = dict( mailer = dict(
name = config.MAILER_NAME, name = config.MAILER_NAME,
address = config.MAILER_ADDRESS address = config.MAILER_ADDRESS
) if config.MAILER_ADDRESS else None, ) if config.MAILER_ADDRESS else None,
machine_enrollment_allowed=config.MACHINE_ENROLLMENT_ALLOWED, machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS,
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED, user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES, user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN, events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
@ -278,8 +287,8 @@ def certidude_app(log_handlers=[]):
log_handlers.append(LogHandler(uri)) log_handlers.append(LogHandler(uri))
app.add_route("/api/log/", LogResource(uri)) app.add_route("/api/log/", LogResource(uri))
elif config.LOGGING_BACKEND == "syslog": elif config.LOGGING_BACKEND == "syslog":
from logging.handlers import SyslogHandler from logging.handlers import SysLogHandler
log_handlers.append(SyslogHandler()) log_handlers.append(SysLogHandler())
# Browsing syslog via HTTP is obviously not possible out of the box # Browsing syslog via HTTP is obviously not possible out of the box
elif config.LOGGING_BACKEND: elif config.LOGGING_BACKEND:
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND) raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)

View File

@ -26,14 +26,15 @@ class AttributeResource(object):
Results made available only to lease IP address. Results made available only to lease IP address.
""" """
try: try:
path, buf, cert, attribs = self.authority.get_attributes(cn, namespace=self.namespace) path, buf, cert, attribs = self.authority.get_attributes(cn,
namespace=self.namespace, flat=True)
except IOError: except IOError:
raise falcon.HTTPNotFound() raise falcon.HTTPNotFound()
else: else:
return attribs return attribs
@csrf_protection @csrf_protection
@whitelist_subject # TODO: sign instead @whitelist_subject
def on_post(self, req, resp, cn): def on_post(self, req, resp, cn):
namespace = ("user.%s." % self.namespace).encode("ascii") namespace = ("user.%s." % self.namespace).encode("ascii")
try: try:
@ -43,7 +44,7 @@ class AttributeResource(object):
else: else:
for key in req.params: for key in req.params:
if not re.match("[a-z0-9_\.]+$", key): if not re.match("[a-z0-9_\.]+$", key):
raise falcon.HTTPBadRequest("Invalid key") raise falcon.HTTPBadRequest("Invalid key %s" % key)
valid = set() valid = set()
for key, value in req.params.items(): for key, value in req.params.items():
identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii") identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii")

View File

@ -4,8 +4,9 @@ import falcon
import logging import logging
import os import os
import subprocess import subprocess
from certidude import config, const from certidude import config, const, authority
from certidude.auth import login_required, authorize_admin from certidude.auth import login_required, authorize_admin
from certidude.common import cert_to_dn
from jinja2 import Template from jinja2 import Template
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -14,6 +15,8 @@ class ImageBuilderResource(object):
@login_required @login_required
@authorize_admin @authorize_admin
def on_get(self, req, resp, profile, suggested_filename): def on_get(self, req, resp, profile, suggested_filename):
router = [j[0] for j in authority.list_signed(
common_name=config.cp2.get(profile, "router"))][0]
model = config.cp2.get(profile, "model") model = config.cp2.get(profile, "model")
build_script_path = config.cp2.get(profile, "command") build_script_path = config.cp2.get(profile, "command")
overlay_path = config.cp2.get(profile, "overlay") overlay_path = config.cp2.get(profile, "overlay")
@ -35,7 +38,10 @@ class ImageBuilderResource(object):
stdout=open(log_path, "w"), stderr=subprocess.STDOUT, stdout=open(log_path, "w"), stderr=subprocess.STDOUT,
close_fds=True, shell=False, close_fds=True, shell=False,
cwd=os.path.dirname(os.path.realpath(build_script_path)), cwd=os.path.dirname(os.path.realpath(build_script_path)),
env={"PROFILE":model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin", env={"PROFILE": model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin",
"ROUTER": router,
"AUTHORITY_CERTIFICATE_ALGORITHM": authority.public_key.algorithm,
"AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME": cert_to_dn(authority.certificate),
"BUILD":build, "OVERLAY":build + "/overlay/"}, "BUILD":build, "OVERLAY":build + "/overlay/"},
startupinfo=None, creationflags=0) startupinfo=None, creationflags=0)
proc.communicate() proc.communicate()

View File

@ -1,6 +1,7 @@
import falcon import falcon
import logging import logging
import os import os
import re
import xattr import xattr
from datetime import datetime from datetime import datetime
from certidude import config, push from certidude import config, push
@ -32,10 +33,9 @@ class LeaseResource(AuthorityHandler):
@authorize_server @authorize_server
def on_post(self, req, resp): def on_post(self, req, resp):
client_common_name = req.get_param("client", required=True) client_common_name = req.get_param("client", required=True)
if "=" in client_common_name: # It's actually DN, resolve it to CN m = re.match("CN=(.+?),", client_common_name) # It's actually DN, resolve it to CN
_, client_common_name = client_common_name.split(" CN=", 1) if m:
if "," in client_common_name: client_common_name, = m.groups()
client_common_name, _ = client_common_name.split(",", 1)
path, buf, cert, signed, expires = self.authority.get_signed(client_common_name) # TODO: catch exceptions path, buf, cert, signed, expires = self.authority.get_signed(client_common_name) # TODO: catch exceptions
if req.get_param("serial") and cert.serial_number != 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

View File

@ -53,24 +53,29 @@ class OCSPResource(AuthorityHandler):
assert serial > 0, "Serial number correctness check failed" assert serial > 0, "Serial number correctness check failed"
try: try:
link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % serial)) link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % serial))
assert link_target.startswith("../") assert link_target.startswith("../")
assert link_target.endswith(".pem") assert link_target.endswith(".pem")
path, buf, cert, signed, expires = self.authority.get_signed(link_target[3:-4]) path, buf, cert, signed, expires = self.authority.get_signed(link_target[3:-4])
if serial != cert.serial_number: if serial != cert.serial_number:
logger.error("Certificate store integrity check failed, %s refers to certificate with serial %x" % (link_target, cert.serial_number)) logger.error("Certificate store integrity check failed, %s refers to certificate with serial %040x", link_target, cert.serial_number)
raise EnvironmentError("Integrity check failed") raise EnvironmentError("Integrity check failed")
logger.debug("OCSP responder queried from %s for %s with serial %040x, returned status 'good'",
req.context.get("remote_addr"), cert.subject.native["common_name"], serial)
status = ocsp.CertStatus(name='good', value=None) status = ocsp.CertStatus(name='good', value=None)
except EnvironmentError: except EnvironmentError:
try: try:
path, buf, cert, signed, expires, revoked = self.authority.get_revoked(serial) path, buf, cert, signed, expires, revoked, reason = self.authority.get_revoked(serial)
logger.debug("OCSP responder queried from %s for %s with serial %040x, returned status 'revoked' due to %s",
req.context.get("remote_addr"), cert.subject.native["common_name"], serial, reason)
status = ocsp.CertStatus( status = ocsp.CertStatus(
name='revoked', name='revoked',
value={ value={
'revocation_time': revoked, 'revocation_time': revoked,
'revocation_reason': "key_compromise", 'revocation_reason': reason,
}) })
except EnvironmentError: except EnvironmentError:
logger.info("OCSP responder queried for unknown serial %040x from %s", serial, req.context.get("remote_addr"))
status = ocsp.CertStatus(name="unknown", value=None) status = ocsp.CertStatus(name="unknown", value=None)
responses.append({ responses.append({

View File

@ -14,7 +14,7 @@ from certidude.profile import SignatureProfile
from datetime import datetime from datetime import datetime
from oscrypto import asymmetric from oscrypto import asymmetric
from oscrypto.errors import SignatureError from oscrypto.errors import SignatureError
from xattr import getxattr from xattr import getxattr, setxattr
from .utils import AuthorityHandler from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets, whitelist_content_types from .utils.firewall import whitelist_subnets, whitelist_content_types
@ -59,12 +59,26 @@ class RequestListResource(AuthorityHandler):
common_name = csr["certification_request_info"]["subject"].native["common_name"] common_name = csr["certification_request_info"]["subject"].native["common_name"]
"""
Determine whether autosign is allowed to overwrite already issued
certificates automatically
"""
overwrite_allowed = False
for subnet in config.OVERWRITE_SUBNETS:
if req.context.get("remote_addr") in subnet:
overwrite_allowed = True
break
""" """
Handle domain computer automatic enrollment Handle domain computer automatic enrollment
""" """
machine = req.context.get("machine") machine = req.context.get("machine")
if machine: if machine:
if config.MACHINE_ENROLLMENT_ALLOWED: reasons.append("machine enrollment not allowed from %s" % req.context.get("remote_addr"))
for subnet in config.MACHINE_ENROLLMENT_SUBNETS:
if req.context.get("remote_addr") in subnet:
if common_name != machine: if common_name != machine:
raise falcon.HTTPBadRequest( raise falcon.HTTPBadRequest(
"Bad request", "Bad request",
@ -73,12 +87,11 @@ class RequestListResource(AuthorityHandler):
# 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")
cert, resp.body = self.authority._sign(csr, body, cert, resp.body = self.authority._sign(csr, body,
profile=config.PROFILES["rw"], overwrite=True) profile=config.PROFILES["rw"], overwrite=overwrite_allowed)
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s", logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
machine, req.context.get("remote_addr")) machine, req.context.get("remote_addr"))
return return
else:
reasons.append("Machine enrollment not allowed")
""" """
Attempt to renew certificate using currently valid key pair Attempt to renew certificate using currently valid key pair
@ -94,20 +107,22 @@ class RequestListResource(AuthorityHandler):
# Same public key # Same public key
if cert_pk == csr_pk: if cert_pk == csr_pk:
buf = req.get_header("X-SSL-CERT") buf = req.get_header("X-SSL-CERT")
# Used mutually authenticated TLS handshake, assume renewal
if buf: if buf:
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii")) # Used mutually authenticated TLS handshake, assume renewal
header, _, der_bytes = pem.unarmor(buf.replace("\t", "\n").replace("\n\n", "\n").encode("ascii"))
handshake_cert = x509.Certificate.load(der_bytes) handshake_cert = x509.Certificate.load(der_bytes)
if handshake_cert.native == cert.native: if handshake_cert.native == cert.native:
for subnet in config.RENEWAL_SUBNETS: for subnet in config.RENEWAL_SUBNETS:
if req.context.get("remote_addr") in subnet: if req.context.get("remote_addr") in subnet:
resp.set_header("Content-Type", "application/x-x509-user-cert") resp.set_header("Content-Type", "application/x-x509-user-cert")
setxattr(path, "user.revocation.reason", "superseded")
_, resp.body = self.authority._sign(csr, body, overwrite=True, _, resp.body = self.authority._sign(csr, body, overwrite=True,
profile=SignatureProfile.from_cert(cert)) profile=SignatureProfile.from_cert(cert))
logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr")) logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
return return
reasons.append("renewal failed")
# No header supplied, redirect to signed API call else:
# No renewal requested, 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) resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name)
return return
@ -117,35 +132,36 @@ class RequestListResource(AuthorityHandler):
Process automatic signing if the IP address is whitelisted, Process automatic signing if the IP address is whitelisted,
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 self.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 = self.authority._sign(csr, body, profile=config.PROFILES["rw"]) _, resp.body = self.authority._sign(csr, body,
overwrite=overwrite_allowed, profile=config.PROFILES["rw"])
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr")) logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
return return
except EnvironmentError: except EnvironmentError:
logger.info("Autosign for %s from %s failed, signed certificate already exists", logger.info("Autosign for %s from %s failed, signed certificate already exists",
common_name, 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:
reasons.append("Autosign failed, IP address not whitelisted") reasons.append("autosign failed, IP address not whitelisted")
else: else:
reasons.append("Autosign failed, only client certificates allowed to be signed automatically") reasons.append("autosign not requested")
# Attempt to save the request otherwise # Attempt to save the request otherwise
try: try:
request_path, _, _ = self.authority.store_request(body, request_path, _, _ = self.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")
# We should still redirect client to long poll URL below # We should still redirect client to long poll URL below
except errors.DuplicateCommonNameError: except errors.DuplicateCommonNameError:
# TODO: Certificate renewal # TODO: Certificate renewal
logger.warning("Rejected signing request with overlapping common name from %s", logger.warning("rejected signing request with overlapping common name from %s",
req.context.get("remote_addr")) req.context.get("remote_addr"))
raise falcon.HTTPConflict( raise falcon.HTTPConflict(
"CSR with such CN already exists", "CSR with such CN already exists",
@ -154,14 +170,15 @@ class RequestListResource(AuthorityHandler):
push.publish("request-submitted", common_name) 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("Stored signing request %s from %s", common_name, req.context.get("remote_addr")) logger.info("Stored signing request %s from %s, reasons: %s", common_name, req.context.get("remote_addr"), reasons)
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()
click.echo("Redirecting to: %s" % url) click.echo("Redirecting to: %s" % url)
resp.status = falcon.HTTP_SEE_OTHER resp.status = falcon.HTTP_SEE_OTHER
resp.set_header("Location", url) resp.set_header("Location", url)
logger.debug("Redirecting signing request from %s to %s", req.context.get("remote_addr"), url) logger.debug("Redirecting signing request from %s to %s, reasons: %s", req.context.get("remote_addr"), url, ", ".join(reasons))
else: else:
# Request was accepted, but not processed # Request was accepted, but not processed
resp.status = falcon.HTTP_202 resp.status = falcon.HTTP_202

View File

@ -1,4 +1,7 @@
import click
import falcon
import hashlib import hashlib
import logging
import os import os
from asn1crypto import cms, algos from asn1crypto import cms, algos
from asn1crypto.core import SetOf, PrintableString from asn1crypto.core import SetOf, PrintableString
@ -9,6 +12,8 @@ from oscrypto.errors import SignatureError
from .utils import AuthorityHandler from .utils import AuthorityHandler
from .utils.firewall import whitelist_subnets from .utils.firewall import whitelist_subnets
logger = logging.getLogger(__name__)
# Monkey patch asn1crypto # Monkey patch asn1crypto
class SetOfPrintableString(SetOf): class SetOfPrintableString(SetOf):
@ -28,21 +33,54 @@ cms.CMSAttribute._oid_specs['sender_nonce'] = cms.SetOfOctetString
cms.CMSAttribute._oid_specs['recipient_nonce'] = cms.SetOfOctetString cms.CMSAttribute._oid_specs['recipient_nonce'] = cms.SetOfOctetString
cms.CMSAttribute._oid_specs['trans_id'] = SetOfPrintableString cms.CMSAttribute._oid_specs['trans_id'] = SetOfPrintableString
class SCEPError(Exception): code = 25 # system failure class SCEPError(Exception):
class SCEPBadAlgo(SCEPError): code = 0 code = 25 # system failure
class SCEPBadMessageCheck(SCEPError): code = 1 explaination = "General system failure"
class SCEPBadRequest(SCEPError): code = 2
class SCEPBadTime(SCEPError): code = 3 class SCEPBadAlgo(SCEPError):
class SCEPBadCertId(SCEPError): code = 4 code = 0
explaination = "Unsupported algorithm in SCEP request"
class SCEPBadMessageCheck(SCEPError):
code = 1
explaination = "Integrity check failed for SCEP request"
class SCEPBadRequest(SCEPError):
code = 2
explaination = "Bad request"
class SCEPBadTime(SCEPError):
code = 3
explaination = "Bad time"
class SCEPBadCertId(SCEPError):
code = 4
explaination = "Certificate authority mismatch"
class SCEPDigestMismatch(SCEPBadMessageCheck):
explaination = "Digest mismatch"
class SCEPSignatureMismatch(SCEPBadMessageCheck):
explaination = "Signature mismatch"
class SCEPResource(AuthorityHandler): class SCEPResource(AuthorityHandler):
@whitelist_subnets(config.SCEP_SUBNETS) @whitelist_subnets(config.SCEP_SUBNETS)
def on_get(self, req, resp): def on_get(self, req, resp):
operation = req.get_param("operation", required=True) operation = req.get_param("operation", required=True)
if operation.lower() == "getcacert": if operation == "GetCACert":
resp.body = keys.parse_certificate(self.authority.certificate_buf).dump() resp.body = keys.parse_certificate(self.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
elif operation == "GetCACaps":
# TODO: return renewal flag based on renewal subnets config option
resp.body = "Renewal\nMD5\nSHA-1\nSHA-256\nSHA-512\nDES3\n"
return
elif operation == "PKIOperation":
pass
else:
raise falcon.HTTPBadRequest(
"Bad request",
"Unknown operation %s" % operation)
# If we bump into exceptions later # If we bump into exceptions later
encrypted_container = b"" encrypted_container = b""
@ -74,8 +112,14 @@ class SCEPResource(AuthorityHandler):
# TODO: compare cert to current one if we are renewing # TODO: compare cert to current one if we are renewing
assert signer["digest_algorithm"]["algorithm"].native == "md5" digest_algorithm = signer["digest_algorithm"]["algorithm"].native
assert signer["signature_algorithm"]["algorithm"].native == "rsassa_pkcs1v15" signature_algorithm = signer["signature_algorithm"]["algorithm"].native
if digest_algorithm not in ("md5", "sha1", "sha256", "sha512"):
raise SCEPBadAlgo()
if signature_algorithm != "rsassa_pkcs1v15":
raise SCEPBadAlgo()
message_digest = None message_digest = None
transaction_id = None transaction_id = None
sender_nonce = None sender_nonce = None
@ -87,8 +131,13 @@ class SCEPResource(AuthorityHandler):
transaction_id, = attr["values"] transaction_id, = attr["values"]
elif attr["type"].native == "message_digest": elif attr["type"].native == "message_digest":
message_digest, = attr["values"] message_digest, = attr["values"]
if hashlib.md5(encap_content.native).digest() != message_digest.native: if getattr(hashlib, digest_algorithm)(encap_content.native).digest() != message_digest.native:
raise SCEPBadMessageCheck() raise SCEPDigestMismatch()
if not sender_nonce:
raise SCEPBadRequest()
if not transaction_id:
raise SCEPBadRequest()
assert message_digest assert message_digest
msg = signer["signed_attrs"].dump(force=True) msg = signer["signed_attrs"].dump(force=True)
@ -102,7 +151,8 @@ class SCEPResource(AuthorityHandler):
b"\x31" + msg[1:], # wtf?! b"\x31" + msg[1:], # wtf?!
"md5") "md5")
except SignatureError: except SignatureError:
raise SCEPBadMessageCheck() raise SCEPSignatureMismatch()
############################### ###############################
### Decrypt inner container ### ### Decrypt inner container ###
@ -122,14 +172,15 @@ class SCEPResource(AuthorityHandler):
if recipient.native["rid"]["serial_number"] != self.authority.certificate.serial_number: if recipient.native["rid"]["serial_number"] != self.authority.certificate.serial_number:
raise SCEPBadCertId() raise SCEPBadCertId()
# Since CA private key is not directly readable here, we'll redirect it to signer socket
key = asymmetric.rsa_pkcs1v15_decrypt( key = asymmetric.rsa_pkcs1v15_decrypt(
self.authority.private_key, self.authority.private_key,
recipient.native["encrypted_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 = self.authority.store_request(buf, overwrite=True) _, _, common_name = self.authority.store_request(buf, overwrite=True)
cert, buf = self.authority.sign(common_name, overwrite=True) logger.info("SCEP client from %s requested with %s digest algorithm, %s signature",
req.context["remote_addr"], digest_algorithm, signature_algorithm)
cert, buf = self.authority.sign(common_name, profile=config.PROFILES["gw"], overwrite=True)
signed_certificate = asymmetric.load_certificate(buf) signed_certificate = asymmetric.load_certificate(buf)
content = signed_certificate.asn1.dump() content = signed_certificate.asn1.dump()
@ -138,6 +189,7 @@ class SCEPResource(AuthorityHandler):
'type': "fail_info", 'type': "fail_info",
'values': ["%d" % e.code] 'values': ["%d" % e.code]
})) }))
logger.info("Failed to sign SCEP request due to: %s" % e.explaination)
else: else:
################################## ##################################
@ -150,7 +202,8 @@ class SCEPResource(AuthorityHandler):
'version': "v1", 'version': "v1",
'certificates': [signed_certificate.asn1], 'certificates': [signed_certificate.asn1],
'digest_algorithms': [cms.DigestAlgorithm({ 'digest_algorithms': [cms.DigestAlgorithm({
'algorithm': "md5" 'algorithm': digest_algorithm
})], })],
'encap_content_info': { 'encap_content_info': {
'content_type': "data", 'content_type': "data",
@ -208,7 +261,7 @@ class SCEPResource(AuthorityHandler):
attr_list = [ attr_list = [
cms.CMSAttribute({ cms.CMSAttribute({
'type': "message_digest", 'type': "message_digest",
'values': [hashlib.sha1(encrypted_container).digest()] 'values': [getattr(hashlib, digest_algorithm)(encrypted_container).digest()]
}), }),
cms.CMSAttribute({ cms.CMSAttribute({
'type': "message_type", 'type': "message_type",
@ -245,12 +298,12 @@ class SCEPResource(AuthorityHandler):
'serial_number': self.authority.certificate.serial_number, 'serial_number': self.authority.certificate.serial_number,
}), }),
}), }),
'digest_algorithm': algos.DigestAlgorithm({'algorithm': "sha1"}), 'digest_algorithm': algos.DigestAlgorithm({'algorithm': digest_algorithm}),
'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': "rsassa_pkcs1v15"}), 'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': "rsassa_pkcs1v15"}),
'signature': asymmetric.rsa_pkcs1v15_sign( 'signature': asymmetric.rsa_pkcs1v15_sign(
self.authority.private_key, self.authority.private_key,
b"\x31" + attrs.dump()[1:], b"\x31" + attrs.dump()[1:],
"sha1" digest_algorithm
) )
}) })
@ -261,7 +314,7 @@ class SCEPResource(AuthorityHandler):
'version': "v1", 'version': "v1",
'certificates': [self.authority.certificate], 'certificates': [self.authority.certificate],
'digest_algorithms': [cms.DigestAlgorithm({ 'digest_algorithms': [cms.DigestAlgorithm({
'algorithm': "sha1" 'algorithm': digest_algorithm
})], })],
'encap_content_info': { 'encap_content_info': {
'content_type': "data", 'content_type': "data",

View File

@ -44,7 +44,7 @@ class SignedCertificateDetailResource(AuthorityHandler):
resp.body = json.dumps(dict( resp.body = json.dumps(dict(
common_name = cn, common_name = cn,
signer = signer_username, signer = signer_username,
serial = "%x" % cert.serial_number, serial = "%040x" % cert.serial_number,
organizational_unit = cert.subject.native.get("organizational_unit_name"), organizational_unit = cert.subject.native.get("organizational_unit_name"),
signed = cert["tbs_certificate"]["validity"]["not_before"].native.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["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
@ -54,7 +54,8 @@ class SignedCertificateDetailResource(AuthorityHandler):
extensions = dict([ extensions = dict([
(e["extn_id"].native, e["extn_value"].native) (e["extn_id"].native, e["extn_value"].native)
for e in cert["tbs_certificate"]["extensions"] for e in cert["tbs_certificate"]["extensions"]
if e["extn_value"] in ("extended_key_usage",)]) if e["extn_id"].native in ("extended_key_usage",)])
)) ))
logger.debug("Served certificate %s to %s as application/json", logger.debug("Served certificate %s to %s as application/json",
cn, req.context.get("remote_addr")) cn, req.context.get("remote_addr"))
@ -69,5 +70,6 @@ class SignedCertificateDetailResource(AuthorityHandler):
def on_delete(self, req, resp, cn): def on_delete(self, req, resp, cn):
logger.info("Revoked certificate %s by %s from %s", logger.info("Revoked certificate %s by %s from %s",
cn, req.context.get("user"), req.context.get("remote_addr")) cn, req.context.get("user"), req.context.get("remote_addr"))
self.authority.revoke(cn) self.authority.revoke(cn,
reason=req.get_param("reason", default="key_compromise"))

View File

@ -30,9 +30,14 @@ def authenticate(optional=False):
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
try:
server_creds = gssapi.creds.Credentials( server_creds = gssapi.creds.Credentials(
usage='accept', usage='accept',
name=gssapi.names.Name('HTTP/%s'% const.FQDN)) name=gssapi.names.Name('HTTP/%s'% const.FQDN))
except gssapi.raw.exceptions.BadNameError:
logger.error("Failed initialize HTTP service principal, possibly bad permissions for %s or /etc/krb5.conf" %
config.KERBEROS_KEYTAB)
raise
context = gssapi.sec_contexts.SecurityContext(creds=server_creds) context = gssapi.sec_contexts.SecurityContext(creds=server_creds)
@ -49,13 +54,13 @@ def authenticate(optional=False):
raise falcon.HTTPBadRequest("Bad request", "Unsupported authentication mechanism (NTLM?) was offered. Please make sure you've logged into the computer with domain user account. The web interface should not prompt for username or password.") raise falcon.HTTPBadRequest("Bad request", "Unsupported authentication mechanism (NTLM?) was offered. Please make sure you've logged into the computer with domain user account. The web interface should not prompt for username or password.")
try: try:
username, domain = str(context.initiator_name).split("@") username, realm = str(context.initiator_name).split("@")
except AttributeError: # TODO: Better exception except AttributeError: # TODO: Better exception
raise falcon.HTTPForbidden("Failed to determine username, are you trying to log in with correct domain account?") raise falcon.HTTPForbidden("Failed to determine username, are you trying to log in with correct domain account?")
if domain.lower() != const.DOMAIN.lower(): if realm != config.KERBEROS_REALM:
raise falcon.HTTPForbidden("Forbidden", raise falcon.HTTPForbidden("Forbidden",
"Invalid realm supplied") "Cross-realm trust not supported")
if username.endswith("$") and optional: if username.endswith("$") and optional:
# Extract machine hostname # Extract machine hostname

View File

@ -12,6 +12,7 @@ from asn1crypto.csr import CertificationRequest
from certbuilder import CertificateBuilder from certbuilder import CertificateBuilder
from certidude import config, push, mailer, const from certidude import config, push, mailer, const
from certidude import errors from certidude import errors
from certidude.common import cn_to_dn
from crlbuilder import CertificateListBuilder, pem_armor_crl from crlbuilder import CertificateListBuilder, pem_armor_crl
from csrbuilder import CSRBuilder, pem_armor_csr from csrbuilder import CSRBuilder, pem_armor_csr
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -21,6 +22,16 @@ from xattr import getxattr, listxattr, setxattr
random = SystemRandom() random = SystemRandom()
try:
from time import time_ns
except ImportError:
from time import time
def time_ns():
return int(time() * 10**9) # 64 bits integer, 32 ns bits
def generate_serial():
return time_ns() << 56 | random.randint(0, 2**56-1)
# https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/ # https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/
# https://jamielinux.com/docs/openssl-certificate-authority/ # https://jamielinux.com/docs/openssl-certificate-authority/
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py # http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
@ -38,6 +49,8 @@ with open(config.AUTHORITY_PRIVATE_KEY_PATH, "rb") as fh:
private_key = asymmetric.load_private_key(key_der_bytes) private_key = asymmetric.load_private_key(key_der_bytes)
def self_enroll(): def self_enroll():
assert os.getuid() == 0 and os.getgid() == 0, "Can self-enroll only as root"
from certidude import const from certidude import const
common_name = const.FQDN common_name = const.FQDN
directory = os.path.join("/var/lib/certidude", const.FQDN) directory = os.path.join("/var/lib/certidude", const.FQDN)
@ -64,13 +77,16 @@ def self_enroll():
builder = CSRBuilder({"common_name": common_name}, self_public_key) builder = CSRBuilder({"common_name": common_name}, self_public_key)
request = builder.build(private_key) request = builder.build(private_key)
with open(os.path.join(directory, "requests", common_name + ".pem"), "wb") as fh:
fh.write(pem_armor_csr(request))
pid = os.fork() pid = os.fork()
if not pid: if not pid:
from certidude import authority from certidude import authority
from certidude.common import drop_privileges from certidude.common import drop_privileges
drop_privileges() drop_privileges()
assert os.getuid() != 0 and os.getgid() != 0
path = os.path.join(directory, "requests", common_name + ".pem")
click.echo("Writing request to %s" % path)
with open(path, "wb") as fh:
fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions
authority.sign(common_name, skip_push=True, overwrite=True, profile=config.PROFILES["srv"]) authority.sign(common_name, skip_push=True, overwrite=True, profile=config.PROFILES["srv"])
sys.exit(0) sys.exit(0)
else: else:
@ -109,18 +125,23 @@ def get_signed(common_name):
def get_revoked(serial): def get_revoked(serial):
if isinstance(serial, str): if isinstance(serial, str):
serial = int(serial, 16) serial = int(serial, 16)
path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial) path = os.path.join(config.REVOKED_DIR, "%040x.pem" % serial)
with open(path, "rb") as fh: with open(path, "rb") as fh:
buf = fh.read() buf = fh.read()
header, _, der_bytes = pem.unarmor(buf) header, _, der_bytes = pem.unarmor(buf)
cert = x509.Certificate.load(der_bytes) cert = x509.Certificate.load(der_bytes)
try:
reason = getxattr(path, "user.revocation.reason").decode("ascii")
except IOError: # TODO: make sure it's not required
reason = "key_compromise"
return path, buf, cert, \ return path, buf, cert, \
cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None), \ cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None), \
cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None), \ cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None), \
datetime.utcfromtimestamp(os.stat(path).st_ctime) datetime.utcfromtimestamp(os.stat(path).st_ctime), \
reason
def get_attributes(cn, namespace=None): def get_attributes(cn, namespace=None, flat=False):
path, buf, cert, signed, expires = get_signed(cn) path, buf, cert, signed, expires = get_signed(cn)
attribs = dict() attribs = dict()
for key in listxattr(path): for key in listxattr(path):
@ -129,7 +150,10 @@ def get_attributes(cn, namespace=None):
continue continue
if namespace and not key.startswith("user.%s." % namespace): if namespace and not key.startswith("user.%s." % namespace):
continue continue
value = getxattr(path, key) value = getxattr(path, key).decode("utf-8")
if flat:
attribs[key[len("user.%s." % namespace):]] = value
else:
current = attribs current = attribs
if "." in key: if "." in key:
prefix, key = key.rsplit(".", 1) prefix, key = key.rsplit(".", 1)
@ -137,7 +161,7 @@ def get_attributes(cn, namespace=None):
if component not in current: if component not in current:
current[component] = dict() current[component] = dict()
current = current[component] current = current[component]
current[key] = value.decode("utf-8") current[key] = value
return path, buf, cert, attribs return path, buf, cert, attribs
@ -159,7 +183,7 @@ def store_request(buf, overwrite=False, address="", user=""):
common_name = csr["certification_request_info"]["subject"].native["common_name"] common_name = csr["certification_request_info"]["subject"].native["common_name"]
if not re.match(const.RE_COMMON_NAME, common_name): if not re.match(const.RE_COMMON_NAME, common_name):
raise ValueError("Invalid common name") raise ValueError("Invalid common name %s" % repr(common_name))
request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
@ -190,14 +214,21 @@ def store_request(buf, overwrite=False, address="", user=""):
return request_path, csr, common_name return request_path, csr, common_name
def revoke(common_name): def revoke(common_name, reason):
""" """
Revoke valid certificate Revoke valid certificate
""" """
signed_path, buf, cert, signed, expires = get_signed(common_name) signed_path, buf, cert, signed, expires = get_signed(common_name)
revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial_number)
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number)) if reason not in ("key_compromise", "ca_compromise", "affiliation_changed",
"superseded", "cessation_of_operation", "certificate_hold",
"remove_from_crl", "privilege_withdrawn"):
raise ValueError("Invalid revocation reason %s" % reason)
setxattr(signed_path, "user.revocation.reason", reason)
revoked_path = os.path.join(config.REVOKED_DIR, "%040x.pem" % cert.serial_number)
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number))
os.rename(signed_path, revoked_path) os.rename(signed_path, revoked_path)
@ -212,7 +243,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_number, serial_hex="%040x" % cert.serial_number,
common_name=common_name) common_name=common_name)
return revoked_path return revoked_path
@ -251,28 +282,40 @@ def _list_certificates(directory):
server = True server = True
yield cert.subject.native["common_name"], path, buf, cert, server yield cert.subject.native["common_name"], path, buf, cert, server
def list_signed(directory=config.SIGNED_DIR): def list_signed(directory=config.SIGNED_DIR, common_name=None):
for filename in os.listdir(directory): for filename in os.listdir(directory):
if filename.endswith(".pem"): if not filename.endswith(".pem"):
common_name = filename[:-4] continue
path, buf, cert, signed, expires = get_signed(common_name) basename = filename[:-4]
yield common_name, path, buf, cert, signed, expires if common_name:
if common_name.startswith("^"):
if not re.match(common_name, basename):
continue
else:
if common_name != basename:
continue
path, buf, cert, signed, expires = get_signed(basename)
yield basename, path, buf, cert, signed, expires
def list_revoked(directory=config.REVOKED_DIR): def list_revoked(directory=config.REVOKED_DIR):
for filename in os.listdir(directory): for filename in os.listdir(directory):
if filename.endswith(".pem"): if filename.endswith(".pem"):
common_name = filename[:-4] common_name = filename[:-4]
path, buf, cert, signed, expired, revoked = get_revoked(common_name) path, buf, cert, signed, expired, revoked, reason = get_revoked(common_name)
yield cert.subject.native["common_name"], path, buf, cert, signed, expired, revoked yield cert.subject.native["common_name"], path, buf, cert, signed, expired, revoked, reason
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(pem=True): def export_crl(pem=True):
# To migrate older installations run following:
# for j in /var/lib/certidude/*/revoked/*.pem; do echo $(attr -s 'revocation.reason' -V key_compromise $j); done
builder = CertificateListBuilder( builder = CertificateListBuilder(
config.AUTHORITY_CRL_URL, config.AUTHORITY_CRL_URL,
certificate, certificate,
1 # TODO: monotonically increasing generate_serial()
) )
for filename in os.listdir(config.REVOKED_DIR): for filename in os.listdir(config.REVOKED_DIR):
@ -281,12 +324,14 @@ def export_crl(pem=True):
serial_number = filename[:-4] serial_number = filename[:-4]
# TODO: Assert serial against regex # TODO: Assert serial against regex
revoked_path = os.path.join(config.REVOKED_DIR, filename) revoked_path = os.path.join(config.REVOKED_DIR, filename)
reason = getxattr(revoked_path, "user.revocation.reason").decode("ascii") # TODO: dedup
# TODO: Skip expired certificates # TODO: Skip expired certificates
s = os.stat(revoked_path) s = os.stat(revoked_path)
builder.add_certificate( builder.add_certificate(
int(filename[:-4], 16), int(filename[:-4], 16),
datetime.utcfromtimestamp(s.st_ctime), datetime.utcfromtimestamp(s.st_ctime),
"key_compromise") reason)
certificate_list = builder.build(private_key) certificate_list = builder.build(private_key)
if pem: if pem:
@ -359,7 +404,7 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
if overwrite: if overwrite:
# TODO: is this the best approach? # TODO: is this the best approach?
prev_serial_hex = "%x" % prev.serial_number prev_serial_hex = "%040x" % 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")]
@ -367,14 +412,10 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
else: else:
raise FileExistsError("Will not overwrite existing certificate") raise FileExistsError("Will not overwrite existing certificate")
dn = {u'common_name': common_name } builder = CertificateBuilder(cn_to_dn(common_name, const.FQDN,
if profile.ou: o=certificate["tbs_certificate"]["subject"].native.get("organization_name"),
dn["organizational_unit_name"] = profile.ou ou=profile.ou), csr_pubkey)
builder.serial_number = generate_serial()
builder = CertificateBuilder(dn, csr_pubkey)
builder.serial_number = random.randint(
0x1000000000000000000000000000000000000000,
0x7fffffffffffffffffffffffffffffffffffffff)
now = datetime.utcnow() now = datetime.utcnow()
builder.begin_date = now - timedelta(minutes=5) builder.begin_date = now - timedelta(minutes=5)
@ -392,10 +433,10 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
os.rename(cert_path + ".part", cert_path) os.rename(cert_path + ".part", cert_path)
attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt")) attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt"))
cert_serial_hex = "%x" % end_entity_cert.serial_number cert_serial_hex = "%040x" % end_entity_cert.serial_number
# Create symlink # Create symlink
link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % end_entity_cert.serial_number) link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % end_entity_cert.serial_number)
assert not os.path.exists(link_name), "Certificate with same serial number already exists: %s" % link_name assert not os.path.exists(link_name), "Certificate with same serial number already exists: %s" % link_name
os.symlink("../%s.pem" % common_name, link_name) os.symlink("../%s.pem" % common_name, link_name)
@ -422,6 +463,10 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
click.echo("Publishing certificate at %s ..." % url) click.echo("Publishing certificate at %s ..." % url)
requests.post(url, data=end_entity_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"})
if renew:
# TODO: certificate-renewed event
push.publish("certificate-revoked", common_name)
push.publish("request-signed", common_name)
else:
push.publish("request-signed", common_name) push.publish("request-signed", common_name)
return end_entity_cert, end_entity_cert_buf return end_entity_cert, end_entity_cert_buf

View File

@ -12,12 +12,13 @@ import subprocess
import sys import sys
from asn1crypto import pem, x509 from asn1crypto import pem, x509
from asn1crypto.csr import CertificationRequest from asn1crypto.csr import CertificationRequest
from asn1crypto.crl import CertificateList
from base64 import b64encode from base64 import b64encode
from certbuilder import CertificateBuilder, pem_armor_certificate from certbuilder import CertificateBuilder, pem_armor_certificate
from certidude import const from certidude import const
from csrbuilder import CSRBuilder, pem_armor_csr from csrbuilder import CSRBuilder, pem_armor_csr
from configparser import ConfigParser, NoOptionError from configparser import ConfigParser, NoOptionError
from certidude.common import apt, rpm, drop_privileges, selinux_fixup from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn
from datetime import datetime, timedelta from datetime import datetime, timedelta
from glob import glob from glob import glob
from ipaddress import ip_network from ipaddress import ip_network
@ -49,7 +50,7 @@ def setup_client(prefix="client_", dh=False):
def wrapped(**arguments): def wrapped(**arguments):
common_name = arguments.get("common_name") common_name = arguments.get("common_name")
authority = arguments.get("authority") authority = arguments.get("authority")
b = os.path.join(const.STORAGE_PATH, authority) b = os.path.join("/etc/certidude/authority", authority)
if dh: if dh:
path = os.path.join(const.STORAGE_PATH, "dh.pem") path = os.path.join(const.STORAGE_PATH, "dh.pem")
if not os.path.exists(path): if not os.path.exists(path):
@ -94,6 +95,8 @@ def setup_client(prefix="client_", dh=False):
@click.option("-s", "--skip-self", default=False, is_flag=True, help="Skip self enroll") @click.option("-s", "--skip-self", default=False, is_flag=True, help="Skip self enroll")
@click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign") @click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign")
def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
assert os.getuid() == 0 and os.getgid() == 0, "Can enroll only as root"
if not skip_self and os.path.exists(const.SERVER_CONFIG_PATH): if not skip_self and os.path.exists(const.SERVER_CONFIG_PATH):
click.echo("Self-enrolling authority's web interface certificate") click.echo("Self-enrolling authority's web interface certificate")
from certidude import authority from certidude import authority
@ -182,7 +185,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
try: try:
authority_path = clients.get(authority_name, "authority path") authority_path = clients.get(authority_name, "authority path")
except NoOptionError: except NoOptionError:
authority_path = "/var/lib/certidude/%s/ca_cert.pem" % authority_name authority_path = "/etc/certidude/authority/%s/ca_cert.pem" % authority_name
finally: finally:
if os.path.exists(authority_path): if os.path.exists(authority_path):
click.echo("Found authority certificate in: %s" % authority_path) click.echo("Found authority certificate in: %s" % authority_path)
@ -233,7 +236,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
# pip # pip
# Firefox (?) on Debian, Ubuntu # Firefox (?) on Debian, Ubuntu
if os.path.exists("/usr/bin/update-ca-certificates"): if os.path.exists("/usr/bin/update-ca-certificates") or os.path.exists("/usr/sbin/update-ca-certificates"):
link_path = "/usr/local/share/ca-certificates/%s" % authority_name link_path = "/usr/local/share/ca-certificates/%s" % authority_name
if not os.path.lexists(link_path): if not os.path.lexists(link_path):
os.symlink(authority_path, link_path) os.symlink(authority_path, link_path)
@ -257,11 +260,13 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
r = requests.get(revoked_url, headers={'accept': 'application/x-pem-file'}) r = requests.get(revoked_url, headers={'accept': 'application/x-pem-file'})
if r.status_code == 200: if r.status_code == 200:
revocations = crl.CertificateList.load(pem.unarmor(r.content)) header, _, crl_der_bytes = pem.unarmor(r.content)
revocations = CertificateList.load(crl_der_bytes)
# TODO: check signature, parse reasons, remove keys if revoked # TODO: check signature, parse reasons, remove keys if revoked
revocations_partial = revocations_path + ".part" revocations_partial = revocations_path + ".part"
with open(revocations_partial, 'wb') as f: with open(revocations_partial, 'wb') as f:
f.write(r.content) f.write(r.content)
os.rename(revocations_partial, revocations_path)
elif r.status_code == 404: elif r.status_code == 404:
click.echo("CRL disabled, server said 404") click.echo("CRL disabled, server said 404")
else: else:
@ -293,8 +298,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
key_path = clients.get(authority_name, "key path") key_path = clients.get(authority_name, "key path")
request_path = clients.get(authority_name, "request path") request_path = clients.get(authority_name, "request path")
except NoOptionError: except NoOptionError:
key_path = "/var/lib/certidude/%s/client_key.pem" % authority_name key_path = "/etc/certidude/authority/%s/host_key.pem" % authority_name
request_path = "/var/lib/certidude/%s/client_csr.pem" % authority_name request_path = "/etc/certidude/authority/%s/host_csr.pem" % authority_name
if os.path.exists(request_path): if os.path.exists(request_path):
with open(request_path, "rb") as fh: with open(request_path, "rb") as fh:
@ -334,7 +339,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
try: try:
certificate_path = clients.get(authority_name, "certificate path") certificate_path = clients.get(authority_name, "certificate path")
except NoOptionError: except NoOptionError:
certificate_path = "/var/lib/certidude/%s/client_cert.pem" % authority_name certificate_path = "/etc/certidude/authority/%s/host_cert.pem" % authority_name
try: try:
renewal_overlap = clients.getint(authority_name, "renewal overlap") renewal_overlap = clients.getint(authority_name, "renewal overlap")
@ -352,10 +357,15 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
except EnvironmentError: # Certificate missing, can't renew except EnvironmentError: # Certificate missing, can't renew
pass pass
try:
autosign = clients.getboolean(authority_name, "autosign")
except NoOptionError:
autosign = True
if not os.path.exists(certificate_path) or renew: if not os.path.exists(certificate_path) or renew:
# Set up URL-s # Set up URL-s
request_params = set() request_params = set()
request_params.add("autosign=true") request_params.add("autosign=%s" % ("yes" if autosign else "no"))
if not no_wait: if not no_wait:
request_params.add("wait=forever") request_params.add("wait=forever")
@ -371,6 +381,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
if renew: # Do mutually authenticated TLS handshake if renew: # Do mutually authenticated TLS handshake
request_url = "https://%s:8443/api/request/" % authority_name request_url = "https://%s:8443/api/request/" % authority_name
kwargs["cert"] = certificate_path, key_path kwargs["cert"] = certificate_path, key_path
click.echo("Renewing using current keypair at %s %s" % kwargs["cert"])
else: else:
# If machine is joined to domain attempt to present machine credentials for authentication # If machine is joined to domain attempt to present machine credentials for authentication
if kerberos: if kerberos:
@ -416,6 +427,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
elif submission.status_code == requests.codes.gone: elif submission.status_code == requests.codes.gone:
# Should the client retry or disable request submission? # Should the client retry or disable request submission?
raise ValueError("Server refused to sign the request") # TODO: Raise proper exception raise ValueError("Server refused to sign the request") # TODO: Raise proper exception
elif submission.status_code == requests.codes.bad_request:
raise ValueError("Server said following, likely current certificate expired/revoked? %s" % submission.text)
else: else:
submission.raise_for_status() submission.raise_for_status()
@ -964,12 +977,9 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default") help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN) @click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN)
@click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN) @click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN)
@click.option("--country", "-c", default=None, help="Country, none by default")
@click.option("--state", "-s", default=None, help="State or country, none by default")
@click.option("--locality", "-l", default=None, help="City or locality, none by default")
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default") @click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default")
@click.option("--organization", "-o", default=None, help="Company or organization name") @click.option("--organization", "-o", default=None, help="Company or organization name")
@click.option("--organizational-unit", "-o", default=None) @click.option("--organizational-unit", "-ou", default="Certificate Authority")
@click.option("--push-server", help="Push server, by default http://%s" % const.FQDN) @click.option("--push-server", help="Push server, by default http://%s" % const.FQDN)
@click.option("--directory", help="Directory for authority files") @click.option("--directory", help="Directory for authority files")
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags") @click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
@ -977,7 +987,8 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages") @click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair") @click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
@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, title, skip_packages, elliptic_curve): def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags, title, skip_packages, elliptic_curve):
assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) == b"xenial\n", "Only Ubuntu 16.04 supported at the moment"
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root" assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
import pwd import pwd
@ -992,7 +1003,8 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
cython3 python3-dev python3-mimeparse \ cython3 python3-dev python3-mimeparse \
python3-markdown python3-pyxattr python3-jinja2 python3-cffi \ python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
software-properties-common libsasl2-modules-gssapi-mit npm nodejs \ software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev rsync attr") libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \
rsync attr wget unzip")
os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam") os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam")
os.system("pip3 install -q --pre --upgrade python-ldap") os.system("pip3 install -q --pre --upgrade python-ldap")
@ -1096,9 +1108,18 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
else: else:
click.echo("Not systemd based OS, don't know how to set up initscripts") click.echo("Not systemd based OS, don't know how to set up initscripts")
# Set umask to 0022
os.umask(0o022)
assert os.getuid() == 0 and os.getgid() == 0 assert os.getuid() == 0 and os.getgid() == 0
bootstrap_pid = os.fork() bootstrap_pid = os.fork()
if not bootstrap_pid: if not bootstrap_pid:
# Create what's usually /var/lib/certidude
if not os.path.exists(directory):
os.makedirs(directory)
assert os.stat(directory).st_mode == 0o40755
# Create bundle directories # Create bundle directories
bundle_js = os.path.join(assets_dir, "js", "bundle.js") bundle_js = os.path.join(assets_dir, "js", "bundle.js")
bundle_css = os.path.join(assets_dir, "css", "bundle.css") bundle_css = os.path.join(assets_dir, "css", "bundle.css")
@ -1108,6 +1129,10 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
click.echo("Creating directory %s" % subdir) click.echo("Creating directory %s" % subdir)
os.makedirs(subdir) os.makedirs(subdir)
# Copy fonts
click.echo("Copying fonts...")
os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir)
# Install JavaScript pacakges # Install JavaScript pacakges
if skip_packages: if skip_packages:
click.echo("Not attempting to install packages from NPM as requested...") click.echo("Not attempting to install packages from NPM as requested...")
@ -1140,10 +1165,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
os.rename(bundle_css + ".part", bundle_css) os.rename(bundle_css + ".part", bundle_css)
os.rename(bundle_js + ".part", bundle_js) os.rename(bundle_js + ".part", bundle_js)
# Copy fonts
click.echo("Copying fonts...")
os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir)
assert os.getuid() == 0 and os.getgid() == 0 assert os.getuid() == 0 and os.getgid() == 0
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
os.setgid(gid) os.setgid(gid)
@ -1152,10 +1173,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
if not os.path.exists(const.CONFIG_DIR): if not os.path.exists(const.CONFIG_DIR):
click.echo("Creating %s" % const.CONFIG_DIR) click.echo("Creating %s" % const.CONFIG_DIR)
os.makedirs(const.CONFIG_DIR) os.makedirs(const.CONFIG_DIR)
os.umask(0o137) # 640
if os.path.exists(const.SERVER_CONFIG_PATH): if os.path.exists(const.SERVER_CONFIG_PATH):
click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH) click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
else: else:
os.umask(0o137)
push_token = "".join([random.choice(string.ascii_letters + string.digits) for j in range(0,32)]) push_token = "".join([random.choice(string.ascii_letters + string.digits) for j in range(0,32)])
with open(const.SERVER_CONFIG_PATH, "w") as fh: with open(const.SERVER_CONFIG_PATH, "w") as fh:
fh.write(env.get_template("server/server.conf").render(vars())) fh.write(env.get_template("server/server.conf").render(vars()))
@ -1169,7 +1191,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
fh.write(env.get_template("server/builder.conf").render(vars())) fh.write(env.get_template("server/builder.conf").render(vars()))
click.echo("File %s created" % const.BUILDER_CONFIG_PATH) click.echo("File %s created" % const.BUILDER_CONFIG_PATH)
# Create image builder config # Create signature profile config
if os.path.exists(const.PROFILE_CONFIG_PATH): if os.path.exists(const.PROFILE_CONFIG_PATH):
click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH) click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
else: else:
@ -1177,10 +1199,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
fh.write(env.get_template("server/profile.conf").render(vars())) fh.write(env.get_template("server/profile.conf").render(vars()))
click.echo("File %s created" % const.PROFILE_CONFIG_PATH) click.echo("File %s created" % const.PROFILE_CONFIG_PATH)
# Create directory with 755 permissions if not os.path.exists("/var/lib/certidude/builder"):
os.umask(0o022) click.echo("Creating %s" % "/var/lib/certidude/builder")
if not os.path.exists(directory): os.makedirs("/var/lib/certidude/builder")
os.makedirs(directory)
# Create subdirectories with 770 permissions # Create subdirectories with 770 permissions
os.umask(0o007) os.umask(0o007)
@ -1191,10 +1212,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
os.mkdir(path) os.mkdir(path)
else: else:
click.echo("Directory already exists %s" % path) click.echo("Directory already exists %s" % path)
assert os.stat(path).st_mode == 0o40770
# Create SQLite database file with correct permissions # Create SQLite database file with correct permissions
if not os.path.exists(sqlite_path):
os.umask(0o117) os.umask(0o117)
if not os.path.exists(sqlite_path):
with open(sqlite_path, "wb") as fh: with open(sqlite_path, "wb") as fh:
pass pass
@ -1207,16 +1229,10 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
click.echo("Generating %d-bit RSA key for CA ..." % const.KEY_SIZE) click.echo("Generating %d-bit RSA key for CA ..." % const.KEY_SIZE)
public_key, private_key = asymmetric.generate_pair("rsa", bit_size=const.KEY_SIZE) public_key, private_key = asymmetric.generate_pair("rsa", bit_size=const.KEY_SIZE)
names = ( # https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx
("country_name", country),
("state_or_province_name", state),
("locality_name", locality),
("organization_name", organization),
("common_name", title)
)
builder = CertificateBuilder( builder = CertificateBuilder(
dict([(k,v) for (k,v) in names if v]), cn_to_dn("Certidude at %s" % common_name, common_name,
o=organization, ou=organizational_unit),
public_key public_key
) )
builder.self_signed = True builder.self_signed = True
@ -1239,7 +1255,13 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
os.umask(0o177) os.umask(0o177)
with open(ca_key, 'wb') as f: with open(ca_key, 'wb') as f:
f.write(asymmetric.dump_private_key(private_key, None)) f.write(asymmetric.dump_private_key(private_key, None))
sys.exit(0) # stop this fork here sys.exit(0) # stop this fork here
assert os.stat(sqlite_path).st_mode == 0o100640
assert os.stat(ca_cert).st_mode == 0o100640
assert os.stat(ca_key).st_mode == 0o100600
assert os.stat("/etc/nginx/sites-available/certidude.conf").st_mode == 0o100640
else: else:
os.waitpid(bootstrap_pid, 0) os.waitpid(bootstrap_pid, 0)
from certidude import authority from certidude import authority
@ -1322,7 +1344,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
click.echo("y " + path) click.echo("y " + path)
continue continue
click.echo() click.echo()
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white")) click.echo(click.style(common_name, fg="blue") + " " + click.style("%040x" % cert.serial_number, fg="white"))
click.echo("="*(len(common_name)+60)) click.echo("="*(len(common_name)+60))
if signed < NOW and NOW < expires: if signed < NOW and NOW < expires:
@ -1338,15 +1360,15 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
click.echo(" - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native))) click.echo(" - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native)))
if show_revoked: if show_revoked:
for common_name, path, buf, cert, signed, expires, revoked in authority.list_revoked(): for common_name, path, buf, cert, signed, expires, revoked, reason 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.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white")) click.echo(click.style(common_name, fg="blue") + " " + click.style("%040x" % cert.serial_number, fg="white"))
click.echo("="*(len(common_name)+60)) click.echo("="*(len(common_name)+60))
click.echo("Status: " + click.style("revoked", fg="red") + " %s%s" % (naturaltime(NOW-revoked), click.style(", %s" % revoked, fg="white"))) click.echo("Status: " + click.style("revoked", fg="red") + " due to " + reason + " %s%s" % (naturaltime(NOW-revoked), click.style(", %s" % revoked, 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"]: for ext in cert["tbs_certificate"]["extensions"]:
@ -1358,17 +1380,18 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
@click.option("--profile", "-p", default="rw", help="Profile") @click.option("--profile", "-p", default="rw", help="Profile")
@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, profile): def certidude_sign(common_name, overwrite, profile):
from certidude import authority from certidude import authority, config
drop_privileges() drop_privileges()
cert = authority.sign(common_name, overwrite=overwrite, profile=config.PROFILES[profile]) cert = authority.sign(common_name, overwrite=overwrite, profile=config.PROFILES[profile])
@click.command("revoke", help="Revoke certificate") @click.command("revoke", help="Revoke certificate")
@click.option("--reason", "-r", default="key_compromise", help="Revocation reason, one of: key_compromise affiliation_changed superseded cessation_of_operation privilege_withdrawn")
@click.argument("common_name") @click.argument("common_name")
def certidude_revoke(common_name): def certidude_revoke(common_name, reason):
from certidude import authority from certidude import authority
drop_privileges() drop_privileges()
authority.revoke(common_name) authority.revoke(common_name, reason)
@click.command("expire", help="Move expired certificates") @click.command("expire", help="Move expired certificates")
@ -1377,13 +1400,13 @@ def certidude_expire():
threshold = datetime.utcnow() - timedelta(minutes=5) # Kerberos tolerance threshold = datetime.utcnow() - timedelta(minutes=5) # Kerberos tolerance
for common_name, path, buf, cert, signed, expires in authority.list_signed(): for common_name, path, buf, cert, signed, expires in authority.list_signed():
if expires < threshold: if expires < threshold:
expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial_number) expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
click.echo("Moving %s to %s" % (path, expired_path)) click.echo("Moving %s to %s" % (path, expired_path))
os.rename(path, expired_path) os.rename(path, expired_path)
os.remove(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number)) os.remove(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number))
for common_name, path, buf, cert, signed, expires, revoked in authority.list_revoked(): for common_name, path, buf, cert, signed, expires, revoked, reason in authority.list_revoked():
if expires < threshold: if expires < threshold:
expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial_number) expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
click.echo("Moving %s to %s" % (path, expired_path)) click.echo("Moving %s to %s" % (path, expired_path))
os.rename(path, expired_path) os.rename(path, expired_path)
# TODO: Send e-mail # TODO: Send e-mail
@ -1412,7 +1435,7 @@ def certidude_serve(port, listen, fork):
# Rebuild reverse mapping # Rebuild reverse mapping
for cn, path, buf, cert, signed, expires in authority.list_signed(): for cn, path, buf, cert, signed, expires in authority.list_signed():
by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number) by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.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)
@ -1423,14 +1446,6 @@ def certidude_serve(port, listen, fork):
os.makedirs(const.RUN_DIR) os.makedirs(const.RUN_DIR)
os.chmod(const.RUN_DIR, 0o755) os.chmod(const.RUN_DIR, 0o755)
# TODO: umask!
from logging.handlers import RotatingFileHandler
rh = RotatingFileHandler("/var/log/certidude.log", maxBytes=1048576*5, backupCount=5)
rh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
log_handlers.append(rh)
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" %
@ -1440,10 +1455,6 @@ def certidude_serve(port, listen, fork):
click.echo("Request submissions allowed from following subnets: %s" % click.echo("Request submissions allowed from following subnets: %s" %
", ".join([str(j) for j in config.REQUEST_SUBNETS])) ", ".join([str(j) for j in config.REQUEST_SUBNETS]))
logging.basicConfig(
filename=const.SERVER_LOG_PATH,
level=logging.DEBUG)
click.echo("Serving API at %s:%d" % (listen, port)) click.echo("Serving API at %s:%d" % (listen, port))
from wsgiref.simple_server import make_server, WSGIServer from wsgiref.simple_server import make_server, WSGIServer
from certidude.api import certidude_app from certidude.api import certidude_app
@ -1474,7 +1485,6 @@ def certidude_serve(port, listen, fork):
for handler in log_handlers: for handler in log_handlers:
j.addHandler(handler) j.addHandler(handler)
if not fork or not os.fork(): if not fork or not os.fork():
pid = os.getpid() pid = os.getpid()
with open(const.SERVER_PID_PATH, "w") as pidfile: with open(const.SERVER_PID_PATH, "w") as pidfile:

View File

@ -3,6 +3,65 @@ import os
import click import click
import subprocess import subprocess
MAPPING = dict(
common_name="CN",
organizational_unit_name="OU",
organization_name="O",
domain_component="DC"
)
def cert_to_dn(cert):
d = []
for key, value in cert["tbs_certificate"]["subject"].native.items():
if not isinstance(value, list):
value = [value]
for comp in value:
d.append("%s=%s" % (MAPPING[key], comp))
return ", ".join(d)
def cn_to_dn(common_name, namespace, o=None, ou=None):
from asn1crypto.x509 import Name, RelativeDistinguishedName, NameType, DirectoryString, RDNSequence, NameTypeAndValue, UTF8String, DNSName
rdns = []
rdns.append(RelativeDistinguishedName([
NameTypeAndValue({
'type': NameType.map("common_name"),
'value': DirectoryString(
name="utf8_string",
value=UTF8String(common_name))
})
]))
if ou:
rdns.append(RelativeDistinguishedName([
NameTypeAndValue({
'type': NameType.map("organizational_unit_name"),
'value': DirectoryString(
name="utf8_string",
value=UTF8String(ou))
})
]))
if o:
rdns.append(RelativeDistinguishedName([
NameTypeAndValue({
'type': NameType.map("organization_name"),
'value': DirectoryString(
name="utf8_string",
value=UTF8String(o))
})
]))
for dc in namespace.split("."):
rdns.append(RelativeDistinguishedName([
NameTypeAndValue({
'type': NameType.map("domain_component"),
'value': DNSName(value=dc)
})
]))
return Name(name='', value=RDNSequence(rdns))
def selinux_fixup(path): def selinux_fixup(path):
""" """
Fix OpenVPN credential store security context on Fedora Fix OpenVPN credential store security context on Fedora

View File

@ -18,6 +18,7 @@ ACCOUNTS_BACKEND = cp.get("accounts", "backend") # posix, ldap
MAIL_SUFFIX = cp.get("accounts", "mail suffix") MAIL_SUFFIX = cp.get("accounts", "mail suffix")
KERBEROS_KEYTAB = cp.get("authentication", "kerberos keytab") KERBEROS_KEYTAB = cp.get("authentication", "kerberos keytab")
KERBEROS_REALM = cp.get("authentication", "kerberos realm")
LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri") LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri")
LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache") LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache")
LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri") LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri")
@ -39,6 +40,10 @@ CRL_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "crl subnets").split(" ") if j]) cp.get("authorization", "crl subnets").split(" ") if j])
RENEWAL_SUBNETS = set([ipaddress.ip_network(j) for j in RENEWAL_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "renewal subnets").split(" ") if j]) cp.get("authorization", "renewal subnets").split(" ") if j])
OVERWRITE_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "overwrite subnets").split(" ") if j])
MACHINE_ENROLLMENT_SUBNETS = set([ipaddress.ip_network(j) for j in
cp.get("authorization", "machine enrollment subnets").split(" ") if j])
AUTHORITY_DIR = "/var/lib/certidude" AUTHORITY_DIR = "/var/lib/certidude"
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
@ -54,9 +59,6 @@ MAILER_ADDRESS = cp.get("mailer", "address")
BOOTSTRAP_TEMPLATE = cp.get("bootstrap", "services template") BOOTSTRAP_TEMPLATE = cp.get("bootstrap", "services template")
MACHINE_ENROLLMENT_ALLOWED = {
"forbidden": False, "allowed": True }[
cp.get("authority", "machine enrollment")]
USER_ENROLLMENT_ALLOWED = { USER_ENROLLMENT_ALLOWED = {
"forbidden": False, "single allowed": True, "multiple allowed": True }[ "forbidden": False, "single allowed": True, "multiple allowed": True }[
cp.get("authority", "user enrollment")] cp.get("authority", "user enrollment")]
@ -117,3 +119,6 @@ cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r"))
IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()] IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()]
TOKEN_OVERWRITE_PERMITTED=True TOKEN_OVERWRITE_PERMITTED=True
SERVICE_PROTOCOLS = set([j.lower() for j in cp.get("service", "protocols").split(" ") if j])
SERVICE_ROUTERS = cp.get("service", "routers")

View File

@ -6,9 +6,9 @@ import sys
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
CURVE_NAME = "secp384r1" CURVE_NAME = "secp384r1"
RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])?$" RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$"
RE_HOSTNAME = "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$" RE_HOSTNAME = "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$"
RE_COMMON_NAME = "^[A-Za-z0-9\-\.\@]+$" RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$"
RUN_DIR = "/run/certidude" RUN_DIR = "/run/certidude"
CONFIG_DIR = "/etc/certidude" CONFIG_DIR = "/etc/certidude"
@ -25,6 +25,8 @@ try:
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
except socket.gaierror: except socket.gaierror:
FQDN = socket.gethostname() FQDN = socket.gethostname()
if hasattr(FQDN, "decode"): # Keep client backwards compatible with Python 2.x
FQDN = FQDN.decode("ascii")
try: try:
HOSTNAME, DOMAIN = FQDN.split(".", 1) HOSTNAME, DOMAIN = FQDN.split(".", 1)

View File

@ -56,8 +56,8 @@
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<a href="http://github.com/laurivosandi/certidude">Certidude</a> by <a href="https://github.com/laurivosandi/certidude">Certidude</a> by
<a href="http://github.com/laurivosandi/">Lauri Võsandi</a> <a href="https://github.com/laurivosandi/">Lauri Võsandi</a>
</div> </div>
</footer> </footer>
</body> </body>

View File

@ -305,7 +305,8 @@ function loadAuthority() {
/** /**
* Render authority views * Render authority views
**/ **/
$("#view").html(env.render('views/authority.html', { session: session, window: window })); $("#view").html(env.render('views/authority.html', { session: session, window: window,
authority_name: window.location.hostname }));
$("time").timeago(); $("time").timeago();
if (session.authority) { if (session.authority) {
$("#log input").each(function(i, e) { $("#log input").each(function(i, e) {
@ -462,12 +463,8 @@ function datetimeFilter(s) {
} }
function serialFilter(s) { function serialFilter(s) {
return s.substring(0,8) + " " + return s.substring(0,s.length-14) + " " +
s.substring(8,12) + " " + s.substring(s.length-14);
s.substring(12,16) + " " +
s.substring(16,28) + " " +
s.substring(28,32) + " " +
s.substring(32);
} }
$(document).ready(function() { $(document).ready(function() {

View File

@ -7,26 +7,25 @@
</div> </div>
<form action="/api/request/" method="post"> <form action="/api/request/" method="post">
<div class="modal-body"> <div class="modal-body">
<h5>Certidude client</h5> {% if "ikev2" in session.service.protocols %}
<h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5>
<p>Submit a certificate signing request from Mac OS X, Ubuntu or Fedora:</p>
<div class="highlight">
<pre><code>easy_install pip;
pip3 install certidude;
certidude bootstrap {{ window.location.hostname }}
</code></pre>
</div>
<h5>Windows 10</h5>
<p>On Windows execute following PowerShell script</p> <p>On Windows execute following PowerShell script</p>
<div class="highlight"> <div class="highlight">
<pre class="code"><code>$hostname = $env:computername.ToLower() <pre class="code"><code># Install CA certificate
$templ = @" @"
[Version] {{ session.authority.certificate.blob }}
Signature="$Windows NT$ "@ | Out-File ca_cert.pem
{% if session.authority.certificate.algorithm == "ec" %}
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
{% else %}
C:\Windows\system32\certutil.exe -addstore Root ca_cert.pem
{% endif %}
# Generate keypair and submit CSR
$hostname = $env:computername.ToLower()
@"
[NewRequest] [NewRequest]
Subject = "CN=$hostname" Subject = "CN=$hostname"
Exportable = FALSE Exportable = FALSE
@ -39,21 +38,14 @@ RequestType = PKCS10
KeyAlgorithm = ECDSA_P384 KeyAlgorithm = ECDSA_P384
{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider" {% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
KeyLength = 2048 KeyLength = 2048
{% endif %}"@ {% endif %}"@ | Out-File req.inf
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem
$templ | Out-File req.inf Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ authority_name }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
# Fetch CA certificate and install it
Invoke-WebRequest -Uri http://{{ window.location.hostname }}/api/certificate -OutFile ca_cert.pem
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
# Generate keypair and submit CSR
C:\Windows\system32\certreq.exe -new -f -q req.inf client_csr.pem
Invoke-WebRequest -TimeoutSec 900 -Uri http://{{ window.location.hostname }}/api/request/?wait=1 -InFile client_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile client_cert.pem
# Import certificate # Import certificate
Import-Certificate -FilePath client_cert.pem -CertStoreLocation Cert:\LocalMachine\My {% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My
{% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem
{% endif %}
# Set up IPSec VPN tunnel # Set up IPSec VPN tunnel
Remove-VpnConnection -AllUserConnection -Force k-space Remove-VpnConnection -AllUserConnection -Force k-space
Add-VpnConnection ` Add-VpnConnection `
@ -84,21 +76,137 @@ IntegrityCheckMethod - IKE hash algorithm, one of: MD5 SHA196 SHA256 SHA384
DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24 DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24
PfsGroup = one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24 PfsGroup = one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24
--> -->
{% endif %}
<h5>UNIX & UNIX-like</h5> <h5>UNIX & UNIX-like</h5>
<p>On other UNIX-like machines generate key pair and submit the signing request using OpenSSL and cURL:</p> <p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
<div class="highlight"> <div class="highlight">
<pre class="code"><code>NAME=$(hostname); <pre class="code"><code>test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname)
{% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout -out client_key.pem{% else %}openssl genrsa -out client_key.pem 2048;{% endif %} test -e /bin/hostname && NAME=$(hostname)
openssl req -new -sha384 -key client_key.pem -out client_req.pem -subj "/CN=$NAME"; test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
curl -f -L -H "Content-type: application/pkcs10" --data-binary @client_req.pem \
http://{{ window.location.hostname }}/api/request/?wait=yes > client_cert.pem</code></pre> mkdir -p /etc/certidude/authority/{{ authority_name }}/
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
{{ session.authority.certificate.blob }}EOF
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|| openssl req -new -sha384 -subj "/CN=$NAME" \
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
echo "If CSR submission fails, you can copy paste it to Certidude:"
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem
test -e /etc/pki/ca-trust/source/anchors \
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ authority_name }} \
&& update-ca-trust
test -e /usr/local/share/ca-certificates/ \
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /usr/local/share/ca-certificates/{{ authority_name }}.crt \
&& update-ca-certificates
curl -f -L -H "Content-type: application/pkcs10" \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'http://{{ authority_name }}/api/request/?wait=yes&autosign=yes'
</code></pre>
</div> </div>
<p>For server certificates use fully qualified hostname as common name and sign request accordingly:</p>
<div class="highlight">
<pre class="code"><code>test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
test -e /bin/hostname && NAME=$(hostname -f)
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
mkdir -p /etc/certidude/authority/{{ authority_name }}/
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
{{ session.authority.certificate.blob }}EOF
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|| openssl req -new -sha384 -subj "/CN=$NAME" \
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
echo "If CSR submission fails, you can copy paste it to Certidude:"
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem
curl -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'https://{{ authority_name }}:8443/api/request/?wait=yes'
</code></pre>
</div>
<p>To renew:</p>
<div class="highlight">
<pre class="code"><code>curl -f -L -H "Content-type: application/pkcs10" \
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
--key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
--cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
'https://{{ authority_name }}:8443/api/request/?wait=yes'
</code></pre>
</div>
{% if "openvpn" in session.service.protocols %}
<h5>OpenVPN as client</h5>
<p>First acquire certificates using the snippet above.</p>
<p>Then install software:</p>
<div class="highlight">
<pre class="code"><code># Install packages on Ubuntu & Fedora
which apt && apt install openvpn
which dnf && dnf install openvpn
cat > /etc/openvpn/{{ authority_name }}.conf << EOF
client
nobind
{% for router in session.service.routers %}
remote {{ router }} 1194 udp
remote {{ router }} 443 tcp-client
{% endfor %}
tls-version-min 1.2
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
cipher AES-128-GCM
auth SHA384
mute-replay-warnings
reneg-sec 0
remote-cert-tls server
dev tun
persist-tun
persist-key
ca /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
key /etc/certidude/authority/{{ authority_name }}/host_key.pem
cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem
EOF
systemctl restart openvpn
</code></pre>
</div>
{% endif %}
{% if "ikev2" in session.service.protocols %}
<h5>StrongSwan as client</h5> <h5>StrongSwan as client</h5>
<p>First enroll certificates:</p> <p>First acquire certificates using the snippet above.</p>
<p>Then install software:</p>
<div class="highlight"> <div class="highlight">
<pre class="code"><code># Install packages on Ubuntu & Fedora, patch Fedora paths <pre class="code"><code># Install packages on Ubuntu & Fedora, patch Fedora paths
which apt && apt install strongswan which apt && apt install strongswan
@ -107,82 +215,68 @@ test -e /etc/strongswan && test -e /etc/ipsec.conf || ln -s strongswan/ipsec.con
test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
FQDN=$(cat /etc/hostname) # Hard link files to prevent Apparmor issues and have more manageable config
ln /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority_name }}.pem
# Install CA certificate ln /etc/certidude/authority/{{ authority_name }}/host_cert.pem /etc/ipsec.d/certs/{{ authority_name }}.pem
cat << EOF > /etc/ipsec.d/cacerts/ca_cert.pem ln /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem
{{ session.authority.certificate.blob }}EOF </code></pre>
# Generate keypair
test -e /etc/ipsec.d/private/client.pem \
|| openssl {% if session.authority.certificate.algorithm == "ec" %}ecparam -name secp384r1 -genkey -noout -out /etc/ipsec.d/private/client.pem{% else %}genrsa -out /etc/ipsec.d/private/client.pem 2048{% endif %}
# Attempt to submit CSR
test -e /etc/ipsec.d/reqs/client.pem \
|| openssl req -new -sha384 \
-key /etc/ipsec.d/private/client.pem \
-out /etc/ipsec.d/reqs/client.pem -subj "/CN=$FQDN"
cat /etc/ipsec.d/reqs/client.pem
curl -f -L -H "Content-type: application/pkcs10" \
--data-binary @/etc/ipsec.d/reqs/client.pem \
-o /etc/ipsec.d/certs/client.pem \
http://{{ window.location.hostname }}/api/request/?wait=yes</code></pre>
</div> </div>
<p>To configure StrongSwan as roadwarrior:</p> <p>To configure StrongSwan as roadwarrior:</p>
<div class="highlight"> <div class="highlight">
<pre class="code"><code>cat > /etc/ipsec.conf << EOF <pre class="code"><code>cat > /etc/ipsec.conf << EOF
conn c2s
ca {{ authority_name }}
auto=add
cacert = {{ authority_name }}.pem
{% if session.features.crl %} crluri = http://{{ authority_name }}/api/revoked/{% endif %}
{% if session.features.ocsp %} ocspuri = http://{{ authority_name }}/api/ocsp/{% endif %}
conn client-to-site
auto=start auto=start
right=router2.k-space.ee right={{ session.service.routers[0] }}
rightsubnet=0.0.0.0/0
rightca="{{ session.authority.certificate.distinguished_name }}"
left=%defaultroute
leftcert={{ authority_name }}.pem
leftsourceip=%config
leftca="{{ session.authority.certificate.distinguished_name }}"
keyexchange=ikev2
keyingtries=%forever
dpdaction=restart dpdaction=restart
closeaction=restart closeaction=restart
left=%defaultroute
rightsubnet=0.0.0.0/0
keyingtries=%forever
rightid=%any
leftsourceip=%config
leftcert=client.pem
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
esp=aes128gcm16-aes128gmac! esp=aes128gcm16-aes128gmac!
EOF EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} client.pem" > /etc/ipsec.secrets echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets
ipsec restart</code></pre> ipsec restart</code></pre>
</div> </div>
{% endif %}
<h5>OpenWrt/LEDE as VPN gateway</h5> <h5>OpenWrt/LEDE as VPN gateway</h5>
<p>First enroll certificates:</p> <p>First enroll certificates using the snippet from UNIX section above</p>
<p>Then:</p>
<div class="highlight"> <div class="highlight">
<pre class="code"><code>opkg install curl libmbedtls <pre class="code"><code>opkg install curl libmbedtls
# Derive FQDN from WAN interface's reverse DNS record # Derive FQDN from WAN interface's reverse DNS record
FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs) FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
mkdir -p /etc/certidude/authority/{{ window.location.hostname }}
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
cat << EOF > /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem
{{ session.authority.certificate.blob }}EOF
test -e /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
|| openssl {% if session.authority.certificate.algorithm == "ec" %}ecparam -name secp384r1 -genkey -noout -out /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem{% else %}genrsa -out /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem 2048{% endif %}
test -e /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
|| openssl req -new -sha384 \
-key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
-out /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem -subj "/CN=$FQDN"
cat /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem
curl -f -L -H "Content-type: application/pkcs10" \
--data-binary @/etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
-o /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem \
http://{{ window.location.hostname }}/api/request/?wait=yes
# Create VPN gateway up/down script for reporting client IP addresses to CA # Create VPN gateway up/down script for reporting client IP addresses to CA
cat <<\EOF > /etc/certidude/authority/{{ window.location.hostname }}/updown cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown
#!/bin/sh #!/bin/sh
CURL="curl -m 3 -f --key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem --cert /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem --cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem https://{{ window.location.hostname }}:8443/api/lease/" CURL="curl -m 3 -f --key /etc/certidude/authority/{{ authority_name }}/host_key.pem --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem https://{{ authority_name }}:8443/api/lease/"
case $PLUTO_VERB in case $PLUTO_VERB in
up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;; up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;;
@ -195,19 +289,23 @@ case $script_type in
esac esac
EOF EOF
chmod +x /etc/certidude/authority/{{ window.location.hostname }}/updown chmod +x /etc/certidude/authority/{{ authority_name }}/updown
</code></pre> </code></pre>
</div> </div>
{% if "openvpn" in session.service.protocols %}
<p>Then either set up OpenVPN service:</p> <p>Then either set up OpenVPN service:</p>
<div class="highlight"> <div class="highlight">
<pre class="code"><code>opkg update <pre class="code"><code>opkg update
opkg install curl openssl-util openvpn-openssl opkg install curl openssl-util openvpn-openssl
{% if session.authority.certificate.algorithm != "ec" %}
# Generate Diffie-Hellman parameters file for OpenVPN # Generate Diffie-Hellman parameters file for OpenVPN
test -e /etc/certidude/dh.pem \ test -e /etc/certidude/dh.pem \
|| openssl dhparam 2048 -out /etc/certidude/dh.pem || openssl dhparam 2048 -out /etc/certidude/dh.pem
{% endif %}
# Create interface definition for tunnel # Create interface definition for tunnel
uci set network.vpn=interface uci set network.vpn=interface
uci set network.vpn.name='vpn' uci set network.vpn.name='vpn'
@ -267,10 +365,10 @@ for section in s2c_tcp s2c_udp; do
# Common paths # Common paths
uci set openvpn.$section.script_security=2 uci set openvpn.$section.script_security=2
uci set openvpn.$section.client_connect='/etc/certidude/updown' uci set openvpn.$section.client_connect='/etc/certidude/updown'
uci set openvpn.$section.key='/etc/certidude/authority/{{ window.location.hostname }}/server_key.pem' uci set openvpn.$section.key='/etc/certidude/authority/{{ authority_name }}/host_key.pem'
uci set openvpn.$section.cert='/etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem' uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem'
uci set openvpn.$section.ca='/etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem' uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem'
uci set openvpn.$section.dh='/etc/certidude/dh.pem' {% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %}
uci set openvpn.$section.enabled=1 uci set openvpn.$section.enabled=1
# DNS and routes # DNS and routes
@ -291,6 +389,9 @@ done
/etc/init.d/firewall restart</code></pre> /etc/init.d/firewall restart</code></pre>
</div> </div>
{% endif %}
{% if "ikev2" in session.service.protocols %}
<p>Alternatively or additionally set up StrongSwan:</p> <p>Alternatively or additionally set up StrongSwan:</p>
<div class="highlight"> <div class="highlight">
<pre class="code"><code>opkg update <pre class="code"><code>opkg update
@ -302,31 +403,46 @@ config setup
strictcrlpolicy=yes strictcrlpolicy=yes
uniqueids = yes uniqueids = yes
ca {{ window.location.hostname }} ca {{ authority_name }}
auto=add auto=add
cacert = /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem cacert = {{ authority_name }}.pem
{% if session.features.crl %} crluri = http://{{ window.location.hostname }}/api/revoked/{% endif %} {% if session.features.crl %} crluri = http://{{ authority_name }}/api/revoked/{% endif %}
{% if session.features.ocsp %} ocspuri = http://{{ window.location.hostname }}/api/ocsp/{% endif %} {% if session.features.ocsp %} ocspuri = http://{{ authority_name }}/api/ocsp/{% endif %}
conn s2c conn default-{{ authority_name }}
auto=add
dpdaction=clear
closeaction=clear
leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
rightsourceip=172.21.0.0/24 # Roadwarrior virtual IP pool
left=$(uci get network.wan.ipaddr) # Bind to this IP address
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors
leftcert=/etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
esp=aes128gcm16-aes128gmac! esp=aes128gcm16-aes128gmac!
leftupdown=/etc/certidude/authority/{{ window.location.hostname }}/updown left=$(uci get network.wan.ipaddr) # Bind to this IP address
leftid={{ session.service.routers | first }}
leftupdown=/etc/certidude/authority/{{ authority_name }}/updown
leftcert={{ authority_name }}.pem
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors
leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
leftca="{{ session.authority.certificate.distinguished_name }}"
rightca="{{ session.authority.certificate.distinguished_name }}"
rightsourceip=172.21.0.0/24 # Roadwarrior virtual IP pool
dpddelay=0
dpdaction=clear
conn site-to-clients
auto=add
also=default-{{ authority_name }}
conn site-to-client1
auto=ignore
also=default-{{ authority_name }}
rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*"
rightsourceip=172.21.0.1
EOF EOF
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem" > /etc/ipsec.secrets echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets
ipsec restart</code></pre> ipsec restart</code></pre>
</div> </div>
{% endif %}
{% if session.authority.builder %} {% if session.authority.builder %}
<h5>OpenWrt/LEDE image builder</h5> <h5>OpenWrt/LEDE image builder</h5>
@ -339,7 +455,7 @@ ipsec restart</code></pre>
{% endif %} {% endif %}
<h5>SCEP</h5> <h5>SCEP</h5>
<p>Use following as the enrollment URL: http://{{ window.location.hostname }}/cgi-bin/pkiclient.exe</p> <p>Use following as the enrollment URL: http://{{ authority_name }}/cgi-bin/pkiclient.exe</p>
<h5>Copy & paste</h5> <h5>Copy & paste</h5>
@ -367,10 +483,10 @@ ipsec restart</code></pre>
<h4 class="modal-title">Revocation lists</h4> <h4 class="modal-title">Revocation lists</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>To fetch <a href="http://{{window.location.hostname}}/api/revoked/">certificate revocation list</a>:</p> <p>To fetch <a href="http://{{authority_name}}/api/revoked/">certificate revocation list</a>:</p>
<pre><code>curl http://{{window.location.hostname}}/api/revoked/ > crl.der <pre><code>curl http://{{authority_name}}/api/revoked/ > crl.der
curl http://{{window.location.hostname}}/api/revoked/ -L -H "Accept: application/x-pem-file" curl http://{{authority_name}}/api/revoked/ -L -H "Accept: application/x-pem-file"
curl http://{{window.location.hostname}}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre> curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn" data-dismiss="modal">Close</button> <button type="button" class="btn" data-dismiss="modal">Close</button>

View File

@ -4,5 +4,5 @@ Last seen
at at
<a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %} <a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %}
from from
<a target="{{ certificate.lease.outer_address }}" href="http://geoiplookup.net/ip/{{ certificate.lease.outer_address }}">{{ certificate.lease.outer_address }}</a>{% endif %}. <a target="{{ certificate.lease.outer_address }}" href="https://geoiplookup.net/ip/{{ certificate.lease.outer_address }}">{{ certificate.lease.outer_address }}</a>{% endif %}.

View File

@ -5,7 +5,7 @@
<i class="fa fa-folder" aria-hidden="true"></i> <i class="fa fa-folder" aria-hidden="true"></i>
{{ certificate.organizational_unit }} / {{ certificate.organizational_unit }} /
{% endif %} {% endif %}
{% if certificate.server %} {% if certificate.extensions.extended_key_usage and "server_auth" in certificate.extensions.extended_key_usage %}
<i class="fa fa-server"></i> <i class="fa fa-server"></i>
{% else %} {% else %}
<i class="fa fa-laptop"></i> <i class="fa fa-laptop"></i>
@ -46,11 +46,11 @@
</button> </button>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a class="dropdown-item" href="#" <a class="dropdown-item" href="#"
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=1',type:'delete'});">Revoke due to key compromise</a> onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=key_compromise',type:'delete'});">Revoke due to key compromise</a>
<a class="dropdown-item" href="#" <a class="dropdown-item" href="#"
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=5',type:'delete'});">Revoke due to cessation of operation</a> onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=cessation_of_operation',type:'delete'});">Revoke due to cessation of operation</a>
<a class="dropdown-item" href="#" <a class="dropdown-item" href="#"
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=9',type:'delete'});">Revoke due to withdrawn privilege</a> onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=privilege_withdrawn',type:'delete'});">Revoke due to withdrawn privilege</a>
</div> </div>
</div> </div>
@ -88,8 +88,10 @@ openssl ocsp -issuer session.pem -CAfile session.pem \
{% endif %} {% endif %}
<p>To fetch script:</p> <p>To fetch script:</p>
<pre><code class="language-bash" data-lang="bash">cd /var/lib/certidude/{{ window.location.hostname }}/ <pre><code class="language-bash" data-lang="bash">curl https://{{ window.location.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/ \
curl --cert client_cert.pem https://{{ window.location.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/</pre></code> --cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem \
--key /etc/certidude/authority/{{ window.location.hostname }}/host_key.pem \
--cert /etc/certidude/authority/{{ window.location.hostname }}/host_cert.pem</pre></code>
<div style="overflow: auto; max-width: 100%;"> <div style="overflow: auto; max-width: 100%;">
<table class="table" id="signed_certificates"> <table class="table" id="signed_certificates">
@ -110,6 +112,9 @@ curl --cert client_cert.pem https://{{ window.location.hostname }}:8443/api/sign
<tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr> <tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr>
--> -->
<tr><th>SHA256</th><td style="word-wrap:break-word; overflow-wrap: break-word; ">{{ certificate.sha256sum }}</td></tr> <tr><th>SHA256</th><td style="word-wrap:break-word; overflow-wrap: break-word; ">{{ certificate.sha256sum }}</td></tr>
{% if certificate.extensions.extended_key_usage %}
<tr><th>Extended key usage</th><td>{{ certificate.extensions.extended_key_usage | join(", ") }}</td></tr>
{% endif %}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -12,12 +12,28 @@
# No tags # No tags
{% endif %} {% endif %}
# Submit some stats to CA ARGS="kernel=$(uname -sr)&\
curl http://{{ authority_name }}/api/signed/{{ common_name }}/attr -X POST -d "\
dmi.product_name=$(cat /sys/class/dmi/id/product_name)&\
dmi.product_serial=$(cat /sys/class/dmi/id/product_serial)&\
kernel=$(uname -sr)&\
dist=$(lsb_release -si) $(lsb_release -sr)&\
cpu=$(cat /proc/cpuinfo | grep '^model name' | head -n1 | cut -d ":" -f2 | xargs)&\ cpu=$(cat /proc/cpuinfo | grep '^model name' | head -n1 | cut -d ":" -f2 | xargs)&\
mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB&\ $(for j in /sys/class/net/[we]*[a-z][0-9]; do echo -en if.$(basename $j).ether=$(cat $j/address)\&; done)"
$(for j in /sys/class/net/[we]*; do echo -en if.$(basename $j).ether=$(cat $j/address)\&; done)"
if [ -e /etc/openwrt_release ]; then
. /etc/openwrt_release
ARGS="$ARGS&dist=$DISTRIB_ID $DISTRIB_RELEASE"
else
ARGS="$ARGS&dist=$(lsb_release -si) $(lsb_release -sr)"
fi
if [ -e /sys/class/dmi ]; then
ARGS="$ARGS&dmi.product_name=$(cat /sys/class/dmi/id/product_name)&dmi.product_serial=$(cat /sys/class/dmi/id/product_serial)"
ARGS="$ARGS&&mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB"
else
ARGS="$ARGS&dmi.product_name=$(cat /proc/cpuinfo | grep '^machine' | head -n 1 | cut -d ":" -f 2 | xargs)"
ARGS="$ARGS&mem=$(echo $(cat /proc/meminfo | grep MemTotal | cut -d ":" -f 2 | xargs | cut -d " " -f 1)/1000+1 | bc) MB"
fi
# Submit some stats to CA
curl https://{{ authority_name }}:8443/api/signed/{{ common_name }}/attr \
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
--key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
--cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
-X POST \-d "$ARGS"

View File

@ -1,15 +1,20 @@
[DEFAULT]
# Path to filesystem overlay used
overlay = {{ doc_path }}/overlay
# Hostname or regex to match the IPSec gateway included in the image
router = ^router\d?\.
# Site specific script to be copied to /etc/uci-defaults/99-site-script
# use it to include SSH keys, set passwords, etc
script =
[tpl-archer-c7] [tpl-archer-c7]
# Title shown in the UI # Title shown in the UI
title = TP-Link Archer C7 (Access Point) title = TP-Link Archer C7 (Access Point)
# Script to build the image, copy file to /etc/certidude/ and make modifications as necessary # Script to build the image, copy file to /etc/certidude/ and make modifications as necessary
command = {{ doc_path }}/build-ap.sh command = {{ doc_path }}/builder/ap.sh
# Path to filesystem overlay, used
overlay = {{ doc_path }}/overlay
# Site specific script to be copied to /etc/uci-defaults/99-site-script
script =
# Device/model/profile selection # Device/model/profile selection
model = archer-c7-v2 model = archer-c7-v2
@ -22,10 +27,21 @@ rename = ArcherC7v2_tp_recovery.bin
[cf-e380ac] [cf-e380ac]
title = Comfast E380AC (Access Point) title = Comfast E380AC (Access Point)
command = {{ doc_path }}/build-ap.sh command = {{ doc_path }}/builder/ap.sh
overlay = {{ doc_path }}/overlay
script =
model = cf-e380ac-v2 model = cf-e380ac-v2
filename = cf-e380ac-v2-squashfs-factory.bin filename = cf-e380ac-v2-squashfs-factory.bin
rename = firmware_auto.bin rename = firmware_auto.bin
[ar150-mfp]
title = GL.iNet GL-AR150 (MFP)
command = {{ doc_path }}/builder/mfp.sh
model = gl-ar150
filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin
rename = mfp-gl-ar150-squashfs-sysupgrade.bin
[ar150-cam]
title = GL.iNet GL-AR150 (IP Camera)
command = {{ doc_path }}/builder/ipcam.sh
model = gl-ar150
filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin
rename = cam-gl-ar150-squashfs-sysupgrade.bin

View File

@ -138,8 +138,9 @@ server {
server_name {{ common_name }}; server_name {{ common_name }};
listen 8443 ssl http2; listen 8443 ssl http2;
# Require client authentication with certificate # Allow client authentication with certificate,
ssl_verify_client on; # backend must still check if certificate was used for TLS handshake
ssl_verify_client optional;
ssl_client_certificate /var/lib/certidude/{{ common_name }}/ca_cert.pem; ssl_client_certificate /var/lib/certidude/{{ common_name }}/ca_cert.pem;
# Proxy pass to backend # Proxy pass to backend

View File

@ -22,31 +22,37 @@ extended key usage = client_auth
[srv] [srv]
title = Server title = Server
;ou = Server ou = Server
common name = RE_FQDN common name = RE_FQDN
lifetime = 365 lifetime = 120
extended key usage = server_auth extended key usage = server_auth client_auth
[gw] [gw]
title = Gateway title = Gateway
ou = Gateway ou = Gateway
common name = RE_FQDN common name = RE_FQDN
renewable = true renewable = true
lifetime = 30 lifetime = 120
extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth
[ap] [ap]
title = Access Point title = Access Point
ou = Access Point ou = Access Point
common name = RE_HOSTNAME common name = RE_HOSTNAME
lifetime = 1825 lifetime = 120
extended key usage = client_auth extended key usage = client_auth
[mfp] [mfp]
title = Printers title = Printers
ou = Printers ou = MFP
common name = ^mfp\- common name = ^mfp\-
lifetime = 30 lifetime = 120
extended key usage = client_auth extended key usage = client_auth
[cam]
title = Camera
ou = IP Camera
common name = ^cam\-
lifetime = 120
extended key usage = client_auth

View File

@ -4,13 +4,31 @@
# sshd PAM service. In case of 'kerberos' SPNEGO is used to authenticate # sshd PAM service. In case of 'kerberos' SPNEGO is used to authenticate
# user against eg. Active Directory or Samba4. # user against eg. Active Directory or Samba4.
{% if realm %}
;backends = pam
backends = kerberos
{% else %}
backends = pam backends = pam
;backends = kerberos ;backends = kerberos
{% endif %}
;backends = ldap ;backends = ldap
;backends = kerberos ldap
;backends = kerberos pam
ldap uri = ldaps://dc.example.lan
kerberos keytab = FILE:{{ kerberos_keytab }} kerberos keytab = FILE:{{ kerberos_keytab }}
{% if realm %}
# Kerberos realm derived from /etc/samba/smb.conf
kerberos realm = {{ realm }}
{% else %}
# Kerberos realm
kerberos realm = EXAMPLE.LAN
{% endif %}
{% if domain %}
# LDAP URI derived from /etc/samba/smb.conf
ldap uri = ldap://dc1.{{ domain }}
{% else %}
# LDAP URI
ldap uri = ldaps://dc1.example.lan
{% endif %}
[accounts] [accounts]
# The accounts backend specifies how the user's given name, surname and e-mail # The accounts backend specifies how the user's given name, surname and e-mail
@ -20,27 +38,63 @@ kerberos keytab = FILE:{{ kerberos_keytab }}
# If certidude setup authority was performed correctly the credential cache should be # If certidude setup authority was performed correctly the credential cache should be
# updated automatically by /etc/cron.hourly/certidude # updated automatically by /etc/cron.hourly/certidude
{% if not realm %}
backend = posix backend = posix
{% else %}
;backend = posix
{% endif %}
mail suffix = example.lan mail suffix = example.lan
{% if realm %}
backend = ldap
{% else %}
;backend = ldap ;backend = ldap
{% endif %}
ldap gssapi credential cache = /run/certidude/krb5cc ldap gssapi credential cache = /run/certidude/krb5cc
ldap uri = ldap://dc.example.lan
ldap base = {% if base %}{{ base }}{% else %}dc=example,dc=lan{% endif %} {% if domain %}
# LDAP URI derived from /etc/samba/smb.conf
ldap uri = ldap://dc1.{{ domain }}
{% else %}
# LDAP URI
ldap uri = ldaps://dc1.example.lan
{% endif %}
{% if base %}
# LDAP base derived from /etc/samba/smb.conf
ldap base = {{ base }}
{% else %}
ldap base = dc=example,dc=lan
{% endif %}
[authorization] [authorization]
# The authorization backend specifies how the users are authorized. # The authorization backend specifies how the users are authorized.
# In case of 'posix' simply group membership is asserted, # In case of 'posix' simply group membership is asserted,
# in case of 'ldap' search filter with username as placeholder is applied. # in case of 'ldap' search filter with username as placeholder is applied.
{% if realm %}
;backend = posix
{% else %}
backend = posix backend = posix
{% endif %}
posix user group = users posix user group = users
posix admin group = sudo posix admin group = sudo
{% if realm %}
backend = ldap
{% else %}
;backend = ldap ;backend = ldap
{% endif %}
ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s)) ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s))
ldap user filter = (&(objectclass=user)(objectcategory=person)(samaccountname=%s)) ldap user filter = (&(objectclass=user)(objectcategory=person)(samaccountname=%s))
ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,{% if base %}{{ base }}{% else %}dc=example,dc=lan{% endif %})(samaccountname=%s)) {% if base %}
# LDAP user filter for administrative accounts, derived from /etc/samba/smb.conf
ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,{{ base }})(samaccountname=%s))
{% else %}
# LDAP user filter for administrative accounts
ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,dc=example,dc=lan)(samaccountname=%s))
{% endif %}
;ldap admin filter = (&(samaccountname=lauri)(samaccountname=%s))
;backend = whitelist ;backend = whitelist
user whitelist = user whitelist =
@ -62,9 +116,9 @@ autosign subnets = 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
scep subnets = scep subnets =
;scep subnets = 0.0.0.0/0 ;scep subnets = 0.0.0.0/0
# Online Certificate Status Protocol enabled subnets # Online Certificate Status Protocol enabled subnets, anywhere by default
ocsp subnets = ;ocsp subnets =
;ocsp subnets = 0.0.0.0/0 ocsp subnets = 0.0.0.0/0
# Certificate Revocation lists can be accessed from anywhere by default # Certificate Revocation lists can be accessed from anywhere by default
;crl subnets = ;crl subnets =
@ -76,12 +130,24 @@ crl subnets = 0.0.0.0/0
renewal subnets = renewal subnets =
;renewal subnets = 0.0.0.0/0 ;renewal subnets = 0.0.0.0/0
# From which subnets autosign and SCEP requests are allowed to overwrite
# already existing certificate with same CN
overwrite subnets =
;overwrite subnets = 0.0.0.0/0
# Source subnets of Kerberos authenticated machines which are automatically
# allowed to enroll with CSR whose common name is set to machine's account name.
# Note that overwriting is not allowed by default, see 'overwrite subnets'
# option above
machine enrollment subnets =
;machine enrollment subnets = 0.0.0.0/0
[logging] [logging]
# Disable logging # Disable logging
;backend = backend =
# Use SQLite backend # Use SQLite backend
backend = sql ;backend = sql
database = sqlite://{{ directory }}/meta/db.sqlite database = sqlite://{{ directory }}/meta/db.sqlite
[signature] [signature]
@ -144,13 +210,6 @@ request submission allowed = false
;user enrollment = single allowed ;user enrollment = single allowed
user enrollment = multiple allowed user enrollment = multiple allowed
# Machine certificate enrollment specifies whether Kerberos authenticated
# machines are allowed to automatically enroll with certificate where
# common name is set to machine's account name
machine enrollment = forbidden
;machine enrollment = allowed
private key path = {{ ca_key }} private key path = {{ ca_key }}
certificate path = {{ ca_cert }} certificate path = {{ ca_cert }}
@ -199,3 +258,7 @@ secret = {{ token_secret }}
path = {{ script_dir }} path = {{ script_dir }}
;path = /etc/certidude/script ;path = /etc/certidude/script
;path = ;path =
[service]
protocols = ikev2 https openvpn
routers = ^router\d?\.

View File

@ -1,58 +0,0 @@
#!/bin/bash
set -e
set -x
umask 022
VERSION=17.01.4
BASENAME=lede-imagebuilder-$VERSION-ar71xx-generic.Linux-x86_64
FILENAME=$BASENAME.tar.xz
URL=http://downloads.lede-project.org/releases/$VERSION/targets/ar71xx/generic/$FILENAME
PACKAGES="luci luci-app-commands \
collectd collectd-mod-conntrack collectd-mod-interface \
collectd-mod-iwinfo collectd-mod-load collectd-mod-memory \
collectd-mod-network collectd-mod-protocols collectd-mod-tcpconns \
collectd-mod-uptime \
openssl-util openvpn-openssl curl ca-certificates \
htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \
-luci-app-firewall \
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6"
if [ ! -e $FILENAME ]; then
wget -q $URL
fi
if [ ! -e $BASENAME ]; then
tar xf $FILENAME
fi
cd $BASENAME
# Copy CA certificate
AUTHORITY=$(hostname -f)
CERTIDUDE_DIR=/var/lib/certidude/$AUTHORITY
if [ -d "$CERTIDUDE_DIR" ]; then
mkdir -p overlay/$CERTIDUDE_DIR
cp $CERTIDUDE_DIR/ca_cert.pem overlay/$CERTIDUDE_DIR
fi
cat < EOF > overlay/etc/config/certidude
config authority
option url http://$AUTHORITY
option authority_path /var/lib/certidude/$AUTHORITY/ca_cert.pem
option request_path /var/lib/certidude/$AUTHORITY/client_req.pem
option certificate_path /var/lib/certidude/$AUTHORITY/client_cert.pem
option key_path /var/lib/certidude/$AUTHORITY/client_key.pem
option key_type rsa
option key_length 1024
option red_led gl-connect:red:wlan
option green_led gl-connect:green:lan
EOF
make image FILES=../overlay/ PACKAGES="$PACKAGES" PROFILE="$PROFILE"

View File

@ -109,11 +109,10 @@ esac
EOF EOF
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci luci-app-commands \ make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci luci-app-commands \
openssl-util curl ca-certificates \ openssl-util curl ca-certificates dropbear \
strongswan-mod-kernel-libipsec kmod-tun ip-full strongswan-full \ strongswan-mod-kernel-libipsec kmod-tun strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \
htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \ htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \
-luci-app-firewall \ -luci-app-firewall \
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \ -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6" -kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6 bc"

View File

@ -9,6 +9,12 @@ BASENAME=lede-imagebuilder-$VERSION-ar71xx-generic.Linux-x86_64
FILENAME=$BASENAME.tar.xz FILENAME=$BASENAME.tar.xz
URL=http://downloads.lede-project.org/releases/$VERSION/targets/ar71xx/generic/$FILENAME URL=http://downloads.lede-project.org/releases/$VERSION/targets/ar71xx/generic/$FILENAME
# curl of vanilla LEDE doesn't support ECDSA at the moment
BASENAME=lede-imagebuilder-ar71xx-generic.Linux-x86_64
FILENAME=$BASENAME.tar.xz
URL=https://www.koodur.com/$FILENAME
if [ ! -e $BUILD/$FILENAME ]; then if [ ! -e $BUILD/$FILENAME ]; then
wget -q $URL -O $BUILD/$FILENAME wget -q $URL -O $BUILD/$FILENAME
fi fi
@ -19,58 +25,94 @@ fi
# Copy CA certificate # Copy CA certificate
AUTHORITY=$(hostname -f) AUTHORITY=$(hostname -f)
CERTIDUDE_DIR=/var/lib/certidude/$AUTHORITY
mkdir -p $OVERLAY/etc/config mkdir -p $OVERLAY/etc/config
mkdir -p $OVERLAY/etc/uci-defaults mkdir -p $OVERLAY/etc/uci-defaults
mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY/
cp /var/lib/certidude/$AUTHORITY/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/ cp /var/lib/certidude/$AUTHORITY/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/
echo /etc/certidude >> $OVERLAY/etc/sysupgrade.conf
cat <<EOF > $OVERLAY/etc/config/certidude cat <<EOF > $OVERLAY/etc/config/certidude
config authority config authority
option gateway router.k-space.ee option gateway "$ROUTER"
option url http://$AUTHORITY option hostname "$AUTHORITY"
option trigger wan option trigger wan
option authority_path /etc/certidude/authority/$AUTHORITY/ca_cert.pem option key_type $AUTHORITY_CERTIFICATE_ALGORITHM
option request_path /etc/certidude/authority/$AUTHORITY/client_req.pem
option certificate_path /etc/certidude/authority/$AUTHORITY/client_cert.pem
option key_path /etc/certidude/authority/$AUTHORITY/client_key.pem
option key_type rsa
option key_length 2048 option key_length 2048
option key_curve secp384r1
EOF EOF
cat << EOF > $OVERLAY/etc/uci-defaults/40-disable-ipsec cat << EOF > $OVERLAY/etc/uci-defaults/40-disable-ipsec
/etc/init.d/ipsec disable /etc/init.d/ipsec disable
EOF EOF
case $AUTHORITY_CERTIFICATE_ALGORITHM in
rsa)
echo ": RSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets
DHGROUP=modp2048
;;
ec)
echo ": ECDSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets
DHGROUP=ecp384
;;
*)
echo "Unknown algorithm $AUTHORITY_CERTIFICATE_ALGORITHM"
exit 1
;;
esac
cat << EOF > $OVERLAY/etc/certidude/authority/$AUTHORITY/updown
#!/bin/sh
cat << EOF > $OVERLAY/etc/ipsec.secrets CURL="curl -m 3 -f --key /etc/certidude/authority/$AUTHORITY/host_key.pem --cert /etc/certidude/authority/$AUTHORITY/host_cert.pem --cacert /etc/certidude/authority/$AUTHORITY/ca_cert.pem"
: RSA /etc/certidude/authority/$AUTHORITY/client_key.pem URL="https://$AUTHORITY:8443/api/signed/\$(uci get system.@system[0].hostname)/script/"
case \$PLUTO_VERB in
up-client)
logger -t certidude -s "Downloading and executing \$URL"
\$CURL \$URL -o /tmp/script.sh && sh /tmp/script.sh
;;
*) ;;
esac
EOF EOF
chmod +x $OVERLAY/etc/certidude/authority/$AUTHORITY/updown
cat << EOF > $OVERLAY/etc/ipsec.conf cat << EOF > $OVERLAY/etc/ipsec.conf
config setup config setup
strictcrlpolicy=yes
ca $AUTHORITY ca $AUTHORITY
cacert=/etc/certidude/authority/$AUTHORITY/ca_cert.pem
auto=add auto=add
cacert=/etc/certidude/authority/$AUTHORITY/ca_cert.pem
ocspuri = http://$AUTHORITY/api/ocsp/
conn router.k-space.ee conn %default
right=router.k-space.ee keyingtries=%forever
dpdaction=restart dpdaction=restart
auto=start
rightsubnet=0.0.0.0/0
rightid=%any
leftsourceip=%config
keyexchange=ikev2
closeaction=restart closeaction=restart
leftcert=/etc/certidude/authority/$AUTHORITY/client_cert.pem ike=aes256-sha384-ecp384!
esp=aes128gcm16-aes128gmac!
left=%defaultroute left=%defaultroute
leftcert=/etc/certidude/authority/$AUTHORITY/host_cert.pem
leftca="$AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME"
rightca="$AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME"
conn client-to-site
auto=start
right="$ROUTER"
rightsubnet=0.0.0.0/0
leftsourceip=%config
leftupdown=/etc/certidude/authority/$AUTHORITY/updown
EOF EOF
cat << EOF > $OVERLAY/etc/uci-defaults/99-uhttpd-disable-https
uci delete uhttpd.main.listen_https
uci delete uhttpd.main.redirect_https
EOF

View File

@ -38,6 +38,7 @@ uci certidude.@authority[0].green_led='gl-connect:green:lan'
EOF EOF
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates strongswan-full htop \ make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates \
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci luci-app-mjpg-streamer kmod-video-uvc \ strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm htop \
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun ip-full" iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun bc"

View File

@ -96,15 +96,15 @@ uci set firewall.@redirect[-1].target=DNAT
uci set firewall.@redirect[-1].proto=tcp uci set firewall.@redirect[-1].proto=tcp
uci set firewall.@redirect[-1].enabled=0 uci set firewall.@redirect[-1].enabled=0
uci set uhttpd.main.listen_http=0.0.0.0:8080
/etc/init.d/dropbear disable /etc/init.d/dropbear disable
uci set uhttpd.main.listen_http=0.0.0.0:8080
EOF EOF
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \ make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci \ iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci dropbear kmod-tun \
strongswan-mod-kernel-libipsec kmod-tun ip-full strongswan-full \ strongswan-default strongswan-mod-kernel-libipsec strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \
pciutils -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm" pciutils -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm bc"

View File

@ -0,0 +1,10 @@
#!/bin/sh
# To test: ACTION=ifup INTERFACE=wan sh /etc/hotplug.d/iface/50-certidude
AUTHORITY=certidude.@authority[0]
[ $ACTION == "ifup" ] || exit 0
[ $INTERFACE == "$(uci get $AUTHORITY.trigger)" ] || exit 0
/usr/bin/certidude-enroll

View File

@ -0,0 +1,9 @@
cat << EOF > /etc/crontabs/root
15 1 * * * sleep 70 && touch /etc/banner && reboot
10 1 1 */2 * /usr/bin/certidude-enroll-renew
EOF
chmod 0600 /etc/crontabs/root
/etc/init.d/cron enable

View File

@ -0,0 +1,123 @@
#!/bin/sh
AUTHORITY=certidude.@authority[0]
# TODO: iterate over all authorities
GATEWAY=$(uci get $AUTHORITY.gateway)
COMMON_NAME=$(uci get system.@system[0].hostname)
DIR=/etc/certidude/authority/$(uci get $AUTHORITY.hostname)
mkdir -p $DIR
AUTHORITY_PATH=$DIR/ca_cert.pem
CERTIFICATE_PATH=$DIR/host_cert.pem
REQUEST_PATH=$DIR/host_req.pem
KEY_PATH=$DIR/host_key.pem
KEY_TYPE=$(uci get $AUTHORITY.key_type)
KEY_LENGTH=$(uci get $AUTHORITY.key_length)
KEY_CURVE=$(uci get $AUTHORITY.key_curve)
NTP_SERVERS=$(uci get system.ntp.server)
logger -t certidude -s "Fetching time from NTP servers: $NTP_SERVERS"
ntpd -q -n -d -p $NTP_SERVERS
logger -t certidude -s "Time is now: $(date)"
# If certificate file is there assume everything's set up
if [ -f $CERTIFICATE_PATH ]; then
SERIAL=$(openssl x509 -in $CERTIFICATE_PATH -noout -serial | cut -d "=" -f 2 | tr [A-F] [a-f])
logger -t certidude -s "Certificate with serial $SERIAL already exists in $CERTIFICATE_PATH, attempting to bring up VPN tunnel..."
ipsec restart
exit 0
fi
#########################################
### Generate private key if necessary ###
#########################################
if [ ! -f $KEY_PATH ]; then
logger -t certidude -s "Generating $KEY_TYPE key for VPN..."
case $KEY_TYPE in
rsa)
openssl genrsa -out $KEY_PATH.part $KEY_LENGTH
;;
ec)
openssl ecparam -name $KEY_CURVE -genkey -noout -out $KEY_PATH.part
;;
esac
mv $KEY_PATH.part $KEY_PATH
fi
############################
### Fetch CA certificate ###
############################
if [ ! -f $AUTHORITY_PATH ]; then
logger -t certidude -s "Fetching CA certificate from $URL/api/certificate/"
curl -f -s http://$(uci get $AUTHORITY.hostname)/api/certificate/ -o $AUTHORITY_PATH.part
if [ $? -ne 0 ]; then
logger -t certidude -s "Failed to receive CA certificate, server responded: $(cat $AUTHORITY_PATH.part)"
exit 10
fi
openssl x509 -in $AUTHORITY_PATH.part -noout
if [ $? -ne 0 ]; then
logger -t certidude -s "Received invalid CA certificate"
exit 11
fi
mv $AUTHORITY_PATH.part $AUTHORITY_PATH
fi
logger -t certidude -s "CA certificate md5sum: $(md5sum -b $AUTHORITY_PATH)"
#####################################
### Generate request if necessary ###
#####################################
if [ ! -f $REQUEST_PATH ]; then
openssl req -new -sha256 -key $KEY_PATH -out $REQUEST_PATH.part -subj "/CN=$COMMON_NAME"
mv $REQUEST_PATH.part $REQUEST_PATH
fi
logger -t certidude -s "Request md5sum is $(md5sum -b $REQUEST_PATH)"
curl -f -L \
-H "Content-Type: application/pkcs10" \
--cacert $AUTHORITY_PATH \
--data-binary @$REQUEST_PATH \
https://$(uci get $AUTHORITY.hostname):8443/api/request/?autosign=true\&wait=yes -o $CERTIFICATE_PATH.part
# TODO: Loop until we get exitcode 0
# TODO: Use backoff time $((2\*X))
if [ $? -ne 0 ]; then
echo "Failed to fetch certificate"
exit 21
fi
# Verify certificate
openssl verify -CAfile $AUTHORITY_PATH $CERTIFICATE_PATH.part
if [ $? -ne 0 ]; then
logger -t certidude -s "Received bogus certificate!"
exit 22
fi
logger -t certidude -s "Certificate md5sum: $(md5sum -b $CERTIFICATE_PATH.part)"
uci commit
mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH
# Start services
logger -t certidude -s "Starting IPSec IKEv2 daemon..."
ipsec restart

View File

@ -0,0 +1,25 @@
#!/bin/sh
AUTHORITY=certidude.@authority[0]
URL=https://$(uci get $AUTHORITY.hostname):8443
DIR=/etc/certidude/authority/$(uci get $AUTHORITY.hostname)
AUTHORITY_PATH=$DIR/ca_cert.pem
CERTIFICATE_PATH=$DIR/host_cert.pem
REQUEST_PATH=$DIR/host_req.pem
KEY_PATH=$DIR/host_key.pem
curl -f -L \
-H "Content-Type: application/pkcs10" \
--data-binary @$REQUEST_PATH \
--cacert $AUTHORITY_PATH \
--key $KEY_PATH \
--cert $CERTIFICATE_PATH \
$URL/api/request/ -o $CERTIFICATE_PATH.part
if [ $? -eq 0 ]; then
logger -t certidude -s "Certificate renewal successful"
mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH
ipsec reload
else
logger -t certidude -s "Failed to renew certificate"
fi

View File

@ -60,12 +60,13 @@ def clean_client():
files = [ files = [
"/etc/certidude/client.conf", "/etc/certidude/client.conf",
"/etc/certidude/services.conf", "/etc/certidude/services.conf",
"/var/lib/certidude/ca.example.lan/client_key.pem", "/etc/certidude/authority/ca.example.lan/ca_cert.pem",
"/var/lib/certidude/ca.example.lan/server_key.pem", "/etc/certidude/authority/ca.example.lan/client_key.pem",
"/var/lib/certidude/ca.example.lan/client_req.pem", "/etc/certidude/authority/ca.example.lan/server_key.pem",
"/var/lib/certidude/ca.example.lan/server_req.pem", "/etc/certidude/authority/ca.example.lan/client_req.pem",
"/var/lib/certidude/ca.example.lan/client_cert.pem", "/etc/certidude/authority/ca.example.lan/server_req.pem",
"/var/lib/certidude/ca.example.lan/server_cert.pem", "/etc/certidude/authority/ca.example.lan/client_cert.pem",
"/etc/certidude/authority/ca.example.lan/server_cert.pem",
] ]
for path in files: for path in files:
if os.path.exists(path): if os.path.exists(path):
@ -85,6 +86,8 @@ def clean_client():
def clean_server(): def clean_server():
os.umask(0o22)
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:
@ -95,30 +98,29 @@ def clean_server():
if os.path.exists("/var/lib/certidude/ca.example.lan"): if os.path.exists("/var/lib/certidude/ca.example.lan"):
shutil.rmtree("/var/lib/certidude/ca.example.lan") shutil.rmtree("/var/lib/certidude/ca.example.lan")
if os.path.exists("/etc/certidude/server.conf"):
os.unlink("/etc/certidude/server.conf")
if os.path.exists("/run/certidude"): if os.path.exists("/run/certidude"):
shutil.rmtree("/run/certidude") shutil.rmtree("/run/certidude")
if os.path.exists("/var/log/certidude.log"):
os.unlink("/var/log/certidude.log")
if os.path.exists("/etc/cron.hourly/certidude"):
os.unlink("/etc/cron.hourly/certidude")
# systemd files = [
if os.path.exists("/etc/systemd/system/certidude.service"): "/etc/krb5.keytab",
os.unlink("/etc/systemd/system/certidude.service") "/etc/samba/smb.conf",
"/etc/certidude/server.conf",
"/etc/certidude/builder.conf",
"/etc/certidude/profile.conf",
"/var/log/certidude.log",
"/etc/cron.hourly/certidude",
"/etc/systemd/system/certidude.service",
"/etc/nginx/sites-available/ca.conf",
"/etc/nginx/sites-enabled/ca.conf",
"/etc/nginx/sites-available/certidude.conf",
"/etc/nginx/sites-enabled/certidude.conf",
"/etc/nginx/conf.d/tls.conf",
"/etc/certidude/server.keytab",
]
# Remove nginx stuff for filename in files:
if os.path.exists("/etc/nginx/sites-available/ca.conf"): if os.path.exists(filename):
os.unlink("/etc/nginx/sites-available/ca.conf") os.unlink(filename)
if os.path.exists("/etc/nginx/sites-enabled/ca.conf"):
os.unlink("/etc/nginx/sites-enabled/ca.conf")
if os.path.exists("/etc/nginx/sites-available/certidude.conf"):
os.unlink("/etc/nginx/sites-available/certidude.conf")
if os.path.exists("/etc/nginx/sites-enabled/certidude.conf"):
os.unlink("/etc/nginx/sites-enabled/certidude.conf")
if os.path.exists("/etc/nginx/conf.d/tls.conf"):
os.unlink("/etc/nginx/conf.d/tls.conf")
# Remove OpenVPN stuff # Remove OpenVPN stuff
if os.path.exists("/etc/openvpn"): if os.path.exists("/etc/openvpn"):
@ -135,8 +137,6 @@ def clean_server():
os.kill(int(fh.read()), 15) os.kill(int(fh.read()), 15)
except OSError: except OSError:
pass pass
if os.path.exists("/etc/certidude/server.keytab"):
os.unlink("/etc/certidude/server.keytab")
os.system("rm -Rfv /var/lib/samba/*") os.system("rm -Rfv /var/lib/samba/*")
# Restore initial resolv.conf # Restore initial resolv.conf
@ -146,7 +146,7 @@ def test_cli_setup_authority():
assert os.getuid() == 0, "Run tests as root in a clean VM or container" assert os.getuid() == 0, "Run tests as root in a clean VM or container"
assert check_output(["/bin/hostname", "-f"]) == b"ca.example.lan\n", "As a safety precaution, unittests only run in a machine whose hostanme -f is ca.example.lan" assert check_output(["/bin/hostname", "-f"]) == b"ca.example.lan\n", "As a safety precaution, unittests only run in a machine whose hostanme -f is ca.example.lan"
os.system("apt-get install -q -y git build-essential python-dev libkrb5-dev") os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y git build-essential python-dev libkrb5-dev")
assert not os.environ.get("KRB5CCNAME"), "Environment contaminated" assert not os.environ.get("KRB5CCNAME"), "Environment contaminated"
assert not os.environ.get("KRB5_KTNAME"), "Environment contaminated" assert not os.environ.get("KRB5_KTNAME"), "Environment contaminated"
@ -189,39 +189,6 @@ def test_cli_setup_authority():
if "userbot" not in buf: if "userbot" not in buf:
os.system("useradd userbot -G users -p '$1$PBkf5waA$n9EV6WJ7PS6lyGWkgeTPf1' -c 'User Bot,,,'") os.system("useradd userbot -G users -p '$1$PBkf5waA$n9EV6WJ7PS6lyGWkgeTPf1' -c 'User Bot,,,'")
# Bootstrap domain controller here,
# Samba startup takes some time
os.system("apt-get install -y samba krb5-user winbind bc")
if os.path.exists("/etc/samba/smb.conf"):
os.unlink("/etc/samba/smb.conf")
os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca")
os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'")
os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'")
os.system("samba-tool group addmembers 'Domain Admins' adminbot")
os.system("samba-tool user setpassword administrator --newpassword=S4l4k4l4")
if os.path.exists("/etc/krb5.keytab"):
os.unlink("/etc/krb5.keytab")
os.symlink("/var/lib/samba/private/secrets.keytab", "/etc/krb5.keytab")
os.chmod("/var/lib/samba/private/secrets.keytab", 0o644) # To allow access to certidude server
if os.path.exists("/etc/krb5.conf"): # Remove the one from krb5-user package
os.unlink("/etc/krb5.conf")
os.symlink("/var/lib/samba/private/krb5.conf", "/etc/krb5.conf")
with open("/etc/resolv.conf", "w") as fh:
fh.write("nameserver 127.0.0.1\nsearch example.lan\n")
# TODO: dig -t srv perhaps?
os.system("samba")
# Samba bind 636 late (probably generating keypair)
# so LDAPS connections below will fail
timeout = 0
while timeout < 30:
if os.path.exists("/var/lib/samba/private/tls/cert.pem"):
break
sleep(1)
timeout += 1
else:
assert False, "Samba startup timed out"
reload(const) reload(const)
from certidude.cli import entry_point as cli from certidude.cli import entry_point as cli
@ -447,15 +414,6 @@ def test_cli_setup_authority():
assert "Stored request " in inbox.pop(), inbox assert "Stored request " in inbox.pop(), inbox
assert not inbox assert not inbox
buf = generate_csr(cn="test2.example.lan")
r = client().simulate_post("/api/request/",
query_string="autosign=1",
body=buf,
headers={"content-type":"application/pkcs10"})
assert r.status_code == 202 # server CN, request stored
assert "Stored request " in inbox.pop(), inbox
assert not inbox
# Test signed certificate API call # Test signed certificate API call
r = client().simulate_get("/api/signed/nonexistant/") r = client().simulate_get("/api/signed/nonexistant/")
assert r.status_code == 404, r.text assert r.status_code == 404, r.text
@ -574,7 +532,7 @@ def test_cli_setup_authority():
# Test tagging integration in scripting framework # Test tagging integration in scripting framework
r = client().simulate_get("/api/signed/test/script/") r = client().simulate_get("/api/signed/test/script/")
assert r.status_code == 200, r.text # script render ok assert r.status_code == 200, r.text # script render ok
assert "curl http://ca.example.lan/api/signed/test/attr " in r.text, r.text assert "curl https://ca.example.lan:8443/api/signed/test/attr " in r.text, r.text
assert "Tartu" in r.text, r.text assert "Tartu" in r.text, r.text
r = client().simulate_post("/api/signed/test/tag/", r = client().simulate_post("/api/signed/test/tag/",
@ -640,7 +598,7 @@ def test_cli_setup_authority():
headers={"Authorization":admintoken}) headers={"Authorization":admintoken})
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
assert "Revoked " in inbox.pop(), inbox assert "Revoked " in inbox.pop(), inbox
"""
# Log can be read only by admin # Log can be read only by admin
r = client().simulate_get("/api/log/") r = client().simulate_get("/api/log/")
@ -652,7 +610,7 @@ def test_cli_setup_authority():
headers={"Authorization":admintoken}) headers={"Authorization":admintoken})
assert r.status_code == 200, r.text assert r.status_code == 200, r.text
assert r.headers.get('content-type') == "application/json; charset=UTF-8" assert r.headers.get('content-type') == "application/json; charset=UTF-8"
"""
# Test session API call # Test session API call
r = client().simulate_get("/api/") r = client().simulate_get("/api/")
@ -708,11 +666,14 @@ def test_cli_setup_authority():
with open("/etc/certidude/client.conf", "a") as fh: with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n") fh.write("insecure = true\n")
fh.write("autosign = false\n")
assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--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 "(Autosign failed, only client certificates allowed to be signed automatically)" in result.output, result.output assert "(autosign not requested)" in result.output, result.output
assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
child_pid = os.fork() child_pid = os.fork()
if not child_pid: if not child_pid:
@ -727,12 +688,12 @@ def test_cli_setup_authority():
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 "Writing certificate to:" in result.output, result.output assert "Writing certificate to:" in result.output, result.output
assert os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
result = runner.invoke(cli, ["enroll", "--skip-self", "--renew", "--no-wait"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--renew", "--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 "Writing certificate to:" in result.output, result.output assert "Renewing using current keypair" in result.output, result.output
assert "Attached renewal signature" 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"
@ -745,7 +706,7 @@ def test_cli_setup_authority():
# First OpenVPN server is set up # First OpenVPN server is set up
clean_client() clean_client()
assert not os.path.exists("/var/lib/certidude/ca.example.lan/server_cert.pem") assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
if not os.path.exists("/etc/openvpn/keys"): if not os.path.exists("/etc/openvpn/keys"):
os.makedirs("/etc/openvpn/keys") os.makedirs("/etc/openvpn/keys")
@ -761,12 +722,13 @@ def test_cli_setup_authority():
with open("/etc/certidude/client.conf", "a") as fh: with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n") fh.write("insecure = true\n")
fh.write("autosign = false\n")
assert not os.path.exists("/var/lib/certidude/ca.example.lan/server_cert.pem") assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--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 "(autosign not requested)" 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") assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/vpn.example.lan.pem")
@ -785,7 +747,7 @@ def test_cli_setup_authority():
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 "Writing certificate to:" in result.output, result.output assert "Writing certificate to:" in result.output, result.output
assert os.path.exists("/var/lib/certidude/ca.example.lan/server_cert.pem") assert os.path.exists("/etc/certidude/authority//ca.example.lan/server_cert.pem")
assert os.path.exists("/etc/openvpn/site-to-client.conf") assert os.path.exists("/etc/openvpn/site-to-client.conf")
# Secondly OpenVPN client is set up # Secondly OpenVPN client is set up
@ -977,7 +939,7 @@ def test_cli_setup_authority():
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"]) result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"])
assert not result.exception, result.output assert not result.exception, result.output
assert open("/etc/ipsec.secrets").read() == ": RSA /var/lib/certidude/ca.example.lan/server_key.pem\n" assert open("/etc/ipsec.secrets").read() == ": RSA /etc/certidude/authority/ca.example.lan/server_key.pem\n"
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem") assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem")
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"]) result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"])
@ -986,10 +948,11 @@ def test_cli_setup_authority():
with open("/etc/certidude/client.conf", "a") as fh: with open("/etc/certidude/client.conf", "a") as fh:
fh.write("insecure = true\n") fh.write("insecure = true\n")
fh.write("autosign = false\n")
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"]) result = runner.invoke(cli, ["enroll", "--skip-self", "--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 "(autosign not requested)" 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/ipsec.example.lan.pem") assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem")
@ -1009,7 +972,7 @@ 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 os.path.exists("/var/lib/certidude/ca.example.lan/server_cert.pem") assert os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
# IPSec client as service # IPSec client as service
@ -1073,12 +1036,39 @@ def test_cli_setup_authority():
r = requests.get("http://ca.example.lan/api/scep/") r = requests.get("http://ca.example.lan/api/scep/")
assert r.status_code == 404 assert r.status_code == 404
r = requests.get("http://ca.example.lan/api/ocsp/")
assert r.status_code == 404
r = requests.post("http://ca.example.lan/api/scep/") r = requests.post("http://ca.example.lan/api/scep/")
assert r.status_code == 404 assert r.status_code == 404
#################
### Test OCSP ###
#################
r = requests.get("http://ca.example.lan/api/ocsp/")
assert r.status_code == 400
r = requests.post("http://ca.example.lan/api/ocsp/") r = requests.post("http://ca.example.lan/api/ocsp/")
assert r.status_code == 404 assert r.status_code == 400
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -CAfile /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/signed/roadwarrior2.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp1.log") == 0
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -CAfile /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/ca_cert.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp2.log") == 0
for filename in os.listdir("/var/lib/certidude/ca.example.lan/revoked"):
if not filename.endswith(".pem"):
continue
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -CAfile /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/revoked/%s -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp3.log" % filename) == 0
break
with open("/tmp/ocsp1.log") as fh:
buf = fh.read()
assert ": good" in buf, buf
with open("/tmp/ocsp2.log") as fh:
buf = fh.read()
assert ": unknown" in buf, buf
with open("/tmp/ocsp3.log") as fh:
buf = fh.read()
assert ": revoked" in buf, buf
#################################### ####################################
@ -1088,8 +1078,53 @@ def test_cli_setup_authority():
# Shut down current instance # Shut down current instance
os.kill(server_pid, 15) os.kill(server_pid, 15)
requests.get("http://ca.example.lan/api/") requests.get("http://ca.example.lan/api/")
# sleep(2)
# os.kill(server_pid, 9) # TODO: Figure out why doesn't shut down gracefully
os.waitpid(server_pid, 0) os.waitpid(server_pid, 0)
# Install packages
os.system("apt-get install -y samba krb5-user winbind bc")
clean_server()
# Bootstrap domain controller here,
# Samba startup takes some timec
os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca")
os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'")
os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'")
os.system("samba-tool group addmembers 'Domain Admins' adminbot")
os.system("samba-tool user setpassword administrator --newpassword=S4l4k4l4")
os.symlink("/var/lib/samba/private/secrets.keytab", "/etc/krb5.keytab")
os.chmod("/var/lib/samba/private/secrets.keytab", 0o644) # To allow access to certidude server
if os.path.exists("/etc/krb5.conf"): # Remove the one from krb5-user package
os.unlink("/etc/krb5.conf")
os.symlink("/var/lib/samba/private/krb5.conf", "/etc/krb5.conf")
with open("/etc/resolv.conf", "w") as fh:
fh.write("nameserver 127.0.0.1\nsearch example.lan\n")
# TODO: dig -t srv perhaps?
os.system("samba")
# Samba bind 636 late (probably generating keypair)
# so LDAPS connections below will fail
timeout = 0
while timeout < 30:
if os.path.exists("/var/lib/samba/private/tls/cert.pem"):
break
sleep(1)
timeout += 1
else:
assert False, "Samba startup timed out"
# Bootstrap authority
bootstrap_pid = os.fork() # TODO: this shouldn't be necessary
if not bootstrap_pid:
result = runner.invoke(cli, ["setup", "authority", "--skip-packages", "--elliptic-curve"])
assert not result.exception, result.output
return
else:
os.waitpid(bootstrap_pid, 0)
assert os.getuid() == 0 and os.getgid() == 0, "Environment contaminated"
# (re)auth against DC # (re)auth against DC
assert os.system("kdestroy") == 0 assert os.system("kdestroy") == 0
assert not os.path.exists("/tmp/krb5cc_0") assert not os.path.exists("/tmp/krb5cc_0")
@ -1116,13 +1151,11 @@ def test_cli_setup_authority():
# Certidude would auth against domain controller # Certidude would auth against domain controller
os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/backends = pam/backends = kerberos ldap/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/backend = posix/backend = ldap/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude") os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude")
os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf") os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/machine enrollment =.*/machine enrollment = allowed/g' -i /etc/certidude/server.conf") os.system("sed -e 's/machine enrollment subnets =.*/machine enrollment subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/ocsp subnets =.*/ocsp subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") os.system("sed -e 's/ocsp subnets =.*/ocsp subnets =/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf") os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf")
os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf") os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf")
from certidude.common import pip from certidude.common import pip
@ -1157,41 +1190,25 @@ def test_cli_setup_authority():
if r.status_code != 502: if r.status_code != 502:
break break
sleep(1) sleep(1)
assert r.status_code == 401 assert r.status_code == 401, "Timed out starting up the API backend"
# CRL-s disabled now # CRL-s disabled now
r = requests.get("http://ca.example.lan/api/revoked/") r = requests.get("http://ca.example.lan/api/revoked/")
assert r.status_code == 404, r.text assert r.status_code == 404, r.text
# OCSP and SCEP should be enabled now # SCEP should be enabled now
r = requests.get("http://ca.example.lan/api/scep/") r = requests.get("http://ca.example.lan/api/scep/")
assert r.status_code == 400 assert r.status_code == 400
r = requests.get("http://ca.example.lan/api/ocsp/")
assert r.status_code == 400
r = requests.post("http://ca.example.lan/api/scep/") r = requests.post("http://ca.example.lan/api/scep/")
assert r.status_code == 405 assert r.status_code == 405
# OCSP should be disabled now
r = requests.get("http://ca.example.lan/api/ocsp/")
assert r.status_code == 404
r = requests.post("http://ca.example.lan/api/ocsp/") r = requests.post("http://ca.example.lan/api/ocsp/")
assert r.status_code == 400 assert r.status_code == 404
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/signed/roadwarrior2.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp1.log") == 0
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/ca_cert.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp2.log") == 0
for filename in os.listdir("/var/lib/certidude/ca.example.lan/revoked"):
if not filename.endswith(".pem"):
continue
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/revoked/%s -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp3.log" % filename) == 0
break
with open("/tmp/ocsp1.log") as fh:
buf = fh.read()
assert ": good" in buf, buf
with open("/tmp/ocsp2.log") as fh:
buf = fh.read()
assert ": unknown" in buf, buf
with open("/tmp/ocsp3.log") as fh:
buf = fh.read()
assert ": revoked" in buf, buf
##################### #####################
@ -1274,7 +1291,6 @@ def test_cli_setup_authority():
### SCEP tests ### ### SCEP tests ###
################## ##################
os.umask(0o022)
if not os.path.exists("/tmp/sscep"): if not os.path.exists("/tmp/sscep"):
assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep") assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep")
if not os.path.exists("/tmp/sscep/sscep_dyn"): if not os.path.exists("/tmp/sscep/sscep_dyn"):
@ -1283,7 +1299,7 @@ def test_cli_setup_authority():
if not os.path.exists("/tmp/key.pem"): if not os.path.exists("/tmp/key.pem"):
assert not os.system("openssl genrsa -out /tmp/key.pem 1024") assert not os.system("openssl genrsa -out /tmp/key.pem 1024")
if not os.path.exists("/tmp/req.pem"): 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("echo '.\n.\n.\n.\nGateway\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
@ -1301,13 +1317,15 @@ def test_cli_setup_authority():
# Shut down server # Shut down server
assert os.path.exists("/proc/%d" % server_pid) assert os.path.exists("/proc/%d" % server_pid)
os.kill(server_pid, 15) os.kill(server_pid, 15)
# sleep(2)
# os.kill(server_pid, 9)
os.waitpid(server_pid, 0) os.waitpid(server_pid, 0)
# Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude # Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude
assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \ assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \
"/var/lib/certidude/ca.example.lan/client_key.pem r,\n" + \ "/etc/certidude/authority/ca.example.lan/client_key.pem r,\n" + \
"/var/lib/certidude/ca.example.lan/ca_cert.pem r,\n" + \ "/etc/certidude/authority/ca.example.lan/ca_cert.pem r,\n" + \
"/var/lib/certidude/ca.example.lan/client_cert.pem r,\n" "/etc/certidude/authority/ca.example.lan/client_cert.pem r,\n"
assert len(inbox) == 0, inbox # Make sure all messages were checked assert len(inbox) == 0, inbox # Make sure all messages were checked
os.system("service nginx stop") os.system("service nginx stop")