有限域上的橢圓曲線
這里略去有限域、射影幾何等數學背景介紹。先給出實數域空間上橢圓曲線的一般形式:
以上式子中,\(x,y,z\)均為變元。而令\(z=1\), 則可以得到平面上的橢圓曲線\(Ep(x,y)\)。
對平面上橢圓曲線上的點P, Q, R,以及關於x軸對稱的點P', Q', R', 定義點的加法:
從幾何上看點的加法:P點和Q點之和R',是P、Q兩點連線與曲線的交點R關於x軸的對稱點。當P、Q點重合時,二者的連線即點的切線。此外,圖中點0即原點/無窮遠點。
加法定義完畢后,乘法和逆運算呼之欲出:
這樣,橢圓曲線上點的加法、乘法、求逆等運算,就等同於平面上求直線與曲線交點、曲線上點的切線等初等解析幾何問題,計算過程是簡單的。
密碼學需要使用離散點,因此要將橢圓曲線上的點限定在一個有限域內,即參數(a,b,c)和變元(x,y)取值在有限域中。橢圓曲線主要使用的有限域有GF(p)素數域,以及GF(2^m)二元域。其中,素數域的橢圓曲線形式如下:
通常p是一個大素數。不同於實數域上點的運算有直觀的幾何形象,有限域中點P(x, y)的運算在等號兩側都需要取模(mod p)。此外一個顯而易見的性質是:由於曲線的各個參數定義在有限域上,符合Abel群,因此上述乘法的因子也滿足Abel群中的交換律和結合律:
橢圓曲線之所以能用於公鑰密碼體系,是基於所謂橢圓曲線難題:
- 已知點P,和因子k,求點的乘積Q,在運算上是簡單的(無非是求交點、取模運算等);
- 反之,若已知點P,Q,求乘法因子k,在計算上是困難的,目前還沒有多項式時間的解法。
這種單向門陷函數(Trapdoor function)特性,與RSA一樣,可以用於公鑰密碼體系。可以將{P, kP}公開作為公鑰,乘法因子k作為私鑰。
由於參數和變元都定義在有限域中,在運算過程中都對大素數p取模,因此有限域中橢圓曲線上點的個數是有限的。對於點P進行乘法運算,乘數因子從0至p:
在上述運算中產生了p+1個點,如果某個因子n滿足:
這樣自n+1起之后的點都會和之前的重合。
近世代數已經證明,對於有限域橢圓曲線,這樣的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進行解密:
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密鑰交換過程如下圖所示
最終,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雙方:
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這邊的簽名生成過程:
- Alice的私鑰為\(k_A\),公鑰為\(k_AG\)
- Alice隨機選取一個整數\(k\),計算與基點的乘積\(K=kG\)
- K的橫坐標為\(K_x\),取其相對於曲線階order的模結果r,即\(r=K_x(\bmod order)\)
- 計算k基於曲線階order的乘法逆元\(k^{-1}(\bmod order)\)
- Alice對消息\(m\)進行Hash得到摘要\(h\)
- 最終的簽名為{\(r,s\)},\(r\)在第2步已經給出;而Alice根據自己的私鑰\(k_A\),得到\(s\)如下
根據模的乘法逆元性質,上式左右各乘以k,可得:
Bob收到消息\(m\)和簽名{\(r,s\)}后,進行簽名驗證:
- Bob計算得出\(s\)基於曲線階order的乘法逆元\(s^{-1}\)
- Bob對所得消息進行Hash,得到\(h'\)
- 計算出\(u_1=h's^{-1}\)
- 計算出\(u_2=rs^{-1}\)
- 根據Alice的公鑰(\(k_AG\)),得到點\(P= u_1G+u_2k_A*G\),展開得:
- 當Bob收到的消息未被篡改時,h'=h,則有:
- 取點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)\),需要判斷是否為平面上的零點/無窮遠點,若是則驗證失敗,不能進行下一步取橫坐標的操作。