2009/08/30

PKI Research #4.5 - Digital Signature 再臨

最近要實作檔案傳輸的部分,由於一開始作使用者登入時,驗證的部分是在本機端 (Client) 作的,伺服器端 (Server) 根本無法得知真實性。所以當有人聲稱某甲要求檔案或傳輸檔案過來時,我們也不能肯定他就是本人。因此,有必要在伺服器端再作一遍。

數位簽章的實作,其實在研究 #4 中已經有了,但是主要是驗證簽章的部分,要在伺服器中執行。搭配接收 POST 上傳檔案的設計,我採用 PHP 來實作。查了一下 PHP 也有 OpenSSL 套件支援 (API),所以我只要使用它提供的函式就可以驗證簽章,關鍵函式 openssl_verify。

openssl_verify 能接受一些參數 (原訊息、數位簽章、公鑰) 來驗證簽章正確性,其中簽章演算法只支援到 SHA1,沒有先前研究的 SHA256,只好把簽章的部分也改用 SHA1 以配合。公鑰的部分,得先從憑證裡抽出來,這有 openssl_get_publickey 可以使用。

大概像這樣:
<?php
$message = 'test'; // Original Message

try {
    $dbh = new PDO('mysql:dbname=FAST;host=127.0.0.1', 'user', 'pass');
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

$sth = $dbh->prepare('SELECT `Public_Key` FROM XXX WHERE ID = ?');
$sth->bindParam(1, $tagID, PDO::PARAM_STR, 8);
$sth->execute();

$row = $sth->fetch();
$pubkeyid = openssl_get_publickey($row['Public_Key']);

// state whether signature is okay or not
$ok = openssl_verify($message, base64_decode($_POST['sigData']), $pubkeyid);
if ($ok == 1) {
    echo "good";
} elseif ($ok == 0) {
    echo "bad";
} else {
    echo "ugly, error checking signature";
}

// free the key from memory
openssl_free_key($pubkeyid);
?>

sigData 是數位簽章內容,為了傳輸方便已先行以 BASE64 編碼,所以這邊再解碼還原。

實際測試時,發現結果一直回傳 bad 即簽章不符,因為 C# 實作驗證都一切正常,所以我懷疑是不是 PHP 的問題,改換使用 JSP 來實作簽章驗證的部分,結果一樣失敗,不同的是 Java 跳出簽章結構錯誤的訊息,我開始懷疑起實作簽章的部分了。在 PHP 上使用 key 私鑰檔實作簽名,跑出來的東西跟 C# 的完全不一樣。C# 簽章驗證的結果,兩種結果都能通過驗證,不過 C# 的結果丟給 PHP 是失敗的,百思不得其解。

回頭想想,會不是會當初實作簽章的時候,想法錯了呢?還記得 .NET Framework 不許我們使用公鑰解密,取而代之的提供 Verify 方法,找看看 Bouncy Castle 有沒有提供類似的 Sign 方法,結果真的有。
public static byte[] signature(byte[] data){
try{
AsymmetricCipherKeyPair keyPair;
using(var reader = File.OpenText(@"D:\Programs\OpenSSL\user1.key")) { // Direct read pem format RSA private key file
keyPair = (AsymmetricCipherKeyPair) new PemReader(reader).ReadObject();
}

RsaDigestSigner rds = new RsaDigestSigner(new Sha1Digest());
rds.Init(true, keyPair.Private);
rds.BlockUpdate(data, 0, data.Length);
return rds.GenerateSignature();
}catch(Exception e){
throw new IOException("problem creating private key: " + e.ToString());
}
}

public static bool verify(byte[] message, byte[] sig){
var crt = new X509Certificate2(@"D:\Programs\OpenSSL\user1.crt"); // Read X509 Certificate file
var rsa = (RSACryptoServiceProvider) crt.PublicKey.Key; // Get public key

return rsa.VerifyData(message, "SHA1", sig);
}

Bouncy Castle 提供了 Org.BouncyCastle.Crypto.Signer 類別來處理數位簽章,RsaDigestSigner 就是給 RSA 公開金鑰演算法使用的數位簽章處理類別,由於 PHP 方的 Hash 演算法只支援到 SHA1,所以我簽章的部分也僅使用 SHA1。使用這個 Signer 類別不必自己手動 Hash 訊息,交給類別去作。

驗證簽章的部分跟上次基本上沒很大的差別,只是我也捨棄 VerifyHash 方法改使用 VerifyData 方法,直接把訊息 Hash 的動作交給了方法去處理,我完全不必實作相關 Hash 方法。

這次修改後,產生簽章的結果跟 PHP 的一模一樣,這樣就可以由 C#產生簽章,由 PHP 接收來驗證,進而確認使用者身份,伺服器端也可以利用 SESSION 暫時紀錄結果,就不必每次都要驗證。預設 30 分鐘 SESSION 會過期,到時候再驗證一次即可。

沒有留言:

張貼留言