证书生成与配置
前置阅读
- 安全模式 / Token 概览请看 Security 加密.
- 构造 Session 时如何传 PFX 请看 构造函数.
1. 生成客户端 PFX
用 OpenSSL CLI (推荐)
openssl req -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes \
-subj "/CN=MyClient/O=Darra/C=CN" \
-addext "subjectAltName=URI:urn:my-company:my-client"
openssl pkcs12 -export -out client.pfx -inkey key.pem -in cert.pem -password pass:123456
# 同时导出 DER 公钥, 用于服务端 trusted/
openssl x509 -in cert.pem -outform der -out client.der
用 Python 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
from cryptography.hazmat.primitives.serialization import pkcs12
import datetime as dt
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, "MyClient"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "My Company"),
x509.NameAttribute(NameOID.COUNTRY_NAME, "CN"),
])
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(dt.datetime.utcnow())
.not_valid_after(dt.datetime.utcnow() + dt.timedelta(days=365))
.add_extension(
x509.SubjectAlternativeName([
x509.UniformResourceIdentifier("urn:my-company:my-client"),
]),
critical=False)
.sign(key, hashes.SHA256())
)
pfx_bytes = pkcs12.serialize_key_and_certificates(
name=b"MyClient",
key=key,
cert=cert,
cas=None,
encryption_algorithm=serialization.BestAvailableEncryption(b"123456"))
with open(r"C:\certs\client.pfx", "wb") as f:
f.write(pfx_bytes)
# DER 公钥 (服务端 trusted/)
with open(r"C:\certs\client.der", "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.DER))
2. 服务端导入信任
把客户端公钥 (.der 或 .pem) 拷贝到服务端的"受信任客户端证书"目录:
| Server | 路径 |
|---|---|
| Darra SimulatorServer | <simulator>/pki/trusted/certs/ |
| open62541 example | <server>/pki/trusted/certs/ |
| UaExpert | 设置 → Certificates → Trust |
3. 客户端构造时传 PFX
from darra_opcua import OpcUaSession, MessageSecurityMode
with OpcUaSession(
endpoint_url="opc.tcp://server:4840",
security_mode=MessageSecurityMode.SignAndEncrypt,
client_cert_path=r"C:\certs\client.pfx",
client_key_path="123456", # PFX 密码
server_cert_path=None) as ua: # None = TOFU (Trust On First Use)
ua.connect()
4. 服务端证书 TOFU
第一次连接时, 客户端会信任服务端证书并保存指纹. 之后每次连接校验指纹一致, 不一致 (中间人) 拒绝.
如果不想 TOFU, 显式传服务端证书 DER 路径:
server_cert_path=r"C:\certs\server.der"
故障排查
| StatusCode | 原因 | 解决 |
|---|---|---|
BadCertificateUntrusted | 服务端没信任客户端 | 把 client.der 拷到服务端 trusted/certs/ |
BadCertificateInvalid | 证书过期 / 未生效 | 重新生成 |
BadCertificateUriInvalid | 证书 SAN.URI 与 ApplicationUri 不匹配 | 重新生成时确保 applicationUri 一致 |
BadCertificateHostNameInvalid | 证书 SAN.DNS 与连接的 host 不匹配 | 生成时加 subjectAltName=DNS:server.example.com |
BadSecurityChecksFailed | 通用安全校验失败 | 看服务端日志查具体原因 |
BadSecurityModeRejected | 服务端不支持该 SecurityMode | get_endpoints 看服务端实际支持哪些 |
BadSecurityPolicyRejected | 服务端不支持 Basic256Sha256 | 服务端配置开启该 Policy |
SAN URI 重要性
OPC UA 校验客户端证书 SAN 中必须有 URI: 字段, 且与 ApplicationUri 一致. 这是防伪造的关键, 必须正确设置. 用 OpenSSL 验证:
openssl pkcs12 -in client.pfx -password pass:123456 -nokeys -out client.pem
openssl x509 -in client.pem -text -noout | grep -A1 "Subject Alternative Name"
或 Python:
from cryptography.hazmat.primitives.serialization import pkcs12
with open(r"C:\certs\client.pfx", "rb") as f:
_, cert, _ = pkcs12.load_key_and_certificates(f.read(), b"123456")
san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
uris = san.get_values_for_type(x509.UniformResourceIdentifier)
if not uris:
print("ERROR: Certificate missing SAN URI - regenerate!")
else:
print(f"SAN URIs: {uris}")
时间同步 (NTP)
加密模式下双方校验时间戳, 偏差超过 ~10 分钟会被拒绝. 生产环境必须配 NTP, 时间偏差控制在秒级.
# Windows
w32tm /resync
# Linux
sudo timedatectl set-ntp true