Last active
March 5, 2026 20:21
-
-
Save sbealer/0744fc6680a71f6fa0a2414b645fa66d to your computer and use it in GitHub Desktop.
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
| # RUN THIS LIKE: | |
| # python gen_pem_cert.py --for_process "sharepoint_etl" --password "MyStrongPass123!" --out-dir ./certs | |
| """ | |
| Generate a password-protected private key (PEM) + a self-signed certificate (PEM). | |
| Outputs: | |
| - private_key_encrypted.pem | |
| - certificate.pem | |
| Install dependency: | |
| pip install cryptography | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import datetime as dt | |
| from pathlib import Path | |
| from cryptography import x509 | |
| from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID | |
| from cryptography.hazmat.primitives import hashes, serialization | |
| from cryptography.hazmat.primitives.asymmetric import rsa | |
| CERT_VALID_YEARS = 100 # effectively "never expires" | |
| def generate_key_rsa(key_size: int = 2048) -> rsa.RSAPrivateKey: | |
| return rsa.generate_private_key(public_exponent=65537, key_size=key_size) | |
| def build_self_signed_cert( | |
| private_key: rsa.RSAPrivateKey, | |
| for_process: str, | |
| ) -> x509.Certificate: | |
| name = x509.Name( | |
| [ | |
| x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), | |
| x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "CA"), | |
| x509.NameAttribute(NameOID.LOCALITY_NAME, "Chico"), | |
| x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Example Org"), | |
| x509.NameAttribute(NameOID.COMMON_NAME, for_process), | |
| ] | |
| ) | |
| now = dt.datetime.utcnow() | |
| not_before = now - dt.timedelta(minutes=5) | |
| # "Never expires" isn't allowed in X.509, so we set it far in the future | |
| not_after = now + dt.timedelta(days=365 * CERT_VALID_YEARS) | |
| builder = ( | |
| x509.CertificateBuilder() | |
| .subject_name(name) | |
| .issuer_name(name) | |
| .public_key(private_key.public_key()) | |
| .serial_number(x509.random_serial_number()) | |
| .not_valid_before(not_before) | |
| .not_valid_after(not_after) | |
| .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True) | |
| .add_extension( | |
| x509.KeyUsage( | |
| digital_signature=True, | |
| key_encipherment=True, | |
| content_commitment=False, | |
| data_encipherment=False, | |
| key_agreement=False, | |
| key_cert_sign=False, | |
| crl_sign=False, | |
| encipher_only=False, | |
| decipher_only=False, | |
| ), | |
| critical=True, | |
| ) | |
| .add_extension( | |
| x509.ExtendedKeyUsage( | |
| [ExtendedKeyUsageOID.CLIENT_AUTH] | |
| ), | |
| critical=False, | |
| ) | |
| .add_extension( | |
| x509.SubjectAlternativeName([x509.DNSName(for_process)]), | |
| critical=False, | |
| ) | |
| ) | |
| return builder.sign(private_key=private_key, algorithm=hashes.SHA256()) | |
| def write_encrypted_private_key_pem( | |
| private_key: rsa.RSAPrivateKey, | |
| password: str, | |
| path: Path, | |
| ) -> None: | |
| pem_bytes = private_key.private_bytes( | |
| encoding=serialization.Encoding.PEM, | |
| format=serialization.PrivateFormat.PKCS8, | |
| encryption_algorithm=serialization.BestAvailableEncryption(password.encode("utf-8")), | |
| ) | |
| path.write_bytes(pem_bytes) | |
| def write_certificate_pem(cert: x509.Certificate, path: Path) -> None: | |
| path.write_bytes(cert.public_bytes(serialization.Encoding.PEM)) | |
| def main() -> int: | |
| parser = argparse.ArgumentParser( | |
| description="Generate encrypted PEM private key + self-signed cert." | |
| ) | |
| parser.add_argument( | |
| "--for_process", | |
| required=True, | |
| help="Identifier for what this cert/key is for (used as CN + SAN).", | |
| ) | |
| parser.add_argument("--key-size", type=int, default=2048, help="RSA key size (2048/3072/4096).") | |
| parser.add_argument("--password", required=True, help="Password to encrypt the private key PEM.") | |
| parser.add_argument("--out-dir", default=".", help="Output directory.") | |
| parser.add_argument("--key-name", default="private_key_encrypted.pem", help="Encrypted private key PEM filename.") | |
| parser.add_argument("--cert-name", default="certificate.pem", help="Certificate PEM filename.") | |
| args = parser.parse_args() | |
| out_dir = Path(args.out_dir).expanduser().resolve() | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| key_path = out_dir / args.key_name | |
| cert_path = out_dir / args.cert_name | |
| private_key = generate_key_rsa(key_size=args.key_size) | |
| cert = build_self_signed_cert(private_key, for_process=args.for_process) | |
| write_encrypted_private_key_pem(private_key, args.password, key_path) | |
| write_certificate_pem(cert, cert_path) | |
| print("Wrote:") | |
| print(f" Encrypted private key: {key_path}") | |
| print(f" Certificate (public): {cert_path}") | |
| print(f"\nCertificate validity: {CERT_VALID_YEARS} years") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment