橢圓曲線密碼學在OpenSSL中的實現


有限域上的橢圓曲線

這里略去有限域、射影幾何等數學背景介紹。先給出實數域空間上橢圓曲線的一般形式:

\[y^2z + a_1xyz + a_3yz^2 = x^3 + a_2x^2z + a_4xz^2 + a_6z^3 \]

以上式子中,\(x,y,z\)均為變元。而令\(z=1\), 則可以得到平面上的橢圓曲線\(Ep(x,y)\)

對平面上橢圓曲線上的點P, Q, R,以及關於x軸對稱的點P', Q', R', 定義點的加法:

\[Add : P+Q = R' \]

image

從幾何上看點的加法:P點和Q點之和R',是P、Q兩點連線與曲線的交點R關於x軸的對稱點。當P、Q點重合時,二者的連線即點的切線。此外,圖中點0即原點/無窮遠點。

加法定義完畢后,乘法和逆運算呼之欲出:

\[Mul : P + P + ... P = nP \]

\[Inverse : P + (-P) = O \]

這樣,橢圓曲線上點的加法、乘法、求逆等運算,就等同於平面上求直線與曲線交點、曲線上點的切線等初等解析幾何問題,計算過程是簡單的。

密碼學需要使用離散點,因此要將橢圓曲線上的點限定在一個有限域內,即參數(a,b,c)和變元(x,y)取值在有限域中。橢圓曲線主要使用的有限域有GF(p)素數域,以及GF(2^m)二元域。其中,素數域的橢圓曲線形式如下:

\[y^2 = (x^3 + ax + b)(\bmod p) \]

\[4a^3 + 27b^2 \not= 0 (\bmod p) \]

通常p是一個大素數。不同於實數域上點的運算有直觀的幾何形象,有限域中點P(x, y)的運算在等號兩側都需要取模(mod p)。此外一個顯而易見的性質是:由於曲線的各個參數定義在有限域上,符合Abel群,因此上述乘法的因子也滿足Abel群中的交換律和結合律:

\[n_1*(n_2*P) = n_2*(n_1*P) = (n_1*n_2)P \]

\[(n_1*n_2)*n_3*P = n_1*(n_2*n_3)*P \]

橢圓曲線之所以能用於公鑰密碼體系,是基於所謂橢圓曲線難題:

\[Q = k*P \]

  • 已知點P,和因子k,求點的乘積Q,在運算上是簡單的(無非是求交點、取模運算等);
  • 反之,若已知點P,Q,求乘法因子k,在計算上是困難的,目前還沒有多項式時間的解法。

這種單向門陷函數(Trapdoor function)特性,與RSA一樣,可以用於公鑰密碼體系。可以將{P, kP}公開作為公鑰,乘法因子k作為私鑰。

由於參數和變元都定義在有限域中,在運算過程中都對大素數p取模,因此有限域中橢圓曲線上點的個數是有限的。對於點P進行乘法運算,乘數因子從0至p:

\[P_0 = 0 \]

\[P_1 = P \]

\[P_2 = 2*P \]

\[... \]

\[P_i = i*P \]

\[... \]

\[P_p = p*P \]

在上述運算中產生了p+1個點,如果某個因子n滿足:

\[P_n = n*P = 0 \]

這樣自n+1起之后的點都會和之前的重合。

\[P_n+1 = 0 + P =P, P_n+i = 0 + P_i =P_i \]

近世代數已經證明,對於有限域橢圓曲線,這樣的n必然是存在的。使n*P = 0的最小的n,稱之為橢圓曲線的階(Order),相應的點P稱之為基點(Genrator)。這樣Order可以視作為曲線上對點P運算的周期。此外,曲線上點的個數與基點階的比值稱之為co-factor

OpenSSL中的橢圓曲線表示

再來看看素數域橢圓曲線——以secp256k1為例,描述這個曲線的參數包括:

  • p : 大素數
  • a : 曲線方程系數
  • b : 曲線方程系數
  • x : 基點Generator的x坐標
  • y : 基點Generator的y坐標
  • order : 曲線的階Order

取值如下:

/* no seed */
/* p */
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFC, 0x2F,
/* a */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
/* b */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
/* x */
0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95,
0xCE, 0x87, 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9,
0x59, 0xF2, 0x81, 0x5B, 0x16, 0xF8, 0x17, 0x98,
/* y */
0x48, 0x3a, 0xda, 0x77, 0x26, 0xa3, 0xc4, 0x65, 0x5d, 0xa4, 0xfb, 0xfc,
0x0e, 0x11, 0x08, 0xa8, 0xfd, 0x17, 0xb4, 0x48, 0xa6, 0x85, 0x54, 0x19,
0x9c, 0x47, 0xd0, 0x8f, 0xfb, 0x10, 0xd4, 0xb8,
/* order */
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B,
0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41

橢圓曲線密碼學中所使用的秘鑰結構體ec_key_st如下:包含公鑰、私鑰、有限域、點運算函數等

struct ec_key_st {
    const EC_KEY_METHOD *meth;//計算函數
    ENGINE *engine;
    int version;
    EC_GROUP *group;//群
    EC_POINT *pub_key;//公鑰
    BIGNUM *priv_key;//私鑰
    unsigned int enc_flag;
    point_conversion_form_t conv_form;
    CRYPTO_REF_COUNT references;
    int flags;
    CRYPTO_EX_DATA ex_data;
    CRYPTO_RWLOCK *lock;
};

其中,字段中有限域EC_GROUP結構體如下:

struct ec_group_st {
    const EC_METHOD *meth;      /* 橢圓曲線運算函數 */
    EC_POINT *generator;        /* optional */
    BIGNUM *order, *cofactor;
    ... ...
    BIGNUM *field;
    int poly[6];
    BIGNUM *a, *b;
    ...
};

橢圓曲線的加密算法

從公鑰密碼學角度來看,基於橢圓曲線難題,私鑰為本地生成的大數k,公鑰則為點k*G。
在橢圓曲線密碼學中的數據通常涉及字符串、大整數、點三類,其中,字符串和大整數可以相互轉換:

BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret) //字符串轉換為大整數
int BN_bn2bin(const BIGNUM *a, unsigned char *to) //大整數轉換為字符串

而大整數和通常可以映射為曲線上某個點的x坐標:

//獲取曲線上點point的x坐標(大整數)
int EC_POINT_get_affine_coordinates_GFp(const EC_GROUP *group,
                                        const EC_POINT *point, BIGNUM *x,
                                        BIGNUM *y, BN_CTX *ctx)
//大整數映射至曲線上點
int EC_POINT_set_affine_coordinates_GFp(const EC_GROUP *group,
                                        EC_POINT *point, const BIGNUM *x,
                                        const BIGNUM *y, BN_CTX *ctx)

這里打算只舉一個最簡單的例子,說明基於橢圓曲線加密過程。Alice的私鑰為k_A,公鑰為k_A*G。

  • Bob將明文m映射至橢圓曲線上一點M。(如何映射?字符串->BigNum->P點x坐標即可)
  • Bob隨機選取整數r,計算出點\(R=r*G\)
  • Bob利用Alice的公鑰加密消息發往Alice,消息密文結構為\(C=(M+r*k_A*G)\),同時將點R也發送給Alice
  • Alice收到密文C后,根據公開的點R和自己的私鑰k_A進行解密:

\[M'=C-k_A*R=M+r*k_A*R-k_A*r*G=M \]

ECDH: 橢圓曲線Diffie-Hellman密鑰交換

所謂D-H交換,即兩個用戶之間安全地交換彼此的密鑰,並在后續的傳輸中使用該密鑰對信息加密。
這里先從實際的應用角度出發,討論一下D-H交換在通信中的一個典型應用場景——密鑰協商

由於非對稱加解密通常運算量都很大,主要用於低頻的密鑰交換,所生成的共享密鑰通常是長期使用的。比如通信雙方先使用ECDH交換,協商出共享密鑰PK,這種協商密鑰的過程只需要一次。然后采用PK作為密碼,使用運算更快的AES、CR4、DES等傳統的對稱加密算法,對每次數據報文進行加解密。

回到D-H交換的具體過程中來。在橢圓曲線加密過程中,曲線參數是公開的,包括基點G和階Order。對於用戶A和B:

  • A選取一個隨機數rA作為私鑰,rA*G作為公鑰。其中G即曲線的基點。由橢圓曲線難題可知,其他用戶很難反求私鑰rA
  • B同理,其私鑰為rB,公鑰為rB*G
  • A、B分別向對方發送公鑰。
  • A、B收到的對方公鑰后,乘以自己的私鑰,本地計算出雙方的共享密鑰對。此時A計算得出\(PKA=rA*(rB*G)\);B計算得出\(PKB=rB*(rA*G)\)
  • 根據Abel乘法運算的結合律,當基點G相同時,PKA=PKB。D-H交換通過。

ECDH密鑰交換過程如下圖所示
image
最終,A、B雙方所生成的共享密鑰PK包含彼此的私鑰信息,成功交換了密鑰信息。A、B之間采用PK作為密鑰,使用對稱加解密技術發送報文,進行加密傳輸。

其實D-H交換算法過程很簡短,就是私鑰之積乘以基點G,來看看共享密鑰計算函數的代碼:

openssl/crypto/ec/Ecdh_ossl.c

int ecdh_simple_compute_key(unsigned char **pout, size_t *poutlen,
                            const EC_POINT *pub_key, const EC_KEY *ecdh)
{        ... ...
    if (!EC_POINT_mul(group, tmp, NULL, pub_key, priv_key, ctx)) 
    {
        ECerr(EC_F_ECDH_SIMPLE_COMPUTE_KEY, EC_R_POINT_ARITHMETIC_FAILURE);
        goto err;
    
    }
    ... ...
}        

參數說明:

  • pout: 輸出,即最后的共享密鑰字符串
  • poutlen: 輸出長度
  • pub_key: 收到的對端用戶公鑰
  • ecdh: 本方的密鑰結構體,包含公鑰和私鑰、有限域等

針對D-H交換的中間人攻擊問題

從私鑰保護的目的來看,D-H密鑰交換是安全的,整個交換過程中不會泄露用戶的私鑰。但是D-H交換也存在局限性,比如要求整個交換過程的信道可信,同時交換雙方必須在線,以保證消息可達。

D-H交換最大的局限性在於不能防范中間人攻擊,這里仍然可以舉一個例子。在不可靠信道上,假設A、B之間存在一個中間人C,可以竊聽到A、B之間的信道消息,盡管信道上傳輸的都是公鑰,但C確實能成功欺騙A、B雙方:
image

A、B在D-H交換過程發送的公鑰信息被用戶C竊聽。C分別與A、B交換了私鑰並生成共享密鑰對PKC_A和PKC_B。這樣C就可以與A、B分別進行通信了。A、B對C的竊聽一無所知,以為彼此協商出了共享密鑰\(PK=rA*rB*G\)。而實際上A與C、B與C都分別協商出了PKC_A與PKC_B。

D-H交換最大的問題,就在於無法鑒別通信的參與方,發送者對信息接受者的身份一無所知。當信道不可靠時,公鑰以廣播形式在網絡中傳播,傳輸路徑中的第三方節點可以很容易竊聽乃至篡改原有消息。

為克服中間人攻擊缺陷,需要引入數字簽名和公鑰證書機制。

ECDSA: 橢圓曲線簽名算法

公鑰密碼體系中的簽名機制是:發送方使用私鑰對消息進行簽名,接收方使用公鑰驗證簽名。ECDSA的簽名機制比RSA略復雜。這里仍然以消息發送方Alice和接收方Bob為例。

先來看看Alice這邊的簽名生成過程:

  1. Alice的私鑰為\(k_A\),公鑰為\(k_AG\)
  2. Alice隨機選取一個整數\(k\),計算與基點的乘積\(K=kG\)
  3. K的橫坐標為\(K_x\),取其相對於曲線階order的模結果r,即\(r=K_x(\bmod order)\)
  4. 計算k基於曲線階order的乘法逆元\(k^{-1}(\bmod order)\)
  5. Alice對消息\(m\)進行Hash得到摘要\(h\)
  6. 最終的簽名為{\(r,s\)},\(r\)在第2步已經給出;而Alice根據自己的私鑰\(k_A\),得到\(s\)如下

\[s = k^{-1}(h+k_Ar)(\bmod order) \]

根據模的乘法逆元性質,上式左右各乘以k,可得:

\[sk= (h+k_Ar)(\bmod order) \]

Bob收到消息\(m\)和簽名{\(r,s\)}后,進行簽名驗證:

  1. Bob計算得出\(s\)基於曲線階order的乘法逆元\(s^{-1}\)
  2. Bob對所得消息進行Hash,得到\(h'\)
  3. 計算出\(u_1=h's^{-1}\)
  4. 計算出\(u_2=rs^{-1}\)
  5. 根據Alice的公鑰(\(k_AG\)),得到點\(P= u_1G+u_2k_A*G\),展開得:

\[P=h's^{-1}G+rs^{-1}k_AG=(h'+k_Ar)s^{-1}G(\bmod order) \]

  1. 當Bob收到的消息未被篡改時,h'=h,則有:

\[P=s^{-1}(h+k_Ar)G=s^{-1}(sk)G=kG(\bmod order) \]

  1. 取點P的x坐標\(P_x\)與簽名中的\(r=K_x(\bmod order)\)相比 ,相等則簽名驗證通過。

ECDSA的過程略長,但仍然遵循公鑰密碼體系中的簽名原理。Alice生成的簽名中包括自己的私鑰,Bob驗證簽名則只需使用簽名和公鑰即可解開簽名內容,驗明正身。

OpenSSL中ECDSA的簽名生成過程如下.

openssl/crypto/ec/ecdsa_ossl.c

static int ecdsa_sign_setup(EC_KEY *eckey, BN_CTX *ctx_in,
                            BIGNUM **kinvp, BIGNUM **rp,
                            const unsigned char *dgst, int dlen)

此函數根據消息摘要,生成了簽名所需的k^(-1)和r。

進函數內看看,首先這里使用了BN_generate_dsa_nonce 進行偽隨機數k生成。

/* get random k */
        do
            if (dgst != NULL) {
                /* 使用dgst和private key,應用於偽隨機數生成器PRNG */
                if (!BN_generate_dsa_nonce
                    (k, order, EC_KEY_get0_private_key(eckey), dgst, dlen,
                     ctx)) {
                    ECerr(EC_F_ECDSA_SIGN_SETUP,
                             EC_R_RANDOM_NUMBER_GENERATION_FAILED);
                    goto err;
                }

\(k^{-1}\)和$r $的生成如下:

if (!EC_POINT_mul(group, tmp_point, k, NULL, NULL, ctx)) {
            ECerr(EC_F_ECDSA_SIGN_SETUP, ERR_R_EC_LIB);
            goto err;
        }
        if (EC_METHOD_get_field_type(EC_GROUP_method_of(group)) ==
            NID_X9_62_prime_field) {
            if (!EC_POINT_get_affine_coordinates_GFp
                (group, tmp_point, X, NULL, ctx)) {
                ECerr(EC_F_ECDSA_SIGN_SETUP, ERR_R_EC_LIB);
                goto err;
            }
        }
        /* r = X%order */
        if (!BN_nnmod(r, X, order, ctx)) {
            ECerr(EC_F_ECDSA_SIGN_SETUP, ERR_R_BN_LIB);
            goto err;
        }
    }
    while (BN_is_zero(r));

    /* compute the inverse of k */
    if (EC_GROUP_get_mont_data(group) != NULL) {
    ... ...
        if (!BN_mod_sub(X, order, X, order, ctx)) {
            ECerr(EC_F_ECDSA_SIGN_SETUP, ERR_R_BN_LIB);
            goto err;
        }
        BN_set_flags(X, BN_FLG_CONSTTIME);
       ... ...
        if (!BN_mod_inverse(k, k, order, ctx)) {
            ECerr(EC_F_ECDSA_SIGN_SETUP, ERR_R_BN_LIB);
            goto err;
        }
    }

最終,生成簽名的接口為:

ECDSA_SIG *ossl_ecdsa_sign_sig(const unsigned char *dgst, int dgst_len,
                               const BIGNUM *in_kinv, const BIGNUM *in_r,
                               EC_KEY *eckey)

此函數使用ecdsa_sign_setup 中的k^(-1)和r 產生了最終的簽名,結構體為:

struct ECDSA_SIG_st {
    BIGNUM *r;
    BIGNUM *s;
};

驗證簽名的過程在函數

int ossl_ecdsa_verify(int type, const unsigned char *dgst, int dgst_len,
                      const unsigned char *sigbuf, int sig_len, EC_KEY *eckey)

與之前分析的一致。值得一提的是對於驗證中的點\(P= u_1G+u_2(k_AG)\),需要判斷是否為平面上的零點/無窮遠點,若是則驗證失敗,不能進行下一步取橫坐標的操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM