Files
sientia-dataops-opc-ingestor/ingestor/connection/security.py
vitor-aignosi ecb055eefb SIENTIAPDE-1973
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.
2026-10-29 12:39:21 -03:00

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)