Created
March 19, 2026 10:04
-
-
Save yakirn/1336c9ea85b0bb8761bb1862ae25795a to your computer and use it in GitHub Desktop.
Self signed certificate utility
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| gencert.py — Self-signed certificate generator | |
| Examples: | |
| python gencert.py # 7-day cert (default) | |
| python gencert.py --days 2 # 2-day cert | |
| python gencert.py --hours 5 # 5-hour cert | |
| python gencert.py --cn myapp.local # custom Common Name | |
| Requires: pip install cryptography | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| from datetime import datetime, timedelta, timezone | |
| try: | |
| from cryptography import x509 | |
| from cryptography.hazmat.primitives import hashes, serialization | |
| from cryptography.hazmat.primitives.asymmetric import rsa | |
| from cryptography.x509.oid import NameOID | |
| except ImportError: | |
| print( | |
| "Missing dependency. Install it with:\n pip install cryptography", | |
| file=sys.stderr, | |
| ) | |
| sys.exit(1) | |
| def _duration_label(days: int, hours: int) -> str: | |
| if hours is not None: | |
| return f"{hours}hour" if hours == 1 else f"{hours}hours" | |
| return f"{days}day" if days == 1 else f"{days}days" | |
| def generate(cn: str, not_after: datetime): | |
| """Generate an RSA-2048 self-signed cert. Returns (key_pem, cert_pem, cert_obj).""" | |
| key = rsa.generate_private_key(public_exponent=65537, key_size=2048) | |
| now = datetime.now(timezone.utc) | |
| name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]) | |
| cert = ( | |
| x509.CertificateBuilder() | |
| .subject_name(name) | |
| .issuer_name(name) | |
| .public_key(key.public_key()) | |
| .serial_number(x509.random_serial_number()) | |
| .not_valid_before(now) | |
| .not_valid_after(not_after) | |
| .add_extension( | |
| x509.SubjectAlternativeName([x509.DNSName(cn)]), | |
| critical=False, | |
| ) | |
| .sign(key, hashes.SHA256()) | |
| ) | |
| key_pem = key.private_bytes( | |
| encoding=serialization.Encoding.PEM, | |
| format=serialization.PrivateFormat.TraditionalOpenSSL, | |
| encryption_algorithm=serialization.NoEncryption(), | |
| ) | |
| cert_pem = cert.public_bytes(serialization.Encoding.PEM) | |
| return key_pem, cert_pem, cert | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Generate a self-signed TLS certificate.", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=__doc__, | |
| ) | |
| group = parser.add_mutually_exclusive_group() | |
| group.add_argument("--days", type=int, help="Validity period in days (default: 7)") | |
| group.add_argument("--hours", type=int, help="Validity period in hours") | |
| parser.add_argument( | |
| "--cn", default="localhost", help="Common Name / hostname (default: localhost)" | |
| ) | |
| args = parser.parse_args() | |
| if args.days is not None and args.days <= 0: | |
| parser.error("--days must be a positive integer") | |
| if args.hours is not None and args.hours <= 0: | |
| parser.error("--hours must be a positive integer") | |
| now = datetime.now(timezone.utc) | |
| if args.hours is not None: | |
| not_after = now + timedelta(hours=args.hours) | |
| label = _duration_label(None, args.hours) | |
| else: | |
| days = args.days if args.days is not None else 7 | |
| not_after = now + timedelta(days=days) | |
| label = _duration_label(days, None) | |
| # Build filename with human-readable expiry (local time) | |
| expiry_local = not_after.astimezone() | |
| expiry_ts = expiry_local.strftime("%Y-%m-%d_%H%M%S") | |
| stem = f"selfsigned_{label}_expires_{expiry_ts}" | |
| out_dir = os.getcwd() | |
| key_path = os.path.join(out_dir, f"{stem}.key") | |
| cert_path = os.path.join(out_dir, f"{stem}.crt") | |
| key_pem, cert_pem, cert_obj = generate(args.cn, not_after) | |
| with open(key_path, "wb") as f: | |
| f.write(key_pem) | |
| with open(cert_path, "wb") as f: | |
| f.write(cert_pem) | |
| expiry_fmt = expiry_local.strftime("%Y-%m-%d %H:%M:%S %Z") | |
| print(f"Common Name : {args.cn}") | |
| print(f"Valid for : {label}") | |
| print(f"Expires : {expiry_fmt}") | |
| print(f"Certificate : {cert_path}") | |
| print(f"Private key : {key_path}") | |
| print() | |
| print("─" * 64) | |
| print(cert_pem.decode(), end="") | |
| print("─" * 64) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment