mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
Migrate signature profiles to separate config file
This commit is contained in:
parent
b9aaec7fa6
commit
94e5f72566
@ -163,7 +163,8 @@ class SessionResource(AuthorityHandler):
|
|||||||
admin_subnets=config.ADMIN_SUBNETS or None,
|
admin_subnets=config.ADMIN_SUBNETS or None,
|
||||||
signature = dict(
|
signature = dict(
|
||||||
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
|
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
|
||||||
profiles = [dict(name=k, server=v[0]=="server", lifetime=v[1], organizational_unit=v[2], title=v[3]) for k,v in config.PROFILES.items()]
|
profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")),
|
||||||
|
|
||||||
)
|
)
|
||||||
) if req.context.get("user").is_admin() else None,
|
) if req.context.get("user").is_admin() else None,
|
||||||
features=dict(
|
features=dict(
|
||||||
|
@ -10,6 +10,7 @@ from base64 import b64decode
|
|||||||
from certidude import config, push, errors
|
from certidude import config, push, errors
|
||||||
from certidude.auth import login_required, login_optional, authorize_admin
|
from certidude.auth import login_required, login_optional, authorize_admin
|
||||||
from certidude.decorators import csrf_protection, MyEncoder
|
from certidude.decorators import csrf_protection, MyEncoder
|
||||||
|
from certidude.profile import SignatureProfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from oscrypto import asymmetric
|
from oscrypto import asymmetric
|
||||||
from oscrypto.errors import SignatureError
|
from oscrypto.errors import SignatureError
|
||||||
@ -71,7 +72,8 @@ class RequestListResource(AuthorityHandler):
|
|||||||
|
|
||||||
# Automatic enroll with Kerberos machine cerdentials
|
# Automatic enroll with Kerberos machine cerdentials
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
cert, resp.body = self.authority._sign(csr, body, overwrite=True)
|
cert, resp.body = self.authority._sign(csr, body,
|
||||||
|
profile=config.PROFILES["rw"], overwrite=True)
|
||||||
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
|
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
|
||||||
machine, req.context.get("remote_addr"))
|
machine, req.context.get("remote_addr"))
|
||||||
return
|
return
|
||||||
@ -89,28 +91,26 @@ class RequestListResource(AuthorityHandler):
|
|||||||
cert_pk = cert["tbs_certificate"]["subject_public_key_info"].native
|
cert_pk = cert["tbs_certificate"]["subject_public_key_info"].native
|
||||||
csr_pk = csr["certification_request_info"]["subject_pk_info"].native
|
csr_pk = csr["certification_request_info"]["subject_pk_info"].native
|
||||||
|
|
||||||
try:
|
# Same public key
|
||||||
|
if cert_pk == csr_pk:
|
||||||
buf = req.get_header("X-SSL-CERT")
|
buf = req.get_header("X-SSL-CERT")
|
||||||
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
|
# Used mutually authenticated TLS handshake, assume renewal
|
||||||
handshake_cert = x509.Certificate.load(der_bytes)
|
if buf:
|
||||||
except:
|
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
|
||||||
raise
|
handshake_cert = x509.Certificate.load(der_bytes)
|
||||||
else:
|
|
||||||
# Same public key
|
|
||||||
if cert_pk == csr_pk:
|
|
||||||
# Used mutually authenticated TLS handshake, assume renewal
|
|
||||||
if handshake_cert.native == cert.native:
|
if handshake_cert.native == cert.native:
|
||||||
for subnet in config.RENEWAL_SUBNETS:
|
for subnet in config.RENEWAL_SUBNETS:
|
||||||
if req.context.get("remote_addr") in subnet:
|
if req.context.get("remote_addr") in subnet:
|
||||||
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
||||||
_, resp.body = self.authority._sign(csr, body, overwrite=True)
|
_, 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"))
|
logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
||||||
return
|
return
|
||||||
|
|
||||||
# No header supplied, redirect to signed API call
|
# No header supplied, redirect to signed API call
|
||||||
resp.status = falcon.HTTP_SEE_OTHER
|
resp.status = falcon.HTTP_SEE_OTHER
|
||||||
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name)
|
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -123,7 +123,7 @@ class RequestListResource(AuthorityHandler):
|
|||||||
if req.context.get("remote_addr") in subnet:
|
if req.context.get("remote_addr") in subnet:
|
||||||
try:
|
try:
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
_, resp.body = self.authority._sign(csr, body)
|
_, resp.body = self.authority._sign(csr, body, profile=config.PROFILES["rw"])
|
||||||
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
||||||
return
|
return
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
@ -222,7 +222,7 @@ class RequestDetailResource(AuthorityHandler):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cert, buf = self.authority.sign(cn,
|
cert, buf = self.authority.sign(cn,
|
||||||
profile=req.get_param("profile", default="default"),
|
profile=config.PROFILES[req.get_param("profile", default="rw")],
|
||||||
overwrite=True,
|
overwrite=True,
|
||||||
signer=req.context.get("user").name)
|
signer=req.context.get("user").name)
|
||||||
# Mailing and long poll publishing implemented in the function above
|
# Mailing and long poll publishing implemented in the function above
|
||||||
|
@ -71,7 +71,7 @@ def self_enroll():
|
|||||||
from certidude import authority
|
from certidude import authority
|
||||||
from certidude.common import drop_privileges
|
from certidude.common import drop_privileges
|
||||||
drop_privileges()
|
drop_privileges()
|
||||||
authority.sign(common_name, skip_push=True, overwrite=True, profile="srv")
|
authority.sign(common_name, skip_push=True, overwrite=True, profile=config.PROFILES["srv"])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
os.waitpid(pid, 0)
|
os.waitpid(pid, 0)
|
||||||
@ -82,7 +82,7 @@ def self_enroll():
|
|||||||
|
|
||||||
|
|
||||||
def get_request(common_name):
|
def get_request(common_name):
|
||||||
if not re.match(const.RE_HOSTNAME, common_name):
|
if not re.match(const.RE_COMMON_NAME, common_name):
|
||||||
raise ValueError("Invalid common name %s" % repr(common_name))
|
raise ValueError("Invalid common name %s" % repr(common_name))
|
||||||
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
||||||
try:
|
try:
|
||||||
@ -95,7 +95,7 @@ def get_request(common_name):
|
|||||||
raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path)
|
raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path)
|
||||||
|
|
||||||
def get_signed(common_name):
|
def get_signed(common_name):
|
||||||
if not re.match(const.RE_HOSTNAME, common_name):
|
if not re.match(const.RE_COMMON_NAME, common_name):
|
||||||
raise ValueError("Invalid common name %s" % repr(common_name))
|
raise ValueError("Invalid common name %s" % repr(common_name))
|
||||||
path = os.path.join(config.SIGNED_DIR, common_name + ".pem")
|
path = os.path.join(config.SIGNED_DIR, common_name + ".pem")
|
||||||
with open(path, "rb") as fh:
|
with open(path, "rb") as fh:
|
||||||
@ -158,7 +158,7 @@ def store_request(buf, overwrite=False, address="", user=""):
|
|||||||
|
|
||||||
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
||||||
|
|
||||||
if not re.match(const.RE_HOSTNAME, common_name):
|
if not re.match(const.RE_COMMON_NAME, common_name):
|
||||||
raise ValueError("Invalid common name")
|
raise ValueError("Invalid common name")
|
||||||
|
|
||||||
request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
||||||
@ -296,7 +296,7 @@ def export_crl(pem=True):
|
|||||||
|
|
||||||
def delete_request(common_name):
|
def delete_request(common_name):
|
||||||
# Validate CN
|
# Validate CN
|
||||||
if not re.match(const.RE_HOSTNAME, common_name):
|
if not re.match(const.RE_COMMON_NAME, common_name):
|
||||||
raise ValueError("Invalid common name")
|
raise ValueError("Invalid common name")
|
||||||
|
|
||||||
path, buf, csr, submitted = get_request(common_name)
|
path, buf, csr, submitted = get_request(common_name)
|
||||||
@ -310,7 +310,7 @@ def delete_request(common_name):
|
|||||||
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
|
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
|
||||||
headers={"User-Agent": "Certidude API"})
|
headers={"User-Agent": "Certidude API"})
|
||||||
|
|
||||||
def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None):
|
def sign(common_name, profile, skip_notify=False, skip_push=False, overwrite=False, signer=None):
|
||||||
"""
|
"""
|
||||||
Sign certificate signing request by it's common name
|
Sign certificate signing request by it's common name
|
||||||
"""
|
"""
|
||||||
@ -323,16 +323,13 @@ def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, profi
|
|||||||
|
|
||||||
|
|
||||||
# Sign with function below
|
# Sign with function below
|
||||||
cert, buf = _sign(csr, csr_buf, skip_notify, skip_push, overwrite, profile, signer)
|
cert, buf = _sign(csr, csr_buf, profile, skip_notify, skip_push, overwrite, signer)
|
||||||
|
|
||||||
os.unlink(req_path)
|
os.unlink(req_path)
|
||||||
return cert, buf
|
return cert, buf
|
||||||
|
|
||||||
def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None):
|
def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False, signer=None):
|
||||||
# TODO: CRLDistributionPoints, OCSP URL, Certificate URL
|
# TODO: CRLDistributionPoints, OCSP URL, Certificate URL
|
||||||
if profile not in config.PROFILES:
|
|
||||||
raise ValueError("Invalid profile supplied '%s'" % profile)
|
|
||||||
|
|
||||||
assert buf.startswith(b"-----BEGIN ")
|
assert buf.startswith(b"-----BEGIN ")
|
||||||
assert isinstance(csr, CertificationRequest)
|
assert isinstance(csr, CertificationRequest)
|
||||||
csr_pubkey = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"])
|
csr_pubkey = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"])
|
||||||
@ -370,10 +367,9 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile
|
|||||||
else:
|
else:
|
||||||
raise FileExistsError("Will not overwrite existing certificate")
|
raise FileExistsError("Will not overwrite existing certificate")
|
||||||
|
|
||||||
# Sign via signer process
|
|
||||||
dn = {u'common_name': common_name }
|
dn = {u'common_name': common_name }
|
||||||
profile_server_flags, lifetime, dn["organizational_unit_name"], _ = config.PROFILES[profile]
|
if profile.ou:
|
||||||
lifetime = int(lifetime)
|
dn["organizational_unit_name"] = profile.ou
|
||||||
|
|
||||||
builder = CertificateBuilder(dn, csr_pubkey)
|
builder = CertificateBuilder(dn, csr_pubkey)
|
||||||
builder.serial_number = random.randint(
|
builder.serial_number = random.randint(
|
||||||
@ -382,18 +378,12 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile
|
|||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
builder.begin_date = now - timedelta(minutes=5)
|
builder.begin_date = now - timedelta(minutes=5)
|
||||||
builder.end_date = now + timedelta(days=lifetime)
|
builder.end_date = now + timedelta(days=profile.lifetime)
|
||||||
builder.issuer = certificate
|
builder.issuer = certificate
|
||||||
builder.ca = False
|
builder.ca = profile.ca
|
||||||
builder.key_usage = set(["digital_signature", "key_encipherment"])
|
builder.key_usage = profile.key_usage
|
||||||
|
builder.extended_key_usage = profile.extended_key_usage
|
||||||
# If we have FQDN and profile suggests server flags, enable them
|
builder.subject_alt_domains = [common_name]
|
||||||
if server_flags(common_name) and profile_server_flags:
|
|
||||||
builder.subject_alt_domains = [common_name] # OpenVPN uses CN while StrongSwan uses SAN to match hostname of the server
|
|
||||||
builder.extended_key_usage = set(["server_auth", "1.3.6.1.5.5.8.2.2", "client_auth"])
|
|
||||||
else:
|
|
||||||
builder.subject_alt_domains = [common_name] # iOS demands SAN also for clients
|
|
||||||
builder.extended_key_usage = set(["client_auth"])
|
|
||||||
|
|
||||||
end_entity_cert = builder.build(private_key)
|
end_entity_cert = builder.build(private_key)
|
||||||
end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert)
|
end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert)
|
||||||
|
@ -281,8 +281,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
common_name = const.FQDN
|
common_name = const.FQDN
|
||||||
elif "$" in common_name:
|
elif "$" in common_name:
|
||||||
raise ValueError("Invalid variable '%s' supplied, only $HOSTNAME and $FQDN allowed" % common_name)
|
raise ValueError("Invalid variable '%s' supplied, only $HOSTNAME and $FQDN allowed" % common_name)
|
||||||
if not re.match(const.RE_HOSTNAME, common_name):
|
if not re.match(const.RE_COMMON_NAME, common_name):
|
||||||
raise ValueError("Invalid common name '%s' supplied" % common_name)
|
raise ValueError("Supplied common name %s doesn't match the expression %s" % (common_name, const.RE_COMMON_NAME))
|
||||||
|
|
||||||
|
|
||||||
################################
|
################################
|
||||||
@ -338,7 +338,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
renewal_overlap = clients.getint(authority_name, "renewal overlap")
|
renewal_overlap = clients.getint(authority_name, "renewal overlap")
|
||||||
except NoOptionError: # Renewal not specified in config
|
except NoOptionError: # Renewal not configured
|
||||||
renewal_overlap = None
|
renewal_overlap = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1169,6 +1169,14 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
|
|||||||
fh.write(env.get_template("server/builder.conf").render(vars()))
|
fh.write(env.get_template("server/builder.conf").render(vars()))
|
||||||
click.echo("File %s created" % const.BUILDER_CONFIG_PATH)
|
click.echo("File %s created" % const.BUILDER_CONFIG_PATH)
|
||||||
|
|
||||||
|
# Create image builder config
|
||||||
|
if os.path.exists(const.PROFILE_CONFIG_PATH):
|
||||||
|
click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
|
||||||
|
else:
|
||||||
|
with open(const.PROFILE_CONFIG_PATH, "w") as fh:
|
||||||
|
fh.write(env.get_template("server/profile.conf").render(vars()))
|
||||||
|
click.echo("File %s created" % const.PROFILE_CONFIG_PATH)
|
||||||
|
|
||||||
# Create directory with 755 permissions
|
# Create directory with 755 permissions
|
||||||
os.umask(0o022)
|
os.umask(0o022)
|
||||||
if not os.path.exists(directory):
|
if not os.path.exists(directory):
|
||||||
@ -1347,12 +1355,12 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
|
|||||||
|
|
||||||
@click.command("sign", help="Sign certificate")
|
@click.command("sign", help="Sign certificate")
|
||||||
@click.argument("common_name")
|
@click.argument("common_name")
|
||||||
@click.option("--profile", "-p", default="default", help="Profile")
|
@click.option("--profile", "-p", default="rw", help="Profile")
|
||||||
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
|
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
|
||||||
def certidude_sign(common_name, overwrite, profile):
|
def certidude_sign(common_name, overwrite, profile):
|
||||||
from certidude import authority
|
from certidude import authority
|
||||||
drop_privileges()
|
drop_privileges()
|
||||||
cert = authority.sign(common_name, overwrite=overwrite, profile=profile)
|
cert = authority.sign(common_name, overwrite=overwrite, profile=config.PROFILES[profile])
|
||||||
|
|
||||||
|
|
||||||
@click.command("revoke", help="Revoke certificate")
|
@click.command("revoke", help="Revoke certificate")
|
||||||
@ -1397,6 +1405,11 @@ def certidude_serve(port, listen, fork):
|
|||||||
|
|
||||||
from certidude import config
|
from certidude import config
|
||||||
|
|
||||||
|
click.echo("Loading signature profiles:")
|
||||||
|
for profile in config.PROFILES.values():
|
||||||
|
click.echo("- %s" % profile)
|
||||||
|
click.echo()
|
||||||
|
|
||||||
# Rebuild reverse mapping
|
# Rebuild reverse mapping
|
||||||
for cn, path, buf, cert, signed, expires in authority.list_signed():
|
for cn, path, buf, cert, signed, expires in authority.list_signed():
|
||||||
by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number)
|
by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number)
|
||||||
|
@ -2,7 +2,9 @@ import configparser
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import os
|
import os
|
||||||
from certidude import const
|
from certidude import const
|
||||||
|
from certidude.profile import SignatureProfile
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
# Options that are parsed from config file are fetched here
|
# Options that are parsed from config file are fetched here
|
||||||
|
|
||||||
@ -96,11 +98,22 @@ TOKEN_SECRET = cp.get("token", "secret").encode("ascii")
|
|||||||
# The API call for looking up scripts uses following directory as root
|
# The API call for looking up scripts uses following directory as root
|
||||||
SCRIPT_DIR = cp.get("script", "path")
|
SCRIPT_DIR = cp.get("script", "path")
|
||||||
|
|
||||||
PROFILES = OrderedDict([[i, [j.strip() for j in cp.get("profile", i).split(",")]] for i in cp.options("profile")])
|
from configparser import ConfigParser
|
||||||
|
profile_config = ConfigParser()
|
||||||
|
profile_config.readfp(open(const.PROFILE_CONFIG_PATH))
|
||||||
|
|
||||||
|
PROFILES = dict([(key, SignatureProfile(key,
|
||||||
|
profile_config.get(key, "title"),
|
||||||
|
profile_config.get(key, "ou"),
|
||||||
|
profile_config.getboolean(key, "ca"),
|
||||||
|
profile_config.getint(key, "lifetime"),
|
||||||
|
profile_config.get(key, "key usage"),
|
||||||
|
profile_config.get(key, "extended key usage"),
|
||||||
|
profile_config.get(key, "common name"),
|
||||||
|
)) for key in profile_config.sections()])
|
||||||
|
|
||||||
cp2 = configparser.RawConfigParser()
|
cp2 = configparser.RawConfigParser()
|
||||||
cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r"))
|
cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r"))
|
||||||
IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()]
|
IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()]
|
||||||
|
|
||||||
|
|
||||||
TOKEN_OVERWRITE_PERMITTED=True
|
TOKEN_OVERWRITE_PERMITTED=True
|
||||||
|
@ -6,12 +6,15 @@ import sys
|
|||||||
|
|
||||||
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
|
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
|
||||||
CURVE_NAME = "secp384r1"
|
CURVE_NAME = "secp384r1"
|
||||||
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$"
|
RE_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"
|
RUN_DIR = "/run/certidude"
|
||||||
CONFIG_DIR = "/etc/certidude"
|
CONFIG_DIR = "/etc/certidude"
|
||||||
SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
|
SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
|
||||||
BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf")
|
BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf")
|
||||||
|
PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf")
|
||||||
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
|
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
|
||||||
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
|
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
|
||||||
SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid")
|
SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid")
|
||||||
|
52
certidude/profile.py
Normal file
52
certidude/profile.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
import click
|
||||||
|
from datetime import timedelta
|
||||||
|
from certidude import const
|
||||||
|
|
||||||
|
class SignatureProfile(object):
|
||||||
|
def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name):
|
||||||
|
self.slug = slug
|
||||||
|
self.title = title
|
||||||
|
self.ou = ou or None
|
||||||
|
self.ca = ca
|
||||||
|
self.lifetime = lifetime
|
||||||
|
self.key_usage = set(key_usage.split(" ")) if key_usage else set()
|
||||||
|
self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set()
|
||||||
|
if common_name.startswith("^"):
|
||||||
|
self.common_name = common_name
|
||||||
|
elif common_name == "RE_HOSTNAME":
|
||||||
|
self.common_name = const.RE_HOSTNAME
|
||||||
|
elif common_name == "RE_FQDN":
|
||||||
|
self.common_name = const.RE_FQDN
|
||||||
|
elif common_name == "RE_COMMON_NAME":
|
||||||
|
self.common_name = const.RE_COMMON_NAME
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid common name constraint %s" % common_name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_cert(self, cert):
|
||||||
|
"""
|
||||||
|
Derive signature profile from an already signed certificate, eg for renewal
|
||||||
|
"""
|
||||||
|
lifetime = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) - \
|
||||||
|
cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None)
|
||||||
|
return SignatureProfile(
|
||||||
|
None, "Renewal", cert.subject.native.get("organizational_unit_name"),
|
||||||
|
cert.ca, lifetime.days,
|
||||||
|
" ".join(cert.key_usage_value.native),
|
||||||
|
" ".join(cert.extended_key_usage_value.native), "^")
|
||||||
|
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
return dict([(key, getattr(self,key)) for key in (
|
||||||
|
"slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name")])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
bits = []
|
||||||
|
if self.lifetime >= 365:
|
||||||
|
bits.append("%d years" % (self.lifetime / 365))
|
||||||
|
if self.lifetime % 365:
|
||||||
|
bits.append("%d days" % (self.lifetime % 365))
|
||||||
|
return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s)" % (
|
||||||
|
self.slug, self.title, self.ca, self.ou, " ".join(bits), self.key_usage, self.extended_key_usage, self.common_name)
|
||||||
|
|
@ -30,11 +30,10 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
{% for p in session.authority.signature.profiles %}
|
{% for p in session.authority.signature.profiles %}
|
||||||
<a class="dropdown-item{% if p.server and not request.server %} disabled{% endif %}"
|
<a class="dropdown-item{% if not request.common_name.match(p.common_name) %} disabled{% endif %}"
|
||||||
{% if p.server and not request.server %}title="Resubmit with FQDN as common name"{% endif %}
|
{% if not request.common_name.match(p.common_name) %} title="Common name doesn't match expression {{ p.common_name }}"{% endif %}
|
||||||
href="#" onclick="javascript:$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}&profile={{ p.name }}',type:'post'});">
|
href="#" onclick="javascript:$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}&profile={{ p.slug }}',type:'post'});">
|
||||||
{% if p.title %}{{ p.title }} ({% if p.server %}server{% else %}client{% endif %}){% else %}
|
{{ p.title }}, expires in {{ p.lifetime }} days</a>
|
||||||
{% if p.server %}Server{% else %}Client{% endif %}{% endif %}, expires in {{ p.lifetime }} days</a>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
52
certidude/templates/server/profile.conf
Normal file
52
certidude/templates/server/profile.conf
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
ou =
|
||||||
|
lifetime = 120
|
||||||
|
ca = false
|
||||||
|
common name = RE_COMMON_NAME
|
||||||
|
key usage = digital_signature key_encipherment
|
||||||
|
extended key usage =
|
||||||
|
|
||||||
|
[ca]
|
||||||
|
title = Certificate Authority
|
||||||
|
common name = ^ca
|
||||||
|
ca = true
|
||||||
|
key usage = key_cert_sign crl_sign
|
||||||
|
extended key usage =
|
||||||
|
lifetime = 1095
|
||||||
|
|
||||||
|
[rw]
|
||||||
|
title = Roadwarrior
|
||||||
|
ou = Roadwarrior
|
||||||
|
common name = RE_HOSTNAME
|
||||||
|
extended key usage = client_auth
|
||||||
|
|
||||||
|
[srv]
|
||||||
|
title = Server
|
||||||
|
;ou = Server
|
||||||
|
common name = RE_FQDN
|
||||||
|
lifetime = 365
|
||||||
|
extended key usage = server_auth
|
||||||
|
|
||||||
|
[gw]
|
||||||
|
title = Gateway
|
||||||
|
ou = Gateway
|
||||||
|
common name = RE_FQDN
|
||||||
|
renewable = true
|
||||||
|
lifetime = 30
|
||||||
|
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
|
||||||
|
extended key usage = client_auth
|
||||||
|
|
||||||
|
[mfp]
|
||||||
|
title = Printers
|
||||||
|
ou = Printers
|
||||||
|
common name = ^mfp\-
|
||||||
|
lifetime = 30
|
||||||
|
extended key usage = client_auth
|
||||||
|
|
||||||
|
|
@ -194,14 +194,6 @@ lifetime = 30
|
|||||||
# Secret for generating and validating tokens, regenerate occasionally
|
# Secret for generating and validating tokens, regenerate occasionally
|
||||||
secret = {{ token_secret }}
|
secret = {{ token_secret }}
|
||||||
|
|
||||||
[profile]
|
|
||||||
# name, flags, lifetime, organizational unit, title
|
|
||||||
default = client, 120, Roadwarrior, Roadwarrior
|
|
||||||
gw = server, 30, Gateway, Gateway
|
|
||||||
srv = server, 365, Server,
|
|
||||||
ap = client, 1825, Access Point, Access Point
|
|
||||||
mfp = client, 30, MFP, Printers
|
|
||||||
|
|
||||||
[script]
|
[script]
|
||||||
# Path to the folder with scripts that can be served to the clients, set none to disable scripting
|
# Path to the folder with scripts that can be served to the clients, set none to disable scripting
|
||||||
path = {{ script_dir }}
|
path = {{ script_dir }}
|
||||||
|
Loading…
Reference in New Issue
Block a user