技术分享|CGo指南:基于安全硬件的KMS系统实践

admin 2024年2月28日13:20:08评论8 views字数 6499阅读21分39秒阅读模式

摘要:KMS 系统的底层安全能力由软件实现升级为由安全硬件实现,在提供了更可靠更全面的能力的同时也带来了兼容性的问题。因此,我们引入 CGo 对安全硬件所提供的 C 标准接口进行适配改造,解决了上述问题。本文将对上述适配改造过程中的一些关键点和相关问题进行了总结沉淀,为其他有相关需求的开发人员提供解决思路、抛砖引玉。


背景介绍

为了向业务服务提供更可靠,更全面的数据安全合规能力,KMS 系统对进行密钥管理、密码运算的底层模块进行了升级,将原有基于软件实现的安全能力改为基于经过国密安全认证的安全硬件实现,使得密码管理、运算更安全可靠、高效,提升了KMS系统的可靠性和服务质量。

技术分享|CGo指南:基于安全硬件的KMS系统实践

(KMS系统升级示意图)

在改造升级过程中,我们遇到的一个关键挑战问题是:安全硬件厂依据国家商用密码局提出的标准,只提供了基于 C 的标准接口,这和贝壳安全基于 Go 开发的 KMS 系统并不兼容。为此,我们基于 CGo 对安全硬件所提供的 C 标准接口进行了适配改造,解决了上述的兼容性问题。本文将对上述适配改造过程中,一些关键点和相关问题进行总结沉淀,为其他有相关需求的开发人员提供解决思路、抛砖引玉。

CGO简要介绍

CGo 是 Go 于 2011 年推出的一项高级特性,如同 JAVA 的 JNI 一样,是用于连接其它语言的桥梁,在 CGo 的帮助下,可以直接在 Go 程序中直接使用 C 的数据结构和 C 库函数

如下所示,通过引入 CGo,可以轻松地使用 C.random() 以及 C.srandom() 等标准库函数,在 Go 程序中充分使用 C/C++ 的资源和特性。


技术分享|CGo指南:基于安全硬件的KMS系统实践


一般来说,在 Go 项目中使用 CGo 通常是以下三个方面的原因:

  • 底层硬件环境依赖,这也是 KMS 项目中引入 CGo 的主要原因。由于 Linux 系统的广泛应用以及 C/C++ 性能卓越的原因,目前绝大部分的底层硬件环境所使用的编程语言都是C/C++。因此,当我们上层基于 Go 的服务或应用需要依赖底层硬件时,往往需要对底层硬件所提供 C/C++ 的接口基于 Go 进行封装才能使用。

  •  C/C++ 具有丰富的工具和库函数资源。由于其历史悠久,长期作为学术界和工程界的主流编程语言,因此,自然而然地存在许多基于 C/C++ 进行编写的软件和库函数,掌握了 CGo,对 Go 来说就如同站在了巨人的肩膀上,充分避免了重复劳动,极大地提升开发效率。

  • C/C++ 具有高效的性能。由于设计的天然优势,C/C++ 拥有极好的性能表现。图 1展示了在不同类型的任务上,C/C++ 的性能优势。因此,对运算速度有着高要求的任务往往需要在 C/C++ 层进行,例如许多的密码学运算和深度学习运算等等。通过 CGo,我们可以将计算任务外包至 C/C++ 层进行运算,从而提高 Go 程序的整体性能。


技术分享|CGo指南:基于安全硬件的KMS系统实践

Go 和 C ++ 在不同类型任务上的性能比较)


在介绍完 CGo 是什么,以及为何要使用 CGo 后,在下一部分我们将介绍如何使用 CGo 。

KMS2.0 中如何使用 CGo

想要在 Go 代码中引入 C/C++,从逻辑上来说需要做到以下两件事情:

  • CGo 配置和引入。首先,需要标识引入的 C 数据类型和 C 函数,并且声明这些代码所处的位置,以便于 Go 进行联合编译,并且在运行时调用相应的 C 代码。

  • C 和 Go 数据类型和数据结构的相互转换。Go 只能识别和管理 Go 自身的数据类型和数据结构,同样 C 也如此。因此,当通过 Go 调用 C 函数时必须对函数的入参进行转换,否则无法编译通过。同样的,Go 也必须先对 C 函数的出参进行类型转换才能进行后续的计算和使用。


在接下来的部分,我们将会详细介绍 KMS 项目中是如何实现这两点的。除此之外,由于 Go 的内存管理机制无法管理 C 的变量,所以开发过程中不可避免地会出现一些疑难杂症,如内存越界和泄漏等等问题。因此我们也会将这些实际问题和解决方法总结成经验以避免大家重复踩坑。

1、KMS 系统中的 CGo 的配置和引入

CGo 本质上只是 Go 引入用于预编译的小工具。而预编译则将会为 Go 代码中所使用到的 C.FunctionName 形式的 C 函数,生成 _Cfunc_FunctionName 形式的 Go 函数(如图 2 所示,定义在 main_cgo.go 中),该函数的主要作用是:

  • 封装 Go 需要传入 C 中的参数

  • 通过系统调用和传入的 C 函数指针,跳转到 C 运行时系统并执行相应函数

同样的,CGo 也会生成一个类似功能的 C 中间函数 _Cfunc_FuncationName(如图 2 所示,定义在 main_cgo.c 中),用于双方进行桥接。

因此在 Go 代码中通过 CGo 调用 C 代码的实际工作流程如下所示:

技术分享|CGo指南:基于安全硬件的KMS系统实践

(CGo 的作用图示)


所以,为了能在 KMS 项目中使用 CGo,首先需要做到以下几步:

1.  安装和配置 C/C++ 环境,这一步是由于 CGO 对 C/C++ 的编译依赖

2.  在 Go 中开启 CGo 的特性,即设置 Go 环境变量 CGO_ENABLE=1,这一步使得 Go 开启对 CGo 特性的支持

3.  在使用了 C 函数的 Go 文件中通过序文引入所使用的 C 函数、动态库以及相应的编译和链接选项,如下代码提供了一个一个典型的例子:

a.  #cgo CFLAGS: 标识了编译选项,-I 指定了头文件的检索目录,指明了头文件在目录 ../inclue 下

b.  #cgo LDFLAGS: 标识了链接选项,-L 指定了链接时库文件的检索目录为 ../lib 和 -l 则指定了链接时所需要链接的库名称

c.  两行 #include 则是引入相应的 C 头文件,以便编译器可以正确识别 C 数据类型和 C 函数

d.  import "C" 则是用于标识其上方的注释部分即为所引入的 C 相关内容,也称为序文

技术分享|CGo指南:基于安全硬件的KMS系统实践


2、KMS 系统中对数据结构的适配

在完成了上述的配置之后,则需要考虑如何在 Go 代码中调用 C 函数。

一个函数,既需要入参,同时也需要出参,对于 C 函数来说,传入的参数和数据结构必须是 C 类型或者 C 中定义的结构。所以在 Go 程序中,我们要将 Go 的数据类型转成 C 的数据结构才能正确调用 C 函数。同时,又需要将 C 返回的结果参数转换成 Go 的数据结构,这样才能在 Go 中正确使用和存储该结果。

由于 KMS 系统涉及到了密钥管理和密码运算等功能,因此自然而然地需要有密钥、密文和签名等密码学数据结构。但在不同的编程语言中,数据类型和结构体的实现等也存在着诸多不同的地方,在本项目中也不例外。因此,对于安全硬件所提供的 C 类型的数据结构,我们需要对其进行适配,才能在 Go 层实现对这些数据的存储和计算。

下面我们将以椭圆曲线的密文结构体 ECCCipher 为例,介绍在 CGo 中如何实现数据结构的转换。其具体的定义如下:

技术分享|CGo指南:基于安全硬件的KMS系统实践

我们可以看到该结构是由多种数据类型构成的,既有 usigned int 等基础数据类型,又有 unsign char[] 等复杂数据类型。因此我们循序渐进,从基础数据类型的转换开始讲起,再到复杂数据类型的转换,最后便自然而然地能够在前两者的基础上实现结构体类型的转换。


2.1、基础类型的转换

对于基础的数据类型来说,这样的转换非常简单,因为在不同的编程语言中,基础数据类型通常具有相同的内存结构,只是名称上有所差异。

因此对于下表 1 中具有相同内存结构的数据类型(同一行的类型),通过强制转换即可。

技术分享|CGo指南:基于安全硬件的KMS系统实践

(Go 和 C 中的数据类型对比)

如下代码是实现 Go 的 uint32 类型和 C 的 unsigned int 类型的相互转换例子。

技术分享|CGo指南:基于安全硬件的KMS系统实践

需要注意的是,对于 C 语言的 int 和 long 类型来说,在 32 位操作系统和 64 位操作系统对应的字节数是不同的。但在 CGO 中,C 语言的 int 和 long 类型都是对应 4 个字节的内存大小。

对于数组来说,也非常简单,只需要对数组中的每一个元素进行强制类型转换并赋值即可。而对于 C 数组转换成 Go 数组也是同理即可。因此在这里不再赘述。

同时对于数组来说,由于 Go 和 C 的内存结构是一致的,都是内存中一块连续的内容,因此也可以直接通过数组首元素的地址实现类型转换,但这需要通过较为复杂的处理,例如 unsafe 包的转换等等,我们将在下一章节中再详细介绍:

技术分享|CGo指南:基于安全硬件的KMS系统实践

除此之外,由于 C 中的字符串数组在结尾处会额外添加一个结束字符 '/0' ,而 Go 中的字符串没有这样的操作,因此进行字符串转换时需要进行额外的处理操作或使用专门 CGo 函数,我们将在下一章详细介绍。


2.2、复杂类型的转换

除了上述的基础数据类型外,更常见的数据类型还有指针,切片,字符串等较为复杂的数据结构。这些复杂类型的转换往往比较困难,有时还需要根据具体的数据结构定义来进行调整。而出现该问题的原因就是在 Go 和 C 中,这些复杂类型对应的内存结构不一致,因此需要我们在转换的过程中进行手动对齐才能实现正确的类型转换。


2.2.1、指针类型的转换

首先需要考虑的是指针类型数据的转换,而指针类型的转换也是其它复杂数据类型,如切片,结构体等转换的基础。因为,在 C 和 Go 中进行函数调用时,往往都是通过传递指针的方法来传递结构体或者长数组等函数参数的。并且,由于 Go 的管理机制非常完善,而对于这些复杂类型的强制转换属于不安全操作,不能直接进行,需要通过 unsafe 包中的 unsafe.Pointer 或者 unsafe.Slice等等中间类型进行中转以实现类型转换。

unsafe.Pointer 指针类型类似于 C 语言中的 void* 类型的指针。任何类型的指针都可以通过强制转换为 unsafe.Pointer 指针类型去掉原有的类型信息,然后再重新赋予新的指针类型而达到指针间的转换的目的。

但需要注意的是,转换前后的指针所指向的内容大小必须保持一致,否则会出现读取越界或者覆盖等问题导致程序崩溃。

技术分享|CGo指南:基于安全硬件的KMS系统实践

(Go 中借助 unsafe package 实现对指针类型的转换)

如下所示是借助 unsafe package 实现对不同类型的指针进行转换的例子:

技术分享|CGo指南:基于安全硬件的KMS系统实践


2.2.1、切片数据的转换

除了指针类型外,切片数据也是 Go 中常见的数据类型。

切片是 Go 中的动态数组,其内存结构如图 4 所示,相比于 C 中的数组,其增加了指向数据的指针,有效长度以及最大容量这三项数据,因此我们在进行转换的时候需要注意,只需要将数据项进行转换即可。

技术分享|CGo指南:基于安全硬件的KMS系统实践

(Go 切片数据的内存结构)

如下所示,是将 Go 切片转换成 *C.uchar 的方法,但是这样仅能传递首元素地址,会丢失切片的有效长度信息,因此若传递指针, C 函数往往还会要求携带有效长度参数。

技术分享|CGo指南:基于安全硬件的KMS系统实践

同样的,由于切片属于数组的扩展,而且转换过程实际也只是对数组进行操作,所以也可以使用和数组一样的转换方法:

技术分享|CGo指南:基于安全硬件的KMS系统实践

对于 C 数组或者数组指针转换为 Go 切片,则可以使用 CGo 的辅助函数来进行转换,以避免循环复制等繁琐的操作,如下:

技术分享|CGo指南:基于安全硬件的KMS系统实践

还有如下的辅助函数:

技术分享|CGo指南:基于安全硬件的KMS系统实践


2.2.3、字符串的转换

在上文中,我们提到了 Go 和 C 的字符串的差异,所以对于 Go 和 C 的字符串转换,我们不要进行逐一拷贝,而是使用 CGo 的辅助函数进行转换,CGo 的辅助函数会自行处理该问题。注意 Go 字符串中有字符 '' 时需要自行进行特殊处理,否则字符串会在 ''处结束。

需要注意的一点是,使用 C.String() 进行转化时,会在 C 的运行时系统中发生内存拷贝,而 Go 的 GC 无法管理 C 运行时系统的内存,因此对于 cString 需要我们自行调用 C 的标准库  中的 C.free 进行释放,否则会发生内存泄露。

技术分享|CGo指南:基于安全硬件的KMS系统实践


2.3、结构体的转换

结构体的转换是 CGo 中的关键难题,因为结构体中可能包含着各种数据结构,甚至还有着嵌套结构体以及占位成员等等。但万变不留其宗,结构体的转换也遵循着和上述转换一样的规则,就是内存结构对齐。

而要做到内存结构对齐,则要结构体内的每个成员变量都要正确进行转换。对于一般的结构体,其成员变量基本都为基本数据类型,或者基础数据类型的数组,只需要依次遵循上述的转换方法对变量进行拷贝和转换即可。

但对于复杂的结构体,需要依据具体的内存结构来分析。我们通过如下的一个存在变长成员变量的结构体例子来进行说明,这也是上文中所提到的椭圆曲线密文结构体 ECCipher:

技术分享|CGo指南:基于安全硬件的KMS系统实践

在该结构体中,最后一个元素 C[1] 是一个占位成员,仅代表首个元素,其实际长度并不是如其声明的一样为 1,而是 L,即该结构体存在着变长的成员变量,因此在进行类型转换的时候,需要依据长度 L,对 C 进行相应的转换。

既然要实现 Go 数据类型和 C 数据类型的相互转换,那么首先需要定义一个 Go 的结构体,以方便在 Go 层进行相应的计算和存储操作。

技术分享|CGo指南:基于安全硬件的KMS系统实践

然后可通过如下的函数段进行转换。为了实现结构体内存结构的对齐,首先需要申请相同大小的内存,由于上述结构体存在变长成员变量,因此需要根据实际的结构体大小申请,之后再依据结构体变量的排列方式,依次对每个变量进行拷贝和转换。

技术分享|CGo指南:基于安全硬件的KMS系统实践

这里需要注意的是,一定要使用 unsafe.Slice 进行拷贝,通过 unsafe.Pointer 拷贝会产生类型不匹配,无法转换的问题,因为 C 结构体中的 C 数组仅仅声明了长度为 1。

KMS2.0 开发中遇到的 CGo 疑难杂症

在进行 CGo 开发时我们也遇到了一些疑难杂症,于是将其记录了下来并且提出了相应的解决方法如下:

1、不要使用 Go test 进行测试

对封装好的 Go 调用 C 的代码进行测试时不要使用 Go test 进行测试,而是应该将测试代码写在 main package 中,通过 go run main.go 进行调用。否则可能会遇到,一些指针和内存无故被释放导致程序奔溃的的问题。

在 stackoverflow (go - import "C" is unsupported in test - looking for alternatives - Stack Overflow)和 Gowiki 中明确指出了该点(Go Wiki: cgo - The Go Programming Language),但具体的原因未得而知。根据 Github 上的 issue(proposal: cmd/go: support cgo in test files · Issue #18647 · golang/go · GitHub)推测可能是因为 go test 没有解析 CGo 的相关配置信息,而没有解析的原因是,这会导致 go test 命令非常复杂,因此权衡之下选择不支持。

2、CGo 中的内存管理

由于涉及到 C 语言,因此我们必须手动对内存进行管理,这里需要牢记一个原则:由 C 运行时系统进行分配内存的变量和指针都必须再调用 C.free 手动释放。

一般会有以下的几个场景,我们将对每种场景中是否需要进行手动释放进行详细分析:

  • 使用了 C.CBytes 或者 C.CString 等辅助函数需要手动释放。因为这两个辅助函数需要对传入的 Go 变量进行转换,返回 C 变量,因此在其函数内部使用到了 C.malloc 为该变量分配内存并返回,所以在该变量使用完毕后释放。

技术分享|CGo指南:基于安全硬件的KMS系统实践
  • 在 Go 中调用了 C.malloc  函数得到的变量需要手动释放,这种情况一般出现在如下初始化结构体时。

技术分享|CGo指南:基于安全硬件的KMS系统实践
  • 通过 Go 关键字 var 声明并申请得到的变量无需手动释放。因为虽然申请的是 C 类型的变量,但是其是通过 Go 来进行内存分配和管理的。

技术分享|CGo指南:基于安全硬件的KMS系统实践


不同 Go 包下相同的 C 类型不匹配

在很多情况下,我们可能会在同一个项目中的多个 Go package 下都使用了某一个 C 的数据结构,如结构体 C.structName,虽然它们都引入的是同一个 C 文件中定义的数据结构,但对于 CGo 来说,它们的类型并不是匹配的。如下所示,在 common package 中定义了 C 和 Go 的 Student 结构体以及相应的转换函数:

技术分享|CGo指南:基于安全硬件的KMS系统实践

我们在另外一个 package 中引入相同的 C 结构体,并且利用该转换函数对其进行转换,却发现编译无法通过:

技术分享|CGo指南:基于安全硬件的KMS系统实践

因此,对于该问题,我们可以使用 unsafe.Pointer 进行解决,即对于需要跨包调用的函数,涉及到的 C.structName 类型一律用 unsafe.Pointer 替换,以实现参数类型的统一,在函数内部或者外部执行具体操作时再将其强转回原结构体指针类型:

技术分享|CGo指南:基于安全硬件的KMS系统实践

调用时:

技术分享|CGo指南:基于安全硬件的KMS系统实践






参考内容:

Go Wiki: cgo - The Go Programming Language

cgo command - cmd/cgo - Go Packages

原文始发于微信公众号(贝壳安全应急响应中心):技术分享|CGo指南:基于安全硬件的KMS系统实践

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月28日13:20:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   技术分享|CGo指南:基于安全硬件的KMS系统实践https://cn-sec.com/archives/2531137.html

发表评论

匿名网友 填写信息