Skip to content
369 changes: 369 additions & 0 deletions documentation/modules/auxiliary/scanner/scada/opcua_endpoint_enum.md

Large diffs are not rendered by default.

124 changes: 124 additions & 0 deletions lib/rex/proto/opc_ua/enums.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# -*- coding: binary -*-
# frozen_string_literal: true

# Enumerated values and identifiers from the OPC-UA specification.
#
# The numeric tables here are transcribed from the OPC Foundation's own
# machine-readable definitions rather than from prose, and can be re-verified
# against them:
#
# StatusCodes reference/opcua/StatusCode.csv
# NodeIds reference/opcua/NodeIds.csv
#
# Both are also published at
# https://github.com/OPCFoundation/UA-Nodeset/blob/latest/Schema/. Every value
# in this file has been checked against the local copies; note that
# StatusCode.csv spells the names without the underscore used here and in the
# specification prose, so Bad_TcpServerTooBusy is BadTcpServerTooBusy there.
module Rex::Proto::OpcUa::Enums
# SecurityPolicy URI for the None policy. An endpoint offering this applies
# no signing or encryption, so a channel opened under it is readable on the
# wire and needs no key material from the client.
NONE_POLICY_URI = 'http://opcfoundation.org/UA/SecurityPolicy#None'

# Returned by the name lookups when a value is outside the enumeration.
UNKNOWN_NAME = 'Unknown'

# NodeId identifiers for the DefaultBinary encodings of the services used
# over this transport. All are in namespace 0. A request and its response
# differ by three, the intervening identifier being the XML encoding.
#
# All six were checked against reference/opcua/NodeIds.csv, where each appears
# as <ServiceName>_Encoding_DefaultBinary. The OpenSecureChannel and
# GetEndpoints response identifiers were also read back off the wire from the
# captures in spec/file_fixtures/opc_ua.
module NodeIds
OPEN_SECURE_CHANNEL_REQUEST = 446
OPEN_SECURE_CHANNEL_RESPONSE = 449
CLOSE_SECURE_CHANNEL_REQUEST = 452
CLOSE_SECURE_CHANNEL_RESPONSE = 455
GET_ENDPOINTS_REQUEST = 428
GET_ENDPOINTS_RESPONSE = 431
end

# MessageSecurityMode (Part 4, section 7.20, Table 139), matching the
# enumeration of the same name in reference/opcua/Opc.Ua.Types.bsd.
SECURITY_MODES = {
0 => 'Invalid',
1 => 'None',
2 => 'Sign',
3 => 'SignAndEncrypt'
}.freeze

# UserTokenType (Part 4, section 7.42, Table 193), matching the enumeration
# of the same name in reference/opcua/Opc.Ua.Types.bsd.
TOKEN_TYPES = {
0 => 'Anonymous',
1 => 'UserName',
2 => 'Certificate',
3 => 'IssuedToken'
}.freeze

# StatusCodes that may appear in an ERR response from the UA TCP transport,
# or as the ServiceResult of a service that failed at the security layer.
STATUS_CODES = {
# The Connection Protocol error codes of Table 79 in OPC-UA Specification
# Part 6, section 7.1.5. That table names the codes; their numeric values
# are in Part 6 Annex A.2, and every one below was checked against
# reference/opcua/StatusCode.csv.
0x807D0000 => 'Bad_TcpServerTooBusy',
0x807E0000 => 'Bad_TcpMessageTypeInvalid',
0x807F0000 => 'Bad_TcpSecureChannelUnknown',
0x80800000 => 'Bad_TcpMessageTooLarge',
0x80810000 => 'Bad_TcpNotEnoughResources',
0x80820000 => 'Bad_TcpInternalError',
0x80830000 => 'Bad_TcpEndpointUrlInvalid',
# Not in Table 79, but seen at this layer all the same.
# Bad_ProtocolVersionUnsupported is named in the Hello Message text of
# section 7.1.2.3; the rest arrive as the ServiceResult of a service that
# failed at the security layer.
0x80BE0000 => 'Bad_ProtocolVersionUnsupported',
0x80130000 => 'Bad_SecurityChecksFailed',
0x80120000 => 'Bad_CertificateInvalid',
0x80840000 => 'Bad_RequestInterrupted',
0x80850000 => 'Bad_RequestTimeout',
0x80860000 => 'Bad_SecureChannelClosed',
0x80870000 => 'Bad_SecureChannelTokenUnknown',
0x80AC0000 => 'Bad_ConnectionRejected',
0x80AE0000 => 'Bad_ConnectionClosed'
}.freeze

module_function

# @param code [Integer] a StatusCode as it appears on the wire.
# @return [String] the StatusCode name, or the value in hexadecimal when it
# is not one this table carries.
def status_code_name(code)
STATUS_CODES[code] || format('0x%08X', code)
end

# @param mode [Integer] a MessageSecurityMode value.
# @return [String] the mode name, or Unknown with the value.
def security_mode_name(mode)
SECURITY_MODES[mode] || "#{UNKNOWN_NAME}(#{mode})"
end

# @param type [Integer] a UserTokenType value.
# @return [String] the token type name, or Unknown with the value.
def user_token_type_name(type)
TOKEN_TYPES[type] || "#{UNKNOWN_NAME}(#{type})"
end

# Reduce a SecurityPolicy URI to the fragment that names the policy, so that
# http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 reports as
# Basic256Sha256. A URI carrying no fragment is returned whole rather than
# discarded, since an unrecognised policy is still worth reporting.
#
# @param uri [String, nil] a SecurityPolicyUri.
# @return [String] the policy name.
def security_policy_name(uri)
return UNKNOWN_NAME if uri.nil? || uri.empty?

uri.include?('#') ? uri.rpartition('#').last : uri
end
end
109 changes: 109 additions & 0 deletions lib/rex/proto/opc_ua/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# -*- coding: binary -*-
# frozen_string_literal: true

# Errors raised by the OPC-UA library.
#
# Everything here descends from OpcUaError, so a caller that only needs to know
# that the conversation failed can rescue the family in one clause, while a
# caller that wants to report why can rescue the individual classes. This
# follows Rex::Proto::Thrift::Error and Rex::Proto::Amqp::Error, which solve the
# same problem for their transports.
#
# OpcUaError descends from Rex::RuntimeError, which means every error here is
# both a StandardError and a Rex::Exception without any of them saying so
# individually: Rex::RuntimeError includes the Rex::Exception marker module, so
# `rescue Rex::Exception` catches these alongside Rex's own.
#
# The distinction the classes draw is between a fault in our reading of the
# connection and a fault the server reported, because a scanner reports those
# very differently: a TimeoutError against a host that never answers is not
# worth printing, whereas a ServerError is a positive result.
module Rex::Proto::OpcUa::Error
# Base class of OPC-UA specific errors.
class OpcUaError < Rex::RuntimeError; end

# Raised when a read does not complete before its deadline, either because
# nothing arrived or because only part of a message did. Rex sockets report a
# closed connection by raising EOFError rather than by timing out, and that is
# left to propagate as itself.
class TimeoutError < OpcUaError; end

# Raised when the UA TCP framing is unusable: a message size outside the
# permitted range, a chunk too short to hold its own headers, a message or
# chunk type that has no meaning here, or a response that ran past the chunk
# ceiling. See OPC-UA Specification Part 6, section 7.1.
class FramingError < OpcUaError; end

# Base of the two errors that carry a StatusCode and a Reason the server put
# on the wire. Both bodies have the same two fields in the same order: an ERR
# message body is Table 76 of OPC-UA Specification Part 6, section 7.1.2.5,
# and an abort chunk body is Table 63 of section 6.7.3.
#
# These are reports from the server rather than faults in reading it, and the
# StatusCode is the useful part, so it is kept as a field rather than only
# interpolated into the message.
class StatusReportError < OpcUaError
# @return [Integer, nil] the StatusCode the server sent, or nil when the
# body could not be decoded.
attr_reader :status_code

# @return [String, nil] the Reason the server sent. Servers routinely leave
# this null.
attr_reader :reason

# @param status_code [Integer, nil] the StatusCode from the body.
# @param reason [String, nil] the Reason from the body. An empty reason is
# stored as nil, since the two say the same thing.
# @param msg [String, nil] overrides the generated message.
# @return [StatusReportError]
def initialize(status_code: nil, reason: nil, msg: nil)
@status_code = status_code
@reason = reason.to_s.empty? ? nil : reason.to_s

super(msg || generate_message)
end

private

# @return [String] what happened, then the StatusCode by name where it is
# one the enumeration carries, with the Reason appended when the server
# supplied one.
def generate_message
name = status_code.nil? ? 'an undecodable status' : Rex::Proto::OpcUa::Enums.status_code_name(status_code)
reason.nil? ? "#{summary}: #{name}" : "#{summary}: #{name} - #{reason}"
end

# @return [String] the leading clause of the generated message.
def summary
raise ::NotImplementedError, "#{self.class} must supply a summary"
end
end

# Raised when the server abandons a response part way through by sending a
# chunk of type A, per OPC-UA Specification Part 6, section 6.7.3. The
# response cannot be completed, but the connection itself is intact and the
# server is behaving to specification.
#
# The chunk carries the same StatusCode and Reason an ERR message would;
# Table 63 in that section gives the body as an Error UInt32 followed by a
# Reason String. No capture under spec/file_fixtures/opc_ua contains an abort,
# so that decode is specified rather than observed, and a body that will not
# decode leaves both fields nil rather than failing the abort report.
class AbortError < StatusReportError
private

def summary
'server aborted the response'
end
end

# Raised when the server answers with an ERR message, per OPC-UA
# Specification Part 6, section 7.1.2.5.
class ServerError < StatusReportError
private

def summary
'server returned ERR'
end
end
end
Loading
Loading