本文利用VS2017和Openssl库实现OPC UA安全服务器,主要包括
1. Windows中Openssl库的安装和使用
2. 服务器证书的生成,信任列表的更新
3. VS2017中Openssl开发环境的配置
4. 支持加密的open62541源文件的编译生成及错误处理文本参考了许多大佬的博客,都附在文章末尾
1. Windows安装Openssl库
这步很简单,http://slproweb.com/products/Win32OpenSSL.html寻找对应版本下载安装, 我的电脑是64位的,故选择64位非轻量级的msi格式文件进行安装,安装成功后将openssl的bin目录添加到系统的环境变量path中,如下图所示
重启。(超大声)
测试是否安装成功,打开命令行窗口,输入命令"openssl"
出现上图结果,第一步操作成功。
2. OPC UA服务器安全证书和私钥文件生成,以及和客户端之间的信任列表更新
使用open62541自带的工具生成证书和私钥文件,从open62541的官方github仓库中下载源码包,查看open62541源码目录tools/certs,运行这个python脚本即可生成证书和私钥。
从命令行进入这个目录,先安装netifaces库
接着运行python脚本
运行命令在没有参数的情况下默认生成服务器的证书和私钥文件。
运行成功后会在原文件夹下生成证书和私钥文件,如下图所示。
打开uaexpert,如下图所示,依次点击Settings——>Manage Certificates
在弹出的对话框中点击Open Certificate Location
上图中的uaexpert.der就是客户端的证书,将这个证书复制到服务器证书所在的文件夹中。
进入uaexpert/PKI/trusted/certs目录下,将刚刚生成的服务器证书复制到该文件夹中,如下图所示。
至此,就完成了服务器证书和私钥的生成,以及服务器和客户端的相互信任。
3. VS2017中Openssl开发环境的配置
首先在VS2017中新建工程,这一步比较简单不赘述。
因为刚刚的Openssl库是64位的,所以在VS2017中最好选择解决方案平台x64,否则会出现不兼容的警告,在解决方案中选择项目——>属性,点击VC++目录,如下图。
在包含目录中选择OpenSSL安装目录中的include文件夹,在库目录中选择OpenSSL安装目录中的lib文件夹。
点击链接器——>输入,如下图所示
在附加依赖项中添加libssl.lib,libcrypto.lib以及WS2_32.Lib。
最后一步将OpenSSL安装目录下的两个dll文件,如下图所示复制到工程目录下,如下图所示
至此,VS的Openssl开发环境基本配置完成。
4. 支持加密的open62541源文件的编译生成及错误处理
按照,open62541官方文档中的build on windows部分进行操作,我在这里使用的是可视化的cmake应用,在选择编译options时,必须勾选
- UA_ENABLE_AMALGAMATION
- UA_ENABLE_ENCRYPTION
- UA_ENABLE_ENCRYPTION_OPENSSL
然后按照文档中的说明进行build,最后生成支持加密等安全操作的open62541.c和open62541.h文件。
在刚刚新建的工程中加入如下图所示的源文件和头文件。
其中open62541.c和open62541.h是刚刚build完成的。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;
}
ServerEnc.c如下(我将证书文件都放在了D:/certs下):
/* 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 <openssl/x509v3.h>
#include "common.h"
#pragma comment(lib, "ws2_32.lib")
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;
}*/
/* 加载server的证书和私匙 */
/*UA_ByteString certificate = loadFile(argv[1]);
UA_ByteString privateKey = loadFile(argv[2]);*/
UA_ByteString certificate = loadFile("D://certs//server_cert.der");
UA_ByteString privateKey = loadFile("D://certs//server_key.der");
/* 加载trustlist */
size_t trustListSize = 1;
/*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]);
// trustList[i] = loadFile("D://certs//uaexpert.der");
trustList[0] = loadFile("D://certs//uaexpert.der");
/* 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);
// 填坑的地方,非常重要,URI需要保证和证书里的URI一致
config->applicationDescription.applicationUri =
UA_STRING_ALLOC("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;
}
启动项目,即可运行。
实现过程中遇到的错误及解决
- 在运行时会出现报错,首先提示我open62541.h文件最后一行的#endif没有对应的#if查了很多资料发现可能是编译器的问题,菜鸡如我,直接把最后一行#endif注释掉,暂时解决了这个问题。
- 第二个错误是open62541.c中函数重复定义的问题,我看了一下代码,发现确实有这个问题,UA_Asy_compareCertificateThumbprint这个函数定义了两次,对比一下函数内部实现是有区别的,在于加密算法的不同,仿照周围函数的命名方式,在中间加上了各自的加密算法,进行了重命名,并在调用的地方也进行了相应的更改,用Basic256算法时,函数名称为UA_Asy_Basic256_compareCertificateThumbprint,用Basic128Rsa15算法时,函数名称为UA_Asy_Basic128Rsa15_compareCertificateThumbprin。另外一个重复定义的函数是UA_Asy_makeCertificateThumbprint,这个函数在两个算法的实现中都定义了,但是函数实现是完全一样的,故将其中一个注释掉即可。
- 第三个错误是提示fopen函数不安全,这一步在项目——>属性——>预处理器定义中加入_CRT_SECURE_NO_WARNINGS即可。
至此,基于Windows的加密服务器可以成功运行了。以上是大致的实现过程,还存在很多问题,望批评指正。
参考博客:
[1]https://blog.csdn.net/whahu1989/article/details/107281639
[2]https://www.jb51.net/article/119025.htm