学习open62541 --- [12] 加密(使用mbedTLS)

80 篇文章 477 订阅

本文主要讲述OPC UA Client和OPC UA Server之间如何加密通信,这是个很重要的功能。在之前的系列文章里,都没有使用加密,比较明显的是Server启动时的打印,如下,
在这里插入图片描述
提示没有安全策略,可能会造成泄密。

另外,在使用UaExpert连接Server时,Server左侧图标是个红色的锁,而且这个锁是打开的,表示没有安全策略。
在这里插入图片描述
以上这些都表示未加密。

本文通过mbedTLS实现加密通信,环境如下,

  • OS:Debian10,Ubuntu也是一样
  • open62541版本:v1.1.6
  • mbedTLS版本:2.26.0

open62541在v1.1.1版本之后开始支持OpenSSL加密,可以参考这篇文章,OpenSSL在桌面端应用广泛,mbedTLS主要用在嵌入式领域,桌面端也可以使用。


一 生成自签名证书和私匙

想实现加密通信,就需要自签名证书和私匙,所以首先要生成它们。如果了解HTTPS的话就比较好理解,如果不了解也没问题,按照操作做就可以了。

使用open62541自带工具生成

在open62541源码根目录下的tools/certs目录里,有2个文件,
在这里插入图片描述
可以使用这个python脚本去生成证书和私匙,但需要预先安装一个python模块 — netifaces,输入以下命令安装,

pip3 install netifaces

PS:使用pip3是因为我们会使用python3去运行这个脚本。

安装完netifaces后,我们先看下这个脚本的帮助信息,输入以下命令,

python3 create_self-signed.py  -h

帮助信息如下,
在这里插入图片描述
其中-u是最重要的参数,用于指定证书的URI值,这个值后面会讲到,代码里需要设置成相同值。

验证加密通信需要server和client,它们都需要证书和私匙。首先生成server的证书,执行下面的命令去生成,

python3 create_self-signed.py ./ -u urn:open62541.server.application -c server

接着生成client的证书,

python3 create_self-signed.py ./ -u urn:open62541.client.application -c client

这样就会在当前目录下生成server_cert.der,server_key.der,client_cert.der,client_key.der这4个文件
在这里插入图片描述

可以使用如下命令来查看server证书的URI参数,

openssl x509 -in server_cert.der -inform der -noout -text

如下,
在这里插入图片描述

URI值和前面-u参数后的值是一样的。同理,可以查看client证书里URI值。

最后创建工程目录,然后把证书拷贝出来,
在这里插入图片描述
encryption_mbedtls即使我们的工程目录,读者可以根据需求随意创建。


二 编译mbedTLS

实现加密功能需要依赖mbedTLS库,这个库可以给软件产品加入加密和 SSL/TLS 功能,在嵌入式领域用的比较多。官网是https://tls.mbed.org/,可以去网站上下载,本文使用版本为2.26.0

下载下来后,cd到其源码目录,然后按如下步骤操作,

  • 新建build目录并cd进入
  • 执行cmake .. && make
  • 源码目录下的include目录和build目录下的library目录拷贝出来(build目录下也有include目录,里面是符号链接,所以不需要)
  • 清理library目录,只保留三个库文件,最后结构如下,
    在这里插入图片描述

三 编译open62541

1. 配置

在open62541源码目录下的CMakeLists.txt里找到以下4个option,

  • UA_ENABLE_AMALGAMATION
  • UA_ENABLE_ENCRYPTION
  • UA_ENABLE_ENCRYPTION_OPENSSL
  • UA_ENABLE_ENCRYPTION_MBEDTLS

把第1,2和4改为ON,第3改为OFF

2. 查找mbedTLS

打开open62541源码目录下的tools/cmake/FindMbedTLS.cmake,有如下内容,

#check environment variable
if("$ENV{MBEDTLS_FOLDER_INCLUDE}")
    set(MBEDTLS_FOLDER_INCLUDE "$ENV{MBEDTLS_FOLDER_INCLUDE}")
endif()
if("$ENV{MBEDTLS_FOLDER_LIBRARY}")
    set(MBEDTLS_FOLDER_LIBRARY "$ENV{MBEDTLS_FOLDER_LIBRARY}")
endif()

CMake变量MBEDTLS_FOLDER_INCLUDE和MBEDTLS_FOLDER_LIBRARY分别用于存放mbedTLS的头文件和库文件。这些语句的意思是从环境变量里查找mbedTLS,由于我们是单独编译的,并没有在环境变量里设置其位置,所以需要进行修改。如果不修改的话,可以参考这篇文章

把上节中mbedTLS的存放目录添加进来就行了,如下,

#check environment variable
if("$ENV{MBEDTLS_FOLDER_INCLUDE}")
    set(MBEDTLS_FOLDER_INCLUDE "$ENV{MBEDTLS_FOLDER_INCLUDE}")
else()
    set(MBEDTLS_FOLDER_INCLUDE "/home/wh/work/opcua/encryption_mbedtls/mbedtls/include")
endif()
if("$ENV{MBEDTLS_FOLDER_LIBRARY}")
    set(MBEDTLS_FOLDER_LIBRARY "$ENV{MBEDTLS_FOLDER_LIBRARY}")
else()
    set(MBEDTLS_FOLDER_LIBRARY "/home/wh/work/opcua/encryption_mbedtls/mbedtls/library")
endif()

这里使用的是绝对路径,也可以使用相对路径,简单测试一下就行了。

当然,也可以不修改FindMbedTLS.cmake,在执行cmake命令时指定一下这2个变量的值就行了,下一步会讲。

3. 编译

按如下步骤操作,

  1. cd到open62541源码目录下,新建build目录并cd进入
  2. 执行cmake .. && make
  3. 把open62541.h和bin下的libopen62541.a拷贝到工程目录下的open62541目录里,
    在这里插入图片描述

如果第2步没有修改FindMbedTLS.cmake,那么执行cmake命令时就要如下这样,

cmake -DMBEDTLS_FOLDER_INCLUDE=/home/wh/work/opcua/encryption_mbedtls/mbedtls/include -DMBEDTLS_FOLDER_LIBRARY=/home/wh/work/opcua/encryption_mbedtls/mbedtls/library  .. && make

四 验证加密通信

这里使用代码和UaExpert来验证加密通信。

1. 使用代码验证

这里的测试代码使用open62541自带的example代码,但是有点小坑需要填一下。

client.c代码如下,配置URI的地方是新加的(第47~48行),要和证书里的URI值一样

/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
 * See http://creativecommons.org/publicdomain/zero/1.0/ for more information. */

#include <stdlib.h>

#include "common.h"


#define MIN_ARGS 4

int main(int argc, char* argv[]) {
    if(argc < MIN_ARGS) {
        UA_LOG_FATAL(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
                     "Arguments are missing. The required arguments are "
                     "<opc.tcp://host:port> "
                     "<client-certificate.der> <client-private-key.der> "
                     "[<trustlist1.der>, ...]");
        return EXIT_FAILURE;
    }

    const char *endpointUrl = argv[1];

    /* Load certificate and private key */
    UA_ByteString certificate = loadFile(argv[2]);
    UA_ByteString privateKey  = loadFile(argv[3]);

    /* Load the trustList. Load revocationList is not supported now */
    size_t trustListSize = 0;
    if(argc > MIN_ARGS)
        trustListSize = (size_t)argc-MIN_ARGS;
    UA_STACKARRAY(UA_ByteString, trustList, trustListSize);
    for(size_t trustListCount = 0; trustListCount < trustListSize; trustListCount++)
        trustList[trustListCount] = loadFile(argv[trustListCount+4]);

    UA_ByteString *revocationList = NULL;
    size_t revocationListSize = 0;

    UA_Client *client = UA_Client_new();
    UA_ClientConfig *cc = UA_Client_getConfig(client);
    cc->securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT;
    cc->securityPolicyUri = UA_STRING_ALLOC("http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15");
    UA_ClientConfig_setDefaultEncryption(cc, certificate, privateKey,
                                         trustList, trustListSize,
                                         revocationList, revocationListSize);
    
    // 填坑的地方,非常重要
    UA_String_deleteMembers(&cc->clientDescription.applicationUri);
    cc->clientDescription.applicationUri = UA_STRING_ALLOC("urn:open62541.client.application");
    
    UA_ByteString_clear(&certificate);
    UA_ByteString_clear(&privateKey);
    for(size_t deleteCount = 0; deleteCount < trustListSize; deleteCount++) {
        UA_ByteString_clear(&trustList[deleteCount]);
    }

    /* Secure client connect */
    cc->securityMode = UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; /* require encryption */
    UA_StatusCode retval = UA_Client_connect(client, endpointUrl);
    if(retval != UA_STATUSCODE_GOOD) {
        UA_Client_delete(client);
        return EXIT_FAILURE;
    }

    UA_Variant value;
    UA_Variant_init(&value);

    /* NodeId of the variable holding the current time */
    const UA_NodeId nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
    retval = UA_Client_readValueAttribute(client, nodeId, &value);

    if(retval == UA_STATUSCODE_GOOD &&
       UA_Variant_hasScalarType(&value, &UA_TYPES[UA_TYPES_DATETIME])) {
        UA_DateTime raw_date  = *(UA_DateTime *) value.data;
        UA_DateTimeStruct dts = UA_DateTime_toStruct(raw_date);
        UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "date is: %u-%u-%u %u:%u:%u.%03u\n",
                    dts.day, dts.month, dts.year, dts.hour, dts.min, dts.sec, dts.milliSec);
    }

    /* Clean up */
    UA_Variant_clear(&value);
    UA_Client_delete(client);
    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

server.c代码如下,配置URI的地方是新加的(第64~71行),要和证书里的URI值一样

/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
 * See http://creativecommons.org/publicdomain/zero/1.0/ for more information.
 *
 *    Copyright 2019 (c) Kalycito Infotech Private Limited
 *
 */


#include <signal.h>
#include <stdlib.h>

#include "common.h"


UA_Boolean running = true;
static void stopHandler(int sig) {
    UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "received ctrl-c");
    running = false;
}

int main(int argc, char* argv[]) {
    signal(SIGINT, stopHandler);
    signal(SIGTERM, stopHandler);

    if(argc < 3) {
        UA_LOG_FATAL(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
                     "Missing arguments. Arguments are "
                     "<server-certificate.der> <private-key.der> "
                     "[<trustlist1.der>, ...]");
        return EXIT_FAILURE;
    }

    /* Load certificate and private key */
    UA_ByteString certificate = loadFile(argv[1]);
    UA_ByteString privateKey  = loadFile(argv[2]);

    /* Load the trustlist */
    size_t trustListSize = 0;
    if(argc > 3)
        trustListSize = (size_t)argc-3;
    UA_STACKARRAY(UA_ByteString, trustList, trustListSize);
    for(size_t i = 0; i < trustListSize; i++)
        trustList[i] = loadFile(argv[i+3]);

    /* Loading of a issuer list, not used in this application */
    size_t issuerListSize = 0;
    UA_ByteString *issuerList = NULL;

    /* Loading of a revocation list currently unsupported */
    UA_ByteString *revocationList = NULL;
    size_t revocationListSize = 0;

    UA_Server *server = UA_Server_new();
    UA_ServerConfig *config = UA_Server_getConfig(server);

    UA_StatusCode retval =
        UA_ServerConfig_setDefaultWithSecurityPolicies(config, 4840,
                                                       &certificate, &privateKey,
                                                       trustList, trustListSize,
                                                       issuerList, issuerListSize,
                                                       revocationList, revocationListSize);
      
    // 填坑的地方,非常重要
    UA_String_deleteMembers(&config->applicationDescription.applicationUri);                                                  
    config->applicationDescription.applicationUri = UA_STRING_ALLOC("urn:open62541.server.application");
    for (size_t i = 0; i < config->endpointsSize; ++i)
    {
        UA_String_deleteMembers(&config->endpoints[i].server.applicationUri);
        config->endpoints[i].server.applicationUri = UA_String_fromChars("urn:open62541.server.application");
    }

    
    UA_ByteString_clear(&certificate);
    UA_ByteString_clear(&privateKey);
    for(size_t i = 0; i < trustListSize; i++)
        UA_ByteString_clear(&trustList[i]);
    if(retval != UA_STATUSCODE_GOOD)
        goto cleanup;

    retval = UA_Server_run(server, &running);

 cleanup:
    UA_Server_delete(server);
    return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
}

common.h代码如下,

/* This work is licensed under a Creative Commons CCZero 1.0 Universal License.
 * See http://creativecommons.org/publicdomain/zero/1.0/ for more information. */

#include "open62541.h"


/* loadFile parses the certificate file.
 *
 * @param  path               specifies the file name given in argv[]
 * @return Returns the file content after parsing */
static UA_INLINE UA_ByteString
loadFile(const char *const path) {
    UA_ByteString fileContents = UA_STRING_NULL;

    /* Open the file */
    FILE *fp = fopen(path, "rb");
    if(!fp) {
        errno = 0; /* We read errno also from the tcp layer... */
        return fileContents;
    }

    /* Get the file length, allocate the data and read */
    fseek(fp, 0, SEEK_END);
    fileContents.length = (size_t)ftell(fp);
    fileContents.data = (UA_Byte *)UA_malloc(fileContents.length * sizeof(UA_Byte));
    if(fileContents.data) {
        fseek(fp, 0, SEEK_SET);
        size_t read = fread(fileContents.data, sizeof(UA_Byte), fileContents.length, fp);
        if(read != fileContents.length)
            UA_ByteString_clear(&fileContents);
    } else {
        fileContents.length = 0;
    }
    fclose(fp);

    return fileContents;
}

这三个文件拷贝到test目录下的src目录里,
在这里插入图片描述

在test目录下生成CMakeLists.txt并新建bin和build目录,CMakeLists.txt内容如下,

cmake_minimum_required(VERSION 3.5)

project(demoOpen62541)

set (EXECUTABLE_OUTPUT_PATH  ${PROJECT_SOURCE_DIR}/bin)

add_definitions(-std=c99)

include_directories(${PROJECT_SOURCE_DIR}/open62541)
include_directories(${PROJECT_SOURCE_DIR}/mbedtls/include)
include_directories(${PROJECT_SOURCE_DIR}/src)

find_library(OPEN62541_LIB libopen62541.a HINTS ${PROJECT_SOURCE_DIR}/open62541)
find_library(MBEDCRYPTO_LIB libmbedcrypto.a HINTS ${PROJECT_SOURCE_DIR}/mbedtls/library)
find_library(MBEDTLS_LIB libmbedtls.a HINTS ${PROJECT_SOURCE_DIR}/mbedtls/library)
find_library(MBEDX509_LIB libmbedx509.a HINTS ${PROJECT_SOURCE_DIR}/mbedtls/library)

add_executable(server ${PROJECT_SOURCE_DIR}/src/server.c)
target_link_libraries(server ${OPEN62541_LIB}  ${MBEDCRYPTO_LIB } ${MBEDTLS_LIB} ${MBEDX509_LIB})

add_executable(client ${PROJECT_SOURCE_DIR}/src/client.c)
target_link_libraries(client ${OPEN62541_LIB}  ${MBEDCRYPTO_LIB } ${MBEDTLS_LIB} ${MBEDX509_LIB})

最后整体工程结构如下,
在这里插入图片描述

cd到build目录下执行cmake .. && make,最后在bin目录下成功生成client和server,
在这里插入图片描述
先运行server,

./server ../certs/server_cert.der ../certs/server_key.der ../certs/client_cert.der

再运行client,

./client opc.tcp://127.0.0.1:4840 ../certs/client_cert.der ../certs/client_key.der ../certs/server_cert.der

这个client的功能就是获取系统时间,最后client端打印时间如下,说明运行成功
在这里插入图片描述
从client端的打印可以看出加密方式是Basic128Rsa15,和代码中选择的是一样的。
在这里插入图片描述
这里再简要说下安全策略的选择,在client端的配置结构体里有4个元素是专门用来设置安全策略的,

typedef struct {
	// ... ...
	
    UA_MessageSecurityMode securityMode;  /* None, Sign, SignAndEncrypt. The
                                           * default is invalid. This indicates
                                           * the client to select any matching
                                           * endpoint. */
    UA_String securityPolicyUri; /* SecurityPolicy for the SecureChannel. An
                                  * empty string indicates the client to select
                                  * any matching SecurityPolicy. */
	
	// ... ...
	
	/* Available SecurityPolicies */
    size_t securityPoliciesSize;
    UA_SecurityPolicy *securityPolicies;

	// ... ...
} UA_ClientConfig;

securityMode元素有以下选择,可以选择不加密 or 签名 or 签名+加密,

/**
 * MessageSecurityMode
 * ^^^^^^^^^^^^^^^^^^^
 * The type of security to use on a message. */
typedef enum {
    UA_MESSAGESECURITYMODE_INVALID = 0,
    UA_MESSAGESECURITYMODE_NONE = 1,
    UA_MESSAGESECURITYMODE_SIGN = 2,
    UA_MESSAGESECURITYMODE_SIGNANDENCRYPT = 3,
    __UA_MESSAGESECURITYMODE_FORCE32BIT = 0x7fffffff
} UA_MessageSecurityMode;

securityPolicyUri有以下三个选择,表示加密方式有三种:Basic128Rsa15,Basic256和Basic256Sha256

UA_STRING_ALLOC("http://opcfoundation.org/UA/SecurityPolicy#Basic128Rsa15");
UA_STRING_ALLOC("http://opcfoundation.org/UA/SecurityPolicy#Basic256");
UA_STRING_ALLOC("http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256");

而对于securityPoliciesSize和securityPolicies,则是由函数UA_ClientConfig_setDefaultEncryption()根据安全证书里的内容对其进行填充,细节可以看该函数的源码。

2. 使用UaExpert验证

先按照上面那样把OPC UA Server运行起来,

./server ../certs/server_cert.der ../certs/server_key.der

然后打开UaExpert,使用UaExpert进行连接的操作方法可以参照这篇文章,这个时候我们可以看到有7个endpoint,
在这里插入图片描述
红色那把锁就是我们之前一直使用的。笔图案的endpoint是指只有签名功能,闭合的锁表示既有签名又有加密,加密方式有三种:Basic128Rsa15,Basic256和Basic256Sha256。

绿色的Basic256Sha256的安全性最高,这里就连接它。双击这个endpoint,弹出如下界面
在这里插入图片描述
点击红圈中的连接按钮,会弹出如下证书验证界面,
在这里插入图片描述
点击Trust Server Certificate,这样UaExpert验证证书就会通过,并信任这个证书,如下,
在这里插入图片描述
最后,点击右下的Continue按钮进行连接,连接成功,如下图
在这里插入图片描述
UaExpert信任的证书可以在Settings->Manage Certificates…里查看,
在这里插入图片描述
在这里插入图片描述


五 总结

本文主要讲述如何OPC UA Client和OPC UA Server之间如何加密通信,这是个非常重要的功能,而且过程也相对复杂一些,但掌握了这个功能,就可以增加软件产品的安全性。

主要核心参数是证书里的URI,只有当Client端拿到正确的证书才可以和Server建立通信,否则就会被Server拒绝。

如果有写的不对的地方,希望能留言指正,谢谢阅读。

评论 70
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值