4

Is-it possible in Eclipse Milo that a client connects to a server with this authentication parameters: "certificate + private key" ? And also with parameters "Security Policy" and "Message Security Mode" ?

(as in UAExpert client : http://documentation.unified-automation.com/uaexpert/1.4.0/html/connect.html)

If yes, then how?


I have at my disposal:

  • one private key at ".perm" file format;
  • one certificate at ".der" file format;
  • one CA of the server at ".der" file format;
  • and the CA of the server with 4096-bit at ".der" file format;
Kevin Herron
  • 6,500
  • 3
  • 26
  • 35
Henry198
  • 53
  • 1
  • 8

2 Answers2

2

yes, it's currently possible although it's not made "easy" like using a username/password is right now.

The client SDK exposes an interface called IdentityProvider which is delegated to while the client is connecting and is given the endpoint and server nonce. It returns a 2-tuple containing a UserIdentityToken and a SignatureData.

You would need to implement this interface for the X509IdentityToken case and return your certificate (in the X509IdentityToken) as well as proof that you have the key to it (in the SignatureData).

Once you have this IdentityProvider you would just tell the client to use it while you were configuring it by calling setIdentityProvider when building the OpcUaClientConfig object.

Since this is a bit burdensome and the point of an SDK is to relieve the user of burden I will make this a feature ticket for Milo as well. If you're not up to implementing it yourself I can get to it this week.

Kevin Herron
  • 6,500
  • 3
  • 26
  • 35
  • 1
    And just to be clear: this is about using an X509 certificate to authenticate the user. It's not the same thing as setting up the certificate that is used by the client to secure communications between the client server. That is, of course, supported. – Kevin Herron Sep 13 '16 at 14:24
  • Ok, I'll see, and I'll try to implement method. I have at my disposal: - one private key at ".perm" file format; - one certificate at ".der" file format; - one CA of the server at ".der" file format; - and the CA of the server with 4096-bit at ".der" file format; – Henry198 Sep 13 '16 at 15:11
  • The `X509IdentityToken` is created using the bytes from the certificate. I believe the `SignatureData` is formed by signing the server certificate + server nonce with the corresponding private key for the above certificate using the asymmetric algorithm from the security policy of the endpoint you connected to... – Kevin Herron Sep 13 '16 at 15:19
  • Ok. And the CA should be used ? – Henry198 Sep 13 '16 at 15:39
  • You would use the server certificate provided by the `EndpointDescription` you're given. You don't need the one you possess out of band. – Kevin Herron Sep 13 '16 at 15:44
1

I've resolved my problem with help of Kevin Herron by using his class X509IdentityProvider.

Here is the solution code :

PemFile.java

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;

import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;

public class PemFile {

    private PemObject pemObject;

    public PemFile(String filename) throws FileNotFoundException, IOException {
        PemReader pemReader = new PemReader(new InputStreamReader(new FileInputStream(filename)));
        try {
            this.pemObject = pemReader.readPemObject();
        } finally {
            pemReader.close();
        }
    }

    public PemObject getPemObject() {
        return pemObject;
    }
}

X509IdentityProvider.java

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.milo.opcua.sdk.client.api.identity.IdentityProvider;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString;
import org.eclipse.milo.opcua.stack.core.types.enumerated.UserTokenType;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import org.eclipse.milo.opcua.stack.core.types.structured.SignatureData;
import org.eclipse.milo.opcua.stack.core.types.structured.UserIdentityToken;
import org.eclipse.milo.opcua.stack.core.types.structured.UserTokenPolicy;
import org.eclipse.milo.opcua.stack.core.types.structured.X509IdentityToken;
import org.eclipse.milo.opcua.stack.core.util.SignatureUtil;
import org.jooq.lambda.tuple.Tuple2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class X509IdentityProvider implements IdentityProvider {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final X509Certificate certificate;
    private final PrivateKey privateKey;

    public X509Certificate getCertificate() {
        return certificate;
    }

    public PrivateKey getPrivateKey() {
        return privateKey;
    }

    public X509IdentityProvider(X509Certificate certificate, PrivateKey privateKey) {
        this.certificate = certificate;
        this.privateKey = privateKey;
    }

    public X509IdentityProvider(String certificate, String privateKey) {
        this.certificate = loadCertificateFromDerFile(certificate);

        Security.addProvider(new BouncyCastleProvider());
        KeyFactory kf;
        PrivateKey privateKeyTmp = null;
        try {
            kf = KeyFactory.getInstance("RSA", "BC");
            privateKeyTmp = loadPrivateKeyFromPemFile(kf, privateKey);
        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException | IOException e) {
            e.printStackTrace();
        }
        this.privateKey = privateKeyTmp;

    }

    @Override
    public Tuple2<UserIdentityToken, SignatureData> getIdentityToken(EndpointDescription endpoint,
            ByteString serverNonce) throws Exception {
        UserTokenPolicy tokenPolicy = Arrays.stream(endpoint.getUserIdentityTokens())
                .filter(t -> t.getTokenType() == UserTokenType.Certificate).findFirst()
                .orElseThrow(() -> new Exception("no x509 certificate token policy found"));
        String policyId = tokenPolicy.getPolicyId();
        SecurityPolicy securityPolicy = SecurityPolicy.Basic256;
        String securityPolicyUri = tokenPolicy.getSecurityPolicyUri();
        try {
            if (securityPolicyUri != null && !securityPolicyUri.isEmpty()) {
                securityPolicy = SecurityPolicy.fromUri(securityPolicyUri);
            } else {
                securityPolicyUri = endpoint.getSecurityPolicyUri();
                securityPolicy = SecurityPolicy.fromUri(securityPolicyUri);
            }
        } catch (Throwable t) {
            logger.warn("Error parsing SecurityPolicy for uri={}", securityPolicyUri);
        }
        X509IdentityToken token = new X509IdentityToken(policyId, ByteString.of(certificate.getEncoded()));
        SignatureData signatureData;
        ByteString serverCertificate = endpoint.getServerCertificate();
        byte[] serverCertificateBytes = serverCertificate.isNotNull() ? serverCertificate.bytes() : new byte[0];
        byte[] serverNonceBytes = serverNonce.isNotNull() ? serverNonce.bytes() : new byte[0];
        assert serverCertificateBytes != null;
        assert serverNonceBytes != null;
        byte[] signature = SignatureUtil.sign(securityPolicy.getAsymmetricSignatureAlgorithm(), privateKey,
                ByteBuffer.wrap(serverCertificateBytes), ByteBuffer.wrap(serverNonceBytes));
        signatureData = new SignatureData(securityPolicy.getAsymmetricSignatureAlgorithm().getUri(),
                ByteString.of(signature));
        return new Tuple2<>(token, signatureData);
    }


    private static X509Certificate loadCertificateFromDerFile(String filename) {
        InputStream in;
        X509Certificate cert = null;
        try {
            in = new FileInputStream(filename);

            CertificateFactory factory = CertificateFactory.getInstance("X.509");
             cert = (X509Certificate) factory.generateCertificate(in);
        } catch (FileNotFoundException | CertificateException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return cert;
    }

    private static PrivateKey loadPrivateKeyFromPemFile(KeyFactory factory, String filename)
            throws InvalidKeySpecException, FileNotFoundException, IOException {
        PemFile pemFile = new PemFile(filename);
        byte[] content = pemFile.getPemObject().getContent();
        PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(content);
        return factory.generatePrivate(privKeySpec);
    }

}

ClientRunner.java

...
...

private OpcUaClient createClient() throws Exception {
        SecurityPolicy securityPolicy = clientExample.getSecurityPolicy(); // For example : SecurityPolicy.Basic256
        String securityMode = clientExample.getSecurityMode(); // For example : "SignAndEncrypt"

        EndpointDescription[] endpoints = UaTcpStackClient.getEndpoints(endpointUrl).get();

        EndpointDescription endpoint = Arrays.stream(endpoints)
                .filter(e -> e.getSecurityPolicyUri().equals(securityPolicy.getSecurityPolicyUri()))//
                .filter(e -> e.getSecurityMode().toString().compareTo(securityMode) == 0)//
                .findFirst()//
                .orElseThrow(() -> new Exception("no desired endpoints returned"));

        logger.info("Using endpoint: {} [{}]", endpoint.getEndpointUrl(), securityPolicy);

        loader.load();

        // Mode : securityPolicy == SecurityPolicy.Basic256 && securityMode.compareTo("SignAndEncrypt") == 0)
        X509IdentityProvider x509IdentityProvider = new X509IdentityProvider("/certificate.der",
                "/privateKey.pem");
        X509Certificate cert = x509IdentityProvider.getCertificate();
        KeyPair keyPair = new KeyPair(cert.getPublicKey(), x509IdentityProvider.getPrivateKey());
        OpcUaClientConfig config = OpcUaClientConfig.builder().setApplicationName(LocalizedText.english("opc-ua client"))//
                .setApplicationUri("urn:opcua client")//
                .setCertificate(cert)//
                .setKeyPair(keyPair)//
                .setEndpoint(endpoint)//
                .setIdentityProvider(x509IdentityProvider)//
                .setIdentityProvider(clientExample.getIdentityProvider())//
                .setRequestTimeout(uint(5000))//
                .build();


        return new OpcUaClient(config);
    }
Community
  • 1
  • 1
Henry198
  • 53
  • 1
  • 8
  • There are two `setIdentityProvider` calls to the `OpcUaClientConfig.builder()`. The second one is presumably a left-over from the original example code and should be deleted. Is that right? – vorrade - Apr 21 '22 at 17:23