npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
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
- Request: Generate key pair and CSR
- Issuance: CA validates and issues certificate
- Deployment: Install certificate on servers
- Monitoring: Track expiration and health
- Renewal: Request new certificate before expiry
- 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
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
- cryptography docs: https://cryptography.io/en/latest/x509/
- Let's Encrypt ACME: https://letsencrypt.org/docs/
- OCSP Stapling: https://datatracker.ietf.org/doc/html/rfc6960
- Certificate Transparency: https://certificate.transparency.dev/
standards.md1.6 KB
Standards and References - SSL Certificate Lifecycle Management
Primary Standards
RFC 5280 - Internet X.509 PKI Certificate and CRL Profile
- URL: https://www.rfc-editor.org/rfc/rfc5280
- Description: Core X.509 certificate format and Certificate Revocation List (CRL)
RFC 6960 - X.509 Online Certificate Status Protocol (OCSP)
- URL: https://www.rfc-editor.org/rfc/rfc6960
- Description: Real-time certificate revocation checking
RFC 8555 - Automatic Certificate Management Environment (ACME)
- URL: https://www.rfc-editor.org/rfc/rfc8555
- Description: Protocol for automating certificate issuance (Let's Encrypt)
RFC 6962 - Certificate Transparency
- URL: https://www.rfc-editor.org/rfc/rfc6962
- Description: Public logging framework for TLS certificates
RFC 2986 - PKCS #10: Certification Request Syntax
- URL: https://www.rfc-editor.org/rfc/rfc2986
- Description: CSR format specification
NIST SP 800-57 Part 3 - Application-Specific Key Management
- URL: https://csrc.nist.gov/publications/detail/sp/800-57-part-3/rev-1/final
- Description: Key management guidance for TLS certificates
Tools
Let's Encrypt / Certbot
- URL: https://letsencrypt.org/
- Certbot: https://certbot.eff.org/
- Rate limits: 50 certificates per domain per week
Certificate Transparency Logs
- Google: https://ct.googleapis.com/logs
- crt.sh: https://crt.sh/ (certificate search)
Mozilla Observatory
- URL: https://observatory.mozilla.org/
- Description: Web security scanning including TLS configuration
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
#!/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
#!/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()