SIENTIAPDE-1973: Implement per-connector OPC client certificates and remove legacy read engine. This update overhauls certificate management, transitioning from a single platform certificate to individual client certificates defined in the slot configuration. The CertProvider now downloads certificates from MinIO based on certificate_id, applying dynamic security policies (mode, policy) from the slot. The legacy READ_ENGINE=legacy option and OpcManager have been entirely removed, standardizing on the ServerReader with strict slot v2 enforcement. Write command resolution (ServerConfigResolver) now consistently uses connectors joined with opc_certificates. The CERT_BUCKET environment variable is no longer used.
66 lines
2.5 KiB
Python
66 lines
2.5 KiB
Python
from asyncua import ua
|
|
from asyncua.crypto.security_policies import (
|
|
SecurityPolicyAes128Sha256RsaOaep,
|
|
SecurityPolicyAes256Sha256RsaPss,
|
|
SecurityPolicyBasic128Rsa15,
|
|
SecurityPolicyBasic256,
|
|
SecurityPolicyBasic256Sha256,
|
|
)
|
|
|
|
SESSION_TIMEOUT_MS = 600_000 # 10 min; tune per server (Samarco: long sessions degrade)
|
|
|
|
# connectors.security.policy / .mode (SIENTIAPDE-1974 D4) to the asyncua classes
|
|
POLICIES = {
|
|
'Basic256Sha256': SecurityPolicyBasic256Sha256,
|
|
'Aes128Sha256RsaOaep': SecurityPolicyAes128Sha256RsaOaep,
|
|
'Aes256Sha256RsaPss': SecurityPolicyAes256Sha256RsaPss,
|
|
'Basic256': SecurityPolicyBasic256,
|
|
'Basic128Rsa15': SecurityPolicyBasic128Rsa15,
|
|
}
|
|
MODES = {
|
|
'Sign': ua.MessageSecurityMode.Sign,
|
|
'SignAndEncrypt': ua.MessageSecurityMode.SignAndEncrypt,
|
|
}
|
|
|
|
|
|
class UnsupportedSecurityError(ValueError):
|
|
"""The server's security mode or policy has no asyncua counterpart."""
|
|
|
|
|
|
def is_secured(server_cfg: dict) -> bool:
|
|
"""Whether a connection applies security: only with a client certificate (D5), and
|
|
not when its mode is 'None'. Without one, it connects without security, and a server
|
|
that requires a certificate refuses the connection."""
|
|
security = server_cfg.get('security') or {}
|
|
return server_cfg.get('certificate') is not None and security.get('mode') != 'None'
|
|
|
|
|
|
async def apply_security(client, security, certificate, cert_path, private_key_path):
|
|
"""Apply the server's mode + policy with the connector's client certificate, and set
|
|
channel > session lifetime (1879 §3: secure_channel_timeout = session_timeout * 1.25).
|
|
|
|
The application URI is the certificate's (its SAN URI): the server rejects a
|
|
different one (KEPServer: BadCertificateUriInvalid).
|
|
|
|
Raises:
|
|
UnsupportedSecurityError: When the mode or policy is unknown.
|
|
"""
|
|
policy = POLICIES.get(security.get('policy'))
|
|
mode = MODES.get(security.get('mode'))
|
|
if policy is None or mode is None:
|
|
raise UnsupportedSecurityError(
|
|
f'Unsupported OPC security mode={security.get("mode")!r} '
|
|
f'policy={security.get("policy")!r} (modes: {", ".join(MODES)}; '
|
|
f'policies: {", ".join(POLICIES)})'
|
|
)
|
|
client.application_uri = certificate['application_uri']
|
|
await client.set_security(
|
|
policy,
|
|
certificate=cert_path,
|
|
private_key=private_key_path,
|
|
server_certificate=None,
|
|
mode=mode,
|
|
)
|
|
client.session_timeout = SESSION_TIMEOUT_MS
|
|
client.secure_channel_timeout = int(SESSION_TIMEOUT_MS * 1.25)
|