EVP 是 OpenSSL 提供的高階資料編碼函數群,參見[evp(3) ]。OpenSSL 的 crypto 函數庫實作了許多資料編碼方法,例如:
程序員可以直接調用那些函數處理資料。但是 EVP 提供了更高一層的抽象化介面,讓我們可以寫完一遍程式碼後,僅需抽換編碼模組,就可以支援多種編碼方法。在實際應用中,透過 OpenSSL 交換資料的雙方,並非事先談好要用什麼編碼方法,而是將自己使用的編碼方法之代號加註於資料檔頭。接收方解密資料時就是根據此編碼代碼,用 EVP 載入相同的編碼模組。
NID 與 SN
使用 EVP 之前,我先說一下什麼是 NID 與 SN 。也許我沒仔細看 OpenSSL 文件或是它根本沒寫,總之我不知道 NID 與 SN 是什麼單字的縮寫。但我知道 OpenSSL 將它目前所實作的每一種資料編碼方法都賦予了一個代號,它稱這種代號為 NID。而 SN 則是對應代號的編碼方法名稱字串。
我們可以查看 OpenSSL 的 obj_mac.h 得知目前到底有多少個代號。最簡單的方法是在命令列下執行 grep NID_ /usr/include/openssl/obj_mac.h
。例如我們常見的 MD5 雜湊法,其 NID 代號為 NID_md5 (實際序列值為 4),SHA256 雜湊法之 NID 代號為 NID_sha256 (實際序列值為 672)。
NID 主要用於資料交換的內部處理過程。如果在需要使用者輸入的場合,那麼多種數字顯然不是一般人記得住的。所以還是要讓使用者輸入文字名稱,OpenSSL 將之稱為 SN。為了方便查詢 NID 與 SN 的對應關係, OpenSSL 提供了一組查詢函數。OBJ_sn2nid()
可由名稱查代號;OBJ_nid2sn()
則以代號查名稱。
evp_sn-nid.c 是一個查詢 NID 與 SN 的範例程式。給它一個數字的參數,會查詢其編碼法名稱。給它編碼法名稱,則查詢其 NID 值。
// gcc -lcrypto -o evp_sn-nid evp_sn-nid.c
#include <stdio.h>
#include <openssl/evp.h>
// program <num or string>
int main ( int argc , char * argv []) {
int nid = 0 ;
const char * digest_method_name = NULL ;
if ( argc < 2 )
return 1 ;
if ( argv [ 1 ][ 0 ] > '9' ) {
digest_method_name = argv [ 1 ];
nid = OBJ_sn2nid ( digest_method_name );
}
else {
nid = atoi ( argv [ 1 ]);
digest_method_name = OBJ_nid2sn ( nid );
}
printf ( "Digest method name: %s \n " , digest_method_name );
printf ( "NID: %d \n " , nid );
return 0 ;
}
執行範例如下:
$ gcc -lcrypto -o evp_sn-nid evp_sn-nid.c
$ ./evp_sn-nid 4
Digest method name: MD5
NID: 4
$ ./evp_sn-nid 671
Digest method name: RSA-SHA224
NID: 671
$ ./evp_sn-nid SHA256
Digest method name: SHA256
NID: 672
$ ./evp_sn-nid RSA
Digest method name: RSA
NID: 19
$ ./evp_sn-nid IDEA-CBC
Digest method name: IDEA-CBC
NID: 34
若輸入的名稱查出的 NID 為 0,表示查無對應的編碼方法。例如,若僅輸入 IDEA ,則查無 NID。要輸入 IDEA-CBC, IDEA-ECB, IDEA-CFB 或 IDEA-OFB 才行。
在 EVP 提供的函數中,通常會帶有一個型態為 ENGINE 的參數。這參數是用於指定額外或自製的編碼模組。一般人不會用到這個參數,都給 NULL。
Digest - 資料摘要
關於 Digest 類型演算法,其行為特徵可參考《在 C 程式中使用 MD5 library 及其應用 》。
主要重點是:
相同的字串內容必定會得出一個固定的摘要(或稱雜湊碼、鍵值),而非每次都算出不同的。
不同字串內容所演算出來的摘要,有可能相同(此稱"碰撞")。不同演算法的碰撞率各有高低。
演算不同長度的字串內容,都將得出固定長度的摘要。摘要長度由演算法決定。
這是單向的雜湊演算,意味著它無法從摘要反推算出原本的字串內容。
Digest 演算法主要用於資料查核,有時用於儲存通行密碼(password)。在儲存通行密碼的案例中,我們並不需要還原通行密碼的原文,所以雜湊法正合所需。且摘要皆為固定長度的特性,也阻絕入侵者根據字串長度猜測通行密碼的機會。
evp_digest.c 是用 EVP 撰寫的摘要程式。使用者必須指定第一個參數,其用途為指定 digest 演算法模組,可以數字(NID)表示,也可用文字名稱(SN)。第二個參數則指定來源文件,若省略則自標準輸入設備讀取資料。其輸出結果有兩組。第一組的數字表示此摘要所用的演算法 NID;第二組便是資料摘要。
加註 NID 的用途,主要用於電子文件交換過程的資料查核作業。這樣接收方才知道要用哪種演算法查核。
#include <stdio.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
/**
ths size of dest should be >= EVP_MAX_MD_SIZE.
See also: http://www.openssl.org/docs/crypto/EVP_DigestInit.html
*/
unsigned char * stream_digest (
int nid ,
BIO * bin ,
unsigned char * dest ,
int * dest_len )
{
unsigned char buf [ 1024 ];
int rc ;
* dest_len = 0 ;
// select digest module.
//const EVP_MD *md = EVP_get_digestbyname("SHA256"); // sn
const EVP_MD * md = EVP_get_digestbynid ( nid ); // nid
if ( md == NULL )
return NULL ;
EVP_MD_CTX ctx ;
EVP_MD_CTX_init ( & ctx );
EVP_DigestInit_ex ( & ctx , md , NULL ); // insert digest module.
while (( rc = BIO_read ( bin , buf , sizeof ( buf ))) > 0 ) {
EVP_DigestUpdate ( & ctx , buf , rc );
}
EVP_DigestFinal_ex ( & ctx , dest , dest_len ); // copy digest to dest.
EVP_MD_CTX_cleanup ( & ctx );
return dest ;
}
// gcc -lcrypto -o evp_digest evp_digest.c
// program <nid> [input_filepath]
int main ( int argc , char * argv []) {
int nid = 0 ;
BIO * bin ;
char dest [ EVP_MAX_MD_SIZE ];
int dest_len , i ;
// nid: for example NID_md5 = 4, NID_sha256 = 672
if ( argc >= 2 ) {
if ( argv [ 1 ][ 0 ] > '9' )
nid = OBJ_sn2nid ( argv [ 1 ]);
else
nid = atoi ( argv [ 1 ]);
}
if ( nid == 0 ) {
puts ( "Unknown digest method." );
return 1 ;
}
if ( argc >= 3 )
bin = BIO_new_file ( argv [ 2 ], "r" );
else
bin = BIO_new_fp ( stdin , BIO_NOCLOSE );
if ( bin == NULL ) {
puts ( "Failed to open input file." );
return 1 ;
}
OpenSSL_add_all_digests (); // load all digest modules.
stream_digest ( nid , bin , dest , & dest_len );
BIO_free_all ( bin );
printf ( "%d," , nid );
for ( i = 0 ; i < dest_len ; ++ i )
printf ( "%02X" , ( unsigned char ) dest [ i ]);
puts ( "" );
return 0 ;
}
執行範例如下:
$ gcc -lcrypto -o evp_digest evp_digest.c
$ ./evp_digest 4 evp_digest
4,12CAD5A78966FF7809B51FA84711F7DA
$ ./evp_digest MD5 evp_digest
4,12CAD5A78966FF7809B51FA84711F7DA
$ ./evp_digest SHA256 evp_digest
672,BE7961BDAF58809523B1C16BFAC4A128BAA686AC206F130ED1DE1961BA660817
Cipher - 對稱式加密
Cipher (對稱式加密) 類型演算法的特徵在於加密與解密皆使用同一組鍵值(key)。
加密後的資料,可以再用同一組鍵值解密還原原文。
某些演算法須額外配合一組矢量(vector)。
OpenSSL 的函數參數中,以 key 表示鍵值,iv 表示矢量。
key 與 iv 的長度與限制,依演算法而有所不同,請自行查看各演算法的說明。
evp_cipher.c 是 OpenSSL Cipher 函數的範例程式,提供文件加密與解密功能。
執行時至少需要提供 4 個參數。第一個參數指定演算法(NID或SN),第二個參數指定鍵值,第三個參數指定額外的演算矢量,第四個參數指示演算行為是加密(encrypt)或解密(decrypt)。
#include <stdio.h>
#include <string.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#define CIPHER_DECRYPT 0
#define CIPHER_ENCRYPT 1
/**
enc: "1 for encryption, 0 for decryption."
See also: http://www.openssl.org/docs/crypto/EVP_EncryptInit.html
*/
int stream_cipher (
int nid ,
BIO * bin ,
unsigned char * key ,
int key_len ,
unsigned char * iv ,
int iv_len ,
int enc )
{
unsigned char raw_buf [ 1024 ], * encrypt_buf = NULL ;
int total_len = 0 , out_len = 0 ;
int rc = 0 ;
// select cipher module.
//const EVP_CIPHER *cipher = EVP_get_cipherbybyname("BF-CBC"); // sn
const EVP_CIPHER * cipher = EVP_get_cipherbynid ( nid ); // nid
if ( cipher == NULL )
return 0 ;
if ( key_len < EVP_CIPHER_key_length ( cipher ) ||
iv_len < EVP_CIPHER_iv_length ( cipher ))
{
return 0 ;
}
int cipher_block_size = EVP_CIPHER_block_size ( cipher );
encrypt_buf = ( unsigned char * ) malloc ( cipher_block_size + sizeof ( raw_buf ));
/* See http://www.openssl.org/docs/crypto/EVP_EncryptInit.html
"the amount of data written may be anything from zero bytes
to (inl + cipher_block_size - 1)"
*/
EVP_CIPHER_CTX ctx ;
memset ( & ctx , 0 , sizeof ( ctx ));
EVP_CIPHER_CTX_init ( & ctx );
EVP_CipherInit_ex ( & ctx , cipher , NULL , key , iv , enc );
// set cipher module, key, iv and enc.
while (( rc = BIO_read ( bin , raw_buf , sizeof ( raw_buf ))) > 0 ) {
if ( ! EVP_CipherUpdate ( & ctx , encrypt_buf , & out_len , raw_buf , rc )) {
total_len = 0 ;
goto end_func ;
}
fwrite ( encrypt_buf , out_len , 1 , stdout );
total_len += out_len ;
}
unsigned char * block_ptr = encrypt_buf + out_len ;
if ( ! EVP_CipherFinal_ex ( & ctx , block_ptr , & out_len )) {
total_len = 0 ;
goto end_func ;
}
fwrite ( block_ptr , out_len , 1 , stdout );
total_len += out_len ;
end_func:
EVP_CIPHER_CTX_cleanup ( & ctx );
free ( encrypt_buf );
return total_len ;
}
// gcc -lcrypto -o evp_cipher evp_cipher.c
// program <nid> <key> <iv> <encrypt|decrypt> [input_filepath]
int main ( int argc , char * argv []) {
int nid = 0 , enc = 0 , rc ;
BIO * bin ;
unsigned char * key , * iv ;
if ( argc < 5 )
return 1 ;
if ( argv [ 1 ][ 0 ] > '9' )
nid = OBJ_sn2nid ( argv [ 1 ]);
else
nid = atoi ( argv [ 1 ]);
if ( nid == 0 ) {
puts ( "Unknown cipher method." );
return 1 ;
}
key = argv [ 2 ];
iv = argv [ 3 ];
if ( argv [ 4 ][ 0 ] == 'd' || argv [ 4 ][ 0 ] == 'D' )
enc = CIPHER_DECRYPT ;
else
enc = CIPHER_ENCRYPT ;
if ( argc >= 6 )
bin = BIO_new_file ( argv [ 5 ], "r" );
else
bin = BIO_new_fp ( stdin , BIO_NOCLOSE );
if ( bin == NULL ) {
puts ( "Failed to open input file." );
return 1 ;
}
OpenSSL_add_all_ciphers (); // load all cipher modules.
//OpenSSL_add_all_algorithms(); // load all algorithms.
rc = stream_cipher ( nid , bin , key , strlen ( key ), iv , strlen ( iv ), enc );
BIO_free_all ( bin );
if ( rc > 0 )
return 0 ;
return 1 ;
}
執行範例如下:
# 資料加密,加密後的資料內容儲存於 cipher.bin
$ ./evp_cipher BF-CBC 0123456789abcdef 12345678 \
encrypt evp_sn-nid.c > cipher.bin
# 解密 cipher.bin ,還原文件。
$ ./evp_cipher BF-CBC 0123456789abcdef 12345678 decrypt cipher.bin
# 使用 openssl 工具檢查我們寫的程式是否正確。
$ openssl bf-cbc -k 30313233343536373839616263646566 -v 3132333435363738 \
-d -in cipher.bin
鍵值與矢量可以使用任何資料,不一定要用可讀字元,使用限制主要在其長度(以位元數計算)。不同演算法要求的資料位元數亦有所不同。evp_cipher.c 第32到33行便是在判斷演算法的最適或最短的鍵值與矢量資料長度。
其他
OpenSSL Library 的系列文章目前已有下列內容:
因為我在先前的工作中並沒有使用公鑰加密函數,所以我目前也還沒整理這一部份的內容。關於公鑰加密函數的內容,就留待日後有空再做了。下一篇應該會繼續談 X509 的內容。
相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/16290045.html