From 3bcb186dc177f71f33e8fd0a7359d337234346fd Mon Sep 17 00:00:00 2001 From: Christoph Ziebuhr Date: Fri, 11 Oct 2024 20:57:43 +0200 Subject: [PATCH 1/6] cryptography is always available --- asyncua/common/connection.py | 8 +------ asyncua/crypto/security_policies.py | 36 +---------------------------- asyncua/server/internal_server.py | 10 +------- asyncua/tools.py | 6 ++--- tests/test_crypto_connect.py | 10 +------- tests/test_permissions.py | 9 +------- 6 files changed, 7 insertions(+), 72 deletions(-) diff --git a/asyncua/common/connection.py b/asyncua/common/connection.py index dea3fe93a..84376a3ec 100644 --- a/asyncua/common/connection.py +++ b/asyncua/common/connection.py @@ -7,13 +7,7 @@ from asyncua import ua from asyncua.ua.uaerrors import UaInvalidParameterError from ..ua.ua_binary import struct_from_binary, struct_to_binary, header_from_binary, header_to_binary - -try: - from ..crypto.uacrypto import InvalidSignature -except ImportError: - - class InvalidSignature(Exception): # type: ignore - pass +from ..crypto.uacrypto import InvalidSignature _logger = logging.getLogger("asyncua.uaprotocol") diff --git a/asyncua/crypto/security_policies.py b/asyncua/crypto/security_policies.py index 372b4eba9..6130c4e95 100644 --- a/asyncua/crypto/security_policies.py +++ b/asyncua/crypto/security_policies.py @@ -4,28 +4,13 @@ from abc import ABCMeta, abstractmethod from ..ua import CryptographyNone, SecurityPolicy, MessageSecurityMode, UaError - -try: - from ..crypto import uacrypto - - CRYPTOGRAPHY_AVAILABLE = True -except ImportError: - CRYPTOGRAPHY_AVAILABLE = False +from ..crypto import uacrypto POLICY_NONE_URI = "http://opcfoundation.org/UA/SecurityPolicy#None" _logger = logging.getLogger(__name__) -def require_cryptography(obj): - """ - Raise exception if cryptography module is not available. - Call this function in constructors. - """ - if not CRYPTOGRAPHY_AVAILABLE: - raise UaError(f"Can't use {obj.__class__.__name__}, cryptography module is not installed") - - class Signer: """ Abstract base class for cryptographic signature algorithm @@ -237,7 +222,6 @@ def remove_padding(self, data): class SignerRsa(Signer): def __init__(self, client_pk): - require_cryptography(self) self.client_pk = client_pk self.key_size = self.client_pk.key_size // 8 @@ -250,7 +234,6 @@ def signature(self, data): class VerifierRsa(Verifier): def __init__(self, server_cert): - require_cryptography(self) self.server_cert = server_cert self.key_size = self.server_cert.public_key().key_size // 8 @@ -263,7 +246,6 @@ def verify(self, data, signature): class EncryptorRsa(Encryptor): def __init__(self, server_cert, enc_fn, padding_size): - require_cryptography(self) self.server_cert = server_cert self.key_size = self.server_cert.public_key().key_size // 8 self.encryptor = enc_fn @@ -285,7 +267,6 @@ def encrypt(self, data): class DecryptorRsa(Decryptor): def __init__(self, client_pk, dec_fn, padding_size): - require_cryptography(self) self.client_pk = client_pk self.key_size = self.client_pk.key_size // 8 self.decryptor = dec_fn @@ -307,7 +288,6 @@ def decrypt(self, data): class SignerAesCbc(Signer): def __init__(self, key): - require_cryptography(self) self.key = key def signature_size(self): @@ -319,7 +299,6 @@ def signature(self, data): class VerifierAesCbc(Verifier): def __init__(self, key): - require_cryptography(self) self.key = key def signature_size(self): @@ -333,7 +312,6 @@ def verify(self, data, signature): class EncryptorAesCbc(Encryptor): def __init__(self, key, init_vec): - require_cryptography(self) self.cipher = uacrypto.cipher_aes_cbc(key, init_vec) def plain_block_size(self): @@ -348,7 +326,6 @@ def encrypt(self, data): class DecryptorAesCbc(Decryptor): def __init__(self, key, init_vec): - require_cryptography(self) self.cipher = uacrypto.cipher_aes_cbc(key, init_vec) def plain_block_size(self): @@ -363,7 +340,6 @@ def decrypt(self, data): class SignerSha256(Signer): def __init__(self, client_pk): - require_cryptography(self) self.client_pk = client_pk self.key_size = self.client_pk.key_size // 8 @@ -376,7 +352,6 @@ def signature(self, data): class VerifierSha256(Verifier): def __init__(self, server_cert): - require_cryptography(self) self.server_cert = server_cert self.key_size = self.server_cert.public_key().key_size // 8 @@ -389,7 +364,6 @@ def verify(self, data, signature): class SignerHMac256(Signer): def __init__(self, key): - require_cryptography(self) self.key = key def signature_size(self): @@ -401,7 +375,6 @@ def signature(self, data): class VerifierHMac256(Verifier): def __init__(self, key): - require_cryptography(self) self.key = key def signature_size(self): @@ -415,7 +388,6 @@ def verify(self, data, signature): class SignerPssSha256(Signer): def __init__(self, client_pk): - require_cryptography(self) self.client_pk = client_pk self.key_size = self.client_pk.key_size // 8 @@ -428,7 +400,6 @@ def signature(self, data): class VerifierPssSha256(Verifier): def __init__(self, server_cert): - require_cryptography(self) self.server_cert = server_cert self.key_size = self.server_cert.public_key().key_size // 8 @@ -476,7 +447,6 @@ def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep(pubkey, data) def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=None): - require_cryptography(self) if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) # even in Sign mode we need to asymmetrically encrypt secrets @@ -550,7 +520,6 @@ def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep_sha256(pubkey, data) def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=None): - require_cryptography(self) if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) # even in Sign mode we need to asymmetrically encrypt secrets @@ -632,7 +601,6 @@ def encrypt_asymmetric(pubkey, data): def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=None): _logger.warning("DEPRECATED! Do not use SecurityPolicyBasic128Rsa15 anymore!") - require_cryptography(self) if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) # even in Sign mode we need to asymmetrically encrypt secrets @@ -712,7 +680,6 @@ def encrypt_asymmetric(pubkey, data): def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=None): _logger.warning("DEPRECATED! Do not use SecurityPolicyBasic256 anymore!") - require_cryptography(self) if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) # even in Sign mode we need to asymmetrically encrypt secrets @@ -790,7 +757,6 @@ def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep(pubkey, data) def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=None): - require_cryptography(self) if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) # even in Sign mode we need to asymmetrically encrypt secrets diff --git a/asyncua/server/internal_server.py b/asyncua/server/internal_server.py index 9fda51142..b2560e208 100644 --- a/asyncua/server/internal_server.py +++ b/asyncua/server/internal_server.py @@ -24,12 +24,7 @@ from .internal_session import InternalSession from .event_generator import EventGenerator from ..crypto.validator import CertificateValidatorMethod - -try: - from asyncua.crypto import uacrypto -except ImportError: - logging.getLogger(__name__).warning("cryptography is not installed, use of crypto disabled") - uacrypto = False +from ..crypto import uacrypto _logger = logging.getLogger(__name__) @@ -403,9 +398,6 @@ def check_user_token(self, isession, token): # decrypt password if we can if str(token.EncryptionAlgorithm) != "None": - if not uacrypto: - # raise # Should I raise a significant exception? - return False try: if token.EncryptionAlgorithm == "http://www.w3.org/2001/04/xmlenc#rsa-1_5": raw_pw = uacrypto.decrypt_rsa15(self.private_key, password) diff --git a/asyncua/tools.py b/asyncua/tools.py index 32f791edb..94ea555ad 100644 --- a/asyncua/tools.py +++ b/asyncua/tools.py @@ -513,10 +513,8 @@ def application_to_strings(app): def cert_to_string(der): if not der: return "[no certificate]" - try: - from .crypto import uacrypto - except ImportError: - return f"{len(der)} bytes" + from .crypto import uacrypto + cert = uacrypto.x509_from_der(der) return uacrypto.x509_to_string(cert) diff --git a/tests/test_crypto_connect.py b/tests/test_crypto_connect.py index 8c7e7ba1a..fd7753b70 100644 --- a/tests/test_crypto_connect.py +++ b/tests/test_crypto_connect.py @@ -13,15 +13,7 @@ from asyncua.server.user_managers import CertificateUserManager from asyncua.crypto.security_policies import Verifier, Decryptor from asyncua.crypto.validator import CertificateValidator, CertificateValidatorOptions - -try: - from asyncua.crypto import uacrypto - from asyncua.crypto import security_policies -except ImportError: - print("WARNING: CRYPTO NOT AVAILABLE, CRYPTO TESTS DISABLED!!") - disable_crypto_tests = True -else: - disable_crypto_tests = False +from asyncua.crypto import uacrypto, security_policies pytestmark = pytest.mark.asyncio diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 258f282d0..61ba83267 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -6,14 +6,7 @@ from asyncua import ua from asyncua.server.users import UserRole from asyncua.server.user_managers import CertificateUserManager - -try: - from asyncua.crypto import security_policies -except ImportError: - print("WARNING: CRYPTO NOT AVAILABLE, CRYPTO TESTS DISABLED!!") - disable_crypto_tests = True -else: - disable_crypto_tests = False +from asyncua.crypto import security_policies pytestmark = pytest.mark.asyncio From 2b0799118b102f789013e16aede0014fb8fbb151 Mon Sep 17 00:00:00 2001 From: Christoph Ziebuhr Date: Fri, 11 Oct 2024 19:33:43 +0200 Subject: [PATCH 2/6] Simplify handling of SecurityPolicyType and SecurityLevel --- asyncua/crypto/security_policies.py | 34 ++++- asyncua/server/server.py | 186 +++------------------------- tests/test_crypto_connect.py | 54 ++------ 3 files changed, 60 insertions(+), 214 deletions(-) diff --git a/asyncua/crypto/security_policies.py b/asyncua/crypto/security_policies.py index 6130c4e95..1064cd4a6 100644 --- a/asyncua/crypto/security_policies.py +++ b/asyncua/crypto/security_policies.py @@ -3,7 +3,7 @@ import time from abc import ABCMeta, abstractmethod -from ..ua import CryptographyNone, SecurityPolicy, MessageSecurityMode, UaError +from ..ua import CryptographyNone, SecurityPolicyType, SecurityPolicy, MessageSecurityMode, UaError from ..crypto import uacrypto @@ -814,3 +814,35 @@ def encrypt_asymmetric(pubkey, data, policy_uri): if not policy_uri or policy_uri == POLICY_NONE_URI: return data, "" raise UaError(f"Unsupported security policy `{policy_uri}`") + + +# policy, mode, security_level +SECURITY_POLICY_TYPE_MAP = { + SecurityPolicyType.NoSecurity: [SecurityPolicy, MessageSecurityMode.None_, 0], + SecurityPolicyType.Basic128Rsa15_Sign: [SecurityPolicyBasic128Rsa15, MessageSecurityMode.Sign, 1], + SecurityPolicyType.Basic128Rsa15_SignAndEncrypt: [ + SecurityPolicyBasic128Rsa15, + MessageSecurityMode.SignAndEncrypt, + 2, + ], + SecurityPolicyType.Basic256_Sign: [SecurityPolicyBasic256, MessageSecurityMode.Sign, 11], + SecurityPolicyType.Basic256_SignAndEncrypt: [SecurityPolicyBasic256, MessageSecurityMode.SignAndEncrypt, 21], + SecurityPolicyType.Basic256Sha256_Sign: [SecurityPolicyBasic256Sha256, MessageSecurityMode.Sign, 50], + SecurityPolicyType.Basic256Sha256_SignAndEncrypt: [ + SecurityPolicyBasic256Sha256, + MessageSecurityMode.SignAndEncrypt, + 70, + ], + SecurityPolicyType.Aes128Sha256RsaOaep_Sign: [SecurityPolicyAes128Sha256RsaOaep, MessageSecurityMode.Sign, 55], + SecurityPolicyType.Aes128Sha256RsaOaep_SignAndEncrypt: [ + SecurityPolicyAes128Sha256RsaOaep, + MessageSecurityMode.SignAndEncrypt, + 75, + ], + SecurityPolicyType.Aes256Sha256RsaPss_Sign: [SecurityPolicyAes256Sha256RsaPss, MessageSecurityMode.Sign, 60], + SecurityPolicyType.Aes256Sha256RsaPss_SignAndEncrypt: [ + SecurityPolicyAes256Sha256RsaPss, + MessageSecurityMode.SignAndEncrypt, + 80, + ], +} diff --git a/asyncua/server/server.py b/asyncua/server/server.py index c28d4b4bd..1bc60c2c8 100644 --- a/asyncua/server/server.py +++ b/asyncua/server/server.py @@ -381,175 +381,27 @@ def set_identity_tokens(self, tokens): async def _setup_server_nodes(self): # to be called just before starting server since it needs all parameters to be setup - if ua.SecurityPolicyType.NoSecurity in self._security_policy: - self._set_endpoints() - self._policies = [ua.SecurityPolicyFactory(permission_ruleset=self._permission_ruleset)] - - if self._security_policy != [ua.SecurityPolicyType.NoSecurity]: - if not (self.certificate and self.iserver.private_key): - _logger.warning("Endpoints other than open requested but private key and certificate are not set.") - return - - if ua.SecurityPolicyType.NoSecurity in self._security_policy: - _logger.warning("Creating an open endpoint to the server, although encrypted endpoints are enabled.") - - if ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt in self._security_policy: - self._set_endpoints( - security_policies.SecurityPolicyBasic256Sha256, ua.MessageSecurityMode.SignAndEncrypt - ) - self._policies.append( - ua.SecurityPolicyFactory( - security_policies.SecurityPolicyBasic256Sha256, - ua.MessageSecurityMode.SignAndEncrypt, - self.certificate, - self.iserver.private_key, - permission_ruleset=self._permission_ruleset, - ) - ) - if ua.SecurityPolicyType.Basic256Sha256_Sign in self._security_policy: - self._set_endpoints(security_policies.SecurityPolicyBasic256Sha256, ua.MessageSecurityMode.Sign) - self._policies.append( - ua.SecurityPolicyFactory( - security_policies.SecurityPolicyBasic256Sha256, - ua.MessageSecurityMode.Sign, - self.certificate, - self.iserver.private_key, - permission_ruleset=self._permission_ruleset, - ) - ) - if ua.SecurityPolicyType.Aes128Sha256RsaOaep_SignAndEncrypt in self._security_policy: - self._set_endpoints( - security_policies.SecurityPolicyAes128Sha256RsaOaep, ua.MessageSecurityMode.SignAndEncrypt - ) - self._policies.append( - ua.SecurityPolicyFactory( - security_policies.SecurityPolicyAes128Sha256RsaOaep, - ua.MessageSecurityMode.SignAndEncrypt, - self.certificate, - self.iserver.private_key, - permission_ruleset=self._permission_ruleset, - ) - ) - if ua.SecurityPolicyType.Aes128Sha256RsaOaep_Sign in self._security_policy: - self._set_endpoints(security_policies.SecurityPolicyAes128Sha256RsaOaep, ua.MessageSecurityMode.Sign) - self._policies.append( - ua.SecurityPolicyFactory( - security_policies.SecurityPolicyAes128Sha256RsaOaep, - ua.MessageSecurityMode.Sign, - self.certificate, - self.iserver.private_key, - permission_ruleset=self._permission_ruleset, - ) - ) - if ua.SecurityPolicyType.Basic128Rsa15_Sign in self._security_policy: - self._set_endpoints(security_policies.SecurityPolicyBasic128Rsa15, ua.MessageSecurityMode.Sign) - self._policies.append( - ua.SecurityPolicyFactory( - security_policies.SecurityPolicyBasic128Rsa15, - ua.MessageSecurityMode.Sign, - self.certificate, - self.iserver.private_key, - permission_ruleset=self._permission_ruleset, - ) - ) - if ua.SecurityPolicyType.Basic128Rsa15_SignAndEncrypt in self._security_policy: - self._set_endpoints( - security_policies.SecurityPolicyBasic128Rsa15, ua.MessageSecurityMode.SignAndEncrypt - ) - self._policies.append( - ua.SecurityPolicyFactory( - security_policies.SecurityPolicyBasic128Rsa15, - ua.MessageSecurityMode.SignAndEncrypt, - self.certificate, - self.iserver.private_key, - permission_ruleset=self._permission_ruleset, - ) - ) - if ua.SecurityPolicyType.Aes256Sha256RsaPss_SignAndEncrypt in self._security_policy: - self._set_endpoints( - security_policies.SecurityPolicyAes256Sha256RsaPss, ua.MessageSecurityMode.SignAndEncrypt - ) - self._policies.append( - ua.SecurityPolicyFactory( - security_policies.SecurityPolicyAes256Sha256RsaPss, - ua.MessageSecurityMode.SignAndEncrypt, - self.certificate, - self.iserver.private_key, - permission_ruleset=self._permission_ruleset, - ) + no_cert = False + for policy_type in self._security_policy: + policy, mode, level = security_policies.SECURITY_POLICY_TYPE_MAP[policy_type] + if policy is not ua.SecurityPolicy and not (self.certificate and self.iserver.private_key): + no_cert = True + continue + self._set_endpoints(policy, mode, level) + self._policies.append( + ua.SecurityPolicyFactory( + policy, + mode, + self.certificate, + self.iserver.private_key, + permission_ruleset=self._permission_ruleset, ) - if ua.SecurityPolicyType.Aes256Sha256RsaPss_Sign in self._security_policy: - self._set_endpoints(security_policies.SecurityPolicyAes256Sha256RsaPss, ua.MessageSecurityMode.Sign) - self._policies.append( - ua.SecurityPolicyFactory( - security_policies.SecurityPolicyAes256Sha256RsaPss, - ua.MessageSecurityMode.Sign, - self.certificate, - self.iserver.private_key, - permission_ruleset=self._permission_ruleset, - ) - ) - - @staticmethod - def lookup_security_level_for_policy_type(security_policy_type: ua.SecurityPolicyType) -> ua.Byte: - """Returns the security level for an ua.SecurityPolicyType. - - This is endpoint & server implementation specific! - - Returns: - ua.Byte: the found security level - """ - - return ua.Byte( - { - ua.SecurityPolicyType.NoSecurity: 0, - ua.SecurityPolicyType.Basic128Rsa15_Sign: 1, - ua.SecurityPolicyType.Basic128Rsa15_SignAndEncrypt: 2, - ua.SecurityPolicyType.Basic256_Sign: 11, - ua.SecurityPolicyType.Basic256_SignAndEncrypt: 21, - ua.SecurityPolicyType.Basic256Sha256_Sign: 50, - ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt: 70, - ua.SecurityPolicyType.Aes128Sha256RsaOaep_Sign: 55, - ua.SecurityPolicyType.Aes128Sha256RsaOaep_SignAndEncrypt: 75, - ua.SecurityPolicyType.Aes256Sha256RsaPss_Sign: 60, - ua.SecurityPolicyType.Aes256Sha256RsaPss_SignAndEncrypt: 80, - }[security_policy_type] - ) - - @staticmethod - def determine_security_level(security_policy_uri: str, security_mode: ua.MessageSecurityMode) -> ua.Byte: - """Determine the security level of an EndPoint. - The security level indicates how secure an EndPoint is, compared to other EndPoints of the same server. - Value 0 is a special value; EndPoint isn't recommended, typical for ua.MessageSecurityMode.None_. - - See Part 4 7.10 - - To the determine the level the value of ua.SecurityPolicyType is used. - The enum values already correspond to and - The Enum ua.SecurityPolicyType already contains a value per Enum entry that already correspond to - - - Args: - security_policy (ua.SecurityPolicy): the used policy - security_mode (ua.MessageSecurityMode): the used security mode for the messages. - - Returns: - ua.Byte: the returned security level - """ - security_level: ua.Byte = ua.Byte(0) - - if security_mode != ua.MessageSecurityMode.None_: - security_policy_name = f'{security_policy_uri.split("#")[1].replace("_","")}_{security_mode.name}' - - try: - security_policy_type: ua.SecurityPolicyType = ua.SecurityPolicyType[security_policy_name] - security_level = Server.lookup_security_level_for_policy_type(security_policy_type) - except KeyError: - _logger.error('"%s" isn\'t a valid security policy', security_policy_name) + ) - return security_level + if no_cert: + _logger.warning("Endpoints other than open requested but private key and certificate are not set.") - def _set_endpoints(self, policy=ua.SecurityPolicy, mode=ua.MessageSecurityMode.None_): + def _set_endpoints(self, policy, mode, level): idtokens = [] tokens = self.iserver.supported_tokens if ua.AnonymousIdentityToken in tokens: @@ -589,7 +441,7 @@ def _set_endpoints(self, policy=ua.SecurityPolicy, mode=ua.MessageSecurityMode.N edp.SecurityPolicyUri = policy.URI edp.UserIdentityTokens = idtokens edp.TransportProfileUri = "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary" - edp.SecurityLevel = Server.determine_security_level(policy.URI, mode) + edp.SecurityLevel = level self.iserver.add_endpoint(edp) def set_server_name(self, name): diff --git a/tests/test_crypto_connect.py b/tests/test_crypto_connect.py index fd7753b70..2d549c29e 100644 --- a/tests/test_crypto_connect.py +++ b/tests/test_crypto_connect.py @@ -11,7 +11,7 @@ from asyncua import Server from asyncua import ua from asyncua.server.user_managers import CertificateUserManager -from asyncua.crypto.security_policies import Verifier, Decryptor +from asyncua.crypto.security_policies import Verifier, Decryptor, SECURITY_POLICY_TYPE_MAP from asyncua.crypto.validator import CertificateValidator, CertificateValidatorOptions from asyncua.crypto import uacrypto, security_policies @@ -493,57 +493,19 @@ async def test_anonymous_rejection(): await srv.stop() -async def test_security_level_all(): - assert Server.determine_security_level( - ua.SecurityPolicy.URI, ua.MessageSecurityMode.None_ - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.NoSecurity) - - assert Server.determine_security_level( - security_policies.SecurityPolicyBasic256Sha256.URI, ua.MessageSecurityMode.Sign - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.Basic256Sha256_Sign) - assert Server.determine_security_level( - security_policies.SecurityPolicyBasic256Sha256.URI, ua.MessageSecurityMode.SignAndEncrypt - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt) - - assert Server.determine_security_level( - security_policies.SecurityPolicyAes128Sha256RsaOaep.URI, ua.MessageSecurityMode.Sign - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.Aes128Sha256RsaOaep_Sign) - assert Server.determine_security_level( - security_policies.SecurityPolicyAes128Sha256RsaOaep.URI, ua.MessageSecurityMode.SignAndEncrypt - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.Aes128Sha256RsaOaep_SignAndEncrypt) - - assert Server.determine_security_level( - security_policies.SecurityPolicyAes256Sha256RsaPss.URI, ua.MessageSecurityMode.Sign - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.Aes256Sha256RsaPss_Sign) - assert Server.determine_security_level( - security_policies.SecurityPolicyAes256Sha256RsaPss.URI, ua.MessageSecurityMode.SignAndEncrypt - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.Aes256Sha256RsaPss_SignAndEncrypt) - - # For the sake of completeness also the old, not recommended, protocols Basic128Rsa15 and Basic256 - assert Server.determine_security_level( - security_policies.SecurityPolicyBasic128Rsa15.URI, ua.MessageSecurityMode.Sign - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.Basic128Rsa15_Sign) - assert Server.determine_security_level( - security_policies.SecurityPolicyBasic128Rsa15.URI, ua.MessageSecurityMode.SignAndEncrypt - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.Basic128Rsa15_SignAndEncrypt) - - assert Server.determine_security_level( - security_policies.SecurityPolicyBasic256.URI, ua.MessageSecurityMode.Sign - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.Basic256_Sign) - assert Server.determine_security_level( - security_policies.SecurityPolicyBasic256.URI, ua.MessageSecurityMode.SignAndEncrypt - ) == Server.lookup_security_level_for_policy_type(ua.SecurityPolicyType.Basic256_SignAndEncrypt) - - async def test_security_level_endpoints(srv_crypto_all_certs: Tuple[Server, str]): srv = srv_crypto_all_certs[0] end_points: list[ua.EndpointDescription] = await srv.get_endpoints() for end_point in end_points: - assert end_point.SecurityLevel == Server.determine_security_level( - end_point.SecurityPolicyUri, end_point.SecurityMode - ) + if end_point.SecurityMode == ua.MessageSecurityMode.None_: + policy_type = ua.SecurityPolicyType.NoSecurity + else: + policy_type = ua.SecurityPolicyType[ + f'{end_point.SecurityPolicyUri.split("#")[1].replace("_", "")}_{end_point.SecurityMode.name}' + ] + assert end_point.SecurityLevel == SECURITY_POLICY_TYPE_MAP[policy_type][2] async def test_certificate_validator(srv_crypto_one_cert): From a523f828121a242d22d9471f7d90f5b4bf6f0bc7 Mon Sep 17 00:00:00 2001 From: Christoph Ziebuhr Date: Wed, 16 Oct 2024 16:05:27 +0200 Subject: [PATCH 3/6] Move SecurityPolicy from ua to crypto --- asyncua/client/client.py | 12 +- asyncua/client/ha/ha_client.py | 5 +- asyncua/client/ua_client.py | 7 +- asyncua/crypto/security_policies.py | 277 +++++++++++++++++++++------- asyncua/server/server.py | 6 +- asyncua/server/uaprocessor.py | 3 +- asyncua/ua/uaprotocol_hand.py | 120 ------------ tests/test_ha_client.py | 4 +- tests/test_unit.py | 3 +- 9 files changed, 234 insertions(+), 203 deletions(-) diff --git a/asyncua/client/client.py b/asyncua/client/client.py index d3652d42b..4677040b3 100644 --- a/asyncua/client/client.py +++ b/asyncua/client/client.py @@ -68,7 +68,7 @@ def __init__(self, url: str, timeout: float = 4, watchdog_intervall: float = 1.0 self.description = self.name self.application_uri = "urn:freeopcua:client" self.product_uri = "urn:freeopcua.github.io:client" - self.security_policy = ua.SecurityPolicy() + self.security_policy = security_policies.SecurityPolicyNone() self.secure_channel_id = None self.secure_channel_timeout = 3600000 # 1 hour self.session_timeout = 3600000 # 1 hour @@ -162,7 +162,7 @@ def set_locale(self, locale: Sequence[str]) -> None: async def set_security_string(self, string: str) -> None: """ Set SecureConnection mode. - :param string: Mode format ``Policy,Mode,certificate,private_key[,server_private_key]`` + :param string: Mode format ``Policy,Mode,certificate,private_key[,server_certificate]`` where: - ``Policy`` is ``Basic128Rsa15``, ``Basic256`` or ``Basic256Sha256`` - ``Mode`` is ``Sign`` or ``SignAndEncrypt`` @@ -190,7 +190,7 @@ async def set_security_string(self, string: str) -> None: async def set_security( self, - policy: Type[ua.SecurityPolicy], + policy: Type[security_policies.SecurityPolicy], certificate: Union[str, uacrypto.CertProperties, bytes, Path], private_key: Union[str, uacrypto.CertProperties, bytes, Path], private_key_password: Optional[Union[str, bytes]] = None, @@ -203,7 +203,7 @@ async def set_security( """ if server_certificate is None: # Force unencrypted/unsigned SecureChannel to list the endpoints - new_policy = ua.SecurityPolicy() + new_policy = security_policies.SecurityPolicyNone() self.security_policy = new_policy self.uaclient.security_policy = new_policy # load certificate from server's list of endpoints @@ -226,7 +226,7 @@ async def set_security( async def _set_security( self, - policy: Type[ua.SecurityPolicy], + policy: Type[security_policies.SecurityPolicy], certificate: uacrypto.CertProperties, private_key: uacrypto.CertProperties, server_cert: uacrypto.CertProperties, @@ -699,7 +699,7 @@ def _add_user_auth(self, params, username: str, password: str): params.UserIdentityToken = ua.UserNameIdentityToken() params.UserIdentityToken.UserName = username policy_uri = self.server_policy_uri(ua.UserTokenType.UserName) - if not policy_uri or policy_uri == security_policies.POLICY_NONE_URI: + if not policy_uri or policy_uri == security_policies.SecurityPolicyNone.URI: # see specs part 4, 7.36.3: if the token is NOT encrypted, # then the password only contains UTF-8 encoded password # and EncryptionAlgorithm is null diff --git a/asyncua/client/ha/ha_client.py b/asyncua/client/ha/ha_client.py index 23241193d..c941d1d6a 100644 --- a/asyncua/client/ha/ha_client.py +++ b/asyncua/client/ha/ha_client.py @@ -16,6 +16,7 @@ from .common import ClientNotFound, event_wait from .virtual_subscription import TypeSubHandler, VirtualSubscription from ...crypto.uacrypto import CertProperties +from ...crypto.security_policies import SecurityPolicy _logger = logging.getLogger(__name__) @@ -64,7 +65,7 @@ class ServerInfo: @dataclass(frozen=True, eq=True) class HaSecurityConfig: - policy: Optional[Type[ua.SecurityPolicy]] = None + policy: Optional[Type[SecurityPolicy]] = None certificate: Optional[CertProperties] = None private_key: Optional[CertProperties] = None server_certificate: Optional[CertProperties] = None @@ -190,7 +191,7 @@ async def stop(self): def set_security( self, - policy: Type[ua.SecurityPolicy], + policy: Type[SecurityPolicy], certificate: CertProperties, private_key: CertProperties, server_certificate: Optional[CertProperties] = None, diff --git a/asyncua/client/ua_client.py b/asyncua/client/ua_client.py index 8e55b674c..679580a91 100644 --- a/asyncua/client/ua_client.py +++ b/asyncua/client/ua_client.py @@ -14,6 +14,7 @@ from ..ua.uaerrors import BadTimeout, BadNoSubscription, BadSessionClosed, BadUserAccessDenied, UaStructParsingError from ..ua.uaprotocol_auto import OpenSecureChannelResult, SubscriptionAcknowledgement from ..common.connection import SecureConnection, TransportLimits +from ..crypto import security_policies class UASocketProtocol(asyncio.Protocol): @@ -29,7 +30,7 @@ class UASocketProtocol(asyncio.Protocol): def __init__( self, timeout: float = 1, - security_policy: ua.SecurityPolicy = ua.SecurityPolicy(), + security_policy: security_policies.SecurityPolicy = security_policies.SecurityPolicyNone(), limits: TransportLimits = None, ): """ @@ -293,13 +294,13 @@ def __init__(self, timeout: float = 1.0): self.logger = logging.getLogger(f"{__name__}.UaClient") self._subscription_callbacks = {} self._timeout = timeout - self.security_policy = ua.SecurityPolicy() + self.security_policy = security_policies.SecurityPolicyNone() self.protocol: UASocketProtocol = None self._publish_task = None self._pre_request_hook: Optional[Callable[[], Awaitable[None]]] = None self._closing: bool = False - def set_security(self, policy: ua.SecurityPolicy): + def set_security(self, policy: security_policies.SecurityPolicy): self.security_policy = policy def _make_protocol(self): diff --git a/asyncua/crypto/security_policies.py b/asyncua/crypto/security_policies.py index 1064cd4a6..1a583dc64 100644 --- a/asyncua/crypto/security_policies.py +++ b/asyncua/crypto/security_policies.py @@ -1,13 +1,18 @@ +from __future__ import annotations import logging import struct import time +from typing import TYPE_CHECKING, Optional from abc import ABCMeta, abstractmethod -from ..ua import CryptographyNone, SecurityPolicyType, SecurityPolicy, MessageSecurityMode, UaError +from ..ua import SecurityPolicyType, MessageSecurityMode, UaError + +if TYPE_CHECKING: + from ..crypto.permission_rules import PermissionRuleset + from ..crypto import uacrypto -POLICY_NONE_URI = "http://opcfoundation.org/UA/SecurityPolicy#None" _logger = logging.getLogger(__name__) @@ -93,6 +98,62 @@ def reset(self): attrs[k] = None +class CryptographyNone: + """ + Base class for symmetric/asymmetric cryptography + """ + + def __init__(self): + pass + + def plain_block_size(self): + """ + Size of plain text block for block cipher. + """ + return 1 + + def encrypted_block_size(self): + """ + Size of encrypted text block for block cipher. + """ + return 1 + + def padding(self, size): + """ + Create padding for a block of given size. + plain_size = size + len(padding) + signature_size() + plain_size = N * plain_block_size() + """ + return b"" + + def min_padding_size(self): + return 0 + + def signature_size(self): + return 0 + + def signature(self, data): + return b"" + + def encrypt(self, data): + return data + + def decrypt(self, data): + return data + + def vsignature_size(self): + return 0 + + def verify(self, data, signature): + """ + Verify signature and raise exception if signature is invalid + """ + pass + + def remove_padding(self, data): + return data + + class Cryptography(CryptographyNone): """ Security policy: Sign or SignAndEncrypt @@ -221,33 +282,33 @@ def remove_padding(self, data): class SignerRsa(Signer): - def __init__(self, client_pk): - self.client_pk = client_pk - self.key_size = self.client_pk.key_size // 8 + def __init__(self, host_privkey): + self.host_privkey = host_privkey + self.key_size = self.host_privkey.key_size // 8 def signature_size(self): return self.key_size def signature(self, data): - return uacrypto.sign_sha1(self.client_pk, data) + return uacrypto.sign_sha1(self.host_privkey, data) class VerifierRsa(Verifier): - def __init__(self, server_cert): - self.server_cert = server_cert - self.key_size = self.server_cert.public_key().key_size // 8 + def __init__(self, peer_cert): + self.peer_cert = peer_cert + self.key_size = self.peer_cert.public_key().key_size // 8 def signature_size(self): return self.key_size def verify(self, data, signature): - uacrypto.verify_sha1(self.server_cert, data, signature) + uacrypto.verify_sha1(self.peer_cert, data, signature) class EncryptorRsa(Encryptor): - def __init__(self, server_cert, enc_fn, padding_size): - self.server_cert = server_cert - self.key_size = self.server_cert.public_key().key_size // 8 + def __init__(self, peer_cert, enc_fn, padding_size): + self.peer_cert = peer_cert + self.key_size = self.peer_cert.public_key().key_size // 8 self.encryptor = enc_fn self.padding_size = padding_size @@ -261,14 +322,14 @@ def encrypt(self, data): encrypted = b"" block_size = self.plain_block_size() for i in range(0, len(data), block_size): - encrypted += self.encryptor(self.server_cert.public_key(), data[i : i + block_size]) + encrypted += self.encryptor(self.peer_cert.public_key(), data[i : i + block_size]) return encrypted class DecryptorRsa(Decryptor): - def __init__(self, client_pk, dec_fn, padding_size): - self.client_pk = client_pk - self.key_size = self.client_pk.key_size // 8 + def __init__(self, host_privkey, dec_fn, padding_size): + self.host_privkey = host_privkey + self.key_size = self.host_privkey.key_size // 8 self.decryptor = dec_fn self.padding_size = padding_size @@ -282,7 +343,7 @@ def decrypt(self, data): decrypted = b"" block_size = self.encrypted_block_size() for i in range(0, len(data), block_size): - decrypted += self.decryptor(self.client_pk, data[i : i + block_size]) + decrypted += self.decryptor(self.host_privkey, data[i : i + block_size]) return decrypted @@ -339,27 +400,27 @@ def decrypt(self, data): class SignerSha256(Signer): - def __init__(self, client_pk): - self.client_pk = client_pk - self.key_size = self.client_pk.key_size // 8 + def __init__(self, host_privkey): + self.host_privkey = host_privkey + self.key_size = self.host_privkey.key_size // 8 def signature_size(self): return self.key_size def signature(self, data): - return uacrypto.sign_sha256(self.client_pk, data) + return uacrypto.sign_sha256(self.host_privkey, data) class VerifierSha256(Verifier): - def __init__(self, server_cert): - self.server_cert = server_cert - self.key_size = self.server_cert.public_key().key_size // 8 + def __init__(self, peer_cert): + self.peer_cert = peer_cert + self.key_size = self.peer_cert.public_key().key_size // 8 def signature_size(self): return self.key_size def verify(self, data, signature): - uacrypto.verify_sha256(self.server_cert, data, signature) + uacrypto.verify_sha256(self.peer_cert, data, signature) class SignerHMac256(Signer): @@ -387,27 +448,83 @@ def verify(self, data, signature): class SignerPssSha256(Signer): - def __init__(self, client_pk): - self.client_pk = client_pk - self.key_size = self.client_pk.key_size // 8 + def __init__(self, host_privkey): + self.host_privkey = host_privkey + self.key_size = self.host_privkey.key_size // 8 def signature_size(self): return self.key_size def signature(self, data): - return uacrypto.sign_pss_sha256(self.client_pk, data) + return uacrypto.sign_pss_sha256(self.host_privkey, data) class VerifierPssSha256(Verifier): - def __init__(self, server_cert): - self.server_cert = server_cert - self.key_size = self.server_cert.public_key().key_size // 8 + def __init__(self, peer_cert): + self.peer_cert = peer_cert + self.key_size = self.peer_cert.public_key().key_size // 8 def signature_size(self): return self.key_size def verify(self, data, signature): - uacrypto.verify_pss_sha256(self.server_cert, data, signature) + uacrypto.verify_pss_sha256(self.peer_cert, data, signature) + + +class SecurityPolicy: + """ + Abstract base class for security policy + """ + + __metaclass__ = ABCMeta + + URI: str + AsymmetricEncryptionURI: str + AsymmetricSignatureURI: str + secure_channel_nonce_length: int + asymmetric_cryptography: CryptographyNone + symmetric_cryptography: CryptographyNone + Mode: MessageSecurityMode + peer_certificate: Optional[bytes] + host_certificate: Optional[bytes] + permissions: Optional[PermissionRuleset] + + @abstractmethod + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): + pass + + @abstractmethod + def make_local_symmetric_key(self, secret, seed): + pass + + @abstractmethod + def make_remote_symmetric_key(self, secret, seed, lifetime): + pass + + +class SecurityPolicyNone(SecurityPolicy): + URI = "http://opcfoundation.org/UA/SecurityPolicy#None" + AsymmetricEncryptionURI: str = "" + AsymmetricSignatureURI: str = "" + secure_channel_nonce_length: int = 0 + + def __init__( + self, peer_cert=None, host_cert=None, host_privkey=None, mode=MessageSecurityMode.None_, permission_ruleset=None + ): + if isinstance(peer_cert, bytes): + peer_cert = uacrypto.x509_from_der(peer_cert) + self.asymmetric_cryptography = CryptographyNone() + self.symmetric_cryptography = CryptographyNone() + self.Mode = mode + self.peer_certificate = uacrypto.der_from_x509(peer_cert) + self.host_certificate = uacrypto.der_from_x509(host_cert) + self.permissions = permission_ruleset + + def make_local_symmetric_key(self, secret, seed): + return None + + def make_remote_symmetric_key(self, secret, seed, lifetime): + return None class SecurityPolicyAes128Sha256RsaOaep(SecurityPolicy): @@ -436,26 +553,27 @@ class SecurityPolicyAes128Sha256RsaOaep(SecurityPolicy): """ URI = "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep" - signature_key_size = 32 - symmetric_key_size = 16 - secure_channel_nonce_length = 32 AsymmetricEncryptionURI = "http://www.w3.org/2001/04/xmlenc#rsa-oaep" AsymmetricSignatureURI = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" + secure_channel_nonce_length = 32 + + signature_key_size = 32 + symmetric_key_size = 16 @staticmethod def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep(pubkey, data) - def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=None): + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) # even in Sign mode we need to asymmetrically encrypt secrets # transmitted in OpenSecureChannel. So SignAndEncrypt here self.asymmetric_cryptography = Cryptography(MessageSecurityMode.SignAndEncrypt) - self.asymmetric_cryptography.Signer = SignerSha256(client_pk) + self.asymmetric_cryptography.Signer = SignerSha256(host_privkey) self.asymmetric_cryptography.Verifier = VerifierSha256(peer_cert) self.asymmetric_cryptography.Encryptor = EncryptorRsa(peer_cert, uacrypto.encrypt_rsa_oaep, 42) - self.asymmetric_cryptography.Decryptor = DecryptorRsa(client_pk, uacrypto.decrypt_rsa_oaep, 42) + self.asymmetric_cryptography.Decryptor = DecryptorRsa(host_privkey, uacrypto.decrypt_rsa_oaep, 42) self.symmetric_cryptography = Cryptography(mode) self.Mode = mode self.peer_certificate = uacrypto.der_from_x509(peer_cert) @@ -509,26 +627,27 @@ class SecurityPolicyAes256Sha256RsaPss(SecurityPolicy): """ URI = "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss" - signature_key_size = 32 - symmetric_key_size = 32 - secure_channel_nonce_length = 32 AsymmetricEncryptionURI = "http://opcfoundation.org/UA/security/rsa-oaep-sha2-256" AsymmetricSignatureURI = "http://opcfoundation.org/UA/security/rsa-pss-sha2-256" + secure_channel_nonce_length = 32 + + signature_key_size = 32 + symmetric_key_size = 32 @staticmethod def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep_sha256(pubkey, data) - def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=None): + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) # even in Sign mode we need to asymmetrically encrypt secrets # transmitted in OpenSecureChannel. So SignAndEncrypt here self.asymmetric_cryptography = Cryptography(MessageSecurityMode.SignAndEncrypt) - self.asymmetric_cryptography.Signer = SignerPssSha256(client_pk) + self.asymmetric_cryptography.Signer = SignerPssSha256(host_privkey) self.asymmetric_cryptography.Verifier = VerifierPssSha256(peer_cert) self.asymmetric_cryptography.Encryptor = EncryptorRsa(peer_cert, uacrypto.encrypt_rsa_oaep_sha256, 66) - self.asymmetric_cryptography.Decryptor = DecryptorRsa(client_pk, uacrypto.decrypt_rsa_oaep_sha256, 66) + self.asymmetric_cryptography.Decryptor = DecryptorRsa(host_privkey, uacrypto.decrypt_rsa_oaep_sha256, 66) self.symmetric_cryptography = Cryptography(mode) self.Mode = mode self.peer_certificate = uacrypto.der_from_x509(peer_cert) @@ -588,17 +707,18 @@ class SecurityPolicyBasic128Rsa15(SecurityPolicy): """ URI = "http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15" - signature_key_size = 16 - symmetric_key_size = 16 - secure_channel_nonce_length = 16 AsymmetricEncryptionURI = "http://www.w3.org/2001/04/xmlenc#rsa-1_5" AsymmetricSignatureURI = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + secure_channel_nonce_length = 16 + + signature_key_size = 16 + symmetric_key_size = 16 @staticmethod def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa15(pubkey, data) - def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=None): + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): _logger.warning("DEPRECATED! Do not use SecurityPolicyBasic128Rsa15 anymore!") if isinstance(peer_cert, bytes): @@ -606,10 +726,10 @@ def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=Non # even in Sign mode we need to asymmetrically encrypt secrets # transmitted in OpenSecureChannel. So SignAndEncrypt here self.asymmetric_cryptography = Cryptography(MessageSecurityMode.SignAndEncrypt) - self.asymmetric_cryptography.Signer = SignerRsa(client_pk) + self.asymmetric_cryptography.Signer = SignerRsa(host_privkey) self.asymmetric_cryptography.Verifier = VerifierRsa(peer_cert) self.asymmetric_cryptography.Encryptor = EncryptorRsa(peer_cert, uacrypto.encrypt_rsa15, 11) - self.asymmetric_cryptography.Decryptor = DecryptorRsa(client_pk, uacrypto.decrypt_rsa15, 11) + self.asymmetric_cryptography.Decryptor = DecryptorRsa(host_privkey, uacrypto.decrypt_rsa15, 11) self.symmetric_cryptography = Cryptography(mode) self.Mode = mode self.peer_certificate = uacrypto.der_from_x509(peer_cert) @@ -667,17 +787,18 @@ class SecurityPolicyBasic256(SecurityPolicy): """ URI = "http://opcfoundation.org/UA/SecurityPolicy#Basic256" - signature_key_size = 24 - symmetric_key_size = 32 - secure_channel_nonce_length = 32 AsymmetricEncryptionURI = "http://www.w3.org/2001/04/xmlenc#rsa-oaep" AsymmetricSignatureURI = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + secure_channel_nonce_length = 32 + + signature_key_size = 24 + symmetric_key_size = 32 @staticmethod def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep(pubkey, data) - def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=None): + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): _logger.warning("DEPRECATED! Do not use SecurityPolicyBasic256 anymore!") if isinstance(peer_cert, bytes): @@ -685,10 +806,10 @@ def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=Non # even in Sign mode we need to asymmetrically encrypt secrets # transmitted in OpenSecureChannel. So SignAndEncrypt here self.asymmetric_cryptography = Cryptography(MessageSecurityMode.SignAndEncrypt) - self.asymmetric_cryptography.Signer = SignerRsa(client_pk) + self.asymmetric_cryptography.Signer = SignerRsa(host_privkey) self.asymmetric_cryptography.Verifier = VerifierRsa(peer_cert) self.asymmetric_cryptography.Encryptor = EncryptorRsa(peer_cert, uacrypto.encrypt_rsa_oaep, 42) - self.asymmetric_cryptography.Decryptor = DecryptorRsa(client_pk, uacrypto.decrypt_rsa_oaep, 42) + self.asymmetric_cryptography.Decryptor = DecryptorRsa(host_privkey, uacrypto.decrypt_rsa_oaep, 42) self.symmetric_cryptography = Cryptography(mode) self.Mode = mode self.peer_certificate = uacrypto.der_from_x509(peer_cert) @@ -746,26 +867,27 @@ class SecurityPolicyBasic256Sha256(SecurityPolicy): """ URI = "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256" - signature_key_size = 32 - symmetric_key_size = 32 - secure_channel_nonce_length = 32 AsymmetricEncryptionURI = "http://www.w3.org/2001/04/xmlenc#rsa-oaep" AsymmetricSignatureURI = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" + secure_channel_nonce_length = 32 + + signature_key_size = 32 + symmetric_key_size = 32 @staticmethod def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep(pubkey, data) - def __init__(self, peer_cert, host_cert, client_pk, mode, permission_ruleset=None): + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) # even in Sign mode we need to asymmetrically encrypt secrets # transmitted in OpenSecureChannel. So SignAndEncrypt here self.asymmetric_cryptography = Cryptography(MessageSecurityMode.SignAndEncrypt) - self.asymmetric_cryptography.Signer = SignerSha256(client_pk) + self.asymmetric_cryptography.Signer = SignerSha256(host_privkey) self.asymmetric_cryptography.Verifier = VerifierSha256(peer_cert) self.asymmetric_cryptography.Encryptor = EncryptorRsa(peer_cert, uacrypto.encrypt_rsa_oaep, 42) - self.asymmetric_cryptography.Decryptor = DecryptorRsa(client_pk, uacrypto.decrypt_rsa_oaep, 42) + self.asymmetric_cryptography.Decryptor = DecryptorRsa(host_privkey, uacrypto.decrypt_rsa_oaep, 42) self.symmetric_cryptography = Cryptography(mode) self.Mode = mode self.peer_certificate = uacrypto.der_from_x509(peer_cert) @@ -811,14 +933,37 @@ def encrypt_asymmetric(pubkey, data, policy_uri): ]: if policy_uri == cls.URI: return (cls.encrypt_asymmetric(pubkey, data), cls.AsymmetricEncryptionURI) - if not policy_uri or policy_uri == POLICY_NONE_URI: + if not policy_uri or policy_uri == SecurityPolicyNone.URI: return data, "" raise UaError(f"Unsupported security policy `{policy_uri}`") +class SecurityPolicyFactory: + """ + Helper class for creating server-side SecurityPolicy. + Server has one certificate and private key, but needs a separate + SecurityPolicy for every client and client's certificate + """ + + def __init__(self, cls, mode, certificate=None, private_key=None, permission_ruleset=None): + self.cls = cls + self.mode = mode + self.certificate = certificate + self.private_key = private_key + self.permission_ruleset = permission_ruleset + + def matches(self, uri, mode=None): + return self.cls.URI == uri and (mode is None or self.mode == mode) + + def create(self, peer_certificate): + return self.cls( + peer_certificate, self.certificate, self.private_key, self.mode, permission_ruleset=self.permission_ruleset + ) + + # policy, mode, security_level SECURITY_POLICY_TYPE_MAP = { - SecurityPolicyType.NoSecurity: [SecurityPolicy, MessageSecurityMode.None_, 0], + SecurityPolicyType.NoSecurity: [SecurityPolicyNone, MessageSecurityMode.None_, 0], SecurityPolicyType.Basic128Rsa15_Sign: [SecurityPolicyBasic128Rsa15, MessageSecurityMode.Sign, 1], SecurityPolicyType.Basic128Rsa15_SignAndEncrypt: [ SecurityPolicyBasic128Rsa15, diff --git a/asyncua/server/server.py b/asyncua/server/server.py index 1bc60c2c8..dfe333174 100644 --- a/asyncua/server/server.py +++ b/asyncua/server/server.py @@ -384,12 +384,14 @@ async def _setup_server_nodes(self): no_cert = False for policy_type in self._security_policy: policy, mode, level = security_policies.SECURITY_POLICY_TYPE_MAP[policy_type] - if policy is not ua.SecurityPolicy and not (self.certificate and self.iserver.private_key): + if policy is not security_policies.SecurityPolicyNone and not ( + self.certificate and self.iserver.private_key + ): no_cert = True continue self._set_endpoints(policy, mode, level) self._policies.append( - ua.SecurityPolicyFactory( + security_policies.SecurityPolicyFactory( policy, mode, self.certificate, diff --git a/asyncua/server/uaprocessor.py b/asyncua/server/uaprocessor.py index 058ae17d2..f2cf203c2 100644 --- a/asyncua/server/uaprocessor.py +++ b/asyncua/server/uaprocessor.py @@ -11,6 +11,7 @@ from .internal_server import InternalServer, InternalSession from ..common.connection import SecureConnection, TransportLimits from ..common.utils import ServiceError +from ..crypto.security_policies import SecurityPolicyNone _logger = logging.getLogger(__name__) @@ -40,7 +41,7 @@ def __init__(self, internal_server: InternalServer, transport, limits: Transport # rely on dict insertion order (therefore can't use set()) self._publish_results_subs: Dict[ua.IntegerId, bool] = {} self._limits = copy.deepcopy(limits) # Copy limits because they get overriden - self._connection = SecureConnection(ua.SecurityPolicy(), self._limits) + self._connection = SecureConnection(SecurityPolicyNone(), self._limits) self._closing: bool = False self._session_watchdog_task: Optional[asyncio.Task] = None self._watchdog_interval: float = 1.0 diff --git a/asyncua/ua/uaprotocol_hand.py b/asyncua/ua/uaprotocol_hand.py index 13687fe14..84c57ca98 100644 --- a/asyncua/ua/uaprotocol_hand.py +++ b/asyncua/ua/uaprotocol_hand.py @@ -110,126 +110,6 @@ def max_size(): return struct.calcsize(" Date: Fri, 11 Oct 2024 19:39:10 +0200 Subject: [PATCH 4/6] Improve usage of UserTokenPolicy.SecurityPolicyUri Don't allow plaintext password to prevent BadSecurityModeInsufficient in some clients --- asyncua/client/client.py | 3 ++- asyncua/server/internal_server.py | 42 ++++++++++++------------------ asyncua/server/internal_session.py | 24 ++++++++++------- asyncua/server/server.py | 26 +++++++++++++++--- 4 files changed, 56 insertions(+), 39 deletions(-) diff --git a/asyncua/client/client.py b/asyncua/client/client.py index 4677040b3..716433958 100644 --- a/asyncua/client/client.py +++ b/asyncua/client/client.py @@ -704,7 +704,8 @@ def _add_user_auth(self, params, username: str, password: str): # then the password only contains UTF-8 encoded password # and EncryptionAlgorithm is null if self._password: - _logger.warning("Sending plain-text password") + if self.security_policy.Mode != ua.MessageSecurityMode.SignAndEncrypt: + _logger.warning("Sending plain-text password") params.UserIdentityToken.Password = password.encode("utf8") params.UserIdentityToken.EncryptionAlgorithm = None elif self._password: diff --git a/asyncua/server/internal_server.py b/asyncua/server/internal_server.py index b2560e208..d03085786 100644 --- a/asyncua/server/internal_server.py +++ b/asyncua/server/internal_server.py @@ -382,40 +382,30 @@ def set_user_manager(self, user_manager): """ self.user_manager = user_manager - def check_user_token(self, isession, token): + def decrypt_user_token(self, isession, token): """ unpack the username and password for the benefit of the user defined user manager """ user_name = token.UserName password = token.Password - # TODO Support all Token Types - # AnonimousIdentityToken - # UserIdentityToken - # UserNameIdentityToken - # X509IdentityToken - # IssuedIdentityToken + # TODO check if algorithm is allowed, throw BadSecurityPolicyRejected if not # decrypt password if we can - if str(token.EncryptionAlgorithm) != "None": - try: - if token.EncryptionAlgorithm == "http://www.w3.org/2001/04/xmlenc#rsa-1_5": - raw_pw = uacrypto.decrypt_rsa15(self.private_key, password) - elif token.EncryptionAlgorithm == "http://www.w3.org/2001/04/xmlenc#rsa-oaep": - raw_pw = uacrypto.decrypt_rsa_oaep(self.private_key, password) - elif token.EncryptionAlgorithm == "http://opcfoundation.org/UA/security/rsa-oaep-sha2-256": - raw_pw = uacrypto.decrypt_rsa_oaep_sha256(self.private_key, password) - else: - self.logger.warning("Unknown password encoding %s", token.EncryptionAlgorithm) - # raise # Should I raise a significant exception? - return user_name, password - length = unpack_from("= InternalSession.max_connections: raise ServiceError(ua.StatusCodes.BadMaxConnectionsReached) - self.nonce = create_nonce(32) - result.ServerNonce = self.nonce for _ in params.ClientSoftwareCertificates: result.Results.append(ua.StatusCode()) id_token = params.UserIdentityToken @@ -124,13 +122,19 @@ def activate_session(self, params, peer_certificate): self.logger.error("Rejected active session UserIdentityToken not supported") raise ServiceError(ua.StatusCodes.BadIdentityTokenRejected) if self.iserver.user_manager is not None: - if isinstance(id_token, ua.UserNameIdentityToken): - username, password = self.iserver.check_user_token(self, id_token) - elif isinstance(id_token, ua.X509IdentityToken): - peer_certificate = id_token.CertificateData - username, password = None, None - else: - username, password = None, None + try: + if isinstance(id_token, ua.UserNameIdentityToken): + username, password = self.iserver.decrypt_user_token(self, id_token) + elif isinstance(id_token, ua.X509IdentityToken): + # TODO implement verify_x509_token + peer_certificate = id_token.CertificateData + username, password = None, None + else: + username, password = None, None + except (ServiceError, ua.uaerrors.UaStatusCodeError): + raise + except Exception: + raise ServiceError(ua.StatusCodes.BadIdentityTokenInvalid) user = self.iserver.user_manager.get_user( self.iserver, username=username, password=password, certificate=peer_certificate @@ -139,6 +143,8 @@ def activate_session(self, params, peer_certificate): raise ServiceError(ua.StatusCodes.BadUserAccessDenied) else: self.user = user + self.nonce = create_nonce(32) + result.ServerNonce = self.nonce self.state = SessionState.Activated InternalSession._current_connections += 1 self.logger.info("Activated internal session %s for user %s", self.name, self.user) diff --git a/asyncua/server/server.py b/asyncua/server/server.py index dfe333174..56a95ff40 100644 --- a/asyncua/server/server.py +++ b/asyncua/server/server.py @@ -410,21 +410,41 @@ def _set_endpoints(self, policy, mode, level): idtoken = ua.UserTokenPolicy() idtoken.PolicyId = "anonymous" idtoken.TokenType = ua.UserTokenType.Anonymous - idtoken.SecurityPolicyUri = policy.URI + idtoken.SecurityPolicyUri = security_policies.SecurityPolicyNone.URI idtokens.append(idtoken) if ua.X509IdentityToken in tokens: idtoken = ua.UserTokenPolicy() - idtoken.PolicyId = "certificate_basic256sha256" + idtoken.PolicyId = "certificate" idtoken.TokenType = ua.UserTokenType.Certificate idtoken.SecurityPolicyUri = policy.URI + # TODO request signing if mode == ua.MessageSecurityMode.None_ (also need to verify signature then) idtokens.append(idtoken) if ua.UserNameIdentityToken in tokens: idtoken = ua.UserTokenPolicy() idtoken.PolicyId = "username" idtoken.TokenType = ua.UserTokenType.UserName - idtoken.SecurityPolicyUri = policy.URI + if mode == ua.MessageSecurityMode.SignAndEncrypt: + # channel is encrypted, no need to encrypt password again + idtoken.SecurityPolicyUri = security_policies.SecurityPolicyNone.URI + elif mode == ua.MessageSecurityMode.Sign: + # use same policy for encryption + idtoken.SecurityPolicyUri = policy.URI + # try to avoid plaintext password, find first policy with encryption + elif self.certificate and self.iserver.private_key: + for token_policy_type in self._security_policy: + token_policy, token_mode, _ = security_policies.SECURITY_POLICY_TYPE_MAP[token_policy_type] + if token_mode != ua.MessageSecurityMode.SignAndEncrypt: + continue + idtoken.SecurityPolicyUri = token_policy.URI + break + else: + _logger.warning("No encrypting policy available, password may get transferred in plaintext") + idtoken.SecurityPolicyUri = security_policies.SecurityPolicyNone.URI + else: + _logger.warning("No encrypting policy available, password may get transferred in plaintext") + idtoken.SecurityPolicyUri = security_policies.SecurityPolicyNone.URI idtokens.append(idtoken) appdesc = ua.ApplicationDescription() From b95867afb4ebceafee8a929b19334bbbf77cf307 Mon Sep 17 00:00:00 2001 From: Christoph Ziebuhr Date: Fri, 11 Oct 2024 20:47:40 +0200 Subject: [PATCH 5/6] Improve user token policy selection --- asyncua/client/client.py | 39 +++++++++++++-------------------------- asyncua/sync.py | 7 ++----- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/asyncua/client/client.py b/asyncua/client/client.py index 716433958..e2ba6b25e 100644 --- a/asyncua/client/client.py +++ b/asyncua/client/client.py @@ -1,6 +1,7 @@ import asyncio import logging import socket +import dataclasses from cryptography import x509 from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type, Union, cast, Callable, Coroutine @@ -615,29 +616,19 @@ async def _renew_channel_loop(self): _logger.exception("Error while renewing session") raise - def server_policy_id(self, token_type: ua.UserTokenType, default: str) -> str: + def server_policy(self, token_type: ua.UserTokenType) -> ua.UserTokenPolicy: """ - Find PolicyId of server's UserTokenPolicy by token_type. - Return default if there's no matching UserTokenPolicy. - """ - for policy in self._policy_ids: - if policy.TokenType == token_type: - return policy.PolicyId - return default - - def server_policy_uri(self, token_type: ua.UserTokenType) -> str: - """ - Find SecurityPolicyUri of server's UserTokenPolicy by token_type. + Find UserTokenPolicy by token_type. If SecurityPolicyUri is empty, use default SecurityPolicyUri of the endpoint """ for policy in self._policy_ids: if policy.TokenType == token_type: if policy.SecurityPolicyUri: - return policy.SecurityPolicyUri + return policy # empty URI means "use this endpoint's policy URI" - return self.security_policy.URI - return self.security_policy.URI + return dataclasses.replace(policy, SecurityPolicyUri=self.security_policy.URI) + return ua.UserTokenPolicy(TokenType=token_type, SecurityPolicyUri=self.security_policy.URI) async def activate_session( self, @@ -671,7 +662,7 @@ async def activate_session( def _add_anonymous_auth(self, params): params.UserIdentityToken = ua.AnonymousIdentityToken() - params.UserIdentityToken.PolicyId = self.server_policy_id(ua.UserTokenType.Anonymous, "anonymous") + params.UserIdentityToken.PolicyId = self.server_policy(ua.UserTokenType.Anonymous).PolicyId def _add_certificate_auth(self, params, certificate, challenge): params.UserIdentityToken = ua.X509IdentityToken() @@ -681,16 +672,12 @@ def _add_certificate_auth(self, params, certificate, challenge): params.UserTokenSignature = ua.SignatureData() # use signature algorithm that was used for certificate generation if certificate.signature_hash_algorithm.name == "sha256": - params.UserIdentityToken.PolicyId = self.server_policy_id( - ua.UserTokenType.Certificate, "certificate_basic256sha256" - ) + params.UserIdentityToken.PolicyId = self.server_policy(ua.UserTokenType.Certificate).PolicyId sig = uacrypto.sign_sha256(self.user_private_key, challenge) params.UserTokenSignature.Algorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" params.UserTokenSignature.Signature = sig else: - params.UserIdentityToken.PolicyId = self.server_policy_id( - ua.UserTokenType.Certificate, "certificate_basic256" - ) + params.UserIdentityToken.PolicyId = self.server_policy(ua.UserTokenType.Certificate).PolicyId sig = uacrypto.sign_sha1(self.user_private_key, challenge) params.UserTokenSignature.Algorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" params.UserTokenSignature.Signature = sig @@ -698,8 +685,8 @@ def _add_certificate_auth(self, params, certificate, challenge): def _add_user_auth(self, params, username: str, password: str): params.UserIdentityToken = ua.UserNameIdentityToken() params.UserIdentityToken.UserName = username - policy_uri = self.server_policy_uri(ua.UserTokenType.UserName) - if not policy_uri or policy_uri == security_policies.SecurityPolicyNone.URI: + policy = self.server_policy(ua.UserTokenType.UserName) + if not policy.SecurityPolicyUri or policy.SecurityPolicyUri == security_policies.SecurityPolicyNone.URI: # see specs part 4, 7.36.3: if the token is NOT encrypted, # then the password only contains UTF-8 encoded password # and EncryptionAlgorithm is null @@ -709,10 +696,10 @@ def _add_user_auth(self, params, username: str, password: str): params.UserIdentityToken.Password = password.encode("utf8") params.UserIdentityToken.EncryptionAlgorithm = None elif self._password: - data, uri = self._encrypt_password(password, policy_uri) + data, uri = self._encrypt_password(password, policy.SecurityPolicyUri) params.UserIdentityToken.Password = data params.UserIdentityToken.EncryptionAlgorithm = uri - params.UserIdentityToken.PolicyId = self.server_policy_id(ua.UserTokenType.UserName, "username_basic256") + params.UserIdentityToken.PolicyId = policy.PolicyId def _encrypt_password(self, password: str, policy_uri) -> Tuple[bytes, str]: pubkey = uacrypto.x509_from_der(self.security_policy.peer_certificate).public_key() diff --git a/asyncua/sync.py b/asyncua/sync.py index 9fe6c73c5..a55944e53 100644 --- a/asyncua/sync.py +++ b/asyncua/sync.py @@ -436,11 +436,8 @@ def create_session(self) -> ua.CreateSessionResult: # type: ignore[empty-body] def check_connection(self) -> None: pass - def server_policy_id(self, token_type: ua.UserTokenType, default: str) -> str: - return self.aio_obj.server_policy_id(token_type, default) - - def server_policy_uri(self, token_type: ua.UserTokenType) -> str: - return self.aio_obj.server_policy_uri(token_type) + def server_policy(self, token_type: ua.UserTokenType) -> ua.UserTokenPolicy: + return self.aio_obj.server_policy(token_type) @syncmethod def activate_session( # type: ignore[empty-body] From 6b4b3e06cb7bdc8c99d6e4deb38dc953d58048e8 Mon Sep 17 00:00:00 2001 From: Christoph Ziebuhr Date: Fri, 18 Oct 2024 16:29:51 +0200 Subject: [PATCH 6/6] Fix usage of X509IdentityToken - sign token with algorithm from policy uri - verify signature in server! --- asyncua/client/client.py | 21 +++----- asyncua/crypto/security_policies.py | 40 ++++++++++++++++ asyncua/server/internal_server.py | 31 ++++++++++++ asyncua/server/internal_session.py | 3 +- asyncua/server/server.py | 36 +++++++++----- tests/test_permissions.py | 74 ++++++++++++++++++++++------- 6 files changed, 158 insertions(+), 47 deletions(-) diff --git a/asyncua/client/client.py b/asyncua/client/client.py index e2ba6b25e..e95799e3f 100644 --- a/asyncua/client/client.py +++ b/asyncua/client/client.py @@ -165,9 +165,9 @@ async def set_security_string(self, string: str) -> None: Set SecureConnection mode. :param string: Mode format ``Policy,Mode,certificate,private_key[,server_certificate]`` where: - - ``Policy`` is ``Basic128Rsa15``, ``Basic256`` or ``Basic256Sha256`` + - ``Policy`` is ``Basic256Sha256``, ``Aes128Sha256RsaOaep`` or ``Aes256Sha256RsaPss`` - ``Mode`` is ``Sign`` or ``SignAndEncrypt`` - - ``certificate`` and ``server_private_key`` are paths to ``.pem`` or ``.der`` files + - ``certificate`` and ``server_certificate`` are paths to ``.pem`` or ``.der`` files - ``private_key`` may be a path to a ``.pem`` or ``.der`` file or a conjunction of ``path``::``password`` where ``password`` is the private key password. Call this before connect() @@ -669,18 +669,11 @@ def _add_certificate_auth(self, params, certificate, challenge): params.UserIdentityToken.CertificateData = uacrypto.der_from_x509(certificate) # specs part 4, 5.6.3.1: the data to sign is created by appending # the last serverNonce to the serverCertificate - params.UserTokenSignature = ua.SignatureData() - # use signature algorithm that was used for certificate generation - if certificate.signature_hash_algorithm.name == "sha256": - params.UserIdentityToken.PolicyId = self.server_policy(ua.UserTokenType.Certificate).PolicyId - sig = uacrypto.sign_sha256(self.user_private_key, challenge) - params.UserTokenSignature.Algorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" - params.UserTokenSignature.Signature = sig - else: - params.UserIdentityToken.PolicyId = self.server_policy(ua.UserTokenType.Certificate).PolicyId - sig = uacrypto.sign_sha1(self.user_private_key, challenge) - params.UserTokenSignature.Algorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" - params.UserTokenSignature.Signature = sig + policy = self.server_policy(ua.UserTokenType.Certificate) + sig, alg = security_policies.sign_asymmetric(self.user_private_key, challenge, policy.SecurityPolicyUri) + params.UserIdentityToken.PolicyId = policy.PolicyId + params.UserTokenSignature.Algorithm = alg + params.UserTokenSignature.Signature = sig def _add_user_auth(self, params, username: str, password: str): params.UserIdentityToken = ua.UserNameIdentityToken() diff --git a/asyncua/crypto/security_policies.py b/asyncua/crypto/security_policies.py index 1a583dc64..8cb23394f 100644 --- a/asyncua/crypto/security_policies.py +++ b/asyncua/crypto/security_policies.py @@ -564,6 +564,10 @@ class SecurityPolicyAes128Sha256RsaOaep(SecurityPolicy): def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep(pubkey, data) + @staticmethod + def sign_asymmetric(privkey, data): + return uacrypto.sign_sha256(privkey, data) + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) @@ -638,6 +642,10 @@ class SecurityPolicyAes256Sha256RsaPss(SecurityPolicy): def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep_sha256(pubkey, data) + @staticmethod + def sign_asymmetric(privkey, data): + return uacrypto.sign_pss_sha256(privkey, data) + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) @@ -718,6 +726,10 @@ class SecurityPolicyBasic128Rsa15(SecurityPolicy): def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa15(pubkey, data) + @staticmethod + def sign_asymmetric(privkey, data): + return uacrypto.sign_sha1(privkey, data) + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): _logger.warning("DEPRECATED! Do not use SecurityPolicyBasic128Rsa15 anymore!") @@ -798,6 +810,10 @@ class SecurityPolicyBasic256(SecurityPolicy): def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep(pubkey, data) + @staticmethod + def sign_asymmetric(privkey, data): + return uacrypto.sign_sha1(privkey, data) + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): _logger.warning("DEPRECATED! Do not use SecurityPolicyBasic256 anymore!") @@ -878,6 +894,10 @@ class SecurityPolicyBasic256Sha256(SecurityPolicy): def encrypt_asymmetric(pubkey, data): return uacrypto.encrypt_rsa_oaep(pubkey, data) + @staticmethod + def sign_asymmetric(privkey, data): + return uacrypto.sign_sha256(privkey, data) + def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None): if isinstance(peer_cert, bytes): peer_cert = uacrypto.x509_from_der(peer_cert) @@ -961,6 +981,26 @@ def create(self, peer_certificate): ) +def sign_asymmetric(privkey, data, policy_uri): + """ + Sign data with privkey using an asymmetric algorithm. + The algorithm is selected by policy_uri. + Returns a tuple (signature, algorithm_uri) + """ + for cls in [ + SecurityPolicyBasic256Sha256, + SecurityPolicyBasic256, + SecurityPolicyBasic128Rsa15, + SecurityPolicyAes128Sha256RsaOaep, + SecurityPolicyAes256Sha256RsaPss, + ]: + if policy_uri == cls.URI: + return (cls.sign_asymmetric(privkey, data), cls.AsymmetricSignatureURI) + if not policy_uri or policy_uri == SecurityPolicyNone.URI: + return data, "" + raise UaError(f"Unsupported security policy `{policy_uri}`") + + # policy, mode, security_level SECURITY_POLICY_TYPE_MAP = { SecurityPolicyType.NoSecurity: [SecurityPolicyNone, MessageSecurityMode.None_, 0], diff --git a/asyncua/server/internal_server.py b/asyncua/server/internal_server.py index d03085786..e1ea82c0d 100644 --- a/asyncua/server/internal_server.py +++ b/asyncua/server/internal_server.py @@ -409,3 +409,34 @@ def decrypt_user_token(self, isession, token): password = password.decode("utf-8") return user_name, password + + def verify_x509_token(self, isession, token, signature): + """ + verify certificate signature + """ + cert = uacrypto.x509_from_der(token.CertificateData) + alg = signature.Algorithm + sig = signature.Signature + + # TODO check if algorithm is allowed, throw BadSecurityPolicyRejected if not + + challenge = b"" + if self.certificate is not None: + challenge += uacrypto.der_from_x509(self.certificate) + if isession.nonce is not None: + challenge += isession.nonce + + if not (alg and sig): + raise ValueError("No signature") + + if alg == "http://www.w3.org/2000/09/xmldsig#rsa-sha1": + uacrypto.verify_sha1(cert, challenge, sig) + elif alg == "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256": + uacrypto.verify_sha256(cert, challenge, sig) + elif alg == "http://opcfoundation.org/UA/security/rsa-pss-sha2-256": + uacrypto.verify_pss_sha256(cert, challenge, sig) + else: + self.logger.warning("Unknown certificate signature algorithm %s", alg) + raise ValueError("Unknown algorithm") + + return token.CertificateData diff --git a/asyncua/server/internal_session.py b/asyncua/server/internal_session.py index 15a62e0d1..6c171f5f1 100644 --- a/asyncua/server/internal_session.py +++ b/asyncua/server/internal_session.py @@ -126,8 +126,7 @@ def activate_session(self, params, peer_certificate): if isinstance(id_token, ua.UserNameIdentityToken): username, password = self.iserver.decrypt_user_token(self, id_token) elif isinstance(id_token, ua.X509IdentityToken): - # TODO implement verify_x509_token - peer_certificate = id_token.CertificateData + peer_certificate = self.iserver.verify_x509_token(self, id_token, params.UserTokenSignature) username, password = None, None else: username, password = None, None diff --git a/asyncua/server/server.py b/asyncua/server/server.py index 56a95ff40..d3bf038e0 100644 --- a/asyncua/server/server.py +++ b/asyncua/server/server.py @@ -5,7 +5,6 @@ import asyncio import logging import math -from cryptography import x509 from datetime import timedelta, datetime import socket from urllib.parse import urlparse @@ -113,7 +112,6 @@ def __init__(self, iserver: InternalServer = None, user_manager=None): ] # allow all certificates by default self._permission_ruleset = SimpleRoleRuleset() - self.certificate: Optional[x509.Certificate] = None # Use acceptable limits buffer_sz = 65535 max_msg_sz = 100 * 1024 * 1024 # 100mb @@ -233,7 +231,7 @@ async def load_certificate(self, path_or_content: Union[str, bytes, Path], forma """ load server certificate from file, either pem or der """ - self.certificate = await uacrypto.load_certificate(path_or_content, format) + self.iserver.certificate = await uacrypto.load_certificate(path_or_content, format) async def load_private_key(self, path_or_content: Union[str, Path, bytes], password=None, format=None): self.iserver.private_key = await uacrypto.load_private_key(path_or_content, password, format) @@ -385,7 +383,7 @@ async def _setup_server_nodes(self): for policy_type in self._security_policy: policy, mode, level = security_policies.SECURITY_POLICY_TYPE_MAP[policy_type] if policy is not security_policies.SecurityPolicyNone and not ( - self.certificate and self.iserver.private_key + self.iserver.certificate and self.iserver.private_key ): no_cert = True continue @@ -394,7 +392,7 @@ async def _setup_server_nodes(self): security_policies.SecurityPolicyFactory( policy, mode, - self.certificate, + self.iserver.certificate, self.iserver.private_key, permission_ruleset=self._permission_ruleset, ) @@ -417,9 +415,21 @@ def _set_endpoints(self, policy, mode, level): idtoken = ua.UserTokenPolicy() idtoken.PolicyId = "certificate" idtoken.TokenType = ua.UserTokenType.Certificate - idtoken.SecurityPolicyUri = policy.URI - # TODO request signing if mode == ua.MessageSecurityMode.None_ (also need to verify signature then) - idtokens.append(idtoken) + # always request signing + if mode == ua.MessageSecurityMode.None_: + # find first policy with signing + for token_policy_type in self._security_policy: + token_policy, token_mode, _ = security_policies.SECURITY_POLICY_TYPE_MAP[token_policy_type] + if token_mode == ua.MessageSecurityMode.None_: + continue + idtoken.SecurityPolicyUri = token_policy.URI + idtokens.append(idtoken) + break + else: + _logger.warning("No signing policy available, user certificate cannot get verified") + else: + idtoken.SecurityPolicyUri = policy.URI + idtokens.append(idtoken) if ua.UserNameIdentityToken in tokens: idtoken = ua.UserTokenPolicy() @@ -432,7 +442,7 @@ def _set_endpoints(self, policy, mode, level): # use same policy for encryption idtoken.SecurityPolicyUri = policy.URI # try to avoid plaintext password, find first policy with encryption - elif self.certificate and self.iserver.private_key: + elif self.iserver.certificate and self.iserver.private_key: for token_policy_type in self._security_policy: token_policy, token_mode, _ = security_policies.SECURITY_POLICY_TYPE_MAP[token_policy_type] if token_mode != ua.MessageSecurityMode.SignAndEncrypt: @@ -457,8 +467,8 @@ def _set_endpoints(self, policy, mode, level): edp = ua.EndpointDescription() edp.EndpointUrl = self.endpoint.geturl() edp.Server = appdesc - if self.certificate: - edp.ServerCertificate = uacrypto.der_from_x509(self.certificate) + if self.iserver.certificate: + edp.ServerCertificate = uacrypto.der_from_x509(self.iserver.certificate) edp.SecurityMode = mode edp.SecurityPolicyUri = policy.URI edp.UserIdentityTokens = idtokens @@ -473,9 +483,9 @@ async def start(self): """ Start to listen on network """ - if self.certificate is not None: + if self.iserver.certificate is not None: # Log warnings about the certificate - uacrypto.check_certificate(self.certificate, self._application_uri, socket.gethostname()) + uacrypto.check_certificate(self.iserver.certificate, self._application_uri, socket.gethostname()) await self._setup_server_nodes() await self.iserver.start() try: diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 61ba83267..4171cf3c3 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -10,17 +10,10 @@ pytestmark = pytest.mark.asyncio -port_num1 = 48515 -port_num2 = 48512 -port_num3 = 48516 -uri_crypto = "opc.tcp://127.0.0.1:{0:d}".format(port_num1) -uri_no_crypto = "opc.tcp://127.0.0.1:{0:d}".format(port_num2) -uri_crypto_cert = "opc.tcp://127.0.0.1:{0:d}".format(port_num3) +uri_crypto_cert = "opc.tcp://127.0.0.1:48516" BASE_DIR = Path(__file__).parent.parent EXAMPLE_PATH = BASE_DIR / "examples" -srv_crypto_params = [ - (EXAMPLE_PATH / "private-key-example.pem", EXAMPLE_PATH / "certificate-example.der"), -] +srv_crypto_params = (EXAMPLE_PATH / "private-key-example.pem", EXAMPLE_PATH / "certificate-example.der") admin_peer_creds = { "certificate": EXAMPLE_PATH / "certificates/peer-certificate-example-1.der", @@ -38,20 +31,20 @@ } -@pytest.fixture(params=srv_crypto_params) +@pytest.fixture(scope="module") async def srv_crypto_one_cert(request): cert_user_manager = CertificateUserManager() admin_peer_certificate = admin_peer_creds["certificate"] user_peer_certificate = user_peer_creds["certificate"] anonymous_peer_certificate = anonymous_peer_creds["certificate"] - key, cert = request.param + key, cert = srv_crypto_params await cert_user_manager.add_admin(admin_peer_certificate, name="Admin") await cert_user_manager.add_user(user_peer_certificate, name="User") await cert_user_manager.add_role(anonymous_peer_certificate, name="Anonymous", user_role=UserRole.Anonymous) srv = Server(user_manager=cert_user_manager) srv.set_endpoint(uri_crypto_cert) - srv.set_security_policy([ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt]) + srv.set_security_policy([ua.SecurityPolicyType.NoSecurity, ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt]) await srv.init() await srv.load_certificate(cert) await srv.load_private_key(key) @@ -67,14 +60,14 @@ async def srv_crypto_one_cert(request): await srv.stop() -async def test_permissions_admin(srv_crypto_one_cert): +async def test_client_admin(srv_crypto_one_cert): clt = Client(uri_crypto_cert) await clt.set_security( security_policies.SecurityPolicyBasic256Sha256, admin_peer_creds["certificate"], admin_peer_creds["private_key"], None, - server_certificate=srv_crypto_params[0][1], + server_certificate=srv_crypto_params[1], mode=ua.MessageSecurityMode.SignAndEncrypt, ) @@ -87,14 +80,14 @@ async def test_permissions_admin(srv_crypto_one_cert): await child.add_property(0, "MyProperty1", 3) -async def test_permissions_user(srv_crypto_one_cert): +async def test_client_user(srv_crypto_one_cert): clt = Client(uri_crypto_cert) await clt.set_security( security_policies.SecurityPolicyBasic256Sha256, user_peer_creds["certificate"], user_peer_creds["private_key"], None, - server_certificate=srv_crypto_params[0][1], + server_certificate=srv_crypto_params[1], mode=ua.MessageSecurityMode.SignAndEncrypt, ) async with clt: @@ -107,17 +100,62 @@ async def test_permissions_user(srv_crypto_one_cert): await child.add_property(0, "MyProperty2", 3) -async def test_permissions_anonymous(srv_crypto_one_cert): +async def test_client_anonymous(srv_crypto_one_cert): clt = Client(uri_crypto_cert) await clt.set_security( security_policies.SecurityPolicyBasic256Sha256, anonymous_peer_creds["certificate"], anonymous_peer_creds["private_key"], None, - server_certificate=srv_crypto_params[0][1], + server_certificate=srv_crypto_params[1], mode=ua.MessageSecurityMode.SignAndEncrypt, ) async with clt: await clt.get_endpoints() with pytest.raises(ua.uaerrors.BadUserAccessDenied): await clt.nodes.objects.get_children() + + +async def test_x509identity_user(srv_crypto_one_cert): + clt = Client(uri_crypto_cert) + await clt.load_client_certificate(user_peer_creds["certificate"]) + await clt.load_private_key(user_peer_creds["private_key"]) + async with clt: + assert await clt.get_objects_node().get_children() + objects = clt.nodes.objects + child = await objects.get_child(["0:MyObject", "0:MyVariable"]) + await child.set_value(46.0) + assert await child.read_value() == 46.0 + with pytest.raises(ua.uaerrors.BadUserAccessDenied): + await child.add_property(0, "MyProperty3", 3) + + +async def test_x509identity_anonymous(srv_crypto_one_cert): + clt = Client(uri_crypto_cert) + await clt.load_client_certificate(anonymous_peer_creds["certificate"]) + await clt.load_private_key(anonymous_peer_creds["private_key"]) + async with clt: + await clt.get_endpoints() + with pytest.raises(ua.uaerrors.BadUserAccessDenied): + await clt.nodes.objects.get_children() + + +async def test_client_user_x509identity_admin(srv_crypto_one_cert): + clt = Client(uri_crypto_cert) + await clt.set_security( + security_policies.SecurityPolicyBasic256Sha256, + user_peer_creds["certificate"], + user_peer_creds["private_key"], + None, + server_certificate=srv_crypto_params[1], + mode=ua.MessageSecurityMode.SignAndEncrypt, + ) + await clt.load_client_certificate(admin_peer_creds["certificate"]) + await clt.load_private_key(admin_peer_creds["private_key"]) + async with clt: + assert await clt.get_objects_node().get_children() + objects = clt.nodes.objects + child = await objects.get_child(["0:MyObject", "0:MyVariable"]) + await child.set_value(48.0) + assert await child.read_value() == 48.0 + await child.add_property(0, "MyProperty4", 3)