.NET Framework で電子署名付きのメールを送信する

.NET Framework でメールを送信するときは SmtpClient を使うことになると思いますが、プロパティなどで簡単にメールに電子署名をつけたり暗号化したりすることはできません。そのため、フォーラムブログで議論されたり、3rd パーティ製品も販売されたりもしているようです。

先のフォーラムもブログも日本語を考慮していないようでしたので、日本語を扱えるように書いてみました。最後に全文を紹介しますが、実行は自己責任でお願いします。なるべく既存の SmtpClient を使用するため完ぺきではないですし、つぎはぎで書いており、例外処理もしていません。

まず初めに、日本語を扱うには文字コードの指定とエンコードが必要となるので、指定した文字コードの文字列を Base64 でエンコードするクラスを作成しておきます。MIME ヘッダーに使用するときは文字コードを示す必要があるので、単純にエンコードをする Encode と、EncodeWithCharecterCode の 2 つの関数を用意します。文字コードには iso-2022-jp や UTF-8 が使用できます。

public class MyBase64str
{
	private Encoding enc;
	private string characterCode;

	public MyBase64str(string CharacterCode)
	{
		enc = Encoding.GetEncoding(CharacterCode);
		characterCode = CharacterCode;
	}

	public string Encode(string str)
	{
		return Convert.ToBase64String(enc.GetBytes(str));
	}

	public string EncodeWithCharecterCode(string str)
	{
		return "?" + characterCode + "?B?" + Convert.ToBase64String(enc.GetBytes(str)) + "?=";
	}
}

それでは、さっそくメールを作っていきます。標準機能では実現できないので、MIME データを作る必要があります。まずは添付ファイルがない場合のパターンです。以下のような形で、MIME データを作ります。

StringBuilder buffer = new StringBuilder();
string MimePart;

buffer.Append("MIME-Version: 1.0rn");

if (IsBodyHtml)
{
	buffer.Append("Content-Type: text/html; charset="UTF-8"rn");
}
else
{
	buffer.Append("Content-Type: text/plain; charset="UTF-8"rn");
}

buffer.Append("Content-Transfer-Encoding: base64rnrn");
buffer.Append(Base64.Encode(Body));

MimePart = buffer.ToString();

もうすでに面倒ですが、今度は添付ファイルがある場合です。マルチ パートの MIME データになります。本文部分は大体同じなので紹介しません。変数の宣言部分を省略していますが、添付ファイルのデータを以下のように作ります。これを添付ファイルの回数だけ繰り返します。

FileInfo FileInfo = new FileInfo(FileName);
buffer.Append("--unique-boundary-1rn");
buffer.Append("Content-Type: application/octet-stream; file="=" + Base64.EncodeWithCharecterCode(FileInfo.Name) + ""rn");
buffer.Append("Content-Transfer-Encoding: base64rn");
buffer.Append("Content-Disposition: attachment; filename="=" + Base64.EncodeWithCharecterCode(FileInfo.Name) + ""rn");
buffer.Append("rn");
byte[] BinaryData = File.ReadAllBytes(FileInfo.FullName);

string Base64Value = Convert.ToBase64String(BinaryData, 0, BinaryData.Length);
int Position = 0;
while (Position < Base64Value.Length)
{
	int ChunkSize = 100;
	if (Base64Value.Length - (Position + ChunkSize) < 0)
		ChunkSize = Base64Value.Length - Position;
		buffer.Append(Base64Value.Substring(Position, ChunkSize));
		buffer.Append("rn");
		Position += ChunkSize;
	}
buffer.Append("rn");

MIME データが生成できたら署名します。使用する証明書の選択方法はのちほど紹介します。

byte[] Data = Encoding.ASCII.GetBytes(MimePart);
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();

署名したら、MailMessage の AlternateViews に設定します。

MemoryStream Stream = new MemoryStream(SignedBytes);
AlternateView View = new AlternateView(Stream, "application/pkcs7-mime; smime-type=signed-data;name=smime.p7m");
Message.AlternateViews.Add(View);

最後に、SmtpClient で送信して完了です。

SmtpClient Client = new SmtpClient("192.168.1.1", 25);
Client.UseDefaultCredentials = true;
Client.Send(Message);

署名に使用する証明書は、証明書ストアから選択するか、ファイルから読み込みます。証明書ストアから選択するのであれば、例えば以下のようになります。

X509Store Store = new X509Store(StoreLocation.CurrentUser);
Store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
X509Certificate2Collection Certs = Store.Certificates;
                
foreach (X509Certificate2 Cert in Certs)
{
	if (Cert.Subject.IndexOf("user01") >= 0)
	{
		Certificate = Cert;
		break;
	}
}

秘密鍵を含む証明書ファイルから読み込む場合は以下のようになります。

Certificate = new X509Certificate2("C:\cert.pfx", "password");

個別の説明は以上です。最後にサンプル コード全文を紹介します。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

namespace MyProgram
{
    class Program
    {
        static void Main(string[] args)
        {
            bool LoadCertificateFromStore = true;
            X509Certificate2 Certificate = null;

            if (LoadCertificateFromStore)
            {
                X509Store Store = new X509Store(StoreLocation.CurrentUser);
                Store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
                X509Certificate2Collection Certs = Store.Certificates;
                
                foreach (X509Certificate2 Cert in Certs)
                {
                    if (Cert.Subject.IndexOf("user01") >= 0)
                    {
                        Certificate = Cert;
                        break;
                    }
                }
            }
            else
            {
                Certificate = new X509Certificate2("C:\cert.pfx", "password");
            }


            MailAddress[] To = { new MailAddress("user02@contoso.com") };
            MailAddress From = new MailAddress("user01@contoso.com");
            string Subject = "電子署名付きメール";
            string Body = @"<html><body><p>このメールは<b>電子署名つき</b>です。</p></body></html>";
            string[] Attachments = { @"C:テスト1.txt", @"C:テスト2.txt" };

            SendEncryptedEmail(To, From, Subject, Body, Certificate, Attachments, true, "UTF-8");
        }

        public static void SendEncryptedEmail(MailAddress[] To, MailAddress From, string Subject, string Body, X509Certificate2 Certificate, string[] Attachments, bool IsBodyHtml, string CharacterCode)
        {
            MyBase64str Base64 = new MyBase64str(CharacterCode);

            MailMessage Message = new MailMessage();
            foreach (MailAddress Address in To)
            {
                Message.To.Add(Address);
            }
            Message.From = From;
            Message.Subject = Subject;

            StringBuilder buffer = new StringBuilder();
            string MimePart;

            if (Attachments != null && Attachments.Length > 0)
            {
                buffer.Append("MIME-Version: 1.0rn");
                buffer.Append("Content-Type: multipart/mixed; boundary=unique-boundary-1rn");
                buffer.Append("rn");
                buffer.Append("This is a multi-part message in MIME format.rn");
                buffer.Append("--unique-boundary-1rn");

                if (IsBodyHtml)
                {
                    buffer.Append("Content-Type: text/html; charset="" + CharacterCode + ""rn");
                }
                else
                {
                    buffer.Append("Content-Type: text/plain; charset="" + CharacterCode + ""rn");
                }

                buffer.Append("Content-Transfer-Encoding: base64rnrn");
                buffer.Append(Base64.Encode(Body));

                if (!Body.EndsWith("rn"))
                    buffer.Append("rn");
                buffer.Append("rnrn");

                foreach (string FileName in Attachments)
                {
                    FileInfo FileInfo = new FileInfo(FileName);
                    buffer.Append("--unique-boundary-1rn");
                    buffer.Append("Content-Type: application/octet-stream; file="=" + Base64.EncodeWithCharecterCode(FileInfo.Name) + ""rn");
                    buffer.Append("Content-Transfer-Encoding: base64rn");
                    buffer.Append("Content-Disposition: attachment; filename="=" + Base64.EncodeWithCharecterCode(FileInfo.Name) + ""rn");
                    buffer.Append("rn");
                    byte[] BinaryData = File.ReadAllBytes(FileInfo.FullName);

                    string Base64Value = Convert.ToBase64String(BinaryData, 0, BinaryData.Length);
                    int Position = 0;
                    while (Position < Base64Value.Length)
                    {
                        int ChunkSize = 100;
                        if (Base64Value.Length - (Position + ChunkSize) < 0)
                            ChunkSize = Base64Value.Length - Position;
                        buffer.Append(Base64Value.Substring(Position, ChunkSize));
                        buffer.Append("rn");
                        Position += ChunkSize;
                    }
                    buffer.Append("rn");
                }

                MimePart = buffer.ToString();
            }
            else
            {
                buffer.Append("MIME-Version: 1.0rn");

                if (IsBodyHtml)
                {
                    buffer.Append("Content-Type: text/html; charset="" + CharacterCode + ""rn");
                }
                else
                {
                    buffer.Append("Content-Type: text/plain; charset="" + CharacterCode + ""rn");
                }

                buffer.Append("Content-Transfer-Encoding: base64rnrn");
                buffer.Append(Base64.Encode(Body));

                MimePart = buffer.ToString();
            }

            byte[] Data = Encoding.ASCII.GetBytes(MimePart);
            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();

            MemoryStream Stream = new MemoryStream(SignedBytes);
            AlternateView View = new AlternateView(Stream, "application/pkcs7-mime; smime-type=signed-data;name=smime.p7m");
            Message.AlternateViews.Add(View);

            SmtpClient Client = new SmtpClient("192.168.1.244", 25);
            Client.UseDefaultCredentials = true;
            Client.Send(Message);
        }
    }

    public class MyBase64str
    {
        private Encoding enc;
        private string characterCode;

        public MyBase64str(string CharacterCode)
        {
            enc = Encoding.GetEncoding(CharacterCode);
            characterCode = CharacterCode;
        }

        public string Encode(string str)
        {
            return Convert.ToBase64String(enc.GetBytes(str));
        }

        public string EncodeWithCharecterCode(string str)
        {
            return "?" + characterCode + "?B?" + Convert.ToBase64String(enc.GetBytes(str)) + "?=";
        }
    }
}

なお、色々とテストしていただくと分かりますが、このコードは完ぺきではありません。1 行のサイズをチェックしていなかったり、受信したメールを Outlook で表示すると添付ファイルがなくても添付ファイルのアイコンが表示されたりします。完ぺきを目指すのであれば、SmtpClient や MailMessage などのクラスをベースに、SMTP の機能を自前で実装する必要がありそうです。