mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 14:14:56 +03:00
456 lines
17 KiB
Python
456 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Certificate Authority Builder using Python cryptography library.
|
|
|
|
Builds a complete two-tier CA hierarchy (Root CA + Intermediate CA)
|
|
and issues server/client certificates programmatically.
|
|
|
|
Requirements:
|
|
pip install cryptography
|
|
|
|
Usage:
|
|
python process.py build-ca --output ./pki --org "My Organization"
|
|
python process.py issue-cert --ca-dir ./pki --domain server.example.com --type server
|
|
python process.py revoke --ca-dir ./pki --serial 1001
|
|
python process.py generate-crl --ca-dir ./pki
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import argparse
|
|
import logging
|
|
import datetime
|
|
from pathlib import Path
|
|
from typing import Dict, Optional, List
|
|
|
|
from cryptography import x509
|
|
from cryptography.x509.oid import NameOID, ExtensionOID
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa, ec
|
|
from cryptography.hazmat.backends import default_backend
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def generate_key(key_type: str = "rsa", key_size: int = 4096):
|
|
"""Generate a private key."""
|
|
if key_type == "ecdsa":
|
|
return ec.generate_private_key(ec.SECP384R1(), default_backend())
|
|
return rsa.generate_private_key(
|
|
public_exponent=65537, key_size=key_size, backend=default_backend()
|
|
)
|
|
|
|
|
|
def save_key(key, path: Path, passphrase: Optional[str] = None):
|
|
"""Save a private key to PEM file."""
|
|
if passphrase:
|
|
enc = serialization.BestAvailableEncryption(passphrase.encode())
|
|
else:
|
|
enc = serialization.NoEncryption()
|
|
path.write_bytes(key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.PKCS8,
|
|
encryption_algorithm=enc,
|
|
))
|
|
|
|
|
|
def save_cert(cert, path: Path):
|
|
"""Save a certificate to PEM file."""
|
|
path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
|
|
|
|
|
|
def build_root_ca(
|
|
output_dir: Path,
|
|
organization: str,
|
|
country: str = "US",
|
|
validity_years: int = 20,
|
|
) -> Dict:
|
|
"""Build a Root CA with self-signed certificate."""
|
|
ca_dir = output_dir / "root-ca"
|
|
ca_dir.mkdir(parents=True, exist_ok=True)
|
|
(ca_dir / "certs").mkdir(exist_ok=True)
|
|
(ca_dir / "private").mkdir(exist_ok=True)
|
|
|
|
key = generate_key("rsa", 4096)
|
|
save_key(key, ca_dir / "private" / "root-ca.key")
|
|
|
|
subject = x509.Name([
|
|
x509.NameAttribute(NameOID.COUNTRY_NAME, country),
|
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization),
|
|
x509.NameAttribute(NameOID.COMMON_NAME, f"{organization} Root CA"),
|
|
])
|
|
|
|
now = datetime.datetime.utcnow()
|
|
cert = (
|
|
x509.CertificateBuilder()
|
|
.subject_name(subject)
|
|
.issuer_name(subject)
|
|
.public_key(key.public_key())
|
|
.serial_number(x509.random_serial_number())
|
|
.not_valid_before(now)
|
|
.not_valid_after(now + datetime.timedelta(days=validity_years * 365))
|
|
.add_extension(
|
|
x509.BasicConstraints(ca=True, path_length=1), critical=True
|
|
)
|
|
.add_extension(
|
|
x509.KeyUsage(
|
|
digital_signature=True, key_cert_sign=True, crl_sign=True,
|
|
content_commitment=False, key_encipherment=False,
|
|
data_encipherment=False, key_agreement=False,
|
|
encipher_only=False, decipher_only=False,
|
|
),
|
|
critical=True,
|
|
)
|
|
.add_extension(
|
|
x509.SubjectKeyIdentifier.from_public_key(key.public_key()),
|
|
critical=False,
|
|
)
|
|
.sign(key, hashes.SHA384(), default_backend())
|
|
)
|
|
|
|
save_cert(cert, ca_dir / "certs" / "root-ca.crt")
|
|
|
|
# Initialize serial number tracker
|
|
(ca_dir / "serial.json").write_text(json.dumps({"next_serial": 1001}))
|
|
(ca_dir / "index.json").write_text(json.dumps({"certificates": []}))
|
|
|
|
logger.info(f"Root CA created: {ca_dir}")
|
|
return {
|
|
"type": "root-ca",
|
|
"subject": subject.rfc4514_string(),
|
|
"key_path": str(ca_dir / "private" / "root-ca.key"),
|
|
"cert_path": str(ca_dir / "certs" / "root-ca.crt"),
|
|
"serial_number": hex(cert.serial_number),
|
|
"valid_until": cert.not_valid_after_utc.isoformat(),
|
|
}
|
|
|
|
|
|
def build_intermediate_ca(
|
|
output_dir: Path,
|
|
organization: str,
|
|
country: str = "US",
|
|
validity_years: int = 10,
|
|
) -> Dict:
|
|
"""Build an Intermediate CA signed by the Root CA."""
|
|
root_dir = output_dir / "root-ca"
|
|
int_dir = output_dir / "intermediate-ca"
|
|
int_dir.mkdir(parents=True, exist_ok=True)
|
|
(int_dir / "certs").mkdir(exist_ok=True)
|
|
(int_dir / "private").mkdir(exist_ok=True)
|
|
|
|
root_key_data = (root_dir / "private" / "root-ca.key").read_bytes()
|
|
root_key = serialization.load_pem_private_key(root_key_data, password=None, backend=default_backend())
|
|
|
|
root_cert_data = (root_dir / "certs" / "root-ca.crt").read_bytes()
|
|
root_cert = x509.load_pem_x509_certificate(root_cert_data, default_backend())
|
|
|
|
int_key = generate_key("rsa", 4096)
|
|
save_key(int_key, int_dir / "private" / "intermediate-ca.key")
|
|
|
|
subject = x509.Name([
|
|
x509.NameAttribute(NameOID.COUNTRY_NAME, country),
|
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization),
|
|
x509.NameAttribute(NameOID.COMMON_NAME, f"{organization} Intermediate CA"),
|
|
])
|
|
|
|
now = datetime.datetime.utcnow()
|
|
cert = (
|
|
x509.CertificateBuilder()
|
|
.subject_name(subject)
|
|
.issuer_name(root_cert.subject)
|
|
.public_key(int_key.public_key())
|
|
.serial_number(x509.random_serial_number())
|
|
.not_valid_before(now)
|
|
.not_valid_after(now + datetime.timedelta(days=validity_years * 365))
|
|
.add_extension(
|
|
x509.BasicConstraints(ca=True, path_length=0), critical=True
|
|
)
|
|
.add_extension(
|
|
x509.KeyUsage(
|
|
digital_signature=True, key_cert_sign=True, crl_sign=True,
|
|
content_commitment=False, key_encipherment=False,
|
|
data_encipherment=False, key_agreement=False,
|
|
encipher_only=False, decipher_only=False,
|
|
),
|
|
critical=True,
|
|
)
|
|
.add_extension(
|
|
x509.SubjectKeyIdentifier.from_public_key(int_key.public_key()),
|
|
critical=False,
|
|
)
|
|
.add_extension(
|
|
x509.AuthorityKeyIdentifier.from_issuer_public_key(root_key.public_key()),
|
|
critical=False,
|
|
)
|
|
.sign(root_key, hashes.SHA384(), default_backend())
|
|
)
|
|
|
|
save_cert(cert, int_dir / "certs" / "intermediate-ca.crt")
|
|
|
|
# Create chain file
|
|
chain = cert.public_bytes(serialization.Encoding.PEM) + root_cert.public_bytes(serialization.Encoding.PEM)
|
|
(int_dir / "certs" / "ca-chain.crt").write_bytes(chain)
|
|
|
|
# Initialize serial and index
|
|
(int_dir / "serial.json").write_text(json.dumps({"next_serial": 2001}))
|
|
(int_dir / "index.json").write_text(json.dumps({"certificates": [], "revoked": []}))
|
|
|
|
logger.info(f"Intermediate CA created: {int_dir}")
|
|
return {
|
|
"type": "intermediate-ca",
|
|
"subject": subject.rfc4514_string(),
|
|
"key_path": str(int_dir / "private" / "intermediate-ca.key"),
|
|
"cert_path": str(int_dir / "certs" / "intermediate-ca.crt"),
|
|
"chain_path": str(int_dir / "certs" / "ca-chain.crt"),
|
|
"serial_number": hex(cert.serial_number),
|
|
"valid_until": cert.not_valid_after_utc.isoformat(),
|
|
}
|
|
|
|
|
|
def issue_certificate(
|
|
ca_dir: Path,
|
|
domain: str,
|
|
cert_type: str = "server",
|
|
validity_days: int = 365,
|
|
san_domains: Optional[List[str]] = None,
|
|
) -> Dict:
|
|
"""Issue a certificate from the Intermediate CA."""
|
|
int_dir = ca_dir / "intermediate-ca"
|
|
|
|
int_key_data = (int_dir / "private" / "intermediate-ca.key").read_bytes()
|
|
int_key = serialization.load_pem_private_key(int_key_data, password=None, backend=default_backend())
|
|
|
|
int_cert_data = (int_dir / "certs" / "intermediate-ca.crt").read_bytes()
|
|
int_cert = x509.load_pem_x509_certificate(int_cert_data, default_backend())
|
|
|
|
# Read and update serial
|
|
serial_data = json.loads((int_dir / "serial.json").read_text())
|
|
serial_num = serial_data["next_serial"]
|
|
serial_data["next_serial"] = serial_num + 1
|
|
(int_dir / "serial.json").write_text(json.dumps(serial_data))
|
|
|
|
# Generate end-entity key
|
|
ee_key = generate_key("ecdsa")
|
|
subject = x509.Name([
|
|
x509.NameAttribute(NameOID.COMMON_NAME, domain),
|
|
])
|
|
|
|
san_list = [x509.DNSName(domain)]
|
|
if san_domains:
|
|
for d in san_domains:
|
|
san_list.append(x509.DNSName(d))
|
|
|
|
if cert_type == "server":
|
|
eku = x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.SERVER_AUTH])
|
|
elif cert_type == "client":
|
|
eku = x509.ExtendedKeyUsage([x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH])
|
|
else:
|
|
eku = x509.ExtendedKeyUsage([
|
|
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
|
|
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH,
|
|
])
|
|
|
|
now = datetime.datetime.utcnow()
|
|
cert = (
|
|
x509.CertificateBuilder()
|
|
.subject_name(subject)
|
|
.issuer_name(int_cert.subject)
|
|
.public_key(ee_key.public_key())
|
|
.serial_number(serial_num)
|
|
.not_valid_before(now)
|
|
.not_valid_after(now + datetime.timedelta(days=validity_days))
|
|
.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(eku, critical=False)
|
|
.add_extension(x509.SubjectAlternativeName(san_list), critical=False)
|
|
.add_extension(
|
|
x509.AuthorityKeyIdentifier.from_issuer_public_key(int_key.public_key()),
|
|
critical=False,
|
|
)
|
|
.sign(int_key, hashes.SHA256(), default_backend())
|
|
)
|
|
|
|
certs_dir = int_dir / "certs" / "issued"
|
|
certs_dir.mkdir(exist_ok=True)
|
|
|
|
save_key(ee_key, certs_dir / f"{domain}.key")
|
|
save_cert(cert, certs_dir / f"{domain}.crt")
|
|
|
|
# Update index
|
|
index_data = json.loads((int_dir / "index.json").read_text())
|
|
index_data["certificates"].append({
|
|
"serial": serial_num,
|
|
"domain": domain,
|
|
"type": cert_type,
|
|
"issued": now.isoformat(),
|
|
"expires": (now + datetime.timedelta(days=validity_days)).isoformat(),
|
|
"status": "valid",
|
|
})
|
|
(int_dir / "index.json").write_text(json.dumps(index_data, indent=2))
|
|
|
|
logger.info(f"Issued {cert_type} certificate for {domain} (serial: {serial_num})")
|
|
return {
|
|
"domain": domain,
|
|
"serial": serial_num,
|
|
"type": cert_type,
|
|
"key_path": str(certs_dir / f"{domain}.key"),
|
|
"cert_path": str(certs_dir / f"{domain}.crt"),
|
|
"chain_path": str(int_dir / "certs" / "ca-chain.crt"),
|
|
"valid_until": cert.not_valid_after_utc.isoformat(),
|
|
}
|
|
|
|
|
|
def revoke_certificate(ca_dir: Path, serial: int, reason: str = "unspecified") -> Dict:
|
|
"""Revoke a certificate by serial number."""
|
|
int_dir = ca_dir / "intermediate-ca"
|
|
index_data = json.loads((int_dir / "index.json").read_text())
|
|
|
|
reason_map = {
|
|
"unspecified": x509.ReasonFlags.unspecified,
|
|
"key_compromise": x509.ReasonFlags.key_compromise,
|
|
"ca_compromise": x509.ReasonFlags.ca_compromise,
|
|
"affiliation_changed": x509.ReasonFlags.affiliation_changed,
|
|
"superseded": x509.ReasonFlags.superseded,
|
|
"cessation_of_operation": x509.ReasonFlags.cessation_of_operation,
|
|
}
|
|
|
|
found = False
|
|
for cert_entry in index_data["certificates"]:
|
|
if cert_entry["serial"] == serial:
|
|
cert_entry["status"] = "revoked"
|
|
cert_entry["revoked_at"] = datetime.datetime.utcnow().isoformat()
|
|
cert_entry["revocation_reason"] = reason
|
|
found = True
|
|
break
|
|
|
|
if not found:
|
|
raise ValueError(f"Certificate with serial {serial} not found")
|
|
|
|
if "revoked" not in index_data:
|
|
index_data["revoked"] = []
|
|
|
|
index_data["revoked"].append({
|
|
"serial": serial,
|
|
"revoked_at": datetime.datetime.utcnow().isoformat(),
|
|
"reason": reason,
|
|
})
|
|
|
|
(int_dir / "index.json").write_text(json.dumps(index_data, indent=2))
|
|
logger.info(f"Revoked certificate serial {serial} (reason: {reason})")
|
|
|
|
return {"serial": serial, "status": "revoked", "reason": reason}
|
|
|
|
|
|
def generate_crl(ca_dir: Path, validity_days: int = 30) -> Dict:
|
|
"""Generate a Certificate Revocation List."""
|
|
int_dir = ca_dir / "intermediate-ca"
|
|
|
|
int_key_data = (int_dir / "private" / "intermediate-ca.key").read_bytes()
|
|
int_key = serialization.load_pem_private_key(int_key_data, password=None, backend=default_backend())
|
|
|
|
int_cert_data = (int_dir / "certs" / "intermediate-ca.crt").read_bytes()
|
|
int_cert = x509.load_pem_x509_certificate(int_cert_data, default_backend())
|
|
|
|
index_data = json.loads((int_dir / "index.json").read_text())
|
|
|
|
now = datetime.datetime.utcnow()
|
|
builder = x509.CertificateRevocationListBuilder()
|
|
builder = builder.issuer_name(int_cert.subject)
|
|
builder = builder.last_update(now)
|
|
builder = builder.next_update(now + datetime.timedelta(days=validity_days))
|
|
|
|
reason_map = {
|
|
"unspecified": x509.ReasonFlags.unspecified,
|
|
"key_compromise": x509.ReasonFlags.key_compromise,
|
|
"ca_compromise": x509.ReasonFlags.ca_compromise,
|
|
"superseded": x509.ReasonFlags.superseded,
|
|
"cessation_of_operation": x509.ReasonFlags.cessation_of_operation,
|
|
}
|
|
|
|
for entry in index_data.get("revoked", []):
|
|
revoked_cert = (
|
|
x509.RevokedCertificateBuilder()
|
|
.serial_number(entry["serial"])
|
|
.revocation_date(datetime.datetime.fromisoformat(entry["revoked_at"]))
|
|
.add_extension(
|
|
x509.CRLReason(reason_map.get(entry.get("reason", "unspecified"), x509.ReasonFlags.unspecified)),
|
|
critical=False,
|
|
)
|
|
.build()
|
|
)
|
|
builder = builder.add_revoked_certificate(revoked_cert)
|
|
|
|
crl = builder.sign(int_key, hashes.SHA256(), default_backend())
|
|
|
|
crl_path = int_dir / "crl" / "intermediate.crl"
|
|
crl_path.parent.mkdir(exist_ok=True)
|
|
crl_path.write_bytes(crl.public_bytes(serialization.Encoding.PEM))
|
|
|
|
logger.info(f"Generated CRL with {len(index_data.get('revoked', []))} revoked certificates")
|
|
return {
|
|
"crl_path": str(crl_path),
|
|
"revoked_count": len(index_data.get("revoked", [])),
|
|
"next_update": (now + datetime.timedelta(days=validity_days)).isoformat(),
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Certificate Authority Builder")
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
build = subparsers.add_parser("build-ca", help="Build complete CA hierarchy")
|
|
build.add_argument("--output", "-o", default="./pki", help="Output directory")
|
|
build.add_argument("--org", required=True, help="Organization name")
|
|
build.add_argument("--country", default="US", help="Country code")
|
|
|
|
issue = subparsers.add_parser("issue-cert", help="Issue a certificate")
|
|
issue.add_argument("--ca-dir", required=True, help="CA directory")
|
|
issue.add_argument("--domain", required=True, help="Domain name")
|
|
issue.add_argument("--type", choices=["server", "client", "both"], default="server")
|
|
issue.add_argument("--days", type=int, default=365, help="Validity days")
|
|
issue.add_argument("--san", nargs="*", help="Additional SAN domains")
|
|
|
|
rev = subparsers.add_parser("revoke", help="Revoke a certificate")
|
|
rev.add_argument("--ca-dir", required=True, help="CA directory")
|
|
rev.add_argument("--serial", type=int, required=True, help="Serial number")
|
|
rev.add_argument("--reason", default="unspecified", help="Revocation reason")
|
|
|
|
crl = subparsers.add_parser("generate-crl", help="Generate CRL")
|
|
crl.add_argument("--ca-dir", required=True, help="CA directory")
|
|
crl.add_argument("--days", type=int, default=30, help="CRL validity days")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "build-ca":
|
|
output = Path(args.output)
|
|
root = build_root_ca(output, args.org, args.country)
|
|
intermediate = build_intermediate_ca(output, args.org, args.country)
|
|
print(json.dumps({"root_ca": root, "intermediate_ca": intermediate}, indent=2))
|
|
elif args.command == "issue-cert":
|
|
result = issue_certificate(
|
|
Path(args.ca_dir), args.domain, args.type, args.days, args.san
|
|
)
|
|
print(json.dumps(result, indent=2))
|
|
elif args.command == "revoke":
|
|
result = revoke_certificate(Path(args.ca_dir), args.serial, args.reason)
|
|
print(json.dumps(result, indent=2))
|
|
elif args.command == "generate-crl":
|
|
result = generate_crl(Path(args.ca_dir), args.days)
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|