Step 1: Collect Information about the Server Using UaDiscovery::findServers, UaDiscovery::getEndpoints
The method discover uses the class UaClientSdk::UaDiscovery to retrieve information about the server. The class UaDiscovery provides two methods:
- findServers
- Gets a list of servers known by the discovery server.
- getEndpoints
- Gets a list of endpoints supported by the server.
We will need this list of endpoints later on in this lesson to set up a secure connection with the server.
Add the following code to sampleclient.h:
...
UaStatus discover();
...
Add the following code to sampleclient.cpp:
#include "sampleclient.h"
#include "uasession.h"
#include "samplesubscription.h"
#include "configuration.h"
#include "uadiscovery.h"
...
UaStatus SampleClient::discover()
{
UaDiscovery discovery;
ServiceSettings serviceSettings;
ClientSecurityInfo clientSecurityInfo;
OpcUa_UInt32 i, j;
OpcUa_Int32 k;
printf("\nCall FindServers on Url %s\n", m_pConfiguration->getDiscoveryUrl().toUtf8());
result = discovery.findServers(
serviceSettings,
m_pConfiguration->getDiscoveryUrl(),
clientSecurityInfo,
applicationDescriptions);
if (result.isGood())
{
printf("\nFindServers succeeded\n");
for (i = 0; i < applicationDescriptions.length(); i++)
{
printf("** Application [%d] **********************************************************\n", i);
sTemp = &applicationDescriptions[i].ApplicationUri;
printf(
" ApplicationUri %s\n", sTemp.
toUtf8());
sTemp = &applicationDescriptions[i].ApplicationName.Text;
printf(" ApplicationName %s\n", sTemp.toUtf8());
for (k = 0; k < applicationDescriptions[i].NoOfDiscoveryUrls; k++)
{
UaString sDiscoveryUrl(applicationDescriptions[i].DiscoveryUrls[k]);
printf("** DiscoveryUrl [%s] ***********************\n", sDiscoveryUrl.toUtf8());
result = discovery.getEndpoints(
serviceSettings,
sDiscoveryUrl,
clientSecurityInfo,
endpointDescriptions);
if (result.isGood())
{
for (j = 0; j < endpointDescriptions.length(); j++)
{
printf("** Endpoint[%d] ***********************************************\n", j);
sTemp = &endpointDescriptions[j].EndpointUrl;
printf(" Endpoint URL %s\n", sTemp.toUtf8());
sTemp = &endpointDescriptions[j].SecurityPolicyUri;
printf(" Security Policy %s\n", sTemp.toUtf8());
sTemp = "Invalid";
{
sTemp = "None";
}
{
sTemp = "Sign";
}
{
sTemp = "SignAndEncrypt";
}
printf(" Security Mode %s\n", sTemp.toUtf8());
printf("**************************************************************\n");
}
}
else
{
printf("GetEndpoints failed with status %s\n", result.toString().toUtf8());
}
printf("************************************************************************\n", i);
}
printf("******************************************************************************\n", i);
}
}
else
{
printf("FindServers failed with status %s\n", result.toString().toUtf8());
}
return result;
}
{
...
}
The method discover calls UaDiscovery::findServers on the discovery URL provided in the configuration file. The Discovery Server returns a list of servers. On each servers in this list, UaDiscovery::getEndpoints is called. The list of servers and provided endpoints is displayed in the console. In our case, findServers is called on the UaServerCpp.
Now we update main for calling discover. The discovered information is only displayed in the console so far. It will be used for a secure connection in Step 3.
Replace the following code in client_cpp_sdk_tutorial.cpp
{
pMyClient->setConfiguration(pMyConfiguration);
status = pMyClient->connect();
}
else
{
delete pMyConfiguration;
pMyConfiguration = NULL;
}
with
{
pMyClient->setConfiguration(pMyConfiguration);
status = pMyClient->discover();
}
else
{
delete pMyConfiguration;
pMyConfiguration = NULL;
}
{
printf("\nPress Enter to connect\n");
getchar();
status = pMyClient->connect();
}
and remove the marked lines (which are no longer needed to show the functionality):
{
...
status = pMyClient->write();
printf("\nPress Enter to do a simple browse\n");
getchar();
status = pMyClient->browseSimple();
printf("\nPress Enter to browse with continuation point\n");
getchar();
status = pMyClient->browseContinuationPoint();
printf("\nPress Enter to disconnect\n");
getchar();
status = pMyClient->disconnect();
}
When compiling and running the application, we can see that the UaServerCpp provides three different endpoints with different security policies:
Call FindServers on Url opc.tcp:
FindServers succeeded
Application [0] **********************************************************
ApplicationUri urn:vbox-xp-sp3:UnifiedAutomation:UaServerCpp
ApplicationName UaServerCpp@vbox-xp-sp3
DiscoveryUrl [opc.tcp:
Endpoint[0] ***********************************************
Endpoint URL opc.tcp:
Security Policy http:
Security Mode None
Endpoint[1] ***********************************************
Endpoint URL opc.tcp:
Security Policy http:
Security Mode Sign
Endpoint[2] ***********************************************
Endpoint URL opc.tcp:
Security Policy http:
Security Mode SignAndEncrypt
Press Enter to connect with security
Step 2: Create and Load an Application Instance Certificate
To set up a secure connection, client and server have to exchange and trust each others certificates. In this step we will create a self-signed certificate for our client and set up the PKI directory structure.
Add the following code to configuration.h
...
class Configuration
{
UA_DISABLE_COPY(Configuration);
public:
...
UaStatus setupSecurity(SessionSecurityInfo& sessionSecurityInfo);
...
private:
...
UaString m_certificateTrustListLocation;
UaString m_certificateRevocationListLocation;
UaString m_issuersRevocationListLocation;
};
Include the following header files in configuration.cpp:
#include "configuration.h"
#include "uasettings.h"
#include "uadir.h"
#include "uapkicertificate.h"
Add the following code to Configuration::loadConfiguration. The paths for creating the directory structure for storing certificates are loaded from the configuration file.
...
pSettings->beginGroup("UaSampleConfig");
value = pSettings->
value(
"CertificateTrustListLocation");
m_certificateTrustListLocation = value.
toString();
value = pSettings->
value(
"CertificateRevocationListLocation");
m_certificateRevocationListLocation = value.
toString();
value = pSettings->
value(
"IssuersCertificatesLocation");
m_issuersCertificatesLocation = value.
toString();
value = pSettings->
value(
"IssuersRevocationListLocation");
m_issuersRevocationListLocation = value.
toString();
value = pSettings->
value(
"ClientCertificate");
m_clientCertificateFile = value.
toString();
value = pSettings->
value(
"ClientPrivateKey");
m_clientPrivateKeyFile = value.
toString();
...
Implement the helper method Configuration::setupSecurity which will be used in Step 3: Set up a Secure Connection.
UaStatus Configuration::setupSecurity(SessionSecurityInfo& sessionSecurityInfo)
{
dirHelper.mkpath(usClientCertificatePath);
dirHelper.mkpath(usPrivateKeyPath);
dirHelper.mkpath(usTrustListLocationPath);
dirHelper.mkpath(usRevocationListPath);
if (clientCertificate.isNull())
{
char szHostName[256];
if ( 0 == UA_GetHostname(szHostName, 256) )
{
sNodeName = szHostName;
}
identity.commonName =
UaString(
"Client_Cpp_SDK@%1").
arg(sNodeName);
identity.organization = "Organization";
identity.organizationUnit = "Unit";
identity.locality = "LocationName";
identity.state = "State";
identity.country = "DE";
identity.domainComponent = sNodeName;
info.URI =
UaString(
"urn:%1:%2:%3").
arg(sNodeName).
arg(COMPANY_NAME).
arg(PRODUCT_NAME);
sNodeName.
copyTo(&info.DNSNames[0]);
IssuerPrivateKey = keyPair.privateKey();
SubjectPublicKey = keyPair.publicKey();
UaPkiCertificate cert ( info, identity, SubjectPublicKey, identity, IssuerPrivateKey );
cert.
toDERFile ( m_clientCertificateFile.toUtf8() );
keyPair.toPEMFile ( m_clientPrivateKeyFile.toUtf8(), 0 );
}
result = sessionSecurityInfo.initializePkiProviderOpenSSL(
m_certificateRevocationListLocation,
m_certificateTrustListLocation,
m_issuersRevocationListLocation,
m_issuersCertificatesLocation);
{
printf("*******************************************************\n");
printf("** setupSecurity failed!\n");
printf("** Could not initialize PKI\n");
printf("*******************************************************\n");
return result;
}
result = sessionSecurityInfo.loadClientCertificateOpenSSL(
m_clientCertificateFile,
m_clientPrivateKeyFile);
{
printf("*******************************************************\n");
printf("** setupSecurity failed!\n");
printf("** Could not load Client certificate\n");
printf("** Connect will work only without security\n");
printf("*******************************************************\n");
return result;
}
return result;
}
First, the folder structure for storing certificates are created using the classes UaDir and UaUniString.
For certificate handling, the SDK contains the class UaPkiCertificate. If no certificate exists, a new one is created. UaPkiIdentity and UaPkiCertificateInfo are used to store the necessary information. An RSA key pair and a self-signed certificate are created and stored in the appropriate directories.
Finally, the PKI provider is initialized and the client certificate as well as the private key are loaded.
Step 3: Set up a Secure Connection
In this step we will add the method connectSecure for establishing a secure connection and some helper methods.
Add the following code to sampleclient.h:
...
public:
...
...
private:
maxReferencesToReturn);
UaStatus connectInternal(
const UaString& serverUrl, SessionSecurityInfo& sessionSecurityInfo);
UaStatus findSecureEndpoint(SessionSecurityInfo& sessionSecurityInfo);
UaStatus checkServerCertificateTrust(SessionSecurityInfo& sessionSecurityInfo);
void printCertificateData(
const UaByteString& serverCertificate);
int userAcceptCertificate();
Add the follwoing include to sampleclient.cpp:
#include "sampleclient.h"
#include "uasession.h"
#include "samplesubscription.h"
#include "configuration.h"
#include "uadiscovery.h"
#include "uapkicertificate.h"
Add a new method SampleClient::connectInternal as shown in the sample code below. You will notice that it contains code which was previously used in the method SampleClient::connect with some modifications. We will use it as a helper method in the methods connect and connectSecure to establish the connection to the server.
UaStatus SampleClient::connectInternal(
const UaString& serverUrl, SessionSecurityInfo& sessionSecurityInfo)
{
SessionConnectInfo sessionConnectInfo;
char szHostName[256];
if (0 == UA_GetHostname(szHostName, 256))
{
sNodeName = szHostName;
}
sessionConnectInfo.sApplicationName = m_pConfiguration->getApplicationName();
sessionConnectInfo.sApplicationUri =
UaString(
"urn:%1:%2:%3").
arg(sNodeName).
arg(COMPANY_NAME).
arg(PRODUCT_NAME);
sessionConnectInfo.sProductUri =
UaString(
"urn:%1:%2").
arg(COMPANY_NAME).
arg(PRODUCT_NAME);
sessionConnectInfo.sSessionName = sessionConnectInfo.sApplicationUri;
sessionConnectInfo.bAutomaticReconnect = m_pConfiguration->getAutomaticReconnect();
sessionConnectInfo.bRetryInitialConnect = m_pConfiguration->getRetryInitialConnect();
printf(
"\nConnecting to %s\n", serverUrl.
toUtf8());
serverUrl,
sessionConnectInfo,
sessionSecurityInfo,
this);
if (result.isGood())
{
printf("Connect succeeded\n");
}
else
{
printf("Connect failed with status %s\n", result.toString().toUtf8());
}
return result;
}
Replace the method connect with the following code. Note that the new helper method connectInternal is used.
{
SessionSecurityInfo sessionSecurityInfo;
return connectInternal(m_pConfiguration->getServerUrl(), sessionSecurityInfo);
}
Add a new method connectSecure():
{
SessionSecurityInfo sessionSecurityInfo;
result = m_pConfiguration->setupSecurity(sessionSecurityInfo);
{
result = findSecureEndpoint(sessionSecurityInfo);
}
{
result = checkServerCertificateTrust(sessionSecurityInfo);
}
{
result = connectInternal(m_pConfiguration->getServerUrl(), sessionSecurityInfo);
{
printf("********************************************************************************************\n");
printf("Connect with security failed. Make sure the client certificate is in the servers trust list.\n");
printf("********************************************************************************************\n");
}
}
return result;
}
Apart from the actual connect by calling the helper method connectInternal, the connection establishment can be divided into three steps:
- Step 1: Load or create a client certificate
- Here, the helper method setupSecurity from Step 2 is used.
- Step 2: Find a secure Endpoint on the server
- A server usually provides different endpoints (see Step 1. The helper method findSecureEndpoint picks the most secure one.
- Step 3: Validate the server certificate
- To establish a secure connection client and server have to exchange certificates and trust each other’s. The helper method checkServerCertificateTrust is used to check and trust or reject the server’s certificate.
Finally, connectInternal is used to connect to the endpoint returned by findSecureEndpoint.
Add the helper method SampleClient::findSecureEndpoint to sampleclient.cpp:
UaStatus SampleClient::findSecureEndpoint(SessionSecurityInfo& sessionSecurityInfo)
{
ServiceSettings serviceSettings;
ClientSecurityInfo clientSecurityInfo;
UaDiscovery discovery;
OpcUa_UInt32 bestSecurityIndex = 0;
printf("\nTry to find secure Endpoint on: %s\n", m_pConfiguration->getServerUrl().toUtf8());
result = discovery.getEndpoints(
serviceSettings,
m_pConfiguration->getServerUrl(),
clientSecurityInfo,
endpointDescriptions);
if (result.isGood())
{
OpcUa_Byte securityLevel = 0;
OpcUa_UInt32 i;
for (i = 0; i < endpointDescriptions.length(); i++)
{
if (endpointDescriptions[i].SecurityLevel > securityLevel)
{
bestSecurityIndex = i;
securityLevel = endpointDescriptions[i].SecurityLevel;
}
}
{
printf("No secure endpoint available on server\n");
result = OpcUa_BadSecurityConfig;
}
else
{
printf("Endpoint with best security found:\n");
sTemp = &endpointDescriptions[bestSecurityIndex].EndpointUrl;
printf(
" Endpoint URL %s\n", sTemp.
toUtf8());
sTemp = &endpointDescriptions[bestSecurityIndex].SecurityPolicyUri;
printf(" Security Policy %s\n", sTemp.toUtf8());
sTemp = "Invalid";
{
sTemp = "None";
}
{
sTemp = "Sign";
}
{
sTemp = "SignAndEncrypt";
}
printf(" Security Mode %s\n", sTemp.toUtf8());
sessionSecurityInfo.serverCertificate = endpointDescriptions[bestSecurityIndex].ServerCertificate;
sessionSecurityInfo.sSecurityPolicy = endpointDescriptions[bestSecurityIndex].SecurityPolicyUri;
sessionSecurityInfo.messageSecurityMode = endpointDescriptions[bestSecurityIndex].SecurityMode;
}
}
else
{
printf("GetEndpoints failed with status %s\n", result.toString().toUtf8());
}
return result;
}
The helper method findSecureEndpoints calls Discovery::getEndpoints (see Step 1). Then the most secure endpoint is picked.
Add helper method checkServerCertificateTrust:
UaStatus SampleClient::checkServerCertificateTrust(SessionSecurityInfo& sessionSecurityInfo)
{
if (sessionSecurityInfo.verifyServerCertificate().isBad())
{
printf("\n");
printf("\n");
printf("-------------------------------------------------------\n");
printf("- The following certificate is not trusted yet -\n");
printf("-------------------------------------------------------\n");
printCertificateData(sessionSecurityInfo.serverCertificate);
printf("\n");
printf("'y' + Enter if you want to trust the certificate temporarily.\n");
printf("'p' + Enter if you want to trust the certificate permanently an copy the server certificate into the client trust list.\n");
printf("Enter if you don't want to trust the certificate.\n");
int accept = userAcceptCertificate();
if (accept == 1)
{
printf("Certificate was acceppted temporarily.\n");
sessionSecurityInfo.doServerCertificateVerify = OpcUa_False;
}
else if (accept == 2)
{
result = sessionSecurityInfo.saveServerCertificate(sThumbprint);
{
printf("Certificate was accepted permanently.\n");
}
else
{
printf(
"Failed to accept certifcate permanently :%s\n", result.
toString().
toUtf8());
}
sessionSecurityInfo.doServerCertificateVerify = OpcUa_True;
}
else
{
printf("Certificate was rejected by user.\n");
result = OpcUa_BadCertificateUntrusted;
}
}
return result;
}
verifyServerCertificate is used to check if the server’s certificate is already in the client’s trust list.
If not, it is displayed using the helper function printCertificateData. Then the user has three options:
- trust the certificate temporarily
- trust the certificate permanently
- not trust the certificate
The helper method userAcceptCertificate is used to read the user’s choice.
If the user chooses to accept the certificate temporarily, SessionSecurityInfo::doServerCertificateVerify is set to OpcUa_False. This means that the SDK does not check if the certificate is in the clients trust list when connecting, because it has already been checked by the user (and won’t be copied to the trust list).
If the option to accept the certificate permanently has been picked, it is copied to the client’s trust list. SessionSecurityInfo::doServerCertificateVerify is set to OpcUa_True (because the certificate can be validated during connect now that it’s in the trust list).
Add helper method printCertificateData to sampleclient.cpp which is used by checkServerCertificateTrust to display the server’s certificate.
void SampleClient::printCertificateData(
const UaByteString& serverCertificate)
{
printf(
"- Issuer.commonName %s\n", cert.
issuer().commonName.
toUtf8() );
printf(
"- Issuer.organization %s\n", cert.
issuer().organization.
toUtf8() );
printf(
"- Issuer.organizationUnit %s\n", cert.
issuer().organizationUnit.
toUtf8() );
printf(
"- Issuer.state %s\n", cert.
issuer().state.
toUtf8() );
printf(
"- Issuer.country %s\n", cert.
issuer().country.
toUtf8() );
}
Add helper method userAcceptCertificate to read the user input when asked what to do with the server’s certificate.
int SampleClient::userAcceptCertificate()
{
int result = 0;
int ch = getchar();
if (ch == 'y' || ch == 'Y')
{
result = 1;
}
else if (ch == 'p' || ch == 'P')
{
result = 2;
}
else
{
result = 0;
}
while (ch != '\n')
{
ch = getchar();
}
return result;
}
Modify main to establish a secure connection:
Replace
{
printf("\nPress Enter to connect\n");
getchar();
status = pMyClient->connect();
}
with
{
printf("\nPress Enter to connect with security\n");
getchar();
status = pMyClient->connectSecure();
}
Finally, modify main to connect with security:
...
{
printf("\nPress Enter to connect with security\n");
getchar();
status = pMyClient->connectSecure();
}
...