Beispiel #1
0
 def find_endpoint(endpoints, security_mode, policy_uri):
     """
     Find endpoint with required security mode and policy URI
     """
     _logger.info("find_endpoint %r %r %r", endpoints, security_mode,
                  policy_uri)
     for ep in endpoints:
         if (ep.EndpointUrl.startswith(ua.OPC_TCP_SCHEME)
                 and ep.SecurityMode == security_mode
                 and ep.SecurityPolicyUri == policy_uri):
             return ep
     raise ua.UaError(
         f"No matching endpoints: {security_mode}, {policy_uri}")
Beispiel #2
0
 def _process_received_message(self, msg: Union[ua.Message, ua.Acknowledge,
                                                ua.ErrorMessage]):
     if msg is None:
         pass
     elif isinstance(msg, ua.Message):
         self._call_callback(msg.request_id(), msg.body())
     elif isinstance(msg, ua.Acknowledge):
         self._call_callback(0, msg)
     elif isinstance(msg, ua.ErrorMessage):
         self.logger.fatal("Received an error: %r", msg)
         self._call_callback(0, ua.UaStatusCodeError(msg.Error.value))
     else:
         raise ua.UaError(f"Unsupported message type: {msg}")
Beispiel #3
0
 async def enable_history_event(self,
                                source,
                                period=timedelta(days=7),
                                count=0):
     """
     Set attribute History Read of object events to True and start storing data for history
     """
     event_notifier = await source.read_event_notifier()
     if ua.EventNotifier.SubscribeToEvents not in event_notifier:
         raise ua.UaError('Node does not generate events', event_notifier)
     if ua.EventNotifier.HistoryRead not in event_notifier:
         event_notifier.add(ua.EventNotifier.HistoryRead)
         await source.set_event_notifier(event_notifier)
     await self.history_manager.historize_event(source, period, count)
Beispiel #4
0
 def __init__(self, server, nodeid):
     self.server = server
     self.nodeid = None
     if isinstance(nodeid, Node):
         self.nodeid = nodeid.nodeid
     elif isinstance(nodeid, ua.NodeId):
         self.nodeid = nodeid
     elif type(nodeid) in (str, bytes):
         self.nodeid = ua.NodeId.from_string(nodeid)
     elif isinstance(nodeid, int):
         self.nodeid = ua.NodeId(nodeid, 0)
     else:
         raise ua.UaError("argument to node must be a NodeId object or a string defining a nodeid found {0} of type {1}".format(nodeid, type(nodeid)))
     self.basenodeid = None
Beispiel #5
0
    def _check_sym_header(self, security_hdr):
        """
        Validates the symmetric header of the message chunk and revolves the
        security token if needed.
        """
        assert isinstance(
            security_hdr, ua.SymmetricAlgorithmHeader
        ), "Expected SymAlgHeader, got: {0}".format(security_hdr)

        if security_hdr.TokenId == self.security_token.TokenId:
            return

        if security_hdr.TokenId == self.next_security_token.TokenId:
            self.revolve_tokens()
            return

        if self._allow_prev_token and security_hdr.TokenId == self.prev_security_token.TokenId:
            # From spec, part 4, section 5.5.2.1: Clients should accept Messages secured by an
            # expired SecurityToken for up to 25 % of the token lifetime. This should ensure that
            # Messages sent by the Server before the token expired are not rejected because of
            # network delays.
            timeout = self.prev_security_token.CreatedAt + timedelta(
                milliseconds=self.prev_security_token.RevisedLifetime * 1.25)
            if timeout < datetime.utcnow():
                raise ua.UaError(
                    "Security token id {} has timed out ({} < {})".format(
                        security_hdr.TokenId, timeout, datetime.utcnow()))
            return

        expected_tokens = [
            self.security_token.TokenId, self.next_security_token.TokenId
        ]
        if self._allow_prev_token:
            expected_tokens.insert(0, self.prev_security_token.TokenId)
        raise ua.UaError(
            "Invalid security token id {}, expected one of: {}".format(
                security_hdr.TokenId, expected_tokens))
Beispiel #6
0
 async def historize_data_change(self,
                                 node,
                                 period=timedelta(days=7),
                                 count=0):
     """
     Subscribe to the nodes' data changes and store the data in the active storage.
     """
     if not self._sub:
         self._sub = await self._create_subscription(
             SubHandler(self.storage, self.iserver.loop))
     if node in self._handlers:
         raise ua.UaError("Node {0} is already historized".format(node))
     await self.storage.new_historized_node(node.nodeid, period, count)
     handler = await self._sub.subscribe_data_change(node)
     self._handlers[node] = handler
Beispiel #7
0
 async def create_session(self):
     """
     send a CreateSessionRequest to server with reasonable parameters.
     If you want o modify settings look at code of this methods
     and make your own
     """
     desc = ua.ApplicationDescription()
     desc.ApplicationUri = self.application_uri
     desc.ProductUri = self.product_uri
     desc.ApplicationName = ua.LocalizedText(self.name)
     desc.ApplicationType = ua.ApplicationType.Client
     params = ua.CreateSessionParameters()
     # 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
     params.ClientDescription = desc
     params.EndpointUrl = self.server_url.geturl()
     params.SessionName = f"{self.description} Session{self._session_counter}"
     # Requested maximum number of milliseconds that a Session should remain open without activity
     params.RequestedSessionTimeout = self.session_timeout
     params.MaxResponseMessageSize = 0  # means no max size
     response = await self.uaclient.create_session(params)
     if self.security_policy.host_certificate is None:
         data = nonce
     else:
         data = self.security_policy.host_certificate + nonce
     self.security_policy.asymmetric_cryptography.verify(
         data, response.ServerSignature.Signature)
     self._server_nonce = response.ServerNonce
     if not self.security_policy.peer_certificate:
         self.security_policy.peer_certificate = response.ServerCertificate
     elif self.security_policy.peer_certificate != response.ServerCertificate:
         raise ua.UaError("Server certificate mismatch")
     # remember PolicyId's: we will use them in activate_session()
     ep = Client.find_endpoint(response.ServerEndpoints,
                               self.security_policy.Mode,
                               self.security_policy.URI)
     self._policy_ids = ep.UserIdentityTokens
     #  Actual maximum number of milliseconds that a Session shall remain open without activity
     if self.session_timeout != response.RevisedSessionTimeout:
         _logger.warning(
             "Requested session timeout to be %dms, got %dms instead",
             self.secure_channel_timeout, response.RevisedSessionTimeout)
         self.session_timeout = response.RevisedSessionTimeout
     self._renew_channel_task = self.loop.create_task(
         self._renew_channel_loop())
     return response
Beispiel #8
0
 def _call_callback(self, request_id, body):
     try:
         self._callbackmap[request_id].set_result(body)
     except KeyError:
         raise ua.UaError(
             f"No request found for request id: {request_id}, pending are {self._callbackmap.keys()}, body was {body}"
         )
     except asyncio.InvalidStateError:
         if not self.closed:
             self.logger.warning("Future for request id %s is already done",
                                 request_id)
             return
         self.logger.debug(
             "Future for request id %s not handled due to disconnect",
             request_id)
     del self._callbackmap[request_id]
Beispiel #9
0
 def __init__(self,
              security_policy,
              body=b'',
              msg_type=ua.MessageType.SecureMessage,
              chunk_type=ua.ChunkType.Single):
     self.MessageHeader = ua.Header(msg_type, chunk_type)
     if msg_type in (ua.MessageType.SecureMessage,
                     ua.MessageType.SecureClose):
         self.SecurityHeader = ua.SymmetricAlgorithmHeader()
     elif msg_type == ua.MessageType.SecureOpen:
         self.SecurityHeader = ua.AsymmetricAlgorithmHeader()
     else:
         raise ua.UaError(f"Unsupported message type: {msg_type}")
     self.SequenceHeader = ua.SequenceHeader()
     self.Body = body
     self.security_policy = security_policy
Beispiel #10
0
async def get_base_data_type(datatype):
    """
    Looks up the base datatype of the provided datatype Node
    The base datatype is either:
    A primitive type (ns=0, i<=21) or a complex one (ns=0 i>21 and i<=30) like Enum and Struct.

    Args:
        datatype: NodeId of a datype of a variable
    Returns:
        NodeId of datatype base or None in case base datype can not be determined
    """
    base = datatype
    while base:
        if base.nodeid.NamespaceIndex == 0 and isinstance(base.nodeid.Identifier, int) and base.nodeid.Identifier <= 30:
            return base
        base = await get_node_supertype(base)
    raise ua.UaError("Datatype must be a subtype of builtin types {0!s}".format(datatype))
 def _receive(self, msg):
     self._check_incoming_chunk(msg)
     self._incoming_parts.append(msg)
     if msg.MessageHeader.ChunkType == ua.ChunkType.Intermediate:
         return None
     if msg.MessageHeader.ChunkType == ua.ChunkType.Abort:
         err = struct_from_binary(ua.ErrorMessage, ua.utils.Buffer(msg.Body))
         logger.warning("Message %s aborted: %s", msg, err)
         # specs Part 6, 6.7.3 say that aborted message shall be ignored
         # and SecureChannel should not be closed
         self._incoming_parts = []
         return None
     if msg.MessageHeader.ChunkType == ua.ChunkType.Single:
         message = ua.Message(self._incoming_parts)
         self._incoming_parts = []
         return message
     raise ua.UaError("Unsupported chunk type: {0}".format(msg))
Beispiel #12
0
 async def set_security_string(self, string: str):
     """
     Set SecureConnection mode. String format:
     Policy,Mode,certificate,private_key[,server_private_key]
     where Policy is Basic128Rsa15, Basic256 or Basic256Sha256,
         Mode is Sign or SignAndEncrypt
         certificate, private_key and server_private_key are
             paths to .pem or .der files
     Call this before connect()
     """
     if not string:
         return
     parts = string.split(",")
     if len(parts) < 4:
         raise ua.UaError("Wrong format: `{}`, expected at least 4 comma-separated values".format(string))
     policy_class = getattr(security_policies, "SecurityPolicy{}".format(parts[0]))
     mode = getattr(ua.MessageSecurityMode, parts[1])
     return await self.set_security(policy_class, parts[2], parts[3], parts[4] if len(parts) >= 5 else None, mode)
    def receive_from_header_and_body(self, header, body):
        """
        Convert MessageHeader and binary body to OPC UA TCP message (see OPC UA
        specs Part 6, 7.1: Hello, Acknowledge or ErrorMessage), or a Message
        object, or None (if intermediate chunk is received)
        """
        if header.MessageType == ua.MessageType.SecureOpen:
            data = body.copy(header.body_size)
            security_header = struct_from_binary(ua.AsymmetricAlgorithmHeader,
                                                 data)

            if not self.is_open():
                # Only call select_policy if the channel isn't open. Otherwise
                # it will break the Secure channel renewal.
                self.select_policy(security_header.SecurityPolicyURI,
                                   security_header.SenderCertificate)

        elif header.MessageType in (ua.MessageType.SecureMessage,
                                    ua.MessageType.SecureClose):
            data = body.copy(header.body_size)
            security_header = struct_from_binary(ua.SymmetricAlgorithmHeader,
                                                 data)
            self._check_sym_header(security_header)

        if header.MessageType in (ua.MessageType.SecureMessage,
                                  ua.MessageType.SecureOpen,
                                  ua.MessageType.SecureClose):
            chunk = MessageChunk.from_header_and_body(self.security_policy,
                                                      header, body)
            return self._receive(chunk)
        if header.MessageType == ua.MessageType.Hello:
            msg = struct_from_binary(ua.Hello, body)
            self._max_chunk_size = msg.ReceiveBufferSize
            return msg
        if header.MessageType == ua.MessageType.Acknowledge:
            msg = struct_from_binary(ua.Acknowledge, body)
            self._max_chunk_size = msg.SendBufferSize
            return msg
        if header.MessageType == ua.MessageType.Error:
            msg = struct_from_binary(ua.ErrorMessage, body)
            logger.warning(f"Received an error: {msg}")
            return msg
        raise ua.UaError(f"Unsupported message type {header.MessageType}")
Beispiel #14
0
    def receive_from_header_and_body(self, header, body):
        """
        Convert MessageHeader and binary body to OPC UA TCP message (see OPC UA
        specs Part 6, 7.1: Hello, Acknowledge or ErrorMessage), or a Message
        object, or None (if intermediate chunk is received)
        """
        if header.MessageType == ua.MessageType.SecureOpen:
            data = body.copy(header.body_size)
            security_header = struct_from_binary(ua.AsymmetricAlgorithmHeader,
                                                 data)
            self.select_policy(security_header.SecurityPolicyURI,
                               security_header.SenderCertificate)
        elif header.MessageType in (ua.MessageType.SecureMessage,
                                    ua.MessageType.SecureClose):
            data = body.copy(header.body_size)
            security_header = struct_from_binary(ua.SymmetricAlgorithmHeader,
                                                 data)
            self._check_sym_header(security_header)

        if header.MessageType in (ua.MessageType.SecureMessage,
                                  ua.MessageType.SecureOpen,
                                  ua.MessageType.SecureClose):
            chunk = MessageChunk.from_header_and_body(self.security_policy,
                                                      header, body)
            return self._receive(chunk)
        elif header.MessageType == ua.MessageType.Hello:
            msg = struct_from_binary(ua.Hello, body)
            self._max_chunk_size = msg.ReceiveBufferSize
            return msg
        elif header.MessageType == ua.MessageType.Acknowledge:
            msg = struct_from_binary(ua.Acknowledge, body)
            self._max_chunk_size = msg.SendBufferSize
            return msg
        elif header.MessageType == ua.MessageType.Error:
            msg = struct_from_binary(ua.ErrorMessage, body)
            logger.warning("Received an error: %s", msg)
            return msg
        else:
            raise ua.UaError("Unsupported message type {0}".format(
                header.MessageType))
Beispiel #15
0
    async def set_security_string(self, string: str):
        """
        Set SecureConnection mode.

        :param string: Mode format ``Policy,Mode,certificate,private_key[,server_private_key]``

        where:

        - ``Policy`` is ``Basic128Rsa15``, ``Basic256`` or ``Basic256Sha256``
        - ``Mode`` is ``Sign`` or ``SignAndEncrypt``
        - ``certificate`` and ``server_private_key`` 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()
        """
        if not string:
            return
        parts = string.split(",")
        if len(parts) < 4:
            raise ua.UaError(
                "Wrong format: `{}`, expected at least 4 comma-separated values"
                .format(string))

        if '::' in parts[
                3]:  # if the filename contains a colon, assume it's a conjunction and parse it
            parts[3], client_key_password = parts[3].split('::')
        else:
            client_key_password = None

        policy_class = getattr(security_policies,
                               "SecurityPolicy{}".format(parts[0]))
        mode = getattr(ua.MessageSecurityMode, parts[1])
        return await self.set_security(policy_class, parts[2], parts[3],
                                       client_key_password,
                                       parts[4] if len(parts) >= 5 else None,
                                       mode)
Beispiel #16
0
 def _call_callback(self, request_id, body):
     future = self._callbackmap.pop(request_id, None)
     if future is None:
         raise ua.UaError("No request found for requestid: {0}, callbacks in list are {1}".format(
             request_id, self._callbackmap.keys()))
     future.set_result(body)
Beispiel #17
0
 def encrypted_size(self, plain_size):
     size = plain_size + self.security_policy.signature_size()
     pbs = self.security_policy.plain_block_size()
     if size % pbs != 0:
         raise ua.UaError("Encryption error")
     return size // pbs * self.security_policy.encrypted_block_size()