1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 08:15:18 +00:00

Several updates

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

4
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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