OpenEuler下go语言使用国密算法的两种方法

在项目当中可能需要使用到GMSSL国密算法库,笔者先在Ubuntu下进行了尝试,但一直搞不定环境,发现OpenEuler下自带国密算法库,进行了初步的调用尝试并取得成功,现分享一下经验。
首先,我们可以在官网文档( https://docs.openeuler.org/zh/docs/24.03_LTS_SP1/docs/ShangMi/%E7%AE%97%E6%B3%95%E5%BA%93.html)当中看到对OpenEuelr封装国密算法的一些简要介绍


可惜的是并未对API使用举例,本人Linux基础薄弱,官方文档也有些看不懂,所以就拿着材料去找了DEEPSEEK,最终也是完成了SM4算法的使用,详情如下。
首先呢,如官方文档所言,我们需要开启SM4加密

在我的代码中,go使用GMSSL,主要是通过CGO来实现的。

package sm4

/*
#cgo LDFLAGS: -lssl -lcrypto
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <string.h>

// 加密函数
void sm4_encrypt(const unsigned char *plaintext, int plaintext_len,
                 const unsigned char *key, const unsigned char *iv,
                 unsigned char *ciphertext, int *ciphertext_len) {
    EVP_CIPHER_CTX *ctx;
    int len;

    // 创建并初始化上下文
    if(!(ctx = EVP_CIPHER_CTX_new())) {
        return;
    }

    // 初始化加密操作,启用填充
    if(1 != EVP_EncryptInit_ex(ctx, EVP_sm4_cbc(), NULL, key, iv)) {
        EVP_CIPHER_CTX_free(ctx);
        return;
    }

    // 提供要加密的数据并进行加密
    if(1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len)) {
        EVP_CIPHER_CTX_free(ctx);
        return;
    }
    *ciphertext_len = len;

    // 完成加密操作
    if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) {
        EVP_CIPHER_CTX_free(ctx);
        return;
    }
    *ciphertext_len += len;

    // 清理上下文
    EVP_CIPHER_CTX_free(ctx);
}

// 解密函数
void sm4_decrypt(const unsigned char *ciphertext, int ciphertext_len,
                 const unsigned char *key, const unsigned char *iv,
                 unsigned char *plaintext, int *plaintext_len) {
    EVP_CIPHER_CTX *ctx;
    int len;

    // 创建并初始化上下文
    if(!(ctx = EVP_CIPHER_CTX_new())) {
        return;
    }

    // 初始化解密操作,启用填充
    if(1 != EVP_DecryptInit_ex(ctx, EVP_sm4_cbc(), NULL, key, iv)) {
        EVP_CIPHER_CTX_free(ctx);
        return;
    }

    // 提供要解密的数据并进行解密
    if(1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len)) {
        EVP_CIPHER_CTX_free(ctx);
        return;
    }
    *plaintext_len = len;

    // 完成解密操作
    if(1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len)) {
        EVP_CIPHER_CTX_free(ctx);
        return;
    }
    *plaintext_len += len;

    // 清理上下文
    EVP_CIPHER_CTX_free(ctx);
}
*/
import "C"
import (
	"crypto/rand"
	"errors"
	"unsafe"
)

const SM4KeySize = 16
const SM4IVSize = 16

// SM4Encrypt 使用 SM4 算法进行加密
func SM4Encrypt(plaintext []byte, key []byte) ([]byte, []byte, error) {
	if len(key) != SM4KeySize {
		return nil, nil, errors.New("SM4 key must be 16 bytes")
	}
	iv := make([]byte, 16)
	_, err := rand.Read(iv)
	if err != nil {
		return nil, nil, err
	}
	const IVSize = 16
	ciphertext := make([]byte, len(plaintext)+IVSize)
	var ciphertextLen C.int
	C.sm4_encrypt(
		(*C.uchar)(unsafe.Pointer(&plaintext[0])),
		C.int(len(plaintext)),
		(*C.uchar)(unsafe.Pointer(&key[0])),
		(*C.uchar)(unsafe.Pointer(&iv[0])),
		(*C.uchar)(unsafe.Pointer(&ciphertext[0])),
		&ciphertextLen,
	)
	return ciphertext[:ciphertextLen], iv, nil
}

// SM4Decrypt 使用 SM4 算法进行解密
func SM4Decrypt(ciphertext []byte, key []byte, iv []byte) ([]byte, error) {

	if len(key) != SM4KeySize {
		return nil, errors.New("SM4 key must be 16 bytes")
	}
	if len(iv) != SM4IVSize {
		return nil, errors.New("SM4 IV must be 16 bytes")
	}
	plaintext := make([]byte, len(ciphertext))
	var plaintextLen C.int
	C.sm4_decrypt(
		(*C.uchar)(unsafe.Pointer(&ciphertext[0])),
		C.int(len(ciphertext)),
		(*C.uchar)(unsafe.Pointer(&key[0])),
		(*C.uchar)(unsafe.Pointer(&iv[0])),
		(*C.uchar)(unsafe.Pointer(&plaintext[0])),
		&plaintextLen,
	)
	return plaintext[:plaintextLen], nil
}

代码结构分析

C语言部分

  1. 头文件引入

    • 引入了OpenSSL的EVP(Envelope)和随机数生成相关头文件
    • EVP提供了统一的加密接口
  2. 加密函数sm4_encrypt

    • 使用EVP接口初始化SM4-CBC加密上下文
    • 处理加密数据并返回结果
    • 自动处理PKCS#7填充
  3. 解密函数sm4_decrypt

    • 使用EVP接口初始化SM4-CBC解密上下文
    • 处理解密数据并返回结果
    • 自动去除PKCS#7填充

Go语言封装部分

  1. 常量定义

    • SM4KeySize = 16:SM4密钥长度16字节(128位)
    • SM4IVSize = 16:初始化向量长度16字节
  2. SM4Encrypt函数

    • 参数:明文和密钥
    • 功能:
      • 检查密钥长度
      • 生成随机IV
      • 调用C函数进行加密
    • 返回:密文和IV
  3. SM4Decrypt函数

    • 参数:密文、密钥和IV
    • 功能:
      • 检查密钥和IV长度
      • 调用C函数进行解密
    • 返回:明文

关键实现细节

  1. CGO调用

    • 使用// #cgo指令指定链接的OpenSSL库
    • 通过unsafe.Pointer在Go和C之间传递数据指针
  2. 加密模式

    • 使用CBC模式(密码分组链接模式)
    • 需要初始化向量(IV)来增强安全性
  3. 错误处理

    • 检查密钥和IV长度
    • 处理随机数生成错误
  4. 内存管理

    • OpenSSL的EVP接口会自动管理加密上下文内存
    • Go的切片操作确保返回正确长度的数据

使用示例

key := []byte("1234567890abcdef") // 16字节密钥
plaintext := []byte("Hello, SM4!")

// 加密
ciphertext, iv, err := SM4Encrypt(plaintext, key)
if err != nil {
    panic(err)
}

// 解密
decrypted, err := SM4Decrypt(ciphertext, key, iv)
if err != nil {
    panic(err)
}

fmt.Println(string(decrypted)) // 输出: Hello, SM4!

安全性考虑

  1. 每次加密使用随机IV,避免相同明文产生相同密文
  2. 自动处理填充,确保数据块对齐
  3. 使用经过验证的OpenSSL实现,避免密码学实现错误

这种封装方式结合了Go的易用性和OpenSSL的成熟密码学实现,适合需要SM4算法的Go应用程序。
可以写一个测试程序,看看是否能成功运行

package sm4

import (
	"bytes"
	"crypto/rand"
	"testing"
)

// 测试辅助函数:生成随机字节
func randomBytes(n int) ([]byte, error) {
	b := make([]byte, n)
	_, err := rand.Read(b)
	if err != nil {
		return nil, err
	}
	return b, nil
}

// 测试用例1:常规短文本加密解密
func TestShortText(t *testing.T) {
	plaintext := []byte("SM4测试")
	const KeySize = 16
	key, err := randomBytes(KeySize)
	if err != nil {
		t.Fatalf("生成随机密钥失败: %v", err)
	}

	ciphertext, iv, err := SM4Encrypt(plaintext, key)
	if err != nil {
		t.Fatalf("加密失败: %v", err)
	}

	decrypted, err := SM4Decrypt(ciphertext, key, iv)
	if err != nil {
		t.Fatalf("解密失败: %v", err)
	}

	if !bytes.Equal(plaintext, decrypted) {
		t.Fatal("解密结果与原文不符")
	}
}

// 测试用例2:长文本加密解密
func TestLongText(t *testing.T) {
	plaintext := bytes.Repeat([]byte("Go语言SM4测试-"), 100) // 约1.5KB数据
	const KeySize = 16
	key, err := randomBytes(KeySize)
	if err != nil {
		t.Fatalf("生成随机密钥失败: %v", err)
	}

	ciphertext, iv, err := SM4Encrypt(plaintext, key)
	if err != nil {
		t.Fatal(err)
	}

	decrypted, err := SM4Decrypt(ciphertext, key, iv)
	if err != nil {
		t.Fatal(err)
	}

	if !bytes.Equal(plaintext, decrypted) {
		t.Fatal("长文本解密结果不一致")
	}
}

// 测试用例3:二进制数据加密解密
func TestBinaryData(t *testing.T) {
	plaintext, err := randomBytes(512) // 512字节随机数据
	if err != nil {
		t.Fatalf("生成随机数据失败: %v", err)
	}

	const KeySize = 16
	key, err := randomBytes(KeySize)
	if err != nil {
		t.Fatalf("生成随机密钥失败: %v", err)
	}

	ciphertext, iv, err := SM4Encrypt(plaintext, key)
	if err != nil {
		t.Fatal(err)
	}

	decrypted, err := SM4Decrypt(ciphertext, key, iv)
	if err != nil {
		t.Fatal(err)
	}

	if !bytes.Equal(plaintext, decrypted) {
		t.Fatal("二进制数据解密失败")
	}
}

// 测试用例4:多次加密结果不同(因IV随机)
func TestRandomIV(t *testing.T) {
	plaintext := []byte("相同输入不同IV")
	const KeySize = 16
	key, err := randomBytes(KeySize)
	if err != nil {
		t.Fatalf("生成随机密钥失败: %v", err)
	}

	// 第一次加密
	ciphertext1, iv1, err := SM4Encrypt(plaintext, key)
	if err != nil {
		t.Fatal(err)
	}

	// 第二次加密
	ciphertext2, iv2, err := SM4Encrypt(plaintext, key)
	if err != nil {
		t.Fatal(err)
	}

	// IV必须不同
	if bytes.Equal(iv1, iv2) {
		t.Fatal("IV未随机化")
	}

	// 密文应该不同
	if bytes.Equal(ciphertext1, ciphertext2) {
		t.Fatal("相同输入生成相同密文,存在安全风险")
	}
}


可以看到测试也是被顺利通过了
第二种方法是调用国密算法的另一个go语言封装,为什么不用官网的封装呢?反正我是在调用的时候碰到了一点问题哈,我安装GMSSL库的时候就出现了一些有关权限的问题,官网的go封装又依赖GMSSL库,所以一直没搞定,最后又发现了一个https://github.com/tjfoc/gmsm,这里的加密算法可以直接在go环境当中使用,非常简单友好

package main

import (
	"crypto/rand"
	"fmt"
	"log"
	"github.com/tjfoc/gmsm/sm4"
)

func main() {
	// 原始数据
	plaintext := []byte("这是一段需要加密的敏感数据,SM4是中国商用密码算法")

	// 生成16字节的随机密钥(SM4密钥长度为128位)
	key := make([]byte, 16)
	if _, err := rand.Read(key); err != nil {
		log.Fatalf("生成随机密钥失败: %v", err)
	}

	// 加密
	ciphertext, err := sm4.Sm4Ecb(key, plaintext, true)
	if err != nil {
		log.Fatalf("加密失败: %v", err)
	}

	fmt.Printf("原始数据: %s\n", plaintext)
	fmt.Printf("加密密钥: %x\n", key)
	fmt.Printf("加密结果: %x\n", ciphertext)

	// 解密
	decrypted, err := sm4.Sm4Ecb(key, ciphertext, false)
	if err != nil {
		log.Fatalf("解密失败: %v", err)
	}

	fmt.Printf("解密结果: %s\n", decrypted)

	// 验证解密结果是否与原始数据一致
	if string(decrypted) != string(plaintext) {
		log.Fatal("解密结果与原始数据不匹配")
	} else {
		fmt.Println("验证通过: 解密结果与原始数据一致")
	}
}


可以看到也是可以正常使用的,代码比系统调用要简洁一些

2 Likes

感谢分享!

1 Like

学到了!