mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
Several updates
* Subnets configuration option for Kerberos machine enrollment * Configurable script snippets via [service] configuration section * Preliminary revocation reason support * Improved signature profile support * Add domain components to DN to distinguish certificate CN's namespace * Image builder improvements, add Elliptic Curve support * Added GetCACaps operation and more digest algorithms for SCEP * Generate certificate and CRL serial from timestamp (64+32bits) and random bytes (56bits) * Move client storage pool to /etc/certidude/authority/ * Cleanups & bugfixes
This commit is contained in:
parent
94e5f72566
commit
5e9251f365
4
.gitignore
vendored
4
.gitignore
vendored
@ -64,3 +64,7 @@ node_modules/
|
|||||||
# Ignore patch
|
# Ignore patch
|
||||||
*.orig
|
*.orig
|
||||||
*.rej
|
*.rej
|
||||||
|
|
||||||
|
lextab.py
|
||||||
|
yacctab.py
|
||||||
|
.pytest_cache
|
||||||
|
@ -8,6 +8,7 @@ import hashlib
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from xattr import listxattr, getxattr
|
from xattr import listxattr, getxattr
|
||||||
from certidude.auth import login_required
|
from certidude.auth import login_required
|
||||||
|
from certidude.common import cert_to_dn
|
||||||
from certidude.user import User
|
from certidude.user import User
|
||||||
from certidude.decorators import serialize, csrf_protection
|
from certidude.decorators import serialize, csrf_protection
|
||||||
from certidude import const, config, authority
|
from certidude import const, config, authority
|
||||||
@ -54,7 +55,7 @@ class SessionResource(AuthorityHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def serialize_revoked(g):
|
def serialize_revoked(g):
|
||||||
for common_name, path, buf, cert, signed, expired, revoked in g():
|
for common_name, path, buf, cert, signed, expired, revoked, reason in g():
|
||||||
yield dict(
|
yield dict(
|
||||||
serial = "%x" % cert.serial_number,
|
serial = "%x" % cert.serial_number,
|
||||||
common_name = common_name,
|
common_name = common_name,
|
||||||
@ -62,6 +63,7 @@ class SessionResource(AuthorityHandler):
|
|||||||
signed = signed,
|
signed = signed,
|
||||||
expired = expired,
|
expired = expired,
|
||||||
revoked = revoked,
|
revoked = revoked,
|
||||||
|
reason = reason,
|
||||||
sha256sum = hashlib.sha256(buf).hexdigest())
|
sha256sum = hashlib.sha256(buf).hexdigest())
|
||||||
|
|
||||||
def serialize_certificates(g):
|
def serialize_certificates(g):
|
||||||
@ -69,7 +71,7 @@ class SessionResource(AuthorityHandler):
|
|||||||
# Extract certificate tags from filesystem
|
# Extract certificate tags from filesystem
|
||||||
try:
|
try:
|
||||||
tags = []
|
tags = []
|
||||||
for tag in getxattr(path, "user.xdg.tags").decode("ascii").split(","):
|
for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","):
|
||||||
if "=" in tag:
|
if "=" in tag:
|
||||||
k, v = tag.split("=", 1)
|
k, v = tag.split("=", 1)
|
||||||
else:
|
else:
|
||||||
@ -116,7 +118,7 @@ class SessionResource(AuthorityHandler):
|
|||||||
extensions = dict([
|
extensions = dict([
|
||||||
(e["extn_id"].native, e["extn_value"].native)
|
(e["extn_id"].native, e["extn_value"].native)
|
||||||
for e in cert["tbs_certificate"]["extensions"]
|
for e in cert["tbs_certificate"]["extensions"]
|
||||||
if e["extn_value"] in ("extended_key_usage",)])
|
if e["extn_id"].native in ("extended_key_usage",)])
|
||||||
)
|
)
|
||||||
|
|
||||||
if req.context.get("user").is_admin():
|
if req.context.get("user").is_admin():
|
||||||
@ -131,6 +133,11 @@ class SessionResource(AuthorityHandler):
|
|||||||
mail=req.context.get("user").mail
|
mail=req.context.get("user").mail
|
||||||
),
|
),
|
||||||
request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED,
|
request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED,
|
||||||
|
service = dict(
|
||||||
|
protocols = config.SERVICE_PROTOCOLS,
|
||||||
|
routers = [j[0] for j in authority.list_signed(
|
||||||
|
common_name=config.SERVICE_ROUTERS)]
|
||||||
|
),
|
||||||
authority = dict(
|
authority = dict(
|
||||||
builder = dict(
|
builder = dict(
|
||||||
profiles = config.IMAGE_BUILDER_PROFILES
|
profiles = config.IMAGE_BUILDER_PROFILES
|
||||||
@ -143,13 +150,15 @@ class SessionResource(AuthorityHandler):
|
|||||||
certificate = dict(
|
certificate = dict(
|
||||||
algorithm = authority.public_key.algorithm,
|
algorithm = authority.public_key.algorithm,
|
||||||
common_name = self.authority.certificate.subject.native["common_name"],
|
common_name = self.authority.certificate.subject.native["common_name"],
|
||||||
|
distinguished_name = cert_to_dn(self.authority.certificate),
|
||||||
|
md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(),
|
||||||
blob = self.authority.certificate_buf.decode("ascii"),
|
blob = self.authority.certificate_buf.decode("ascii"),
|
||||||
),
|
),
|
||||||
mailer = dict(
|
mailer = dict(
|
||||||
name = config.MAILER_NAME,
|
name = config.MAILER_NAME,
|
||||||
address = config.MAILER_ADDRESS
|
address = config.MAILER_ADDRESS
|
||||||
) if config.MAILER_ADDRESS else None,
|
) if config.MAILER_ADDRESS else None,
|
||||||
machine_enrollment_allowed=config.MACHINE_ENROLLMENT_ALLOWED,
|
machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS,
|
||||||
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
|
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
|
||||||
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
|
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
|
||||||
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
|
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
|
||||||
@ -278,8 +287,8 @@ def certidude_app(log_handlers=[]):
|
|||||||
log_handlers.append(LogHandler(uri))
|
log_handlers.append(LogHandler(uri))
|
||||||
app.add_route("/api/log/", LogResource(uri))
|
app.add_route("/api/log/", LogResource(uri))
|
||||||
elif config.LOGGING_BACKEND == "syslog":
|
elif config.LOGGING_BACKEND == "syslog":
|
||||||
from logging.handlers import SyslogHandler
|
from logging.handlers import SysLogHandler
|
||||||
log_handlers.append(SyslogHandler())
|
log_handlers.append(SysLogHandler())
|
||||||
# Browsing syslog via HTTP is obviously not possible out of the box
|
# Browsing syslog via HTTP is obviously not possible out of the box
|
||||||
elif config.LOGGING_BACKEND:
|
elif config.LOGGING_BACKEND:
|
||||||
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)
|
raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND)
|
||||||
|
@ -26,14 +26,15 @@ class AttributeResource(object):
|
|||||||
Results made available only to lease IP address.
|
Results made available only to lease IP address.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
path, buf, cert, attribs = self.authority.get_attributes(cn, namespace=self.namespace)
|
path, buf, cert, attribs = self.authority.get_attributes(cn,
|
||||||
|
namespace=self.namespace, flat=True)
|
||||||
except IOError:
|
except IOError:
|
||||||
raise falcon.HTTPNotFound()
|
raise falcon.HTTPNotFound()
|
||||||
else:
|
else:
|
||||||
return attribs
|
return attribs
|
||||||
|
|
||||||
@csrf_protection
|
@csrf_protection
|
||||||
@whitelist_subject # TODO: sign instead
|
@whitelist_subject
|
||||||
def on_post(self, req, resp, cn):
|
def on_post(self, req, resp, cn):
|
||||||
namespace = ("user.%s." % self.namespace).encode("ascii")
|
namespace = ("user.%s." % self.namespace).encode("ascii")
|
||||||
try:
|
try:
|
||||||
@ -43,7 +44,7 @@ class AttributeResource(object):
|
|||||||
else:
|
else:
|
||||||
for key in req.params:
|
for key in req.params:
|
||||||
if not re.match("[a-z0-9_\.]+$", key):
|
if not re.match("[a-z0-9_\.]+$", key):
|
||||||
raise falcon.HTTPBadRequest("Invalid key")
|
raise falcon.HTTPBadRequest("Invalid key %s" % key)
|
||||||
valid = set()
|
valid = set()
|
||||||
for key, value in req.params.items():
|
for key, value in req.params.items():
|
||||||
identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii")
|
identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii")
|
||||||
|
@ -4,8 +4,9 @@ import falcon
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from certidude import config, const
|
from certidude import config, const, authority
|
||||||
from certidude.auth import login_required, authorize_admin
|
from certidude.auth import login_required, authorize_admin
|
||||||
|
from certidude.common import cert_to_dn
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -14,6 +15,8 @@ class ImageBuilderResource(object):
|
|||||||
@login_required
|
@login_required
|
||||||
@authorize_admin
|
@authorize_admin
|
||||||
def on_get(self, req, resp, profile, suggested_filename):
|
def on_get(self, req, resp, profile, suggested_filename):
|
||||||
|
router = [j[0] for j in authority.list_signed(
|
||||||
|
common_name=config.cp2.get(profile, "router"))][0]
|
||||||
model = config.cp2.get(profile, "model")
|
model = config.cp2.get(profile, "model")
|
||||||
build_script_path = config.cp2.get(profile, "command")
|
build_script_path = config.cp2.get(profile, "command")
|
||||||
overlay_path = config.cp2.get(profile, "overlay")
|
overlay_path = config.cp2.get(profile, "overlay")
|
||||||
@ -35,7 +38,10 @@ class ImageBuilderResource(object):
|
|||||||
stdout=open(log_path, "w"), stderr=subprocess.STDOUT,
|
stdout=open(log_path, "w"), stderr=subprocess.STDOUT,
|
||||||
close_fds=True, shell=False,
|
close_fds=True, shell=False,
|
||||||
cwd=os.path.dirname(os.path.realpath(build_script_path)),
|
cwd=os.path.dirname(os.path.realpath(build_script_path)),
|
||||||
env={"PROFILE":model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin",
|
env={"PROFILE": model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin",
|
||||||
|
"ROUTER": router,
|
||||||
|
"AUTHORITY_CERTIFICATE_ALGORITHM": authority.public_key.algorithm,
|
||||||
|
"AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME": cert_to_dn(authority.certificate),
|
||||||
"BUILD":build, "OVERLAY":build + "/overlay/"},
|
"BUILD":build, "OVERLAY":build + "/overlay/"},
|
||||||
startupinfo=None, creationflags=0)
|
startupinfo=None, creationflags=0)
|
||||||
proc.communicate()
|
proc.communicate()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import falcon
|
import falcon
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import xattr
|
import xattr
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from certidude import config, push
|
from certidude import config, push
|
||||||
@ -32,10 +33,9 @@ class LeaseResource(AuthorityHandler):
|
|||||||
@authorize_server
|
@authorize_server
|
||||||
def on_post(self, req, resp):
|
def on_post(self, req, resp):
|
||||||
client_common_name = req.get_param("client", required=True)
|
client_common_name = req.get_param("client", required=True)
|
||||||
if "=" in client_common_name: # It's actually DN, resolve it to CN
|
m = re.match("CN=(.+?),", client_common_name) # It's actually DN, resolve it to CN
|
||||||
_, client_common_name = client_common_name.split(" CN=", 1)
|
if m:
|
||||||
if "," in client_common_name:
|
client_common_name, = m.groups()
|
||||||
client_common_name, _ = client_common_name.split(",", 1)
|
|
||||||
|
|
||||||
path, buf, cert, signed, expires = self.authority.get_signed(client_common_name) # TODO: catch exceptions
|
path, buf, cert, signed, expires = self.authority.get_signed(client_common_name) # TODO: catch exceptions
|
||||||
if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan
|
if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan
|
||||||
|
@ -53,24 +53,29 @@ class OCSPResource(AuthorityHandler):
|
|||||||
assert serial > 0, "Serial number correctness check failed"
|
assert serial > 0, "Serial number correctness check failed"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % serial))
|
link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % serial))
|
||||||
assert link_target.startswith("../")
|
assert link_target.startswith("../")
|
||||||
assert link_target.endswith(".pem")
|
assert link_target.endswith(".pem")
|
||||||
path, buf, cert, signed, expires = self.authority.get_signed(link_target[3:-4])
|
path, buf, cert, signed, expires = self.authority.get_signed(link_target[3:-4])
|
||||||
if serial != cert.serial_number:
|
if serial != cert.serial_number:
|
||||||
logger.error("Certificate store integrity check failed, %s refers to certificate with serial %x" % (link_target, cert.serial_number))
|
logger.error("Certificate store integrity check failed, %s refers to certificate with serial %040x", link_target, cert.serial_number)
|
||||||
raise EnvironmentError("Integrity check failed")
|
raise EnvironmentError("Integrity check failed")
|
||||||
|
logger.debug("OCSP responder queried from %s for %s with serial %040x, returned status 'good'",
|
||||||
|
req.context.get("remote_addr"), cert.subject.native["common_name"], serial)
|
||||||
status = ocsp.CertStatus(name='good', value=None)
|
status = ocsp.CertStatus(name='good', value=None)
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
try:
|
try:
|
||||||
path, buf, cert, signed, expires, revoked = self.authority.get_revoked(serial)
|
path, buf, cert, signed, expires, revoked, reason = self.authority.get_revoked(serial)
|
||||||
|
logger.debug("OCSP responder queried from %s for %s with serial %040x, returned status 'revoked' due to %s",
|
||||||
|
req.context.get("remote_addr"), cert.subject.native["common_name"], serial, reason)
|
||||||
status = ocsp.CertStatus(
|
status = ocsp.CertStatus(
|
||||||
name='revoked',
|
name='revoked',
|
||||||
value={
|
value={
|
||||||
'revocation_time': revoked,
|
'revocation_time': revoked,
|
||||||
'revocation_reason': "key_compromise",
|
'revocation_reason': reason,
|
||||||
})
|
})
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
|
logger.info("OCSP responder queried for unknown serial %040x from %s", serial, req.context.get("remote_addr"))
|
||||||
status = ocsp.CertStatus(name="unknown", value=None)
|
status = ocsp.CertStatus(name="unknown", value=None)
|
||||||
|
|
||||||
responses.append({
|
responses.append({
|
||||||
|
@ -14,7 +14,7 @@ from certidude.profile import SignatureProfile
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from oscrypto import asymmetric
|
from oscrypto import asymmetric
|
||||||
from oscrypto.errors import SignatureError
|
from oscrypto.errors import SignatureError
|
||||||
from xattr import getxattr
|
from xattr import getxattr, setxattr
|
||||||
from .utils import AuthorityHandler
|
from .utils import AuthorityHandler
|
||||||
from .utils.firewall import whitelist_subnets, whitelist_content_types
|
from .utils.firewall import whitelist_subnets, whitelist_content_types
|
||||||
|
|
||||||
@ -59,26 +59,39 @@ class RequestListResource(AuthorityHandler):
|
|||||||
|
|
||||||
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
||||||
|
|
||||||
|
"""
|
||||||
|
Determine whether autosign is allowed to overwrite already issued
|
||||||
|
certificates automatically
|
||||||
|
"""
|
||||||
|
|
||||||
|
overwrite_allowed = False
|
||||||
|
for subnet in config.OVERWRITE_SUBNETS:
|
||||||
|
if req.context.get("remote_addr") in subnet:
|
||||||
|
overwrite_allowed = True
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Handle domain computer automatic enrollment
|
Handle domain computer automatic enrollment
|
||||||
"""
|
"""
|
||||||
machine = req.context.get("machine")
|
machine = req.context.get("machine")
|
||||||
if machine:
|
if machine:
|
||||||
if config.MACHINE_ENROLLMENT_ALLOWED:
|
reasons.append("machine enrollment not allowed from %s" % req.context.get("remote_addr"))
|
||||||
if common_name != machine:
|
for subnet in config.MACHINE_ENROLLMENT_SUBNETS:
|
||||||
raise falcon.HTTPBadRequest(
|
if req.context.get("remote_addr") in subnet:
|
||||||
"Bad request",
|
if common_name != machine:
|
||||||
"Common name %s differs from Kerberos credential %s!" % (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
|
Attempt to renew certificate using currently valid key pair
|
||||||
@ -94,58 +107,61 @@ class RequestListResource(AuthorityHandler):
|
|||||||
# Same public key
|
# Same public key
|
||||||
if cert_pk == csr_pk:
|
if cert_pk == csr_pk:
|
||||||
buf = req.get_header("X-SSL-CERT")
|
buf = req.get_header("X-SSL-CERT")
|
||||||
# Used mutually authenticated TLS handshake, assume renewal
|
|
||||||
if buf:
|
if buf:
|
||||||
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
|
# Used mutually authenticated TLS handshake, assume renewal
|
||||||
|
header, _, der_bytes = pem.unarmor(buf.replace("\t", "\n").replace("\n\n", "\n").encode("ascii"))
|
||||||
handshake_cert = x509.Certificate.load(der_bytes)
|
handshake_cert = x509.Certificate.load(der_bytes)
|
||||||
if handshake_cert.native == cert.native:
|
if handshake_cert.native == cert.native:
|
||||||
for subnet in config.RENEWAL_SUBNETS:
|
for subnet in config.RENEWAL_SUBNETS:
|
||||||
if req.context.get("remote_addr") in subnet:
|
if req.context.get("remote_addr") in subnet:
|
||||||
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
||||||
|
setxattr(path, "user.revocation.reason", "superseded")
|
||||||
_, resp.body = self.authority._sign(csr, body, overwrite=True,
|
_, resp.body = self.authority._sign(csr, body, overwrite=True,
|
||||||
profile=SignatureProfile.from_cert(cert))
|
profile=SignatureProfile.from_cert(cert))
|
||||||
logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
||||||
return
|
return
|
||||||
|
reasons.append("renewal failed")
|
||||||
# No header supplied, redirect to signed API call
|
else:
|
||||||
resp.status = falcon.HTTP_SEE_OTHER
|
# No renewal requested, redirect to signed API call
|
||||||
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name)
|
resp.status = falcon.HTTP_SEE_OTHER
|
||||||
return
|
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Process automatic signing if the IP address is whitelisted,
|
Process automatic signing if the IP address is whitelisted,
|
||||||
autosigning was requested and certificate can be automatically signed
|
autosigning was requested and certificate can be automatically signed
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if req.get_param_as_bool("autosign"):
|
if req.get_param_as_bool("autosign"):
|
||||||
if not self.authority.server_flags(common_name):
|
for subnet in config.AUTOSIGN_SUBNETS:
|
||||||
for subnet in config.AUTOSIGN_SUBNETS:
|
if req.context.get("remote_addr") in subnet:
|
||||||
if req.context.get("remote_addr") in subnet:
|
try:
|
||||||
try:
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
_, resp.body = self.authority._sign(csr, body,
|
||||||
_, resp.body = self.authority._sign(csr, body, profile=config.PROFILES["rw"])
|
overwrite=overwrite_allowed, profile=config.PROFILES["rw"])
|
||||||
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
||||||
return
|
return
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
logger.info("Autosign for %s from %s failed, signed certificate already exists",
|
logger.info("Autosign for %s from %s failed, signed certificate already exists",
|
||||||
common_name, req.context.get("remote_addr"))
|
common_name, req.context.get("remote_addr"))
|
||||||
reasons.append("Autosign failed, signed certificate already exists")
|
reasons.append("autosign failed, signed certificate already exists")
|
||||||
break
|
break
|
||||||
else:
|
|
||||||
reasons.append("Autosign failed, IP address not whitelisted")
|
|
||||||
else:
|
else:
|
||||||
reasons.append("Autosign failed, only client certificates allowed to be signed automatically")
|
reasons.append("autosign failed, IP address not whitelisted")
|
||||||
|
else:
|
||||||
|
reasons.append("autosign not requested")
|
||||||
|
|
||||||
# Attempt to save the request otherwise
|
# Attempt to save the request otherwise
|
||||||
try:
|
try:
|
||||||
request_path, _, _ = self.authority.store_request(body,
|
request_path, _, _ = self.authority.store_request(body,
|
||||||
address=str(req.context.get("remote_addr")))
|
address=str(req.context.get("remote_addr")))
|
||||||
except errors.RequestExists:
|
except errors.RequestExists:
|
||||||
reasons.append("Same request already uploaded exists")
|
reasons.append("same request already uploaded exists")
|
||||||
# We should still redirect client to long poll URL below
|
# We should still redirect client to long poll URL below
|
||||||
except errors.DuplicateCommonNameError:
|
except errors.DuplicateCommonNameError:
|
||||||
# TODO: Certificate renewal
|
# TODO: Certificate renewal
|
||||||
logger.warning("Rejected signing request with overlapping common name from %s",
|
logger.warning("rejected signing request with overlapping common name from %s",
|
||||||
req.context.get("remote_addr"))
|
req.context.get("remote_addr"))
|
||||||
raise falcon.HTTPConflict(
|
raise falcon.HTTPConflict(
|
||||||
"CSR with such CN already exists",
|
"CSR with such CN already exists",
|
||||||
@ -154,14 +170,15 @@ class RequestListResource(AuthorityHandler):
|
|||||||
push.publish("request-submitted", common_name)
|
push.publish("request-submitted", common_name)
|
||||||
|
|
||||||
# Wait the certificate to be signed if waiting is requested
|
# Wait the certificate to be signed if waiting is requested
|
||||||
logger.info("Stored signing request %s from %s", common_name, req.context.get("remote_addr"))
|
logger.info("Stored signing request %s from %s, reasons: %s", common_name, req.context.get("remote_addr"), reasons)
|
||||||
|
|
||||||
if req.get_param("wait"):
|
if req.get_param("wait"):
|
||||||
# Redirect to nginx pub/sub
|
# Redirect to nginx pub/sub
|
||||||
url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest()
|
url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest()
|
||||||
click.echo("Redirecting to: %s" % url)
|
click.echo("Redirecting to: %s" % url)
|
||||||
resp.status = falcon.HTTP_SEE_OTHER
|
resp.status = falcon.HTTP_SEE_OTHER
|
||||||
resp.set_header("Location", url)
|
resp.set_header("Location", url)
|
||||||
logger.debug("Redirecting signing request from %s to %s", req.context.get("remote_addr"), url)
|
logger.debug("Redirecting signing request from %s to %s, reasons: %s", req.context.get("remote_addr"), url, ", ".join(reasons))
|
||||||
else:
|
else:
|
||||||
# Request was accepted, but not processed
|
# Request was accepted, but not processed
|
||||||
resp.status = falcon.HTTP_202
|
resp.status = falcon.HTTP_202
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
import click
|
||||||
|
import falcon
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from asn1crypto import cms, algos
|
from asn1crypto import cms, algos
|
||||||
from asn1crypto.core import SetOf, PrintableString
|
from asn1crypto.core import SetOf, PrintableString
|
||||||
@ -9,6 +12,8 @@ from oscrypto.errors import SignatureError
|
|||||||
from .utils import AuthorityHandler
|
from .utils import AuthorityHandler
|
||||||
from .utils.firewall import whitelist_subnets
|
from .utils.firewall import whitelist_subnets
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Monkey patch asn1crypto
|
# Monkey patch asn1crypto
|
||||||
|
|
||||||
class SetOfPrintableString(SetOf):
|
class SetOfPrintableString(SetOf):
|
||||||
@ -28,21 +33,54 @@ cms.CMSAttribute._oid_specs['sender_nonce'] = cms.SetOfOctetString
|
|||||||
cms.CMSAttribute._oid_specs['recipient_nonce'] = cms.SetOfOctetString
|
cms.CMSAttribute._oid_specs['recipient_nonce'] = cms.SetOfOctetString
|
||||||
cms.CMSAttribute._oid_specs['trans_id'] = SetOfPrintableString
|
cms.CMSAttribute._oid_specs['trans_id'] = SetOfPrintableString
|
||||||
|
|
||||||
class SCEPError(Exception): code = 25 # system failure
|
class SCEPError(Exception):
|
||||||
class SCEPBadAlgo(SCEPError): code = 0
|
code = 25 # system failure
|
||||||
class SCEPBadMessageCheck(SCEPError): code = 1
|
explaination = "General system failure"
|
||||||
class SCEPBadRequest(SCEPError): code = 2
|
|
||||||
class SCEPBadTime(SCEPError): code = 3
|
class SCEPBadAlgo(SCEPError):
|
||||||
class SCEPBadCertId(SCEPError): code = 4
|
code = 0
|
||||||
|
explaination = "Unsupported algorithm in SCEP request"
|
||||||
|
|
||||||
|
class SCEPBadMessageCheck(SCEPError):
|
||||||
|
code = 1
|
||||||
|
explaination = "Integrity check failed for SCEP request"
|
||||||
|
|
||||||
|
class SCEPBadRequest(SCEPError):
|
||||||
|
code = 2
|
||||||
|
explaination = "Bad request"
|
||||||
|
|
||||||
|
class SCEPBadTime(SCEPError):
|
||||||
|
code = 3
|
||||||
|
explaination = "Bad time"
|
||||||
|
|
||||||
|
class SCEPBadCertId(SCEPError):
|
||||||
|
code = 4
|
||||||
|
explaination = "Certificate authority mismatch"
|
||||||
|
|
||||||
|
class SCEPDigestMismatch(SCEPBadMessageCheck):
|
||||||
|
explaination = "Digest mismatch"
|
||||||
|
|
||||||
|
class SCEPSignatureMismatch(SCEPBadMessageCheck):
|
||||||
|
explaination = "Signature mismatch"
|
||||||
|
|
||||||
class SCEPResource(AuthorityHandler):
|
class SCEPResource(AuthorityHandler):
|
||||||
@whitelist_subnets(config.SCEP_SUBNETS)
|
@whitelist_subnets(config.SCEP_SUBNETS)
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
operation = req.get_param("operation", required=True)
|
operation = req.get_param("operation", required=True)
|
||||||
if operation.lower() == "getcacert":
|
if operation == "GetCACert":
|
||||||
resp.body = keys.parse_certificate(self.authority.certificate_buf).dump()
|
resp.body = keys.parse_certificate(self.authority.certificate_buf).dump()
|
||||||
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
||||||
return
|
return
|
||||||
|
elif operation == "GetCACaps":
|
||||||
|
# TODO: return renewal flag based on renewal subnets config option
|
||||||
|
resp.body = "Renewal\nMD5\nSHA-1\nSHA-256\nSHA-512\nDES3\n"
|
||||||
|
return
|
||||||
|
elif operation == "PKIOperation":
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise falcon.HTTPBadRequest(
|
||||||
|
"Bad request",
|
||||||
|
"Unknown operation %s" % operation)
|
||||||
|
|
||||||
# If we bump into exceptions later
|
# If we bump into exceptions later
|
||||||
encrypted_container = b""
|
encrypted_container = b""
|
||||||
@ -74,8 +112,14 @@ class SCEPResource(AuthorityHandler):
|
|||||||
|
|
||||||
# TODO: compare cert to current one if we are renewing
|
# TODO: compare cert to current one if we are renewing
|
||||||
|
|
||||||
assert signer["digest_algorithm"]["algorithm"].native == "md5"
|
digest_algorithm = signer["digest_algorithm"]["algorithm"].native
|
||||||
assert signer["signature_algorithm"]["algorithm"].native == "rsassa_pkcs1v15"
|
signature_algorithm = signer["signature_algorithm"]["algorithm"].native
|
||||||
|
|
||||||
|
if digest_algorithm not in ("md5", "sha1", "sha256", "sha512"):
|
||||||
|
raise SCEPBadAlgo()
|
||||||
|
if signature_algorithm != "rsassa_pkcs1v15":
|
||||||
|
raise SCEPBadAlgo()
|
||||||
|
|
||||||
message_digest = None
|
message_digest = None
|
||||||
transaction_id = None
|
transaction_id = None
|
||||||
sender_nonce = None
|
sender_nonce = None
|
||||||
@ -87,8 +131,13 @@ class SCEPResource(AuthorityHandler):
|
|||||||
transaction_id, = attr["values"]
|
transaction_id, = attr["values"]
|
||||||
elif attr["type"].native == "message_digest":
|
elif attr["type"].native == "message_digest":
|
||||||
message_digest, = attr["values"]
|
message_digest, = attr["values"]
|
||||||
if hashlib.md5(encap_content.native).digest() != message_digest.native:
|
if getattr(hashlib, digest_algorithm)(encap_content.native).digest() != message_digest.native:
|
||||||
raise SCEPBadMessageCheck()
|
raise SCEPDigestMismatch()
|
||||||
|
|
||||||
|
if not sender_nonce:
|
||||||
|
raise SCEPBadRequest()
|
||||||
|
if not transaction_id:
|
||||||
|
raise SCEPBadRequest()
|
||||||
|
|
||||||
assert message_digest
|
assert message_digest
|
||||||
msg = signer["signed_attrs"].dump(force=True)
|
msg = signer["signed_attrs"].dump(force=True)
|
||||||
@ -102,7 +151,8 @@ class SCEPResource(AuthorityHandler):
|
|||||||
b"\x31" + msg[1:], # wtf?!
|
b"\x31" + msg[1:], # wtf?!
|
||||||
"md5")
|
"md5")
|
||||||
except SignatureError:
|
except SignatureError:
|
||||||
raise SCEPBadMessageCheck()
|
raise SCEPSignatureMismatch()
|
||||||
|
|
||||||
|
|
||||||
###############################
|
###############################
|
||||||
### Decrypt inner container ###
|
### Decrypt inner container ###
|
||||||
@ -122,14 +172,15 @@ class SCEPResource(AuthorityHandler):
|
|||||||
if recipient.native["rid"]["serial_number"] != self.authority.certificate.serial_number:
|
if recipient.native["rid"]["serial_number"] != self.authority.certificate.serial_number:
|
||||||
raise SCEPBadCertId()
|
raise SCEPBadCertId()
|
||||||
|
|
||||||
# Since CA private key is not directly readable here, we'll redirect it to signer socket
|
|
||||||
key = asymmetric.rsa_pkcs1v15_decrypt(
|
key = asymmetric.rsa_pkcs1v15_decrypt(
|
||||||
self.authority.private_key,
|
self.authority.private_key,
|
||||||
recipient.native["encrypted_key"])
|
recipient.native["encrypted_key"])
|
||||||
if len(key) == 8: key = key * 3 # Convert DES to 3DES
|
if len(key) == 8: key = key * 3 # Convert DES to 3DES
|
||||||
buf = symmetric.tripledes_cbc_pkcs5_decrypt(key, encrypted_content, iv)
|
buf = symmetric.tripledes_cbc_pkcs5_decrypt(key, encrypted_content, iv)
|
||||||
_, _, common_name = self.authority.store_request(buf, overwrite=True)
|
_, _, common_name = self.authority.store_request(buf, overwrite=True)
|
||||||
cert, buf = self.authority.sign(common_name, overwrite=True)
|
logger.info("SCEP client from %s requested with %s digest algorithm, %s signature",
|
||||||
|
req.context["remote_addr"], digest_algorithm, signature_algorithm)
|
||||||
|
cert, buf = self.authority.sign(common_name, profile=config.PROFILES["gw"], overwrite=True)
|
||||||
signed_certificate = asymmetric.load_certificate(buf)
|
signed_certificate = asymmetric.load_certificate(buf)
|
||||||
content = signed_certificate.asn1.dump()
|
content = signed_certificate.asn1.dump()
|
||||||
|
|
||||||
@ -138,6 +189,7 @@ class SCEPResource(AuthorityHandler):
|
|||||||
'type': "fail_info",
|
'type': "fail_info",
|
||||||
'values': ["%d" % e.code]
|
'values': ["%d" % e.code]
|
||||||
}))
|
}))
|
||||||
|
logger.info("Failed to sign SCEP request due to: %s" % e.explaination)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
@ -150,7 +202,8 @@ class SCEPResource(AuthorityHandler):
|
|||||||
'version': "v1",
|
'version': "v1",
|
||||||
'certificates': [signed_certificate.asn1],
|
'certificates': [signed_certificate.asn1],
|
||||||
'digest_algorithms': [cms.DigestAlgorithm({
|
'digest_algorithms': [cms.DigestAlgorithm({
|
||||||
'algorithm': "md5"
|
'algorithm': digest_algorithm
|
||||||
|
|
||||||
})],
|
})],
|
||||||
'encap_content_info': {
|
'encap_content_info': {
|
||||||
'content_type': "data",
|
'content_type': "data",
|
||||||
@ -208,7 +261,7 @@ class SCEPResource(AuthorityHandler):
|
|||||||
attr_list = [
|
attr_list = [
|
||||||
cms.CMSAttribute({
|
cms.CMSAttribute({
|
||||||
'type': "message_digest",
|
'type': "message_digest",
|
||||||
'values': [hashlib.sha1(encrypted_container).digest()]
|
'values': [getattr(hashlib, digest_algorithm)(encrypted_container).digest()]
|
||||||
}),
|
}),
|
||||||
cms.CMSAttribute({
|
cms.CMSAttribute({
|
||||||
'type': "message_type",
|
'type': "message_type",
|
||||||
@ -245,12 +298,12 @@ class SCEPResource(AuthorityHandler):
|
|||||||
'serial_number': self.authority.certificate.serial_number,
|
'serial_number': self.authority.certificate.serial_number,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'digest_algorithm': algos.DigestAlgorithm({'algorithm': "sha1"}),
|
'digest_algorithm': algos.DigestAlgorithm({'algorithm': digest_algorithm}),
|
||||||
'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': "rsassa_pkcs1v15"}),
|
'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': "rsassa_pkcs1v15"}),
|
||||||
'signature': asymmetric.rsa_pkcs1v15_sign(
|
'signature': asymmetric.rsa_pkcs1v15_sign(
|
||||||
self.authority.private_key,
|
self.authority.private_key,
|
||||||
b"\x31" + attrs.dump()[1:],
|
b"\x31" + attrs.dump()[1:],
|
||||||
"sha1"
|
digest_algorithm
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -261,7 +314,7 @@ class SCEPResource(AuthorityHandler):
|
|||||||
'version': "v1",
|
'version': "v1",
|
||||||
'certificates': [self.authority.certificate],
|
'certificates': [self.authority.certificate],
|
||||||
'digest_algorithms': [cms.DigestAlgorithm({
|
'digest_algorithms': [cms.DigestAlgorithm({
|
||||||
'algorithm': "sha1"
|
'algorithm': digest_algorithm
|
||||||
})],
|
})],
|
||||||
'encap_content_info': {
|
'encap_content_info': {
|
||||||
'content_type': "data",
|
'content_type': "data",
|
||||||
|
@ -44,7 +44,7 @@ class SignedCertificateDetailResource(AuthorityHandler):
|
|||||||
resp.body = json.dumps(dict(
|
resp.body = json.dumps(dict(
|
||||||
common_name = cn,
|
common_name = cn,
|
||||||
signer = signer_username,
|
signer = signer_username,
|
||||||
serial = "%x" % cert.serial_number,
|
serial = "%040x" % cert.serial_number,
|
||||||
organizational_unit = cert.subject.native.get("organizational_unit_name"),
|
organizational_unit = cert.subject.native.get("organizational_unit_name"),
|
||||||
signed = cert["tbs_certificate"]["validity"]["not_before"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
signed = cert["tbs_certificate"]["validity"]["not_before"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||||
expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||||
@ -54,7 +54,8 @@ class SignedCertificateDetailResource(AuthorityHandler):
|
|||||||
extensions = dict([
|
extensions = dict([
|
||||||
(e["extn_id"].native, e["extn_value"].native)
|
(e["extn_id"].native, e["extn_value"].native)
|
||||||
for e in cert["tbs_certificate"]["extensions"]
|
for e in cert["tbs_certificate"]["extensions"]
|
||||||
if e["extn_value"] in ("extended_key_usage",)])
|
if e["extn_id"].native in ("extended_key_usage",)])
|
||||||
|
|
||||||
))
|
))
|
||||||
logger.debug("Served certificate %s to %s as application/json",
|
logger.debug("Served certificate %s to %s as application/json",
|
||||||
cn, req.context.get("remote_addr"))
|
cn, req.context.get("remote_addr"))
|
||||||
@ -69,5 +70,6 @@ class SignedCertificateDetailResource(AuthorityHandler):
|
|||||||
def on_delete(self, req, resp, cn):
|
def on_delete(self, req, resp, cn):
|
||||||
logger.info("Revoked certificate %s by %s from %s",
|
logger.info("Revoked certificate %s by %s from %s",
|
||||||
cn, req.context.get("user"), req.context.get("remote_addr"))
|
cn, req.context.get("user"), req.context.get("remote_addr"))
|
||||||
self.authority.revoke(cn)
|
self.authority.revoke(cn,
|
||||||
|
reason=req.get_param("reason", default="key_compromise"))
|
||||||
|
|
||||||
|
@ -30,9 +30,14 @@ def authenticate(optional=False):
|
|||||||
|
|
||||||
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
||||||
|
|
||||||
server_creds = gssapi.creds.Credentials(
|
try:
|
||||||
usage='accept',
|
server_creds = gssapi.creds.Credentials(
|
||||||
name=gssapi.names.Name('HTTP/%s'% const.FQDN))
|
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)
|
context = gssapi.sec_contexts.SecurityContext(creds=server_creds)
|
||||||
|
|
||||||
@ -49,13 +54,13 @@ def authenticate(optional=False):
|
|||||||
raise falcon.HTTPBadRequest("Bad request", "Unsupported authentication mechanism (NTLM?) was offered. Please make sure you've logged into the computer with domain user account. The web interface should not prompt for username or password.")
|
raise falcon.HTTPBadRequest("Bad request", "Unsupported authentication mechanism (NTLM?) was offered. Please make sure you've logged into the computer with domain user account. The web interface should not prompt for username or password.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
username, domain = str(context.initiator_name).split("@")
|
username, realm = str(context.initiator_name).split("@")
|
||||||
except AttributeError: # TODO: Better exception
|
except AttributeError: # TODO: Better exception
|
||||||
raise falcon.HTTPForbidden("Failed to determine username, are you trying to log in with correct domain account?")
|
raise falcon.HTTPForbidden("Failed to determine username, are you trying to log in with correct domain account?")
|
||||||
|
|
||||||
if domain.lower() != const.DOMAIN.lower():
|
if realm != config.KERBEROS_REALM:
|
||||||
raise falcon.HTTPForbidden("Forbidden",
|
raise falcon.HTTPForbidden("Forbidden",
|
||||||
"Invalid realm supplied")
|
"Cross-realm trust not supported")
|
||||||
|
|
||||||
if username.endswith("$") and optional:
|
if username.endswith("$") and optional:
|
||||||
# Extract machine hostname
|
# Extract machine hostname
|
||||||
|
@ -12,6 +12,7 @@ from asn1crypto.csr import CertificationRequest
|
|||||||
from certbuilder import CertificateBuilder
|
from certbuilder import CertificateBuilder
|
||||||
from certidude import config, push, mailer, const
|
from certidude import config, push, mailer, const
|
||||||
from certidude import errors
|
from certidude import errors
|
||||||
|
from certidude.common import cn_to_dn
|
||||||
from crlbuilder import CertificateListBuilder, pem_armor_crl
|
from crlbuilder import CertificateListBuilder, pem_armor_crl
|
||||||
from csrbuilder import CSRBuilder, pem_armor_csr
|
from csrbuilder import CSRBuilder, pem_armor_csr
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@ -21,6 +22,16 @@ from xattr import getxattr, listxattr, setxattr
|
|||||||
|
|
||||||
random = SystemRandom()
|
random = SystemRandom()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from time import time_ns
|
||||||
|
except ImportError:
|
||||||
|
from time import time
|
||||||
|
def time_ns():
|
||||||
|
return int(time() * 10**9) # 64 bits integer, 32 ns bits
|
||||||
|
|
||||||
|
def generate_serial():
|
||||||
|
return time_ns() << 56 | random.randint(0, 2**56-1)
|
||||||
|
|
||||||
# https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/
|
# https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/
|
||||||
# https://jamielinux.com/docs/openssl-certificate-authority/
|
# https://jamielinux.com/docs/openssl-certificate-authority/
|
||||||
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
|
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
|
||||||
@ -38,6 +49,8 @@ with open(config.AUTHORITY_PRIVATE_KEY_PATH, "rb") as fh:
|
|||||||
private_key = asymmetric.load_private_key(key_der_bytes)
|
private_key = asymmetric.load_private_key(key_der_bytes)
|
||||||
|
|
||||||
def self_enroll():
|
def self_enroll():
|
||||||
|
assert os.getuid() == 0 and os.getgid() == 0, "Can self-enroll only as root"
|
||||||
|
|
||||||
from certidude import const
|
from certidude import const
|
||||||
common_name = const.FQDN
|
common_name = const.FQDN
|
||||||
directory = os.path.join("/var/lib/certidude", const.FQDN)
|
directory = os.path.join("/var/lib/certidude", const.FQDN)
|
||||||
@ -64,13 +77,16 @@ def self_enroll():
|
|||||||
|
|
||||||
builder = CSRBuilder({"common_name": common_name}, self_public_key)
|
builder = CSRBuilder({"common_name": common_name}, self_public_key)
|
||||||
request = builder.build(private_key)
|
request = builder.build(private_key)
|
||||||
with open(os.path.join(directory, "requests", common_name + ".pem"), "wb") as fh:
|
|
||||||
fh.write(pem_armor_csr(request))
|
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if not pid:
|
if not pid:
|
||||||
from certidude import authority
|
from certidude import authority
|
||||||
from certidude.common import drop_privileges
|
from certidude.common import drop_privileges
|
||||||
drop_privileges()
|
drop_privileges()
|
||||||
|
assert os.getuid() != 0 and os.getgid() != 0
|
||||||
|
path = os.path.join(directory, "requests", common_name + ".pem")
|
||||||
|
click.echo("Writing request to %s" % path)
|
||||||
|
with open(path, "wb") as fh:
|
||||||
|
fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions
|
||||||
authority.sign(common_name, skip_push=True, overwrite=True, profile=config.PROFILES["srv"])
|
authority.sign(common_name, skip_push=True, overwrite=True, profile=config.PROFILES["srv"])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
@ -109,18 +125,23 @@ def get_signed(common_name):
|
|||||||
def get_revoked(serial):
|
def get_revoked(serial):
|
||||||
if isinstance(serial, str):
|
if isinstance(serial, str):
|
||||||
serial = int(serial, 16)
|
serial = int(serial, 16)
|
||||||
path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial)
|
path = os.path.join(config.REVOKED_DIR, "%040x.pem" % serial)
|
||||||
with open(path, "rb") as fh:
|
with open(path, "rb") as fh:
|
||||||
buf = fh.read()
|
buf = fh.read()
|
||||||
header, _, der_bytes = pem.unarmor(buf)
|
header, _, der_bytes = pem.unarmor(buf)
|
||||||
cert = x509.Certificate.load(der_bytes)
|
cert = x509.Certificate.load(der_bytes)
|
||||||
|
try:
|
||||||
|
reason = getxattr(path, "user.revocation.reason").decode("ascii")
|
||||||
|
except IOError: # TODO: make sure it's not required
|
||||||
|
reason = "key_compromise"
|
||||||
return path, buf, cert, \
|
return path, buf, cert, \
|
||||||
cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None), \
|
cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None), \
|
||||||
cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None), \
|
cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None), \
|
||||||
datetime.utcfromtimestamp(os.stat(path).st_ctime)
|
datetime.utcfromtimestamp(os.stat(path).st_ctime), \
|
||||||
|
reason
|
||||||
|
|
||||||
|
|
||||||
def get_attributes(cn, namespace=None):
|
def get_attributes(cn, namespace=None, flat=False):
|
||||||
path, buf, cert, signed, expires = get_signed(cn)
|
path, buf, cert, signed, expires = get_signed(cn)
|
||||||
attribs = dict()
|
attribs = dict()
|
||||||
for key in listxattr(path):
|
for key in listxattr(path):
|
||||||
@ -129,15 +150,18 @@ def get_attributes(cn, namespace=None):
|
|||||||
continue
|
continue
|
||||||
if namespace and not key.startswith("user.%s." % namespace):
|
if namespace and not key.startswith("user.%s." % namespace):
|
||||||
continue
|
continue
|
||||||
value = getxattr(path, key)
|
value = getxattr(path, key).decode("utf-8")
|
||||||
current = attribs
|
if flat:
|
||||||
if "." in key:
|
attribs[key[len("user.%s." % namespace):]] = value
|
||||||
prefix, key = key.rsplit(".", 1)
|
else:
|
||||||
for component in prefix.split("."):
|
current = attribs
|
||||||
if component not in current:
|
if "." in key:
|
||||||
current[component] = dict()
|
prefix, key = key.rsplit(".", 1)
|
||||||
current = current[component]
|
for component in prefix.split("."):
|
||||||
current[key] = value.decode("utf-8")
|
if component not in current:
|
||||||
|
current[component] = dict()
|
||||||
|
current = current[component]
|
||||||
|
current[key] = value
|
||||||
return path, buf, cert, attribs
|
return path, buf, cert, attribs
|
||||||
|
|
||||||
|
|
||||||
@ -159,7 +183,7 @@ def store_request(buf, overwrite=False, address="", user=""):
|
|||||||
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
||||||
|
|
||||||
if not re.match(const.RE_COMMON_NAME, common_name):
|
if not re.match(const.RE_COMMON_NAME, common_name):
|
||||||
raise ValueError("Invalid common name")
|
raise ValueError("Invalid common name %s" % repr(common_name))
|
||||||
|
|
||||||
request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
||||||
|
|
||||||
@ -190,14 +214,21 @@ def store_request(buf, overwrite=False, address="", user=""):
|
|||||||
return request_path, csr, common_name
|
return request_path, csr, common_name
|
||||||
|
|
||||||
|
|
||||||
def revoke(common_name):
|
def revoke(common_name, reason):
|
||||||
"""
|
"""
|
||||||
Revoke valid certificate
|
Revoke valid certificate
|
||||||
"""
|
"""
|
||||||
signed_path, buf, cert, signed, expires = get_signed(common_name)
|
signed_path, buf, cert, signed, expires = get_signed(common_name)
|
||||||
revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial_number)
|
|
||||||
|
|
||||||
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number))
|
if reason not in ("key_compromise", "ca_compromise", "affiliation_changed",
|
||||||
|
"superseded", "cessation_of_operation", "certificate_hold",
|
||||||
|
"remove_from_crl", "privilege_withdrawn"):
|
||||||
|
raise ValueError("Invalid revocation reason %s" % reason)
|
||||||
|
|
||||||
|
setxattr(signed_path, "user.revocation.reason", reason)
|
||||||
|
revoked_path = os.path.join(config.REVOKED_DIR, "%040x.pem" % cert.serial_number)
|
||||||
|
|
||||||
|
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number))
|
||||||
os.rename(signed_path, revoked_path)
|
os.rename(signed_path, revoked_path)
|
||||||
|
|
||||||
|
|
||||||
@ -212,7 +243,7 @@ def revoke(common_name):
|
|||||||
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
|
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
|
||||||
mailer.send("certificate-revoked.md",
|
mailer.send("certificate-revoked.md",
|
||||||
attachments=(attach_cert,),
|
attachments=(attach_cert,),
|
||||||
serial_hex="%x" % cert.serial_number,
|
serial_hex="%040x" % cert.serial_number,
|
||||||
common_name=common_name)
|
common_name=common_name)
|
||||||
return revoked_path
|
return revoked_path
|
||||||
|
|
||||||
@ -251,28 +282,40 @@ def _list_certificates(directory):
|
|||||||
server = True
|
server = True
|
||||||
yield cert.subject.native["common_name"], path, buf, cert, server
|
yield cert.subject.native["common_name"], path, buf, cert, server
|
||||||
|
|
||||||
def list_signed(directory=config.SIGNED_DIR):
|
def list_signed(directory=config.SIGNED_DIR, common_name=None):
|
||||||
for filename in os.listdir(directory):
|
for filename in os.listdir(directory):
|
||||||
if filename.endswith(".pem"):
|
if not filename.endswith(".pem"):
|
||||||
common_name = filename[:-4]
|
continue
|
||||||
path, buf, cert, signed, expires = get_signed(common_name)
|
basename = filename[:-4]
|
||||||
yield common_name, path, buf, cert, signed, expires
|
if common_name:
|
||||||
|
if common_name.startswith("^"):
|
||||||
|
if not re.match(common_name, basename):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if common_name != basename:
|
||||||
|
continue
|
||||||
|
path, buf, cert, signed, expires = get_signed(basename)
|
||||||
|
yield basename, path, buf, cert, signed, expires
|
||||||
|
|
||||||
def list_revoked(directory=config.REVOKED_DIR):
|
def list_revoked(directory=config.REVOKED_DIR):
|
||||||
for filename in os.listdir(directory):
|
for filename in os.listdir(directory):
|
||||||
if filename.endswith(".pem"):
|
if filename.endswith(".pem"):
|
||||||
common_name = filename[:-4]
|
common_name = filename[:-4]
|
||||||
path, buf, cert, signed, expired, revoked = get_revoked(common_name)
|
path, buf, cert, signed, expired, revoked, reason = get_revoked(common_name)
|
||||||
yield cert.subject.native["common_name"], path, buf, cert, signed, expired, revoked
|
yield cert.subject.native["common_name"], path, buf, cert, signed, expired, revoked, reason
|
||||||
|
|
||||||
|
|
||||||
def list_server_names():
|
def list_server_names():
|
||||||
return [cn for cn, path, buf, cert, server in list_signed() if server]
|
return [cn for cn, path, buf, cert, server in list_signed() if server]
|
||||||
|
|
||||||
|
|
||||||
def export_crl(pem=True):
|
def export_crl(pem=True):
|
||||||
|
# To migrate older installations run following:
|
||||||
|
# for j in /var/lib/certidude/*/revoked/*.pem; do echo $(attr -s 'revocation.reason' -V key_compromise $j); done
|
||||||
builder = CertificateListBuilder(
|
builder = CertificateListBuilder(
|
||||||
config.AUTHORITY_CRL_URL,
|
config.AUTHORITY_CRL_URL,
|
||||||
certificate,
|
certificate,
|
||||||
1 # TODO: monotonically increasing
|
generate_serial()
|
||||||
)
|
)
|
||||||
|
|
||||||
for filename in os.listdir(config.REVOKED_DIR):
|
for filename in os.listdir(config.REVOKED_DIR):
|
||||||
@ -281,12 +324,14 @@ def export_crl(pem=True):
|
|||||||
serial_number = filename[:-4]
|
serial_number = filename[:-4]
|
||||||
# TODO: Assert serial against regex
|
# TODO: Assert serial against regex
|
||||||
revoked_path = os.path.join(config.REVOKED_DIR, filename)
|
revoked_path = os.path.join(config.REVOKED_DIR, filename)
|
||||||
|
reason = getxattr(revoked_path, "user.revocation.reason").decode("ascii") # TODO: dedup
|
||||||
|
|
||||||
# TODO: Skip expired certificates
|
# TODO: Skip expired certificates
|
||||||
s = os.stat(revoked_path)
|
s = os.stat(revoked_path)
|
||||||
builder.add_certificate(
|
builder.add_certificate(
|
||||||
int(filename[:-4], 16),
|
int(filename[:-4], 16),
|
||||||
datetime.utcfromtimestamp(s.st_ctime),
|
datetime.utcfromtimestamp(s.st_ctime),
|
||||||
"key_compromise")
|
reason)
|
||||||
|
|
||||||
certificate_list = builder.build(private_key)
|
certificate_list = builder.build(private_key)
|
||||||
if pem:
|
if pem:
|
||||||
@ -359,7 +404,7 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
|
|||||||
|
|
||||||
if overwrite:
|
if overwrite:
|
||||||
# TODO: is this the best approach?
|
# TODO: is this the best approach?
|
||||||
prev_serial_hex = "%x" % prev.serial_number
|
prev_serial_hex = "%040x" % prev.serial_number
|
||||||
revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex)
|
revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex)
|
||||||
os.rename(cert_path, revoked_path)
|
os.rename(cert_path, revoked_path)
|
||||||
attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")]
|
attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")]
|
||||||
@ -367,14 +412,10 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
|
|||||||
else:
|
else:
|
||||||
raise FileExistsError("Will not overwrite existing certificate")
|
raise FileExistsError("Will not overwrite existing certificate")
|
||||||
|
|
||||||
dn = {u'common_name': common_name }
|
builder = CertificateBuilder(cn_to_dn(common_name, const.FQDN,
|
||||||
if profile.ou:
|
o=certificate["tbs_certificate"]["subject"].native.get("organization_name"),
|
||||||
dn["organizational_unit_name"] = profile.ou
|
ou=profile.ou), csr_pubkey)
|
||||||
|
builder.serial_number = generate_serial()
|
||||||
builder = CertificateBuilder(dn, csr_pubkey)
|
|
||||||
builder.serial_number = random.randint(
|
|
||||||
0x1000000000000000000000000000000000000000,
|
|
||||||
0x7fffffffffffffffffffffffffffffffffffffff)
|
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
builder.begin_date = now - timedelta(minutes=5)
|
builder.begin_date = now - timedelta(minutes=5)
|
||||||
@ -392,10 +433,10 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
|
|||||||
|
|
||||||
os.rename(cert_path + ".part", cert_path)
|
os.rename(cert_path + ".part", cert_path)
|
||||||
attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt"))
|
attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt"))
|
||||||
cert_serial_hex = "%x" % end_entity_cert.serial_number
|
cert_serial_hex = "%040x" % end_entity_cert.serial_number
|
||||||
|
|
||||||
# Create symlink
|
# Create symlink
|
||||||
link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % end_entity_cert.serial_number)
|
link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % end_entity_cert.serial_number)
|
||||||
assert not os.path.exists(link_name), "Certificate with same serial number already exists: %s" % link_name
|
assert not os.path.exists(link_name), "Certificate with same serial number already exists: %s" % link_name
|
||||||
os.symlink("../%s.pem" % common_name, link_name)
|
os.symlink("../%s.pem" % common_name, link_name)
|
||||||
|
|
||||||
@ -422,6 +463,10 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
|
|||||||
click.echo("Publishing certificate at %s ..." % url)
|
click.echo("Publishing certificate at %s ..." % url)
|
||||||
requests.post(url, data=end_entity_cert_buf,
|
requests.post(url, data=end_entity_cert_buf,
|
||||||
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
|
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
|
||||||
|
if renew:
|
||||||
push.publish("request-signed", common_name)
|
# 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
|
return end_entity_cert, end_entity_cert_buf
|
||||||
|
130
certidude/cli.py
130
certidude/cli.py
@ -12,12 +12,13 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from asn1crypto import pem, x509
|
from asn1crypto import pem, x509
|
||||||
from asn1crypto.csr import CertificationRequest
|
from asn1crypto.csr import CertificationRequest
|
||||||
|
from asn1crypto.crl import CertificateList
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from certbuilder import CertificateBuilder, pem_armor_certificate
|
from certbuilder import CertificateBuilder, pem_armor_certificate
|
||||||
from certidude import const
|
from certidude import const
|
||||||
from csrbuilder import CSRBuilder, pem_armor_csr
|
from csrbuilder import CSRBuilder, pem_armor_csr
|
||||||
from configparser import ConfigParser, NoOptionError
|
from configparser import ConfigParser, NoOptionError
|
||||||
from certidude.common import apt, rpm, drop_privileges, selinux_fixup
|
from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
@ -49,7 +50,7 @@ def setup_client(prefix="client_", dh=False):
|
|||||||
def wrapped(**arguments):
|
def wrapped(**arguments):
|
||||||
common_name = arguments.get("common_name")
|
common_name = arguments.get("common_name")
|
||||||
authority = arguments.get("authority")
|
authority = arguments.get("authority")
|
||||||
b = os.path.join(const.STORAGE_PATH, authority)
|
b = os.path.join("/etc/certidude/authority", authority)
|
||||||
if dh:
|
if dh:
|
||||||
path = os.path.join(const.STORAGE_PATH, "dh.pem")
|
path = os.path.join(const.STORAGE_PATH, "dh.pem")
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
@ -94,6 +95,8 @@ def setup_client(prefix="client_", dh=False):
|
|||||||
@click.option("-s", "--skip-self", default=False, is_flag=True, help="Skip self enroll")
|
@click.option("-s", "--skip-self", default=False, is_flag=True, help="Skip self enroll")
|
||||||
@click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign")
|
@click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign")
|
||||||
def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
||||||
|
assert os.getuid() == 0 and os.getgid() == 0, "Can enroll only as root"
|
||||||
|
|
||||||
if not skip_self and os.path.exists(const.SERVER_CONFIG_PATH):
|
if not skip_self and os.path.exists(const.SERVER_CONFIG_PATH):
|
||||||
click.echo("Self-enrolling authority's web interface certificate")
|
click.echo("Self-enrolling authority's web interface certificate")
|
||||||
from certidude import authority
|
from certidude import authority
|
||||||
@ -182,7 +185,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
try:
|
try:
|
||||||
authority_path = clients.get(authority_name, "authority path")
|
authority_path = clients.get(authority_name, "authority path")
|
||||||
except NoOptionError:
|
except NoOptionError:
|
||||||
authority_path = "/var/lib/certidude/%s/ca_cert.pem" % authority_name
|
authority_path = "/etc/certidude/authority/%s/ca_cert.pem" % authority_name
|
||||||
finally:
|
finally:
|
||||||
if os.path.exists(authority_path):
|
if os.path.exists(authority_path):
|
||||||
click.echo("Found authority certificate in: %s" % authority_path)
|
click.echo("Found authority certificate in: %s" % authority_path)
|
||||||
@ -233,7 +236,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
# pip
|
# pip
|
||||||
|
|
||||||
# Firefox (?) on Debian, Ubuntu
|
# Firefox (?) on Debian, Ubuntu
|
||||||
if os.path.exists("/usr/bin/update-ca-certificates"):
|
if os.path.exists("/usr/bin/update-ca-certificates") or os.path.exists("/usr/sbin/update-ca-certificates"):
|
||||||
link_path = "/usr/local/share/ca-certificates/%s" % authority_name
|
link_path = "/usr/local/share/ca-certificates/%s" % authority_name
|
||||||
if not os.path.lexists(link_path):
|
if not os.path.lexists(link_path):
|
||||||
os.symlink(authority_path, link_path)
|
os.symlink(authority_path, link_path)
|
||||||
@ -257,11 +260,13 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
r = requests.get(revoked_url, headers={'accept': 'application/x-pem-file'})
|
r = requests.get(revoked_url, headers={'accept': 'application/x-pem-file'})
|
||||||
|
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
revocations = crl.CertificateList.load(pem.unarmor(r.content))
|
header, _, crl_der_bytes = pem.unarmor(r.content)
|
||||||
|
revocations = CertificateList.load(crl_der_bytes)
|
||||||
# TODO: check signature, parse reasons, remove keys if revoked
|
# TODO: check signature, parse reasons, remove keys if revoked
|
||||||
revocations_partial = revocations_path + ".part"
|
revocations_partial = revocations_path + ".part"
|
||||||
with open(revocations_partial, 'wb') as f:
|
with open(revocations_partial, 'wb') as f:
|
||||||
f.write(r.content)
|
f.write(r.content)
|
||||||
|
os.rename(revocations_partial, revocations_path)
|
||||||
elif r.status_code == 404:
|
elif r.status_code == 404:
|
||||||
click.echo("CRL disabled, server said 404")
|
click.echo("CRL disabled, server said 404")
|
||||||
else:
|
else:
|
||||||
@ -293,8 +298,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
key_path = clients.get(authority_name, "key path")
|
key_path = clients.get(authority_name, "key path")
|
||||||
request_path = clients.get(authority_name, "request path")
|
request_path = clients.get(authority_name, "request path")
|
||||||
except NoOptionError:
|
except NoOptionError:
|
||||||
key_path = "/var/lib/certidude/%s/client_key.pem" % authority_name
|
key_path = "/etc/certidude/authority/%s/host_key.pem" % authority_name
|
||||||
request_path = "/var/lib/certidude/%s/client_csr.pem" % authority_name
|
request_path = "/etc/certidude/authority/%s/host_csr.pem" % authority_name
|
||||||
|
|
||||||
if os.path.exists(request_path):
|
if os.path.exists(request_path):
|
||||||
with open(request_path, "rb") as fh:
|
with open(request_path, "rb") as fh:
|
||||||
@ -334,7 +339,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
try:
|
try:
|
||||||
certificate_path = clients.get(authority_name, "certificate path")
|
certificate_path = clients.get(authority_name, "certificate path")
|
||||||
except NoOptionError:
|
except NoOptionError:
|
||||||
certificate_path = "/var/lib/certidude/%s/client_cert.pem" % authority_name
|
certificate_path = "/etc/certidude/authority/%s/host_cert.pem" % authority_name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
renewal_overlap = clients.getint(authority_name, "renewal overlap")
|
renewal_overlap = clients.getint(authority_name, "renewal overlap")
|
||||||
@ -352,10 +357,15 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
except EnvironmentError: # Certificate missing, can't renew
|
except EnvironmentError: # Certificate missing, can't renew
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
autosign = clients.getboolean(authority_name, "autosign")
|
||||||
|
except NoOptionError:
|
||||||
|
autosign = True
|
||||||
|
|
||||||
if not os.path.exists(certificate_path) or renew:
|
if not os.path.exists(certificate_path) or renew:
|
||||||
# Set up URL-s
|
# Set up URL-s
|
||||||
request_params = set()
|
request_params = set()
|
||||||
request_params.add("autosign=true")
|
request_params.add("autosign=%s" % ("yes" if autosign else "no"))
|
||||||
if not no_wait:
|
if not no_wait:
|
||||||
request_params.add("wait=forever")
|
request_params.add("wait=forever")
|
||||||
|
|
||||||
@ -371,6 +381,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
if renew: # Do mutually authenticated TLS handshake
|
if renew: # Do mutually authenticated TLS handshake
|
||||||
request_url = "https://%s:8443/api/request/" % authority_name
|
request_url = "https://%s:8443/api/request/" % authority_name
|
||||||
kwargs["cert"] = certificate_path, key_path
|
kwargs["cert"] = certificate_path, key_path
|
||||||
|
click.echo("Renewing using current keypair at %s %s" % kwargs["cert"])
|
||||||
else:
|
else:
|
||||||
# If machine is joined to domain attempt to present machine credentials for authentication
|
# If machine is joined to domain attempt to present machine credentials for authentication
|
||||||
if kerberos:
|
if kerberos:
|
||||||
@ -416,6 +427,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
elif submission.status_code == requests.codes.gone:
|
elif submission.status_code == requests.codes.gone:
|
||||||
# Should the client retry or disable request submission?
|
# Should the client retry or disable request submission?
|
||||||
raise ValueError("Server refused to sign the request") # TODO: Raise proper exception
|
raise ValueError("Server refused to sign the request") # TODO: Raise proper exception
|
||||||
|
elif submission.status_code == requests.codes.bad_request:
|
||||||
|
raise ValueError("Server said following, likely current certificate expired/revoked? %s" % submission.text)
|
||||||
else:
|
else:
|
||||||
submission.raise_for_status()
|
submission.raise_for_status()
|
||||||
|
|
||||||
@ -964,12 +977,9 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
|
|||||||
help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default")
|
help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default")
|
||||||
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN)
|
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN)
|
||||||
@click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN)
|
@click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN)
|
||||||
@click.option("--country", "-c", default=None, help="Country, none by default")
|
|
||||||
@click.option("--state", "-s", default=None, help="State or country, none by default")
|
|
||||||
@click.option("--locality", "-l", default=None, help="City or locality, none by default")
|
|
||||||
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default")
|
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default")
|
||||||
@click.option("--organization", "-o", default=None, help="Company or organization name")
|
@click.option("--organization", "-o", default=None, help="Company or organization name")
|
||||||
@click.option("--organizational-unit", "-o", default=None)
|
@click.option("--organizational-unit", "-ou", default="Certificate Authority")
|
||||||
@click.option("--push-server", help="Push server, by default http://%s" % const.FQDN)
|
@click.option("--push-server", help="Push server, by default http://%s" % const.FQDN)
|
||||||
@click.option("--directory", help="Directory for authority files")
|
@click.option("--directory", help="Directory for authority files")
|
||||||
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
|
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
|
||||||
@ -977,7 +987,8 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
|
|||||||
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
|
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
|
||||||
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
|
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
|
||||||
@fqdn_required
|
@fqdn_required
|
||||||
def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags, title, skip_packages, elliptic_curve):
|
def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags, title, skip_packages, elliptic_curve):
|
||||||
|
assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) == b"xenial\n", "Only Ubuntu 16.04 supported at the moment"
|
||||||
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
|
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
|
||||||
|
|
||||||
import pwd
|
import pwd
|
||||||
@ -992,7 +1003,8 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
cython3 python3-dev python3-mimeparse \
|
cython3 python3-dev python3-mimeparse \
|
||||||
python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
|
python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
|
||||||
software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
|
software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
|
||||||
libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev rsync attr")
|
libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \
|
||||||
|
rsync attr wget unzip")
|
||||||
os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam")
|
os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam")
|
||||||
os.system("pip3 install -q --pre --upgrade python-ldap")
|
os.system("pip3 install -q --pre --upgrade python-ldap")
|
||||||
|
|
||||||
@ -1096,9 +1108,18 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
else:
|
else:
|
||||||
click.echo("Not systemd based OS, don't know how to set up initscripts")
|
click.echo("Not systemd based OS, don't know how to set up initscripts")
|
||||||
|
|
||||||
|
# Set umask to 0022
|
||||||
|
os.umask(0o022)
|
||||||
assert os.getuid() == 0 and os.getgid() == 0
|
assert os.getuid() == 0 and os.getgid() == 0
|
||||||
|
|
||||||
bootstrap_pid = os.fork()
|
bootstrap_pid = os.fork()
|
||||||
if not bootstrap_pid:
|
if not bootstrap_pid:
|
||||||
|
|
||||||
|
# Create what's usually /var/lib/certidude
|
||||||
|
if not os.path.exists(directory):
|
||||||
|
os.makedirs(directory)
|
||||||
|
assert os.stat(directory).st_mode == 0o40755
|
||||||
|
|
||||||
# Create bundle directories
|
# Create bundle directories
|
||||||
bundle_js = os.path.join(assets_dir, "js", "bundle.js")
|
bundle_js = os.path.join(assets_dir, "js", "bundle.js")
|
||||||
bundle_css = os.path.join(assets_dir, "css", "bundle.css")
|
bundle_css = os.path.join(assets_dir, "css", "bundle.css")
|
||||||
@ -1108,6 +1129,10 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
click.echo("Creating directory %s" % subdir)
|
click.echo("Creating directory %s" % subdir)
|
||||||
os.makedirs(subdir)
|
os.makedirs(subdir)
|
||||||
|
|
||||||
|
# Copy fonts
|
||||||
|
click.echo("Copying fonts...")
|
||||||
|
os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir)
|
||||||
|
|
||||||
# Install JavaScript pacakges
|
# Install JavaScript pacakges
|
||||||
if skip_packages:
|
if skip_packages:
|
||||||
click.echo("Not attempting to install packages from NPM as requested...")
|
click.echo("Not attempting to install packages from NPM as requested...")
|
||||||
@ -1140,10 +1165,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
os.rename(bundle_css + ".part", bundle_css)
|
os.rename(bundle_css + ".part", bundle_css)
|
||||||
os.rename(bundle_js + ".part", bundle_js)
|
os.rename(bundle_js + ".part", bundle_js)
|
||||||
|
|
||||||
# Copy fonts
|
|
||||||
click.echo("Copying fonts...")
|
|
||||||
os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir)
|
|
||||||
|
|
||||||
assert os.getuid() == 0 and os.getgid() == 0
|
assert os.getuid() == 0 and os.getgid() == 0
|
||||||
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
|
_, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
|
||||||
os.setgid(gid)
|
os.setgid(gid)
|
||||||
@ -1152,10 +1173,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
if not os.path.exists(const.CONFIG_DIR):
|
if not os.path.exists(const.CONFIG_DIR):
|
||||||
click.echo("Creating %s" % const.CONFIG_DIR)
|
click.echo("Creating %s" % const.CONFIG_DIR)
|
||||||
os.makedirs(const.CONFIG_DIR)
|
os.makedirs(const.CONFIG_DIR)
|
||||||
|
|
||||||
|
os.umask(0o137) # 640
|
||||||
if os.path.exists(const.SERVER_CONFIG_PATH):
|
if os.path.exists(const.SERVER_CONFIG_PATH):
|
||||||
click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
|
click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
|
||||||
else:
|
else:
|
||||||
os.umask(0o137)
|
|
||||||
push_token = "".join([random.choice(string.ascii_letters + string.digits) for j in range(0,32)])
|
push_token = "".join([random.choice(string.ascii_letters + string.digits) for j in range(0,32)])
|
||||||
with open(const.SERVER_CONFIG_PATH, "w") as fh:
|
with open(const.SERVER_CONFIG_PATH, "w") as fh:
|
||||||
fh.write(env.get_template("server/server.conf").render(vars()))
|
fh.write(env.get_template("server/server.conf").render(vars()))
|
||||||
@ -1169,7 +1191,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
fh.write(env.get_template("server/builder.conf").render(vars()))
|
fh.write(env.get_template("server/builder.conf").render(vars()))
|
||||||
click.echo("File %s created" % const.BUILDER_CONFIG_PATH)
|
click.echo("File %s created" % const.BUILDER_CONFIG_PATH)
|
||||||
|
|
||||||
# Create image builder config
|
# Create signature profile config
|
||||||
if os.path.exists(const.PROFILE_CONFIG_PATH):
|
if os.path.exists(const.PROFILE_CONFIG_PATH):
|
||||||
click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
|
click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
|
||||||
else:
|
else:
|
||||||
@ -1177,10 +1199,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
fh.write(env.get_template("server/profile.conf").render(vars()))
|
fh.write(env.get_template("server/profile.conf").render(vars()))
|
||||||
click.echo("File %s created" % const.PROFILE_CONFIG_PATH)
|
click.echo("File %s created" % const.PROFILE_CONFIG_PATH)
|
||||||
|
|
||||||
# Create directory with 755 permissions
|
if not os.path.exists("/var/lib/certidude/builder"):
|
||||||
os.umask(0o022)
|
click.echo("Creating %s" % "/var/lib/certidude/builder")
|
||||||
if not os.path.exists(directory):
|
os.makedirs("/var/lib/certidude/builder")
|
||||||
os.makedirs(directory)
|
|
||||||
|
|
||||||
# Create subdirectories with 770 permissions
|
# Create subdirectories with 770 permissions
|
||||||
os.umask(0o007)
|
os.umask(0o007)
|
||||||
@ -1191,10 +1212,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
os.mkdir(path)
|
os.mkdir(path)
|
||||||
else:
|
else:
|
||||||
click.echo("Directory already exists %s" % path)
|
click.echo("Directory already exists %s" % path)
|
||||||
|
assert os.stat(path).st_mode == 0o40770
|
||||||
|
|
||||||
# Create SQLite database file with correct permissions
|
# Create SQLite database file with correct permissions
|
||||||
|
os.umask(0o117)
|
||||||
if not os.path.exists(sqlite_path):
|
if not os.path.exists(sqlite_path):
|
||||||
os.umask(0o117)
|
|
||||||
with open(sqlite_path, "wb") as fh:
|
with open(sqlite_path, "wb") as fh:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -1207,16 +1229,10 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
click.echo("Generating %d-bit RSA key for CA ..." % const.KEY_SIZE)
|
click.echo("Generating %d-bit RSA key for CA ..." % const.KEY_SIZE)
|
||||||
public_key, private_key = asymmetric.generate_pair("rsa", bit_size=const.KEY_SIZE)
|
public_key, private_key = asymmetric.generate_pair("rsa", bit_size=const.KEY_SIZE)
|
||||||
|
|
||||||
names = (
|
# https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx
|
||||||
("country_name", country),
|
|
||||||
("state_or_province_name", state),
|
|
||||||
("locality_name", locality),
|
|
||||||
("organization_name", organization),
|
|
||||||
("common_name", title)
|
|
||||||
)
|
|
||||||
|
|
||||||
builder = CertificateBuilder(
|
builder = CertificateBuilder(
|
||||||
dict([(k,v) for (k,v) in names if v]),
|
cn_to_dn("Certidude at %s" % common_name, common_name,
|
||||||
|
o=organization, ou=organizational_unit),
|
||||||
public_key
|
public_key
|
||||||
)
|
)
|
||||||
builder.self_signed = True
|
builder.self_signed = True
|
||||||
@ -1239,7 +1255,13 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
os.umask(0o177)
|
os.umask(0o177)
|
||||||
with open(ca_key, 'wb') as f:
|
with open(ca_key, 'wb') as f:
|
||||||
f.write(asymmetric.dump_private_key(private_key, None))
|
f.write(asymmetric.dump_private_key(private_key, None))
|
||||||
|
|
||||||
sys.exit(0) # stop this fork here
|
sys.exit(0) # stop this fork here
|
||||||
|
|
||||||
|
assert os.stat(sqlite_path).st_mode == 0o100640
|
||||||
|
assert os.stat(ca_cert).st_mode == 0o100640
|
||||||
|
assert os.stat(ca_key).st_mode == 0o100600
|
||||||
|
assert os.stat("/etc/nginx/sites-available/certidude.conf").st_mode == 0o100640
|
||||||
else:
|
else:
|
||||||
os.waitpid(bootstrap_pid, 0)
|
os.waitpid(bootstrap_pid, 0)
|
||||||
from certidude import authority
|
from certidude import authority
|
||||||
@ -1322,7 +1344,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
|
|||||||
click.echo("y " + path)
|
click.echo("y " + path)
|
||||||
continue
|
continue
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white"))
|
click.echo(click.style(common_name, fg="blue") + " " + click.style("%040x" % cert.serial_number, fg="white"))
|
||||||
click.echo("="*(len(common_name)+60))
|
click.echo("="*(len(common_name)+60))
|
||||||
|
|
||||||
if signed < NOW and NOW < expires:
|
if signed < NOW and NOW < expires:
|
||||||
@ -1338,15 +1360,15 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
|
|||||||
click.echo(" - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native)))
|
click.echo(" - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native)))
|
||||||
|
|
||||||
if show_revoked:
|
if show_revoked:
|
||||||
for common_name, path, buf, cert, signed, expires, revoked in authority.list_revoked():
|
for common_name, path, buf, cert, signed, expires, revoked, reason in authority.list_revoked():
|
||||||
if not verbose:
|
if not verbose:
|
||||||
click.echo("r " + path)
|
click.echo("r " + path)
|
||||||
continue
|
continue
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white"))
|
click.echo(click.style(common_name, fg="blue") + " " + click.style("%040x" % cert.serial_number, fg="white"))
|
||||||
click.echo("="*(len(common_name)+60))
|
click.echo("="*(len(common_name)+60))
|
||||||
|
|
||||||
click.echo("Status: " + click.style("revoked", fg="red") + " %s%s" % (naturaltime(NOW-revoked), click.style(", %s" % revoked, fg="white")))
|
click.echo("Status: " + click.style("revoked", fg="red") + " due to " + reason + " %s%s" % (naturaltime(NOW-revoked), click.style(", %s" % revoked, fg="white")))
|
||||||
click.echo("openssl x509 -in %s -text -noout" % path)
|
click.echo("openssl x509 -in %s -text -noout" % path)
|
||||||
dump_common(common_name, path, cert)
|
dump_common(common_name, path, cert)
|
||||||
for ext in cert["tbs_certificate"]["extensions"]:
|
for ext in cert["tbs_certificate"]["extensions"]:
|
||||||
@ -1358,17 +1380,18 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
|
|||||||
@click.option("--profile", "-p", default="rw", help="Profile")
|
@click.option("--profile", "-p", default="rw", help="Profile")
|
||||||
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
|
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
|
||||||
def certidude_sign(common_name, overwrite, profile):
|
def certidude_sign(common_name, overwrite, profile):
|
||||||
from certidude import authority
|
from certidude import authority, config
|
||||||
drop_privileges()
|
drop_privileges()
|
||||||
cert = authority.sign(common_name, overwrite=overwrite, profile=config.PROFILES[profile])
|
cert = authority.sign(common_name, overwrite=overwrite, profile=config.PROFILES[profile])
|
||||||
|
|
||||||
|
|
||||||
@click.command("revoke", help="Revoke certificate")
|
@click.command("revoke", help="Revoke certificate")
|
||||||
|
@click.option("--reason", "-r", default="key_compromise", help="Revocation reason, one of: key_compromise affiliation_changed superseded cessation_of_operation privilege_withdrawn")
|
||||||
@click.argument("common_name")
|
@click.argument("common_name")
|
||||||
def certidude_revoke(common_name):
|
def certidude_revoke(common_name, reason):
|
||||||
from certidude import authority
|
from certidude import authority
|
||||||
drop_privileges()
|
drop_privileges()
|
||||||
authority.revoke(common_name)
|
authority.revoke(common_name, reason)
|
||||||
|
|
||||||
|
|
||||||
@click.command("expire", help="Move expired certificates")
|
@click.command("expire", help="Move expired certificates")
|
||||||
@ -1377,13 +1400,13 @@ def certidude_expire():
|
|||||||
threshold = datetime.utcnow() - timedelta(minutes=5) # Kerberos tolerance
|
threshold = datetime.utcnow() - timedelta(minutes=5) # Kerberos tolerance
|
||||||
for common_name, path, buf, cert, signed, expires in authority.list_signed():
|
for common_name, path, buf, cert, signed, expires in authority.list_signed():
|
||||||
if expires < threshold:
|
if expires < threshold:
|
||||||
expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial_number)
|
expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
|
||||||
click.echo("Moving %s to %s" % (path, expired_path))
|
click.echo("Moving %s to %s" % (path, expired_path))
|
||||||
os.rename(path, expired_path)
|
os.rename(path, expired_path)
|
||||||
os.remove(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number))
|
os.remove(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number))
|
||||||
for common_name, path, buf, cert, signed, expires, revoked in authority.list_revoked():
|
for common_name, path, buf, cert, signed, expires, revoked, reason in authority.list_revoked():
|
||||||
if expires < threshold:
|
if expires < threshold:
|
||||||
expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial_number)
|
expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
|
||||||
click.echo("Moving %s to %s" % (path, expired_path))
|
click.echo("Moving %s to %s" % (path, expired_path))
|
||||||
os.rename(path, expired_path)
|
os.rename(path, expired_path)
|
||||||
# TODO: Send e-mail
|
# TODO: Send e-mail
|
||||||
@ -1412,7 +1435,7 @@ def certidude_serve(port, listen, fork):
|
|||||||
|
|
||||||
# Rebuild reverse mapping
|
# Rebuild reverse mapping
|
||||||
for cn, path, buf, cert, signed, expires in authority.list_signed():
|
for cn, path, buf, cert, signed, expires in authority.list_signed():
|
||||||
by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number)
|
by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number)
|
||||||
if not os.path.exists(by_serial):
|
if not os.path.exists(by_serial):
|
||||||
click.echo("Linking %s to ../%s.pem" % (by_serial, cn))
|
click.echo("Linking %s to ../%s.pem" % (by_serial, cn))
|
||||||
os.symlink("../%s.pem" % cn, by_serial)
|
os.symlink("../%s.pem" % cn, by_serial)
|
||||||
@ -1423,14 +1446,6 @@ def certidude_serve(port, listen, fork):
|
|||||||
os.makedirs(const.RUN_DIR)
|
os.makedirs(const.RUN_DIR)
|
||||||
os.chmod(const.RUN_DIR, 0o755)
|
os.chmod(const.RUN_DIR, 0o755)
|
||||||
|
|
||||||
# TODO: umask!
|
|
||||||
|
|
||||||
|
|
||||||
from logging.handlers import RotatingFileHandler
|
|
||||||
rh = RotatingFileHandler("/var/log/certidude.log", maxBytes=1048576*5, backupCount=5)
|
|
||||||
rh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
|
|
||||||
log_handlers.append(rh)
|
|
||||||
|
|
||||||
click.echo("Users subnets: %s" %
|
click.echo("Users subnets: %s" %
|
||||||
", ".join([str(j) for j in config.USER_SUBNETS]))
|
", ".join([str(j) for j in config.USER_SUBNETS]))
|
||||||
click.echo("Administrative subnets: %s" %
|
click.echo("Administrative subnets: %s" %
|
||||||
@ -1440,10 +1455,6 @@ def certidude_serve(port, listen, fork):
|
|||||||
click.echo("Request submissions allowed from following subnets: %s" %
|
click.echo("Request submissions allowed from following subnets: %s" %
|
||||||
", ".join([str(j) for j in config.REQUEST_SUBNETS]))
|
", ".join([str(j) for j in config.REQUEST_SUBNETS]))
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
filename=const.SERVER_LOG_PATH,
|
|
||||||
level=logging.DEBUG)
|
|
||||||
|
|
||||||
click.echo("Serving API at %s:%d" % (listen, port))
|
click.echo("Serving API at %s:%d" % (listen, port))
|
||||||
from wsgiref.simple_server import make_server, WSGIServer
|
from wsgiref.simple_server import make_server, WSGIServer
|
||||||
from certidude.api import certidude_app
|
from certidude.api import certidude_app
|
||||||
@ -1474,7 +1485,6 @@ def certidude_serve(port, listen, fork):
|
|||||||
for handler in log_handlers:
|
for handler in log_handlers:
|
||||||
j.addHandler(handler)
|
j.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
if not fork or not os.fork():
|
if not fork or not os.fork():
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
with open(const.SERVER_PID_PATH, "w") as pidfile:
|
with open(const.SERVER_PID_PATH, "w") as pidfile:
|
||||||
|
@ -3,6 +3,65 @@ import os
|
|||||||
import click
|
import click
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
MAPPING = dict(
|
||||||
|
common_name="CN",
|
||||||
|
organizational_unit_name="OU",
|
||||||
|
organization_name="O",
|
||||||
|
domain_component="DC"
|
||||||
|
)
|
||||||
|
|
||||||
|
def cert_to_dn(cert):
|
||||||
|
d = []
|
||||||
|
for key, value in cert["tbs_certificate"]["subject"].native.items():
|
||||||
|
if not isinstance(value, list):
|
||||||
|
value = [value]
|
||||||
|
for comp in value:
|
||||||
|
d.append("%s=%s" % (MAPPING[key], comp))
|
||||||
|
return ", ".join(d)
|
||||||
|
|
||||||
|
def cn_to_dn(common_name, namespace, o=None, ou=None):
|
||||||
|
from asn1crypto.x509 import Name, RelativeDistinguishedName, NameType, DirectoryString, RDNSequence, NameTypeAndValue, UTF8String, DNSName
|
||||||
|
|
||||||
|
rdns = []
|
||||||
|
rdns.append(RelativeDistinguishedName([
|
||||||
|
NameTypeAndValue({
|
||||||
|
'type': NameType.map("common_name"),
|
||||||
|
'value': DirectoryString(
|
||||||
|
name="utf8_string",
|
||||||
|
value=UTF8String(common_name))
|
||||||
|
})
|
||||||
|
]))
|
||||||
|
|
||||||
|
if ou:
|
||||||
|
rdns.append(RelativeDistinguishedName([
|
||||||
|
NameTypeAndValue({
|
||||||
|
'type': NameType.map("organizational_unit_name"),
|
||||||
|
'value': DirectoryString(
|
||||||
|
name="utf8_string",
|
||||||
|
value=UTF8String(ou))
|
||||||
|
})
|
||||||
|
]))
|
||||||
|
|
||||||
|
if o:
|
||||||
|
rdns.append(RelativeDistinguishedName([
|
||||||
|
NameTypeAndValue({
|
||||||
|
'type': NameType.map("organization_name"),
|
||||||
|
'value': DirectoryString(
|
||||||
|
name="utf8_string",
|
||||||
|
value=UTF8String(o))
|
||||||
|
})
|
||||||
|
]))
|
||||||
|
|
||||||
|
for dc in namespace.split("."):
|
||||||
|
rdns.append(RelativeDistinguishedName([
|
||||||
|
NameTypeAndValue({
|
||||||
|
'type': NameType.map("domain_component"),
|
||||||
|
'value': DNSName(value=dc)
|
||||||
|
})
|
||||||
|
]))
|
||||||
|
|
||||||
|
return Name(name='', value=RDNSequence(rdns))
|
||||||
|
|
||||||
def selinux_fixup(path):
|
def selinux_fixup(path):
|
||||||
"""
|
"""
|
||||||
Fix OpenVPN credential store security context on Fedora
|
Fix OpenVPN credential store security context on Fedora
|
||||||
|
@ -18,6 +18,7 @@ ACCOUNTS_BACKEND = cp.get("accounts", "backend") # posix, ldap
|
|||||||
MAIL_SUFFIX = cp.get("accounts", "mail suffix")
|
MAIL_SUFFIX = cp.get("accounts", "mail suffix")
|
||||||
|
|
||||||
KERBEROS_KEYTAB = cp.get("authentication", "kerberos keytab")
|
KERBEROS_KEYTAB = cp.get("authentication", "kerberos keytab")
|
||||||
|
KERBEROS_REALM = cp.get("authentication", "kerberos realm")
|
||||||
LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri")
|
LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri")
|
||||||
LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache")
|
LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache")
|
||||||
LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri")
|
LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri")
|
||||||
@ -39,6 +40,10 @@ CRL_SUBNETS = set([ipaddress.ip_network(j) for j in
|
|||||||
cp.get("authorization", "crl subnets").split(" ") if j])
|
cp.get("authorization", "crl subnets").split(" ") if j])
|
||||||
RENEWAL_SUBNETS = set([ipaddress.ip_network(j) for j in
|
RENEWAL_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||||
cp.get("authorization", "renewal subnets").split(" ") if j])
|
cp.get("authorization", "renewal subnets").split(" ") if j])
|
||||||
|
OVERWRITE_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||||
|
cp.get("authorization", "overwrite subnets").split(" ") if j])
|
||||||
|
MACHINE_ENROLLMENT_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||||
|
cp.get("authorization", "machine enrollment subnets").split(" ") if j])
|
||||||
|
|
||||||
AUTHORITY_DIR = "/var/lib/certidude"
|
AUTHORITY_DIR = "/var/lib/certidude"
|
||||||
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
|
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
|
||||||
@ -54,9 +59,6 @@ MAILER_ADDRESS = cp.get("mailer", "address")
|
|||||||
|
|
||||||
BOOTSTRAP_TEMPLATE = cp.get("bootstrap", "services template")
|
BOOTSTRAP_TEMPLATE = cp.get("bootstrap", "services template")
|
||||||
|
|
||||||
MACHINE_ENROLLMENT_ALLOWED = {
|
|
||||||
"forbidden": False, "allowed": True }[
|
|
||||||
cp.get("authority", "machine enrollment")]
|
|
||||||
USER_ENROLLMENT_ALLOWED = {
|
USER_ENROLLMENT_ALLOWED = {
|
||||||
"forbidden": False, "single allowed": True, "multiple allowed": True }[
|
"forbidden": False, "single allowed": True, "multiple allowed": True }[
|
||||||
cp.get("authority", "user enrollment")]
|
cp.get("authority", "user enrollment")]
|
||||||
@ -117,3 +119,6 @@ cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r"))
|
|||||||
IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()]
|
IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()]
|
||||||
|
|
||||||
TOKEN_OVERWRITE_PERMITTED=True
|
TOKEN_OVERWRITE_PERMITTED=True
|
||||||
|
|
||||||
|
SERVICE_PROTOCOLS = set([j.lower() for j in cp.get("service", "protocols").split(" ") if j])
|
||||||
|
SERVICE_ROUTERS = cp.get("service", "routers")
|
||||||
|
@ -6,9 +6,9 @@ import sys
|
|||||||
|
|
||||||
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
|
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
|
||||||
CURVE_NAME = "secp384r1"
|
CURVE_NAME = "secp384r1"
|
||||||
RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])?$"
|
RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$"
|
||||||
RE_HOSTNAME = "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
|
RE_HOSTNAME = "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$"
|
||||||
RE_COMMON_NAME = "^[A-Za-z0-9\-\.\@]+$"
|
RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$"
|
||||||
|
|
||||||
RUN_DIR = "/run/certidude"
|
RUN_DIR = "/run/certidude"
|
||||||
CONFIG_DIR = "/etc/certidude"
|
CONFIG_DIR = "/etc/certidude"
|
||||||
@ -25,6 +25,8 @@ try:
|
|||||||
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
|
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
|
||||||
except socket.gaierror:
|
except socket.gaierror:
|
||||||
FQDN = socket.gethostname()
|
FQDN = socket.gethostname()
|
||||||
|
if hasattr(FQDN, "decode"): # Keep client backwards compatible with Python 2.x
|
||||||
|
FQDN = FQDN.decode("ascii")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
HOSTNAME, DOMAIN = FQDN.split(".", 1)
|
HOSTNAME, DOMAIN = FQDN.split(".", 1)
|
||||||
|
@ -56,8 +56,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a href="http://github.com/laurivosandi/certidude">Certidude</a> by
|
<a href="https://github.com/laurivosandi/certidude">Certidude</a> by
|
||||||
<a href="http://github.com/laurivosandi/">Lauri Võsandi</a>
|
<a href="https://github.com/laurivosandi/">Lauri Võsandi</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
@ -305,7 +305,8 @@ function loadAuthority() {
|
|||||||
/**
|
/**
|
||||||
* Render authority views
|
* Render authority views
|
||||||
**/
|
**/
|
||||||
$("#view").html(env.render('views/authority.html', { session: session, window: window }));
|
$("#view").html(env.render('views/authority.html', { session: session, window: window,
|
||||||
|
authority_name: window.location.hostname }));
|
||||||
$("time").timeago();
|
$("time").timeago();
|
||||||
if (session.authority) {
|
if (session.authority) {
|
||||||
$("#log input").each(function(i, e) {
|
$("#log input").each(function(i, e) {
|
||||||
@ -462,12 +463,8 @@ function datetimeFilter(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function serialFilter(s) {
|
function serialFilter(s) {
|
||||||
return s.substring(0,8) + " " +
|
return s.substring(0,s.length-14) + " " +
|
||||||
s.substring(8,12) + " " +
|
s.substring(s.length-14);
|
||||||
s.substring(12,16) + " " +
|
|
||||||
s.substring(16,28) + " " +
|
|
||||||
s.substring(28,32) + " " +
|
|
||||||
s.substring(32);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
@ -7,26 +7,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<form action="/api/request/" method="post">
|
<form action="/api/request/" method="post">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h5>Certidude client</h5>
|
{% if "ikev2" in session.service.protocols %}
|
||||||
|
<h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5>
|
||||||
<p>Submit a certificate signing request from Mac OS X, Ubuntu or Fedora:</p>
|
|
||||||
<div class="highlight">
|
|
||||||
<pre><code>easy_install pip;
|
|
||||||
pip3 install certidude;
|
|
||||||
certidude bootstrap {{ window.location.hostname }}
|
|
||||||
</code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5>Windows 10</h5>
|
|
||||||
|
|
||||||
<p>On Windows execute following PowerShell script</p>
|
<p>On Windows execute following PowerShell script</p>
|
||||||
|
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre class="code"><code>$hostname = $env:computername.ToLower()
|
<pre class="code"><code># Install CA certificate
|
||||||
$templ = @"
|
@"
|
||||||
[Version]
|
{{ session.authority.certificate.blob }}
|
||||||
Signature="$Windows NT$
|
"@ | Out-File ca_cert.pem
|
||||||
|
{% if session.authority.certificate.algorithm == "ec" %}
|
||||||
|
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
|
||||||
|
{% else %}
|
||||||
|
C:\Windows\system32\certutil.exe -addstore Root ca_cert.pem
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
# Generate keypair and submit CSR
|
||||||
|
$hostname = $env:computername.ToLower()
|
||||||
|
@"
|
||||||
[NewRequest]
|
[NewRequest]
|
||||||
Subject = "CN=$hostname"
|
Subject = "CN=$hostname"
|
||||||
Exportable = FALSE
|
Exportable = FALSE
|
||||||
@ -39,21 +38,14 @@ RequestType = PKCS10
|
|||||||
KeyAlgorithm = ECDSA_P384
|
KeyAlgorithm = ECDSA_P384
|
||||||
{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
|
{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
|
||||||
KeyLength = 2048
|
KeyLength = 2048
|
||||||
{% endif %}"@
|
{% endif %}"@ | Out-File req.inf
|
||||||
|
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem
|
||||||
$templ | Out-File req.inf
|
Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ authority_name }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
|
||||||
|
|
||||||
# Fetch CA certificate and install it
|
|
||||||
Invoke-WebRequest -Uri http://{{ window.location.hostname }}/api/certificate -OutFile ca_cert.pem
|
|
||||||
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
|
|
||||||
|
|
||||||
# Generate keypair and submit CSR
|
|
||||||
C:\Windows\system32\certreq.exe -new -f -q req.inf client_csr.pem
|
|
||||||
Invoke-WebRequest -TimeoutSec 900 -Uri http://{{ window.location.hostname }}/api/request/?wait=1 -InFile client_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile client_cert.pem
|
|
||||||
|
|
||||||
# Import certificate
|
# Import certificate
|
||||||
Import-Certificate -FilePath client_cert.pem -CertStoreLocation Cert:\LocalMachine\My
|
{% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My
|
||||||
|
{% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem
|
||||||
|
{% endif %}
|
||||||
# Set up IPSec VPN tunnel
|
# Set up IPSec VPN tunnel
|
||||||
Remove-VpnConnection -AllUserConnection -Force k-space
|
Remove-VpnConnection -AllUserConnection -Force k-space
|
||||||
Add-VpnConnection `
|
Add-VpnConnection `
|
||||||
@ -84,21 +76,137 @@ IntegrityCheckMethod - IKE hash algorithm, one of: MD5 SHA196 SHA256 SHA384
|
|||||||
DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24
|
DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24
|
||||||
PfsGroup = one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24
|
PfsGroup = one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24
|
||||||
-->
|
-->
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h5>UNIX & UNIX-like</h5>
|
<h5>UNIX & UNIX-like</h5>
|
||||||
|
|
||||||
<p>On other UNIX-like machines generate key pair and submit the signing request using OpenSSL and cURL:</p>
|
<p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre class="code"><code>NAME=$(hostname);
|
<pre class="code"><code>test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname)
|
||||||
{% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout -out client_key.pem{% else %}openssl genrsa -out client_key.pem 2048;{% endif %}
|
test -e /bin/hostname && NAME=$(hostname)
|
||||||
openssl req -new -sha384 -key client_key.pem -out client_req.pem -subj "/CN=$NAME";
|
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
||||||
curl -f -L -H "Content-type: application/pkcs10" --data-binary @client_req.pem \
|
|
||||||
http://{{ window.location.hostname }}/api/request/?wait=yes > client_cert.pem</code></pre>
|
mkdir -p /etc/certidude/authority/{{ authority_name }}/
|
||||||
|
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|
||||||
|
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
|
||||||
|
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||||
|
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
||||||
|
{{ session.authority.certificate.blob }}EOF
|
||||||
|
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||||
|
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
|
||||||
|
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
|
||||||
|
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
|
||||||
|
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||||
|
|| openssl req -new -sha384 -subj "/CN=$NAME" \
|
||||||
|
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||||
|
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
||||||
|
echo "If CSR submission fails, you can copy paste it to Certidude:"
|
||||||
|
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
||||||
|
|
||||||
|
test -e /etc/pki/ca-trust/source/anchors \
|
||||||
|
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ authority_name }} \
|
||||||
|
&& update-ca-trust
|
||||||
|
test -e /usr/local/share/ca-certificates/ \
|
||||||
|
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /usr/local/share/ca-certificates/{{ authority_name }}.crt \
|
||||||
|
&& update-ca-certificates
|
||||||
|
|
||||||
|
curl -f -L -H "Content-type: application/pkcs10" \
|
||||||
|
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||||
|
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||||
|
'http://{{ authority_name }}/api/request/?wait=yes&autosign=yes'
|
||||||
|
</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p>For server certificates use fully qualified hostname as common name and sign request accordingly:</p>
|
||||||
|
<div class="highlight">
|
||||||
|
<pre class="code"><code>test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
||||||
|
test -e /bin/hostname && NAME=$(hostname -f)
|
||||||
|
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
||||||
|
|
||||||
|
mkdir -p /etc/certidude/authority/{{ authority_name }}/
|
||||||
|
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|
||||||
|
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
|
||||||
|
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||||
|
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
||||||
|
{{ session.authority.certificate.blob }}EOF
|
||||||
|
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||||
|
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
|
||||||
|
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
|
||||||
|
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
|
||||||
|
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||||
|
|| openssl req -new -sha384 -subj "/CN=$NAME" \
|
||||||
|
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||||
|
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
||||||
|
echo "If CSR submission fails, you can copy paste it to Certidude:"
|
||||||
|
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
||||||
|
|
||||||
|
curl -f -L -H "Content-type: application/pkcs10" \
|
||||||
|
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||||
|
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||||
|
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||||
|
'https://{{ authority_name }}:8443/api/request/?wait=yes'
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>To renew:</p>
|
||||||
|
|
||||||
|
<div class="highlight">
|
||||||
|
<pre class="code"><code>curl -f -L -H "Content-type: application/pkcs10" \
|
||||||
|
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||||
|
--key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||||
|
--cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||||
|
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||||
|
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||||
|
'https://{{ authority_name }}:8443/api/request/?wait=yes'
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if "openvpn" in session.service.protocols %}
|
||||||
|
|
||||||
|
<h5>OpenVPN as client</h5>
|
||||||
|
|
||||||
|
<p>First acquire certificates using the snippet above.</p>
|
||||||
|
|
||||||
|
<p>Then install software:</p>
|
||||||
|
|
||||||
|
<div class="highlight">
|
||||||
|
<pre class="code"><code># Install packages on Ubuntu & Fedora
|
||||||
|
which apt && apt install openvpn
|
||||||
|
which dnf && dnf install openvpn
|
||||||
|
|
||||||
|
cat > /etc/openvpn/{{ authority_name }}.conf << EOF
|
||||||
|
client
|
||||||
|
nobind
|
||||||
|
{% for router in session.service.routers %}
|
||||||
|
remote {{ router }} 1194 udp
|
||||||
|
remote {{ router }} 443 tcp-client
|
||||||
|
{% endfor %}
|
||||||
|
tls-version-min 1.2
|
||||||
|
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
|
||||||
|
cipher AES-128-GCM
|
||||||
|
auth SHA384
|
||||||
|
mute-replay-warnings
|
||||||
|
reneg-sec 0
|
||||||
|
remote-cert-tls server
|
||||||
|
dev tun
|
||||||
|
persist-tun
|
||||||
|
persist-key
|
||||||
|
ca /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
||||||
|
key /etc/certidude/authority/{{ authority_name }}/host_key.pem
|
||||||
|
cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl restart openvpn
|
||||||
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if "ikev2" in session.service.protocols %}
|
||||||
<h5>StrongSwan as client</h5>
|
<h5>StrongSwan as client</h5>
|
||||||
|
|
||||||
<p>First enroll certificates:</p>
|
<p>First acquire certificates using the snippet above.</p>
|
||||||
|
|
||||||
|
<p>Then install software:</p>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre class="code"><code># Install packages on Ubuntu & Fedora, patch Fedora paths
|
<pre class="code"><code># Install packages on Ubuntu & Fedora, patch Fedora paths
|
||||||
which apt && apt install strongswan
|
which apt && apt install strongswan
|
||||||
@ -107,82 +215,68 @@ test -e /etc/strongswan && test -e /etc/ipsec.conf || ln -s strongswan/ipsec.con
|
|||||||
test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d
|
test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d
|
||||||
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
|
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
|
||||||
|
|
||||||
FQDN=$(cat /etc/hostname)
|
# Hard link files to prevent Apparmor issues and have more manageable config
|
||||||
|
ln /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority_name }}.pem
|
||||||
# Install CA certificate
|
ln /etc/certidude/authority/{{ authority_name }}/host_cert.pem /etc/ipsec.d/certs/{{ authority_name }}.pem
|
||||||
cat << EOF > /etc/ipsec.d/cacerts/ca_cert.pem
|
ln /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem
|
||||||
{{ session.authority.certificate.blob }}EOF
|
</code></pre>
|
||||||
|
|
||||||
# Generate keypair
|
|
||||||
test -e /etc/ipsec.d/private/client.pem \
|
|
||||||
|| openssl {% if session.authority.certificate.algorithm == "ec" %}ecparam -name secp384r1 -genkey -noout -out /etc/ipsec.d/private/client.pem{% else %}genrsa -out /etc/ipsec.d/private/client.pem 2048{% endif %}
|
|
||||||
|
|
||||||
# Attempt to submit CSR
|
|
||||||
test -e /etc/ipsec.d/reqs/client.pem \
|
|
||||||
|| openssl req -new -sha384 \
|
|
||||||
-key /etc/ipsec.d/private/client.pem \
|
|
||||||
-out /etc/ipsec.d/reqs/client.pem -subj "/CN=$FQDN"
|
|
||||||
cat /etc/ipsec.d/reqs/client.pem
|
|
||||||
curl -f -L -H "Content-type: application/pkcs10" \
|
|
||||||
--data-binary @/etc/ipsec.d/reqs/client.pem \
|
|
||||||
-o /etc/ipsec.d/certs/client.pem \
|
|
||||||
http://{{ window.location.hostname }}/api/request/?wait=yes</code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p>To configure StrongSwan as roadwarrior:</p>
|
<p>To configure StrongSwan as roadwarrior:</p>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre class="code"><code>cat > /etc/ipsec.conf << EOF
|
<pre class="code"><code>cat > /etc/ipsec.conf << EOF
|
||||||
conn c2s
|
|
||||||
|
ca {{ authority_name }}
|
||||||
|
auto=add
|
||||||
|
cacert = {{ authority_name }}.pem
|
||||||
|
{% if session.features.crl %} crluri = http://{{ authority_name }}/api/revoked/{% endif %}
|
||||||
|
{% if session.features.ocsp %} ocspuri = http://{{ authority_name }}/api/ocsp/{% endif %}
|
||||||
|
|
||||||
|
conn client-to-site
|
||||||
auto=start
|
auto=start
|
||||||
right=router2.k-space.ee
|
right={{ session.service.routers[0] }}
|
||||||
|
rightsubnet=0.0.0.0/0
|
||||||
|
rightca="{{ session.authority.certificate.distinguished_name }}"
|
||||||
|
left=%defaultroute
|
||||||
|
leftcert={{ authority_name }}.pem
|
||||||
|
leftsourceip=%config
|
||||||
|
leftca="{{ session.authority.certificate.distinguished_name }}"
|
||||||
|
keyexchange=ikev2
|
||||||
|
keyingtries=%forever
|
||||||
dpdaction=restart
|
dpdaction=restart
|
||||||
closeaction=restart
|
closeaction=restart
|
||||||
left=%defaultroute
|
|
||||||
rightsubnet=0.0.0.0/0
|
|
||||||
keyingtries=%forever
|
|
||||||
rightid=%any
|
|
||||||
leftsourceip=%config
|
|
||||||
leftcert=client.pem
|
|
||||||
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||||
esp=aes128gcm16-aes128gmac!
|
esp=aes128gcm16-aes128gmac!
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} client.pem" > /etc/ipsec.secrets
|
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets
|
||||||
|
|
||||||
ipsec restart</code></pre>
|
ipsec restart</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<h5>OpenWrt/LEDE as VPN gateway</h5>
|
<h5>OpenWrt/LEDE as VPN gateway</h5>
|
||||||
|
|
||||||
<p>First enroll certificates:</p>
|
<p>First enroll certificates using the snippet from UNIX section above</p>
|
||||||
|
|
||||||
|
<p>Then:</p>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre class="code"><code>opkg install curl libmbedtls
|
<pre class="code"><code>opkg install curl libmbedtls
|
||||||
# Derive FQDN from WAN interface's reverse DNS record
|
# Derive FQDN from WAN interface's reverse DNS record
|
||||||
FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
||||||
|
|
||||||
mkdir -p /etc/certidude/authority/{{ window.location.hostname }}
|
|
||||||
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
|
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
|
||||||
cat << EOF > /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem
|
|
||||||
{{ session.authority.certificate.blob }}EOF
|
|
||||||
|
|
||||||
test -e /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
|
|
||||||
|| openssl {% if session.authority.certificate.algorithm == "ec" %}ecparam -name secp384r1 -genkey -noout -out /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem{% else %}genrsa -out /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem 2048{% endif %}
|
|
||||||
test -e /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
|
|
||||||
|| openssl req -new -sha384 \
|
|
||||||
-key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem \
|
|
||||||
-out /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem -subj "/CN=$FQDN"
|
|
||||||
cat /etc/certidude/authority/{{ window.location.hostname }}/server_req.pem
|
|
||||||
curl -f -L -H "Content-type: application/pkcs10" \
|
|
||||||
--data-binary @/etc/certidude/authority/{{ window.location.hostname }}/server_req.pem \
|
|
||||||
-o /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem \
|
|
||||||
http://{{ window.location.hostname }}/api/request/?wait=yes
|
|
||||||
|
|
||||||
# Create VPN gateway up/down script for reporting client IP addresses to CA
|
# Create VPN gateway up/down script for reporting client IP addresses to CA
|
||||||
cat <<\EOF > /etc/certidude/authority/{{ window.location.hostname }}/updown
|
cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
CURL="curl -m 3 -f --key /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem --cert /etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem --cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem https://{{ window.location.hostname }}:8443/api/lease/"
|
CURL="curl -m 3 -f --key /etc/certidude/authority/{{ authority_name }}/host_key.pem --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem https://{{ authority_name }}:8443/api/lease/"
|
||||||
|
|
||||||
case $PLUTO_VERB in
|
case $PLUTO_VERB in
|
||||||
up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;;
|
up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;;
|
||||||
@ -195,19 +289,23 @@ case $script_type in
|
|||||||
esac
|
esac
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x /etc/certidude/authority/{{ window.location.hostname }}/updown
|
chmod +x /etc/certidude/authority/{{ authority_name }}/updown
|
||||||
</code></pre>
|
</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% if "openvpn" in session.service.protocols %}
|
||||||
|
|
||||||
<p>Then either set up OpenVPN service:</p>
|
<p>Then either set up OpenVPN service:</p>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre class="code"><code>opkg update
|
<pre class="code"><code>opkg update
|
||||||
opkg install curl openssl-util openvpn-openssl
|
opkg install curl openssl-util openvpn-openssl
|
||||||
|
|
||||||
|
{% if session.authority.certificate.algorithm != "ec" %}
|
||||||
# Generate Diffie-Hellman parameters file for OpenVPN
|
# Generate Diffie-Hellman parameters file for OpenVPN
|
||||||
test -e /etc/certidude/dh.pem \
|
test -e /etc/certidude/dh.pem \
|
||||||
|| openssl dhparam 2048 -out /etc/certidude/dh.pem
|
|| openssl dhparam 2048 -out /etc/certidude/dh.pem
|
||||||
|
{% endif %}
|
||||||
# Create interface definition for tunnel
|
# Create interface definition for tunnel
|
||||||
uci set network.vpn=interface
|
uci set network.vpn=interface
|
||||||
uci set network.vpn.name='vpn'
|
uci set network.vpn.name='vpn'
|
||||||
@ -267,10 +365,10 @@ for section in s2c_tcp s2c_udp; do
|
|||||||
# Common paths
|
# Common paths
|
||||||
uci set openvpn.$section.script_security=2
|
uci set openvpn.$section.script_security=2
|
||||||
uci set openvpn.$section.client_connect='/etc/certidude/updown'
|
uci set openvpn.$section.client_connect='/etc/certidude/updown'
|
||||||
uci set openvpn.$section.key='/etc/certidude/authority/{{ window.location.hostname }}/server_key.pem'
|
uci set openvpn.$section.key='/etc/certidude/authority/{{ authority_name }}/host_key.pem'
|
||||||
uci set openvpn.$section.cert='/etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem'
|
uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem'
|
||||||
uci set openvpn.$section.ca='/etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem'
|
uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem'
|
||||||
uci set openvpn.$section.dh='/etc/certidude/dh.pem'
|
{% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %}
|
||||||
uci set openvpn.$section.enabled=1
|
uci set openvpn.$section.enabled=1
|
||||||
|
|
||||||
# DNS and routes
|
# DNS and routes
|
||||||
@ -291,6 +389,9 @@ done
|
|||||||
/etc/init.d/firewall restart</code></pre>
|
/etc/init.d/firewall restart</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if "ikev2" in session.service.protocols %}
|
||||||
<p>Alternatively or additionally set up StrongSwan:</p>
|
<p>Alternatively or additionally set up StrongSwan:</p>
|
||||||
<div class="highlight">
|
<div class="highlight">
|
||||||
<pre class="code"><code>opkg update
|
<pre class="code"><code>opkg update
|
||||||
@ -302,31 +403,46 @@ config setup
|
|||||||
strictcrlpolicy=yes
|
strictcrlpolicy=yes
|
||||||
uniqueids = yes
|
uniqueids = yes
|
||||||
|
|
||||||
ca {{ window.location.hostname }}
|
ca {{ authority_name }}
|
||||||
auto=add
|
auto=add
|
||||||
cacert = /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem
|
cacert = {{ authority_name }}.pem
|
||||||
{% if session.features.crl %} crluri = http://{{ window.location.hostname }}/api/revoked/{% endif %}
|
{% if session.features.crl %} crluri = http://{{ authority_name }}/api/revoked/{% endif %}
|
||||||
{% if session.features.ocsp %} ocspuri = http://{{ window.location.hostname }}/api/ocsp/{% endif %}
|
{% if session.features.ocsp %} ocspuri = http://{{ authority_name }}/api/ocsp/{% endif %}
|
||||||
|
|
||||||
conn s2c
|
conn default-{{ authority_name }}
|
||||||
auto=add
|
|
||||||
dpdaction=clear
|
|
||||||
closeaction=clear
|
|
||||||
leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
|
|
||||||
rightsourceip=172.21.0.0/24 # Roadwarrior virtual IP pool
|
|
||||||
left=$(uci get network.wan.ipaddr) # Bind to this IP address
|
|
||||||
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors
|
|
||||||
leftcert=/etc/certidude/authority/{{ window.location.hostname }}/server_cert.pem
|
|
||||||
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||||
esp=aes128gcm16-aes128gmac!
|
esp=aes128gcm16-aes128gmac!
|
||||||
leftupdown=/etc/certidude/authority/{{ window.location.hostname }}/updown
|
left=$(uci get network.wan.ipaddr) # Bind to this IP address
|
||||||
|
leftid={{ session.service.routers | first }}
|
||||||
|
leftupdown=/etc/certidude/authority/{{ authority_name }}/updown
|
||||||
|
leftcert={{ authority_name }}.pem
|
||||||
|
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors
|
||||||
|
leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
|
||||||
|
leftca="{{ session.authority.certificate.distinguished_name }}"
|
||||||
|
rightca="{{ session.authority.certificate.distinguished_name }}"
|
||||||
|
rightsourceip=172.21.0.0/24 # Roadwarrior virtual IP pool
|
||||||
|
dpddelay=0
|
||||||
|
dpdaction=clear
|
||||||
|
|
||||||
|
conn site-to-clients
|
||||||
|
auto=add
|
||||||
|
also=default-{{ authority_name }}
|
||||||
|
|
||||||
|
conn site-to-client1
|
||||||
|
auto=ignore
|
||||||
|
also=default-{{ authority_name }}
|
||||||
|
rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*"
|
||||||
|
rightsourceip=172.21.0.1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ window.location.hostname }}/server_key.pem" > /etc/ipsec.secrets
|
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets
|
||||||
|
|
||||||
ipsec restart</code></pre>
|
ipsec restart</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if session.authority.builder %}
|
{% if session.authority.builder %}
|
||||||
<h5>OpenWrt/LEDE image builder</h5>
|
<h5>OpenWrt/LEDE image builder</h5>
|
||||||
@ -339,7 +455,7 @@ ipsec restart</code></pre>
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h5>SCEP</h5>
|
<h5>SCEP</h5>
|
||||||
<p>Use following as the enrollment URL: http://{{ window.location.hostname }}/cgi-bin/pkiclient.exe</p>
|
<p>Use following as the enrollment URL: http://{{ authority_name }}/cgi-bin/pkiclient.exe</p>
|
||||||
|
|
||||||
<h5>Copy & paste</h5>
|
<h5>Copy & paste</h5>
|
||||||
|
|
||||||
@ -367,10 +483,10 @@ ipsec restart</code></pre>
|
|||||||
<h4 class="modal-title">Revocation lists</h4>
|
<h4 class="modal-title">Revocation lists</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>To fetch <a href="http://{{window.location.hostname}}/api/revoked/">certificate revocation list</a>:</p>
|
<p>To fetch <a href="http://{{authority_name}}/api/revoked/">certificate revocation list</a>:</p>
|
||||||
<pre><code>curl http://{{window.location.hostname}}/api/revoked/ > crl.der
|
<pre><code>curl http://{{authority_name}}/api/revoked/ > crl.der
|
||||||
curl http://{{window.location.hostname}}/api/revoked/ -L -H "Accept: application/x-pem-file"
|
curl http://{{authority_name}}/api/revoked/ -L -H "Accept: application/x-pem-file"
|
||||||
curl http://{{window.location.hostname}}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre>
|
curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn" data-dismiss="modal">Close</button>
|
<button type="button" class="btn" data-dismiss="modal">Close</button>
|
||||||
|
@ -4,5 +4,5 @@ Last seen
|
|||||||
at
|
at
|
||||||
<a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %}
|
<a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %}
|
||||||
from
|
from
|
||||||
<a target="{{ certificate.lease.outer_address }}" href="http://geoiplookup.net/ip/{{ certificate.lease.outer_address }}">{{ certificate.lease.outer_address }}</a>{% endif %}.
|
<a target="{{ certificate.lease.outer_address }}" href="https://geoiplookup.net/ip/{{ certificate.lease.outer_address }}">{{ certificate.lease.outer_address }}</a>{% endif %}.
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<i class="fa fa-folder" aria-hidden="true"></i>
|
<i class="fa fa-folder" aria-hidden="true"></i>
|
||||||
{{ certificate.organizational_unit }} /
|
{{ certificate.organizational_unit }} /
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if certificate.server %}
|
{% if certificate.extensions.extended_key_usage and "server_auth" in certificate.extensions.extended_key_usage %}
|
||||||
<i class="fa fa-server"></i>
|
<i class="fa fa-server"></i>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i class="fa fa-laptop"></i>
|
<i class="fa fa-laptop"></i>
|
||||||
@ -46,11 +46,11 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<a class="dropdown-item" href="#"
|
<a class="dropdown-item" href="#"
|
||||||
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=1',type:'delete'});">Revoke due to key compromise</a>
|
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=key_compromise',type:'delete'});">Revoke due to key compromise</a>
|
||||||
<a class="dropdown-item" href="#"
|
<a class="dropdown-item" href="#"
|
||||||
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=5',type:'delete'});">Revoke due to cessation of operation</a>
|
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=cessation_of_operation',type:'delete'});">Revoke due to cessation of operation</a>
|
||||||
<a class="dropdown-item" href="#"
|
<a class="dropdown-item" href="#"
|
||||||
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=9',type:'delete'});">Revoke due to withdrawn privilege</a>
|
onclick="javascript:$(this).button('loading');$.ajax({url:'/api/signed/{{certificate.common_name}}/?sha256sum={{ certificate.sha256sum }}&reason=privilege_withdrawn',type:'delete'});">Revoke due to withdrawn privilege</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -88,8 +88,10 @@ openssl ocsp -issuer session.pem -CAfile session.pem \
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>To fetch script:</p>
|
<p>To fetch script:</p>
|
||||||
<pre><code class="language-bash" data-lang="bash">cd /var/lib/certidude/{{ window.location.hostname }}/
|
<pre><code class="language-bash" data-lang="bash">curl https://{{ window.location.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/ \
|
||||||
curl --cert client_cert.pem https://{{ window.location.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/</pre></code>
|
--cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem \
|
||||||
|
--key /etc/certidude/authority/{{ window.location.hostname }}/host_key.pem \
|
||||||
|
--cert /etc/certidude/authority/{{ window.location.hostname }}/host_cert.pem</pre></code>
|
||||||
|
|
||||||
<div style="overflow: auto; max-width: 100%;">
|
<div style="overflow: auto; max-width: 100%;">
|
||||||
<table class="table" id="signed_certificates">
|
<table class="table" id="signed_certificates">
|
||||||
@ -110,6 +112,9 @@ curl --cert client_cert.pem https://{{ window.location.hostname }}:8443/api/sign
|
|||||||
<tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr>
|
<tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr>
|
||||||
-->
|
-->
|
||||||
<tr><th>SHA256</th><td style="word-wrap:break-word; overflow-wrap: break-word; ">{{ certificate.sha256sum }}</td></tr>
|
<tr><th>SHA256</th><td style="word-wrap:break-word; overflow-wrap: break-word; ">{{ certificate.sha256sum }}</td></tr>
|
||||||
|
{% if certificate.extensions.extended_key_usage %}
|
||||||
|
<tr><th>Extended key usage</th><td>{{ certificate.extensions.extended_key_usage | join(", ") }}</td></tr>
|
||||||
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,12 +12,28 @@
|
|||||||
# No tags
|
# No tags
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# Submit some stats to CA
|
ARGS="kernel=$(uname -sr)&\
|
||||||
curl http://{{ authority_name }}/api/signed/{{ common_name }}/attr -X POST -d "\
|
|
||||||
dmi.product_name=$(cat /sys/class/dmi/id/product_name)&\
|
|
||||||
dmi.product_serial=$(cat /sys/class/dmi/id/product_serial)&\
|
|
||||||
kernel=$(uname -sr)&\
|
|
||||||
dist=$(lsb_release -si) $(lsb_release -sr)&\
|
|
||||||
cpu=$(cat /proc/cpuinfo | grep '^model name' | head -n1 | cut -d ":" -f2 | xargs)&\
|
cpu=$(cat /proc/cpuinfo | grep '^model name' | head -n1 | cut -d ":" -f2 | xargs)&\
|
||||||
mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB&\
|
$(for j in /sys/class/net/[we]*[a-z][0-9]; do echo -en if.$(basename $j).ether=$(cat $j/address)\&; done)"
|
||||||
$(for j in /sys/class/net/[we]*; do echo -en if.$(basename $j).ether=$(cat $j/address)\&; done)"
|
|
||||||
|
if [ -e /etc/openwrt_release ]; then
|
||||||
|
. /etc/openwrt_release
|
||||||
|
ARGS="$ARGS&dist=$DISTRIB_ID $DISTRIB_RELEASE"
|
||||||
|
else
|
||||||
|
ARGS="$ARGS&dist=$(lsb_release -si) $(lsb_release -sr)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -e /sys/class/dmi ]; then
|
||||||
|
ARGS="$ARGS&dmi.product_name=$(cat /sys/class/dmi/id/product_name)&dmi.product_serial=$(cat /sys/class/dmi/id/product_serial)"
|
||||||
|
ARGS="$ARGS&&mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB"
|
||||||
|
else
|
||||||
|
ARGS="$ARGS&dmi.product_name=$(cat /proc/cpuinfo | grep '^machine' | head -n 1 | cut -d ":" -f 2 | xargs)"
|
||||||
|
ARGS="$ARGS&mem=$(echo $(cat /proc/meminfo | grep MemTotal | cut -d ":" -f 2 | xargs | cut -d " " -f 1)/1000+1 | bc) MB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Submit some stats to CA
|
||||||
|
curl https://{{ authority_name }}:8443/api/signed/{{ common_name }}/attr \
|
||||||
|
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||||
|
--key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||||
|
--cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||||
|
-X POST \-d "$ARGS"
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
# Path to filesystem overlay used
|
||||||
|
overlay = {{ doc_path }}/overlay
|
||||||
|
|
||||||
|
# Hostname or regex to match the IPSec gateway included in the image
|
||||||
|
router = ^router\d?\.
|
||||||
|
|
||||||
|
# Site specific script to be copied to /etc/uci-defaults/99-site-script
|
||||||
|
# use it to include SSH keys, set passwords, etc
|
||||||
|
script =
|
||||||
|
|
||||||
[tpl-archer-c7]
|
[tpl-archer-c7]
|
||||||
# Title shown in the UI
|
# Title shown in the UI
|
||||||
title = TP-Link Archer C7 (Access Point)
|
title = TP-Link Archer C7 (Access Point)
|
||||||
|
|
||||||
# Script to build the image, copy file to /etc/certidude/ and make modifications as necessary
|
# Script to build the image, copy file to /etc/certidude/ and make modifications as necessary
|
||||||
command = {{ doc_path }}/build-ap.sh
|
command = {{ doc_path }}/builder/ap.sh
|
||||||
|
|
||||||
# Path to filesystem overlay, used
|
|
||||||
overlay = {{ doc_path }}/overlay
|
|
||||||
|
|
||||||
# Site specific script to be copied to /etc/uci-defaults/99-site-script
|
|
||||||
script =
|
|
||||||
|
|
||||||
# Device/model/profile selection
|
# Device/model/profile selection
|
||||||
model = archer-c7-v2
|
model = archer-c7-v2
|
||||||
@ -22,10 +27,21 @@ rename = ArcherC7v2_tp_recovery.bin
|
|||||||
|
|
||||||
[cf-e380ac]
|
[cf-e380ac]
|
||||||
title = Comfast E380AC (Access Point)
|
title = Comfast E380AC (Access Point)
|
||||||
command = {{ doc_path }}/build-ap.sh
|
command = {{ doc_path }}/builder/ap.sh
|
||||||
overlay = {{ doc_path }}/overlay
|
|
||||||
script =
|
|
||||||
model = cf-e380ac-v2
|
model = cf-e380ac-v2
|
||||||
filename = cf-e380ac-v2-squashfs-factory.bin
|
filename = cf-e380ac-v2-squashfs-factory.bin
|
||||||
rename = firmware_auto.bin
|
rename = firmware_auto.bin
|
||||||
|
|
||||||
|
[ar150-mfp]
|
||||||
|
title = GL.iNet GL-AR150 (MFP)
|
||||||
|
command = {{ doc_path }}/builder/mfp.sh
|
||||||
|
model = gl-ar150
|
||||||
|
filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin
|
||||||
|
rename = mfp-gl-ar150-squashfs-sysupgrade.bin
|
||||||
|
|
||||||
|
[ar150-cam]
|
||||||
|
title = GL.iNet GL-AR150 (IP Camera)
|
||||||
|
command = {{ doc_path }}/builder/ipcam.sh
|
||||||
|
model = gl-ar150
|
||||||
|
filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin
|
||||||
|
rename = cam-gl-ar150-squashfs-sysupgrade.bin
|
||||||
|
@ -138,8 +138,9 @@ server {
|
|||||||
server_name {{ common_name }};
|
server_name {{ common_name }};
|
||||||
listen 8443 ssl http2;
|
listen 8443 ssl http2;
|
||||||
|
|
||||||
# Require client authentication with certificate
|
# Allow client authentication with certificate,
|
||||||
ssl_verify_client on;
|
# backend must still check if certificate was used for TLS handshake
|
||||||
|
ssl_verify_client optional;
|
||||||
ssl_client_certificate /var/lib/certidude/{{ common_name }}/ca_cert.pem;
|
ssl_client_certificate /var/lib/certidude/{{ common_name }}/ca_cert.pem;
|
||||||
|
|
||||||
# Proxy pass to backend
|
# Proxy pass to backend
|
||||||
|
@ -22,31 +22,37 @@ extended key usage = client_auth
|
|||||||
|
|
||||||
[srv]
|
[srv]
|
||||||
title = Server
|
title = Server
|
||||||
;ou = Server
|
ou = Server
|
||||||
common name = RE_FQDN
|
common name = RE_FQDN
|
||||||
lifetime = 365
|
lifetime = 120
|
||||||
extended key usage = server_auth
|
extended key usage = server_auth client_auth
|
||||||
|
|
||||||
[gw]
|
[gw]
|
||||||
title = Gateway
|
title = Gateway
|
||||||
ou = Gateway
|
ou = Gateway
|
||||||
common name = RE_FQDN
|
common name = RE_FQDN
|
||||||
renewable = true
|
renewable = true
|
||||||
lifetime = 30
|
lifetime = 120
|
||||||
extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth
|
extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth
|
||||||
|
|
||||||
[ap]
|
[ap]
|
||||||
title = Access Point
|
title = Access Point
|
||||||
ou = Access Point
|
ou = Access Point
|
||||||
common name = RE_HOSTNAME
|
common name = RE_HOSTNAME
|
||||||
lifetime = 1825
|
lifetime = 120
|
||||||
extended key usage = client_auth
|
extended key usage = client_auth
|
||||||
|
|
||||||
[mfp]
|
[mfp]
|
||||||
title = Printers
|
title = Printers
|
||||||
ou = Printers
|
ou = MFP
|
||||||
common name = ^mfp\-
|
common name = ^mfp\-
|
||||||
lifetime = 30
|
lifetime = 120
|
||||||
extended key usage = client_auth
|
extended key usage = client_auth
|
||||||
|
|
||||||
|
[cam]
|
||||||
|
title = Camera
|
||||||
|
ou = IP Camera
|
||||||
|
common name = ^cam\-
|
||||||
|
lifetime = 120
|
||||||
|
extended key usage = client_auth
|
||||||
|
|
||||||
|
@ -4,13 +4,31 @@
|
|||||||
# sshd PAM service. In case of 'kerberos' SPNEGO is used to authenticate
|
# sshd PAM service. In case of 'kerberos' SPNEGO is used to authenticate
|
||||||
# user against eg. Active Directory or Samba4.
|
# user against eg. Active Directory or Samba4.
|
||||||
|
|
||||||
|
{% if realm %}
|
||||||
|
;backends = pam
|
||||||
|
backends = kerberos
|
||||||
|
{% else %}
|
||||||
backends = pam
|
backends = pam
|
||||||
;backends = kerberos
|
;backends = kerberos
|
||||||
|
{% endif %}
|
||||||
;backends = ldap
|
;backends = ldap
|
||||||
;backends = kerberos ldap
|
|
||||||
;backends = kerberos pam
|
|
||||||
ldap uri = ldaps://dc.example.lan
|
|
||||||
kerberos keytab = FILE:{{ kerberos_keytab }}
|
kerberos keytab = FILE:{{ kerberos_keytab }}
|
||||||
|
{% if realm %}
|
||||||
|
# Kerberos realm derived from /etc/samba/smb.conf
|
||||||
|
kerberos realm = {{ realm }}
|
||||||
|
{% else %}
|
||||||
|
# Kerberos realm
|
||||||
|
kerberos realm = EXAMPLE.LAN
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if domain %}
|
||||||
|
# LDAP URI derived from /etc/samba/smb.conf
|
||||||
|
ldap uri = ldap://dc1.{{ domain }}
|
||||||
|
{% else %}
|
||||||
|
# LDAP URI
|
||||||
|
ldap uri = ldaps://dc1.example.lan
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
[accounts]
|
[accounts]
|
||||||
# The accounts backend specifies how the user's given name, surname and e-mail
|
# The accounts backend specifies how the user's given name, surname and e-mail
|
||||||
@ -20,27 +38,63 @@ kerberos keytab = FILE:{{ kerberos_keytab }}
|
|||||||
# If certidude setup authority was performed correctly the credential cache should be
|
# If certidude setup authority was performed correctly the credential cache should be
|
||||||
# updated automatically by /etc/cron.hourly/certidude
|
# updated automatically by /etc/cron.hourly/certidude
|
||||||
|
|
||||||
|
{% if not realm %}
|
||||||
backend = posix
|
backend = posix
|
||||||
|
{% else %}
|
||||||
|
;backend = posix
|
||||||
|
{% endif %}
|
||||||
mail suffix = example.lan
|
mail suffix = example.lan
|
||||||
|
|
||||||
|
{% if realm %}
|
||||||
|
backend = ldap
|
||||||
|
{% else %}
|
||||||
;backend = ldap
|
;backend = ldap
|
||||||
|
{% endif %}
|
||||||
ldap gssapi credential cache = /run/certidude/krb5cc
|
ldap gssapi credential cache = /run/certidude/krb5cc
|
||||||
ldap uri = ldap://dc.example.lan
|
|
||||||
ldap base = {% if base %}{{ base }}{% else %}dc=example,dc=lan{% endif %}
|
{% if domain %}
|
||||||
|
# LDAP URI derived from /etc/samba/smb.conf
|
||||||
|
ldap uri = ldap://dc1.{{ domain }}
|
||||||
|
{% else %}
|
||||||
|
# LDAP URI
|
||||||
|
ldap uri = ldaps://dc1.example.lan
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if base %}
|
||||||
|
# LDAP base derived from /etc/samba/smb.conf
|
||||||
|
ldap base = {{ base }}
|
||||||
|
{% else %}
|
||||||
|
ldap base = dc=example,dc=lan
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
[authorization]
|
[authorization]
|
||||||
# The authorization backend specifies how the users are authorized.
|
# The authorization backend specifies how the users are authorized.
|
||||||
# In case of 'posix' simply group membership is asserted,
|
# In case of 'posix' simply group membership is asserted,
|
||||||
# in case of 'ldap' search filter with username as placeholder is applied.
|
# in case of 'ldap' search filter with username as placeholder is applied.
|
||||||
|
|
||||||
|
{% if realm %}
|
||||||
|
;backend = posix
|
||||||
|
{% else %}
|
||||||
backend = posix
|
backend = posix
|
||||||
|
{% endif %}
|
||||||
posix user group = users
|
posix user group = users
|
||||||
posix admin group = sudo
|
posix admin group = sudo
|
||||||
|
|
||||||
|
{% if realm %}
|
||||||
|
backend = ldap
|
||||||
|
{% else %}
|
||||||
;backend = ldap
|
;backend = ldap
|
||||||
|
{% endif %}
|
||||||
ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s))
|
ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s))
|
||||||
ldap user filter = (&(objectclass=user)(objectcategory=person)(samaccountname=%s))
|
ldap user filter = (&(objectclass=user)(objectcategory=person)(samaccountname=%s))
|
||||||
ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,{% if base %}{{ base }}{% else %}dc=example,dc=lan{% endif %})(samaccountname=%s))
|
{% if base %}
|
||||||
|
# LDAP user filter for administrative accounts, derived from /etc/samba/smb.conf
|
||||||
|
ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,{{ base }})(samaccountname=%s))
|
||||||
|
{% else %}
|
||||||
|
# LDAP user filter for administrative accounts
|
||||||
|
ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,dc=example,dc=lan)(samaccountname=%s))
|
||||||
|
{% endif %}
|
||||||
|
;ldap admin filter = (&(samaccountname=lauri)(samaccountname=%s))
|
||||||
|
|
||||||
;backend = whitelist
|
;backend = whitelist
|
||||||
user whitelist =
|
user whitelist =
|
||||||
@ -62,9 +116,9 @@ autosign subnets = 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
|
|||||||
scep subnets =
|
scep subnets =
|
||||||
;scep subnets = 0.0.0.0/0
|
;scep subnets = 0.0.0.0/0
|
||||||
|
|
||||||
# Online Certificate Status Protocol enabled subnets
|
# Online Certificate Status Protocol enabled subnets, anywhere by default
|
||||||
ocsp subnets =
|
;ocsp subnets =
|
||||||
;ocsp subnets = 0.0.0.0/0
|
ocsp subnets = 0.0.0.0/0
|
||||||
|
|
||||||
# Certificate Revocation lists can be accessed from anywhere by default
|
# Certificate Revocation lists can be accessed from anywhere by default
|
||||||
;crl subnets =
|
;crl subnets =
|
||||||
@ -76,12 +130,24 @@ crl subnets = 0.0.0.0/0
|
|||||||
renewal subnets =
|
renewal subnets =
|
||||||
;renewal subnets = 0.0.0.0/0
|
;renewal subnets = 0.0.0.0/0
|
||||||
|
|
||||||
|
# From which subnets autosign and SCEP requests are allowed to overwrite
|
||||||
|
# already existing certificate with same CN
|
||||||
|
overwrite subnets =
|
||||||
|
;overwrite subnets = 0.0.0.0/0
|
||||||
|
|
||||||
|
# Source subnets of Kerberos authenticated machines which are automatically
|
||||||
|
# allowed to enroll with CSR whose common name is set to machine's account name.
|
||||||
|
# Note that overwriting is not allowed by default, see 'overwrite subnets'
|
||||||
|
# option above
|
||||||
|
machine enrollment subnets =
|
||||||
|
;machine enrollment subnets = 0.0.0.0/0
|
||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
# Disable logging
|
# Disable logging
|
||||||
;backend =
|
backend =
|
||||||
|
|
||||||
# Use SQLite backend
|
# Use SQLite backend
|
||||||
backend = sql
|
;backend = sql
|
||||||
database = sqlite://{{ directory }}/meta/db.sqlite
|
database = sqlite://{{ directory }}/meta/db.sqlite
|
||||||
|
|
||||||
[signature]
|
[signature]
|
||||||
@ -144,13 +210,6 @@ request submission allowed = false
|
|||||||
;user enrollment = single allowed
|
;user enrollment = single allowed
|
||||||
user enrollment = multiple allowed
|
user enrollment = multiple allowed
|
||||||
|
|
||||||
# Machine certificate enrollment specifies whether Kerberos authenticated
|
|
||||||
# machines are allowed to automatically enroll with certificate where
|
|
||||||
# common name is set to machine's account name
|
|
||||||
machine enrollment = forbidden
|
|
||||||
;machine enrollment = allowed
|
|
||||||
|
|
||||||
|
|
||||||
private key path = {{ ca_key }}
|
private key path = {{ ca_key }}
|
||||||
certificate path = {{ ca_cert }}
|
certificate path = {{ ca_cert }}
|
||||||
|
|
||||||
@ -199,3 +258,7 @@ secret = {{ token_secret }}
|
|||||||
path = {{ script_dir }}
|
path = {{ script_dir }}
|
||||||
;path = /etc/certidude/script
|
;path = /etc/certidude/script
|
||||||
;path =
|
;path =
|
||||||
|
|
||||||
|
[service]
|
||||||
|
protocols = ikev2 https openvpn
|
||||||
|
routers = ^router\d?\.
|
||||||
|
@ -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
|
EOF
|
||||||
|
|
||||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci luci-app-commands \
|
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci luci-app-commands \
|
||||||
openssl-util curl ca-certificates \
|
openssl-util curl ca-certificates dropbear \
|
||||||
strongswan-mod-kernel-libipsec kmod-tun ip-full strongswan-full \
|
strongswan-mod-kernel-libipsec kmod-tun strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \
|
||||||
htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \
|
htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \
|
||||||
-luci-app-firewall \
|
-luci-app-firewall \
|
||||||
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
|
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
|
||||||
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6"
|
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6 bc"
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,6 +9,12 @@ BASENAME=lede-imagebuilder-$VERSION-ar71xx-generic.Linux-x86_64
|
|||||||
FILENAME=$BASENAME.tar.xz
|
FILENAME=$BASENAME.tar.xz
|
||||||
URL=http://downloads.lede-project.org/releases/$VERSION/targets/ar71xx/generic/$FILENAME
|
URL=http://downloads.lede-project.org/releases/$VERSION/targets/ar71xx/generic/$FILENAME
|
||||||
|
|
||||||
|
# curl of vanilla LEDE doesn't support ECDSA at the moment
|
||||||
|
BASENAME=lede-imagebuilder-ar71xx-generic.Linux-x86_64
|
||||||
|
FILENAME=$BASENAME.tar.xz
|
||||||
|
URL=https://www.koodur.com/$FILENAME
|
||||||
|
|
||||||
|
|
||||||
if [ ! -e $BUILD/$FILENAME ]; then
|
if [ ! -e $BUILD/$FILENAME ]; then
|
||||||
wget -q $URL -O $BUILD/$FILENAME
|
wget -q $URL -O $BUILD/$FILENAME
|
||||||
fi
|
fi
|
||||||
@ -19,58 +25,94 @@ fi
|
|||||||
|
|
||||||
# Copy CA certificate
|
# Copy CA certificate
|
||||||
AUTHORITY=$(hostname -f)
|
AUTHORITY=$(hostname -f)
|
||||||
CERTIDUDE_DIR=/var/lib/certidude/$AUTHORITY
|
|
||||||
|
|
||||||
mkdir -p $OVERLAY/etc/config
|
mkdir -p $OVERLAY/etc/config
|
||||||
mkdir -p $OVERLAY/etc/uci-defaults
|
mkdir -p $OVERLAY/etc/uci-defaults
|
||||||
mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY
|
mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY/
|
||||||
cp /var/lib/certidude/$AUTHORITY/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/
|
cp /var/lib/certidude/$AUTHORITY/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/
|
||||||
|
|
||||||
|
echo /etc/certidude >> $OVERLAY/etc/sysupgrade.conf
|
||||||
|
|
||||||
cat <<EOF > $OVERLAY/etc/config/certidude
|
cat <<EOF > $OVERLAY/etc/config/certidude
|
||||||
|
|
||||||
config authority
|
config authority
|
||||||
option gateway router.k-space.ee
|
option gateway "$ROUTER"
|
||||||
option url http://$AUTHORITY
|
option hostname "$AUTHORITY"
|
||||||
option trigger wan
|
option trigger wan
|
||||||
option authority_path /etc/certidude/authority/$AUTHORITY/ca_cert.pem
|
option key_type $AUTHORITY_CERTIFICATE_ALGORITHM
|
||||||
option request_path /etc/certidude/authority/$AUTHORITY/client_req.pem
|
|
||||||
option certificate_path /etc/certidude/authority/$AUTHORITY/client_cert.pem
|
|
||||||
option key_path /etc/certidude/authority/$AUTHORITY/client_key.pem
|
|
||||||
option key_type rsa
|
|
||||||
option key_length 2048
|
option key_length 2048
|
||||||
|
option key_curve secp384r1
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|
||||||
cat << EOF > $OVERLAY/etc/uci-defaults/40-disable-ipsec
|
cat << EOF > $OVERLAY/etc/uci-defaults/40-disable-ipsec
|
||||||
/etc/init.d/ipsec disable
|
/etc/init.d/ipsec disable
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
case $AUTHORITY_CERTIFICATE_ALGORITHM in
|
||||||
|
rsa)
|
||||||
|
echo ": RSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets
|
||||||
|
DHGROUP=modp2048
|
||||||
|
;;
|
||||||
|
ec)
|
||||||
|
echo ": ECDSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets
|
||||||
|
DHGROUP=ecp384
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown algorithm $AUTHORITY_CERTIFICATE_ALGORITHM"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cat << EOF > $OVERLAY/etc/certidude/authority/$AUTHORITY/updown
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
cat << EOF > $OVERLAY/etc/ipsec.secrets
|
CURL="curl -m 3 -f --key /etc/certidude/authority/$AUTHORITY/host_key.pem --cert /etc/certidude/authority/$AUTHORITY/host_cert.pem --cacert /etc/certidude/authority/$AUTHORITY/ca_cert.pem"
|
||||||
: RSA /etc/certidude/authority/$AUTHORITY/client_key.pem
|
URL="https://$AUTHORITY:8443/api/signed/\$(uci get system.@system[0].hostname)/script/"
|
||||||
|
|
||||||
|
case \$PLUTO_VERB in
|
||||||
|
up-client)
|
||||||
|
logger -t certidude -s "Downloading and executing \$URL"
|
||||||
|
\$CURL \$URL -o /tmp/script.sh && sh /tmp/script.sh
|
||||||
|
;;
|
||||||
|
*) ;;
|
||||||
|
esac
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
chmod +x $OVERLAY/etc/certidude/authority/$AUTHORITY/updown
|
||||||
|
|
||||||
cat << EOF > $OVERLAY/etc/ipsec.conf
|
cat << EOF > $OVERLAY/etc/ipsec.conf
|
||||||
|
|
||||||
config setup
|
config setup
|
||||||
|
strictcrlpolicy=yes
|
||||||
|
|
||||||
ca $AUTHORITY
|
ca $AUTHORITY
|
||||||
cacert=/etc/certidude/authority/$AUTHORITY/ca_cert.pem
|
auto=add
|
||||||
auto=add
|
cacert=/etc/certidude/authority/$AUTHORITY/ca_cert.pem
|
||||||
|
ocspuri = http://$AUTHORITY/api/ocsp/
|
||||||
|
|
||||||
conn router.k-space.ee
|
conn %default
|
||||||
right=router.k-space.ee
|
keyingtries=%forever
|
||||||
dpdaction=restart
|
dpdaction=restart
|
||||||
auto=start
|
closeaction=restart
|
||||||
rightsubnet=0.0.0.0/0
|
ike=aes256-sha384-ecp384!
|
||||||
rightid=%any
|
esp=aes128gcm16-aes128gmac!
|
||||||
leftsourceip=%config
|
left=%defaultroute
|
||||||
keyexchange=ikev2
|
leftcert=/etc/certidude/authority/$AUTHORITY/host_cert.pem
|
||||||
closeaction=restart
|
leftca="$AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME"
|
||||||
leftcert=/etc/certidude/authority/$AUTHORITY/client_cert.pem
|
rightca="$AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME"
|
||||||
left=%defaultroute
|
|
||||||
|
conn client-to-site
|
||||||
|
auto=start
|
||||||
|
right="$ROUTER"
|
||||||
|
rightsubnet=0.0.0.0/0
|
||||||
|
leftsourceip=%config
|
||||||
|
leftupdown=/etc/certidude/authority/$AUTHORITY/updown
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
cat << EOF > $OVERLAY/etc/uci-defaults/99-uhttpd-disable-https
|
||||||
|
uci delete uhttpd.main.listen_https
|
||||||
|
uci delete uhttpd.main.redirect_https
|
||||||
|
EOF
|
||||||
|
@ -38,6 +38,7 @@ uci certidude.@authority[0].green_led='gl-connect:green:lan'
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|
||||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates strongswan-full htop \
|
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates \
|
||||||
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci luci-app-mjpg-streamer kmod-video-uvc \
|
strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm htop \
|
||||||
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun ip-full"
|
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \
|
||||||
|
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun bc"
|
||||||
|
@ -96,15 +96,15 @@ uci set firewall.@redirect[-1].target=DNAT
|
|||||||
uci set firewall.@redirect[-1].proto=tcp
|
uci set firewall.@redirect[-1].proto=tcp
|
||||||
uci set firewall.@redirect[-1].enabled=0
|
uci set firewall.@redirect[-1].enabled=0
|
||||||
|
|
||||||
uci set uhttpd.main.listen_http=0.0.0.0:8080
|
|
||||||
|
|
||||||
/etc/init.d/dropbear disable
|
/etc/init.d/dropbear disable
|
||||||
|
|
||||||
|
uci set uhttpd.main.listen_http=0.0.0.0:8080
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \
|
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \
|
||||||
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci \
|
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci dropbear kmod-tun \
|
||||||
strongswan-mod-kernel-libipsec kmod-tun ip-full strongswan-full \
|
strongswan-default strongswan-mod-kernel-libipsec strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \
|
||||||
pciutils -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm"
|
pciutils -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm bc"
|
||||||
|
|
||||||
|
|
||||||
|
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 = [
|
files = [
|
||||||
"/etc/certidude/client.conf",
|
"/etc/certidude/client.conf",
|
||||||
"/etc/certidude/services.conf",
|
"/etc/certidude/services.conf",
|
||||||
"/var/lib/certidude/ca.example.lan/client_key.pem",
|
"/etc/certidude/authority/ca.example.lan/ca_cert.pem",
|
||||||
"/var/lib/certidude/ca.example.lan/server_key.pem",
|
"/etc/certidude/authority/ca.example.lan/client_key.pem",
|
||||||
"/var/lib/certidude/ca.example.lan/client_req.pem",
|
"/etc/certidude/authority/ca.example.lan/server_key.pem",
|
||||||
"/var/lib/certidude/ca.example.lan/server_req.pem",
|
"/etc/certidude/authority/ca.example.lan/client_req.pem",
|
||||||
"/var/lib/certidude/ca.example.lan/client_cert.pem",
|
"/etc/certidude/authority/ca.example.lan/server_req.pem",
|
||||||
"/var/lib/certidude/ca.example.lan/server_cert.pem",
|
"/etc/certidude/authority/ca.example.lan/client_cert.pem",
|
||||||
|
"/etc/certidude/authority/ca.example.lan/server_cert.pem",
|
||||||
]
|
]
|
||||||
for path in files:
|
for path in files:
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
@ -85,6 +86,8 @@ def clean_client():
|
|||||||
|
|
||||||
|
|
||||||
def clean_server():
|
def clean_server():
|
||||||
|
os.umask(0o22)
|
||||||
|
|
||||||
if os.path.exists("/run/certidude/server.pid"):
|
if os.path.exists("/run/certidude/server.pid"):
|
||||||
with open("/run/certidude/server.pid") as fh:
|
with open("/run/certidude/server.pid") as fh:
|
||||||
try:
|
try:
|
||||||
@ -95,30 +98,29 @@ def clean_server():
|
|||||||
|
|
||||||
if os.path.exists("/var/lib/certidude/ca.example.lan"):
|
if os.path.exists("/var/lib/certidude/ca.example.lan"):
|
||||||
shutil.rmtree("/var/lib/certidude/ca.example.lan")
|
shutil.rmtree("/var/lib/certidude/ca.example.lan")
|
||||||
if os.path.exists("/etc/certidude/server.conf"):
|
|
||||||
os.unlink("/etc/certidude/server.conf")
|
|
||||||
if os.path.exists("/run/certidude"):
|
if os.path.exists("/run/certidude"):
|
||||||
shutil.rmtree("/run/certidude")
|
shutil.rmtree("/run/certidude")
|
||||||
if os.path.exists("/var/log/certidude.log"):
|
|
||||||
os.unlink("/var/log/certidude.log")
|
|
||||||
if os.path.exists("/etc/cron.hourly/certidude"):
|
|
||||||
os.unlink("/etc/cron.hourly/certidude")
|
|
||||||
|
|
||||||
# systemd
|
files = [
|
||||||
if os.path.exists("/etc/systemd/system/certidude.service"):
|
"/etc/krb5.keytab",
|
||||||
os.unlink("/etc/systemd/system/certidude.service")
|
"/etc/samba/smb.conf",
|
||||||
|
"/etc/certidude/server.conf",
|
||||||
|
"/etc/certidude/builder.conf",
|
||||||
|
"/etc/certidude/profile.conf",
|
||||||
|
"/var/log/certidude.log",
|
||||||
|
"/etc/cron.hourly/certidude",
|
||||||
|
"/etc/systemd/system/certidude.service",
|
||||||
|
"/etc/nginx/sites-available/ca.conf",
|
||||||
|
"/etc/nginx/sites-enabled/ca.conf",
|
||||||
|
"/etc/nginx/sites-available/certidude.conf",
|
||||||
|
"/etc/nginx/sites-enabled/certidude.conf",
|
||||||
|
"/etc/nginx/conf.d/tls.conf",
|
||||||
|
"/etc/certidude/server.keytab",
|
||||||
|
]
|
||||||
|
|
||||||
# Remove nginx stuff
|
for filename in files:
|
||||||
if os.path.exists("/etc/nginx/sites-available/ca.conf"):
|
if os.path.exists(filename):
|
||||||
os.unlink("/etc/nginx/sites-available/ca.conf")
|
os.unlink(filename)
|
||||||
if os.path.exists("/etc/nginx/sites-enabled/ca.conf"):
|
|
||||||
os.unlink("/etc/nginx/sites-enabled/ca.conf")
|
|
||||||
if os.path.exists("/etc/nginx/sites-available/certidude.conf"):
|
|
||||||
os.unlink("/etc/nginx/sites-available/certidude.conf")
|
|
||||||
if os.path.exists("/etc/nginx/sites-enabled/certidude.conf"):
|
|
||||||
os.unlink("/etc/nginx/sites-enabled/certidude.conf")
|
|
||||||
if os.path.exists("/etc/nginx/conf.d/tls.conf"):
|
|
||||||
os.unlink("/etc/nginx/conf.d/tls.conf")
|
|
||||||
|
|
||||||
# Remove OpenVPN stuff
|
# Remove OpenVPN stuff
|
||||||
if os.path.exists("/etc/openvpn"):
|
if os.path.exists("/etc/openvpn"):
|
||||||
@ -135,8 +137,6 @@ def clean_server():
|
|||||||
os.kill(int(fh.read()), 15)
|
os.kill(int(fh.read()), 15)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
if os.path.exists("/etc/certidude/server.keytab"):
|
|
||||||
os.unlink("/etc/certidude/server.keytab")
|
|
||||||
os.system("rm -Rfv /var/lib/samba/*")
|
os.system("rm -Rfv /var/lib/samba/*")
|
||||||
|
|
||||||
# Restore initial resolv.conf
|
# Restore initial resolv.conf
|
||||||
@ -146,7 +146,7 @@ def test_cli_setup_authority():
|
|||||||
assert os.getuid() == 0, "Run tests as root in a clean VM or container"
|
assert os.getuid() == 0, "Run tests as root in a clean VM or container"
|
||||||
assert check_output(["/bin/hostname", "-f"]) == b"ca.example.lan\n", "As a safety precaution, unittests only run in a machine whose hostanme -f is ca.example.lan"
|
assert check_output(["/bin/hostname", "-f"]) == b"ca.example.lan\n", "As a safety precaution, unittests only run in a machine whose hostanme -f is ca.example.lan"
|
||||||
|
|
||||||
os.system("apt-get install -q -y git build-essential python-dev libkrb5-dev")
|
os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y git build-essential python-dev libkrb5-dev")
|
||||||
|
|
||||||
assert not os.environ.get("KRB5CCNAME"), "Environment contaminated"
|
assert not os.environ.get("KRB5CCNAME"), "Environment contaminated"
|
||||||
assert not os.environ.get("KRB5_KTNAME"), "Environment contaminated"
|
assert not os.environ.get("KRB5_KTNAME"), "Environment contaminated"
|
||||||
@ -189,39 +189,6 @@ def test_cli_setup_authority():
|
|||||||
if "userbot" not in buf:
|
if "userbot" not in buf:
|
||||||
os.system("useradd userbot -G users -p '$1$PBkf5waA$n9EV6WJ7PS6lyGWkgeTPf1' -c 'User Bot,,,'")
|
os.system("useradd userbot -G users -p '$1$PBkf5waA$n9EV6WJ7PS6lyGWkgeTPf1' -c 'User Bot,,,'")
|
||||||
|
|
||||||
# Bootstrap domain controller here,
|
|
||||||
# Samba startup takes some time
|
|
||||||
os.system("apt-get install -y samba krb5-user winbind bc")
|
|
||||||
if os.path.exists("/etc/samba/smb.conf"):
|
|
||||||
os.unlink("/etc/samba/smb.conf")
|
|
||||||
os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca")
|
|
||||||
os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'")
|
|
||||||
os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'")
|
|
||||||
os.system("samba-tool group addmembers 'Domain Admins' adminbot")
|
|
||||||
os.system("samba-tool user setpassword administrator --newpassword=S4l4k4l4")
|
|
||||||
if os.path.exists("/etc/krb5.keytab"):
|
|
||||||
os.unlink("/etc/krb5.keytab")
|
|
||||||
os.symlink("/var/lib/samba/private/secrets.keytab", "/etc/krb5.keytab")
|
|
||||||
os.chmod("/var/lib/samba/private/secrets.keytab", 0o644) # To allow access to certidude server
|
|
||||||
if os.path.exists("/etc/krb5.conf"): # Remove the one from krb5-user package
|
|
||||||
os.unlink("/etc/krb5.conf")
|
|
||||||
os.symlink("/var/lib/samba/private/krb5.conf", "/etc/krb5.conf")
|
|
||||||
with open("/etc/resolv.conf", "w") as fh:
|
|
||||||
fh.write("nameserver 127.0.0.1\nsearch example.lan\n")
|
|
||||||
# TODO: dig -t srv perhaps?
|
|
||||||
os.system("samba")
|
|
||||||
|
|
||||||
# Samba bind 636 late (probably generating keypair)
|
|
||||||
# so LDAPS connections below will fail
|
|
||||||
timeout = 0
|
|
||||||
while timeout < 30:
|
|
||||||
if os.path.exists("/var/lib/samba/private/tls/cert.pem"):
|
|
||||||
break
|
|
||||||
sleep(1)
|
|
||||||
timeout += 1
|
|
||||||
else:
|
|
||||||
assert False, "Samba startup timed out"
|
|
||||||
|
|
||||||
reload(const)
|
reload(const)
|
||||||
from certidude.cli import entry_point as cli
|
from certidude.cli import entry_point as cli
|
||||||
|
|
||||||
@ -447,15 +414,6 @@ def test_cli_setup_authority():
|
|||||||
assert "Stored request " in inbox.pop(), inbox
|
assert "Stored request " in inbox.pop(), inbox
|
||||||
assert not inbox
|
assert not inbox
|
||||||
|
|
||||||
buf = generate_csr(cn="test2.example.lan")
|
|
||||||
r = client().simulate_post("/api/request/",
|
|
||||||
query_string="autosign=1",
|
|
||||||
body=buf,
|
|
||||||
headers={"content-type":"application/pkcs10"})
|
|
||||||
assert r.status_code == 202 # server CN, request stored
|
|
||||||
assert "Stored request " in inbox.pop(), inbox
|
|
||||||
assert not inbox
|
|
||||||
|
|
||||||
# Test signed certificate API call
|
# Test signed certificate API call
|
||||||
r = client().simulate_get("/api/signed/nonexistant/")
|
r = client().simulate_get("/api/signed/nonexistant/")
|
||||||
assert r.status_code == 404, r.text
|
assert r.status_code == 404, r.text
|
||||||
@ -574,7 +532,7 @@ def test_cli_setup_authority():
|
|||||||
# Test tagging integration in scripting framework
|
# Test tagging integration in scripting framework
|
||||||
r = client().simulate_get("/api/signed/test/script/")
|
r = client().simulate_get("/api/signed/test/script/")
|
||||||
assert r.status_code == 200, r.text # script render ok
|
assert r.status_code == 200, r.text # script render ok
|
||||||
assert "curl http://ca.example.lan/api/signed/test/attr " in r.text, r.text
|
assert "curl https://ca.example.lan:8443/api/signed/test/attr " in r.text, r.text
|
||||||
assert "Tartu" in r.text, r.text
|
assert "Tartu" in r.text, r.text
|
||||||
|
|
||||||
r = client().simulate_post("/api/signed/test/tag/",
|
r = client().simulate_post("/api/signed/test/tag/",
|
||||||
@ -640,7 +598,7 @@ def test_cli_setup_authority():
|
|||||||
headers={"Authorization":admintoken})
|
headers={"Authorization":admintoken})
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
assert "Revoked " in inbox.pop(), inbox
|
assert "Revoked " in inbox.pop(), inbox
|
||||||
|
"""
|
||||||
|
|
||||||
# Log can be read only by admin
|
# Log can be read only by admin
|
||||||
r = client().simulate_get("/api/log/")
|
r = client().simulate_get("/api/log/")
|
||||||
@ -652,7 +610,7 @@ def test_cli_setup_authority():
|
|||||||
headers={"Authorization":admintoken})
|
headers={"Authorization":admintoken})
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
assert r.headers.get('content-type') == "application/json; charset=UTF-8"
|
assert r.headers.get('content-type') == "application/json; charset=UTF-8"
|
||||||
|
"""
|
||||||
|
|
||||||
# Test session API call
|
# Test session API call
|
||||||
r = client().simulate_get("/api/")
|
r = client().simulate_get("/api/")
|
||||||
@ -708,11 +666,14 @@ def test_cli_setup_authority():
|
|||||||
|
|
||||||
with open("/etc/certidude/client.conf", "a") as fh:
|
with open("/etc/certidude/client.conf", "a") as fh:
|
||||||
fh.write("insecure = true\n")
|
fh.write("insecure = true\n")
|
||||||
|
fh.write("autosign = false\n")
|
||||||
|
|
||||||
|
assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
|
||||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||||
assert "(Autosign failed, only client certificates allowed to be signed automatically)" in result.output, result.output
|
assert "(autosign not requested)" in result.output, result.output
|
||||||
|
assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
|
||||||
|
|
||||||
child_pid = os.fork()
|
child_pid = os.fork()
|
||||||
if not child_pid:
|
if not child_pid:
|
||||||
@ -727,12 +688,12 @@ def test_cli_setup_authority():
|
|||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||||
assert "Writing certificate to:" in result.output, result.output
|
assert "Writing certificate to:" in result.output, result.output
|
||||||
|
assert os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
|
||||||
|
|
||||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--renew", "--no-wait"])
|
result = runner.invoke(cli, ["enroll", "--skip-self", "--renew", "--no-wait"])
|
||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||||
#assert "Writing certificate to:" in result.output, result.output
|
assert "Renewing using current keypair" in result.output, result.output
|
||||||
assert "Attached renewal signature" in result.output, result.output
|
|
||||||
|
|
||||||
# Test nginx setup
|
# Test nginx setup
|
||||||
assert os.system("nginx -t") == 0, "Generated nginx config was invalid"
|
assert os.system("nginx -t") == 0, "Generated nginx config was invalid"
|
||||||
@ -745,7 +706,7 @@ def test_cli_setup_authority():
|
|||||||
# First OpenVPN server is set up
|
# First OpenVPN server is set up
|
||||||
|
|
||||||
clean_client()
|
clean_client()
|
||||||
assert not os.path.exists("/var/lib/certidude/ca.example.lan/server_cert.pem")
|
assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
|
||||||
|
|
||||||
if not os.path.exists("/etc/openvpn/keys"):
|
if not os.path.exists("/etc/openvpn/keys"):
|
||||||
os.makedirs("/etc/openvpn/keys")
|
os.makedirs("/etc/openvpn/keys")
|
||||||
@ -761,12 +722,13 @@ def test_cli_setup_authority():
|
|||||||
|
|
||||||
with open("/etc/certidude/client.conf", "a") as fh:
|
with open("/etc/certidude/client.conf", "a") as fh:
|
||||||
fh.write("insecure = true\n")
|
fh.write("insecure = true\n")
|
||||||
|
fh.write("autosign = false\n")
|
||||||
|
|
||||||
assert not os.path.exists("/var/lib/certidude/ca.example.lan/server_cert.pem")
|
assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
|
||||||
|
|
||||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert "(Autosign failed, only client certificates allowed to be signed automatically)" in result.output, result.output
|
assert "(autosign not requested)" in result.output, result.output
|
||||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||||
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/vpn.example.lan.pem")
|
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/vpn.example.lan.pem")
|
||||||
|
|
||||||
@ -785,7 +747,7 @@ def test_cli_setup_authority():
|
|||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||||
assert "Writing certificate to:" in result.output, result.output
|
assert "Writing certificate to:" in result.output, result.output
|
||||||
assert os.path.exists("/var/lib/certidude/ca.example.lan/server_cert.pem")
|
assert os.path.exists("/etc/certidude/authority//ca.example.lan/server_cert.pem")
|
||||||
assert os.path.exists("/etc/openvpn/site-to-client.conf")
|
assert os.path.exists("/etc/openvpn/site-to-client.conf")
|
||||||
|
|
||||||
# Secondly OpenVPN client is set up
|
# Secondly OpenVPN client is set up
|
||||||
@ -977,7 +939,7 @@ def test_cli_setup_authority():
|
|||||||
|
|
||||||
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"])
|
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"])
|
||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert open("/etc/ipsec.secrets").read() == ": RSA /var/lib/certidude/ca.example.lan/server_key.pem\n"
|
assert open("/etc/ipsec.secrets").read() == ": RSA /etc/certidude/authority/ca.example.lan/server_key.pem\n"
|
||||||
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem")
|
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem")
|
||||||
|
|
||||||
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"])
|
result = runner.invoke(cli, ['setup', 'strongswan', 'server', "-cn", "ipsec.example.lan", "ca.example.lan"])
|
||||||
@ -986,10 +948,11 @@ def test_cli_setup_authority():
|
|||||||
|
|
||||||
with open("/etc/certidude/client.conf", "a") as fh:
|
with open("/etc/certidude/client.conf", "a") as fh:
|
||||||
fh.write("insecure = true\n")
|
fh.write("insecure = true\n")
|
||||||
|
fh.write("autosign = false\n")
|
||||||
|
|
||||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
||||||
assert not result.exception, result.output
|
assert not result.exception, result.output
|
||||||
assert "(Autosign failed, only client certificates allowed to be signed automatically" in result.output, result.output
|
assert "(autosign not requested)" in result.output, result.output
|
||||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||||
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem")
|
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem")
|
||||||
|
|
||||||
@ -1009,7 +972,7 @@ def test_cli_setup_authority():
|
|||||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||||
|
|
||||||
assert "Writing certificate to:" in result.output, result.output
|
assert "Writing certificate to:" in result.output, result.output
|
||||||
assert os.path.exists("/var/lib/certidude/ca.example.lan/server_cert.pem")
|
assert os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
|
||||||
|
|
||||||
# IPSec client as service
|
# IPSec client as service
|
||||||
|
|
||||||
@ -1073,12 +1036,39 @@ def test_cli_setup_authority():
|
|||||||
|
|
||||||
r = requests.get("http://ca.example.lan/api/scep/")
|
r = requests.get("http://ca.example.lan/api/scep/")
|
||||||
assert r.status_code == 404
|
assert r.status_code == 404
|
||||||
r = requests.get("http://ca.example.lan/api/ocsp/")
|
|
||||||
assert r.status_code == 404
|
|
||||||
r = requests.post("http://ca.example.lan/api/scep/")
|
r = requests.post("http://ca.example.lan/api/scep/")
|
||||||
assert r.status_code == 404
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
#################
|
||||||
|
### Test OCSP ###
|
||||||
|
#################
|
||||||
|
|
||||||
|
r = requests.get("http://ca.example.lan/api/ocsp/")
|
||||||
|
assert r.status_code == 400
|
||||||
r = requests.post("http://ca.example.lan/api/ocsp/")
|
r = requests.post("http://ca.example.lan/api/ocsp/")
|
||||||
assert r.status_code == 404
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -CAfile /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/signed/roadwarrior2.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp1.log") == 0
|
||||||
|
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -CAfile /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/ca_cert.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp2.log") == 0
|
||||||
|
|
||||||
|
for filename in os.listdir("/var/lib/certidude/ca.example.lan/revoked"):
|
||||||
|
if not filename.endswith(".pem"):
|
||||||
|
continue
|
||||||
|
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -CAfile /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/revoked/%s -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp3.log" % filename) == 0
|
||||||
|
break
|
||||||
|
|
||||||
|
with open("/tmp/ocsp1.log") as fh:
|
||||||
|
buf = fh.read()
|
||||||
|
assert ": good" in buf, buf
|
||||||
|
with open("/tmp/ocsp2.log") as fh:
|
||||||
|
buf = fh.read()
|
||||||
|
assert ": unknown" in buf, buf
|
||||||
|
with open("/tmp/ocsp3.log") as fh:
|
||||||
|
buf = fh.read()
|
||||||
|
assert ": revoked" in buf, buf
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
@ -1088,8 +1078,53 @@ def test_cli_setup_authority():
|
|||||||
# Shut down current instance
|
# Shut down current instance
|
||||||
os.kill(server_pid, 15)
|
os.kill(server_pid, 15)
|
||||||
requests.get("http://ca.example.lan/api/")
|
requests.get("http://ca.example.lan/api/")
|
||||||
|
# sleep(2)
|
||||||
|
# os.kill(server_pid, 9) # TODO: Figure out why doesn't shut down gracefully
|
||||||
os.waitpid(server_pid, 0)
|
os.waitpid(server_pid, 0)
|
||||||
|
|
||||||
|
# Install packages
|
||||||
|
os.system("apt-get install -y samba krb5-user winbind bc")
|
||||||
|
clean_server()
|
||||||
|
|
||||||
|
# Bootstrap domain controller here,
|
||||||
|
# Samba startup takes some timec
|
||||||
|
os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca")
|
||||||
|
os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'")
|
||||||
|
os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'")
|
||||||
|
os.system("samba-tool group addmembers 'Domain Admins' adminbot")
|
||||||
|
os.system("samba-tool user setpassword administrator --newpassword=S4l4k4l4")
|
||||||
|
os.symlink("/var/lib/samba/private/secrets.keytab", "/etc/krb5.keytab")
|
||||||
|
os.chmod("/var/lib/samba/private/secrets.keytab", 0o644) # To allow access to certidude server
|
||||||
|
if os.path.exists("/etc/krb5.conf"): # Remove the one from krb5-user package
|
||||||
|
os.unlink("/etc/krb5.conf")
|
||||||
|
os.symlink("/var/lib/samba/private/krb5.conf", "/etc/krb5.conf")
|
||||||
|
with open("/etc/resolv.conf", "w") as fh:
|
||||||
|
fh.write("nameserver 127.0.0.1\nsearch example.lan\n")
|
||||||
|
# TODO: dig -t srv perhaps?
|
||||||
|
os.system("samba")
|
||||||
|
|
||||||
|
# Samba bind 636 late (probably generating keypair)
|
||||||
|
# so LDAPS connections below will fail
|
||||||
|
timeout = 0
|
||||||
|
while timeout < 30:
|
||||||
|
if os.path.exists("/var/lib/samba/private/tls/cert.pem"):
|
||||||
|
break
|
||||||
|
sleep(1)
|
||||||
|
timeout += 1
|
||||||
|
else:
|
||||||
|
assert False, "Samba startup timed out"
|
||||||
|
|
||||||
|
# Bootstrap authority
|
||||||
|
bootstrap_pid = os.fork() # TODO: this shouldn't be necessary
|
||||||
|
if not bootstrap_pid:
|
||||||
|
result = runner.invoke(cli, ["setup", "authority", "--skip-packages", "--elliptic-curve"])
|
||||||
|
assert not result.exception, result.output
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
os.waitpid(bootstrap_pid, 0)
|
||||||
|
|
||||||
|
assert os.getuid() == 0 and os.getgid() == 0, "Environment contaminated"
|
||||||
|
|
||||||
# (re)auth against DC
|
# (re)auth against DC
|
||||||
assert os.system("kdestroy") == 0
|
assert os.system("kdestroy") == 0
|
||||||
assert not os.path.exists("/tmp/krb5cc_0")
|
assert not os.path.exists("/tmp/krb5cc_0")
|
||||||
@ -1116,13 +1151,11 @@ def test_cli_setup_authority():
|
|||||||
# Certidude would auth against domain controller
|
# Certidude would auth against domain controller
|
||||||
os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
|
os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
|
||||||
os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
|
os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
|
||||||
os.system("sed -e 's/backends = pam/backends = kerberos ldap/g' -i /etc/certidude/server.conf")
|
|
||||||
os.system("sed -e 's/backend = posix/backend = ldap/g' -i /etc/certidude/server.conf")
|
|
||||||
os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude")
|
os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude")
|
||||||
os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf")
|
os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf")
|
||||||
os.system("sed -e 's/machine enrollment =.*/machine enrollment = allowed/g' -i /etc/certidude/server.conf")
|
os.system("sed -e 's/machine enrollment subnets =.*/machine enrollment subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
|
||||||
os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
|
os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
|
||||||
os.system("sed -e 's/ocsp subnets =.*/ocsp subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
|
os.system("sed -e 's/ocsp subnets =.*/ocsp subnets =/g' -i /etc/certidude/server.conf")
|
||||||
os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf")
|
os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf")
|
||||||
os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf")
|
os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf")
|
||||||
from certidude.common import pip
|
from certidude.common import pip
|
||||||
@ -1157,41 +1190,25 @@ def test_cli_setup_authority():
|
|||||||
if r.status_code != 502:
|
if r.status_code != 502:
|
||||||
break
|
break
|
||||||
sleep(1)
|
sleep(1)
|
||||||
assert r.status_code == 401
|
assert r.status_code == 401, "Timed out starting up the API backend"
|
||||||
|
|
||||||
# CRL-s disabled now
|
# CRL-s disabled now
|
||||||
r = requests.get("http://ca.example.lan/api/revoked/")
|
r = requests.get("http://ca.example.lan/api/revoked/")
|
||||||
assert r.status_code == 404, r.text
|
assert r.status_code == 404, r.text
|
||||||
|
|
||||||
# OCSP and SCEP should be enabled now
|
# SCEP should be enabled now
|
||||||
r = requests.get("http://ca.example.lan/api/scep/")
|
r = requests.get("http://ca.example.lan/api/scep/")
|
||||||
assert r.status_code == 400
|
assert r.status_code == 400
|
||||||
r = requests.get("http://ca.example.lan/api/ocsp/")
|
|
||||||
assert r.status_code == 400
|
|
||||||
r = requests.post("http://ca.example.lan/api/scep/")
|
r = requests.post("http://ca.example.lan/api/scep/")
|
||||||
assert r.status_code == 405
|
assert r.status_code == 405
|
||||||
|
|
||||||
|
# OCSP should be disabled now
|
||||||
|
r = requests.get("http://ca.example.lan/api/ocsp/")
|
||||||
|
assert r.status_code == 404
|
||||||
r = requests.post("http://ca.example.lan/api/ocsp/")
|
r = requests.post("http://ca.example.lan/api/ocsp/")
|
||||||
assert r.status_code == 400
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/signed/roadwarrior2.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp1.log") == 0
|
|
||||||
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/ca_cert.pem -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp2.log") == 0
|
|
||||||
|
|
||||||
for filename in os.listdir("/var/lib/certidude/ca.example.lan/revoked"):
|
|
||||||
if not filename.endswith(".pem"):
|
|
||||||
continue
|
|
||||||
assert os.system("openssl ocsp -issuer /var/lib/certidude/ca.example.lan/ca_cert.pem -cert /var/lib/certidude/ca.example.lan/revoked/%s -text -url http://ca.example.lan/api/ocsp/ -out /tmp/ocsp3.log" % filename) == 0
|
|
||||||
break
|
|
||||||
|
|
||||||
with open("/tmp/ocsp1.log") as fh:
|
|
||||||
buf = fh.read()
|
|
||||||
assert ": good" in buf, buf
|
|
||||||
with open("/tmp/ocsp2.log") as fh:
|
|
||||||
buf = fh.read()
|
|
||||||
assert ": unknown" in buf, buf
|
|
||||||
with open("/tmp/ocsp3.log") as fh:
|
|
||||||
buf = fh.read()
|
|
||||||
assert ": revoked" in buf, buf
|
|
||||||
|
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
@ -1274,7 +1291,6 @@ def test_cli_setup_authority():
|
|||||||
### SCEP tests ###
|
### SCEP tests ###
|
||||||
##################
|
##################
|
||||||
|
|
||||||
os.umask(0o022)
|
|
||||||
if not os.path.exists("/tmp/sscep"):
|
if not os.path.exists("/tmp/sscep"):
|
||||||
assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep")
|
assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep")
|
||||||
if not os.path.exists("/tmp/sscep/sscep_dyn"):
|
if not os.path.exists("/tmp/sscep/sscep_dyn"):
|
||||||
@ -1283,7 +1299,7 @@ def test_cli_setup_authority():
|
|||||||
if not os.path.exists("/tmp/key.pem"):
|
if not os.path.exists("/tmp/key.pem"):
|
||||||
assert not os.system("openssl genrsa -out /tmp/key.pem 1024")
|
assert not os.system("openssl genrsa -out /tmp/key.pem 1024")
|
||||||
if not os.path.exists("/tmp/req.pem"):
|
if not os.path.exists("/tmp/req.pem"):
|
||||||
assert not os.system("echo '.\n.\n.\n.\n.\ntest8\n\n\n\n' | openssl req -new -sha256 -key /tmp/key.pem -out /tmp/req.pem")
|
assert not os.system("echo '.\n.\n.\n.\nGateway\ntest8\n\n\n\n' | openssl req -new -sha256 -key /tmp/key.pem -out /tmp/req.pem")
|
||||||
assert not os.system("/tmp/sscep/sscep_dyn enroll -c /tmp/sscep/ca.pem -u http://ca.example.lan/cgi-bin/pkiclient.exe -k /tmp/key.pem -r /tmp/req.pem -l /tmp/cert.pem")
|
assert not os.system("/tmp/sscep/sscep_dyn enroll -c /tmp/sscep/ca.pem -u http://ca.example.lan/cgi-bin/pkiclient.exe -k /tmp/key.pem -r /tmp/req.pem -l /tmp/cert.pem")
|
||||||
# TODO: test e-mails at this point
|
# TODO: test e-mails at this point
|
||||||
|
|
||||||
@ -1301,13 +1317,15 @@ def test_cli_setup_authority():
|
|||||||
# Shut down server
|
# Shut down server
|
||||||
assert os.path.exists("/proc/%d" % server_pid)
|
assert os.path.exists("/proc/%d" % server_pid)
|
||||||
os.kill(server_pid, 15)
|
os.kill(server_pid, 15)
|
||||||
|
# sleep(2)
|
||||||
|
# os.kill(server_pid, 9)
|
||||||
os.waitpid(server_pid, 0)
|
os.waitpid(server_pid, 0)
|
||||||
|
|
||||||
# Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude
|
# Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude
|
||||||
assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \
|
assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \
|
||||||
"/var/lib/certidude/ca.example.lan/client_key.pem r,\n" + \
|
"/etc/certidude/authority/ca.example.lan/client_key.pem r,\n" + \
|
||||||
"/var/lib/certidude/ca.example.lan/ca_cert.pem r,\n" + \
|
"/etc/certidude/authority/ca.example.lan/ca_cert.pem r,\n" + \
|
||||||
"/var/lib/certidude/ca.example.lan/client_cert.pem r,\n"
|
"/etc/certidude/authority/ca.example.lan/client_cert.pem r,\n"
|
||||||
assert len(inbox) == 0, inbox # Make sure all messages were checked
|
assert len(inbox) == 0, inbox # Make sure all messages were checked
|
||||||
|
|
||||||
os.system("service nginx stop")
|
os.system("service nginx stop")
|
||||||
|
Loading…
Reference in New Issue
Block a user