65.9K
CodeProject 正在变化。 阅读更多。
Home

用于发送签名和加密电子邮件的 S/MIME 库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (48投票s)

2009年8月24日

CPOL

8分钟阅读

viewsIcon

250286

downloadIcon

4990

根据 RFC 2633 发送签名和加密电子邮件

SignedEmail.png

概述

电子邮件(最简单的形式)本质上是不安全的。它没有内置加密,因此很容易在消息的网络传输过程中被窃听,而且它没有内置验证,因此很容易伪造消息,使其看起来像是来自其他人。如果你是一个淘气的大学生,你可以利用这些漏洞来阅读你室友的邮件,或者从你室友那里寄送假的爱信给走廊里那个阴沉的哥特女孩。但如果你是一个真正的罪犯,你可以利用这些漏洞来进行欺诈、工业间谍活动或叛国罪。S/MIME 通过提供一定的加密保证来解决这些问题,包括身份验证、消息完整性和隐私。

为什么要使用 S/MIME?

保证身份验证、消息完整性和隐私(并使其易于使用)实际上是一件非常困难的事情。在网上搜索“加密电子邮件”会找到诸如此解决方案之类的方案,但这些类型的应用程序往往存在相同类型的问题。

  1. 密钥分发:自制系统倾向于使用所有受信任方共享的单个对称密钥。但是,试图让受信任方知道密钥是什么就会导致鸡生蛋还是蛋生鸡的困境。没有秘密密钥就没有安全通道,这意味着你必须以不安全的方式将秘密密钥发送给所有受信任方。一个更好的系统将允许你在不预先分发任何秘密信息的情况下,以安全的方式协商通道。
  2. 密钥管理:单个共享密钥会创建两个类别的人。如果你知道密钥,你就被信任;如果不知道,你就不被信任。这是一个非常粗略的区别。这是一个虚构的场景。你有 5 个朋友都拥有共享密钥,而你正在为其中一个朋友策划一个惊喜派对。你想发送一条消息,除了你正在为其举办派对的那个人之外,所有人都能够阅读。单个共享密钥不允许这种区分。一个更好的系统将允许你根据用户逐个控制哪些用户可以解密加密的消息。
  3. 发件人身份验证:如果秘密密钥由,比如说,3 个人共享,那么你可以保证,如果你收到一条加密消息,它来自这 3 个人中的一个。但除此之外,你没有任何保证。如果 Alice、Bob 和 Charlie 都共享一个密钥,而 Alice 收到一条看起来来自 Bob 的加密消息,她可以确信一个不受信任的人(如 Dorothy)没有伪造该消息,但她**不能**确信一个受信任的人(如 Charlie)没有伪造它。一个更好的系统将提供强大的发件人身份验证,以确保**没有人**能够令人信服地伪造消息。
  4. 实现:自制系统往往会在实现中偷工减料。上面引用的示例解决方案是基于密码的,这几乎肯定意味着派生的加密密钥实际上不会像它们应该的那样强大(大多数密码的强度远不及 128 位加密密钥),并且它对初始化向量的处理不当。尽可能使用由真正了解自己在做什么的人设计的安全协议,而不是自己构建

S/MIME 的设计旨在解决所有这些问题。它是一个定义明确的标准(RFC 2633),并且已在所有主流邮件客户端中实现(尽管我只测试了 Outlook、Outlook Express 和 Mozilla Thunderbird)。但是,.NET **不**原生支持它。这就是这个库的用武之地。

假设

我将假定你已经了解了一些关于公钥密码学的一般知识,以及 S/MIME 的具体知识。我还将假定你已经拥有一个用于签名的证书(包含私钥),以及你想要发送加密邮件的任何人的加密证书(当然,没有私钥)。如果你和你的朋友还没有证书,它们很容易获得。我从Verisign获得了我的个人签名/加密证书(每年约 20 美元,价格非常便宜)。

Using the Code

对象模型的设计非常接近 System.Net.Mail 类,所以你应该觉得它很直观。主要区别仅在于证书处理。

如果你要发送一条签名消息,你需要创建一个带有签名证书的 SecureMailAddress。你的证书将存储在证书存储区,或者存储在一个文件(如 .pfx 文件)中。如果你从文件中读取证书,创建发件人 SecureMailAddress 的代码将如下所示

X509Certificate2 myCert = 
	new X509Certificate2(@"c:\certs\myCert.pfx", "SomeSecretPassword");

从证书存储区读取证书的工作量稍大一些,所以我包含了一个辅助方法来为你完成这项工作。只需指定序列号,它就会从你的本地存储区检索证书。

X509Certificate2 myCertFromStore = CryptoHelper.FindCertificate("1B37D3");

加载证书后,你需要将它们附加到 SecureMailAddress,如下所示

SecureMailAddress senderAddress = new SecureMailAddress
	("alice@cynicalpirate.com", "Alice", aliceEncryptionCert, aliceSigningCert);
 
SecureMailAddress recipientAddress = new SecureMailAddress
	("bob@cynicalpirate.com", "Bob", bobEncryptionCert);

请注意,我们为发件人指定了签名证书,但如果我们要发送签名消息,则只需要一个。对于所有收件人,我们只需要加密证书。另请注意,在发送加密消息时,我们需要指定发件人的加密证书,以及每个收件人的加密证书。如果我们不这样做,发件人将无法读取他自己的消息,即使是他发送的。

SecureMailMessage 类支持常规 System.Net.Mail.MailMessage 类的大部分功能。你可以抄送和密送收件人,设置回复地址,发送附件,发送 HTML 邮件。这是一个完整的示例,从头到尾演示了如何发送一条签名和加密的消息。

SecureMailMessage message = new SecureMailMessage();
 
// Look up your signing cert by serial number in your cert store
X509Certificate2 signingCert = CryptoHelper.FindCertificate("1B37D3");
// Look up your encryption cert the same way
X509Certificate2 encryptionCert = CryptoHelper.FindCertificate("22C590");
 
// Load the recipient's encryption cert from a file.
X509Certificate2 recipientCert = new X509Certificate2(@"c:\certs\bob.cer");
 
message.From = new SecureMailAddress
	("alice@cynicalpirate.com", "Alice", encryptionCert, signingCert);
message.To.Add(new SecureMailAddress
	("bob@cynicalpirate.com", "Bob", recipientCert));
 
message.Subject = "This is a signed and encrypted message";
 
message.Body = "<h2>Sent from the Cpi.Net.SecureMail library!</h2>";
message.IsBodyHtml = true;
 
message.IsSigned = true;
 
message.IsEncrypted = true;
 
// Instantiate a good old-fashioned SmtpClient to send your message
System.Net.Mail.SmtpClient client = 
		new System.Net.Mail.SmtpClient("mymailserver", 25);
 
// If your SMTP server requires you to authenticate, you need to specify your
// username and password here.
client.Credentials = new NetworkCredential("YourSmtpUserName", "YourSmtpPassword");
 
client.Send(message);

请注意,你可以使用常规的 System.Net.Mail.SmtpClient 类来发送消息。这是因为 SecureMailMessage 类有一个隐式转换运算符,可以将它转换为 System.Net.Mail.MailMessage 对象。

技术说明

  • 该库根据 RFC 2633 对消息进行签名、加密和格式化,但需要注意的是,我个人并未实现实际的加密签名过程。(我没有能力安全地实现加密算法。你也很可能没有。不要尝试。)加密过程由 .NET 2.0 中引入的 System.Security.Cryptography.Pkcs 命名空间中的类处理。谢谢微软,你们做了很多繁重的工作。
  • 据我所知,该库是 RFC 2633 的合规实现,但它不是一个**完整**的实现。它实现了 RFC 中的所有 **MUST**项,但不是所有的 **SHOULD**项。这意味着任何符合标准的邮件客户端都应该能够读取此库发送的消息。(已在 Outlook、Outlook Express 和 Mozilla Thunderbird 中进行测试。)
  • 我构建此库部分是通过阅读相关规范,部分是通过设置自制的 TCP 代理并窃听现有邮件客户端发送的流量。(TCP 代理尚未准备好投入使用,但可能会在未来的文章中出现。)规范中有一些地方允许各个实现拥有一定的自由度(例如,是先签名再加密,还是先加密再签名。规范允许任意嵌套的签名和加密层。)在这些情况下,我只是复制了现有实现的做法。(在此特定情况下,先签名,再加密。)
  • SecureMailMessage 的一个构造函数同时接受签名证书和加密证书。你可能有一个用于签名和加密的单一证书。(我的个人证书就是这样工作的。)将它们分成两个单独的参数是因为某些组织(如我的工作单位)会为签名和加密颁发单独的证书。这样,组织就可以保留你的加密证书的备份(以便在你辞职或去世时,或者他们只是觉得需要时,他们可以读取你的邮件),但他们**不**保留你的签名证书的备份(因为如果他们这样做了,那么一个恶意的系统管理员仍然可以以你的名义发送签名邮件,这违背了加密签名的整个目的)。如果你有单独的签名和加密证书,则将它们提供给相应的 SecureMailAddress 构造函数参数。但如果你只有一个,只需将其同时作为签名证书和加密证书提供。

个人笔记

  • 这是一个相当严肃的主题,与我职业上的工作密切相关,所以我比我以前的一些文章更认真地对待它,那些文章纯粹是为了好玩。(例如,里面没有玩具飞机死鱼。)抱歉。
  • 每当谈论密码学时,总会有一套标准的虚构用户(Alice、Bob、Charlie 等)在你的示例中引用。我承认我有时会觉得这些虚构人物比我自己的朋友和同事更亲近。(当然,除了 Mallory……我**非常**讨厌她!)

历史

  • 2009 年 8 月 23 日 - 初次发布
  • 2010 年 2 月 19 日 - 修复了一个阻止非 ASCII 字符在加密电子邮件中正确编码的错误
  • 2010 年 3 月 12 日 - 修复了一个在发送正文为空的消息时导致 NullReferenceException 的错误
  • 2010 年 7 月 14 日 - 修复了一个导致某些消息损坏的错误,如果任何行以句点开头。愚蠢的 SMTP 协议……
© . All rights reserved.