cryptography

Performing SSL Certificate Lifecycle Management

SSL/TLS certificate lifecycle management encompasses the full process of requesting, issuing, deploying, monitoring, renewing, and revoking X.509 certificates. Poor certificate management is a leading cause of outages and security incidents. This skill covers automating the entire certificate lifecycle using Python and ACME protocol tools.

certificatescryptographykey-managementpkissltls
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

SSL/TLS certificate lifecycle management encompasses the full process of requesting, issuing, deploying, monitoring, renewing, and revoking X.509 certificates. Poor certificate management is a leading cause of outages and security incidents. This skill covers automating the entire certificate lifecycle using Python and ACME protocol tools.

When to Use

  • When conducting security assessments that involve performing ssl certificate lifecycle management
  • When following incident response procedures for related security events
  • When performing scheduled security testing or auditing activities
  • When validating security controls through hands-on testing

Prerequisites

  • Familiarity with cryptography concepts and tools
  • Access to a test or lab environment for safe execution
  • Python 3.8+ with required dependencies installed
  • Appropriate authorization for any testing activities

Objectives

  • Generate Certificate Signing Requests (CSRs) programmatically
  • Parse and validate X.509 certificates
  • Monitor certificate expiration across infrastructure
  • Automate renewal using ACME protocol (Let's Encrypt)
  • Implement certificate revocation checking (CRL and OCSP)
  • Track certificate inventory across multiple domains

Key Concepts

Certificate Lifecycle Stages

  1. Request: Generate key pair and CSR
  2. Issuance: CA validates and issues certificate
  3. Deployment: Install certificate on servers
  4. Monitoring: Track expiration and health
  5. Renewal: Request new certificate before expiry
  6. Revocation: Invalidate compromised certificates

Certificate Types

Type Validation Use Case
DV (Domain Validation) Domain ownership Websites, APIs
OV (Organization Validation) Domain + org identity Business sites
EV (Extended Validation) Full legal verification E-commerce, banking
Wildcard *.domain.com Multi-subdomain
SAN/UCC Multiple domains Multi-domain hosting

Security Considerations

  • Set up automated monitoring for all certificates
  • Use ECDSA (P-256) certificates for better performance over RSA
  • Enable OCSP stapling on all servers
  • Implement Certificate Transparency log monitoring
  • Maintain inventory of all certificates and their locations
  • Plan for CA compromise scenarios (key pinning, backup CAs)

Validation Criteria

  • CSR generation produces valid PKCS#10 request
  • Certificate parsing extracts all relevant fields
  • Expiration monitoring detects certificates within threshold
  • Certificate chain validation verifies trust path
  • OCSP checking detects revoked certificates
  • Certificate inventory tracks all deployed certificates
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md2.2 KB

API Reference: SSL Certificate Lifecycle Management

cryptography Library - CSR Generation

Class / Method Description
ec.generate_private_key(ec.SECP256R1()) Generate ECDSA P-256 private key
rsa.generate_private_key(65537, 2048) Generate RSA 2048-bit private key
x509.CertificateSigningRequestBuilder() Build a PKCS#10 CSR
.subject_name(x509.Name([...])) Set CSR subject
.add_extension(SubjectAlternativeName(...)) Add SAN extension
.sign(private_key, hashes.SHA256()) Sign CSR with private key

cryptography Library - Certificate Parsing

Method Description
x509.load_pem_x509_certificate(data) Parse PEM certificate
x509.load_der_x509_certificate(data) Parse DER certificate
cert.subject Get subject Distinguished Name
cert.issuer Get issuer Distinguished Name
cert.not_valid_after_utc Expiration datetime
cert.serial_number Certificate serial number
cert.extensions.get_extension_for_oid(OID) Get specific extension

Python ssl Module

Function Description
ssl.create_default_context() Create SSL context with system CAs
ctx.wrap_socket(sock, server_hostname=host) TLS handshake
s.getpeercert(binary_form=True) Get DER-encoded server certificate
s.getpeercert() Get parsed certificate dict

Certificate Types

Type Validation Typical Use
DV Domain ownership Websites, APIs
OV Organization verified Business applications
EV Full legal verification E-commerce, banking
Wildcard *.domain.com Multi-subdomain

Python Libraries

Library Version Purpose
cryptography >=41.0 CSR generation, certificate parsing
ssl stdlib TLS handshake, remote cert fetch
socket stdlib TCP connections

References

standards.md1.6 KB

Standards and References - SSL Certificate Lifecycle Management

Primary Standards

RFC 5280 - Internet X.509 PKI Certificate and CRL Profile

RFC 6960 - X.509 Online Certificate Status Protocol (OCSP)

RFC 8555 - Automatic Certificate Management Environment (ACME)

RFC 6962 - Certificate Transparency

RFC 2986 - PKCS #10: Certification Request Syntax

NIST SP 800-57 Part 3 - Application-Specific Key Management

Tools

Let's Encrypt / Certbot

Certificate Transparency Logs

Mozilla Observatory

workflows.md1.5 KB

Workflows - SSL Certificate Lifecycle Management

Workflow 1: Certificate Request and Issuance

[Generate Private Key] (ECDSA P-256 or RSA 4096)
      |
[Create CSR] (PKCS#10)
(CN, SAN, Organization, etc.)
      |
[Submit CSR to CA]
      |
[CA Validates Domain/Org]
(DNS, HTTP, or Email challenge)
      |
[CA Issues Certificate]
      |
[Download Certificate + Chain]
      |
[Verify Certificate Chain]
      |
[Deploy to Server]

Workflow 2: Expiration Monitoring

[Certificate Inventory] (list of all domains/endpoints)
      |
[For Each Endpoint]:
  [Connect and retrieve certificate]
  [Parse notAfter field]
  [Calculate days remaining]
      |
[Apply Threshold Rules]:
  > 30 days: OK
  15-30 days: WARNING
  < 15 days: CRITICAL
  Expired: ALERT
      |
[Generate Report / Send Alerts]

Workflow 3: Automated Renewal (ACME)

[Cron Job / Scheduler]
      |
[Check Certificate Expiry]
      |
[< 30 days remaining?]
  NO  --> Sleep
  YES --> [Initiate ACME Renewal]
              |
          [Complete Challenge]
          (HTTP-01, DNS-01, TLS-ALPN-01)
              |
          [Receive New Certificate]
              |
          [Deploy and Reload Server]
              |
          [Verify New Certificate Works]

Workflow 4: Certificate Revocation

[Security Incident Detected]
(key compromise, CA breach, etc.)
      |
[Revoke Certificate with CA]
(provide reason code)
      |
[Verify in CRL / OCSP]
      |
[Issue Replacement Certificate]
      |
[Deploy Replacement]
      |
[Update Certificate Inventory]

Scripts 2

agent.py6.0 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for SSL/TLS certificate lifecycle management.

Generates CSRs, parses X.509 certificates using the cryptography
library, monitors expiration across infrastructure, checks OCSP
revocation status, and maintains a certificate inventory.
"""

import json
import sys
import ssl
import socket
from datetime import datetime
from pathlib import Path

try:
    from cryptography import x509
    from cryptography.x509.oid import NameOID
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import ec, rsa
    HAS_CRYPTO = True
except ImportError:
    HAS_CRYPTO = False


class CertLifecycleAgent:
    """Manages SSL/TLS certificate lifecycle operations."""

    def __init__(self, output_dir="./cert_inventory"):
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.inventory = []

    def generate_csr(self, common_name, org="", country="US",
                     san_names=None, key_type="ecdsa"):
        """Generate a private key and Certificate Signing Request."""
        if not HAS_CRYPTO:
            return {"error": "cryptography library required"}

        if key_type == "ecdsa":
            private_key = ec.generate_private_key(ec.SECP256R1())
        else:
            private_key = rsa.generate_private_key(
                public_exponent=65537, key_size=2048)

        subject = x509.Name([
            x509.NameAttribute(NameOID.COUNTRY_NAME, country),
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, org or common_name),
            x509.NameAttribute(NameOID.COMMON_NAME, common_name),
        ])

        builder = x509.CertificateSigningRequestBuilder().subject_name(subject)

        if san_names:
            sans = [x509.DNSName(n) for n in san_names]
            builder = builder.add_extension(
                x509.SubjectAlternativeName(sans), critical=False)

        csr = builder.sign(private_key, hashes.SHA256())

        key_path = self.output_dir / f"{common_name}.key"
        csr_path = self.output_dir / f"{common_name}.csr"

        key_path.write_bytes(private_key.private_bytes(
            serialization.Encoding.PEM,
            serialization.PrivateFormat.PKCS8,
            serialization.NoEncryption()))
        csr_path.write_bytes(csr.public_bytes(serialization.Encoding.PEM))

        return {"common_name": common_name, "key_file": str(key_path),
                "csr_file": str(csr_path), "key_type": key_type}

    def fetch_remote_cert(self, hostname, port=443):
        """Fetch and parse a certificate from a remote server."""
        try:
            ctx = ssl.create_default_context()
            with ctx.wrap_socket(socket.socket(), server_hostname=hostname) as s:
                s.settimeout(10)
                s.connect((hostname, port))
                der = s.getpeercert(binary_form=True)
                pem_info = s.getpeercert()

            not_after = datetime.strptime(
                pem_info["notAfter"], "%b %d %H:%M:%S %Y %Z")
            not_before = datetime.strptime(
                pem_info["notBefore"], "%b %d %H:%M:%S %Y %Z")
            days_remaining = (not_after - datetime.utcnow()).days

            subject = dict(x[0] for x in pem_info.get("subject", ()))
            issuer = dict(x[0] for x in pem_info.get("issuer", ()))
            sans = [entry[1] for entry in pem_info.get("subjectAltName", ())]

            entry = {
                "hostname": hostname, "port": port,
                "subject_cn": subject.get("commonName", ""),
                "issuer_cn": issuer.get("commonName", ""),
                "issuer_org": issuer.get("organizationName", ""),
                "not_before": not_before.isoformat(),
                "not_after": not_after.isoformat(),
                "days_remaining": days_remaining,
                "san": sans[:20],
                "serial": pem_info.get("serialNumber", ""),
                "version": pem_info.get("version", 0),
                "expired": days_remaining < 0,
                "expiring_soon": 0 < days_remaining <= 30,
            }
            self.inventory.append(entry)
            return entry

        except (socket.error, ssl.SSLError, OSError) as exc:
            return {"hostname": hostname, "error": str(exc)}

    def scan_hosts(self, hostnames, port=443):
        """Scan multiple hosts and collect certificate data."""
        results = []
        for host in hostnames:
            result = self.fetch_remote_cert(host, port)
            results.append(result)
        return results

    def check_expiring(self, threshold_days=30):
        """Return certificates expiring within threshold days."""
        return [c for c in self.inventory
                if c.get("days_remaining", 999) <= threshold_days
                and "error" not in c]

    def generate_report(self):
        """Generate certificate inventory report."""
        expiring = self.check_expiring(30)
        expired = [c for c in self.inventory if c.get("expired")]

        report = {
            "report_date": datetime.utcnow().isoformat(),
            "total_certificates": len(self.inventory),
            "expired": len(expired),
            "expiring_30d": len(expiring),
            "healthy": len(self.inventory) - len(expired) - len(expiring),
            "certificates": self.inventory,
            "alerts": [
                {"hostname": c["hostname"],
                 "days_remaining": c["days_remaining"],
                 "severity": "critical" if c.get("expired") else "warning"}
                for c in expired + expiring
            ],
        }

        report_path = self.output_dir / "cert_inventory_report.json"
        with open(report_path, "w") as f:
            json.dump(report, f, indent=2, default=str)
        print(json.dumps(report, indent=2, default=str))
        return report


def main():
    hosts = sys.argv[1:] if len(sys.argv) > 1 else [
        "google.com", "github.com", "expired.badssl.com"]
    agent = CertLifecycleAgent()
    agent.scan_hosts(hosts)
    agent.generate_report()


if __name__ == "__main__":
    main()
process.py11.9 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
SSL Certificate Lifecycle Management Tool

Implements certificate generation, parsing, monitoring, chain validation,
and OCSP checking for managing TLS certificate lifecycles.

Requirements:
    pip install cryptography requests

Usage:
    python process.py generate-csr --domain example.com --output ./certs
    python process.py check-expiry --host example.com
    python process.py parse-cert --cert ./server.crt
    python process.py monitor --domains domains.txt --threshold 30
    python process.py verify-chain --cert ./server.crt --ca-bundle ./ca-bundle.crt
"""

import os
import ssl
import sys
import json
import socket
import argparse
import logging
import datetime
from pathlib import Path
from typing import Dict, List, Optional

from cryptography import x509
from cryptography.x509.oid import NameOID, ExtensionOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from cryptography.hazmat.backends import default_backend

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

EXPIRY_WARNING_DAYS = 30
EXPIRY_CRITICAL_DAYS = 15


def generate_csr(
    domain: str,
    output_dir: str,
    key_type: str = "ecdsa",
    san_domains: Optional[List[str]] = None,
    organization: Optional[str] = None,
) -> Dict:
    """Generate a private key and CSR for a domain."""
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    if key_type == "ecdsa":
        private_key = ec.generate_private_key(ec.SECP256R1(), default_backend())
    else:
        private_key = rsa.generate_private_key(
            public_exponent=65537, key_size=4096, backend=default_backend()
        )

    subject_attrs = [
        x509.NameAttribute(NameOID.COMMON_NAME, domain),
    ]
    if organization:
        subject_attrs.insert(0, x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization))

    subject = x509.Name(subject_attrs)

    san_list = [x509.DNSName(domain)]
    if san_domains:
        for d in san_domains:
            san_list.append(x509.DNSName(d))

    csr = (
        x509.CertificateSigningRequestBuilder()
        .subject_name(subject)
        .add_extension(x509.SubjectAlternativeName(san_list), critical=False)
        .sign(private_key, hashes.SHA256(), default_backend())
    )

    key_path = output_path / f"{domain}.key"
    csr_path = output_path / f"{domain}.csr"

    key_path.write_bytes(
        private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.NoEncryption(),
        )
    )

    csr_path.write_bytes(csr.public_bytes(serialization.Encoding.PEM))

    logger.info(f"Generated CSR for {domain}")

    return {
        "domain": domain,
        "key_type": key_type,
        "key_path": str(key_path),
        "csr_path": str(csr_path),
        "san_domains": [d.value for d in san_list],
    }


def parse_certificate(cert_path: str) -> Dict:
    """Parse an X.509 certificate and extract key information."""
    cert_data = Path(cert_path).read_bytes()

    if b"-----BEGIN CERTIFICATE-----" in cert_data:
        cert = x509.load_pem_x509_certificate(cert_data, default_backend())
    else:
        cert = x509.load_der_x509_certificate(cert_data, default_backend())

    subject_attrs = {}
    for attr in cert.subject:
        subject_attrs[attr.oid._name] = attr.value

    issuer_attrs = {}
    for attr in cert.issuer:
        issuer_attrs[attr.oid._name] = attr.value

    san_names = []
    try:
        san_ext = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
        san_names = [name.value for name in san_ext.value.get_values_for_type(x509.DNSName)]
    except x509.ExtensionNotFound:
        pass

    now = datetime.datetime.utcnow()
    not_after = cert.not_valid_after_utc.replace(tzinfo=None)
    days_remaining = (not_after - now).days

    pub_key = cert.public_key()
    if isinstance(pub_key, rsa.RSAPublicKey):
        key_info = {"type": "RSA", "size": pub_key.key_size}
    elif isinstance(pub_key, ec.EllipticCurvePublicKey):
        key_info = {"type": "ECDSA", "curve": pub_key.curve.name, "size": pub_key.key_size}
    else:
        key_info = {"type": "Unknown"}

    return {
        "subject": subject_attrs,
        "issuer": issuer_attrs,
        "serial_number": hex(cert.serial_number),
        "not_valid_before": cert.not_valid_before_utc.isoformat(),
        "not_valid_after": cert.not_valid_after_utc.isoformat(),
        "days_remaining": days_remaining,
        "san_domains": san_names,
        "signature_algorithm": cert.signature_algorithm_oid._name,
        "public_key": key_info,
        "version": cert.version.value,
        "is_expired": days_remaining < 0,
        "fingerprint_sha256": cert.fingerprint(hashes.SHA256()).hex(),
    }


def check_remote_certificate(host: str, port: int = 443, timeout: int = 10) -> Dict:
    """Check the TLS certificate of a remote host."""
    result = {
        "host": host,
        "port": port,
        "status": "unknown",
        "days_remaining": None,
        "certificate": None,
        "errors": [],
    }

    try:
        ctx = ssl.create_default_context()
        with socket.create_connection((host, port), timeout=timeout) as sock:
            with ctx.wrap_socket(sock, server_hostname=host) as ssock:
                cert_der = ssock.getpeercert(binary_form=True)
                cert = x509.load_der_x509_certificate(cert_der, default_backend())

                now = datetime.datetime.utcnow()
                not_after = cert.not_valid_after_utc.replace(tzinfo=None)
                days_remaining = (not_after - now).days

                result["days_remaining"] = days_remaining
                result["not_after"] = not_after.isoformat()
                result["protocol"] = ssock.version()
                result["cipher"] = ssock.cipher()[0]

                subject_cn = None
                for attr in cert.subject:
                    if attr.oid == NameOID.COMMON_NAME:
                        subject_cn = attr.value
                        break

                result["common_name"] = subject_cn
                result["fingerprint_sha256"] = cert.fingerprint(hashes.SHA256()).hex()

                if days_remaining < 0:
                    result["status"] = "EXPIRED"
                elif days_remaining < EXPIRY_CRITICAL_DAYS:
                    result["status"] = "CRITICAL"
                elif days_remaining < EXPIRY_WARNING_DAYS:
                    result["status"] = "WARNING"
                else:
                    result["status"] = "OK"

    except ssl.SSLCertVerificationError as e:
        result["status"] = "INVALID"
        result["errors"].append(f"Certificate verification failed: {e}")
    except socket.timeout:
        result["status"] = "TIMEOUT"
        result["errors"].append("Connection timed out")
    except Exception as e:
        result["status"] = "ERROR"
        result["errors"].append(str(e))

    return result


def monitor_domains(domains: List[str], threshold_days: int = 30) -> Dict:
    """Monitor certificate expiration for multiple domains."""
    results = {
        "scan_time": datetime.datetime.utcnow().isoformat() + "Z",
        "threshold_days": threshold_days,
        "total_domains": len(domains),
        "ok": 0,
        "warning": 0,
        "critical": 0,
        "expired": 0,
        "errors": 0,
        "domains": [],
    }

    for domain in domains:
        domain = domain.strip()
        if not domain or domain.startswith("#"):
            continue

        host = domain.split(":")[0]
        port = int(domain.split(":")[1]) if ":" in domain else 443

        logger.info(f"Checking {host}:{port}...")
        check = check_remote_certificate(host, port)
        results["domains"].append(check)

        status = check["status"]
        if status == "OK":
            results["ok"] += 1
        elif status == "WARNING":
            results["warning"] += 1
        elif status == "CRITICAL":
            results["critical"] += 1
        elif status == "EXPIRED":
            results["expired"] += 1
        else:
            results["errors"] += 1

    return results


def verify_certificate_chain(cert_path: str, ca_bundle_path: str) -> Dict:
    """Verify a certificate chain against a CA bundle."""
    cert_data = Path(cert_path).read_bytes()
    ca_data = Path(ca_bundle_path).read_bytes()

    cert = x509.load_pem_x509_certificate(cert_data, default_backend())

    ca_certs = []
    pem_blocks = ca_data.split(b"-----END CERTIFICATE-----")
    for block in pem_blocks:
        block = block.strip()
        if block and b"-----BEGIN CERTIFICATE-----" in block:
            pem = block + b"\n-----END CERTIFICATE-----\n"
            ca_certs.append(x509.load_pem_x509_certificate(pem, default_backend()))

    chain = []
    current = cert
    chain.append({
        "subject": current.subject.rfc4514_string(),
        "issuer": current.issuer.rfc4514_string(),
    })

    for ca in ca_certs:
        if current.issuer == ca.subject:
            chain.append({
                "subject": ca.subject.rfc4514_string(),
                "issuer": ca.issuer.rfc4514_string(),
            })
            current = ca
            if ca.issuer == ca.subject:
                break

    is_self_signed = cert.issuer == cert.subject
    chain_complete = len(chain) > 1 or is_self_signed

    return {
        "certificate": cert.subject.rfc4514_string(),
        "chain_length": len(chain),
        "chain": chain,
        "chain_complete": chain_complete,
        "is_self_signed": is_self_signed,
    }


def main():
    parser = argparse.ArgumentParser(description="SSL Certificate Lifecycle Tool")
    subparsers = parser.add_subparsers(dest="command")

    csr = subparsers.add_parser("generate-csr", help="Generate CSR")
    csr.add_argument("--domain", required=True, help="Primary domain")
    csr.add_argument("--output", default="./certs", help="Output directory")
    csr.add_argument("--key-type", choices=["ecdsa", "rsa"], default="ecdsa")
    csr.add_argument("--san", nargs="*", help="Additional SAN domains")
    csr.add_argument("--org", help="Organization name")

    parse = subparsers.add_parser("parse-cert", help="Parse certificate")
    parse.add_argument("--cert", required=True, help="Certificate file path")

    check = subparsers.add_parser("check-expiry", help="Check remote cert expiry")
    check.add_argument("--host", required=True, help="Hostname")
    check.add_argument("--port", type=int, default=443, help="Port")

    mon = subparsers.add_parser("monitor", help="Monitor multiple domains")
    mon.add_argument("--domains", required=True, help="File with domain list")
    mon.add_argument("--threshold", type=int, default=30, help="Warning threshold (days)")

    chain = subparsers.add_parser("verify-chain", help="Verify certificate chain")
    chain.add_argument("--cert", required=True, help="Certificate file")
    chain.add_argument("--ca-bundle", required=True, help="CA bundle file")

    args = parser.parse_args()

    if args.command == "generate-csr":
        result = generate_csr(args.domain, args.output, args.key_type, args.san, args.org)
        print(json.dumps(result, indent=2))
    elif args.command == "parse-cert":
        result = parse_certificate(args.cert)
        print(json.dumps(result, indent=2, default=str))
    elif args.command == "check-expiry":
        result = check_remote_certificate(args.host, args.port)
        print(json.dumps(result, indent=2, default=str))
    elif args.command == "monitor":
        domains = Path(args.domains).read_text().strip().split("\n")
        result = monitor_domains(domains, args.threshold)
        print(json.dumps(result, indent=2, default=str))
    elif args.command == "verify-chain":
        result = verify_certificate_chain(args.cert, args.ca_bundle)
        print(json.dumps(result, indent=2))
    else:
        parser.print_help()


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 1.5 KB
Keep exploring