SendGrid SMTP API を使って S/MIME 署名されたメールを送る

メール送信サービスと言えば SendGrid ですね.説明する必要がないほど有名だと思います.

いくつかの種類の API が提供されています.

一般的には Web API を使うかと思います.
ただ、残念なことに Web API では S/MIME による署名が使えません.
DKIM による署名のみ可能です.

S/MIME と DKIM は署名の有効範囲が異なります.

総務省 標的型攻撃に対抗するための通信規格の標準化動向に関する調査結果 より抜粋

セキュリティ面を考えれば S/MIME による署名の方が優れていると言えます.

では、SendGrid には S/MIME 署名を使う方法は無いのかというと、ちゃんと用意されています.それが SMTP API です.
要は SMTP サーバーの代わりを SendGrid にやってもらおうという訳です.

しっかりとしたサンプルも用意されています(ちょっと見つけ辛いかな).

このサンプルを元に、S/MIME 署名を行ったメールを SendGrid SMTP API を用いて送ってみました.

build.gradle

build.gradle は以下のようになります.

apply plugin: 'eu.appsatori.fatjar'
apply plugin: 'java'
apply plugin: 'findbugs'
apply plugin: 'pmd'

version = "0.0.3"

def defaultEncoding = 'UTF-8'
[compileJava, compileTestJava]*.options*.encoding = defaultEncoding

repositories {
  mavenCentral()
}

buildscript {
  dependencies {
    classpath 'eu.appsatori:gradle-fatjar-plugin:0.3' // adds fatJar task
  }
  repositories {
    jcenter()
  }
}

dependencies {
  compile 'javax.mail:javax.mail-api:1.5.3'
  compile 'com.sun.mail:javax.mail:1.5.3'
  compile 'com.sendgrid:smtpapi-java:1.2.0'
  compile 'net.markenwerk:utils-mail-smime:1.0.8'
}

// adds 'with-dependencies' to the fatJar name
fatJar {
  classifier 'jar'
  baseName "sendgrid-java-example"
  manifest {
    attributes("Implementation-Title": "SendgridJavaExample", "Implementation-Version": version)
  }
}

// copy fatJar to base project directory so they will be in git (and on github for download)
build << {
  copy {
    println "Copying ${fatJar.archiveName} to $projectDir/repo/com/github/sendgrid-java-example/$version"
    from("$buildDir/libs/${fatJar.archiveName}")
    into("$projectDir/repo/com/github/sendgrid-java-example/$version")
  }
}

artifacts {
  archives fatJar
}


tasks.withType(FindBugs) {
    reports {
        xml.enabled = false
        html.enabled = true
    }
 }

tasks.withType(Pmd) {
    reports {
        xml.enabled = false
        html.enabled = true
    }
 }

ほぼサンプル通りですが、S/MIME 署名用に以下のライブラリを追加しています.

compile 'net.markenwerk:utils-mail-smime:1.0.8'

以上が build.gradle でした.

 

Java ソースコード

package com.github.sendgridjp;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
import java.security.Security;
import java.security.cert.CertificateException;
import java.util.Properties;

import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import com.sendgrid.smtpapi.SMTPAPI;

import net.markenwerk.utils.mail.smime.SmimeKey;
import net.markenwerk.utils.mail.smime.SmimeKeyStore;
import net.markenwerk.utils.mail.smime.SmimeUtil;

public class JavaMailTextExample {

	// 設定情報
	static final String USERNAME = "USER_NAME";
	static final String PASSWORD = "PASSWORD";
	static final String[] TOS = { "TO_ADDRESS1" };
	static final String[] NAMES = { "NAME1" };
	static final String FROM = "email@project-respite.com";
	static final String CHARSET = "UTF-8"; // "UTF-8";
	static final String ENCODE = "base64"; // "base64"; // "quoted-printable";

	public static void main(String[] args)
			throws MessagingException, UnsupportedEncodingException, FileNotFoundException, CertificateException {

		Security.addProvider(new BouncyCastleProvider());

		// 署名用
		SmimeKeyStore smimeKeyStore = new SmimeKeyStore(new FileInputStream("smime.pfx"),
				"PASSWORD".toCharArray());
		SmimeKey smimeKey = smimeKeyStore.getPrivateKey("aa33520e355b40b5517398376109f95cae4c2871",
				"PASSWORD".toCharArray());

		// SMTP接続情報
		Properties props = new Properties();
		props.put("mail.transport.protocol", "smtps");
		props.put("mail.smtp.host", "smtp.sendgrid.net");		
		props.put("mail.smtp.port", "587");
		props.put("mail.smtp.starttls.enable", "true");
		props.put("mail.smtp.starttls.required", "true");
		props.put("mail.smtp.auth", "true");
		Authenticator auth = new SMTPAuthenticator();
		Session mailSession = Session.getDefaultInstance(props, auth);
		mailSession.setDebug(true); // console log for debug

		// メッセージの構築
		MimeMessage message = new MimeMessage(mailSession);

		// ダミーの宛先(X-SMTPAPIの宛先が優先される)
		message.addRecipient(Message.RecipientType.TO, new InternetAddress("email@project-respite.com"));

		// From
		message.setFrom(FROM);

		// Subject
		message.setSubject("こんにちはSendGrid", CHARSET);

		// Body
		String body = "こんにちは、nameさん\r\nようこそ〜テキストメールの世界へ!";
		message.setText(body, CHARSET, "plain");
		message.setHeader("Content-Transfer-Encoding", ENCODE);

		// X-SMTPAPIヘッダ
		String smtpapi = createSmtpapi(TOS, NAMES);
		smtpapi = MimeUtility.encodeText(smtpapi);
		message.setHeader("X-SMTPAPI", MimeUtility.fold(76, smtpapi));

		// 暗号化
		X509Certificate certificate = smimeKey.getCertificate();
		message = SmimeUtil.encrypt(mailSession, message, certificate);

		// 署名
		message = SmimeUtil.sign(mailSession, message, smimeKey);

		// 送信
		mailSession.getTransport().send(message);
	}

	// X-SMTPAPIヘッダに設定する値の生成
	private static String createSmtpapi(String[] tos, String[] names) {
		SMTPAPI smtpapi = new SMTPAPI();
		smtpapi.setTos(tos);
		smtpapi.addSubstitutions("name", names);
		smtpapi.addCategory("category1");
		return smtpapi.rawJsonString();
	}

	private static class SMTPAuthenticator extends javax.mail.Authenticator {
		public PasswordAuthentication getPasswordAuthentication() {
			return new PasswordAuthentication(USERNAME, PASSWORD);
		}
	}
}

これも大枠はサンプル通りです.
署名の部分を抜粋します.

SmimeKeyStore smimeKeyStore = new SmimeKeyStore(new FileInputStream("smime.pfx"),
		"PASSWORD".toCharArray());
SmimeKey smimeKey = smimeKeyStore.getPrivateKey("aa33520e355*********cae4c2871",
		"PASSWORD".toCharArray());

.pfx 鍵を読み込み SmimeKeyStore オブジェクトを生成します.

message = SmimeUtil.sign(mailSession, message, smimeKey);

署名は SmimeUtil#sign で、引数には Session, MimeMessage, SmimeKeyStore を指定します.

X509Certificate certificate = smimeKey.getCertificate();
message = SmimeUtil.encrypt(mailSession, message, certificate);

ちなみに S/MIME による暗号化も行えます.引数には Session, MimeMessage, X509Certificate を指定します.


Gmail で見ると署名はこんな感じです.
DKIM と S/MIME の署名者が違うので不正っぽくなっていますが、両方で署名できている証拠です.
(確認のため、意図的にこうしています)

 

まとめ

今回は SendGrid の SMTP API を使って S/MIME 署名のメールを送ってみました.
どうしても S/MIME での署名が必要になったときなど、ご活用ください.

以上です.