ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于OpenSSL的Delphi国密SM2/SM3/SM4跨平台动态库封装方案

基于OpenSSL的Delphi国密SM2/SM3/SM4跨平台动态库封装方案 简介面向 Delphi 开发者的跨平台国密算法加解密工具包重点解决 Windows 与 Linux 系统下 32 位、64 位环境集成 SM2、SM3、SM4 的兼容需求适合政务、金融等对国产密码算法有合规要求的项目也适用于需要快速实现国密签名、摘要与对称加密的桌面端和服务端工程师。压缩包内共 10 个文件主要包含 Pascal 单元与编译结果、C 语言头文件、静态链接库、测试用例和多平台动态库整体仅 277KB体积小巧其中动态库已适配 32 位与 64 位架构静态库可满足链接期集成需求。已有 128 人学习下载。开箱即用无需额外编译环境Delphi 10.2 及以上版本可直接引用内附的 Pascal 封装通过简单函数调用完成 SM2 密钥生成、签名验签与加解密、SM3 摘要计算、SM4 对称加解密目录按 include 与 lib 分层头文件和库文件一目了然便于按需选用并快速部署。附带测试用例便于开发者理解调用方式并完成二次开发。 做Delphi开发十几年我几乎没见过哪个社区像Delphi圈子这样对国密算法SM2/SM3/SM4又需要又无奈的。这两年很多C/S老系统被拉到密评改造清单里Win/Linux x86/x64环境都得覆盖加解密库还得能在一个Delphi工程里干净利落地调用。网上搜一圈能找到的要么是只跑Windows的demo要么是某个大神随手写的sm4.pasSM2和SM3根本不全更别提Linux下的动态库了。最后我干脆自己做了一套底层用OpenSSL实现动态库上层写Pascal封装Windows和Linux的x86/x64四个目标全编出来。这篇文章把整个设计过程和踩坑记录整理出来给同样被困在Delphi国密需求里的朋友一个参考。1. 国密改造的第一道坎Delphi生态里几乎没有能直接用的SM2/SM3/SM4库1.1 一个很典型的改造场景先交代一下背景。我手上这个老项目是Delphi 2007时代留下来的C/S架构客户端跑在Windows上服务端部署在Linux服务器现在要求登录过程用SM2签名、传输过程用SM4加密、关键数据的完整性校验用SM3。这几乎是国产密码改造里最标准的组合。问题在于这个系统跨度太大了。客户端有Win32和Win64两种服务端有x86_64的Linux还有几台老工控机是32位Linux客户端和服务端之间要做跨语言验签和解密。也就是说算法库本身不仅要能用还必须以稳定的形态提供不管是Delphi调用还是将来C#、Java、Python的服务端进程调用同一个库结果都得一致。1.2 现有方案都不够顺手我在开发前专门把Delphi圈子里的国密方案翻了一遍结论是三个字不好办。首先GitHub上确实有一些纯Pascal写的sm4.pas但大多数只覆盖了SM4的ECB模式SM2签名和SM3摘要要不就没有要不就是用Demo级代码拼出来的正确性和边界条件都没保证。其次OpenSSL官方不支持Delphi虽然可以自己声明external函数去调但OpenSSL的API对Delphi开发者极不友好光是EVP_PKEY_CTX那套指针生命周期就够喝一壶而且还有linux版本的so依赖问题要处理。商用的加密控件我也评估过价格贵先不说大部分只提供Windows DLLLinux下基本没戏。所以最终只能走自研路线把算法能力放在动态库层Pascal层只做薄封装。1.3 定下来的整体架构这套工具包最终分成三层C动态库层基于OpenSSL的EVP接口封装出SM2、SM3、SM4的扁平C API编译成Win32/Win64/Linux x64/Linux x86四种形态。Pascal封装层一个统一的GmCrypto.pas单元声明external函数提供TGmCrypto类把数组长度、错误码、内存管理这些细节吞掉。业务调用层不管是VCL客户端还是Linux服务端的Delphi程序只跟TBytes打交道不需要了解C接口的细节。选择动态库而不是纯Pascal实现核心逻辑在于算法正确性和性能交给OpenSSL这个经过大规模验证的库去保证Delphi侧只需要维护一个相对稳定的封装边界。动态库的C ABI非常稳定将来换算法版本、加国密算法都只需要替换库文件不用动Delphi代码。2. 底层C API的设计一个动态库把算法全部包圆2.1 基于OpenSSL封装为什么不用GmSSL算法底座我用的是OpenSSL而不是GmSSL。原因是OpenSSL从1.1.1开始就原生支持SM2、SM3、SM4不需要打任何补丁跨平台编译文档也全。GmSSL对国密算法的支持更全比如ZUC祖冲之算法但它的编译链和版本迭代不像OpenSSL那么稳对于只需要SM2/SM3/SM4的项目OpenSSL已经绰绰有余。我用的是OpenSSL 1.1.1系列后来也顺手在OpenSSL 3.0上编了一版。两者的EVP接口写法有差异但C API这层是固定的内部改就好上层Pascal不需要动。2.2 导出的C接口长什么样动态库导出的API按“谁调用谁释放”的原则设计输入输出全是uint8_t *加长度不做任何编码假设这样Delphi、C#、Java都能对接。头文件大概是这个形态#ifndef GM_CRYPTO_H #define GM_CRYPTO_H #include stddef.h #include stdint.h #define GM_OK 0 #define GM_ERR_PARAM -1 #define GM_ERR_MEM -2 #define GM_ERR_CRYPTO -3 /* SM2密钥对生成pub_key输出65字节(04||X||Y)pri_key输出32字节 */ int gm_sm2_keypair(uint8_t *pub_key, uint8_t *pri_key); /* SM2签名sig输出64字节(r||s)sig_len传入缓冲区容量输出实际长度 */ int gm_sm2_sign(const uint8_t *data, size_t len, const uint8_t *pri_key, uint8_t *sig, size_t *sig_len); /* SM2验签签名格式为r||s返回GM_OK表示验签通过 */ int gm_sm2_verify(const uint8_t *data, size_t len, const uint8_t *pub_key, const uint8_t *sig, size_t sig_len); /* SM2加密密文格式为C1||C3||C2pub_key为65字节 */ int gm_sm2_encrypt(const uint8_t *data, size_t len, const uint8_t *pub_key, uint8_t *out, size_t *out_len); int gm_sm2_decrypt(const uint8_t *data, size_t len, const uint8_t *pri_key, uint8_t *out, size_t *out_len); /* SM3摘要输出固定32字节 */ int gm_sm3(const uint8_t *data, size_t len, uint8_t digest[32]); /* SM4 ECB/CBC自动做PKCS7填充 */ int gm_sm4_ecb_encrypt(const uint8_t *key, const uint8_t *in, size_t len, uint8_t *out, size_t *out_len); int gm_sm4_ecb_decrypt(const uint8_t *key, const uint8_t *in, size_t len, uint8_t *out, size_t *out_len); int gm_sm4_cbc_encrypt(const uint8_t *key, const uint8_t *iv, const uint8_t *in, size_t len, uint8_t *out, size_t *out_len); int gm_sm4_cbc_decrypt(const uint8_t *key, const uint8_t *iv, const uint8_t *in, size_t len, uint8_t *out, size_t *out_len); void gm_free(void *ptr); #endif几个设计细节值得说明。SM2签名在OpenSSL内部的输出是DER编码的我在C层直接用ECDSA_do_sign拿到的r和s值用BN_bn2binpad压成32字节再拼接成64字节的r||s这样Delphi侧不需要引入任何ASN.1解析代码。SM2加密同理OpenSSL返回的是ASN.1序列我在C层拆成C1||C3||C2的紧凑二进制Delphi和Java端对接都很直接。公钥格式这里也踩了不少坑我统一用65字节的未压缩点格式。开头那个04前缀不能丢很多跨语言验签失败就是因为公钥被截成64字节或者去掉了前缀两边格式对不上。2.3 动态库编译时的工程细节Windows下的DLL我推荐用MinGW-w64编译因为静态链接OpenSSL比MSVC省事生成的dll无外部依赖。MSVC也可以但要处理libcrypto.lib的引入库和运行时依赖麻烦一些。Linux下编译要注意符号导出问题。直接用-shared会把所有非static符号都导出去包含OpenSSL的符号容易和别人程序里的OpenSSL版本冲突。我用了-fvisibilityhidden然后在API函数上加__attribute__((visibility(default)))只对外暴露gm_开头的函数。另外我在Linux版本里选择了静态链接libcrypto.a这一点在后面的部署章节会详细说它直接影响动态库在陌生机器上能不能跑起来。3. Pascal封装层的桥接方案类型映射和内存管理3.1 external声明、cdecl和平台库名切换Pascal封装的第一步是把C函数声明到Delphi里。这里必须统一使用cdecl调用约定千万不能用stdcall。很多Delphi老项目习惯了Windows下API的stdcall拿到C库就照着写结果32位下偶尔能跑64位下一调就崩。库名的处理我放在一个常量里做条件编译unit GmCrypto; interface uses System.SysUtils; const {$IFDEF MSWINDOWS} GM_DLL gmcrypto.dll; {$ELSE} GM_DLL libgmcrypto.so; {$ENDIF} function gm_sm2_keypair(pub_key, pri_key: PByte): Integer; cdecl; external GM_DLL; function gm_sm2_sign(const data: PByte; len: NativeUInt; const pri_key: PByte; sig: PByte; var sig_len: NativeUInt): Integer; cdecl; external GM_DLL; ...C语言里的size_t在Delphi里对应NativeUInt这点很重要。在Win32下size_t是4字节Win64和Linux x64下是8字节如果用Integer替代64位下栈上参数布局会错位轻则返回错误重则直接访问违例。3.2 类型映射表和TBytes传参细节整个封装的类型映射可以总结成一张表C语言类型Delphi类型说明uint8_t *PByte指向字节数组的指针size_tNativeUInt长度随平台自动变化int错误码Integer返回值不用BOOLconst uint8_t *const PByte输入数据指针输出缓冲区PBytevar NativeUInt调用方传入缓冲区容量函数返回实际长度Delphi的TBytes是动态数组直接PByte(Data)就能拿到指向第一个元素的指针这点和C#的byte[]完全不一样理解对了就能省很多事。需要注意如果Data是空数组PByte(Data)返回nilC层的接口设计上必须能容忍dataNULL且len0的情况我在C代码里每个函数入口都做了这个判断。还有一个坑不要用PChar来传二进制数据。PChar在Delphi里跟string混用会自动做编码转换碰到包含0x00的密文数据截断不说转换也会出乱子。二进制数据一律TBytes只有文本才在业务层转成UTF-8字节。3.3 把C接口封装成类错误码统一转异常动态库返回的错误码如果每个调用点都去判断代码会非常啰嗦。我封装了一个TGmCrypto类内部用GmCheck过程统一处理type EGmCryptoError class(Exception); procedure GmCheck(ErrCode: Integer); begin if ErrCode GM_OK then raise EGmCryptoError.CreateFmt(GM crypto error, code p a hrefhttps://download.csdn.net/download/m0n1o2p/92781709 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表