最近更新: 2011-11-03

OpenSSL - SOD 與 ASN1 解讀

在上一篇《SOD 安全文件概論》中,我直接使用工具 openssl 解讀 SOD 的內容。但它只是按照各項資料的結構順序,用粗略的格式顯示資料內容。本文則是直接用 OpenSSL C 函數庫解讀 SOD 內容。

由於本文案例中的 SOD 採用 ASN.1 格式儲存,所以解讀 SOD 的工作實際上就是 ASN.1 文件的解讀工作,需要利用 OpenSSL C 函數庫中與 ASN.1 相關的函數。不幸的是,在已經很貧乏的 OpenSSL C 函數庫文件中, ASN.1 函數更是連份說明文件都沒有。如果不直接去讀 OpenSSL 的 C 源碼,可能根本寫不出 ASN.1 解讀程式。在此提供大家一個指引方向,想要進行這項挑戰的人,只需要去讀 OpenSSL C 源碼的 crypto/asn1/asn1_par.c 這份源碼文件。本文也沒有足夠的資訊仔細說明那些 C 函數的用法。

本文案例規劃的 SOD 是加上 X.509 金鑰蠟封的 ASN.1 文件,所以 SOD 的解讀工作分成兩部份,第一部份是檢查蠟封是否完好,第二部份才是解讀 ASN.1 文件內容。

解讀程式有些複雜,所以我保留了除錯用工具程式碼。定義於 sod-info.h 。

#define DEBUG 1

#ifdef DEBUG
#define _dmsg(...) fprintf(stderr, __VA_ARGS__)
#else
#define _dmsg(...) NULL
#endif

int sod_verify(const char *cert_filepath, const char *sod_filepath, BIO *out);
int sod_dump(unsigned char *sod_data, size_t sod_len);

檢查文件蠟封

第一動要利用 PKCS7 函數載入 SOD 文件。PKCS7 有三個載入函數,根據你規劃的 SOD 文件儲存格式使用。分別是:

  • 若儲存格式為 SMIME,則以 SMIME_read_PKCS7() 載入。
  • 若儲存格式為 PEM,則以 PEM_read_bio_PKCS7() 載入。
  • 若儲存格式為 DER,則以 d2i_PKCS7_bio() 載入。

本文案例規劃的 SOD 儲存格式為 SMIME ,所以會用 SMIME_read_PKCS7() 載入。

第二動則是檢查 X.509 的憑證資訊,這部份的程式碼在《讀取 X509 certificate 的資訊》中就已經說明。在此就直接引用。

第三動就是用 X.509 公鑰查驗 SOD 文件的完好性,調用 PKCS7_verify() 為之。

綜合以上三動,設計了 sod_verify() ,程式碼存為 sod-verify.c 。

#include <stdlib.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/crypto.h>
#include "show-x509-info.h"
#include "sod-info.h"

#define FORMAT_ASN1     1
#define FORMAT_TEXT     2
#define FORMAT_PEM      3
#define FORMAT_SMIME    6

#if DEBUG
static int smime_verify_callback(int ok, X509_STORE_CTX *ctx)
{
	int error;
	error = X509_STORE_CTX_get_error(ctx);
	_dmsg("error: %d, ok: %d\n", error, ok);
    return ok;
}
#endif

/**
Copy from openssl sources:apps/apps.c:setup_verify()
 */
X509_STORE *setup_verify(char *CAfile, char *CApath)
{
    X509_STORE *store;
    X509_LOOKUP *lookup;
    if(!(store = X509_STORE_new())) goto end;
    lookup=X509_STORE_add_lookup(store,X509_LOOKUP_file());
    if (lookup == NULL) goto end;
    if (CAfile) {
        if(!X509_LOOKUP_load_file(lookup,CAfile,X509_FILETYPE_PEM)) {
            fprintf(stderr, "Error loading file %s\n", CAfile);
            goto end;
        }
    } else X509_LOOKUP_load_file(lookup,NULL,X509_FILETYPE_DEFAULT);

    lookup=X509_STORE_add_lookup(store,X509_LOOKUP_hash_dir());
    if (lookup == NULL) goto end;
    if (CApath) {
        if(!X509_LOOKUP_add_dir(lookup,CApath,X509_FILETYPE_PEM)) {
            fprintf(stderr, "Error loading directory %s\n", CApath);
            goto end;
        }
    } else {
        X509_LOOKUP_add_dir(lookup,NULL,X509_FILETYPE_DEFAULT);
    }
    return store;
  end:
    X509_STORE_free(store);
    return NULL;
}

// See openssl sources:apps/apps.c:load_certs()
STACK_OF(X509) *load_certfile(const char *file, int format)
{
	BIO *certs;
	int i;
	STACK_OF(X509) *othercerts = NULL;
	STACK_OF(X509_INFO) *allcerts = NULL;
	X509_INFO *xi;

	if((certs = BIO_new(BIO_s_file())) == NULL) {
		fprintf(stderr, "err certs = BIO_new(BIO_s_file())\n");
		goto end;
	}

	if (BIO_read_filename(certs,file) <= 0)	{
		fprintf(stderr, "err open %s.\n", file);
		goto end;
	}

	if (format == FORMAT_PEM) {
		othercerts = sk_X509_new_null();
		if(!othercerts) {
			sk_X509_free(othercerts);
			othercerts = NULL;
			goto end;
		}
		allcerts = PEM_X509_INFO_read_bio(certs, NULL,
		        (pem_password_cb *)NULL, NULL);
				//(pem_password_cb *)password_callback, &cb_data);
		for(i = 0; i < sk_X509_INFO_num(allcerts); i++) {
			xi = sk_X509_INFO_value (allcerts, i);
			if (xi->x509) {
				sk_X509_push(othercerts, xi->x509);
				xi->x509 = NULL;
			}
		}
		goto end;
	}
	else {
		fprintf(stderr, "bad input format.\n");
		goto end;
	}
  end:
	if (othercerts == NULL) fprintf(stderr,"unable to load certificates\n");
	if (allcerts) sk_X509_INFO_pop_free(allcerts, X509_INFO_free);
	if (certs != NULL) BIO_free(certs);
	return(othercerts);
}

int sod_verify(
    const char *cert_filepath, 
    const char *sod_filepath,
    //out
    BIO *out)
{
    PKCS7 *p7 = NULL;
    X509 *x509 = NULL;
    X509_STORE *store = NULL;
    BIO *in = NULL;
    STACK_OF(X509) *signers = NULL;
    int rc = 1;

    if (!show_X509_info(cert_filepath, PEM, &x509, (State*)&rc)) {
        _dmsg("X509 Cert is invalid.\n");
        goto end_func;
    }

    in = BIO_new_file(sod_filepath, "r");
    //in = BIO_new_mem_buf(sod_der, sod_der_len);
    if (in == NULL) {
        _dmsg("new BIO in failed\n");
        goto end_func;
    }

    BIO *indata = NULL;
	p7 = SMIME_read_PKCS7(in, &indata); // if format=SMIME
    //p7 = PEM_read_bio_PKCS7(in, NULL, NULL, NULL); // if inform=PEM
    //p7 = d2i_PKCS7_bio(in, NULL); // if inform=DER
    if (p7 == NULL) {
        _dmsg("SOD is invalid.\n");
        goto end_func;
    }
    printf("PKCS7 SOD ok.\n");

    store = setup_verify(NULL, "certs");
    #if DEBUG
	X509_STORE_set_verify_cb_func(store, smime_verify_callback);
    #endif

    STACK_OF(X509) *other = load_certfile(cert_filepath, FORMAT_PEM);
    if (!other) {
        _dmsg("Load cert failed.\n");
        goto end_func;
    }

    if (PKCS7_verify(p7, other, store, indata, out, 0) == FALSE) {
        _dmsg("SOD Verification failure\n");
        goto end_func;
    }
    printf("SOD Verification successful\n");
    rc = 0;

  end_func:
    OPENSSL_free(x509);
	sk_X509_pop_free(other, X509_free);
    BIO_free_all(indata);
    BIO_free_all(in);
    PKCS7_free(p7);
	X509_STORE_free(store);
    sk_X509_free(signers);
    return rc;
}

解讀文件內容

調用 PKCS7_verify() 之後,如果蠟封完好,它就會一併傳回拆封後的 ASN.1 文件內容。 我們需要解讀這份 ASN.1 文件,得到其他文件的摘要資訊。

本文案例將 ASN.1 文件記載的資訊分成三節。第一節記載 SOD 版本號碼。如果你們的案子可能隨著版本更迭而變更 ASN.1 文件記載的結構,那麼你們就可利用版本號碼判斷後續解讀動作要採用哪一套流程。第二節記載摘要資訊所用的演算器。第三節則是一組記載檔案號碼與摘要內容的序列。

按照上述結構,我的解讀流程也設計成兩個函數。一、sod_dump() 讀出 SOD 版本號碼與摘要演算器。二、dump_digests() 印出檔案號碼與摘要內容。程式碼存為 sod-dump.c 。

ASN.1 文件是一種隨機記錄檔,其中儲存的每一筆記錄都屬於 ASN1_object 類。一律先用 ASN1_get_object() 取得該記錄的資料位址與標籤(tag)。再根據標籤判斷記錄型態,調用對應的轉換函數取得資料內容。

依本文案例的規劃,只用了四種標籤,即: SEQUENCE, OID, INTEGER, OCTETSTRING

SEQUENCE 是一個包含其他項目的序列,它使 ASN.1 文件的內容形成巢狀結構。解讀 SEQUENCE 最簡單的方法就是用遞迴。不過本文選擇按照節區劃分。前兩節的 SEQUENCE 屬於 SOD 基本資訊,交由 get_sod_information()解讀。第三節的 SEQUENCE 則是檔案號碼與摘要內容序列,交由 dump_digests() 解讀。

INTEGER 的項目則以 c2i_ASN1_INTEGER() 讀取轉換為 C 語言的整數項,以 M_ASN1_INTEGER_free() 釋放。

OID 則是儲存個體ID的項目,在本文中儲放的是演算器ID。應以 d2i_ASN1_OBJECT() 讀取後,再以 OBJ_obj2nid() 取得其 NID。

#include <stdlib.h>
#include <openssl/pem.h>
#include "show-x509-info.h"
#include "sod-info.h"

// See openssl sources:crypto/asn1/asn1_par.c:ASN1_parse_dump()
int get_sod_information(
    const unsigned char **signers, 
    size_t signers_len, 
    int target_tag, 
    const unsigned char **out_object, 
    size_t *out_len, 
    int *out_offset)
{
    boolean rc = FALSE;
    const unsigned char *current_object = NULL, *pre_object = NULL;
    size_t length, len;
    int tag, xclass, constructed;

    length = signers_len;
    current_object = *signers;
    pre_object = current_object;
    *out_object = NULL;
    *out_offset = length;

    while (current_object < *signers + signers_len) {

        constructed = ASN1_get_object(&current_object, &len, &tag, &xclass, length);
        // 調用之後,p 會變成此object的位址,len 則是此object的資料長度。
        // 如果 tag == V_ASN1_SEQUENCE, 則 len 指的是包含其子項目的資料長度。
    
        length -= current_object - pre_object;
        pre_object = current_object;

        if (tag == target_tag) {
            *out_object = current_object;
            *out_len = len;
            current_object += len;
            break;
        }
        else {
            *out_offset = *out_offset - length;
        }
    }

    rc = TRUE;
  end_func:
    *signers = current_object;
    return rc;
}

static boolean dump_digests(
    int nid, 
    const unsigned char *signers, 
    size_t signers_len)
{
    boolean rc = FALSE;
    const unsigned char *current_object = NULL, *pre_object = NULL, *end_sequence = NULL;
    size_t length, len;
    int tag, xclass, constructed;

    ASN1_INTEGER *asn1_int = NULL;
    const unsigned char *tmp;

    length = signers_len;
    current_object = signers;
    pre_object = current_object;

    while (current_object < signers + signers_len) {
        // 調用之後,p 會變成此object的位址,len 則是此object的資料長度。
        // 如果 tag == V_ASN1_SEQUENCE, 則 len 指的是包含其子項目的資料長度。
        constructed = ASN1_get_object(&current_object, &len, &tag, &xclass, length);
        if ((constructed & V_ASN1_CONSTRUCTED) == 0) {
            _dmsg("not constructed, invalid format\n");
            goto end_func;
        }

        unsigned int current_dg_num = 0;
        unsigned char digest_buf[EVP_MAX_MD_SIZE];
        unsigned int digest_len = 0;

        length -= current_object - pre_object;
        pre_object = current_object;
        end_sequence = current_object + len;
        while (current_object < end_sequence) {
            constructed = ASN1_get_object(&current_object, &len, &tag, &xclass, length);

            length -= current_object - pre_object;
            pre_object = current_object;

            if (tag == V_ASN1_INTEGER) {
                tmp = current_object;
                asn1_int = c2i_ASN1_INTEGER(NULL, &tmp, len);
                if (asn1_int == NULL) {
                    _dmsg("convert to int failed\n");
                    goto end_func;
                }
                printf("DG: %ld\n", ASN1_INTEGER_get(asn1_int));
                current_dg_num = ASN1_INTEGER_get(asn1_int);
                M_ASN1_INTEGER_free(asn1_int);
            }
            else if (tag == V_ASN1_OCTET_STRING) {
                int i;
                printf("Digest: ");
                for (i = 0; i < len; ++i) {
                    printf("%c", current_object[i]);
                }
                printf("\n");
                current_dg_num = 0;
            }
            else {
                _dmsg("format invalid\n");
                goto end_func;
            }
            current_object += len;
        }
    }    

    rc = TRUE;
  end_func:
    return rc;
}

int sod_dump(
    unsigned char *sod_data,
    size_t sod_len)
{
    const unsigned char *p;
    const unsigned char *tmp;
    long len = 0;
    int tag = 0, xclass = 0;
    int offset;
    
    size_t length = sod_len;
    p = sod_data;

    const unsigned char *pre_object = sod_data;

    //printf("step 1. get version of SOD\n");
    get_sod_information(&p, length, V_ASN1_INTEGER, &tmp, &len, &offset);

    ASN1_INTEGER *asn1_int = NULL;
    asn1_int = c2i_ASN1_INTEGER(NULL, &tmp, len);
    if (asn1_int == NULL) {
        _dmsg("convert to int failed\n");
        goto end_func;
    }
    printf("SOD Version: %ld\n", ASN1_INTEGER_get(asn1_int));
    M_ASN1_INTEGER_free(asn1_int);

    //printf("step 2. get SOD algorithm\n");
    const unsigned char *seq_addr = p;
    get_sod_information(&p, length, V_ASN1_OBJECT, &tmp, &len, &offset);
    tmp -= offset;
    // I really do not know why it have to go back. - rock.

    ASN1_OBJECT *asn1_obj = NULL;
    asn1_obj = d2i_ASN1_OBJECT(NULL, &tmp, len+offset);
    if (asn1_obj == NULL) {
        _dmsg("get algorithm object failed\n");
        goto end_func;
    }
    int nid = OBJ_obj2nid(asn1_obj);
    printf("Algorithm NID: %d; SN: %s; LN: %s.\n", nid, OBJ_nid2sn(nid), OBJ_nid2ln(nid));
    ASN1_OBJECT_free(asn1_obj);

    //printf("step 3. get DG hash.\n");
    ASN1_get_object(&p, &len, &tag, &xclass, length);
    length -= p - pre_object;
    pre_object = p;

    if (tag != V_ASN1_SEQUENCE) {
        _dmsg("get DGGroups failed\n");
        goto end_func;
    }

    dump_digests(nid, p, length);

  end_func:
    return TRUE;
}

sod-info.c 是調用前面說明的解讀函數,查驗並解讀 SOD 的示範程式。它解讀的對象,是我在前一篇《SOD 安全文件概論》中,以 sod_generate.php 產生的 SOD 。

// gcc -lssl -o sod-info sod-info.c sod-verify.c sod-dump.c show-x509-info.c
#include <stdlib.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/pem.h>
#include "sod-info.h"

void openssl_init()
{
    OpenSSL_add_all_algorithms();
}

int main() {
    int rc = 0;
    openssl_init();

    BIO *sod_data = BIO_new(BIO_s_mem());
    
    rc = sod_verify("mycert.pem", "/tmp/sod.dat", sod_data);
    if (rc == 0) {
        BUF_MEM *out_data = NULL;
        BIO_get_mem_ptr(sod_data, &out_data);
        //_dmsg("size of out data: %d; max: %d.\n", out_data->length, out_data->max);
        sod_dump(out_data->data, out_data->length);
    }
    return rc;
}

下列為執行結果。

$ gcc -lssl -o sod-info sod-info.c sod-verify.c sod-dump.c show-x509-info.c
error: 0, ok: 1
Issuer  name: /CN=rocksaying/O=Rock's blog./C=TW/ST=Some-State
Subject name: /CN=rocksaying/O=Rock's blog./C=TW/ST=Some-State
Validity from: 111011075144Z
Validity till: 111110075144Z
PKCS7 SOD ok.
SOD Verification successful
SOD Version: 1
Algorithm NID: 672; SN: SHA256; LN: sha256.
DG: 1
Digest: 3315B46DC11FEBFF188C8A4BEC95C7CAB800E7F52848AAFEFDB6CB6550CD3EC5
DG: 2
Digest: BD231484C813F1C78F6497B7ACF3B9B28F534A5B7C61D4BBA3AFB580336ACF2E
DG: 13
Digest: 5D35444622F70AB896A01C8D2B4904BECFFD30BD81AE9C4AA45B489A3164F0EB

本文僅將演算器與各檔案的摘要內容傾印於畫面上,並未進行摘要查核動作。請參考《OpenSSL Library - EVP, Digest and Cipher》了解如何透過 NID 調用演算器,以查核晶片內其他文件的完整性。

本文是針對 OpenSSL 應用於文件保全工作的最後一篇文章。下列為已完成的其他四篇:

樂多舊網址: http://blog.roodo.com/rocksaying/archives/17760845.html