Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request]: Basic256Sha256 policy not implemented? #1844

Closed
2 of 16 tasks
sciortid opened this issue Oct 23, 2024 · 10 comments
Closed
2 of 16 tasks

[Feature Request]: Basic256Sha256 policy not implemented? #1844

sciortid opened this issue Oct 23, 2024 · 10 comments
Assignees
Labels
java Pull requests that update Java code OPC-UA https://plc4x.apache.org/users/protocols/opcua.html python Pull requests that update Python code

Comments

@sciortid
Copy link

What would you like to happen?

Hello! First of all I'm pretty sure I'm missing something but i'll try.

I created an OPCUA Python server with username or certificates authentication policies. It has been tested with OPCUA Expert and i can access it with both policies methods.

I (and ChatGPT) created a simple standalone example to connect to the server:

package opcua;
import org.apache.plc4x.java.api.PlcConnection;
import org.apache.plc4x.java.api.messages.PlcReadRequest;
import org.apache.plc4x.java.api.messages.PlcReadResponse;
import org.apache.plc4x.java.DefaultPlcDriverManager;

import java.util.concurrent.CompletableFuture;

public class OpcUaClientWithCert {

    public static void main(String[] args) {
        // URL del server OPC UA, con certificato client e chiave privata inclusi
        String serverUrl = "opcua:tcp://127.0.0.1:4840";
        String discovery = "true";
        String securityPolicy = "Basic256Sha256";  // Percorso del certificato del client
        String clientKeyStore = ".../client_keystore.p12";
        String clientKeySotrePass = "clientKeystorePassword";         // Password della chiave

        // Modificare la stringa di connessione per includere certificato e chiave privata
        String connectionString = String.format(
                "%s?discovery=%s&security-policy=%s&key-store-file=%s&key-store-password=%s",
                serverUrl, discovery, securityPolicy, clientKeyStore, clientKeySotrePass
        );

        String nodeId = "ns=2;i=2";  // Sostituire con il nodo che si desidera leggere

        // Connettersi al server OPC UA con certificati
        DefaultPlcDriverManager driverManager = new DefaultPlcDriverManager(); // Crea un'istanza di DefaultPlcDriverManager
		try (PlcConnection connection = driverManager.getConnection(connectionString)) {
            if (connection != null && connection.isConnected()) {
                System.out.println("Connessione stabilita con il server OPC UA usando certificati.");

                // Creare una richiesta di lettura
                PlcReadRequest.Builder builder = connection.readRequestBuilder();
                builder.addTagAddress("myNode", nodeId);  // Aggiungere il nodo da leggere

                PlcReadRequest readRequest = builder.build();

                // Eseguire la lettura in modo asincrono
                CompletableFuture<? extends PlcReadResponse> future = readRequest.execute();
                PlcReadResponse response = future.get();

                // Stampare il risultato
                System.out.println("Valore letto dal nodo: " + response.getObject("myNode"));
            } else {
                System.out.println("Connessione fallita.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

And this is the pom.xml file:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  
  <modelVersion>4.0.0</modelVersion>
  <groupId>test</groupId>
  <artifactId>opcua</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>test</name>
  <description>test</description>
  
	<dependencies>
	
		<dependency>
				<groupId>org.apache.plc4x</groupId>
				<artifactId>plc4j-api</artifactId>
				<version>0.12.0</version>
		</dependency>
		
		<dependency>
			  <groupId>org.apache.plc4x</groupId>
			  <artifactId>plc4j-driver-opcua</artifactId>
			  <version>0.12.0</version>
		</dependency>
		
		<dependency>
			    <groupId>org.apache.plc4x</groupId>
			    <artifactId>plc4j-transport-tcp</artifactId>
			    <version>0.12.0</version>
		</dependency>
		
	    <dependency>
	        <groupId>org.slf4j</groupId>
	        <artifactId>slf4j-api</artifactId>
	        <version>1.7.36</version> 
	    </dependency>
	    
	    <dependency>
	        <groupId>org.slf4j</groupId>
	        <artifactId>slf4j-simple</artifactId>
	        <version>1.7.36</version>
	    </dependency>

	</dependencies>
</project>

When it runs, I get the "Unable to find endpoint.." exception in this part of SecureChannel.java

    private void selectEndpoint(CreateSessionResponse sessionResponse) throws PlcRuntimeException {
        // Get a list of the endpoints which match ours.
        Stream<EndpointDescription> filteredEndpoints = sessionResponse.getServerEndpoints().stream()
            .map(e -> (EndpointDescription) e)
            .filter(this::isEndpoint);

        //Determine if the requested security policy is included in the endpoint
        filteredEndpoints.forEach(endpoint -> hasIdentity(
            endpoint.getUserIdentityTokens().stream()
                .map(p -> (UserTokenPolicy) p)
                .toArray(UserTokenPolicy[]::new)
        ));

        if (this.policyId == null) {
            throw new PlcRuntimeException("Unable to find endpoint - " + this.endpoints.get(0));
        }
        if (this.tokenType == null) {
            throw new PlcRuntimeException("Unable to find Security Policy for endpoint - " + this.endpoints.get(0));
        }
    }

And noticed this is because the policyId is set to null but because apparently the hasIdentity function has no option for policy other than anonymous and username:

    private void hasIdentity(UserTokenPolicy[] policies) {
        for (UserTokenPolicy identityToken : policies) {
            if ((identityToken.getTokenType() == UserTokenType.userTokenTypeAnonymous) && (this.username == null)) {
                policyId = identityToken.getPolicyId();
                tokenType = identityToken.getTokenType();
            } else if ((identityToken.getTokenType() == UserTokenType.userTokenTypeUserName) && (this.username != null)) {
                policyId = identityToken.getPolicyId();
                tokenType = identityToken.getTokenType();
            }
        }
    }

The policies that my server returns correspond to the ones i set with the python script:

image
image

What am I missing? Shouldn't the library allow this kind of access with just the cetificate? I expected other "if" for certificate policies. Did i end up in other functions that should not be used for this policies? In this case, is there something wrong with the main function?

Please note that the example works if i modify the connection string by just adding the username & password, correctly reading the node value:

      String serverUrl = "opcua:tcp://127.0.0.1:4840";
       String discovery = "true";
       String securityPolicy = "Basic256Sha256";  // Percorso del certificato del client
       String clientKeyStore = "..../client_keystore.p12";
       String clientKeySotrePass = "clientKeystorePassword";         // Password della chiave
       String userName = "user1";
       String password = "password1";
       // Modificare la stringa di connessione per includere certificato e chiave privata
       String connectionString = String.format(
               "%s?discovery=%s&security-policy=%s&key-store-file=%s&key-store-password=%s&username=%s&password=%s",
               serverUrl, discovery, securityPolicy, clientKeyStore, clientKeySotrePass, userName, password
       );

But it does not surprise me because it is using the username policy in this case (even if Basic256Sha256 is still specified)

Programming Languages

  • plc4j
  • plc4go
  • plc4c
  • plc4net

Protocols

  • AB-Ethernet
  • ADS /AMS
  • BACnet/IP
  • CANopen
  • DeltaV
  • DF1
  • EtherNet/IP
  • Firmata
  • KNXnet/IP
  • Modbus
  • OPC-UA
  • S7
@ottlukas ottlukas added java Pull requests that update Java code OPC-UA https://plc4x.apache.org/users/protocols/opcua.html feature python Pull requests that update Python code labels Oct 23, 2024
@splatch splatch self-assigned this Oct 24, 2024
@splatch
Copy link
Contributor

splatch commented Oct 24, 2024

Hello @sciortid,
All security policies defined by spec are covered, with a small note of bug fixed in 0.13-SNAPSHOT and discovered thanks to issue #1802. In 0.12 you need to make sure that both client and server certificates use same private key length.
One important note - certificate authentication policy is not supported yet.

With regard to endpoint selection logic - you most likely need to define also message security (message-security=SIGN or SIGN_ENCRYPT) in order to use password authentication. I think spec mandates use of encryption in case of password authentication, however most of software servers are quite relaxed about that.
There was small adjustment of endpoint selection logic in #1830, so 0.13-SNAPSHOT again might behave slightly different than 0.12. Later have a bug which could lead to incompatibilities with servers which defined multiple UserTokenPolicy with distinct identifiers.

My advise is to try with develop (checkout project and do mvn install -Pwith-java) to see if its any better. If not, then please enable debug logging for org.apache.plc4x.java.opcua.protocol and attach logs generated by your application.

@sciortid
Copy link
Author

Hello @splatch. Thank you for the support.

I admit it's a lack of knowledge from my point of view. Just a bit of context:
I'm developing an OPCUA client for a server developed by a third party, it currently has no authentication but i know for a fact that this third party will want to use certificate authentication OR username+password.
I developed a python OPCUA server to test my client, so it currently looks like this:

from opcua import Server, ua
import time
import random

# Imposta il server
server = Server()

# Imposta l'endpoint del server
server.set_endpoint("opc.tcp://127.0.0.1:4840")

# Aggiungi un namespace personalizzato
uri = "urn:freeopcua:python:server"
idx = server.register_namespace(uri)

# Imposta le politiche di sicurezza
server.set_security_policy([
    ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt  # Connessioni sicure con cifratura
])

# Imposta i tipi di sicurezza
server.set_security_IDs(["Basic256Sha256", "Username"])

### AUTENTICAZIONE CON USERNAME E PASSWORD ###
def user_manager(isession, username, password):
    users = {
        "user1": "password1",
        "user2": "password2"
    }
    if username in users and users[username] == password:
        print(f"Login riuscito per l'utente: {username}")
        return True
    else:
        print(f"Tentativo di login fallito per l'utente: {username}")
        return False

# Configura il server per utilizzare l'autenticazione username/password
server.user_manager.set_user_manager(user_manager)

### AUTENTICAZIONE CON CERTIFICATI ###
server.load_certificate("certificates/server_certificate.der")
server.load_private_key("certificates/server_private_key.pem")

[...]
server.start()

try:
    while True:
        [...]
        time.sleep(1)  # Aspetta 1 secondo
finally:
    # Ferma il server al termine
    server.stop()

So what I'm doing is trying to authenticate with either username+password or certificates.
I proved that i can authenticate with user+psw, but did i correctly understand that even the 0.13SNAPSHOT does not support the certificate authentication method? It doesn't look like it does from the commit you linked, will the library support it in the foreseeable future?

Alo, is the server setup correctly for this scope (authentication with user+sw OR certificate) ? Again, lack of my knowledge, sorry for that, but i just want to ensure that I'm not misinterpreting things:

server.set_security_policy([
    ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt  
])

server.set_security_IDs(["Basic256Sha256", "Username"])

@splatch
Copy link
Contributor

splatch commented Oct 24, 2024

Buongiorno @sciortid,

I proved that i can authenticate with user+psw, but did i correctly understand that even the 0.13SNAPSHOT does not support the certificate authentication method? It doesn't look like it does from the commit you linked, will the library support it in the foreseeable future?

Looking at your code I think you probably mix certificate authentication with transport level encryption. The OPC-UA protocol can use certificates to secure communication channel as well as to authenticate user. While I know little about python, below lines look more like transport level security:

server.load_certificate("certificates/server_certificate.der")
server.load_private_key("certificates/server_private_key.pem")

Another point - python code snippet uses ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt. In Apache PLC4X ua client this value is split into two connection options security-policy=Basic256Sha256 and message-security=SIGN_ENCRYPT.

Both 0.12 and 0.13-SNAPSHOT support SIGN_ENCRYPT and can work with certificates. The keystore you configure is client private key and certificate used to secure channel. This certificate can be used by server to accept or refuse connection.

If you will have a look on https://reference.opcfoundation.org/Core/Part4/v104/docs/7.37 you will find there are few options for tokenType (UserTokenType): Anonymous, Username, Certificate, IssuedToken. We support only first two, however this does not prevent you at all from using certificates to secure channel.

@sciortid
Copy link
Author

sciortid commented Oct 24, 2024

Ok so certificate authentication method is currently not supported, But

You're right, I'm mixing certificates authentication with encryption.
Apparently the python opcua library uses those same key/certificates for both purposes, both on client and server side.
I'm sure about this because i can login into the configured server via UA Expert by providing the client certificates path.

I still don't understand the following:
I now configured the python server to use encryption communication and user/psw authentication

#Communication 
server.set_security_policy([
    ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt 
])

#Authentication
server.set_security_IDs(["Username"])

#Certificates
server.load_certificate("certificates/server_certificate.der")
server.load_private_key("certificates/server_private_key.pem")

But how is that possible that I'm able to login into the server via PLC4J / UA Expert by just providing the username+password without a certificate?

String serverUrl = "opcua:tcp://127.0.0.1:4840";
String discovery = "true";
String securityPolicy = "Basic256Sha256";  // Percorso del certificato del client
String messageSecurity = "SIGN_ENCRYPT";
String username = "user1";
String password = "password1";

String connectionString = String.format(
       "%s?discovery=%s&security-policy=%s&message-securty=%s&username=%s&password=%s",
       serverUrl, discovery, securityPolicy, messageSecurity, username, password
);

I again admit to be a noob, but my only explanation to this is that only server certificate matters for communication encryption? Why do you say that the keystore on client side is used to secure channel?
Maybe i can use it instead to provide it to the server in case it need a trusted clients list?

@splatch
Copy link
Contributor

splatch commented Oct 24, 2024

But how is that possible that I'm able to login into the server via PLC4J / UA Expert by just providing the username+password without a certificate?

In case if user does not provide a certificate it will be generated by client. It is an one off certificate which is not retained. For servers which validate transport level certificates we have options to provide PKCS12 encoded certificate and private key. Then you can specify these using following parameters: key-store-type=pkcs12, key-store-file=/a/b.p12, key-store-password=<pass>.

@sciortid
Copy link
Author

That's clear, thank you again for your patience!
You can close the issue from my side, or you can leave it open in case you need to reference it in case of certificate authentication development

@splatch
Copy link
Contributor

splatch commented Oct 24, 2024

You're welcome. Please open new issue for certificate authentication once it will be needed.

@splatch splatch closed this as completed Oct 24, 2024
@sciortid
Copy link
Author

Well i really would need it but it really depends on how long would it take 😅

@splatch
Copy link
Contributor

splatch commented Oct 24, 2024

It is not a lot of code per say, but I currently miss reference environment where I could test such setup. Prosys Simulation Server docs and forums is a bit cryptic in this regard as a lot people fall in the same trap as you did.

For now I've created #1845 so you can sign for updates there.

@sciortid
Copy link
Author

I would be glad to help in case you need some dumb tester 😅
Here is my server that accepts both user and certificate authentication, as said I tested it with UA expert with both methods

from opcua import Server, ua
import time
import random

# Imposta il server
server = Server()

# Imposta l'endpoint del server
server.set_endpoint("opc.tcp://127.0.0.1:4840")

# Aggiungi un namespace personalizzato
uri = "urn:freeopcua:python:server"
idx = server.register_namespace(uri)

# Imposta le politiche di sicurezza
server.set_security_policy([
    #ua.SecurityPolicyType.NoSecurity,  # Connessioni non sicure (solo per testing)
    # ua.SecurityPolicyType.Basic256Sha256_Sign, # Connessioni sicure seza cifratura
    ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt  # Connessioni sicure con cifratura
])

# Imposta i tipi di sicurezza
server.set_security_IDs(["Basic256Sha256", "Username"])

### AUTENTICAZIONE CON USERNAME E PASSWORD ###
def user_manager(isession, username, password):
    users = {
        "user1": "password1",
        "user2": "password2"
    }
    if username in users and users[username] == password:
        print(f"Login riuscito per l'utente: {username}")
        return True
    else:
        print(f"Tentativo di login fallito per l'utente: {username}")
        return False

# Configura il server per utilizzare l'autenticazione username/password
server.user_manager.set_user_manager(user_manager)

### AUTENTICAZIONE CON CERTIFICATI ###
server.load_certificate("certificates/server_certificate.der")
server.load_private_key("certificates/server_private_key.pem")

# Crea un oggetto principale per i tag
tags_container = server.nodes.objects.add_object("ns=2;i=1", "Tags")

# Dizionario per memorizzare i riferimenti ai nodi delle variabili
node_references = {}

# Aggiungi variabili all'oggetto Tags
variables = {
    "Var1": (44, ua.VariantType.Int32),
    "Var2": (19, ua.VariantType.Int32)
}

# Aggiungi le variabili al container Tags e memorizza i riferimenti
for name, (value, var_type) in variables.items():
    variable = tags_container.add_variable("ns=2;s={}".format(name), name, value, varianttype=var_type)
    variable.set_writable()  # Imposta la variabile come scrivibile
    node_references[name] = variable  # Memorizza il riferimento

# Avvia il server
server.start()

try:
    while True:
        # Aggiorna i valori casualmente per alcune variabili
        node_references["Var1"].set_value(random.randint(0, 100))
        time.sleep(1)  # Aspetta 1 secondo
finally:
    # Ferma il server al termine
    server.stop()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
java Pull requests that update Java code OPC-UA https://plc4x.apache.org/users/protocols/opcua.html python Pull requests that update Python code
Projects
None yet
Development

No branches or pull requests

3 participants