300 lines
9.5 KiB
Python
300 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate self-signed SSL/TLS certificates for LDAP server testing.
|
|
|
|
This script creates a CA certificate and a server certificate for development use.
|
|
For production, use proper certificates from your dev-ca or a trusted CA.
|
|
"""
|
|
|
|
import argparse
|
|
import ipaddress
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from cryptography import x509
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from cryptography.x509.oid import ExtensionOID, NameOID
|
|
except ImportError:
|
|
print("Error: cryptography library not found.")
|
|
print("Install it with: uv pip install cryptography")
|
|
sys.exit(1)
|
|
|
|
|
|
def generate_private_key(key_size: int = 4096) -> rsa.RSAPrivateKey:
|
|
"""Generate an RSA private key."""
|
|
return rsa.generate_private_key(
|
|
public_exponent=65537,
|
|
key_size=key_size,
|
|
backend=default_backend(),
|
|
)
|
|
|
|
|
|
def generate_ca_certificate(
|
|
private_key: rsa.RSAPrivateKey,
|
|
common_name: str = "Testing CA",
|
|
days_valid: int = 3650,
|
|
) -> x509.Certificate:
|
|
"""Generate a self-signed CA certificate."""
|
|
subject = issuer = x509.Name(
|
|
[
|
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
|
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Development"),
|
|
x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"),
|
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Testing Organization"),
|
|
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
|
]
|
|
)
|
|
|
|
certificate = (
|
|
x509.CertificateBuilder()
|
|
.subject_name(subject)
|
|
.issuer_name(issuer)
|
|
.public_key(private_key.public_key())
|
|
.serial_number(x509.random_serial_number())
|
|
.not_valid_before(datetime.utcnow())
|
|
.not_valid_after(datetime.utcnow() + timedelta(days=days_valid))
|
|
.add_extension(
|
|
x509.BasicConstraints(ca=True, path_length=None),
|
|
critical=True,
|
|
)
|
|
.add_extension(
|
|
x509.KeyUsage(
|
|
digital_signature=True,
|
|
key_cert_sign=True,
|
|
crl_sign=True,
|
|
key_encipherment=False,
|
|
content_commitment=False,
|
|
data_encipherment=False,
|
|
key_agreement=False,
|
|
encipher_only=False,
|
|
decipher_only=False,
|
|
),
|
|
critical=True,
|
|
)
|
|
.add_extension(
|
|
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
|
critical=False,
|
|
)
|
|
.sign(private_key, hashes.SHA256(), default_backend())
|
|
)
|
|
|
|
return certificate
|
|
|
|
|
|
def generate_server_certificate(
|
|
private_key: rsa.RSAPrivateKey,
|
|
ca_cert: x509.Certificate,
|
|
ca_key: rsa.RSAPrivateKey,
|
|
hostname: str = "ldap.testing.local",
|
|
san_list: list[str] = None,
|
|
days_valid: int = 365,
|
|
) -> x509.Certificate:
|
|
"""Generate a server certificate signed by the CA."""
|
|
if san_list is None:
|
|
san_list = [
|
|
"ldap.testing.local",
|
|
"localhost",
|
|
]
|
|
|
|
subject = x509.Name(
|
|
[
|
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
|
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Development"),
|
|
x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"),
|
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Testing Organization"),
|
|
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
|
|
]
|
|
)
|
|
|
|
# Build Subject Alternative Names
|
|
san_entries = []
|
|
for name in san_list:
|
|
san_entries.append(x509.DNSName(name))
|
|
# Add IP addresses
|
|
san_entries.append(x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")))
|
|
|
|
certificate = (
|
|
x509.CertificateBuilder()
|
|
.subject_name(subject)
|
|
.issuer_name(ca_cert.subject)
|
|
.public_key(private_key.public_key())
|
|
.serial_number(x509.random_serial_number())
|
|
.not_valid_before(datetime.utcnow())
|
|
.not_valid_after(datetime.utcnow() + timedelta(days=days_valid))
|
|
.add_extension(
|
|
x509.SubjectAlternativeName(san_entries),
|
|
critical=False,
|
|
)
|
|
.add_extension(
|
|
x509.BasicConstraints(ca=False, path_length=None),
|
|
critical=True,
|
|
)
|
|
.add_extension(
|
|
x509.KeyUsage(
|
|
digital_signature=True,
|
|
key_encipherment=True,
|
|
key_cert_sign=False,
|
|
crl_sign=False,
|
|
content_commitment=False,
|
|
data_encipherment=False,
|
|
key_agreement=False,
|
|
encipher_only=False,
|
|
decipher_only=False,
|
|
),
|
|
critical=True,
|
|
)
|
|
.add_extension(
|
|
x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]),
|
|
critical=False,
|
|
)
|
|
.add_extension(
|
|
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
|
critical=False,
|
|
)
|
|
.add_extension(
|
|
x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
|
|
critical=False,
|
|
)
|
|
.sign(ca_key, hashes.SHA256(), default_backend())
|
|
)
|
|
|
|
return certificate
|
|
|
|
|
|
def save_private_key(key: rsa.RSAPrivateKey, filepath: Path) -> None:
|
|
"""Save a private key to a file."""
|
|
pem = key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
encryption_algorithm=serialization.NoEncryption(),
|
|
)
|
|
filepath.write_bytes(pem)
|
|
filepath.chmod(0o600) # Set restrictive permissions
|
|
print(f"✓ Private key saved: {filepath}")
|
|
|
|
|
|
def save_certificate(cert: x509.Certificate, filepath: Path) -> None:
|
|
"""Save a certificate to a file."""
|
|
pem = cert.public_bytes(serialization.Encoding.PEM)
|
|
filepath.write_bytes(pem)
|
|
filepath.chmod(0o644)
|
|
print(f"✓ Certificate saved: {filepath}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate SSL/TLS certificates for LDAP development server"
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
type=Path,
|
|
default=Path(__file__).parent.parent / "certs",
|
|
help="Output directory for certificates (default: ./certs)",
|
|
)
|
|
parser.add_argument(
|
|
"--hostname",
|
|
default="ldap.testing.local",
|
|
help="Server hostname (default: ldap.testing.local)",
|
|
)
|
|
parser.add_argument(
|
|
"--san",
|
|
action="append",
|
|
help="Additional Subject Alternative Names (can be used multiple times)",
|
|
)
|
|
parser.add_argument(
|
|
"--force",
|
|
action="store_true",
|
|
help="Overwrite existing certificates",
|
|
)
|
|
parser.add_argument(
|
|
"--ca-days",
|
|
type=int,
|
|
default=3650,
|
|
help="CA certificate validity in days (default: 3650)",
|
|
)
|
|
parser.add_argument(
|
|
"--server-days",
|
|
type=int,
|
|
default=365,
|
|
help="Server certificate validity in days (default: 365)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Ensure output directory exists
|
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Define file paths
|
|
ca_key_path = args.output_dir / "ca.key"
|
|
ca_cert_path = args.output_dir / "ca.crt"
|
|
server_key_path = args.output_dir / "server.key"
|
|
server_cert_path = args.output_dir / "server.crt"
|
|
|
|
# Check if certificates already exist
|
|
if not args.force:
|
|
existing = [
|
|
p for p in [ca_cert_path, server_cert_path, server_key_path] if p.exists()
|
|
]
|
|
if existing:
|
|
print("Error: The following certificate files already exist:")
|
|
for p in existing:
|
|
print(f" - {p}")
|
|
print("\nUse --force to overwrite existing certificates")
|
|
sys.exit(1)
|
|
|
|
print("Generating certificates for LDAP server...")
|
|
print(f"Hostname: {args.hostname}")
|
|
print(f"Output directory: {args.output_dir}")
|
|
print()
|
|
|
|
# Generate CA
|
|
print("Step 1: Generating CA certificate...")
|
|
ca_key = generate_private_key()
|
|
ca_cert = generate_ca_certificate(ca_key, days_valid=args.ca_days)
|
|
save_private_key(ca_key, ca_key_path)
|
|
save_certificate(ca_cert, ca_cert_path)
|
|
print()
|
|
|
|
# Generate Server Certificate
|
|
print("Step 2: Generating server certificate...")
|
|
server_key = generate_private_key()
|
|
|
|
san_list = [args.hostname, "localhost"]
|
|
if args.san:
|
|
san_list.extend(args.san)
|
|
|
|
server_cert = generate_server_certificate(
|
|
server_key,
|
|
ca_cert,
|
|
ca_key,
|
|
hostname=args.hostname,
|
|
san_list=san_list,
|
|
days_valid=args.server_days,
|
|
)
|
|
save_private_key(server_key, server_key_path)
|
|
save_certificate(server_cert, server_cert_path)
|
|
print()
|
|
|
|
print("✅ Certificate generation complete!")
|
|
print()
|
|
print("Generated files:")
|
|
print(f" - CA Certificate: {ca_cert_path}")
|
|
print(f" - CA Private Key: {ca_key_path} (keep secure!)")
|
|
print(f" - Server Certificate: {server_cert_path}")
|
|
print(f" - Server Private Key: {server_key_path} (keep secure!)")
|
|
print()
|
|
print("Next steps:")
|
|
print(" 1. Keep the CA private key secure (you can delete it if not needed)")
|
|
print(" 2. Start the LDAP server: docker-compose up -d")
|
|
print(" 3. Test the connection: ldapsearch -H ldaps://localhost:636 -x")
|
|
print()
|
|
print("Note: These certificates are for DEVELOPMENT ONLY!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|