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

support communication x509 certificate chain

This commit is contained in:
Hryhorii Biloshenko
2025-09-17 16:12:23 +02:00
committed by oroulet
parent 2b6f3e5bd6
commit a43af60bd1
3 changed files with 71 additions and 32 deletions

View File

@@ -1,29 +1,30 @@
import asyncio
import dataclasses
import logging
import socket
import dataclasses
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 typing import Any, Callable, Coroutine, Dict, Iterable, List, Optional, Sequence, Tuple, Type, Union, cast
from urllib.parse import ParseResult, unquote, urlparse
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
import asyncua
from asyncua import ua
from .ua_client import UaClient
from ..common.xmlimporter import XmlImporter
from ..common.xmlexporter import XmlExporter
from ..common.node import Node
from ..common.manage_nodes import delete_nodes
from ..common.subscription import Subscription, SubscriptionHandler
from ..common.node import Node
from ..common.shortcuts import Shortcuts
from ..common.structures import load_type_definitions, load_enums
from ..common.structures import load_enums, load_type_definitions
from ..common.structures104 import load_data_type_definitions
from ..common.utils import create_nonce, ServiceError
from ..common.ua_utils import value_to_datavalue, copy_dataclass_attr
from ..crypto import uacrypto, security_policies
from ..common.subscription import Subscription, SubscriptionHandler
from ..common.ua_utils import copy_dataclass_attr, value_to_datavalue
from ..common.utils import ServiceError, create_nonce
from ..common.xmlexporter import XmlExporter
from ..common.xmlimporter import XmlImporter
from ..crypto import security_policies, uacrypto
from ..crypto.validator import CertificateValidatorMethod
from .ua_client import UaClient
_logger = logging.getLogger(__name__)
@@ -198,6 +199,7 @@ class Client:
private_key_password: Optional[Union[str, bytes]] = None,
server_certificate: Optional[Union[str, uacrypto.CertProperties, bytes]] = None,
mode: ua.MessageSecurityMode = ua.MessageSecurityMode.SignAndEncrypt,
certificate_chain: Optional[Sequence[Union[str, uacrypto.CertProperties, bytes, Path]]] = None,
) -> None:
"""
Set SecureConnection mode.
@@ -222,9 +224,14 @@ class Client:
server_certificate = uacrypto.CertProperties(server_certificate)
if not isinstance(certificate, uacrypto.CertProperties):
certificate = uacrypto.CertProperties(certificate)
certificate_chain = certificate_chain or []
chain = [
cert if isinstance(cert, uacrypto.CertProperties) else uacrypto.CertProperties(cert)
for cert in certificate_chain
]
if not isinstance(private_key, uacrypto.CertProperties):
private_key = uacrypto.CertProperties(private_key, password=private_key_password)
return await self._set_security(policy, certificate, private_key, server_certificate, mode)
return await self._set_security(policy, certificate, private_key, server_certificate, mode, chain)
async def _set_security(
self,
@@ -233,17 +240,22 @@ class Client:
private_key: uacrypto.CertProperties,
server_cert: uacrypto.CertProperties,
mode: ua.MessageSecurityMode = ua.MessageSecurityMode.SignAndEncrypt,
certificate_chain: Optional[Sequence[uacrypto.CertProperties]] = None,
) -> None:
if isinstance(server_cert, uacrypto.CertProperties):
server_cert = await uacrypto.load_certificate(server_cert.path_or_content, server_cert.extension)
cert = await uacrypto.load_certificate(certificate.path_or_content, certificate.extension)
certificate_chain = certificate_chain or []
chain = await asyncio.gather(
*(uacrypto.load_certificate(cert.path_or_content, cert.extension) for cert in certificate_chain)
)
pk = await uacrypto.load_private_key(
private_key.path_or_content,
private_key.password,
private_key.extension,
)
uacrypto.check_certificate(cert, self.application_uri, socket.gethostname())
self.security_policy = policy(server_cert, cert, pk, mode) # type: ignore
self.security_policy = policy(server_cert, cert, pk, mode, host_cert_chain=chain)
self.uaclient.set_security(self.security_policy)
async def load_client_certificate(self, path: str, extension: Optional[str] = None) -> None:

View File

@@ -1,14 +1,14 @@
from dataclasses import dataclass
import hashlib
from datetime import datetime, timedelta, timezone
import logging
import copy
import hashlib
import logging
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
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
from ..crypto.uacrypto import InvalidSignature
from ..crypto.uacrypto import InvalidSignature
from ..ua.ua_binary import header_from_binary, header_to_binary, struct_from_binary, struct_to_binary
_logger = logging.getLogger("asyncua.uaprotocol")
@@ -190,6 +190,8 @@ class MessageChunk:
chunk.SecurityHeader.SecurityPolicyURI = security_policy.URI
if security_policy.host_certificate and security_policy.Mode != ua.MessageSecurityMode.None_:
chunk.SecurityHeader.SenderCertificate = security_policy.host_certificate
for cert in security_policy.host_certificate_chain:
chunk.SecurityHeader.SenderCertificate += cert
if security_policy.peer_certificate:
chunk.SecurityHeader.ReceiverCertificateThumbPrint = hashlib.sha1(
security_policy.peer_certificate

View File

@@ -1,18 +1,18 @@
from __future__ import annotations
import logging
import struct
import time
from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING, Optional
from abc import ABCMeta, abstractmethod
from ..ua import SecurityPolicyType, MessageSecurityMode, UaError
from ..ua import MessageSecurityMode, SecurityPolicyType, UaError
if TYPE_CHECKING:
from ..crypto.permission_rules import PermissionRuleset
from ..crypto import uacrypto
_logger = logging.getLogger(__name__)
@@ -488,9 +488,10 @@ class SecurityPolicy:
peer_certificate: Optional[bytes]
host_certificate: Optional[bytes]
permissions: Optional[PermissionRuleset]
host_certificate_chain: list[bytes]
@abstractmethod
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None):
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None, host_cert_chain=None):
pass
@abstractmethod
@@ -509,7 +510,13 @@ class SecurityPolicyNone(SecurityPolicy):
secure_channel_nonce_length: int = 0
def __init__(
self, peer_cert=None, host_cert=None, host_privkey=None, mode=MessageSecurityMode.None_, permission_ruleset=None
self,
peer_cert=None,
host_cert=None,
host_privkey=None,
mode=MessageSecurityMode.None_,
permission_ruleset=None,
host_cert_chain=None,
):
if isinstance(peer_cert, bytes):
peer_cert = uacrypto.x509_from_der(peer_cert)
@@ -518,6 +525,8 @@ class SecurityPolicyNone(SecurityPolicy):
self.Mode = mode
self.peer_certificate = uacrypto.der_from_x509(peer_cert)
self.host_certificate = uacrypto.der_from_x509(host_cert)
host_cert_chain = host_cert_chain or []
self.host_certificate_chain = [uacrypto.der_from_x509(cert) for cert in host_cert_chain]
self.permissions = permission_ruleset
def make_local_symmetric_key(self, secret, seed):
@@ -568,7 +577,7 @@ class SecurityPolicyAes128Sha256RsaOaep(SecurityPolicy):
def sign_asymmetric(privkey, data):
return uacrypto.sign_sha256(privkey, data)
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None):
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None, host_cert_chain=None):
if isinstance(peer_cert, bytes):
peer_cert = uacrypto.x509_from_der(peer_cert)
# even in Sign mode we need to asymmetrically encrypt secrets
@@ -582,6 +591,8 @@ class SecurityPolicyAes128Sha256RsaOaep(SecurityPolicy):
self.Mode = mode
self.peer_certificate = uacrypto.der_from_x509(peer_cert)
self.host_certificate = uacrypto.der_from_x509(host_cert)
host_cert_chain = host_cert_chain or []
self.host_certificate_chain = [uacrypto.der_from_x509(cert) for cert in host_cert_chain]
self.permissions = permission_ruleset
def make_local_symmetric_key(self, secret, seed):
@@ -646,7 +657,7 @@ class SecurityPolicyAes256Sha256RsaPss(SecurityPolicy):
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):
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None, host_cert_chain=None):
if isinstance(peer_cert, bytes):
peer_cert = uacrypto.x509_from_der(peer_cert)
# even in Sign mode we need to asymmetrically encrypt secrets
@@ -660,6 +671,8 @@ class SecurityPolicyAes256Sha256RsaPss(SecurityPolicy):
self.Mode = mode
self.peer_certificate = uacrypto.der_from_x509(peer_cert)
self.host_certificate = uacrypto.der_from_x509(host_cert)
host_cert_chain = host_cert_chain or []
self.host_certificate_chain = [uacrypto.der_from_x509(cert) for cert in host_cert_chain]
self.permissions = permission_ruleset
def make_local_symmetric_key(self, secret, seed):
@@ -730,7 +743,7 @@ class SecurityPolicyBasic128Rsa15(SecurityPolicy):
def sign_asymmetric(privkey, data):
return uacrypto.sign_sha1(privkey, data)
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None):
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None, host_cert_chain=None):
_logger.warning("DEPRECATED! Do not use SecurityPolicyBasic128Rsa15 anymore!")
if isinstance(peer_cert, bytes):
@@ -746,6 +759,8 @@ class SecurityPolicyBasic128Rsa15(SecurityPolicy):
self.Mode = mode
self.peer_certificate = uacrypto.der_from_x509(peer_cert)
self.host_certificate = uacrypto.der_from_x509(host_cert)
host_cert_chain = host_cert_chain or []
self.host_certificate_chain = [uacrypto.der_from_x509(cert) for cert in host_cert_chain]
self.permissions = permission_ruleset
def make_local_symmetric_key(self, secret, seed):
@@ -814,7 +829,7 @@ class SecurityPolicyBasic256(SecurityPolicy):
def sign_asymmetric(privkey, data):
return uacrypto.sign_sha1(privkey, data)
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None):
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None, host_cert_chain=None):
_logger.warning("DEPRECATED! Do not use SecurityPolicyBasic256 anymore!")
if isinstance(peer_cert, bytes):
@@ -830,6 +845,8 @@ class SecurityPolicyBasic256(SecurityPolicy):
self.Mode = mode
self.peer_certificate = uacrypto.der_from_x509(peer_cert)
self.host_certificate = uacrypto.der_from_x509(host_cert)
host_cert_chain = host_cert_chain or []
self.host_certificate_chain = [uacrypto.der_from_x509(cert) for cert in host_cert_chain]
self.permissions = permission_ruleset
def make_local_symmetric_key(self, secret, seed):
@@ -898,7 +915,7 @@ class SecurityPolicyBasic256Sha256(SecurityPolicy):
def sign_asymmetric(privkey, data):
return uacrypto.sign_sha256(privkey, data)
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None):
def __init__(self, peer_cert, host_cert, host_privkey, mode, permission_ruleset=None, host_cert_chain=None):
if isinstance(peer_cert, bytes):
peer_cert = uacrypto.x509_from_der(peer_cert)
# even in Sign mode we need to asymmetrically encrypt secrets
@@ -912,6 +929,8 @@ class SecurityPolicyBasic256Sha256(SecurityPolicy):
self.Mode = mode
self.peer_certificate = uacrypto.der_from_x509(peer_cert)
self.host_certificate = uacrypto.der_from_x509(host_cert)
host_cert_chain = host_cert_chain or []
self.host_certificate_chain = [uacrypto.der_from_x509(cert) for cert in host_cert_chain]
self.permissions = permission_ruleset
def make_local_symmetric_key(self, secret, seed):
@@ -965,11 +984,12 @@ class SecurityPolicyFactory:
SecurityPolicy for every client and client's certificate
"""
def __init__(self, cls, mode, certificate=None, private_key=None, permission_ruleset=None):
def __init__(self, cls, mode, certificate=None, private_key=None, permission_ruleset=None, certificate_chain=None):
self.cls = cls
self.mode = mode
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
self.permission_ruleset = permission_ruleset
def matches(self, uri, mode=None):
@@ -977,7 +997,12 @@ class SecurityPolicyFactory:
def create(self, peer_certificate):
return self.cls(
peer_certificate, self.certificate, self.private_key, self.mode, permission_ruleset=self.permission_ruleset
peer_certificate,
self.certificate,
self.private_key,
self.mode,
permission_ruleset=self.permission_ruleset,
host_cert_chain=self.certificate_chain,
)