3、核心库cryptography实战:生成RSA/ECC密钥对、创建自签名证书、解析X.509证书字段

说实话,很多运维同学写脚本时喜欢直接调 openssl 命令行。我个人习惯用 Python 的 cryptography 库来做——更可控,也更容易集成到自动化流程里。这一章我们直接上手,把密钥生成、自签名证书创建、证书字段解析这三个核心操作练熟。

本章目标: 掌握用 Python 代码完成证书轮换中最基础的三个动作,不依赖外部命令。

3.1 环境准备与库导入

先装好库:

pip install cryptography

然后导入我们需要的模块:

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

嗯,这里要注意:hazmat 这个名字看着吓人,其实它只是表示“底层密码学原语”,用的时候小心参数别传错就行。

3.2 生成 RSA 密钥对

RSA 目前还是最通用的选择。我在项目中遇到过有些老旧设备只支持 RSA 2048,所以轮换脚本里我一般会保留 RSA 的生成分支。

def generate_rsa_keypair(key_size=2048):
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=key_size,
        backend=default_backend()
    )
    return private_key

为什么公钥指数固定用 65537?说白了,这是一个安全性和性能的平衡点。更小的指数有安全风险,更大的指数加解密太慢。

参数 推荐值 说明
public_exponent 65537 固定值,不要改
key_size 2048 / 4096 2048 是底线,4096 更安全但慢

3.3 生成 ECC 密钥对

ECC 密钥更短,性能更好。我个人偏好用 SECP256R1(也叫 P-256),兼容性最好。

def generate_ecc_keypair(curve=ec.SECP256R1()):
    private_key = ec.generate_private_key(
        curve,
        backend=default_backend()
    )
    return private_key

你想想看,同样的安全强度,RSA 需要 3072 位,ECC 只要 256 位。在 IoT 设备或高并发场景下,这个差距很明显。

我的建议: 新系统优先用 ECC P-256,兼容老系统时用 RSA 2048。脚本里最好两个分支都支持,通过配置切换。

3.4 创建自签名证书

自签名证书在内部测试、开发环境、临时服务里用得很多。核心逻辑就是:用私钥给自己签发一张证书。

def create_self_signed_cert(private_key, subject_name="example.com"):
    subject = issuer = x509.Name([
        x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"),
        x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Beijing"),
        x509.NameAttribute(NameOID.LOCALITY_NAME, "Beijing"),
        x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MyOrg"),
        x509.NameAttribute(NameOID.COMMON_NAME, subject_name),
    ])

    cert = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(issuer)  # 自签名:subject == issuer
        .public_key(private_key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.datetime.utcnow())
        .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))
        .add_extension(
            x509.BasicConstraints(ca=True, path_length=None), critical=True
        )
        .sign(private_key, hashes.SHA256(), default_backend())
    )
    return cert

我曾经踩过一个坑:not_valid_after 忘记加时区,导致证书在 UTC 零点之后立刻过期。后来我统一用 datetime.datetime.utcnow() 配合 timedelta,避免时区问题。

注意: 生产环境的自签名证书有效期不要超过 1 年。有些团队图省事设 10 年,一旦私钥泄露,后果很严重。

3.5 解析 X.509 证书字段

轮换脚本里经常需要读取现有证书的信息,比如检查是否快过期、提取公钥、验证签名算法。下面这段代码可以帮你把证书的常用字段都解析出来:

def parse_certificate(cert_pem_bytes):
    cert = x509.load_pem_x509_certificate(cert_pem_bytes, default_backend())

    info = {
        "subject": cert.subject.rfc4514_string(),
        "issuer": cert.issuer.rfc4514_string(),
        "serial_number": cert.serial_number,
        "not_valid_before": cert.not_valid_before_utc,
        "not_valid_after": cert.not_valid_after_utc,
        "signature_algorithm": cert.signature_algorithm_oid._name,
        "public_key_type": type(cert.public_key()).__name__,
    }

    # 提取公钥详细信息
    pub_key = cert.public_key()
    if isinstance(pub_key, rsa.RSAPublicKey):
        info["key_size"] = pub_key.key_size
    elif isinstance(pub_key, ec.EllipticCurvePublicKey):
        info["curve_name"] = pub_key.curve.name

    return info

你想想看,这个函数在证书轮换脚本里有多重要?轮换前先解析旧证书,拿到 subject、有效期、密钥类型,然后生成新证书时直接复用这些字段,保证无缝衔接。

3.6 完整流程:从生成到持久化

把上面几个函数串起来,就是一个完整的证书创建流程:

# 1. 生成密钥对
private_key = generate_rsa_keypair(2048)

# 2. 创建自签名证书
cert = create_self_signed_cert(private_key, "myservice.internal")

# 3. 持久化私钥(PEM 格式,加密保存)
with open("server.key", "wb") as f:
    f.write(private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.BestAvailableEncryption(b"your-password")
    ))

# 4. 持久化证书
with open("server.crt", "wb") as f:
    f.write(cert.public_bytes(serialization.Encoding.PEM))

# 5. 解析验证
parsed = parse_certificate(open("server.crt", "rb").read())
print(parsed)
避坑指南: 私钥一定要加密保存。我曾经见过直接把私钥明文写进配置文件的案例,那基本等于把大门钥匙挂在门把手上。

3.7 本章知识体系

下面这张图帮你理清本章的核心逻辑:

证书轮换核心库实战知识体系 cryptography 库 密钥对生成 RSA (2048/4096) ECC (P-256/P-384) 自签名证书 Subject == Issuer 有效期 / 序列号 X.509 字段解析 Subject / Issuer 公钥 / 签名算法 PEM 文件输出(.key + .crt) 核心流程:密钥生成 → 证书创建 → 字段解析 → 持久化,每一步都是轮换脚本的基础积木

3.8 常见问题与避坑

  • 私钥格式不兼容: 有些老系统只认 PKCS#1 格式,而 cryptography 默认输出 PKCS#8。用 PrivateFormat.TraditionalOpenSSL 可以解决。
  • 证书序列号重复: 手动指定序列号可能导致冲突。用 x509.random_serial_number() 生成随机序列号,避免重复。
  • 签名算法不匹配: RSA 密钥用 SHA256,ECC 密钥也用 SHA256,但 ECDSA 签名算法会自动选择。不要手动混用。
重要提醒: 本章所有代码都基于 cryptography 42.0+ 版本。老版本 API 有差异,升级后记得测试兼容性。