最近更新: 2011-10-12

OpenSSL - SOD 安全文件概論

在 RFID 或其他微型儲存媒體的應用情境中,我們會將使用者的相關資訊儲存在 RFID 內。再透過讀取 RFID 的內容,快速判讀使用者的識別資訊,完成所需的工作。例如我們將員工的識別資訊存入 RFID 。員工持卡刷過門禁系統上的讀卡區後,門禁系統就可取得持卡人的身份,判斷是否允許進入。

但是我們要如何確認這張卡的內容是由我們發出的呢?再者,RFID 是可重覆寫入的儲存媒體,亦即他人可以自行修改裡面的內容。我們又要如何保證 RFID 的內容沒有被使用者自行竄改呢? 這些議題,可以藉由 OpenSSL 滿足其安全需求。

SOD 概論

我前陣子完成了一項與 RFID 有關的專案子。在該專案中,每張 RFID 卡中,都存有一份由發卡者所封存的安全文件(Secure Document),以下簡稱為 SOD 。我們利用 OpenSSL 提供的工具與函數庫, 在 SOD 記錄了 RFID 內其他文件的摘要內容,而 SOD 本身也使用發卡者的私鑰加以蠟封。這實現了下列三點安全需求:

  1. 藉由封蠟上印的發行者名稱,我們可以得知這份 SOD 的發行者是不是我們或認可者。
  2. 查驗封蠟的完整性,我們可以檢查這份 SOD 保存的摘要內容是否被竄改過。
  3. 從 SOD 取出摘要內容後,我們可以用摘要內容再去檢查其他文件的嚴密性。

那麼,我們要如何利用 OpenSSL 工具與函數產生這樣一份 SOD 呢?

SOD 製作過程

我們先來看 SOD 製作示意圖。

SOD製作示意圖

第一步,先製作一對金鑰(公鑰與私鑰),作為我們的蠟封工具。可以利用 OpenSSL 提供的工具,產生符合 X.509 憑證格式的一對金鑰。這對金鑰中的私鑰將用於蠟封 SOD。而公鑰提供給其他人查驗 SOD 。

第二步,以 OpenSSL 提供的摘要函數,取得 RFID 中儲存的每份文件(不含 SOD 本身)的摘要。以便日後檢核這些文件的嚴密性。

第三步,我們需要採用一套正式且無歧義的規則記錄這些摘要與我們使用的摘要函數。OpenSSL 提供的 ASN.1 標記方法可以滿足此一需求。 所以將第二步取得的摘要內容,記錄在一份符合 ASN.1 格式的文件中。

第四步,用第一步製作的私鑰蠟封第三步的文件。蠟封過的文件就是 SOD 。

SOD 的保密性依安全性需求而有所不同。在本文的案例中,SOD 的保密等級是公開的,大家都可以讀取,而其目的僅用於確保內容未被竄改。在某些安全等級更高的案例中, SOD 本身也是用金鑰加密的密件,只有持有配對金鑰的使用者才能解讀 SOD 的內容。SOD 的製作方法,隨案例需求而不同,本文案例算是最簡單的應用。

製作金鑰

建立證書

可以用 openssl 工具製作金鑰,搭配的指令選項如下:

  • req
    建立一個證書。
  • -x509
    直接輸出一份 X.509 的證書,而不是憑證請求書。 這選項通常用於建立自己背書而非第三方公證的證書。
  • -days n
    有效日期。
  • -newkey alog:key_length
    隨機產生一組鍵值,使用指定的演算法與鍵值長度。
  • -subj "X.500 message"
    發行者資訊。格式為 X.500 訊息。例如 /CN=rocksaying/O=Rock's blog./C=TW。
  • -out your_x509_certificate_filename
    證書公鑰檔案名稱。
  • -keyout your_certificate_private_key_filename
    證書私鑰檔案名稱。

以下示範製作一對有效日期30天的金鑰。此操作會得到兩個檔案,一為 mycert.pem 的公鑰,一為 mycert_privkey.pem 的私鑰。 操作過程中,openssl 會詢問私鑰密碼。請記住這個密碼,因為使用私鑰時需要這個密碼。

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

下列的 x509-cert-generate.php 則是使用 PHP 撰寫的金鑰製作程式。其效果等於上述的 openssl 操作。

<?php
// See http://www.php.net/manual/en/function.openssl-csr-new.php
$mycert_filepath = 'mycert.pem';
$myprivkey_filepath = 'mycert_privkey.pem';
$days = 30;

// -newkey rsa:1024
$pkey_config_args = array(
    'private_key_type'  => OPENSSL_KEYTYPE_RSA,
    'private_key_bits'  => 1024,
);
$pkey = openssl_pkey_new($pkey_config_args);

// -subj "..."
$dn = array(
    #'CN'    => 'rocksaying',
    'commonName' => 'rocksaying', # 'commonName' will be converted to 'CN'
    'O'     => "Rock's blog.",
    'C'     => 'TW'
);

// req -x509
$csr = openssl_csr_new($dn, $pkey);

// -days 30
$self_signed_cert = openssl_csr_sign($csr, null, $pkey, $days);

// -out mycert.pem
// mycert.pem is just a self signed certificate.
// not a trusted certificate issued by CA.
openssl_x509_export_to_file($self_signed_cert, $mycert_filepath);

// -keyout mycert_privkey.pem
$pkey_password = '1234';
openssl_pkey_export_to_file($pkey, $myprivkey_filepath, $pkey_password);

# export my certificate signing request. 
# this should be send to third CA org for issue.
#openssl_csr_export($csr, 'mycert.csr');
?>
證書可信度

前一節提到 -x509 選項多用於建立「自己背書」(Self signed certificate)的證書,通常這是在軟體開發時期的測試用證書。 在 OpenSSL 文件中,金鑰證書依其可信度,粗分成兩種。

  • 一、Trusted certificate: 經過公證機構(CA)背書過或是使用者本人認可的證書。 在正式場合中,這是具公信力的有效證書。
  • 二、Self signed certificate: 自己背書的證書,未經公證機構背書。 通常這是在軟體開發時期的測試用證書,不公開發佈。

我們可用 openssl 工具的 verify 選項查驗我們拿到的證書種類。如下所示範:

# Verify X509 self-signed certificate.
$ openssl verify mycert.pem
mycert.pem: /CN=rocksaying/O=Rock's blog./C=TW/ST=Some-State
error 18 at 0 depth lookup:self signed certificate
OK

# Verify Trusted certificate.
$ openssl verify mycert.pem
mycert.pem: /CN=rocksaying/O=Rock's blog./C=TW/ST=Some-State
OK

如果你查驗的證書是自己背書的,則查驗過程會失敗,提示錯誤代號 18 ,並說明原因為此證書是自己背書的證書。若該證書經過公證或是你本人認可,則不會顯示任何錯誤訊息。

如何管理認可的證書?

在一個區域性或私人的使用環境中,你可能拿到來自朋友自簽的公鑰證書。 在你們的社交小圈子中,不會存在什麼公證機構。 所以你可以透過你的人際關係與私下聯繫,確認你拿到的自己背書的證書是真的、可信的。 但 OpenSSL 函數並不這麼認為,它還是認為那份證書不具公信力。 你需要做一些管理工作,才能讓你的應用軟體的 OpenSSL 函數,認可這些自己背書的證書。

  1. 將這份證書存放在你儲存證書的資料夾. 例如 $HOME/certs。
  2. 產生證書的雜湊碼。使用 openssl 工具: openssl x509 -noout -hash -in $HOME/certs/$cert_filename
  3. 在你儲存證書的資料夾中,建立一個指向證書的符號連結(或是另外複製一份),名稱為該證書的雜湊碼,再補上一個數字作為副檔名。
  4. 當你使用 openssl 工具時,加上 -CApath 選項告知你認可的證書儲存的資料夾。

舉例來說。強者我朋友給了我一份他自己背書的證書 john_cert.pem 。 如果我直接用 openssl 工具去查證它的可信度,openssl 會說這是一份自己背書的證書。

$ openssl verify john_cert.pem
john_cert.pem: ....
error 18 at 0 depth lookup: self signed certifcate
OK

我需要先透過自己的管道,確認這份證書的可信度,然後把它納入我認可過的證書管理資料夾中。 如果你曾經使用瀏覽器瀏覽某些 https 的網站,有時瀏覽器也會跳出提問是否接受此網站簽證的訊息,這其實也是在問你是否要認可該網站自己背書的證書。

1.向強者我朋友確認證書的真偽。

2.產生 john_cert.pem 的雜湊碼。(應該由強者我朋友告知)

$ openssl x509 -noout -hash -in john_cert.pem
de3389fd

3.將證書保存在你的證書管理資料夾中,並建立雜湊碼的符號連結(或複製)。

$ cp john_cert.pem $HOME/certs
$ cd $HOME/certs
$ ln -s john_cert.pem de3389fd.0

現在,我用 openssl 工具加上 -CApath 選項後,openssl 就會承認這份證書的可信度了。

$ openssl verify -CApath $HOME/certs  john_cert.pem
john_cert.pem: OK

參考 How do I verify a certificate?

建立 SOD

第一步、收集各文件的摘要

收集各文件的摘要,記錄於建立 ASN.1 文件所需的配置文件中。本文開頭就說 SOD 將儲存在 ASN.1 格式的文件中。 openssl 工具若要實現這項操作,必須提供一份配置文件給它。這份文件的格式必須符合 openssl 工具的期望,其內容則須描述此文件的結構與內容。

關於 ASN.1 配置文件的格式,請參考 OpenSSL::asn1parse(1)

sod_asn1.conf 是一個範例,它使用 sha1 摘要演算法,記錄了三份文件的摘要。還有,在 RFID 中儲存的文件,並不像一般電腦磁碟系統用文字名稱,而是用代號作為檔名,所以 sod_asn1.conf 中記錄的檔名都是數字。

sod_asn1.conf
asn1=SEQUENCE:sod_info

[sod_info]
version=INTEGER:1
algorithm=SEQUENCE:digest_method
digests=SEQUENCE:digest_set

[digest_method]
algorithm=OID:sha1

[digest_set]
1=SEQUENCE:digest1
2=SEQUENCE:digest2
3=SEQUENCE:digest13

[digest1]
file_no=INTEGER:1
digest=OCTETSTRING:D560D2D999F12923D92DBB1E5EE55232B4A4C1B1

[digest2]
file_no=INTEGER:2
digest=OCTETSTRING:E560D2D999F12923D92DBB1E5EE55232B4A4C1B1

[digest13]
file_no=INTEGER:13
digest=OCTETSTRING:F560D2D999F12923D92DBB1E5EE55232B4A4C1B1

sod_asn1.conf 所規劃的結構,以 JSON 表示法示意如下。

{
    version: 1,
    algorithm: {
        algorithm: sha1
    },
    digests: [
        {
            file_no: 1,
            digest: "...."
        },
        {
            file_no: 2,
            digest: "...."
        },
        {
            file_no: 13,
            digest: "...."
        }
    ]
}

注意,上述所列的結構只是本文案例所用的結構。每個案例都可以依照自己的需求定義自己 SOD 的結構,並沒有通用規範。

第二步、將摘要與摘要演算器保存於 ASN.1 文件中。

使用 openssl 工具的 asn1parse 選項就可以完成。須提供參數 -genconf 告知你的 ASN.1 文件的配置結構與內容。例如:

$ openssl asn1parse -genconf sod_asn1.conf -noout -out sod_asn1.der

使用參數 -in 則可以查看 ASN.1 文件的內容。例如:

# ps. read this ASN1 document:
$ openssl asn1parse -in sod_asn1.der -inform DER -i
蠟封 ASN.1 文件

以證書私鑰蠟封 ASN.1 文件 (蠟封後的文件格式為 PKCS7 文件)。蠟封後的 PKCS7 文件就是我們需要的 SOD 。將這份 SOD 一併存入 RFID ,就可以讓其他持有公鑰的人查驗 RFID 內容的完整性。

openssl 工具的使用方法參考 How do I sign a S/MIME message?

自動化工具

我們當然不可能每次都像前一節那樣手動操作。但是了解上述過程後,要寫一個建立 SOD 的工具就很簡單了。 sod_generate.php 就是一個用 PHP 語言撰寫的建立 SOD 的範例工具。

<?php
$certificate_filepath = 'mycert.pem';
$private_key_filepath = 'mycert_privkey.pem'; 
$private_key_password = '1234';

$asn1_conf_filepath = '/tmp/digests_asn1.conf';
$digests_filepath = '/tmp/digests.der'; // ASN1 format document
$sod_filepath = '/tmp/sod.dat'; // SMIME format document

$digest_method = 'sha256';
// See openssl_get_md_methods() to get available digest methods.

$digests = array();
/*
$content = file_get_contents('ef1.dat');
$digests[1] = strtoupper(openssl_digest($content, $digest_method));
$content = file_get_contents('ef2.dat');
$digests[2] = strtoupper(openssl_digest($content, $digest_method));
$content = file_get_contents('ef13.dat');
$digests[13] = strtoupper(openssl_digest($content, $digest_method));
*/
$digests[1] = strtoupper(openssl_digest('conetnt of EF1', $digest_method));
$digests[2] = strtoupper(openssl_digest('conetnt of EF2', $digest_method));
$digests[13] = strtoupper(openssl_digest('conetnt of EF13', $digest_method));

$asn1_conf_content = <<<ASN1_CONF_HEAD
asn1=SEQUENCE:sod_info

[sod_info]
version=INTEGER:1
algorithm=SEQUENCE:digest_method
digests=SEQUENCE:digest_set

[digest_method]
algorithm=OID:${digest_method}

[digest_set]

ASN1_CONF_HEAD;

$i = 1;
foreach ($digests as $k => $digest) {
    $asn1_conf_content .= "$i=SEQUENCE:digest$k\n";
    ++$i;
}

foreach ($digests as $k => $digest) {
    $asn1_conf_content .= "
[digest$k]
file_no=INTEGER:$k
digest=OCTETSTRING:$digest
";
}
//echo $asn1_conf_content;

file_put_contents($asn1_conf_filepath, $asn1_conf_content);
$output = system("openssl asn1parse -noout \
    -genconf $asn1_conf_filepath \
    -out $digests_filepath", $rc);

if ($rc) {
    echo $output;
    exit($rc);
}
echo "Ok. The ASN1 formated secure document is saved as $digests_filepath.\n";

// openssl smime -sign -in /tmp/digests.der -out /tmp/sod.dat  \
//     -signer mycert.pem -inkey privkey.pem -nocerts
$rc = openssl_pkcs7_sign($digests_filepath, // -in
    $sod_filepath, // -out
    'file://' . realpath($certificate_filepath), // -signer
    array('file://' . realpath($private_key_filepath), 
            $private_key_password), // -inkey
    array(),
    PKCS7_DETACHED | PKCS7_NOCERTS); // -nocerts

// NOTICE: filepath should be a full quality URL. 
//         like file:///home/rock/myperm.pem.

if ($rc) {
    echo "Success.\n";
    echo "The PKCS7 signed secure document is saved as $sod_filepath.\n";
}
else {
    echo "Failure.\n";
}
?>

查驗 SOD

再提醒一次,要查驗 SOD 前,你必須先取得發行者的公鑰。你可以用 openssl 工具查驗並取出內封的摘要記錄文件。例如:

$ openssl smime -verify -in sod.dat -CApath certs -certfile mycert.pem \
   -out unpacked_digests.der

前一節的 sod_generate.php 產生的 SOD ,也可以用下列的 sod_verify.php 查驗。

<?php
$certificate_filepath = 'mycert.pem'; //case verify
//$certificate_filepath = 'mycert2.pem'; //case not correct
$sod_filepath = '/tmp/sod.dat';

$unpacked_digests_filepath = '/tmp/unpacked_digests.der';

// openssl smime -verify -in sod.dat -CApath certs -certfile mycert.pem \
//  -out unpacked_digests.der
$rc = openssl_pkcs7_verify($sod_filepath, // -in
    0, 
    '/dev/null', // -out
    array('certs'), // -CAfile -CApath
    $certificate_filepath, // -certfile
    $unpacked_digests_filepath); // -out

if ($rc) {
    echo "Verify.\n";
    echo "The verified content is saved as $unpacked_digests_filepath.\n";

    $output = system("openssl asn1parse -i \
        -in $unpacked_digests_filepath -inform DER");

    echo $output;
}
else {
    echo "Not correct.\n";
}
?>

sod_verify.php 首先調用 PHP 函數 pkcs7_verify() 查驗並取出摘要內容(ASN.1 文件)。 然後再調用外部工具 openssl 顯示 ASN.1 文件的內容。

注意, PHP 函數 pkcs7_verify() 要求你的公鑰證書必須是可信的(Trusted)。在本文的案例中,你必須要先按照「如何管理認可的證書?」這節的內容認可這份公鑰證書。

查驗過程如下所示。

Verify.
The verified content is saved as /tmp/unpacked_digests.der.
    0:d=0  hl=3 l= 232 cons: SEQUENCE          
    3:d=1  hl=2 l=   1 prim:  INTEGER           :01
    6:d=1  hl=2 l=  11 cons:  SEQUENCE          
    8:d=2  hl=2 l=   9 prim:   OBJECT            :sha256
   19:d=1  hl=3 l= 213 cons:  SEQUENCE          
   22:d=2  hl=2 l=  69 cons:   SEQUENCE          
   24:d=3  hl=2 l=   1 prim:    INTEGER           :01
   27:d=3  hl=2 l=  64 prim:    OCTET STRING      :3315B46DC11FEBFF188...
   93:d=2  hl=2 l=  69 cons:   SEQUENCE          
   95:d=3  hl=2 l=   1 prim:    INTEGER           :02
   98:d=3  hl=2 l=  64 prim:    OCTET STRING      :BD231484C813F1C78F6...
  164:d=2  hl=2 l=  69 cons:   SEQUENCE          
  166:d=3  hl=2 l=   1 prim:    INTEGER           :0D
  169:d=3  hl=2 l=  64 prim:    OCTET STRING      :5D35444622F70AB896A...

這是本人 OpenSSL 系列文章的第四篇。下列為已完成的其他三篇:

至於如何用程式解析 ASN.1 文件的內容,則留待下一篇再說明。

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