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(¤t_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(¤t_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(¤t_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 應用於文件保全工作的最後一篇文章。下列為已完成的其他四篇:
- OpenSSL Library - 讀取X509 certificate 的資訊
- OpenSSL Library - BIO 概論
- OpenSSL Library - EVP, Digest and Cipher
- OpenSSL Library - SOD 安全文件概論