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

学习open62541 — [12] 加密(使用mbedTLS)使用mbedTLS进行加密通信。

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

本文主要讲述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

Jetbrains全家桶1年46,售后保障稳定

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拒绝。

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/230741.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号