GmSSLを用いたSM2署名アルゴリズムの実装と解決策

背景

先週、自動車メーカーから問題が報告されました:販売済みの一部車両からプラットフォームに報告される排ガスデータの署名検証が失敗しているとのことです。

内部確認の結果、これは当社のプロセス設計上の問題でした。デバイスの署名プロセスを簡単に説明します(下図参照):

プロセス:

  1. モジュールがI2Cシリアルポートを介して署名データをSE暗号チップに送信します。
  2. SE暗号チップは、自身のuserID_Aと事前に設定された秘密鍵を使用してSM2標準署名インターフェースで署名を行います。署名値を4Gモジュールに返します。
  3. 4Gモジュールは、署名データ、userID_B、事前に合意された公開鍵、署名情報をプラットフォームに報告します。

上記のプロセスから、SE署名に使用されるuserID_Aとモジュールが報告するuserID_Bが一致しないため、プラットフォームでの署名検証が失敗していることがわかりました。

解決策:

  1. 4Gモジュールが報告するuserID_BをuserID_Aに変更します。しかし、プラットフォームに報告するuserIDの形式は自動車メーカーの要件により、指定された形式でなければなりません。【不適】;
  2. 4GモジュールはuserIDをSE暗号チップに渡す必要があります。【適用】;

理論的にはここで問題を解決できるはずです。しかし、方案2はシリアルプロトコルの変更およびSEチップの内部プログラムの更新が必要です。一方、販売済みの車両はSEプログラムのリモートアップデートをサポートしておらず、4Gモジュールプログラムのリモートアップデートのみをサポートしています。

販売済み車両の問題を解決するため、一時的な方案を準備せざるを得ませんでした:4Gモジュールによる署名検証、SE暗号チップに依存しない。

GmSSL

調査の結果、国産暗号SM署名はオープンソースのOpenSSLライブラリに依存せず、GmSSLオープンソースライブラリに依存することがわかりました。公式サイトのアドレスは以下の通りです:

GmSSL

公式サイトの指示に従い、コンパイル・インストール後:

$ unzip GmSSL-master.zip
$ cd GmSSL-master
$ mkdir build
$ cd build
$ cmake ..
$ make
$ make test
$ sudo make install
$
// インストールが成功したか確認
$ gmssl version
GmSSL 3.1.0 Dev

コマンドラインを通じてSM2署名・検証プロセスを検証します:

$ gmssl sm2keygen -pass 1234 -out sm2.pem -pubout sm2pub.pem

$ echo hello | gmssl sm2sign -key sm2.pem -pass 1234 -out sm2.sig #-id 1234567812345678
$ echo hello | gmssl sm2verify -pubkey sm2pub.pem -sig sm2.sig -id 1234567812345678

$ echo hello | gmssl sm2encrypt -pubkey sm2pub.pem -out sm2.der
$ gmssl sm2decrypt -key sm2.pem -pass 1234 -in sm2.der

署名・検証の最終結果は成功しましたが、2つの問題が存在します:

  1. この検証方法はコマンドライン形式であり、非対称鍵は証明書形式です。私たちが期待する形式(API形式、かつ公開鍵は64バイト配列、秘密鍵は32バイト配列)と一致しません
  2. 検証は成功しましたが、この署名・検証プロセスが国の1239プラットフォームに適合しているかどうかは保証できません

最初の問題については、GmSSLソースコードライブラリを分析することで理解しました。

秘密鍵、公開鍵をバイト配列に変換する方法

コマンドラインgmssl sm2keygen -pass 1234 -out sm2.pem -pubout sm2pub.pemから、gmsslsm2keygenインターフェースを通じて秘密鍵と公開鍵証明書を生成することがわかります。そこで、sm2keygenインターフェースの分析から始めました。ソースコード分析を通じて、概ね以下のプロセスが明らかになりました:

/** SM2非対称鍵を生成 */
SM2_KEY key;
sm2_key_generate(&key);

/** SM2非対称鍵から秘密鍵証明書を生成 */
char* pass = NULL;
FILE* outfp = NULL;
...
sm2_private_key_info_encrypt_to_pem(&key, pass, outfp);

/** SM2非対称鍵から公開鍵証明書を生成 */
FILE* puboutfp = NULL;
sm2_public_key_info_to_pem(&key, puboutfp);

上記から:

  1. 非対称鍵オブジェクトはSM2_KEY keyであり、構造体形式です。定義は以下の通り:
typedef struct {
	SM2_Z256_POINT public_key;
	sm2_z256_t private_key;
} SM2_KEY;

  1. sm2_private_key_info_encrypt_to_pemインターフェース内部では、SM2_KEYを変換してPEM形式の証明書を生成しているはずです。同様に、公開鍵証明書も同様の処理を行うと考えられます。

そこで、sm2_private_key_info_encrypt_to_pemインターフェースの分析を続けました。最終的に以下のコードにたどり着き、このインターフェースが32バイト長の配列を取得することを基本的に確認しました。

uint8_t prikey[32];
sm2_z256_to_bytes(key->private_key, prikey);

同様に、64バイト長の公開鍵配列の変換は以下の通り:

uint8_t octets[65];
out[0] = SM2_point_uncompressed;
(void)sm2_z256_point_to_bytes(&key->public_key, octets + 1);

上記のインターフェースを通じて、秘密鍵・公開鍵のバイト配列を取得できます。

署名プロセス

同様に、SM2署名プロセスはsm2signインターフェースの分析から始められます。なぜなら、最終的に得たいのは署名情報(32バイトのr値配列、32バイトのs値配列)だからです。概ね以下のプロセスです:

/** SM2署名オブジェクトを初期化 */
SM2_SIGN_CTX sign_ctx;
SM2_KEY key;
char * id = NULL;
...
sm2_sign_init(&sign_ctx, &key, id, strlen(id));

/** 署名対象情報を挿入 */
char* buf = NULL;
int len = 0;
...
sm2_sign_update(&sign_ctx, buf, len);

/** 署名 */
uint8_t dgst[SM3_DIGEST_SIZE];
SM2_SIGNATURE signature; //署名情報

sm3_finish(&sign_ctx.sm3_ctx, dgst);

if (sign_ctx.num_pre_comp == 0) {
    if (sm2_fast_sign_pre_compute(sign_ctx.pre_comp) != 1) {
        printf("sm2_fast_sign failed");
        return -1;
    }
    sign_ctx.num_pre_comp = SM2_SIGN_PRE_COMP_COUNT;
}

sign_ctx.num_pre_comp--;
if (sm2_fast_sign(sign_ctx.fast_sign_private, &sign_ctx.pre_comp[sign_ctx.num_pre_comp],
    dgst, &signature) != 1) {
    printf("sm2_fast_sign failed");
    return -1;
}

上記の分析に基づき、テストプログラムを出力します:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <gmssl/mem.h>
#include <gmssl/sm2.h>

int main()
{
    
    SM2_KEY key_pair;
    uint8_t private_key[32];
    uint8_t public_key[65];

    SM2_SIGN_CTX sign_context;
    /**
     * 1. 鍵ペアを生成
     */
    if (sm2_key_generate(&key_pair) != 1)
    {
        printf("sm2_key_generate failed\n");
    }

    /** 秘密鍵を取得 */
    sm2_z256_to_bytes(key_pair.private_key, private_key);

    /** 公開鍵を取得 */
    sm2_z256_point_to_uncompressed_octets(&(key_pair.public_key), public_key);

    printf("秘密鍵:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",private_key[i]);
    }
    printf("\n");

    printf("公開鍵:");
    for(int i = 1 ; i < 65 ; i++)
    {
        printf("%02x ",public_key[i]);
    }
    printf("\n");

    const char* user_id = "1234567812345678";
    /** SM2を初期化 */
    if (sm2_sign_init(&sign_context, &key_pair, user_id, strlen(user_id)) != 1) 
    {
        printf("sm2_sign_init failed\n");
        return -1;
    }

    /** 署名データを挿入 */
    char data[] = {0x68,0x74,0x74,0x70,0x73,0x3A,0x2F,0x2F,0x63,0x6F,0x6E,0x73,0x74,0x2E,0x6E,0x65,0x74,0x2E,0x63,0x6E,0x2F};
    if (sm2_sign_update(&sign_context, data, sizeof(data)) != 1) 
    {
        printf("sm2_sign_update\n");
        return -1;
	}
   
    /** 署名 */
    uint8_t digest[SM3_DIGEST_SIZE];
	SM2_SIGNATURE signature_result;

	sm3_finish(&sign_context.sm3_ctx, digest);

	if (sign_context.num_pre_comp == 0) {
		if (sm2_fast_sign_pre_compute(sign_context.pre_comp) != 1) {
			printf("sm2_fast_sign failed");
			return -1;
		}
		sign_context.num_pre_comp = SM2_SIGN_PRE_COMP_COUNT;
	}

	sign_context.num_pre_comp--;
	if (sm2_fast_sign(sign_context.fast_sign_private, &sign_context.pre_comp[sign_context.num_pre_comp],
		digest, &signature_result) != 1) {
		printf("sm2_fast_sign failed");
		return -1;
	}

    printf("署名r:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",signature_result.r[i]);
    }
    printf("\n");

    printf("署名s:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",signature_result.s[i]);
    }
    printf("\n");

    return 0;
}

コンパイルして検証した結果は以下の通り:

xieyihua@xieyihua:~/GmSSL-master$ gcc test.c -o 1 -lgmssl
xieyihua@xieyihua:~/GmSSL-master$ ./1 
秘密鍵:83 ce bc 99 57 95 4a 62 1c 59 89 fa ca 05 c1 b8 47 b1 b4 4f 32 3f 8c 12 b8 12 c4 36 98 32 8e 03 
公開鍵:19 66 23 f7 5e 17 43 7d 19 8f 77 fe cb 7f f8 a9 61 f6 80 50 2f f7 cb 50 26 9d aa 62 56 4c e4 8b 61 91 c2 1d 82 05 17 2b bd 85 29 b5 ba 58 f5 fe 0b d1 ae a7 ce 0c 2c 13 e1 48 2b 96 a2 d2 08 24 
署名r:ac cd 90 46 c0 9f 1f b9 76 7c a9 4a 75 31 85 91 09 df 61 00 61 e3 86 d2 da 2d 4e 08 74 e6 5c 86 
署名s:1b 04 8f 7e da 1d 78 7d 63 6d 01 fe 47 1c f1 e5 42 dc 57 f3 43 27 2b 5b 65 95 9c 1d 25 49 27 11 
xieyihua@xieyihua:~/GmSSL-master$ 

この署名方式がプラットフォームの検証と一致することをどうやって確認するか?

自動車メーカーの担当者によると、以下のオンラインSM2署名検証インターフェースを通じて確認できます:

SM2オンライン署名検証ツール

上記のサンプルプログラムの公開鍵、userId、データ、署名値を入力します。以下のように:

注意:SM署名内部では乱数が使用されているため、userID、秘密鍵、署名データが同じであっても、毎回生成される署名は異なります。

統合

上記のテストサンプルでインターフェースが使用可能であることを検証した後、次はビジネスシナリオに基づいてインターフェースをカプセル化し、プロジェクトに統合するだけです。一般的には2つのステップが必要です:

  1. オープンソースコードのクロスコンパイル
  2. インターフェースのカプセル化

クロスコンパイル

クロスコンパイルの前提は、何が必要かを知ることです。例えば、私たちのプロジェクトでは実際にGmSSL静的ライブラリが必要です。しかし、このオープンソースプロジェクトはデフォルトで動的ライブラリを生成します。したがって、以下のようにcmakeを修正する必要があります:

---:add_library(gmssl ${src}) # デフォルトは動的ライブラリ
+++:add_library(gmssl STATIC ${src}) # 明示的に静的ライブラリを生成するように指示

操作手順:

  1. 環境変数をsource:
xieyihua@xieyihua:~/GmSSL-master$ source ~/3503-MPU/sdk/ql-ol-crosstool/ql-ol-crosstool-env-init
QUECTEL_PROJECT_NAME      =AG35CENFAN
QUECTEL_PROJECT_REV       =AG35CENFNR07A02M4G_OCPU
xieyihua@xieyihua:~/GmSSL-master$

  1. GmSSLをコンパイル:
xieyihua@xieyihua:~/GmSSL-master$ rm build/ -rf
xieyihua@xieyihua:~/GmSSL-master$ mkdir build
xieyihua@xieyihua:~/GmSSL-master$ cd build/
xieyihua@xieyihua:~/GmSSL-master/build$ cmake ..
-- The C compiler identification is GNU 4.9.3
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /home/xieyihua/3503-MPU/sdk/ql-ol-crosstool/sysroots/x86_64-oesdk-linux/usr/bin/arm-oe-linux-gnueabi/arm-oe-linux-gnueabi-gcc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- ENABLE_ASM_UNDERSCORE_PREFIX is ON
-- ENABLE_SM4_ECB is ON
-- ENABLE_SM4_OFB is ON
-- ENABLE_SM4_CFB is ON
-- ENABLE_SM4_CCM is ON
-- ENABLE_SM4_XTS is ON
-- ENABLE_SM3_XMSS is ON
-- ENABLE_SHA1 is ON
-- ENABLE_SHA2 is ON
-- ENABLE_AES is ON
-- ENABLE_CHACHA20 is ON
-- ENABLE_SM4_CBC_MAC is ON
-- Looking for getentropy
-- Looking for getentropy - not found
-- ENABLE_SDF is ON
-- Detected Linux, configuring /etc/ld.so.conf.d/gmssl.conf
-- Configuring done (1.3s)
-- Generating done (0.4s)
-- Build files have been written to: /home/xieyihua/GmSSL-master/build
xieyihua@xieyihua:~/GmSSL-master/build$ make -j8

  1. 生成されたターゲットファイルがクロスコンパイルに成功したか確認

  2. 生成された静的ライブラリlibgmssl.aおよびヘッダーファイルincludeをプロジェクトの対応するディレクトリに配置するだけです。

プロジェクトのカプセル化

1239プロトコルおよびビジネス要件に基づき、最終的なインターフェースのカプセル化は以下の通りです:

//gmsslSign.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <gmssl/mem.h>
#include <gmssl/sm2.h>

const uint8_t g_private_key[32] = {0x83,0xce,0xbc,0x99,0x57,0x95,0x4a,0x62,0x1c,0x59,0x89,0xfa,0xca,0x05,0xc1,0xb8,0x47,0xb1,0xb4,0x4f,0x32,0x3f,0x8c,0x12,0xb8,0x12,0xc4,0x36,0x98,0x32,0x8e,0x03};


const uint8_t g_public_key[65] = {0x04,0x19,0x66,0x23,0xF7,0x5E,0x17,0x43,0x7D,
0x19,0x8F,0x77,0xFE,0xCB,0x7F,0xF8,0xA9,
0x61,0xF6,0x80,0x50,0x2F,0xF7,0xCB,0x50,
0x26,0x9D,0xAA,0x62,0x56,0x4C,0xE4,0x8B,
0x61,0x91,0xC2,0x1D,0x82,0x05,0x17,0x2B,
0xBD,0x85,0x29,0xB5,0xBA,0x58,0xF5,0xFE,
0x0B,0xD1,0xAE,0xA7,0xCE,0x0C,0x2C,0x13,
0xE1,0x48,0x2B,0x96,0xA2,0xD2,0x08,0x24};
/**
 * @brief GmSSLオープンソースライブラリを使用してSM2ソフトウェア署名を実装
 * 
 * @param user_id  [in] ユーザーID
 * @param id_len   [in] ユーザーIDの長さ
 * @param data     [in] 署名対象データ
 * @param data_len [in] 署名対象データの長さ
 * @param signature [out] 署名配列、呼び出し元がメモリを確保し、64Byte以上である必要あり
 * @param sig_len  [out] 署名長さ、デフォルト64
 * 
 * @return 0:成功  非0:失敗
 * 
 * @note
 */
int sa_gmssl_SM2_sign(const char* user_id,
                        size_t id_len, 
                        const uint8_t * data, 
                        size_t data_len, 
                        uint8_t* signature, 
                        int32_t * sig_len)
{
    SM2_KEY key_pair;
    uint8_t private_key[32];
    uint8_t public_key[65];

    SM2_SIGN_CTX sign_context;

    sm2_z256_from_bytes(key_pair.private_key, g_private_key);

    sm2_z256_point_from_octets(&key_pair.public_key, g_public_key, 65);

#ifdef DEGUG
     /** 秘密鍵を取得 */
    sm2_z256_to_bytes(key_pair.private_key, private_key);

    /** 公開鍵を取得 */
    sm2_z256_point_to_uncompressed_octets(&(key_pair.public_key), public_key);

    printf("秘密鍵:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",private_key[i]);
    }
    printf("\n");

    printf("公開鍵:");
    for(int i = 1 ; i < 65 ; i++)
    {
        printf("%02x ",public_key[i]);
    }
    printf("\n");
#endif
    /** SM2を初期化 */
    if (sm2_sign_init(&sign_context, &key_pair, user_id, id_len) != 1) 
    {
        printf("sm2_sign_init failed\n");
        return -1;
    }

    /** 署名データを挿入 */
    if (sm2_sign_update(&sign_context, data, data_len) != 1) 
    {
        printf("sm2_sign_update\n");
        return -1;
	}
   
    /** 署名 */
    uint8_t digest[SM3_DIGEST_SIZE];
	SM2_SIGNATURE signature_result;

	sm3_finish(&sign_context.sm3_ctx, digest);

	if (sign_context.num_pre_comp == 0) {
		if (sm2_fast_sign_pre_compute(sign_context.pre_comp) != 1) {
			printf("sm2_fast_sign failed");
			return -1;
		}
		sign_context.num_pre_comp = SM2_SIGN_PRE_COMP_COUNT;
	}

	sign_context.num_pre_comp--;
	if (sm2_fast_sign(sign_context.fast_sign_private, &sign_context.pre_comp[sign_context.num_pre_comp],
		digest, &signature_result) != 1) {
		printf("sm2_fast_sign failed");
		return -1;
	}
#ifdef DEGUG
    printf("署名r:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",signature_result.r[i]);
    }
    printf("\n");

    printf("署名s:");
    for(int i = 0 ; i < 32 ; i++)
    {
        printf("%02x ",signature_result.s[i]);
    }
    printf("\n");
#endif
/** 署名rを設定 */
    memcpy(signature, signature_result.r, 32);
    /** 署名sを設定 */
    memcpy(signature+32, signature_result.s, 32);
    *sig_len = 64;
    
    return 0;
}

//gmsslSign.h
#include <stdint.h>

#ifndef __GMSSL_SIGN_H__
#define __GMSSL_SIGN_H__

#ifdef __cplusplus
extern "C"{
#endif
/**
 * @brief GmSSLオープンソースライブラリを使用してSM2ソフトウェア署名を実装
 * 
 * @param user_id  [in] ユーザーID
 * @param id_len   [in] ユーザーIDの長さ
 * @param data     [in] 署名対象データ
 * @param data_len [in] 署名対象データの長さ
 * @param signature [out] 署名配列、呼び出し元がメモリを確保し、64Byte以上である必要あり
 * @param sig_len  [out] 署名長さ、デフォルト64
 * 
 * @return 0:成功  非0:失敗
 * 
 * @note
 */
int sa_gmssl_SM2_sign(const char* user_id,
                        size_t id_len, 
                        const uint8_t * data, 
                        size_t data_len, 
                        uint8_t* signature, 
                        int32_t * sig_len);

#ifdef __cplusplus
extern }
#endif

#endif

ここでextern "C"を使用する理由は:このソースファイルは.cですが、私たちのプロジェクトにはc++cの混在した記述が存在します。.cppファイルで参照した場合、コンパイルが通らないため、この宣言を追加する必要があります。

cmakeの修正

ソースコードとヘッダーファイルをプロジェクトに追加した後、それをコンパイルして他のソースファイルから呼び出せるようにする必要があります。したがって、cmakeを修正する必要があります。

# ヘッダーファイル検索パスを設定
include_directories(
    ${CMAKE_SOURCE_DIR}/lib/gmssl/include
    )

# ソースコードをプロジェクトにコンパイル
set(LIB_SRC
    ${CMAKE_SOURCE_DIR}/soc/stTsp/Acl16Api/gmsslSign.c
    )

# ライブラリ検索パスを設定
link_directories(${CMAKE_SOURCE_DIR}/lib/gmssl/lib)

# 静的ライブラリlibgmssl.aを明示的にリンク
target_link_libraries(stTsp
                        libgmssl.a
)

コンパイル、コード提出。

まとめ

問題は最終的に解決されましたが、見たところそれほど難しいことではありませんでした。その中の苦労は、経験した人だけが理解できるでしょう。類似の未経験の問題に直面した場合、私のアドバイスは:

  1. 正確性を検証できる方法を見つける。本記事ではSM2オンライン署名検証ツールがそれです
  2. 方向性が確実になった後、考えを注ぎ込みます。例えば、私はgmsslコマンドラインを通じてSM2署名・検証が成功した後、このオープンソースライブラリが要件を満たしていると判断しました。
  3. 不明な内容については、困難に直面しても簡単に諦めず、考え方を変えて、機転を利かせます。

私の経験が皆さんの助けになれば幸いです。

私のコンテンツが皆さんのお役に立てれば、私の公式アカウントをフォローしてください。定期的に実用的な情報を共有し、ケースを分析し、一緒に議論・共有することもできます。
私の理念:
皆さんの仕事で直面するすべての落とし穴を踏み抜き、それを皆さんと共有し、皆さんの仕事にバグがなく、人生がすべて平坦な道のりとなることを目指します

タグ: GmSSL SM2 国密算法 署名验证 嵌入式开发

6月15日 20:09 投稿