The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何透過 .NET 送出一個包含 S/MIME 簽章的郵件

加入憑證簽章的郵件必須使用 S/MIME 標準,之前為了讓系統發出的郵件可以加入憑證簽章費了好一番功夫,有鑑於此需求非常罕見,在國內我是沒找到任何相關資料,即便在國外的網站相關資訊也很少,所以特此紀錄一下當時研究的過程。

要發送 S/MIME 郵件至少必須有以下條件先成立:

然後就可以開始寫程式了,共有 7 大步驟:

1. 先在專案中加入組件參考:System.Security

在專案中加入組件參考:System.Security

2. 設定 using 以下兩個命名空間

using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;

3. 準備郵件內容

string EmailBody = "XXXXX";

StringBuilder sb = new StringBuilder();

sb.AppendLine("Content-Type: text/html; charset=\"big5\"");
sb.AppendLine("Content-Transfer-Encoding: 8bit");
sb.AppendLine();
sb.AppendLine(EmailContent.ToString());

byte[] data = Encoding.GetEncoding("Big5").GetBytes(sb.ToString());

4. 替郵件內容做簽章 ( 重點程式 )

   4.1 一般 .Net 的寫法

string pfxPath = "test.pfx";
string pfxPassword = "test";
X509Certificate2 certificate = new X509Certificate2(pfxPath, pfxPassword);

ContentInfo content = new ContentInfo(data);
SignedCms signedCms = new SignedCms(content, false);
CmsSigner signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, certificate);
signedCms.ComputeSignature(signer);
byte[] signedbytes = signedCms.Encode();

   4.2 在 ASP.NET 中的寫法 ( 憑證必須先匯入本機憑證儲存區 )

X509Store store = new X509Store("My", StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

X509Certificate2 signCert = store
    .Certificates.Find(X509FindType.FindBySubjectName, "憑證名稱", false)[0];

SignedCms signedCms = new SignedCms(new ContentInfo(data), false);
CmsSigner signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, signCert);
signedCms.ComputeSignature(signer);
byte[] signedbytes = signedCms.Encode();

5. 準備郵件內容(MailMessage)

MailMessage msg = new MailMessage();
msg.From = new MailAddress("test@example.com");
msg.To.Add(new MailAddress("test@example.com"));
msg.Subject = "test s/mime";
// 千萬不要加上 msg.Body 內容,否則驗證 S/MIME 郵件時會失敗
// msg.Body = EmailBody;

6. 將簽章的內容加入到訊息的另一個 View 中 ( AlternateView ),這樣可以讓「可以讀取 S/MIME 郵件」的人開啟 S/MIME 郵件檢視,也可以讓「無法讀取 S/MIME 郵件」的人也可以看到郵件。

MemoryStream ms = new MemoryStream(signedbytes);
AlternateView av = new AlternateView(ms, 
    "application/pkcs7-mime; smime-type=signed-data;name=smime.p7m");
msg.AlternateViews.Add(av);

7. 發出郵件

SmtpClient client = new SmtpClient("localhost", 25);
client.UseDefaultCredentials = true;
client.Send(msg);

都撰寫完成並測試成功後,所收到的郵件會呈現如下圖 ( 以 Outlook 2007 為例 ):

 Outlook 2007: 當簽章過的郵件收到時會出現一個「簽名者」列,並會附上一個「簽章圖示」

最後我要說明一下,使用 .NET 2.0 內建的郵件相關類別寄送有 AlternativeView 的郵件時,在 msg.Body 屬性一定只能用「純文字」當郵件內容,就算你將 msg.IsBodyHtml 設定為 true,在加入 AlternativeView 之後,當讀取郵件的工具對 AlternativeView 無法解析或顯示時,還是只會顯示成純文字,這點問題算是 .NET 2.0 內建的一個小瑕疵吧,真的要發進階 MIME 格式的郵件,我想只能買元件,否則就要自己寫了。

若要利用 .NET 發送含有 S/MIME 簽章的郵件絕對不能設定 msg.Body 的內容加上任何附件檔案,否則簽章永遠會失敗,進而導致在支援 S/MIME 協定的讀信軟體(如:Outlook)無法正確驗證郵件,如果在 Outlook 中就會發現根本看不到任何「簽章圖示」,因為 S/MIME 簽章的目的就是確保郵件內容不被竄改,但基於  .NET 郵件類別的限制,要發送具有附件檔案的 S/MIME 郵件應該是不太可能的。

我好幾次都有這樣的衝動想自己寫一組 Mail Library,只是這真的是件大工程,最終還是妥協。目前在 .NET 1.1 與 .NET 2.0 內建的 Mail 類別都還是有問題,我也反應給微軟好久了 ( 一年多 ),也反應不止一次,依然是音訊全無,真不知道 .NET BCL 對於郵件的控制何時可以更強些。

相關連結