Commit be43b413 authored by ORD's avatar ORD

Merge pull request #81 from alkor/security

Security policies implementation
parents a80a7be6 a2d79771
......@@ -10,15 +10,6 @@ from concurrent.futures import Future
import opcua.uaprotocol as ua
import opcua.utils as utils
class CachedRequest(object):
def __init__(self, binary):
self.binary = binary
def to_binary(self):
return self.binary
class UASocketClient(object):
"""
handle socket connection and send ua messages
......@@ -111,62 +102,21 @@ class UASocketClient(object):
break
self.logger.info("Thread ended")
def _receive_header(self):
self.logger.debug("Waiting for header")
header = ua.Header.from_string(self._socket)
self.logger.info("received header: %s", header)
return header
def _receive_body(self, size):
self.logger.debug("reading body of message (%s bytes)", size)
data = self._socket.read(size)
if size != len(data):
raise Exception("Error, did not receive expected number of bytes, got {}, asked for {}".format(len(data), size))
return utils.Buffer(data)
def _receive(self):
body_chunk = b""
while True:
ret = self._receive_complete_msg()
if ret is None:
return
hdr, algohdr, seqhdr, body = ret
if hdr.ChunkType in (b"F", b"A"):
body.data = body_chunk + body.data
break
elif hdr.ChunkType == b"C":
self.logger.debug("Received an intermediate message with header %s, waiting for next message", hdr)
body_chunk += body.data
else:
self.logger.warning("Received a message with unknown ChunkType %s, in header %s", hdr.ChunkType, hdr)
return
self._call_callback(seqhdr.RequestId, body)
def _receive_complete_msg(self):
header = self._receive_header()
if header is None:
return None
body = self._receive_body(header.body_size)
if header.MessageType == ua.MessageType.Error:
self.logger.warning("Received an error message type")
err = ua.ErrorMessage.from_binary(body)
self.logger.warning(err)
return None
if header.MessageType == ua.MessageType.Acknowledge:
self._call_callback(0, body)
return None
elif header.MessageType == ua.MessageType.SecureOpen:
algohdr = ua.AsymmetricAlgorithmHeader.from_binary(body)
self.logger.info(algohdr)
elif header.MessageType == ua.MessageType.SecureMessage:
algohdr = ua.SymmetricAlgorithmHeader.from_binary(body)
self.logger.info(algohdr)
msg = ua.tcp_message_from_socket(self._security_policy, self._socket)
if isinstance(msg, ua.MessageChunk):
chunks = [msg]
# TODO: check everything
while chunks[-1].MessageHeader.ChunkType == ua.ChunkType.Intermediate:
chunks.append(ua.tcp_message_from_socket(self._security_policy, self._socket))
body = b"".join([c.Body for c in chunks])
self._call_callback(msg.SequenceHeader.RequestId, utils.Buffer(body))
elif isinstance(msg, ua.Acknowledge):
self._call_callback(0, msg)
elif isinstance(msg, ua.ErrorMessage):
self.logger.warning("Received an error: {}".format(msg))
else:
self.logger.warning("Unsupported message type: %s", header.MessageType)
return None
seqhdr = ua.SequenceHeader.from_binary(body)
self.logger.debug(seqhdr)
return header, algohdr, seqhdr, body
raise Exception("Unsupported message type: {}".format(msg))
def _call_callback(self, request_id, body):
with self._lock:
......@@ -219,7 +169,7 @@ class UASocketClient(object):
with self._lock:
self._callbackmap[0] = future
self._write_socket(header, hello)
ack = ua.Acknowledge.from_binary(future.result(self.timeout))
ack = future.result(self.timeout)
self._max_chunk_size = ack.SendBufferSize # client shouldn't send chunks larger than this
return ack
......@@ -262,18 +212,19 @@ class BinaryClient(object):
uaprotocol_auto.py and uaprotocol_hand.py
"""
def __init__(self, timeout=1):
def __init__(self, timeout=1, security_policy=ua.SecurityPolicy()):
self.logger = logging.getLogger(__name__)
self._publishcallbacks = {}
self._lock = Lock()
self._timeout = timeout
self._uasocket = None
self._security_policy = security_policy
def connect_socket(self, host, port):
"""
connect to server socket and start receiving thread
"""
self._uasocket = UASocketClient(self._timeout)
self._uasocket = UASocketClient(self._timeout, security_policy=self._security_policy)
return self._uasocket.connect_socket(host, port)
def disconnect_socket(self):
......
......@@ -67,7 +67,7 @@ class Client(object):
which offers a raw OPC-UA interface.
"""
def __init__(self, url, timeout=1):
def __init__(self, url, timeout=1, security_policy=ua.SecurityPolicy()):
"""
used url argument to connect to server.
if you are unsure of url, write at least hostname and port
......@@ -82,8 +82,7 @@ class Client(object):
self.description = self.name
self.application_uri = "urn:freeopcua:client"
self.product_uri = "urn:freeopcua.github.no:client"
self.security_policy_uri = "http://opcfoundation.org/UA/SecurityPolicy#None"
self.security_mode = ua.MessageSecurityMode.None_
self.security_policy = security_policy
self.secure_channel_id = None
self.default_timeout = 3600000
self.secure_channel_timeout = self.default_timeout
......@@ -92,8 +91,7 @@ class Client(object):
self.server_certificate = b""
self.client_certificate = b""
self.private_key = b""
self.bclient = BinaryClient(timeout)
self._nonce = None
self.bclient = BinaryClient(timeout, security_policy=security_policy)
self._session_counter = 1
self.keepalive = None
......@@ -192,10 +190,12 @@ class Client(object):
params.RequestType = ua.SecurityTokenRequestType.Issue
if renew:
params.RequestType = ua.SecurityTokenRequestType.Renew
params.SecurityMode = self.security_mode
params.SecurityMode = self.security_policy.Mode
params.RequestedLifetime = self.secure_channel_timeout
params.ClientNonce = '\x00'
nonce = utils.create_nonce(self.security_policy.symmetric_key_size) # length should be equal to the length of key of symmetric encryption
params.ClientNonce = nonce # this nonce is used to create a symmetric key
result = self.bclient.open_secure_channel(params)
self.security_policy.make_symmetric_key(nonce, result.ServerNonce)
self.secure_channel_timeout = result.SecurityToken.RevisedLifetime
def close_secure_channel(self):
......@@ -251,18 +251,20 @@ class Client(object):
desc.ApplicationType = ua.ApplicationType.Client
params = ua.CreateSessionParameters()
params.ClientNonce = utils.create_nonce()
params.ClientCertificate = b''
nonce = utils.create_nonce(32) # at least 32 random bytes for server to prove possession of private key (specs part 4, 5.6.2.2)
params.ClientNonce = nonce
params.ClientCertificate = self.security_policy.client_certificate
params.ClientDescription = desc
params.EndpointUrl = self.server_url.geturl()
params.SessionName = self.description + " Session" + str(self._session_counter)
params.RequestedSessionTimeout = 3600000
params.MaxResponseMessageSize = 0 # means no max size
params.ClientCertificate = self.client_certificate
response = self.bclient.create_session(params)
self.security_policy.asymmetric_cryptography.verify(self.security_policy.client_certificate + nonce, response.ServerSignature.Signature)
self._server_nonce = response.ServerNonce
self.server_certificate = response.ServerCertificate
for ep in response.ServerEndpoints:
if urlparse(ep.EndpointUrl).scheme == self.server_url.scheme and ep.SecurityMode == self.security_mode:
if urlparse(ep.EndpointUrl).scheme == self.server_url.scheme and ep.SecurityMode == self.security_policy.Mode:
# remember PolicyId's: we will use them in activate_session()
self._policy_ids = ep.UserIdentityTokens
self.session_timeout = response.RevisedSessionTimeout
......@@ -285,6 +287,9 @@ class Client(object):
Activate session using either username and password or private_key
"""
params = ua.ActivateSessionParameters()
challenge = self.security_policy.server_certificate + self._server_nonce
params.ClientSignature.Algorithm = b"http://www.w3.org/2000/09/xmldsig#rsa-sha1"
params.ClientSignature.Signature = self.security_policy.asymmetric_cryptography.signature(challenge)
params.LocaleIds.append("en")
if not username and not certificate:
params.UserIdentityToken = ua.AnonymousIdentityToken()
......
This diff is collapsed.
......@@ -19,6 +19,7 @@ from opcua import Client
from opcua import Server
from opcua import Node
from opcua import uamethod
from opcua import security_policies
def add_minimum_args(parser):
......@@ -58,7 +59,29 @@ def add_common_args(parser):
type=int,
default=0,
metavar="NAMESPACE")
parser.add_argument("--security",
help="Security settings, for example: Basic256,SignAndEncrypt,cert.der,pk.pem[,server_cert.der]. Default: None",
default='')
def client_security(security, url, timeout):
parts = security.split(',')
if len(parts) < 4:
raise Exception('Wrong format: `{}`, expected at least 4 comma-separated values'.format(security))
policy_class = getattr(security_policies, 'SecurityPolicy' + parts[0])
mode = getattr(ua.MessageSecurityMode, parts[1])
cert = open(parts[2], 'rb').read()
pk = open(parts[3], 'rb').read()
server_cert = None
if len(parts) == 5:
server_cert = open(parts[4], 'rb').read()
else:
# we need server's certificate too. Let's get it from the list of endpoints
client = Client(url, timeout=timeout)
for ep in client.connect_and_get_server_endpoints():
if ep.EndpointUrl.startswith(ua.OPC_TCP_SCHEME) and ep.SecurityMode == mode and ep.SecurityPolicyUri == policy_class.URI:
server_cert = ep.ServerCertificate
return policy_class(server_cert, cert, pk, mode)
def parse_args(parser, requirenodeid=False):
args = parser.parse_args()
......@@ -66,6 +89,8 @@ def parse_args(parser, requirenodeid=False):
if args.url and '://' not in args.url:
logging.info("Adding default scheme %s to URL %s", ua.OPC_TCP_SCHEME, args.url)
args.url = ua.OPC_TCP_SCHEME + '://' + args.url
if hasattr(args, 'security') and args.security:
args.security = client_security(args.security, args.url, args.timeout)
# check that a nodeid has been given explicitly, a bit hackish...
if requirenodeid and args.nodeid == "i=84" and args.path == "":
parser.print_usage()
......@@ -99,7 +124,7 @@ def uaread():
args = parse_args(parser, requirenodeid=True)
client = Client(args.url, timeout=args.timeout)
client = Client(args.url, timeout=args.timeout, security_policy=args.security)
client.connect()
try:
node = get_node(client, args)
......@@ -235,7 +260,7 @@ def uawrite():
metavar="VALUE")
args = parse_args(parser, requirenodeid=True)
client = Client(args.url, timeout=args.timeout)
client = Client(args.url, timeout=args.timeout, security_policy=args.security)
client.connect()
try:
node = get_node(client, args)
......@@ -266,7 +291,7 @@ def uals():
if args.long_format is None:
args.long_format = 1
client = Client(args.url, timeout=args.timeout)
client = Client(args.url, timeout=args.timeout, security_policy=args.security)
client.connect()
try:
node = get_node(client, args)
......@@ -351,7 +376,7 @@ def uasubscribe():
args = parse_args(parser, requirenodeid=True)
client = Client(args.url, timeout=args.timeout)
client = Client(args.url, timeout=args.timeout, security_policy=args.security)
client.connect()
try:
node = get_node(client, args)
......@@ -432,7 +457,7 @@ def uaclient():
help="set client private key")
args = parse_args(parser)
client = Client(args.url, timeout=args.timeout)
client = Client(args.url, timeout=args.timeout, security_policy=args.security)
client.connect()
if args.certificate:
client.load_client_certificate(args.certificate)
......@@ -593,7 +618,7 @@ def uahistoryread():
args = parse_args(parser, requirenodeid=True)
client = Client(args.url, timeout=args.timeout)
client = Client(args.url, timeout=args.timeout, security_policy=args.security)
client.connect()
try:
node = get_node(client, args)
......
......@@ -254,7 +254,7 @@ class CryptographyNone:
"""
Base class for symmetric/asymmetric cryprography
"""
def __init__(self, mode=auto.MessageSecurityMode.None_):
def __init__(self):
pass
def plain_block_size(self):
......@@ -309,17 +309,17 @@ class SecurityPolicy:
"""
Base class for security policy
"""
URI = "http://opcfoundation.org/UA/SecurityPolicy#None"
signature_key_size = 0
symmetric_key_size = 0
def __init__(self):
self.asymmetric_cryptography = CryptographyNone()
self.symmetric_cryptography = CryptographyNone()
self.Mode = auto.MessageSecurityMode.None_
self.URI = "http://opcfoundation.org/UA/SecurityPolicy#None"
self.server_certificate = b""
self.client_certificate = b""
def symmetric_key_size(self):
return 0
def make_symmetric_key(self, a, b):
pass
......@@ -433,6 +433,33 @@ class MessageChunk(uatypes.FrozenClass):
__repr__ = __str__
def tcp_message_from_header_and_body(security_policy, header, body):
"""
Convert binary stream to OPC UA TCP message (see OPC UA specs Part 6, 7.1)
The only supported message types are Hello, Acknowledge and Error
"""
if header.MessageType in (MessageType.SecureOpen, MessageType.SecureClose, MessageType.SecureMessage):
return MessageChunk.from_header_and_body(security_policy, header, body)
elif header.MessageType == MessageType.Hello:
return Hello.from_binary(body)
elif header.MessageType == MessageType.Acknowledge:
return Acknowledge.from_binary(body)
elif header.MessageType == MessageType.Error:
return ErrorMessage.from_binary(body)
else:
raise Exception("Unsupported message type {}".format(header.MessageType))
def tcp_message_from_socket(security_policy, socket):
logger.debug("Waiting for header")
header = Header.from_string(socket)
logger.info("received header: %s", header)
body = socket.read(header.body_size)
if len(body) != header.body_size:
raise Exception("{} bytes expected, {} available".format(header.body_size, len(body)))
return tcp_message_from_header_and_body(security_policy, header, utils.Buffer(body))
# FIXES for missing switchfield in NodeAttributes classes
ana = auto.NodeAttributesMask
......
import logging
import uuid
import os
from concurrent.futures import Future
import functools
import threading
......@@ -108,8 +108,8 @@ class SocketWrapper(object):
def create_nonce():
return uuid.uuid4().bytes + uuid.uuid4().bytes # seems we need at least 32 bytes not 16 as python gives us...
def create_nonce(size=32):
return os.urandom(size)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment