--- a/runtime/WampClient.py Tue Mar 18 15:59:13 2025 +0100
+++ b/runtime/WampClient.py Thu Mar 20 15:02:11 2025 +0100
@@ -33,7 +33,7 @@
from autobahn.wamp.serializer import MsgPackSerializer
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.python.components import registerAdapter
-from twisted.internet.ssl import optionsForClientTLS, VerificationError
+from twisted.internet.ssl import PrivateCertificate, optionsForClientTLS, VerificationError from twisted.internet._sslverify import OpenSSLCertificateAuthorities
from OpenSSL import crypto
@@ -46,6 +46,13 @@
mandatoryConfigItems = ["ID", "active", "realm", "url"]
+AUTH_CLIENTCERT = "ClientCertificate" +AUTHENTICATION_TYPES = [AUTH_NONE, AUTH_PSK, AUTH_CLIENTCERT] +SSL_AUTHENTICATION_TYPES = [AUTH_CLIENTCERT] @@ -55,6 +62,7 @@
@@ -87,6 +95,7 @@
+ "authentication": "None", @@ -115,7 +124,21 @@
user = self.config.extra["ID"]
- self.join(self.config.realm, ["wampcra"], user)
+ auth = self.config.extra["authentication"] + self.join(self.config.realm, ["wampcra"], user) + elif auth == AUTH_NONE: + self.join(self.config.realm, ["anonymous"]) + elif auth in SSL_AUTHENTICATION_TYPES: + 'channel_binding': "None" # "tls-unique" + self.join(self.config.realm, + raise Exception("Invalid authentication: "+auth) def onChallenge(self, challenge):
if challenge.method == "wampcra":
@@ -294,7 +317,7 @@
def RegisterWampClient(wampconf=None, wampsecret=None, ConfDir=None, KeyStore=None, servicename=None):
from twisted.internet import reactor
- global _WampConf, _WampSecret, _WampSercretFile, _WampTrust, defaultWampConfig
+ global _WampConf, _WampSecret, _WampSercretFile, _WampClientCert, _WampTrust, defaultWampConfig defaultWampConfig["ID"] = servicename
@@ -305,6 +328,9 @@
_WampConfDefault = os.path.join(ConfDir, "wampconf.json")
_WampSecretDefault = os.path.join(KeyStore, "wamp.secret")
+ if _WampClientCert is None: + _WampClientCert = os.path.join(KeyStore, "wampClientCert.pem") _WampTrust = os.path.join(KeyStore, "wampTrustStore.crt")
@@ -344,6 +370,8 @@
reactor.callInThread(_RegisterWampClient)
def _RegisterWampClient():
global _WampSecret, _transportFactory
WampClientConf = GetConfiguration()
@@ -365,11 +393,38 @@
url=WampClientConf["url"],
serializers=[MsgPackSerializer()])
- # start the client from a Twisted endpoint
+ auth = WampClientConf["authentication"] + verify = WampClientConf["verifyHostname"] if _transportFactory.isSecure:
- contextFactory = MakeSecureContextFactory(WampClientConf["verifyHostname"])
+ ssl_auth = auth in SSL_AUTHENTICATION_TYPES + if os.path.exists(_WampClientCert): + client_cert = PrivateCertificate.loadPEM(open(_WampClientCert, 'rb').read()) + GetPLCObjectSingleton().LogMessage(LogLevelsDict["ERROR"], + "WAMP client certificate not provided for:", WampClientConf["url"]) + if os.path.exists(_WampTrust): + cert = crypto.load_certificate(crypto.FILETYPE_PEM, + open(_WampTrust, 'rb').read()) + trustRoot = OpenSSLCertificateAuthorities([cert]) + contextFactory=optionsForClientTLS(_transportFactory.host, + clientCertificate=client_cert) + # non encrypted connection is not accepted in case some security is requested + if auth != AUTH_NONE or verify: + GetPLCObjectSingleton().LogMessage(LogLevelsDict["ERROR"], + "WAMP connection must be secure:", WampClientConf["url"]) connectWS(_transportFactory, contextFactory)
print("WAMP client connecting to :", WampClientConf["url"])
@@ -377,19 +432,6 @@
GetPLCObjectSingleton().LogMessage(LogLevelsDict["WARNING"],
"WAMP configuration invalid:", WampClientConf["url"])
-def MakeSecureContextFactory(verifyHostname):
- global _transportFactory
- if os.path.exists(_WampTrust):
- cert = crypto.load_certificate(
- open(_WampTrust, 'rb').read()
- trustRoot=OpenSSLCertificateAuthorities([cert])
- return optionsForClientTLS(_transportFactory.host, trustRoot=trustRoot)
def StopReconnectWampClient():
if _transportFactory is not None:
_transportFactory.stopTrying()
@@ -436,12 +478,14 @@
# WEB CONFIGURATION INTERFACE
WAMP_SECRET_URL = "secret"
+WAMP_DELETE_CLIENTCERT_URL = "delete_clientcert" WAMP_DELETE_TRUSTSTORE_URL = "delete_truststore"
webExposedConfigItems = [
"clientFactoryOptions.maxDelay",
"protocolOptions.autoPingInterval",
"protocolOptions.autoPingTimeout",
@@ -468,6 +512,14 @@
shutil.copyfileobj(secretfile,destfd)
+ clientCert_field = kwargs["clientCert"] + if clientCert_field is not None: + clientCert_file = getattr(clientCert_field, "file", None) + if clientCert_file is not None: + with open(os.path.realpath(_WampClientCert), 'w') as destfd: + clientCert_file.seek(0) + shutil.copyfileobj(clientCert_file,destfd) trustStore_field = kwargs["trustStore"]
if trustStore_field is not None:
trustStore_file = getattr(trustStore_field, "file", None)
@@ -547,6 +599,14 @@
formless.iformless.ITypedRenderer)
+def getClientCertPresence(): + return os.path.exists(_WampClientCert) +def getClientCertDeleteUrl(ctx, argument): + return url.URL.fromContext(ctx).child(WAMP_DELETE_CLIENTCERT_URL) def getTrustStorePresence():
return os.path.exists(_WampTrust)
@@ -585,6 +645,11 @@
FileUploadDelete(label=_("File containing server certificate"),
file_exists=getTrustStorePresence,
file_delete=getTrustStoreDeleteUrl)),
+ annotate.Choice(AUTHENTICATION_TYPES, + label=_("Authentication type"))), annotate.Boolean(label=_("Verify hostname matches certificate hostname"),
default=wampConfigDefault)),
@@ -601,6 +666,11 @@
secret = LoadWampSecret(_WampSercretFile)
return static.Data(secret, 'application/octet-stream'), ()
+def deleteClientCert(ctx, segments): + if os.path.exists(_WampClientCert): + os.remove(_WampClientCert) + return static.Data("ClientCert deleted", 'text/html'), () def deleteTrustStore(ctx, segments):
if os.path.exists(_WampTrust):