--- a/CertManagement.py Thu Mar 20 15:02:11 2025 +0100
+++ b/CertManagement.py Fri Mar 21 11:10:59 2025 +0100
@@ -163,14 +163,14 @@
own_keystore = os.path.join(keystore_path, "own")
if not os.path.exists(own_keystore):
os.makedirs(own_keystore)
return os.path.join(own_keystore, "client.crt")
def GetClientCertificateInfo():
- file_path = _clientCertPath()
+ file_path = GetClientCert() if os.path.exists(file_path):
@@ -196,10 +196,10 @@
return "No client certificate available"
def ImportClientCert(filepath):
- certpath = _clientCertPath()
+ certpath = GetClientCert() shutil.copyfile(filepath, certpath)
- certpath = _clientCertPath()
+ certpath = GetClientCert() if os.path.exists(certpath ):
--- a/connectors/WAMP/__init__.py Thu Mar 20 15:02:11 2025 +0100
+++ b/connectors/WAMP/__init__.py Fri Mar 21 11:10:59 2025 +0100
@@ -31,7 +31,7 @@
from twisted.internet import reactor, threads
from twisted.internet._sslverify import OpenSSLCertificateAuthorities
-from twisted.internet.ssl import optionsForClientTLS, VerificationError
+from twisted.internet.ssl import PrivateCertificate, optionsForClientTLS, VerificationError from autobahn.twisted import wamp
from autobahn.twisted.websocket import WampWebSocketClientFactory, connectWS
from autobahn.wamp import types, auth
@@ -50,10 +50,30 @@
+AUTH_CLIENTCERT = "ClientCertificate" +AUTHENTICATION_TYPES = [AUTH_NONE, AUTH_PSK, AUTH_CLIENTCERT] +SSL_AUTHENTICATION_TYPES = [AUTH_CLIENTCERT] class WampSession(wamp.ApplicationSession):
- user = self.config.extra["IDE_ID"]
- self.join(self.config.realm, ["wampcra"], user)
+ auth = self.config.extra["authentication"] + accepted_method = "anonymous" + authID = self.config.extra["IDE_ID"] + accepted_method = "wampcra" + elif auth in SSL_AUTHENTICATION_TYPES: + accepted_method = "tls" + raise Exception("Invalid authentication: "+auth) + self.join(self.config.realm, + authmethods=[accepted_method], def onChallenge(self, challenge):
if challenge.method == "wampcra":
@@ -112,17 +132,28 @@
WAMP://127.0.0.1:12345/path#realm#PLC_ID
WAMPS://127.0.0.1:12345/path#realm#PLC_ID
+ WAMP-CRT://127.0.0.1:12345/path#realm#PLC_ID scheme, location = uri.split("://")
urlpath, realm, PLC_ID = location.split('#')
- urlprefix = {"WAMP": "ws",
- "WAMPS": "wss"}[scheme]
+ urlprefix , ssl_auth, use_secret, use_ssl, verify, auth = { + "WAMP": ("ws" , 0 , 1 , 0 , 0 , AUTH_PSK ), + "WAMP-ANNON": ("ws" , 0 , 0 , 0 , 0 , AUTH_NONE ), + "WAMPS": ("wss", 0 , 1 , 1 , 1 , AUTH_PSK ), + "WAMPS-ANNON": ("wss", 0 , 0 , 1 , 1 , AUTH_NONE ), + "WAMPS-INSECURE":("wss", 0 , 0 , 1 , 0 , AUTH_NONE ), + "WAMPS-NOVERIFY":("wss", 0 , 1 , 1 , 0 , AUTH_PSK ), + "WAMPS-CRT": ("wss", 1 , 0 , 1 , 1 , AUTH_CLIENTCERT ), url = urlprefix+"://"+urlpath
CN = urlpath.split("/")[0].split(":")[0]
+ IDE_ID, secret = PSK.GetIDEIdentity() + trust_store = Cert.GetCertPath(CN) + client_cert_file = Cert.GetClientCert() - IDE_ID, secret = PSK.GetIDEIdentity()
- trust_store = Cert.GetCertPath(CN)
confnodesroot.logger.write_error(
_("Connection to {loc} failed with exception {ex}\n").format(
@@ -134,15 +165,17 @@
# start logging to console
# log.startLogging(sys.stdout)
+ extraconf["secret"] = secret # create a WAMP application session factory
- component_config = types.ComponentConfig(
session_factory = wamp.ApplicationSessionFactory(
- config=component_config)
+ config=types.ComponentConfig( session_factory.session = cls
# create a WAMP-over-WebSocket transport client factory
@@ -153,17 +186,25 @@
if transport_factory.isSecure:
+ if os.path.exists(client_cert_file): + client_cert = PrivateCertificate.loadPEM(open(client_cert_file, 'rb').read()) + confnodesroot.logger.write_error( + _("WAMP client certificate not provided for: {loc}\n").format(loc=uri)) if os.path.exists(trust_store):
- cert = crypto.load_certificate(
- open(trust_store, 'rb').read()
- trustRoot=OpenSSLCertificateAuthorities([cert])
- confnodesroot.logger.write_warning("Wamp trust store not found")
- contextFactory = optionsForClientTLS(transport_factory.host, trustRoot=trustRoot)
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, + open(trust_store, 'rb').read()) + trustRoot = OpenSSLCertificateAuthorities([cert]) + contextFactory=optionsForClientTLS(transport_factory.host, + clientCertificate=client_cert) # start the client from a Twisted endpoint
conn = connectWS(transport_factory, contextFactory)
--- a/connectors/__init__.py Thu Mar 20 15:02:11 2025 +0100
+++ b/connectors/__init__.py Fri Mar 21 11:10:59 2025 +0100
@@ -79,12 +79,13 @@
runtime_port = confnodesroot.StartLocalRuntime()
uri = f"ERPC://{LocalHost}:{runtime_port}"
- elif _scheme in connectors:
+ _scheme = _scheme.split("-")[0] + if _scheme in connectors: elif _scheme[-1] == 'S' and _scheme[:-1] in connectors:
- elif _scheme.split("-")[0] in connectors:
- scheme = _scheme.split("-")[0]
--- a/runtime/WampClient.py Thu Mar 20 15:02:11 2025 +0100
+++ b/runtime/WampClient.py Fri Mar 21 11:10:59 2025 +0100
@@ -123,22 +123,22 @@
class WampSession(wamp.ApplicationSession):
- user = self.config.extra["ID"]
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,
+ accepted_method = "anonymous" - raise Exception("Invalid authentication: "+auth)
+ authID = self.config.extra["ID"] + accepted_method = "wampcra" + elif auth in SSL_AUTHENTICATION_TYPES: + accepted_method = "tls" + raise Exception("Invalid authentication: "+auth) + self.join(self.config.realm, + authmethods=[accepted_method], def onChallenge(self, challenge):
if challenge.method == "wampcra":
--- a/tests/cli_tests/wamp_test_PSK_and_TLS.bash Thu Mar 20 15:02:11 2025 +0100
+++ b/tests/cli_tests/wamp_test_PSK_and_TLS.bash Fri Mar 21 11:10:59 2025 +0100
@@ -5,6 +5,9 @@
APPDATA=$HOME/.local/share/beremiz
KEYSTORE=$APPDATA/keystore
+# Set BEREMIZ_LOCAL_HOST to localhost if not already set +:${BEREMIZ_LOCAL_HOST:=localhost} # Start runtime one first time to generate PLC PSK
$BEREMIZPYTHONPATH $BEREMIZPATH/Beremiz_service.py -s psk.txt -n test_wamp_ID -x 0 &
@@ -30,9 +33,10 @@
IFS=':' read -r PLC_wamp_ID PLC_wamp_secret < psk.txt
+URI="WAMPS://${BEREMIZ_LOCAL_HOST}:8888/ws#Automation#${PLC_wamp_ID}" cp -a $BEREMIZPATH/tests/projects/wamp .
-URI="WAMPS://localhost:8888/ws#Automation#${PLC_wamp_ID}"
sed -i "s,TEST_URI,${URI},g" wamp/beremiz.xml
# Start CLI one first time to generate IDE PSK
@@ -67,7 +71,7 @@
yes "" | openssl req -nodes -new -x509 -keyout ./.crossbar/server.key \
- -addext "subjectAltName = DNS:localhost" \
+ -addext "subjectAltName = DNS:${BEREMIZ_LOCAL_HOST}" \ -out ./.crossbar/server.crt
cat > .crossbar/config.json <<JsonEnd
@@ -177,7 +181,7 @@
- "url": "wss://localhost:8888/ws"
+ "url": "wss://${BEREMIZ_LOCAL_HOST}:8888/ws" @@ -225,10 +229,7 @@
# Re-use self-signed server cert for client in test project
-cp .crossbar/server.crt $IDE_CERT/localhost.crt
-# TODO: patch project's URI to connect to $BEREMIZ_LOCAL_HOST
-# used in tests instead of 127.0.0.1
+cp .crossbar/server.crt $IDE_CERT/${BEREMIZ_LOCAL_HOST}.crt # Use CLI to build transfer and start PLC
$BEREMIZPYTHONPATH $BEREMIZPATH/Beremiz_cli.py -k \
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/cli_tests/wamp_test_client_cert.bash Fri Mar 21 11:10:59 2025 +0100
@@ -0,0 +1,329 @@
+rm -f ./CLI_OK ./PLC_OK ./PLC_CONNECTED +APPDATA=$HOME/.local/share/beremiz +KEYSTORE=$APPDATA/keystore +# Set BEREMIZ_LOCAL_HOST to localhost if not already set +:${BEREMIZ_LOCAL_HOST:=localhost} +URI="WAMPS-CRT://${BEREMIZ_LOCAL_HOST}:8888/ws#Automation#${PLC_wamp_ID}" +client_cns=(${IDE_wamp_ID} ${PLC_wamp_ID}) +# Create base directory for the certificates and keys +mkdir -p certs/ca certs/server certs/clients +yes "" | openssl req -nodes -new -x509 -keyout certs/server/server.key \ + -addext "subjectAltName = DNS:${BEREMIZ_LOCAL_HOST}" \ + -out certs/server/server.crt +# Declare an associative array to store client certificate SHA1 fingerprints +declare -A client_fingerprints +# Loop through each client CN and generate keys and certificates +for cn in "${client_cns[@]}"; do + # Generate client cert to be signed + openssl req -nodes -newkey rsa:2048 -keyout certs/clients/${cn}.key -out certs/clients/${cn}.csr -subj "/C=AU/ST=NSW/L=Sydney/O=Beremiz/OU=client/CN=${cn}" + openssl x509 -req -in certs/clients/${cn}.csr -CA certs/server/server.crt -CAkey certs/server/server.key -out certs/clients/${cn}.crt + # Get the SHA1 fingerprint of the client certificate + fingerprint=$(openssl x509 -in certs/clients/${cn}.crt -noout -fingerprint -sha1 | sed 's/.*=//') + client_fingerprints["${cn}"]="${fingerprint}" + # Create a PEM file containing the client certificate and private key + cat "certs/clients/${cn}.crt" "certs/clients/${cn}.key" > "certs/clients/${cn}.pem" +# Prepare crossbar server configuration +cp certs/server/server.crt ./.crossbar/ca.crt # In our test server is CA +cp certs/server/server.key ./.crossbar/server.key +cp certs/server/server.crt ./.crossbar/server.crt +# Crossbar need a Python Authenticator component to decide if Client Cert is OK +cat > authenticator.py <<PythonEnd +from twisted.internet.defer import inlineCallbacks +from autobahn.twisted.wamp import ApplicationSession +from autobahn.wamp.exception import ApplicationError +class AutomationAuthenticator(ApplicationSession): + # our "database" of accepted client certificate fingerprints +for client in "${!client_fingerprints[@]}"; do + echo " '${client_fingerprints[$client]}':'${client}'," >> authenticator.py +cat >> authenticator.py <<PythonEnd + def onJoin(self, details): + def authenticate(realm, authid, details): + client_cert = details['transport'].get('peer_cert', None) + raise ApplicationError("automation.no_cert", "no client certificate presented") + sha1 = client_cert['sha1'] + subject_cn = client_cert['subject']['cn'] + if sha1 not in self.ACCEPTED_CERTS: + print("AutomationAuthenticator.authenticate: client denied.") + raise ApplicationError("automation.invalid_cert", "certificate with SHA1 {} denied".format(sha1)) + print("AutomationAuthenticator.authenticate: client accepted.") + 'role': 'authenticated' + yield self.register(authenticate, 'automation.authenticate') + print("AutomationAuthenticator: dynamic authenticator registered.") + print("AutomationAuthenticator: could not register dynamic authenticator - {}".format(e)) +# Crossbar configuration that uses Python Authenticator component +cat > .crossbar/config.json <<JsonEnd + "id": "automation_router", + "name": "authenticated", + "name": "authenticator", + "uri": "automation.authenticate", + "certificate": "server.crt", + "authenticator": "automation.authenticate" + "classname": "authenticator.AutomationAuthenticator", + "role": "authenticator" +crossbar start &> crossbar_log.txt & +res=110 # default to ETIMEDOUT + if [[ -a .crossbar/node.pid ]]; then + echo found crossbar pid + echo wait for crossbar to start.... $c +if [ "$res" != "0" ] ; then + echo timeout starting crossbar. +# give more time to crossbar +# Prepare runtime Wamp config +cat > wampconf.json <<JsonEnd + "ID": "${PLC_wamp_ID}", + "autoPingInterval": 60, + "authentication": "ClientCertificate", + "verifyHostname": true, + "url": "wss://${BEREMIZ_LOCAL_HOST}:8888/ws" +# Re-use self-signed server cert for client +cp .crossbar/server.crt wampTrustStore.crt +cp certs/clients/${PLC_wamp_ID}.pem wampClientCert.pem +# Start Beremiz runtime again, with wamp enabled +$BEREMIZPYTHONPATH $BEREMIZPATH/Beremiz_service.py -c wampconf.json -s psk.txt -n test_wamp_ID -x 0 &> >( + # Wait for server to print modified value + if [[ "$line" =~ "WAMP session joined" ]]; then + echo "PLC is connected" + if [[ "$line" == "PLCobject : PLC started" ]]; then + echo "PLC was programmed" +echo wait for runtime to come up +res=110 # default to ETIMEDOUT + if [[ -a ./PLC_CONNECTED ]]; then +if [ "$res" != "0" ] ; then + echo timeout connecting PLC to crossbar. +cp -a $BEREMIZPATH/tests/projects/wamp . +sed -i "s,TEST_URI,${URI},g" wamp/beremiz.xml +# Re-use self-signed server cert for client in test project +cp .crossbar/server.crt $IDE_CERT/${BEREMIZ_LOCAL_HOST}.crt +IDE_CLIENT_CERT=$KEYSTORE/own/client.crt +cp certs/clients/${IDE_wamp_ID}.pem $IDE_CLIENT_CERT +# Use CLI to build transfer and start PLC +$BEREMIZPYTHONPATH $BEREMIZPATH/Beremiz_cli.py -k \ + --project-home wamp build transfer run &> >( + # Wait for PLC runtime to output expected value on stdout + if [[ "$line" == "PLC installed successfully." ]]; then + echo "CLI did transfer PLC program" +echo all subprocess started, start polling results +res=110 # default to ETIMEDOUT + if [[ -a ./CLI_OK && -a ./PLC_OK ]]; then +# Kill PLC and subprocess +echo will kill PLC:$PLC_PID, SERVER:$SERVER_PID and CLI:$CLI_PID