Skip to content
51 changes: 43 additions & 8 deletions dementor/assets/Dementor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -440,19 +440,26 @@ EncType = "aes256_cts_hmac_sha1_96"
# =============================================================================
[LDAP]

# A list of capabilities to support. You can also set this option in each server

# Capabilities = []
# A list of capabilities to support. You can also set this option in each server.
# These OIDs identify Active Directory-specific features per MS-ADTS §3.1.1.3.3.

# Capabilities = [
# "1.2.840.113556.1.4.800", # LDAP_CAP_ACTIVE_DIRECTORY_OID
# "1.2.840.113556.1.4.1791", # LDAP_CAP_ACTIVE_DIRECTORY_LDAP_INTEG_OID
# "1.2.840.113556.1.4.1670", # LDAP_CAP_ACTIVE_DIRECTORY_V51_OID
# ]

# A list of SASL mechanisms to return. Can be set in each server
# A list of SASL mechanisms to return. Can be set in each server.
# Supported mechanisms: GSS-SPNEGO, DIGEST-MD5, PLAIN, NTLM

# SASLMechanisms = []
# SASLMechanisms = ["GSS-SPNEGO", "DIGEST-MD5", "PLAIN", "NTLM"]

# Global timeout configuration (can be changed in each server). Zero (0) indeicates
# Global timeout configuration (can be changed in each server). Zero (0) indicates
# no timeout (default), all other values are set in seconds. Note that no timeout
# will lead to problems when closing Dementor.

Timeout = 2
Timeout = 0

# Hostname + fully qualified domain name, whereby the domain name is optional
# Full example: "HOSTNAME.domain.local"
Expand All @@ -469,11 +476,34 @@ TLS = false
# Key = "/path/to/key"

# The error code to return after successful authentication. It is recommended to return
# an actual error code rather that success(0). By default, "unwillingToPerform" will be
# an actual error code rather than success(0). By default, "unwillingToPerform" will be
# returned.

# ErrorCode = "unwillingToPerform"

# Enable channel binding per RFC 5929 to prevent LDAP relay attacks.
# Requires TLS to be active.

# ChannelBinding = false

# Require SASL signing (integrity protection) for all connections.

# RequireSigning = false

# Require SASL sealing (encryption) for all connections.

# RequireSealing = false

# List of supported channel binding types per RFC 5929 §4.
# Options: "tls-unique", "tls-server-end-point"

# ChannelBindingTypes = ["tls-unique", "tls-server-end-point"]

# List of supported SASL Quality of Protection (QoP) options per RFC 4513 §5.2.4.
# Options: "auth" (authentication only), "auth-int" (integrity), "auth-conf" (confidentiality)

# SASLQoPOptions = ["auth", "auth-int", "auth-conf"]

# NTLM settings: Challenge, DisableExtendedSessionSecurity, DisableNTLMv2
# Not set here → falls back to [NTLM]. Set here to override [NTLM] for all
# LDAP servers, or inside [[LDAP.Server]] to override for a single server only.
Expand All @@ -487,10 +517,15 @@ Connectionless = false
Port = 389

[[LDAP.Server]]
# means UDP
# means UDP (CLDAP)
Port = 389
Connectionless = true

[[LDAP.Server]]
Connectionless = false
Port = 636
TLS = true


# =============================================================================
# QUIC
Expand Down
71 changes: 71 additions & 0 deletions dementor/config/attr.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
from dementor.config.toml import Attribute
from dementor.config.util import is_true

# TLS/Certificate Configuration Attributes
# These attributes are shared across protocols that support TLS and
# certificate-based authentication


ATTR_CERT = Attribute(
attr_name="certfile",
qname="Cert",
Expand All @@ -42,3 +47,69 @@
default_val=False,
factory=is_true,
)


# Self-Signed Certificate Generation Attributes
# These attributes configure automatic self-signed certificate generation
# when TLS is enabled but no certificates are provided.
# Similar to QUIC implementation, allows global configuration across protocols.

ATTR_SELF_SIGNED = Attribute(
attr_name="self_signed",
qname="EnableSelfSigned",
default_val=True,
factory=is_true,
section_local=False,
)

ATTR_CERT_CN = Attribute(
attr_name="cert_cn",
qname="CertCommonName",
default_val="dementor.local",
section_local=False,
)

ATTR_CERT_ORG = Attribute(
attr_name="cert_org",
qname="CertOrganization",
default_val="Dementor",
section_local=False,
)

ATTR_CERT_COUNTRY = Attribute(
attr_name="cert_country",
qname="CertCountry",
default_val="US",
section_local=False,
)

ATTR_CERT_STATE = Attribute(
attr_name="cert_state",
qname="CertState",
default_val="CA",
section_local=False,
)

ATTR_CERT_LOCALITY = Attribute(
attr_name="cert_locality",
qname="CertLocality",
default_val="San Francisco",
section_local=False,
)

ATTR_CERT_VALIDITY_DAYS = Attribute(
attr_name="cert_validity_days",
qname="CertValidityDays",
default_val=365,
section_local=False,
)

SELFSIGNED_COMMON_ATTRS = [
ATTR_SELF_SIGNED,
ATTR_CERT_CN,
ATTR_CERT_COUNTRY,
ATTR_CERT_LOCALITY,
ATTR_CERT_ORG,
ATTR_CERT_STATE,
ATTR_CERT_VALIDITY_DAYS,
]
89 changes: 89 additions & 0 deletions dementor/config/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,18 @@
import random
import string
import secrets
import os
import tempfile

from typing import Any
from jinja2.sandbox import SandboxedEnvironment

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes

from dementor.config import get_global_config

# --------------------------------------------------------------------------- #
Expand Down Expand Up @@ -219,3 +227,84 @@ def now() -> str:
:rtype: str
"""
return datetime.datetime.now(tz=datetime.UTC).strftime("%Y-%m-%d-%H-%M-%S")


def generate_self_signed_cert(
cn: str,
org: str,
country: str,
state: str,
locality: str,
validity_days: int,
) -> tuple[str, str, tempfile.TemporaryDirectory]:
"""
Generate a self-signed certificate and private key in a temporary directory.

:param cn: Common name for the certificate.
:param org: Organization name.
:param country: Country code.
:param state: State or province.
:param locality: Locality or city.
:param validity_days: Number of days the certificate is valid.
:return: Tuple of (certificate path, key path, temporary directory object).
"""
# Create temp dir
temp_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)

# Generate private key
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)

# Create certificate
subject = issuer = x509.Name(
[
x509.NameAttribute(NameOID.COUNTRY_NAME, country),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, state),
x509.NameAttribute(NameOID.LOCALITY_NAME, locality),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org),
x509.NameAttribute(NameOID.COMMON_NAME, cn),
]
)

cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(private_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.now(datetime.UTC))
.not_valid_after(
datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=validity_days)
)
.add_extension(
x509.SubjectAlternativeName(
[
x509.DNSName(cn),
]
),
critical=False,
)
.sign(private_key, hashes.SHA256())
)

# Save private key
key_id = "".join(random.choices(string.hexdigits))
key_path = os.path.join(temp_dir.name, f"key_{key_id}.pem")
with open(key_path, "wb") as f:
f.write(
private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
)

# Save certificate
cert_id = "".join(random.choices(string.hexdigits))
cert_path = os.path.join(temp_dir.name, f"cert_{cert_id}.pem")
with open(cert_path, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))

return cert_path, key_path, temp_dir
Loading