mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 08:15:18 +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:
parent
94e5f72566
commit
5e9251f365
4
.gitignore
vendored
4
.gitignore
vendored
@ -64,3 +64,7 @@ node_modules/
|
||||
# Ignore patch
|
||||
*.orig
|
||||
*.rej
|
||||
|
||||
lextab.py
|
||||
yacctab.py
|
||||
.pytest_cache
|
||||
|
@ -8,6 +8,7 @@ import hashlib
|
||||
from datetime import datetime
|
||||
from xattr import listxattr, getxattr
|
||||
from certidude.auth import login_required
|
||||
from certidude.common import cert_to_dn
|
||||
from certidude.user import User
|
||||
from certidude.decorators import serialize, csrf_protection
|
||||
from certidude import const, config, authority
|
||||
@ -54,7 +55,7 @@ class SessionResource(AuthorityHandler):
|
||||
)
|
||||
|
||||
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(
|
||||
serial = "%x" % cert.serial_number,
|
||||
common_name = common_name,
|
||||
@ -62,6 +63,7 @@ class SessionResource(AuthorityHandler):
|
||||
signed = signed,
|
||||
expired = expired,
|
||||
revoked = revoked,
|
||||
reason = reason,
|
||||
sha256sum = hashlib.sha256(buf).hexdigest())
|
||||
|
||||
def serialize_certificates(g):
|
||||
@ -69,7 +71,7 @@ class SessionResource(AuthorityHandler):
|
||||
# Extract certificate tags from filesystem
|
||||
try:
|
||||
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:
|
||||
k, v = tag.split("=", 1)
|
||||
else:
|
||||
@ -116,7 +118,7 @@ class SessionResource(AuthorityHandler):
|
||||
extensions = dict([
|
||||
(e["extn_id"].native, e["extn_value"].native)
|
||||
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():
|
||||
@ -131,6 +133,11 @@ class SessionResource(AuthorityHandler):
|
||||
mail=req.context.get("user").mail
|
||||
),
|
||||
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(
|
||||
builder = dict(
|
||||
profiles = config.IMAGE_BUILDER_PROFILES
|
||||
@ -143,13 +150,15 @@ class SessionResource(AuthorityHandler):
|
||||
certificate = dict(
|
||||
algorithm = authority.public_key.algorithm,
|
||||
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"),
|
||||
),
|
||||
mailer = dict(
|
||||
name = config.MAILER_NAME,
|
||||
address = config.MAILER_ADDRESS
|
||||
) 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_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
|
||||
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
|
||||
@ -278,8 +287,8 @@ def certidude_app(log_handlers=[]):
|
||||
log_handlers.append(LogHandler(uri))
|
||||
app.add_route("/api/log/", LogResource(uri))
|
||||
elif config.LOGGING_BACKEND == "syslog":
|
||||
from logging.handlers import SyslogHandler
|
||||
log_handlers.append(SyslogHandler())
|
||||
from logging.handlers import SysLogHandler
|
||||
log_handlers.append(SysLogHandler())
|
||||
# Browsing syslog via HTTP is obviously not possible out of the box
|
||||
elif config.LOGGING_BACKEND:
|
||||
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)
|
||||
|
@ -26,14 +26,15 @@ class AttributeResource(object):
|
||||
Results made available only to lease IP address.
|
||||
"""
|
||||
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:
|
||||
raise falcon.HTTPNotFound()
|
||||
else:
|
||||
return attribs
|
||||
|
||||
@csrf_protection
|
||||
@whitelist_subject # TODO: sign instead
|
||||
@whitelist_subject
|
||||
def on_post(self, req, resp, cn):
|
||||
namespace = ("user.%s." % self.namespace).encode("ascii")
|
||||
try:
|
||||
@ -43,7 +44,7 @@ class AttributeResource(object):
|
||||
else:
|
||||
for key in req.params:
|
||||
if not re.match("[a-z0-9_\.]+$", key):
|
||||
raise falcon.HTTPBadRequest("Invalid key")
|
||||
raise falcon.HTTPBadRequest("Invalid key %s" % key)
|
||||
valid = set()
|
||||
for key, value in req.params.items():
|
||||
identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii")
|
||||
|
@ -4,8 +4,9 @@ import falcon
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from certidude import config, const
|
||||
from certidude import config, const, authority
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.common import cert_to_dn
|
||||
from jinja2 import Template
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -14,6 +15,8 @@ class ImageBuilderResource(object):
|
||||
@login_required
|
||||
@authorize_admin
|
||||
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")
|
||||
build_script_path = config.cp2.get(profile, "command")
|
||||
overlay_path = config.cp2.get(profile, "overlay")
|
||||
@ -35,7 +38,10 @@ class ImageBuilderResource(object):
|
||||
stdout=open(log_path, "w"), stderr=subprocess.STDOUT,
|
||||
close_fds=True, shell=False,
|
||||
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/"},
|
||||
startupinfo=None, creationflags=0)
|
||||
proc.communicate()
|
||||
|
@ -1,6 +1,7 @@
|
||||
import falcon
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import xattr
|
||||
from datetime import datetime
|
||||
from certidude import config, push
|
||||
@ -32,10 +33,9 @@ class LeaseResource(AuthorityHandler):
|
||||
@authorize_server
|
||||
def on_post(self, req, resp):
|
||||
client_common_name = req.get_param("client", required=True)
|
||||
if "=" in client_common_name: # It's actually DN, resolve it to CN
|
||||
_, client_common_name = client_common_name.split(" CN=", 1)
|
||||
if "," in client_common_name:
|
||||
client_common_name, _ = client_common_name.split(",", 1)
|
||||
m = re.match("CN=(.+?),", client_common_name) # It's actually DN, resolve it to CN
|
||||
if m:
|
||||
client_common_name, = m.groups()
|
||||
|
||||
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
|
||||
|
@ -53,24 +53,29 @@ class OCSPResource(AuthorityHandler):
|
||||
assert serial > 0, "Serial number correctness check failed"
|
||||
|
||||
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.endswith(".pem")
|
||||
path, buf, cert, signed, expires = self.authority.get_signed(link_target[3:-4])
|
||||
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")
|
||||
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)
|
||||
except EnvironmentError:
|
||||
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(
|
||||
name='revoked',
|
||||
value={
|
||||
'revocation_time': revoked,
|
||||
'revocation_reason': "key_compromise",
|
||||
'revocation_reason': reason,
|
||||
})
|
||||
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)
|
||||
|
||||
responses.append({
|
||||
|
@ -14,7 +14,7 @@ from certidude.profile import SignatureProfile
|
||||
from datetime import datetime
|
||||
from oscrypto import asymmetric
|
||||
from oscrypto.errors import SignatureError
|
||||
from xattr import getxattr
|
||||
from xattr import getxattr, setxattr
|
||||
from .utils import AuthorityHandler
|
||||
from .utils.firewall import whitelist_subnets, whitelist_content_types
|
||||
|
||||
@ -59,26 +59,39 @@ class RequestListResource(AuthorityHandler):
|
||||
|
||||
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
|
||||
"""
|
||||
machine = req.context.get("machine")
|
||||
if machine:
|
||||
if config.MACHINE_ENROLLMENT_ALLOWED:
|
||||
if common_name != machine:
|
||||
raise falcon.HTTPBadRequest(
|
||||
"Bad request",
|
||||
"Common name %s differs from Kerberos credential %s!" % (common_name, machine))
|
||||
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:
|
||||
raise falcon.HTTPBadRequest(
|
||||
"Bad request",
|
||||
"Common name %s differs from Kerberos credential %s!" % (common_name, machine))
|
||||
|
||||
# Automatic enroll with Kerberos machine cerdentials
|
||||
resp.set_header("Content-Type", "application/x-pem-file")
|
||||
cert, resp.body = self.authority._sign(csr, body,
|
||||
profile=config.PROFILES["rw"], overwrite=overwrite_allowed)
|
||||
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
|
||||
machine, req.context.get("remote_addr"))
|
||||
return
|
||||
|
||||
# Automatic enroll with Kerberos machine cerdentials
|
||||
resp.set_header("Content-Type", "application/x-pem-file")
|
||||
cert, resp.body = self.authority._sign(csr, body,
|
||||
profile=config.PROFILES["rw"], overwrite=True)
|
||||
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
|
||||
machine, req.context.get("remote_addr"))
|
||||
return
|
||||
else:
|
||||
reasons.append("Machine enrollment not allowed")
|
||||
|
||||
"""
|
||||
Attempt to renew certificate using currently valid key pair
|
||||
@ -94,58 +107,61 @@ class RequestListResource(AuthorityHandler):
|
||||
# Same public key
|
||||
if cert_pk == csr_pk:
|
||||
buf = req.get_header("X-SSL-CERT")
|
||||
# Used mutually authenticated TLS handshake, assume renewal
|
||||
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)
|
||||
if handshake_cert.native == cert.native:
|
||||
for subnet in config.RENEWAL_SUBNETS:
|
||||
if req.context.get("remote_addr") in subnet:
|
||||
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,
|
||||
profile=SignatureProfile.from_cert(cert))
|
||||
logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
||||
return
|
||||
|
||||
# No header supplied, redirect to signed API call
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name)
|
||||
return
|
||||
reasons.append("renewal failed")
|
||||
else:
|
||||
# No renewal requested, redirect to signed API call
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name)
|
||||
return
|
||||
|
||||
|
||||
"""
|
||||
Process automatic signing if the IP address is whitelisted,
|
||||
autosigning was requested and certificate can be automatically signed
|
||||
"""
|
||||
|
||||
if req.get_param_as_bool("autosign"):
|
||||
if not self.authority.server_flags(common_name):
|
||||
for subnet in config.AUTOSIGN_SUBNETS:
|
||||
if req.context.get("remote_addr") in subnet:
|
||||
try:
|
||||
resp.set_header("Content-Type", "application/x-pem-file")
|
||||
_, resp.body = self.authority._sign(csr, body, profile=config.PROFILES["rw"])
|
||||
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
||||
return
|
||||
except EnvironmentError:
|
||||
logger.info("Autosign for %s from %s failed, signed certificate already exists",
|
||||
common_name, req.context.get("remote_addr"))
|
||||
reasons.append("Autosign failed, signed certificate already exists")
|
||||
break
|
||||
else:
|
||||
reasons.append("Autosign failed, IP address not whitelisted")
|
||||
for subnet in config.AUTOSIGN_SUBNETS:
|
||||
if req.context.get("remote_addr") in subnet:
|
||||
try:
|
||||
resp.set_header("Content-Type", "application/x-pem-file")
|
||||
_, resp.body = 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"))
|
||||
return
|
||||
except EnvironmentError:
|
||||
logger.info("Autosign for %s from %s failed, signed certificate already exists",
|
||||
common_name, req.context.get("remote_addr"))
|
||||
reasons.append("autosign failed, signed certificate already exists")
|
||||
break
|
||||
else:
|
||||
reasons.append("Autosign failed, only client certificates allowed to be signed automatically")
|
||||
reasons.append("autosign failed, IP address not whitelisted")
|
||||
else:
|
||||
reasons.append("autosign not requested")
|
||||
|
||||
# Attempt to save the request otherwise
|
||||
try:
|
||||
request_path, _, _ = self.authority.store_request(body,
|
||||
address=str(req.context.get("remote_addr")))
|
||||
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
|
||||
except errors.DuplicateCommonNameError:
|
||||
# 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"))
|
||||
raise falcon.HTTPConflict(
|
||||
"CSR with such CN already exists",
|
||||
@ -154,14 +170,15 @@ class RequestListResource(AuthorityHandler):
|
||||
push.publish("request-submitted", common_name)
|
||||
|
||||
# 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"):
|
||||
# Redirect to nginx pub/sub
|
||||
url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest()
|
||||
click.echo("Redirecting to: %s" % url)
|
||||
resp.status = falcon.HTTP_SEE_OTHER
|
||||
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:
|
||||
# Request was accepted, but not processed
|
||||
resp.status = falcon.HTTP_202
|
||||
|
@ -1,4 +1,7 @@
|
||||
import click
|
||||
import falcon
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
from asn1crypto import cms, algos
|
||||
from asn1crypto.core import SetOf, PrintableString
|
||||
@ -9,6 +12,8 @@ from oscrypto.errors import SignatureError
|
||||
from .utils import AuthorityHandler
|
||||
from .utils.firewall import whitelist_subnets
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Monkey patch asn1crypto
|
||||
|
||||
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['trans_id'] = SetOfPrintableString
|
||||
|
||||
class SCEPError(Exception): code = 25 # system failure
|
||||
class SCEPBadAlgo(SCEPError): code = 0
|
||||
class SCEPBadMessageCheck(SCEPError): code = 1
|
||||
class SCEPBadRequest(SCEPError): code = 2
|
||||
class SCEPBadTime(SCEPError): code = 3
|
||||
class SCEPBadCertId(SCEPError): code = 4
|
||||
class SCEPError(Exception):
|
||||
code = 25 # system failure
|
||||
explaination = "General system failure"
|
||||
|
||||
class SCEPBadAlgo(SCEPError):
|
||||
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):
|
||||
@whitelist_subnets(config.SCEP_SUBNETS)
|
||||
def on_get(self, req, resp):
|
||||
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.append_header("Content-Type", "application/x-x509-ca-cert")
|
||||
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
|
||||
encrypted_container = b""
|
||||
@ -74,8 +112,14 @@ class SCEPResource(AuthorityHandler):
|
||||
|
||||
# TODO: compare cert to current one if we are renewing
|
||||
|
||||
assert signer["digest_algorithm"]["algorithm"].native == "md5"
|
||||
assert signer["signature_algorithm"]["algorithm"].native == "rsassa_pkcs1v15"
|
||||
digest_algorithm = signer["digest_algorithm"]["algorithm"].native
|
||||
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
|
||||
transaction_id = None
|
||||
sender_nonce = None
|
||||
@ -87,8 +131,13 @@ class SCEPResource(AuthorityHandler):
|
||||
transaction_id, = attr["values"]
|
||||
elif attr["type"].native == "message_digest":
|
||||
message_digest, = attr["values"]
|
||||
if hashlib.md5(encap_content.native).digest() != message_digest.native:
|
||||
raise SCEPBadMessageCheck()
|
||||
if getattr(hashlib, digest_algorithm)(encap_content.native).digest() != message_digest.native:
|
||||
raise SCEPDigestMismatch()
|
||||
|
||||
if not sender_nonce:
|
||||
raise SCEPBadRequest()
|
||||
if not transaction_id:
|
||||
raise SCEPBadRequest()
|
||||
|
||||
assert message_digest
|
||||
msg = signer["signed_attrs"].dump(force=True)
|
||||
@ -102,7 +151,8 @@ class SCEPResource(AuthorityHandler):
|
||||
b"\x31" + msg[1:], # wtf?!
|
||||
"md5")
|
||||
except SignatureError:
|
||||
raise SCEPBadMessageCheck()
|
||||
raise SCEPSignatureMismatch()
|
||||
|
||||
|
||||
###############################
|
||||
### Decrypt inner container ###
|
||||
@ -122,14 +172,15 @@ class SCEPResource(AuthorityHandler):
|
||||
if recipient.native["rid"]["serial_number"] != self.authority.certificate.serial_number:
|
||||
raise SCEPBadCertId()
|
||||
|
||||
# Since CA private key is not directly readable here, we'll redirect it to signer socket
|
||||
key = asymmetric.rsa_pkcs1v15_decrypt(
|
||||
self.authority.private_key,
|
||||
recipient.native["encrypted_key"])
|
||||
if len(key) == 8: key = key * 3 # Convert DES to 3DES
|
||||
buf = symmetric.tripledes_cbc_pkcs5_decrypt(key, encrypted_content, iv)
|
||||
_, _, common_name = 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)
|
||||
content = signed_certificate.asn1.dump()
|
||||
|
||||
@ -138,6 +189,7 @@ class SCEPResource(AuthorityHandler):
|
||||
'type': "fail_info",
|
||||
'values': ["%d" % e.code]
|
||||
}))
|
||||
logger.info("Failed to sign SCEP request due to: %s" % e.explaination)
|
||||
else:
|
||||
|
||||
##################################
|
||||
@ -150,7 +202,8 @@ class SCEPResource(AuthorityHandler):
|
||||
'version': "v1",
|
||||
'certificates': [signed_certificate.asn1],
|
||||
'digest_algorithms': [cms.DigestAlgorithm({
|
||||
'algorithm': "md5"
|
||||
'algorithm': digest_algorithm
|
||||
|
||||
})],
|
||||
'encap_content_info': {
|
||||
'content_type': "data",
|
||||
@ -208,7 +261,7 @@ class SCEPResource(AuthorityHandler):
|
||||
attr_list = [
|
||||
cms.CMSAttribute({
|
||||
'type': "message_digest",
|
||||
'values': [hashlib.sha1(encrypted_container).digest()]
|
||||
'values': [getattr(hashlib, digest_algorithm)(encrypted_container).digest()]
|
||||
}),
|
||||
cms.CMSAttribute({
|
||||
'type': "message_type",
|
||||
@ -245,12 +298,12 @@ class SCEPResource(AuthorityHandler):
|
||||
'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': asymmetric.rsa_pkcs1v15_sign(
|
||||
self.authority.private_key,
|
||||
b"\x31" + attrs.dump()[1:],
|
||||
"sha1"
|
||||
digest_algorithm
|
||||
)
|
||||
})
|
||||
|
||||
@ -261,7 +314,7 @@ class SCEPResource(AuthorityHandler):
|
||||
'version': "v1",
|
||||
'certificates': [self.authority.certificate],
|
||||
'digest_algorithms': [cms.DigestAlgorithm({
|
||||
'algorithm': "sha1"
|
||||
'algorithm': digest_algorithm
|
||||
})],
|
||||
'encap_content_info': {
|
||||
'content_type': "data",
|
||||
|
@ -44,7 +44,7 @@ class SignedCertificateDetailResource(AuthorityHandler):
|
||||
resp.body = json.dumps(dict(
|
||||
common_name = cn,
|
||||
signer = signer_username,
|
||||
serial = "%x" % cert.serial_number,
|
||||
serial = "%040x" % cert.serial_number,
|
||||
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",
|
||||
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([
|
||||
(e["extn_id"].native, e["extn_value"].native)
|
||||
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",
|
||||
cn, req.context.get("remote_addr"))
|
||||
@ -69,5 +70,6 @@ class SignedCertificateDetailResource(AuthorityHandler):
|
||||
def on_delete(self, req, resp, cn):
|
||||
logger.info("Revoked certificate %s by %s from %s",
|
||||
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"))
|
||||
|
||||
|
@ -30,9 +30,14 @@ def authenticate(optional=False):
|
||||
|
||||
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
||||
|
||||
server_creds = gssapi.creds.Credentials(
|
||||
usage='accept',
|
||||
name=gssapi.names.Name('HTTP/%s'% const.FQDN))
|
||||
try:
|
||||
server_creds = gssapi.creds.Credentials(
|
||||
usage='accept',
|
||||
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)
|
||||
|
||||
@ -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.")
|
||||
|
||||
try:
|
||||
username, domain = str(context.initiator_name).split("@")
|
||||
username, realm = str(context.initiator_name).split("@")
|
||||
except AttributeError: # TODO: Better exception
|
||||
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",
|
||||
"Invalid realm supplied")
|
||||
"Cross-realm trust not supported")
|
||||
|
||||
if username.endswith("$") and optional:
|
||||
# Extract machine hostname
|
||||
|
@ -12,6 +12,7 @@ from asn1crypto.csr import CertificationRequest
|
||||
from certbuilder import CertificateBuilder
|
||||
from certidude import config, push, mailer, const
|
||||
from certidude import errors
|
||||
from certidude.common import cn_to_dn
|
||||
from crlbuilder import CertificateListBuilder, pem_armor_crl
|
||||
from csrbuilder import CSRBuilder, pem_armor_csr
|
||||
from datetime import datetime, timedelta
|
||||
@ -21,6 +22,16 @@ from xattr import getxattr, listxattr, setxattr
|
||||
|
||||
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://jamielinux.com/docs/openssl-certificate-authority/
|
||||
# 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)
|
||||
|
||||
def self_enroll():
|
||||
assert os.getuid() == 0 and os.getgid() == 0, "Can self-enroll only as root"
|
||||
|
||||
from certidude import const
|
||||
common_name = 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)
|
||||
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()
|
||||
if not pid:
|
||||
from certidude import authority
|
||||
from certidude.common import 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"])
|
||||
sys.exit(0)
|
||||
else:
|
||||
@ -109,18 +125,23 @@ def get_signed(common_name):
|
||||
def get_revoked(serial):
|
||||
if isinstance(serial, str):
|
||||
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:
|
||||
buf = fh.read()
|
||||
header, _, der_bytes = pem.unarmor(buf)
|
||||
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, \
|
||||
cert["tbs_certificate"]["validity"]["not_before"].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)
|
||||
attribs = dict()
|
||||
for key in listxattr(path):
|
||||
@ -129,15 +150,18 @@ def get_attributes(cn, namespace=None):
|
||||
continue
|
||||
if namespace and not key.startswith("user.%s." % namespace):
|
||||
continue
|
||||
value = getxattr(path, key)
|
||||
current = attribs
|
||||
if "." in key:
|
||||
prefix, key = key.rsplit(".", 1)
|
||||
for component in prefix.split("."):
|
||||
if component not in current:
|
||||
current[component] = dict()
|
||||
current = current[component]
|
||||
current[key] = value.decode("utf-8")
|
||||
value = getxattr(path, key).decode("utf-8")
|
||||
if flat:
|
||||
attribs[key[len("user.%s." % namespace):]] = value
|
||||
else:
|
||||
current = attribs
|
||||
if "." in key:
|
||||
prefix, key = key.rsplit(".", 1)
|
||||
for component in prefix.split("."):
|
||||
if component not in current:
|
||||
current[component] = dict()
|
||||
current = current[component]
|
||||
current[key] = value
|
||||
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"]
|
||||
|
||||
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")
|
||||
|
||||
@ -190,14 +214,21 @@ def store_request(buf, overwrite=False, address="", user=""):
|
||||
return request_path, csr, common_name
|
||||
|
||||
|
||||
def revoke(common_name):
|
||||
def revoke(common_name, reason):
|
||||
"""
|
||||
Revoke valid certificate
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
@ -212,7 +243,7 @@ def revoke(common_name):
|
||||
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
|
||||
mailer.send("certificate-revoked.md",
|
||||
attachments=(attach_cert,),
|
||||
serial_hex="%x" % cert.serial_number,
|
||||
serial_hex="%040x" % cert.serial_number,
|
||||
common_name=common_name)
|
||||
return revoked_path
|
||||
|
||||
@ -251,28 +282,40 @@ def _list_certificates(directory):
|
||||
server = True
|
||||
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):
|
||||
if filename.endswith(".pem"):
|
||||
common_name = filename[:-4]
|
||||
path, buf, cert, signed, expires = get_signed(common_name)
|
||||
yield common_name, path, buf, cert, signed, expires
|
||||
if not filename.endswith(".pem"):
|
||||
continue
|
||||
basename = filename[:-4]
|
||||
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):
|
||||
for filename in os.listdir(directory):
|
||||
if filename.endswith(".pem"):
|
||||
common_name = filename[:-4]
|
||||
path, buf, cert, signed, expired, revoked = get_revoked(common_name)
|
||||
yield cert.subject.native["common_name"], path, buf, cert, signed, expired, revoked
|
||||
path, buf, cert, signed, expired, revoked, reason = get_revoked(common_name)
|
||||
yield cert.subject.native["common_name"], path, buf, cert, signed, expired, revoked, reason
|
||||
|
||||
|
||||
def list_server_names():
|
||||
return [cn for cn, path, buf, cert, server in list_signed() if server]
|
||||
|
||||
|
||||
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(
|
||||
config.AUTHORITY_CRL_URL,
|
||||
certificate,
|
||||
1 # TODO: monotonically increasing
|
||||
generate_serial()
|
||||
)
|
||||
|
||||
for filename in os.listdir(config.REVOKED_DIR):
|
||||
@ -281,12 +324,14 @@ def export_crl(pem=True):
|
||||
serial_number = filename[:-4]
|
||||
# TODO: Assert serial against regex
|
||||
revoked_path = os.path.join(config.REVOKED_DIR, filename)
|
||||
reason = getxattr(revoked_path, "user.revocation.reason").decode("ascii") # TODO: dedup
|
||||
|
||||
# TODO: Skip expired certificates
|
||||
s = os.stat(revoked_path)
|
||||
builder.add_certificate(
|
||||
int(filename[:-4], 16),
|
||||
datetime.utcfromtimestamp(s.st_ctime),
|
||||
"key_compromise")
|
||||
reason)
|
||||
|
||||
certificate_list = builder.build(private_key)
|
||||
if pem:
|
||||
@ -359,7 +404,7 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
|
||||
|
||||
if overwrite:
|
||||
# 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)
|
||||
os.rename(cert_path, revoked_path)
|
||||
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:
|
||||
raise FileExistsError("Will not overwrite existing certificate")
|
||||
|
||||
dn = {u'common_name': common_name }
|
||||
if profile.ou:
|
||||
dn["organizational_unit_name"] = profile.ou
|
||||
|
||||
builder = CertificateBuilder(dn, csr_pubkey)
|
||||
builder.serial_number = random.randint(
|
||||
0x1000000000000000000000000000000000000000,
|
||||
0x7fffffffffffffffffffffffffffffffffffffff)
|
||||
builder = CertificateBuilder(cn_to_dn(common_name, const.FQDN,
|
||||
o=certificate["tbs_certificate"]["subject"].native.get("organization_name"),
|
||||
ou=profile.ou), csr_pubkey)
|
||||
builder.serial_number = generate_serial()
|
||||
|
||||
now = datetime.utcnow()
|
||||
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)
|
||||
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
|
||||
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
|
||||
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)
|
||||
requests.post(url, data=end_entity_cert_buf,
|
||||
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
|
||||
|
||||
push.publish("request-signed", common_name)
|
||||
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)
|
||||
return end_entity_cert, end_entity_cert_buf
|
||||
|
130
certidude/cli.py
130
certidude/cli.py
@ -12,12 +12,13 @@ import subprocess
|
||||
import sys
|
||||
from asn1crypto import pem, x509
|
||||
from asn1crypto.csr import CertificationRequest
|
||||
from asn1crypto.crl import CertificateList
|
||||
from base64 import b64encode
|
||||
from certbuilder import CertificateBuilder, pem_armor_certificate
|
||||
from certidude import const
|
||||
from csrbuilder import CSRBuilder, pem_armor_csr
|
||||
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 glob import glob
|
||||
from ipaddress import ip_network
|
||||
@ -49,7 +50,7 @@ def setup_client(prefix="client_", dh=False):
|
||||
def wrapped(**arguments):
|
||||
common_name = arguments.get("common_name")
|
||||
authority = arguments.get("authority")
|
||||
b = os.path.join(const.STORAGE_PATH, authority)
|
||||
b = os.path.join("/etc/certidude/authority", authority)
|
||||
if dh:
|
||||
path = os.path.join(const.STORAGE_PATH, "dh.pem")
|
||||
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("-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):
|
||||
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):
|
||||
click.echo("Self-enrolling authority's web interface certificate")
|
||||
from certidude import authority
|
||||
@ -182,7 +185,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
||||
try:
|
||||
authority_path = clients.get(authority_name, "authority path")
|
||||
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:
|
||||
if os.path.exists(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
|
||||
|
||||
# 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
|
||||
if not os.path.lexists(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'})
|
||||
|
||||
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
|
||||
revocations_partial = revocations_path + ".part"
|
||||
with open(revocations_partial, 'wb') as f:
|
||||
f.write(r.content)
|
||||
os.rename(revocations_partial, revocations_path)
|
||||
elif r.status_code == 404:
|
||||
click.echo("CRL disabled, server said 404")
|
||||
else:
|
||||
@ -293,8 +298,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
||||
key_path = clients.get(authority_name, "key path")
|
||||
request_path = clients.get(authority_name, "request path")
|
||||
except NoOptionError:
|
||||
key_path = "/var/lib/certidude/%s/client_key.pem" % authority_name
|
||||
request_path = "/var/lib/certidude/%s/client_csr.pem" % authority_name
|
||||
key_path = "/etc/certidude/authority/%s/host_key.pem" % authority_name
|
||||
request_path = "/etc/certidude/authority/%s/host_csr.pem" % authority_name
|
||||
|
||||
if os.path.exists(request_path):
|
||||
with open(request_path, "rb") as fh:
|
||||
@ -334,7 +339,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
||||
try:
|
||||
certificate_path = clients.get(authority_name, "certificate path")
|
||||
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:
|
||||
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
|
||||
pass
|
||||
|
||||
try:
|
||||
autosign = clients.getboolean(authority_name, "autosign")
|
||||
except NoOptionError:
|
||||
autosign = True
|
||||
|
||||
if not os.path.exists(certificate_path) or renew:
|
||||
# Set up URL-s
|
||||
request_params = set()
|
||||
request_params.add("autosign=true")
|
||||
request_params.add("autosign=%s" % ("yes" if autosign else "no"))
|
||||
if not no_wait:
|
||||
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
|
||||
request_url = "https://%s:8443/api/request/" % authority_name
|
||||
kwargs["cert"] = certificate_path, key_path
|
||||
click.echo("Renewing using current keypair at %s %s" % kwargs["cert"])
|
||||
else:
|
||||
# If machine is joined to domain attempt to present machine credentials for authentication
|
||||
if kerberos:
|
||||
@ -416,6 +427,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
||||
elif submission.status_code == requests.codes.gone:
|
||||
# Should the client retry or disable request submission?
|
||||
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:
|
||||
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")
|
||||
@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("--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("--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("--directory", help="Directory for authority files")
|
||||
@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("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
|
||||
@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"
|
||||
|
||||
import pwd
|
||||
@ -992,7 +1003,8 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
||||
cython3 python3-dev python3-mimeparse \
|
||||
python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
|
||||
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 --pre --upgrade python-ldap")
|
||||
|
||||
@ -1096,9 +1108,18 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
||||
else:
|
||||
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
|
||||
|
||||
bootstrap_pid = os.fork()
|
||||
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
|
||||
bundle_js = os.path.join(assets_dir, "js", "bundle.js")
|
||||
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)
|
||||
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
|
||||
if skip_packages:
|
||||
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_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
|
||||
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
|
||||
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):
|
||||
click.echo("Creating %s" % const.CONFIG_DIR)
|
||||
os.makedirs(const.CONFIG_DIR)
|
||||
|
||||
os.umask(0o137) # 640
|
||||
if os.path.exists(const.SERVER_CONFIG_PATH):
|
||||
click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
|
||||
else:
|
||||
os.umask(0o137)
|
||||
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:
|
||||
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()))
|
||||
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):
|
||||
click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
|
||||
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()))
|
||||
click.echo("File %s created" % const.PROFILE_CONFIG_PATH)
|
||||
|
||||
# Create directory with 755 permissions
|
||||
os.umask(0o022)
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
if not os.path.exists("/var/lib/certidude/builder"):
|
||||
click.echo("Creating %s" % "/var/lib/certidude/builder")
|
||||
os.makedirs("/var/lib/certidude/builder")
|
||||
|
||||
# Create subdirectories with 770 permissions
|
||||
os.umask(0o007)
|
||||
@ -1191,10 +1212,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
||||
os.mkdir(path)
|
||||
else:
|
||||
click.echo("Directory already exists %s" % path)
|
||||
assert os.stat(path).st_mode == 0o40770
|
||||
|
||||
# Create SQLite database file with correct permissions
|
||||
os.umask(0o117)
|
||||
if not os.path.exists(sqlite_path):
|
||||
os.umask(0o117)
|
||||
with open(sqlite_path, "wb") as fh:
|
||||
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)
|
||||
public_key, private_key = asymmetric.generate_pair("rsa", bit_size=const.KEY_SIZE)
|
||||
|
||||
names = (
|
||||
("country_name", country),
|
||||
("state_or_province_name", state),
|
||||
("locality_name", locality),
|
||||
("organization_name", organization),
|
||||
("common_name", title)
|
||||
)
|
||||
|
||||
# https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx
|
||||
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
|
||||
)
|
||||
builder.self_signed = True
|
||||
@ -1239,7 +1255,13 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
||||
os.umask(0o177)
|
||||
with open(ca_key, 'wb') as f:
|
||||
f.write(asymmetric.dump_private_key(private_key, None))
|
||||
|
||||
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:
|
||||
os.waitpid(bootstrap_pid, 0)
|
||||
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)
|
||||
continue
|
||||
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))
|
||||
|
||||
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)))
|
||||
|
||||
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:
|
||||
click.echo("r " + path)
|
||||
continue
|
||||
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("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)
|
||||
dump_common(common_name, path, cert)
|
||||
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("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
|
||||
def certidude_sign(common_name, overwrite, profile):
|
||||
from certidude import authority
|
||||
from certidude import authority, config
|
||||
drop_privileges()
|
||||
cert = authority.sign(common_name, overwrite=overwrite, profile=config.PROFILES[profile])
|
||||
|
||||
|
||||
@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")
|
||||
def certidude_revoke(common_name):
|
||||
def certidude_revoke(common_name, reason):
|
||||
from certidude import authority
|
||||
drop_privileges()
|
||||
authority.revoke(common_name)
|
||||
authority.revoke(common_name, reason)
|
||||
|
||||
|
||||
@click.command("expire", help="Move expired certificates")
|
||||
@ -1377,13 +1400,13 @@ def certidude_expire():
|
||||
threshold = datetime.utcnow() - timedelta(minutes=5) # Kerberos tolerance
|
||||
for common_name, path, buf, cert, signed, expires in authority.list_signed():
|
||||
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))
|
||||
os.rename(path, expired_path)
|
||||
os.remove(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number))
|
||||
for common_name, path, buf, cert, signed, expires, revoked in authority.list_revoked():
|
||||
os.remove(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number))
|
||||
for common_name, path, buf, cert, signed, expires, revoked, reason in authority.list_revoked():
|
||||
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))
|
||||
os.rename(path, expired_path)
|
||||
# TODO: Send e-mail
|
||||
@ -1412,7 +1435,7 @@ def certidude_serve(port, listen, fork):
|
||||
|
||||
# Rebuild reverse mapping
|
||||
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):
|
||||
click.echo("Linking %s to ../%s.pem" % (by_serial, cn))
|
||||
os.symlink("../%s.pem" % cn, by_serial)
|
||||
@ -1423,14 +1446,6 @@ def certidude_serve(port, listen, fork):
|
||||
os.makedirs(const.RUN_DIR)
|
||||
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" %
|
||||
", ".join([str(j) for j in config.USER_SUBNETS]))
|
||||
click.echo("Administrative subnets: %s" %
|
||||
@ -1440,10 +1455,6 @@ def certidude_serve(port, listen, fork):
|
||||
click.echo("Request submissions allowed from following subnets: %s" %
|
||||
", ".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))
|
||||
from wsgiref.simple_server import make_server, WSGIServer
|
||||
from certidude.api import certidude_app
|
||||
@ -1474,7 +1485,6 @@ def certidude_serve(port, listen, fork):
|
||||
for handler in log_handlers:
|
||||
j.addHandler(handler)
|
||||
|
||||
|
||||
if not fork or not os.fork():
|
||||
pid = os.getpid()
|
||||
with open(const.SERVER_PID_PATH, "w") as pidfile:
|
||||
|
@ -3,6 +3,65 @@ import os
|
||||
import click
|
||||
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):
|
||||
"""
|
||||
Fix OpenVPN credential store security context on Fedora
|
||||
|
@ -18,6 +18,7 @@ ACCOUNTS_BACKEND = cp.get("accounts", "backend") # posix, ldap
|
||||
MAIL_SUFFIX = cp.get("accounts", "mail suffix")
|
||||
|
||||
KERBEROS_KEYTAB = cp.get("authentication", "kerberos keytab")
|
||||
KERBEROS_REALM = cp.get("authentication", "kerberos realm")
|
||||
LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri")
|
||||
LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache")
|
||||
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])
|
||||
RENEWAL_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
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_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")
|
||||
|
||||
MACHINE_ENROLLMENT_ALLOWED = {
|
||||
"forbidden": False, "allowed": True }[
|
||||
cp.get("authority", "machine enrollment")]
|
||||
USER_ENROLLMENT_ALLOWED = {
|
||||
"forbidden": False, "single allowed": True, "multiple allowed": True }[
|
||||
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()]
|
||||
|
||||
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")
|
||||
|
@ -6,9 +6,9 @@ import sys
|
||||
|
||||
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
|
||||
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_HOSTNAME = "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
|
||||
RE_COMMON_NAME = "^[A-Za-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_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$"
|
||||
|
||||
RUN_DIR = "/run/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]
|
||||
except socket.gaierror:
|
||||
FQDN = socket.gethostname()
|
||||
if hasattr(FQDN, "decode"): # Keep client backwards compatible with Python 2.x
|
||||
FQDN = FQDN.decode("ascii")
|
||||
|
||||
try:
|
||||
HOSTNAME, DOMAIN = FQDN.split(".", 1)
|
||||
|
@ -56,8 +56,8 @@
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<a href="http://github.com/laurivosandi/certidude">Certidude</a> by
|
||||
<a href="http://github.com/laurivosandi/">Lauri Võsandi</a>
|
||||
<a href="https://github.com/laurivosandi/certidude">Certidude</a> by
|
||||
<a href="https://github.com/laurivosandi/">Lauri Võsandi</a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
@ -305,7 +305,8 @@ function loadAuthority() {
|
||||
/**
|
||||
* 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();
|
||||
if (session.authority) {
|
||||
$("#log input").each(function(i, e) {
|
||||
@ -462,12 +463,8 @@ function datetimeFilter(s) {
|
||||
}
|
||||
|
||||
function serialFilter(s) {
|
||||
return s.substring(0,8) + " " +
|
||||
s.substring(8,12) + " " +
|
||||
s.substring(12,16) + " " +
|
||||
s.substring(16,28) + " " +
|
||||
s.substring(28,32) + " " +
|
||||
s.substring(32);
|
||||
return s.substring(0,s.length-14) + " " +
|
||||
s.substring(s.length-14);
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
|
@ -7,26 +7,25 @@
|
||||
</div>
|
||||
<form action="/api/request/" method="post">
|
||||
<div class="modal-body">
|
||||
<h5>Certidude client</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>
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5>
|
||||
|
||||
<p>On Windows execute following PowerShell script</p>
|
||||
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>$hostname = $env:computername.ToLower()
|
||||
$templ = @"
|
||||
[Version]
|
||||
Signature="$Windows NT$
|
||||
<pre class="code"><code># Install CA certificate
|
||||
@"
|
||||
{{ session.authority.certificate.blob }}
|
||||
"@ | 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]
|
||||
Subject = "CN=$hostname"
|
||||
Exportable = FALSE
|
||||
@ -39,21 +38,14 @@ RequestType = PKCS10
|
||||
KeyAlgorithm = ECDSA_P384
|
||||
{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
|
||||
KeyLength = 2048
|
||||
{% endif %}"@
|
||||
|
||||
$templ | Out-File req.inf
|
||||
|
||||
# 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
|
||||
{% endif %}"@ | Out-File req.inf
|
||||
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem
|
||||
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
|
||||
|
||||
# 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
|
||||
Remove-VpnConnection -AllUserConnection -Force k-space
|
||||
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
|
||||
PfsGroup = one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24
|
||||
-->
|
||||
{% endif %}
|
||||
|
||||
<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">
|
||||
<pre class="code"><code>NAME=$(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 %}
|
||||
openssl req -new -sha384 -key client_key.pem -out client_req.pem -subj "/CN=$NAME";
|
||||
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>
|
||||
<pre class="code"><code>test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname)
|
||||
test -e /bin/hostname && NAME=$(hostname)
|
||||
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
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<p>First enroll certificates:</p>
|
||||
<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, patch Fedora paths
|
||||
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.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
|
||||
|
||||
FQDN=$(cat /etc/hostname)
|
||||
|
||||
# Install CA certificate
|
||||
cat << EOF > /etc/ipsec.d/cacerts/ca_cert.pem
|
||||
{{ session.authority.certificate.blob }}EOF
|
||||
|
||||
# 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>
|
||||
# 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
|
||||
ln /etc/certidude/authority/{{ authority_name }}/host_cert.pem /etc/ipsec.d/certs/{{ authority_name }}.pem
|
||||
ln /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>To configure StrongSwan as roadwarrior:</p>
|
||||
<div class="highlight">
|
||||
<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
|
||||
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
|
||||
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 %}!
|
||||
esp=aes128gcm16-aes128gmac!
|
||||
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
<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">
|
||||
<pre class="code"><code>opkg install curl libmbedtls
|
||||
# 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)
|
||||
|
||||
mkdir -p /etc/certidude/authority/{{ window.location.hostname }}
|
||||
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
|
||||
cat <<\EOF > /etc/certidude/authority/{{ window.location.hostname }}/updown
|
||||
cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown
|
||||
#!/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
|
||||
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
|
||||
EOF
|
||||
|
||||
chmod +x /etc/certidude/authority/{{ window.location.hostname }}/updown
|
||||
chmod +x /etc/certidude/authority/{{ authority_name }}/updown
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
|
||||
{% if "openvpn" in session.service.protocols %}
|
||||
|
||||
<p>Then either set up OpenVPN service:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>opkg update
|
||||
opkg install curl openssl-util openvpn-openssl
|
||||
|
||||
{% if session.authority.certificate.algorithm != "ec" %}
|
||||
# Generate Diffie-Hellman parameters file for OpenVPN
|
||||
test -e /etc/certidude/dh.pem \
|
||||
|| openssl dhparam 2048 -out /etc/certidude/dh.pem
|
||||
|
||||
{% endif %}
|
||||
# Create interface definition for tunnel
|
||||
uci set network.vpn=interface
|
||||
uci set network.vpn.name='vpn'
|
||||
@ -267,10 +365,10 @@ for section in s2c_tcp s2c_udp; do
|
||||
# Common paths
|
||||
uci set openvpn.$section.script_security=2
|
||||
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.cert='/etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem'
|
||||
uci set openvpn.$section.ca='/etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem'
|
||||
uci set openvpn.$section.dh='/etc/certidude/dh.pem'
|
||||
uci set openvpn.$section.key='/etc/certidude/authority/{{ authority_name }}/host_key.pem'
|
||||
uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem'
|
||||
uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem'
|
||||
{% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %}
|
||||
uci set openvpn.$section.enabled=1
|
||||
|
||||
# DNS and routes
|
||||
@ -291,6 +389,9 @@ done
|
||||
/etc/init.d/firewall restart</code></pre>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<p>Alternatively or additionally set up StrongSwan:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>opkg update
|
||||
@ -302,31 +403,46 @@ config setup
|
||||
strictcrlpolicy=yes
|
||||
uniqueids = yes
|
||||
|
||||
ca {{ window.location.hostname }}
|
||||
ca {{ authority_name }}
|
||||
auto=add
|
||||
cacert = /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem
|
||||
{% if session.features.crl %} crluri = http://{{ window.location.hostname }}/api/revoked/{% endif %}
|
||||
{% if session.features.ocsp %} ocspuri = http://{{ window.location.hostname }}/api/ocsp/{% endif %}
|
||||
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 s2c
|
||||
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
|
||||
conn default-{{ authority_name }}
|
||||
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||
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
|
||||
|
||||
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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if session.authority.builder %}
|
||||
<h5>OpenWrt/LEDE image builder</h5>
|
||||
@ -339,7 +455,7 @@ ipsec restart</code></pre>
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
|
||||
@ -367,10 +483,10 @@ ipsec restart</code></pre>
|
||||
<h4 class="modal-title">Revocation lists</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>To fetch <a href="http://{{window.location.hostname}}/api/revoked/">certificate revocation list</a>:</p>
|
||||
<pre><code>curl http://{{window.location.hostname}}/api/revoked/ > crl.der
|
||||
curl http://{{window.location.hostname}}/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>
|
||||
<p>To fetch <a href="http://{{authority_name}}/api/revoked/">certificate revocation list</a>:</p>
|
||||
<pre><code>curl http://{{authority_name}}/api/revoked/ > crl.der
|
||||
curl http://{{authority_name}}/api/revoked/ -L -H "Accept: application/x-pem-file"
|
||||
curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" data-dismiss="modal">Close</button>
|
||||
|
@ -4,5 +4,5 @@ Last seen
|
||||
at
|
||||
<a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %}
|
||||
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 %}.
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
<i class="fa fa-folder" aria-hidden="true"></i>
|
||||
{{ certificate.organizational_unit }} /
|
||||
{% 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>
|
||||
{% else %}
|
||||
<i class="fa fa-laptop"></i>
|
||||
@ -46,11 +46,11 @@
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<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="#"
|
||||
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="#"
|
||||
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>
|
||||
|
||||
@ -88,8 +88,10 @@ openssl ocsp -issuer session.pem -CAfile session.pem \
|
||||
{% endif %}
|
||||
|
||||
<p>To fetch script:</p>
|
||||
<pre><code class="language-bash" data-lang="bash">cd /var/lib/certidude/{{ window.location.hostname }}/
|
||||
curl --cert client_cert.pem https://{{ window.location.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/</pre></code>
|
||||
<pre><code class="language-bash" data-lang="bash">curl https://{{ window.location.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/ \
|
||||
--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%;">
|
||||
<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>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>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -12,12 +12,28 @@
|
||||
# No tags
|
||||
{% endif %}
|
||||
|
||||
# Submit some stats to CA
|
||||
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)&\
|
||||
ARGS="kernel=$(uname -sr)&\
|
||||
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]*; do echo -en if.$(basename $j).ether=$(cat $j/address)\&; done)"
|
||||
$(for j in /sys/class/net/[we]*[a-z][0-9]; 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"
|
||||
|
@ -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]
|
||||
# Title shown in the UI
|
||||
title = TP-Link Archer C7 (Access Point)
|
||||
|
||||
# Script to build the image, copy file to /etc/certidude/ and make modifications as necessary
|
||||
command = {{ doc_path }}/build-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 =
|
||||
command = {{ doc_path }}/builder/ap.sh
|
||||
|
||||
# Device/model/profile selection
|
||||
model = archer-c7-v2
|
||||
@ -22,10 +27,21 @@ rename = ArcherC7v2_tp_recovery.bin
|
||||
|
||||
[cf-e380ac]
|
||||
title = Comfast E380AC (Access Point)
|
||||
command = {{ doc_path }}/build-ap.sh
|
||||
overlay = {{ doc_path }}/overlay
|
||||
script =
|
||||
command = {{ doc_path }}/builder/ap.sh
|
||||
model = cf-e380ac-v2
|
||||
filename = cf-e380ac-v2-squashfs-factory.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
|
||||
|
@ -138,8 +138,9 @@ server {
|
||||
server_name {{ common_name }};
|
||||
listen 8443 ssl http2;
|
||||
|
||||
# Require client authentication with certificate
|
||||
ssl_verify_client on;
|
||||
# Allow client authentication with certificate,
|
||||
# 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;
|
||||
|
||||
# Proxy pass to backend
|
||||
|
@ -22,31 +22,37 @@ extended key usage = client_auth
|
||||
|
||||
[srv]
|
||||
title = Server
|
||||
;ou = Server
|
||||
ou = Server
|
||||
common name = RE_FQDN
|
||||
lifetime = 365
|
||||
extended key usage = server_auth
|
||||
lifetime = 120
|
||||
extended key usage = server_auth client_auth
|
||||
|
||||
[gw]
|
||||
title = Gateway
|
||||
ou = Gateway
|
||||
common name = RE_FQDN
|
||||
renewable = true
|
||||
lifetime = 30
|
||||
lifetime = 120
|
||||
extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth
|
||||
|
||||
[ap]
|
||||
title = Access Point
|
||||
ou = Access Point
|
||||
common name = RE_HOSTNAME
|
||||
lifetime = 1825
|
||||
lifetime = 120
|
||||
extended key usage = client_auth
|
||||
|
||||
[mfp]
|
||||
title = Printers
|
||||
ou = Printers
|
||||
ou = MFP
|
||||
common name = ^mfp\-
|
||||
lifetime = 30
|
||||
lifetime = 120
|
||||
extended key usage = client_auth
|
||||
|
||||
[cam]
|
||||
title = Camera
|
||||
ou = IP Camera
|
||||
common name = ^cam\-
|
||||
lifetime = 120
|
||||
extended key usage = client_auth
|
||||
|
||||
|
@ -4,13 +4,31 @@
|
||||
# sshd PAM service. In case of 'kerberos' SPNEGO is used to authenticate
|
||||
# user against eg. Active Directory or Samba4.
|
||||
|
||||
{% if realm %}
|
||||
;backends = pam
|
||||
backends = kerberos
|
||||
{% else %}
|
||||
backends = pam
|
||||
;backends = kerberos
|
||||
{% endif %}
|
||||
;backends = ldap
|
||||
;backends = kerberos ldap
|
||||
;backends = kerberos pam
|
||||
ldap uri = ldaps://dc.example.lan
|
||||
|
||||
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]
|
||||
# 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
|
||||
# updated automatically by /etc/cron.hourly/certidude
|
||||
|
||||
{% if not realm %}
|
||||
backend = posix
|
||||
{% else %}
|
||||
;backend = posix
|
||||
{% endif %}
|
||||
mail suffix = example.lan
|
||||
|
||||
{% if realm %}
|
||||
backend = ldap
|
||||
{% else %}
|
||||
;backend = ldap
|
||||
{% endif %}
|
||||
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]
|
||||
# The authorization backend specifies how the users are authorized.
|
||||
# In case of 'posix' simply group membership is asserted,
|
||||
# in case of 'ldap' search filter with username as placeholder is applied.
|
||||
|
||||
{% if realm %}
|
||||
;backend = posix
|
||||
{% else %}
|
||||
backend = posix
|
||||
{% endif %}
|
||||
posix user group = users
|
||||
posix admin group = sudo
|
||||
|
||||
{% if realm %}
|
||||
backend = ldap
|
||||
{% else %}
|
||||
;backend = ldap
|
||||
{% endif %}
|
||||
ldap computer filter = (&(objectclass=user)(objectclass=computer)(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
|
||||
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 = 0.0.0.0/0
|
||||
|
||||
# Online Certificate Status Protocol enabled subnets
|
||||
ocsp subnets =
|
||||
;ocsp subnets = 0.0.0.0/0
|
||||
# Online Certificate Status Protocol enabled subnets, anywhere by default
|
||||
;ocsp subnets =
|
||||
ocsp subnets = 0.0.0.0/0
|
||||
|
||||
# Certificate Revocation lists can be accessed from anywhere by default
|
||||
;crl subnets =
|
||||
@ -76,12 +130,24 @@ crl subnets = 0.0.0.0/0
|
||||
renewal subnets =
|
||||
;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]
|
||||
# Disable logging
|
||||
;backend =
|
||||
backend =
|
||||
|
||||
# Use SQLite backend
|
||||
backend = sql
|
||||
;backend = sql
|
||||
database = sqlite://{{ directory }}/meta/db.sqlite
|
||||
|
||||
[signature]
|
||||
@ -144,13 +210,6 @@ request submission allowed = false
|
||||
;user enrollment = single 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 }}
|
||||
certificate path = {{ ca_cert }}
|
||||
|
||||
@ -199,3 +258,7 @@ secret = {{ token_secret }}
|
||||
path = {{ script_dir }}
|
||||
;path = /etc/certidude/script
|
||||
;path =
|
||||
|
||||
[service]
|
||||
protocols = ikev2 https openvpn
|
||||
routers = ^router\d?\.
|
||||
|
@ -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"
|
||||
|
@ -109,11 +109,10 @@ esac
|
||||
EOF
|
||||
|
||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci luci-app-commands \
|
||||
openssl-util curl ca-certificates \
|
||||
strongswan-mod-kernel-libipsec kmod-tun ip-full strongswan-full \
|
||||
openssl-util curl ca-certificates dropbear \
|
||||
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 \
|
||||
-luci-app-firewall \
|
||||
-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"
|
||||
|
||||
|
@ -9,6 +9,12 @@ 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
|
||||
|
||||
# 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
|
||||
wget -q $URL -O $BUILD/$FILENAME
|
||||
fi
|
||||
@ -19,58 +25,94 @@ fi
|
||||
|
||||
# Copy CA certificate
|
||||
AUTHORITY=$(hostname -f)
|
||||
CERTIDUDE_DIR=/var/lib/certidude/$AUTHORITY
|
||||
|
||||
mkdir -p $OVERLAY/etc/config
|
||||
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/
|
||||
|
||||
echo /etc/certidude >> $OVERLAY/etc/sysupgrade.conf
|
||||
|
||||
cat <<EOF > $OVERLAY/etc/config/certidude
|
||||
|
||||
config authority
|
||||
option gateway router.k-space.ee
|
||||
option url http://$AUTHORITY
|
||||
option gateway "$ROUTER"
|
||||
option hostname "$AUTHORITY"
|
||||
option trigger wan
|
||||
option authority_path /etc/certidude/authority/$AUTHORITY/ca_cert.pem
|
||||
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_type $AUTHORITY_CERTIFICATE_ALGORITHM
|
||||
option key_length 2048
|
||||
option key_curve secp384r1
|
||||
|
||||
EOF
|
||||
|
||||
|
||||
cat << EOF > $OVERLAY/etc/uci-defaults/40-disable-ipsec
|
||||
/etc/init.d/ipsec disable
|
||||
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
|
||||
: RSA /etc/certidude/authority/$AUTHORITY/client_key.pem
|
||||
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"
|
||||
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
|
||||
|
||||
chmod +x $OVERLAY/etc/certidude/authority/$AUTHORITY/updown
|
||||
|
||||
cat << EOF > $OVERLAY/etc/ipsec.conf
|
||||
|
||||
config setup
|
||||
strictcrlpolicy=yes
|
||||
|
||||
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
|
||||
right=router.k-space.ee
|
||||
dpdaction=restart
|
||||
auto=start
|
||||
rightsubnet=0.0.0.0/0
|
||||
rightid=%any
|
||||
leftsourceip=%config
|
||||
keyexchange=ikev2
|
||||
closeaction=restart
|
||||
leftcert=/etc/certidude/authority/$AUTHORITY/client_cert.pem
|
||||
left=%defaultroute
|
||||
conn %default
|
||||
keyingtries=%forever
|
||||
dpdaction=restart
|
||||
closeaction=restart
|
||||
ike=aes256-sha384-ecp384!
|
||||
esp=aes128gcm16-aes128gmac!
|
||||
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
|
||||
|
||||
|
||||
cat << EOF > $OVERLAY/etc/uci-defaults/99-uhttpd-disable-https
|
||||
uci delete uhttpd.main.listen_https
|
||||
uci delete uhttpd.main.redirect_https
|
||||
EOF
|
||||
|
@ -38,6 +38,7 @@ uci certidude.@authority[0].green_led='gl-connect:green:lan'
|
||||
EOF
|
||||
|
||||
|
||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates strongswan-full htop \
|
||||
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci luci-app-mjpg-streamer kmod-video-uvc \
|
||||
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun ip-full"
|
||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates \
|
||||
strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm htop \
|
||||
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"
|
||||
|
@ -96,15 +96,15 @@ uci set firewall.@redirect[-1].target=DNAT
|
||||
uci set firewall.@redirect[-1].proto=tcp
|
||||
uci set firewall.@redirect[-1].enabled=0
|
||||
|
||||
uci set uhttpd.main.listen_http=0.0.0.0:8080
|
||||
|
||||
/etc/init.d/dropbear disable
|
||||
|
||||
uci set uhttpd.main.listen_http=0.0.0.0:8080
|
||||
|
||||
EOF
|
||||
|
||||
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 \
|
||||
strongswan-mod-kernel-libipsec kmod-tun ip-full strongswan-full \
|
||||
pciutils -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm"
|
||||
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci dropbear kmod-tun \
|
||||
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 bc"
|
||||
|
||||
|
||||
|
10
doc/overlay/etc/hotplug.d/iface/50-certidude
Normal file
10
doc/overlay/etc/hotplug.d/iface/50-certidude
Normal 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
|
9
doc/overlay/etc/uci-defaults/60-cron
Normal file
9
doc/overlay/etc/uci-defaults/60-cron
Normal 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
|
||||
|
123
doc/overlay/usr/bin/certidude-enroll
Executable file
123
doc/overlay/usr/bin/certidude-enroll
Executable 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
|
25
doc/overlay/usr/bin/certidude-enroll-renew
Executable file
25
doc/overlay/usr/bin/certidude-enroll-renew
Executable 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
|
@ -60,12 +60,13 @@ def clean_client():
|
||||
files = [
|
||||
"/etc/certidude/client.conf",
|
||||
"/etc/certidude/services.conf",
|
||||
"/var/lib/certidude/ca.example.lan/client_key.pem",
|
||||
"/var/lib/certidude/ca.example.lan/server_key.pem",
|
||||
"/var/lib/certidude/ca.example.lan/client_req.pem",
|
||||
"/var/lib/certidude/ca.example.lan/server_req.pem",
|
||||
"/var/lib/certidude/ca.example.lan/client_cert.pem",
|
||||
"/var/lib/certidude/ca.example.lan/server_cert.pem",
|
||||
"/etc/certidude/authority/ca.example.lan/ca_cert.pem",
|
||||
"/etc/certidude/authority/ca.example.lan/client_key.pem",
|
||||
"/etc/certidude/authority/ca.example.lan/server_key.pem",
|
||||
"/etc/certidude/authority/ca.example.lan/client_req.pem",
|
||||
"/etc/certidude/authority/ca.example.lan/server_req.pem",
|
||||
"/etc/certidude/authority/ca.example.lan/client_cert.pem",
|
||||
"/etc/certidude/authority/ca.example.lan/server_cert.pem",
|
||||
]
|
||||
for path in files:
|
||||
if os.path.exists(path):
|
||||
@ -85,6 +86,8 @@ def clean_client():
|
||||
|
||||
|
||||
def clean_server():
|
||||
os.umask(0o22)
|
||||
|
||||
if os.path.exists("/run/certidude/server.pid"):
|
||||
with open("/run/certidude/server.pid") as fh:
|
||||
try:
|
||||
@ -95,30 +98,29 @@ def clean_server():
|
||||
|
||||
if os.path.exists("/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"):
|
||||
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
|
||||
if os.path.exists("/etc/systemd/system/certidude.service"):
|
||||
os.unlink("/etc/systemd/system/certidude.service")
|
||||
files = [
|
||||
"/etc/krb5.keytab",
|
||||
"/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
|
||||
if os.path.exists("/etc/nginx/sites-available/ca.conf"):
|
||||
os.unlink("/etc/nginx/sites-available/ca.conf")
|
||||
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")
|
||||
for filename in files:
|
||||
if os.path.exists(filename):
|
||||
os.unlink(filename)
|
||||
|
||||
# Remove OpenVPN stuff
|
||||
if os.path.exists("/etc/openvpn"):
|
||||
@ -135,8 +137,6 @@ def clean_server():
|
||||
os.kill(int(fh.read()), 15)
|
||||
except OSError:
|
||||
pass
|
||||
if os.path.exists("/etc/certidude/server.keytab"):
|
||||
os.unlink("/etc/certidude/server.keytab")
|
||||
os.system("rm -Rfv /var/lib/samba/*")
|
||||
|
||||
# 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 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("KRB5_KTNAME"), "Environment contaminated"
|
||||
@ -189,39 +189,6 @@ def test_cli_setup_authority():
|
||||
if "userbot" not in buf:
|
||||
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)
|
||||
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 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
|
||||
r = client().simulate_get("/api/signed/nonexistant/")
|
||||
assert r.status_code == 404, r.text
|
||||
@ -574,7 +532,7 @@ def test_cli_setup_authority():
|
||||
# Test tagging integration in scripting framework
|
||||
r = client().simulate_get("/api/signed/test/script/")
|
||||
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
|
||||
|
||||
r = client().simulate_post("/api/signed/test/tag/",
|
||||
@ -640,7 +598,7 @@ def test_cli_setup_authority():
|
||||
headers={"Authorization":admintoken})
|
||||
assert r.status_code == 200, r.text
|
||||
assert "Revoked " in inbox.pop(), inbox
|
||||
|
||||
"""
|
||||
|
||||
# Log can be read only by admin
|
||||
r = client().simulate_get("/api/log/")
|
||||
@ -652,7 +610,7 @@ def test_cli_setup_authority():
|
||||
headers={"Authorization":admintoken})
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.headers.get('content-type') == "application/json; charset=UTF-8"
|
||||
|
||||
"""
|
||||
|
||||
# Test session API call
|
||||
r = client().simulate_get("/api/")
|
||||
@ -708,11 +666,14 @@ def test_cli_setup_authority():
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
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"])
|
||||
assert not result.exception, 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()
|
||||
if not child_pid:
|
||||
@ -727,12 +688,12 @@ def test_cli_setup_authority():
|
||||
assert not result.exception, result.output
|
||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||
assert "Writing certificate to:" in result.output, result.output
|
||||
assert os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
|
||||
|
||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--renew", "--no-wait"])
|
||||
assert not result.exception, result.output
|
||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||
#assert "Writing certificate to:" in result.output, result.output
|
||||
assert "Attached renewal signature" in result.output, result.output
|
||||
assert "Renewing using current keypair" in result.output, result.output
|
||||
|
||||
# Test nginx setup
|
||||
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
|
||||
|
||||
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"):
|
||||
os.makedirs("/etc/openvpn/keys")
|
||||
@ -761,12 +722,13 @@ def test_cli_setup_authority():
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
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"])
|
||||
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("/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 os.path.exists("/run/certidude/ca.example.lan.pid"), 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")
|
||||
|
||||
# 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"])
|
||||
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")
|
||||
|
||||
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:
|
||||
fh.write("insecure = true\n")
|
||||
fh.write("autosign = false\n")
|
||||
|
||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
||||
assert not result.exception, result.output
|
||||
assert "(Autosign failed, only client certificates allowed to be signed automatically" in result.output, result.output
|
||||
assert "(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("/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 "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
|
||||
|
||||
@ -1073,12 +1036,39 @@ def test_cli_setup_authority():
|
||||
|
||||
r = requests.get("http://ca.example.lan/api/scep/")
|
||||
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/")
|
||||
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/")
|
||||
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
|
||||
os.kill(server_pid, 15)
|
||||
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)
|
||||
|
||||
# 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
|
||||
assert os.system("kdestroy") == 0
|
||||
assert not os.path.exists("/tmp/krb5cc_0")
|
||||
@ -1116,13 +1151,11 @@ def test_cli_setup_authority():
|
||||
# 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 = 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/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/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/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf")
|
||||
from certidude.common import pip
|
||||
@ -1157,41 +1190,25 @@ def test_cli_setup_authority():
|
||||
if r.status_code != 502:
|
||||
break
|
||||
sleep(1)
|
||||
assert r.status_code == 401
|
||||
assert r.status_code == 401, "Timed out starting up the API backend"
|
||||
|
||||
# CRL-s disabled now
|
||||
r = requests.get("http://ca.example.lan/api/revoked/")
|
||||
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/")
|
||||
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/")
|
||||
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/")
|
||||
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 ###
|
||||
##################
|
||||
|
||||
os.umask(0o022)
|
||||
if not os.path.exists("/tmp/sscep"):
|
||||
assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep")
|
||||
if not os.path.exists("/tmp/sscep/sscep_dyn"):
|
||||
@ -1283,7 +1299,7 @@ def test_cli_setup_authority():
|
||||
if not os.path.exists("/tmp/key.pem"):
|
||||
assert not os.system("openssl genrsa -out /tmp/key.pem 1024")
|
||||
if not os.path.exists("/tmp/req.pem"):
|
||||
assert not os.system("echo '.\n.\n.\n.\n.\ntest8\n\n\n\n' | openssl req -new -sha256 -key /tmp/key.pem -out /tmp/req.pem")
|
||||
assert not os.system("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")
|
||||
# TODO: test e-mails at this point
|
||||
|
||||
@ -1301,13 +1317,15 @@ def test_cli_setup_authority():
|
||||
# Shut down server
|
||||
assert os.path.exists("/proc/%d" % server_pid)
|
||||
os.kill(server_pid, 15)
|
||||
# sleep(2)
|
||||
# os.kill(server_pid, 9)
|
||||
os.waitpid(server_pid, 0)
|
||||
|
||||
# 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() == \
|
||||
"/var/lib/certidude/ca.example.lan/client_key.pem r,\n" + \
|
||||
"/var/lib/certidude/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_key.pem r,\n" + \
|
||||
"/etc/certidude/authority/ca.example.lan/ca_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
|
||||
|
||||
os.system("service nginx stop")
|
||||
|
Loading…
Reference in New Issue
Block a user