背景
先週、自動車メーカーから問題が報告されました:販売済みの一部車両からプラットフォームに報告される排ガスデータの署名検証が失敗しているとのことです。
内部確認の結果、これは当社のプロセス設計上の問題でした。デバイスの署名プロセスを簡単に説明します(下図参照):
プロセス:
- モジュールがI2Cシリアルポートを介して署名データをSE暗号チップに送信します。
- SE暗号チップは、自身のuserID_Aと事前に設定された秘密鍵を使用してSM2標準署名インターフェースで署名を行います。署名値を4Gモジュールに返します。
- 4Gモジュールは、署名データ、userID_B、事前に合意された公開鍵、署名情報をプラットフォームに報告します。
上記のプロセスから、SE署名に使用されるuserID_Aとモジュールが報告するuserID_Bが一致しないため、プラットフォームでの署名検証が失敗していることがわかりました。
解決策:
- 4Gモジュールが報告するuserID_BをuserID_Aに変更します。しかし、プラットフォームに報告するuserIDの形式は自動車メーカーの要件により、指定された形式でなければなりません。【不適】;
- 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つの問題が存在します:
- この検証方法はコマンドライン形式であり、非対称鍵は証明書形式です。私たちが期待する形式(API形式、かつ公開鍵は64バイト配列、秘密鍵は32バイト配列)と一致しません。
- 検証は成功しましたが、この署名・検証プロセスが国の1239プラットフォームに適合しているかどうかは保証できません。
最初の問題については、GmSSLソースコードライブラリを分析することで理解しました。
秘密鍵、公開鍵をバイト配列に変換する方法
コマンドラインgmssl sm2keygen -pass 1234 -out sm2.pem -pubout sm2pub.pemから、gmsslはsm2keygenインターフェースを通じて秘密鍵と公開鍵証明書を生成することがわかります。そこで、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);
上記から:
- 非対称鍵オブジェクトは
SM2_KEY keyであり、構造体形式です。定義は以下の通り:
typedef struct {
SM2_Z256_POINT public_key;
sm2_z256_t private_key;
} SM2_KEY;
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つのステップが必要です:
- オープンソースコードのクロスコンパイル
- インターフェースのカプセル化
クロスコンパイル
クロスコンパイルの前提は、何が必要かを知ることです。例えば、私たちのプロジェクトでは実際にGmSSL静的ライブラリが必要です。しかし、このオープンソースプロジェクトはデフォルトで動的ライブラリを生成します。したがって、以下のようにcmakeを修正する必要があります:
---:add_library(gmssl ${src}) # デフォルトは動的ライブラリ
+++:add_library(gmssl STATIC ${src}) # 明示的に静的ライブラリを生成するように指示
操作手順:
- 環境変数を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$
- 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
-
生成されたターゲットファイルがクロスコンパイルに成功したか確認
-
生成された静的ライブラリ
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
)
コンパイル、コード提出。
まとめ
問題は最終的に解決されましたが、見たところそれほど難しいことではありませんでした。その中の苦労は、経験した人だけが理解できるでしょう。類似の未経験の問題に直面した場合、私のアドバイスは:
- 正確性を検証できる方法を見つける。本記事ではSM2オンライン署名検証ツールがそれです
- 方向性が確実になった後、考えを注ぎ込みます。例えば、私は
gmsslコマンドラインを通じてSM2署名・検証が成功した後、このオープンソースライブラリが要件を満たしていると判断しました。 - 不明な内容については、困難に直面しても簡単に諦めず、考え方を変えて、機転を利かせます。
私の経験が皆さんの助けになれば幸いです。
私のコンテンツが皆さんのお役に立てれば、私の公式アカウントをフォローしてください。定期的に実用的な情報を共有し、ケースを分析し、一緒に議論・共有することもできます。
私の理念:
皆さんの仕事で直面するすべての落とし穴を踏み抜き、それを皆さんと共有し、皆さんの仕事にバグがなく、人生がすべて平坦な道のりとなることを目指します