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.
93 lines
2.7 KiB
Python
93 lines
2.7 KiB
Python
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
from asyncua import ua
|
|
from asyncua.crypto.security_policies import (
|
|
SecurityPolicyAes128Sha256RsaOaep,
|
|
SecurityPolicyBasic256Sha256,
|
|
)
|
|
|
|
from ingestor.connection.security import (
|
|
SESSION_TIMEOUT_MS,
|
|
UnsupportedSecurityError,
|
|
apply_security,
|
|
is_secured,
|
|
)
|
|
|
|
CERTIFICATE = {'certificate_id': 1, 'application_uri': 'urn:sientia:opc-ingestor'}
|
|
|
|
|
|
def _client():
|
|
client = MagicMock()
|
|
client.set_security = AsyncMock()
|
|
return client
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_security_uses_mode_and_policy_and_channel_lifetime():
|
|
client = _client()
|
|
|
|
await apply_security(
|
|
client, {'mode': 'SignAndEncrypt', 'policy': 'Basic256Sha256'}, CERTIFICATE, 'c', 'k'
|
|
)
|
|
|
|
assert client.application_uri == 'urn:sientia:opc-ingestor'
|
|
assert client.session_timeout == SESSION_TIMEOUT_MS
|
|
assert client.secure_channel_timeout == int(SESSION_TIMEOUT_MS * 1.25)
|
|
args, kwargs = client.set_security.call_args
|
|
assert args[0] is SecurityPolicyBasic256Sha256
|
|
assert kwargs == {
|
|
'certificate': 'c',
|
|
'private_key': 'k',
|
|
'server_certificate': None,
|
|
'mode': ua.MessageSecurityMode.SignAndEncrypt,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_apply_security_sign_with_another_policy():
|
|
client = _client()
|
|
|
|
await apply_security(
|
|
client, {'mode': 'Sign', 'policy': 'Aes128Sha256RsaOaep'}, CERTIFICATE, 'c', 'k'
|
|
)
|
|
|
|
args, kwargs = client.set_security.call_args
|
|
assert args[0] is SecurityPolicyAes128Sha256RsaOaep
|
|
assert kwargs['mode'] == ua.MessageSecurityMode.Sign
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
'security',
|
|
[
|
|
{'mode': 'Encrypt', 'policy': 'Basic256Sha256'},
|
|
{'mode': 'Sign', 'policy': 'Basic999'},
|
|
{'mode': 'Sign', 'policy': 'None'},
|
|
{},
|
|
],
|
|
)
|
|
async def test_apply_security_rejects_unknown_mode_or_policy(security):
|
|
client = _client()
|
|
|
|
with pytest.raises(UnsupportedSecurityError):
|
|
await apply_security(client, security, CERTIFICATE, 'c', 'k')
|
|
client.set_security.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('server_cfg', 'secured'),
|
|
[
|
|
({'security': {'mode': 'SignAndEncrypt'}, 'certificate': CERTIFICATE}, True),
|
|
({'security': {'mode': 'Sign'}, 'certificate': CERTIFICATE}, True),
|
|
# No certificate: no security (D5), whatever the mode
|
|
({'security': {'mode': 'SignAndEncrypt'}, 'certificate': None}, False),
|
|
({'security': {'mode': 'SignAndEncrypt'}}, False),
|
|
# mode None: no security even with a certificate
|
|
({'security': {'mode': 'None'}, 'certificate': CERTIFICATE}, False),
|
|
({}, False),
|
|
],
|
|
)
|
|
def test_is_secured(server_cfg, secured):
|
|
assert is_secured(server_cfg) is secured
|