最近更新: 2011-08-11

OpenSSL Library - EVP, Digest and Cipher

EVP 是 OpenSSL 提供的高階資料編碼函數群,參見[evp(3)]。OpenSSL 的 crypto 函數庫實作了許多資料編碼方法,例如:

  • 採用 MD2, MD4, MD5, SHA 等雜湊演算法的資料摘要函數。
  • 採用 RC2, RC4, RC5, DES, IDEA, blowfish 演算法的對稱式加密函數。
  • 採用 DSA, RSA 演算法的非對稱式(公鑰)加密函數。

程序員可以直接調用那些函數處理資料。但是 EVP 提供了更高一層的抽象化介面,讓我們可以寫完一遍程式碼後,僅需抽換編碼模組,就可以支援多種編碼方法。在實際應用中,透過 OpenSSL 交換資料的雙方,並非事先談好要用什麼編碼方法,而是將自己使用的編碼方法之代號加註於資料檔頭。接收方解密資料時就是根據此編碼代碼,用 EVP 載入相同的編碼模組。

NID 與 SN

使用 EVP 之前,我先說一下什麼是 NIDSN。也許我沒仔細看 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 及其應用》。 主要重點是:

  1. 相同的字串內容必定會得出一個固定的摘要(或稱雜湊碼、鍵值),而非每次都算出不同的。
  2. 不同字串內容所演算出來的摘要,有可能相同(此稱"碰撞")。不同演算法的碰撞率各有高低。
  3. 演算不同長度的字串內容,都將得出固定長度的摘要。摘要長度由演算法決定。
  4. 這是單向的雜湊演算,意味著它無法從摘要反推算出原本的字串內容。

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