1
0
mirror of https://github.com/FreeOpcUa/opcua-asyncio synced 2025-10-29 17:07:18 +08:00

fix connecting to new prosys opcua server (#1827)

* make prosys accept our certificate again in examples

* allow to send certificate without encryption or user identification as prosys now requires.
Had to fix a few new bugs that sudently appeared...

* clean imports

* remove leftover prints, less verbose when renaming struct

* remove unused variable

---------

Co-authored-by: Olivier <olivier@helitech>
This commit is contained in:
oroulet
2025-04-29 15:55:27 +02:00
committed by GitHub
parent 120d2e8c09
commit 5b1091795d
19 changed files with 163 additions and 48 deletions

View File

@@ -2,12 +2,13 @@ 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
from urllib.parse import urlparse, unquote, ParseResult
from pathlib import Path
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
import asyncua
from asyncua import ua
from .ua_client import UaClient
@@ -65,9 +66,9 @@ class Client:
if have_password:
self._password = unquote(password)
self.name = "Pure Python Async. Client"
self.name = "Pure Python Async Client"
self.description = self.name
self.application_uri = "urn:freeopcua:client"
self.application_uri = "urn:example.org:FreeOpcUa:opcua-asyncio"
self.product_uri = "urn:freeopcua.github.io:client"
self.security_policy = security_policies.SecurityPolicyNone()
self.secure_channel_id = None
@@ -497,10 +498,16 @@ class Client:
desc.ApplicationName = ua.LocalizedText(self.name)
desc.ApplicationType = ua.ApplicationType.Client
params = ua.CreateSessionParameters()
params.ServerUri = f"urn:{self.server_url.hostname}{self.server_url.path.replace('/', ':')}"
# at least 32 random bytes for server to prove possession of private key (specs part 4, 5.6.2.2)
nonce = create_nonce(32)
params.ClientNonce = nonce
params.ClientCertificate = self.security_policy.host_certificate
if self.security_policy.host_certificate:
params.ClientCertificate = self.security_policy.host_certificate
elif self.user_certificate:
params.ClientCertificate = uacrypto.der_from_x509(self.user_certificate)
else:
params.ClientCertificate = None
params.ClientDescription = desc
params.EndpointUrl = self.server_url.geturl()
params.SessionName = f"{self.description} Session{self._session_counter}"
@@ -639,7 +646,7 @@ class Client:
"""
Activate session using either username and password or private_key
"""
user_certificate = certificate or self.user_certificate
user_certificate = certificate
params = ua.ActivateSessionParameters()
challenge = b""
if self.security_policy.peer_certificate is not None:
@@ -652,7 +659,7 @@ class Client:
params.ClientSignature.Algorithm = security_policies.SecurityPolicyBasic256.AsymmetricSignatureURI
params.ClientSignature.Signature = self.security_policy.asymmetric_cryptography.signature(challenge)
params.LocaleIds = self._locale
if not username and not user_certificate:
if not username and not (user_certificate and self.user_private_key):
self._add_anonymous_auth(params)
elif user_certificate:
self._add_certificate_auth(params, user_certificate, challenge)

View File

@@ -219,7 +219,6 @@ class UaDirectory:
and shall be ignored by the caller.
"""
_logger.debug("Request to create file %s in %s", file_name, self._directory_node)
print(f"Request to create file {file_name} in {self._directory_node}")
create_file_node = await self._directory_node.get_child("CreateFile")
arg1_file_name = Variant(file_name, VariantType.String)
arg2_request_file_open = Variant(request_file_open, VariantType.Boolean)

View File

@@ -132,7 +132,7 @@ def clean_name(name):
return name
newname = re.sub(r"\W+", "_", name)
newname = re.sub(r"^[0-9]+", r"_\g<0>", newname)
_logger.warning("renamed %s to %s due to Python syntax", name, newname)
_logger.info("renamed %s to %s due to Python syntax", name, newname)
return newname
@@ -363,7 +363,8 @@ async def _recursive_parse(server, base_node, dtypes, parent_sdef=None, add_exis
if parent_sdef:
for sfield in reversed(parent_sdef.Fields):
sdef.Fields.insert(0, sfield)
dtypes.append(DataTypeSorter(desc.NodeId, name, desc, sdef))
if isinstance(sdef, ua.StructureDefinition):
dtypes.append(DataTypeSorter(desc.NodeId, name, desc, sdef))
return _recursive_parse(
server,
server.get_node(desc.NodeId),

View File

@@ -1,5 +1,8 @@
from enum import Enum
from dataclasses import dataclass
from typing import Optional
from asyncua import ua
from asyncua.server.users import UserRole
ADMIN_TYPES = [
ua.ObjectIds.RegisterServerRequest_Encoding_DefaultBinary,
@@ -37,6 +40,22 @@ USER_TYPES = [
]
class UserRole(Enum):
"""
User Roles
"""
Admin = 0
Anonymous = 1
User = 3
@dataclass
class User:
role: UserRole = UserRole.Anonymous
name: Optional[str] = None
class PermissionRuleset:
"""
Base class for permission ruleset

View File

@@ -139,9 +139,8 @@ class TrustStore:
store_ctx.verify_certificate()
_logger.debug("Use trusted certificate : '%s'", _certificate.get_subject().CN)
return True
except crypto.X509StoreContextError as exp:
print(exp)
_logger.warning('Not trusted certificate used: "%s"', _certificate.get_subject().CN)
except crypto.X509StoreContextError:
_logger.exception('Not trusted certificate used: "%s"', _certificate.get_subject().CN)
return False
async def _load_trust_location(self, location: Path):

View File

@@ -33,8 +33,8 @@ if TYPE_CHECKING:
] # FIXME Check, if there are missing attribute types.
from asyncua import ua
from asyncua.crypto.permission_rules import User, UserRole
from .users import User, UserRole
_logger = logging.getLogger(__name__)

View File

@@ -20,7 +20,7 @@ from .history import HistoryManager
from .address_space import NodeData, AddressSpace, AttributeService, ViewService, NodeManagementService, MethodService
from .subscription_service import SubscriptionService
from .standard_address_space import standard_address_space
from .users import User, UserRole
from asyncua.crypto.permission_rules import User, UserRole
from .internal_session import InternalSession
from .event_generator import EventGenerator
from ..crypto.validator import CertificateValidatorMethod

View File

@@ -10,7 +10,7 @@ from ..common.callback import CallbackType, ServerItemCallback
from ..common.utils import create_nonce, ServiceError
from ..crypto.uacrypto import x509
from .address_space import AddressSpace
from .users import User, UserRole
from asyncua.crypto.permission_rules import User, UserRole
from .subscription_service import SubscriptionService
if TYPE_CHECKING:

View File

@@ -3,7 +3,7 @@ from pathlib import Path
from typing import Union
from asyncua.crypto import uacrypto
from asyncua.server.users import User, UserRole
from asyncua.crypto.permission_rules import User, UserRole
class UserManager:

View File

@@ -1,23 +0,0 @@
"""
Implement user management here.
"""
from enum import Enum
from dataclasses import dataclass
from typing import Optional
class UserRole(Enum):
"""
User Roles
"""
Admin = 0
Anonymous = 1
User = 3
@dataclass
class User:
role: UserRole = UserRole.Anonymous
name: Optional[str] = None

18
examples/cert-config.cnf Normal file
View File

@@ -0,0 +1,18 @@
[ req ]
default_bits = 2048
default_md = sha512
distinguished_name = req_distinguished_name
x509_extensions = v3_ext
prompt = no
[req_distinguished_name]
CN= freeopcua@somewhere
O= My Organization
DC= helitack
[ v3_ext ]
subjectAltName = URI:urn:example.org:FreeOpcUa:opcua-asyncio
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = clientAuth, serverAuth
basicConstraints = critical, CA:false

View File

@@ -17,13 +17,15 @@ class SubHandler:
async def main():
url = "opc.tcp://localhost:53530/OPCUA/SimulationServer/"
url = "opc.tcp://localhost:53530/OPCUA/SimulationServer"
# url = "opc.tcp://olivier:olivierpass@localhost:53530/OPCUA/SimulationServer/"
async with Client(url=url) as client:
client = Client(url=url)
await client.load_client_certificate("my_cert.der")
async with client:
await client.load_data_type_definitions(overwrite_existing=True)
print("Root children are", await client.nodes.root.get_children())
if __name__ == "__main__":
logging.basicConfig(level=logging.WARN)
logging.basicConfig(level=logging.DEBUG)
asyncio.run(main())

View File

@@ -9,7 +9,7 @@ from asyncua import Client
async def main():
client = Client("opc.tcp://localhost:53530/OPCUA/SimulationServer/")
await client.set_security_string("Basic256Sha256,Sign,certificate-example.der,private-key-example.pem")
await client.set_security_string("Basic256Sha256,Sign,my_cert.der,my_private_key.pem")
client.session_timeout = 2000
async with client:
root = client.nodes.root

View File

@@ -38,5 +38,13 @@ Step 3: openssl req -x509 -days 365 -new -out certificate.pem -key key.pem -conf
this way is proved with Siemens OPC UA Client/Server!
'
openssl req -x509 -newkey rsa:4096 -sha256 -keyout my_private_key.pem -out my_cert.pem -days 3650 -nodes -addext "subjectAltName = URI:urn:example.org:FreeOpcUa:python-opcua"
# Step 1: Generate PEM certificate and private key with correct extensions
openssl req -x509 -newkey rsa:4096 -sha512 \
-keyout my_private_key.pem -out my_cert.pem \
-days 3650 -nodes -config cert-config.cnf
# Step 2: Convert certificate to DER format for OPC UA
openssl x509 -outform der -in my_cert.pem -out my_cert.der

BIN
examples/my_cert.der Normal file

Binary file not shown.

33
examples/my_cert.pem Normal file
View File

@@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE-----

View File

@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
+uSCWvvuNLawTWfq56DriXUkrXxQn/Y=
-----END PRIVATE KEY-----

View File

@@ -1,7 +1,7 @@
import pytest
from asyncua import Client, Server, ua
from asyncua.server.users import UserRole, User
from asyncua.crypto.permission_rules import UserRole, User
uri = "opc.tcp://127.0.0.1:48517/baz/server"
uri_creds = "opc.tcp://foobar:hR%26yjjGhP%246%40nQ4e@127.0.0.1:48517/baz/server"

View File

@@ -4,7 +4,7 @@ import pytest
from asyncua import Client
from asyncua import Server
from asyncua import ua
from asyncua.server.users import UserRole
from asyncua.crypto.permission_rules import UserRole
from asyncua.server.user_managers import CertificateUserManager
from asyncua.crypto import security_policies