最近更新: 2011-07-27

OpenSSL Library - 讀取 X509 certificate 的資訊

前陣子完成一個案子,這案子要驗證 RFID 卡的文件內容是否可信。 本文內容是工作過程中寫出來的一個練習程式與小工具。 此工具用途為查看本地的 X509 證書資訊,並回報其是否有效。 工具名稱為 x509-cert-info。

程式內容使用了 OpenSSL 函數庫。OpenSSL 函數庫內容很多,但公式文件的內容很少,就是些草稿。 我將陸續整理工作過程中得到的相關經驗,記錄在部落格上。

x509-cert-info

show-x509-info.h 只是一些自己定義的代號。與 OpenSSL 無關,不多解釋。

typedef enum {
    OK,
    ERROR_UNKNOWN,
    ERROR_ALLOCATE_MEMORY_IS_FAILED,
    ERROR_X509_IS_INVALID,
    ERROR_X509_IS_EXPIRED,
    ERROR_CA_FILE_IS_NOT_FOUND
} State;

typedef enum {
    DER,
    PEM
} Inform;

#define TRUE 1
#define FALSE 0
typedef unsigned char boolean;

boolean show_X509_info(
    const char *cert_filepath, 
    Inform inform,
    X509 **out_x509,
    State *state);

show-x509-info.c 定義了函數 show_x509_info()。 此函數要求指定證書(certificate)的文件路徑(cert_filepath)與文件格式(inform)。 讀入的X509資訊放入指標 out_x509 傳回,另將詳細的函數執行狀態存入 state。 函數回傳值若為 TRUE,則此證書的格式正確且日期尚在有效期間內。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/x509.h>
#include "show-x509-info.h"

boolean show_X509_info(
    const char *cert_filepath, 
    Inform inform,
    X509 **out_x509,
    State *state)
{
    boolean rc = FALSE;
    FILE *fp = NULL;
    X509 *x509data = NULL;
    
    X509_free(*out_x509);

    fp = fopen(cert_filepath, "r");
    if (fp == NULL) {
        *state = ERROR_CA_FILE_IS_NOT_FOUND;
        return FALSE;
    }

    if (inform == DER) {
        d2i_X509_fp(fp, &x509data);
    }
    else {
        PEM_read_X509(fp, &x509data, NULL, NULL);
    }
    fclose(fp);
    if (x509data == NULL) {
	    *state = ERROR_X509_IS_INVALID;
	    return FALSE;
    }

    char issuer_name[1024];
    char subject_name[1024];
    char now_timez[16]; // YYYYMMDDhhmmss: 15, yyMMDDhhmmss: 13
    time_t t;
    struct tm tm;

    X509_NAME_oneline(X509_get_issuer_name(x509data), issuer_name, 
                    sizeof(issuer_name));
    X509_NAME_oneline(X509_get_subject_name(x509data), subject_name, 
                    sizeof(subject_name));

    printf("Issuer  name: %s\n", issuer_name);
    printf("Subject name: %s\n", subject_name);
    printf("Validity from: %s\n", x509data->cert_info->validity->notBefore->data);
    printf("Validity till: %s\n", x509data->cert_info->validity->notAfter->data);
    // x509data->cert_info->validity->notBefore->length
    
    if (strcmp(issuer_name, subject_name) != 0) {
        *state = ERROR_X509_IS_INVALID;
        goto end_func;
    }
    
    t = time(NULL);
    if (localtime_r(&t, &tm) == NULL) {
	    *state = ERROR_ALLOCATE_MEMORY_IS_FAILED;
        goto end_func;
    }

    strftime(now_timez, sizeof(now_timez), "%y%m%d%H%M%SZ", &tm);
    // notBefore < now_timez < notAfter
    //     0     <   n       < 0
    if ((0 < strcmp(now_timez, x509data->cert_info->validity->notBefore->data))
      && (strcmp(now_timez, x509data->cert_info->validity->notAfter->data) <0) )
    {
        rc = TRUE;
    }
    else
    {
	    *state = ERROR_X509_IS_EXPIRED;
        rc = FALSE;
    }

  end_func:
    *out_x509 = x509data;
    return rc;
}

在 OpenSSL 函數庫中,主要的資料輸出入行為是透過它定義的 BIO 類別進行。 但也提供其他的方式,例如透過 C 標準庫的 FILE。 本文還不會用到 BIO ,只用 FILE 讀入指定的文件內容。

處理 DER 格式的X509證書需要使用 d2i_X509, i2d_X509 家族函數,細節宣告於 x509.h (預設位於/usr/include/openssl目錄)。 處理 PEM 格式的X509證書則需要使用 PEM_read_X509, PEM_write_X509 家族函數,細節宣告於 pem.h。 請自行閱讀其標頭檔了解更多內容。

讀入的 X509 結構細節,查看 x509.h 的 struct x509_st。有些內容可以直接取出,例如有效日期。 有些內容則要配合指定的方法,例如 issuer_name 與 subject_name。 至於有哪些方法可以用,請查看 x509.h ,善用你的聯想力。

x509-cert-info 是主程式,它會根據使用者指定的證書路徑與格式,調用函數 show_X509_info() 顯示內容。 並根據 state 回報執行狀態。外部工具 – 例如 shell – 就可以根據程式狀態碼得知證書是否有效。

// gcc -lssl  -o x509-cert-info x509-cert-info.c show-x509-info.c
#include <stdio.h>
#include <stdlib.h>
#include <openssl/x509.h>
#include "show-x509-info.h"

int main(int argc, char *args[])
{
    char *cert_filepath = NULL;
    State state = ERROR_UNKNOWN;
    X509 *x509 = NULL;
    int inform = DER;

    if (argc < 2) {
        printf("Show X509 certificate's information.\n");
        printf("Usage: %s <cert_filepath> [format]\n", args[0]);
        printf("Format: DER or PEM, Default format is DER.\n");
        printf("Return status:\n");
        printf("\t0: ok.\n");
        printf("\t1: UNKNOWN.\n");
        printf("\t2: ALLOCATE_MEMORY_IS_FAILED.\n");
        printf("\t3: X509_IS_INVALID.\n");
        printf("\t4: X509_IS_EXPIRED.\n");
        printf("\t5: CA_FILE_IS_NOT_FOUND.\n");
        return 255;
    }
    
    cert_filepath = args[1];
    
    if (argc >= 3) {
        if (args[2][0] == 'P' || args[2][0] == 'p')
            inform = PEM;
    }

    printf("Information of %s:\n", cert_filepath);
    if (!show_X509_info(cert_filepath, inform, &x509, &state)) {
        switch (state)
        {
        case ERROR_ALLOCATE_MEMORY_IS_FAILED:
            puts("ERROR: ALLOCATE_MEMORY_IS_FAILED\n");
            break;
        case ERROR_X509_IS_INVALID:
            puts("ERROR: X509_IS_INVALID\n");
            break;
        case ERROR_X509_IS_EXPIRED:
            puts("ERROR: X509_IS_EXPIRED\n");
            break;
        case ERROR_CA_FILE_IS_NOT_FOUND:
            puts("ERROR: CA_FILE_IS_NOT_FOUND\n");
            break;
        default:
            puts("ERROR: UNKNOWN\n");
            break;
        }
        return state;
    }

    return 0;
}

編譯參數如下:

gcc -lssl -o x509-cert-info x509-cert-info.c show-x509-info.c

測試

建立測試用的證書

使用 openssl 工具的 req 選項可以建立一份證書。 在此例中,我們只是要一份測試程式功能的證書,不必考慮公信力,所以建立一份我們自己背書的證書即可。 用例如下:

# create PEM format certificate
openssl req \
  -x509 -days 30 -newkey rsa:1024 \
  -subj "/CN=rocksaying/O=Rock's blog./C=TW" \
  -out mycert.pem

# create DER format certificate
openssl req \
  -x509 -days 30 -newkey rsa:1024 \
  -subj "/CN=rocksaying/O=Rock's blog./C=TW" \
  -out mycert.der -outform DER

days 是有效天數。subj 是證書名稱,在此同時也會是發照者名稱,文字格式是 X.500。

證書格式通常使用 DER 或 PEM。 DER 是內容為二進位碼的證書,而 PEM 則是 Base64 編碼加上特定檔頭的證書。 這兩者可以互轉。例如我有一份 DER 格式的X509證書,要轉成 PEM 格式的話,只需要以 openssl 工具操作: openssl x509 -in x509.crt -inform DER -out x509.pem -outform PEM,即可轉得。 由於兩種格式可以互轉,所以建立證書時,通常只需要選擇其中一種格式發布。

測試結果

建立一份 PEM 格式的X509證書,檔名為 mycert.pem。測試的輸出過程如下:

$ openssl req \
>   -x509 -days 30 -newkey rsa:1024 \
>   -subj "/CN=rocksaying/O=Rock's blog./C=TW" \
>   -out mycert.pem
Generating a 1024 bit RSA private key
................++++++
...........++++++
writing new private key to 'privkey.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----

$ ./x509-cert-info mycert.pem 
Information of mycert.pem:
ERROR: X509_IS_INVALID

$ ./x509-cert-info mycert.pem PEM
Information of mycert.pem:
Issuer  name: /CN=rocksaying/O=Rock's blog./C=TW
Subject name: /CN=rocksaying/O=Rock's blog./C=TW
Validity from: 110727071525Z
Validity till: 110826071525Z

我先不帶參數地交給 x509-cert-info 查看內容,由於 x509-cert-info 預設格式為 DER ,故判定證書格式不符。 接著指定選項 PEM,就可成功查看證書內容。

openssl 工具的 x509 選項也可以查看證書內容。用例如: openssl x509 -text -in mycert.pem。 可與本文程式所輸出的結果對照,確認程式無誤。

References

  1. PEM_read_X509()
  2. d2i_X509_fp()
  3. HOWTO certificates
  4. OpenSSL Command-Line HOWTO - Paul Heinlein.

OpenSSL 文件至今仍在草稿階段,有許多函數與結構的內容並未提及。 程序員需要自己查看 OpenSSL 的標頭文件(預設位於/usr/include/openssl)找尋有用的內容。 此外,OpenSSL 的源碼可說是必要的參考文件。 我建議將閱讀重點放在 apps 目錄內的源碼,其內容對應 openssl 命令項目,較容易理解其工作內容。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/16158079.html

樂多舊回應
kelvin692@yahoo.com.tw(聶曉楓) (#comment-22721602)
Sun, 30 Dec 2012 15:52:03 +0800
你好我第一次進這網頁來看,我只有專科程度資料結構與演算法大概沒有教那麼深,像在Linux下也教過c語言寫但能看能你文章真的很好!!謝謝分享