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

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

要發送 [S/MIME](http://en.wikipedia.org/wiki/S/MIME) 郵件至少必須有以下條件先成立：

-   取得一個 PKCS#12 憑證檔 ( \*.pfx )，這個憑證可以到相關單位申請「電子郵件專用」憑證
-   要在 ASP.NET 應用程式中使用憑證必須再參考我之前寫的幾篇文章：
    -   [如何正確安裝政府簽發的憑證到正式主機](/post/2009/04/27/A-certificate-chain-could-not-be-built-to-a-trusted-root-authority)
    -   [ASP.NET 使用 X509Certificate2 類別匯入憑證檔時發生錯誤](/post/2009/04/14/Importing-a-PFX-file-using-X509Certificate-from-ASPNET-fails)
    -   [CryptographicException: 控制碼無效(The handle is invalid)](/post/2009/04/17/CryptographicException-The-handle-is-invalid)

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

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

![在專案中加入組件參考：System.Security](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/c0f34a6a58472b72c54e-image_3.png "在專案中加入組件參考：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: 當簽章過的郵件收到時會出現一個「簽名者」列，並會附上一個「簽章圖示」](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/894c48e69e259bb4fa14-image_5.png "Outlook 2007: 當簽章過的郵件收到時會出現一個「簽名者」列，並會附上一個「簽章圖示」")

最後我要說明一下，使用 .NET 2.0 內建的郵件相關類別寄送有 [AlternativeView](http://msdn.microsoft.com/zh-tw/library/system.net.mail.alternateview.aspx) 的郵件時，在 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 類別都還是[有問題](/post/2008/02/05/SystemWebMail-too-weak-and-SystemNetMail-has-problem)，我也反應給微軟好久了 ( 一年多 )，也反應不止一次，依然是音訊全無，真不知道 .NET BCL 對於郵件的控制何時可以更強些。

**相關連結**

-   Sending Encrypted Emails With S/MIME Protocol
-   [X509Certificate2 類別](http://msdn.microsoft.com/zh-tw/library/system.security.cryptography.x509certificates.x509certificate2\(VS.80\).aspx)  
    表示 X.509 憑證。這個類別無法被繼承。
-   [ContentInfo 類別](http://msdn.microsoft.com/zh-tw/library/system.security.cryptography.pkcs.contentinfo\(VS.80\).aspx)  
    ContentInfo 類別表示 CMS/PKCS #7 標準文件中所定義的 CMS/PKCS #7 ContentInfo 資料結構。此資料結構為所有 CMS/PKCS #7 訊息的基礎。
-   [SignedCms 類別](http://msdn.microsoft.com/zh-tw/library/system.security.cryptography.pkcs.signedcms.aspx)  
    SignedCms 類別可以對 CMS/PKCS #7 訊息進行簽署和驗證。
-   [CmsSigner 類別](http://msdn.microsoft.com/zh-tw/library/system.security.cryptography.pkcs.cmssigner\(VS.80\).aspx)  
    CmsSigner 類別提供簽章功能。
-   [S/MIME - Wikipedia, the free encyclopedia](http://en.wikipedia.org/wiki/S/MIME)
-   [E-mail Client Testing for S/MIME Compliance](https://web.archive.org/web/20110119153537/http://www.ripe.net:80/db/support/security/mail_client_tests.html)
-   [Sending S/MIME encrypted email using C#](http://social.msdn.microsoft.com/Forums/en-US/netfxnetcom/thread/74e4711e-1f66-43a7-9e3b-bc9cfbcd1b73)
-   [S/MIME C# Examples](https://www.example-code.com/csharp/smime_verify_signature.asp)
-   [MIME PKCS7 Signature (S/MIME)](http://www.example-code.com/csharp/mime_pkcs7_signature.asp)
