dkfilterを使用した、PostfixでのDomainKeysへの対応

DomainKeysは、Yahooが提唱している送信者認証の技術です。 送信側は秘密鍵でメールに署名し、受信側はDNSで入手した公開鍵で 署名を検証する事によって、送信者の確認を行います。
今回はPostfix用のDomainKeysフィルタであるdkfilterを使用して、 PostfixでDomainKeysの署名および検証を行ってみます。

動作環境

今回使用したサーバのOSはFreeBSD 5.4-STABLEです。
PostfixやPerlが動作するならば、他のOSでも問題ないと思います。

今回使用したPostfixのバージョンは2.2.5です。
標準的なdkfilterの設定ではBefore Queue Content Filterを使用するので、 Postfixのバージョンはこの機能に対応した2.1以降である方がいいでしょう。
Before Queue Content Filterの代わりにAfter Queue Content Filterを 使うのならばPostfix-2.0でも構わないと思いますが、この場合、状況によっては 問題が起きる可能性があります。

dkfilterはPerlでかかれたスクリプトで、使用するためには以下のPerlの モジュールが必要になります。

モジュールの依存関係で、以下のモジュールも必要になるかもしれません。

今回はFreeBSDのportsを使用して、以下のportをインストールしました。

CPANシェルを使用して、自動でインストールする事もできます。 その時は以下のようにコマンドを実行すればいいでしょう。

  # perl -MCPAN -e shell
  cpan> install Crypt::OpenSSL::RSA
  cpan> install Mail::Address
  cpan> install MIME::Base64
  cpan> install Net::DNS
  cpan> install Test::More

dkfilterのインストール

ダウンロード

dkfilterの配布サイトからdkfilterをダウンロードします。
2005年8月27日時点での最新版はVersion 0.8です。

インストール

アーカイブを展開し、インストールを実施します。
今回は、/usr/local/dkfilterにインストールする事にしました。

  % tar zxvf dkfilter.tgz
  % cd dkfilter-0.8
  % ./configure --prefix=/usr/local/dkfilter
  % su
  # make install

dkfilter用のユーザおよびグループを作成します。
今回は、両方ともdkfilterにしました。

  # vi /etc/group
    グループdkfilterを追加する。
  # vipw
    ユーザdkfilterを追加する。

配布パッケージにはOS起動時の自動起動用スクリプト(init-script.sh)が付属していますが、 これはSuSE用に書かれた物のようで、FreeBSDではそのままでは使用できません。
今回は、とりあえずこのようなスクリプトを作成して、 /usr/local/etc/rc.dにコピーしました。

  #!/bin/sh

  DKFILTER_DIR=/usr/local/dkfilter
  KEYFILE=$DKFILTER_DIR/keys/selector1.key
  SELECTOR=selector1
  USERNAME=dkfilter

  if [ -x /usr/local/sbin/postconf ]; then
	  POSTCONF=/usr/local/sbin/postconf
  elif [ -x /usr/sbin/postconf ]; then
	  POSTCONF=/usr/sbin/postconf
  else
	  echo "postconf not found!!"
	  exit 1
  fi

  HOSTNAME=`$POSTCONF -h myhostname`
  DOMAINNAME=`$POSTCONF -h mydomain`

  INBOUND_PROG="$DKFILTER_DIR/bin/dkfilter.in"
  OUTBOUND_PROG="$DKFILTER_DIR/bin/dkfilter.out"
  INBOUND_ARGS="--hostname=$HOSTNAME 127.0.0.1:10025 127.0.0.1:10026"
  OUTBOUND_ARGS="--keyfile=$KEYFILE --selector=$SELECTOR --domain=$DOMAINNAME --method=nofws --headers 127.0.0.1:10027 127.0.0.1:10028"

  case "$1" in
	  start)
		  if [ -x $INBOUND_PROG ]; then
			  echo "Starting dkfilter-inbound."
			  su $USERNAME -c \
			      "$INBOUND_PROG $INBOUND_ARGS > /dev/null 2>&1 &"
		  fi
		  if [ -x $OUTBOUND_PROG ]; then
			  echo "Starting dkfilter-outbound."
			  su $USERNAME -c \
			      "$OUTBOUND_PROG $OUTBOUND_ARGS > /dev/null 2>&1 &"
		  fi
		  ;;
	  stop)
		  echo "Stopping dkfilter."
		  killall -u dkfilter perl
		  ;;
	  restart)
		  $0 stop
		  $0 start
		  ;;
	  *)
		  echo "Usage: $0 [ start | stop | restart ]"
		  exit 1
		  ;;
  esac

dkfilterの設定

dkfilterには受信(署名検証)用と送信(メール署名)用の二つのフィルタがあります。 受信用フィルタは、smtpポート(TCP:25)経由でのメールに、送信用フィルタは submissionポート(TCP:587)経由でのメール、およびメールサーバからsendmail コマンドを使用して送信したメールに対して適用する事にします。

受信用フィルタの設定(署名の検証)

まず、受信用フィルタの設定を行う事にします。
受信用フィルタはPostfixのBefore Qeueu Content Filterとして動作します。
Postfixからdkfilterへはポート10025、dkfilterからPostfixへはポート10026を使う事にしました。
まず、試験的にコマンドラインからdkfilterを起動してみます。

  # /usr/local/dkfilter/bin/dkfilter.in --hostname=[自ホスト名] 127.0.0.1:10025 127.0.0.1:10026 &

次に、Postfix側の設定を行います。
受信メールをdkfilterに渡すために、master.cfのsmtpサービスの設定を以下のように変更します。

  smtp      inet  n       -       n       -       -       smtpd
    -o smtpd_proxy_filter=127.0.0.1:10025
    -o smtpd_client_connection_count_limit=10

dkfilterから戻されるメールを受け取る為に、master.cfに以下の設定を追加します。

  localhost:10026 inet  n       -       n       -       -       smtpd
    -o smtpd_authorized_xforward_hosts=127.0.0.0/8
    -o smtpd_client_restrictions=
    -o smtpd_helo_restrictions=
    -o smtpd_sender_restrictions=
    -o smtpd_recipient_restrictions=permit_mynetworks,reject
    -o smtpd_data_restrictions=
    -o mynetworks=127.0.0.0/8
    -o receive_override_options=no_unknown_recipient_checks
    -o smtpd_use_tls=no               # TLS使用時のみ(なくても大丈夫なはずですが、念のため)

master.cfの修正を反映します。

  # postfix reload

これで、受信(署名検証)用フィルタの設定が出来ました。

設定が出来ましたので、テストを行います。
まず、通常のメール配送に問題がないかを確認する為に、自分のアドレス宛に メールを送信してみます。
送信方法は、普通にメールソフトを使ってもいいですし、以下の例のように telnetコマンドを使用してもいいでしょう。
ただし、sendmailコマンドやmailコマンド(大抵は内部でsendmailコマンドを呼ぶ) 等は使用できません。(smtpd経由ではないので意味がありません)

  % telnet 127.0.0.1 25
  Trying 127.0.0.1...
  Connected to localhost.
  Escape character is '^]'.
  220 mail.example.org ESMTP Postfix
  HELO localhost
  250 mail.example.org
  MAIL FROM:<user@example.org>
  250 Ok
  RCPT TO:<user@example.org>
  250 Ok
  DATA
  354 End data with <CR><LF>.<CR><LF>
  Subject: test

  test mail.
  .
  250 Ok: queued as 7BB371733A
  QUIT
  221 Bye
  Connection closed by foreign host.

メールが問題なく送信できましたら、届いたメールを確認します。

  Return-Path: <user@example.org>
  X-Original-To: user@example.org
  Delivered-To: user@example.org
  Received: from mail.example.org (localhost [127.0.0.1])
	  by mail.example.org (Postfix) with ESMTP id 7BB371733A
	  for <user@example.org>; Tue, 30 Aug 2005 17:41:58 +0900 (JST)
  Authentication-Results: mail.example.org; domainkey=neutral (unable to determine sender domain)
  Received: from localhost (localhost [127.0.0.1])
	  by mail.example.org (Postfix) with SMTP
	  for <user@example.org>; Tue, 30 Aug 2005 17:41:52 +0900 (JST)
  Subject: test
  Message-Id: <20050830084158.7BB371733A@mail.example.org>
  Date: Tue, 30 Aug 2005 17:41:58 +0900 (JST)
  From: user@example.org
  To: undisclosed-recipients:;

  test mail.

受信用フィルタが正常に動作していると、Authentication-Results:というヘッダが 付加されます。 このテストメールはDomainKeysでの署名がされていないため、domainkey=neutral (unable to determine sender domain)という結果が記録されています。

次に、署名検証が正常に行われるかを確認します。
手軽な方法としては、GmailYahoo!Mail等のDomainKeysに対応した メールサービスを利用するというのがあります。
これらのサービスのアカウントを持っている場合は、そこから自分宛にメールを 送信します。
もしアカウントを持っていなくても、Yahoo!Mailならば無料で取得できますし、 Gmailは利用者からの招待(invite)があればアカウントが取得できますので、 身近に利用者がいないか探してみましょう。

メールが送信できたら、届いたメールのヘッダを確認します。

  Return-Path: <domainkeys.test@gmail.com>
  X-Original-To: user@example.org
  Delivered-To: user@example.org
  Received: from mail.example.org (localhost [127.0.0.1])
	  by mail.example.org (Postfix) with ESMTP id AD19C28426
	  for <user@example.org>; Fri,  2 Sep 2005 13:58:07 +0900 (JST)
  Authentication-Results: mail.example.org from=domainkeys.test@gmail.com; domainkey=pass
  Received: from zproxy.gmail.com (zproxy.gmail.com [64.233.162.197])
	  by mail.example.org (Postfix) with ESMTP
	  for <user@example.org>; Fri,  2 Sep 2005 13:58:06 +0900 (JST)
  Received: by zproxy.gmail.com with SMTP id j2so221327nzf
	  for <user@example.org>; Thu, 01 Sep 2005 21:57:34 -0700 (PDT)
  DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
	  s=beta; d=gmail.com;
	  h=received:message-id:date:from:to:subject:mime-version:content-type:content-transfer-encoding:content-disposition;
	  b=DEIXKymTYiq2[〜略〜]I8NJI1uicxjU=
  Received: by 10.36.221.76 with SMTP id t76mr1787938nzg;
	  Thu, 01 Sep 2005 21:57:34 -0700 (PDT)
  Received: by 10.36.119.14 with HTTP; Thu, 1 Sep 2005 21:57:34 -0700 (PDT)
  Message-ID: <923fa33c05090121572d0845e6@mail.gmail.com>
  Date: Fri, 2 Sep 2005 13:57:34 +0900
  From: DomainKeys Test <domainkeys.test@gmail.com>
  To: user@example.org
  Subject: test
  Mime-Version: 1.0
  Content-Type: text/plain; charset=ISO-2022-JP
  Content-Transfer-Encoding: 7bit
  Content-Disposition: inline

  test

署名が検証され結果が問題ないと、Authentication-Results:ヘッダに domainkey=passと記録されます。

送信用フィルタの設定(メールの署名)

次に、送信用フィルタの設定を行います。
送信用フィルタは、PostfixのAfter Queue Content Filterとして動作します。 Postfixからdkfilterへはポート10027、dkfilterからPostfixへはポート10028を 使う事にしました。

まず、署名するための秘密鍵、およびDNSで公開する公開鍵を生成します。
生成する鍵は、DomainKeysで必ずサポートされているRSA鍵で、鍵長は1024ビットとします

  # cd /usr/local/dkfilter
  # mkdir keys
  # chown dkfilter:dkfilter keys
  # chmod 700 keys
  # cd keys
  # openssl genrsa -out selector1.key 1024
  Generating RSA private key, 1024 bit long modulus
  ..........................................................................................++++++
  .............................++++++
  e is 65537 (0x10001)
  # chown dkfilter:dkfilter selector1.key
  # chmod 400 selector1.key
  # openssl rsa -in selector1.key -pubout | grep -v "^--" | tr -d '\012' > pubkey.tmp
  writing RSA key

生成されたselector1.keyが秘密鍵で、pubkey.tmpが公開鍵になります。 pubkey.tmpは次のDNSへの登録が済んだ後は削除しても構いません。

次に、公開鍵をDNSに登録します。
公開鍵は、"セレクタ名._domainkey.example.org(自ドメイン名がexample.orgの場合) のTXTレコードとして登録します。
セレクタとは、一つのドメインで複数の鍵を使う時に、どの鍵を使っているか判別 するために付ける名前で、DNSおよびメールヘッダの制限の範囲内で自由に決められます。
例えば、BINDでセレクタ名selector1の公開鍵を登録する場合は、次の設定を example.orgのゾーンデータに追加します。

  selector1._domainkey  IN  TXT  "k=rsa; p=pubkey.tmpの内容; t=y"

k=rsaというのは、公開鍵の種類がRSA鍵である事を表します。
p=というのが公開鍵の本体で、pubkey.tmpの内容をここに記述します
t=yというのは、この公開鍵はテストモードで使用している事を表します。 テストモードの公開鍵での署名検証が失敗しても、メールを拒絶したりしない事に なっています。

秘密鍵を生成したら、試験的にコマンドラインから送信用フィルタを起動してみます。

  # dkfilter.out \
      --keyfile=/usr/local/dkfilter/keys/selector1.key \
      --selector=selector1 \
      --domain=[ドメイン名] \
      --method=nofws \
      --headers \
      127.0.0.1:10027 127.0.0.1:10028 &

次に、Postfixの設定を行います。 送信用フィルタ関連では、以下の修正をmaster.cfに行います。

まずは、submissionポートに送られて来たメールを送信用フィルタに渡すために、 submissionの設定を以下のように変更します。(submissionがない場合は追加してください)
この例では、SASL認証は必須としています。SASLの設定は PostfixのぺーじSMTP-AUTH対応版インストール記録 などを参考にして適宜行ってください。

  submission inet n       -       n       -       -       smtpd
    -o smtpd_sasl_auth_enable=yes
    -o smtpd_sasl_security_options=noplaintext,noanonymous
    -o broken_sasl_auth_clients=yes
    -o smtpd_client_restrictions=
    -o smtpd_helo_restrictions=
    -o smtpd_sender_restrictions=
    -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
    -o smtpd_data_restrictions=
    -o smtpd_etrn_restrictions=reject
    -o content_filter=dksign:[127.0.0.1]:10027

sendmailコマンドを使用しての送信時にも署名するために、pickupサービスを 以下のように変更します。

  pickup    fifo  n       -       n       60      1       pickup
    -o content_filter=dksign:[127.0.0.1]:10027

dkfilterへメールを渡すためのトランスポートであるdksignを定義するため、 以下の設定を追加します。

  dksign    unix  -       -       n       -       10      smtp
    -o smtp_send_xforward_command=yes
    -o smtp_use_tls=no                # TLS使用時のみ

署名されたメールをdkfilterから受け取るため、以下の設定を追加します。

  localhost:10028 inet n  -       n       -       10      smtpd
    -o smtpd_authorized_xforward_hosts=127.0.0.0/8
    -o smtpd_client_restrictions=
    -o smtpd_helo_restrictions=
    -o smtpd_sender_restrictions=
    -o smtpd_recipient_restrictions=permit_mynetworks,reject
    -o smtpd_data_restrictions=
    -o mynetworks=127.0.0.0/8
    -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks
    -o content_filter=
    -o smtpd_use_tls=no               # TLS使用時のみ

上記のmaster.cfの修正を行ったら、設定を反映する為にreloadを行います。

  # postfix reload

これで、送信(署名)用フィルタの設定が出来ました。

送信用フィルタが正常に動作するか確認するため、GmailやYahoo!Mailのアカウントに テストメールを送ってみます。 submissionポートからのメールのみ署名するようにしているので、 メールソフトの送信用ポートを587に変更するのを忘れないでください。

メールが送信できたら、送信先に届いたメールのヘッダを確認します。

Gmailの場合は、"詳細オプション" -> "オリジナルを表示" でヘッダ部分も表示できます。

  〜略〜
  Received-SPF: pass (gmail.com: best guess record for domain of user@example.org designates xxx.xxx.xxx.xxx as permitted sender)
  DomainKey-Status: good (test mode)
  Received: from mail.example.org (localhost [127.0.0.1])
      by mail.example.org (Postfix) with ESMTP id 43A4162D01;
      Sat,  3 Sep 2005 03:41:54 +0900 (JST)
  DomainKey-Signature: a=rsa-sha1; h=Received:Date:From:To:Subject:Message-Id:X-Mailer:Mime-Version:Content-Type:Content-Transfer-Encoding; b=m31f[〜略〜]a3U=; c=nofws; d=example.org; q=dns; s=selector1
  Received: from mail.example.org (localhost [IPv6:::1])
      (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
      (No client certificate requested)
      by mail.example.org (Postfix) with ESMTP id 7ADA362D00;
      Sat,  3 Sep 2005 03:41:51 +0900 (JST)
  Date: Sat, 03 Sep 2005 03:41:52 +0900
  〜略〜

DomainKey-Signature:ヘッダが付加されている事、およびDomainKey-Status:ヘッダに goodと記録されている事を確認します。
Gmailは古い仕様のDomainKeysに従ったソフトを使っているようで、 Authentication-Results:ヘッダではなくDomainKey-Status:ヘッダに 検証結果が記録されます。

Yahoo!Mailの場合は、"詳細ヘッダ"で全ヘッダの表示ができます。

  〜略〜
  Authentication-Results: mta44.mail.bbt.yahoo.co.jp from=example.org; domainkeys=pass (ok)
  X-Originating-IP: [xxx.xxx.xxx.xxx]
  Return-Path: 
  Received: from xxx.xxx.xxx.xxx (EHLO mail.example.org) (xxx.xxx.xxx.xxx)
      by mta44.mail.bbt.yahoo.co.jp with SMTP;
      Sun, 28 Aug 2005 11:02:32 +0900
  Received: from mail.example.org (localhost [127.0.0.1])
      by mail.example.org (Postfix) with ESMTP id 4B4571729B;
      Sun, 28 Aug 2005 11:02:32 +0900 (JST)
  DomainKey-Signature: a=rsa-sha1; h=Received:Date:From:To:Subject:Message-Id:X-Mailer:Mime-Version:Content-Type:Content-Transfer-Encoding; b=Y4mt[〜略〜]Z5k=; c=nofws; d=example.org; q=dns; s=selector1
  Received: from mail.example.org (localhost [IPv6:::1])
      (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
      (No client certificate requested)
      by mail.example.org (Postfix) with ESMTP id 7FF6017260;
      Sun, 28 Aug 2005 11:02:31 +0900 (JST)
  Date: Sun, 28 Aug 2005 11:02:29 +0900
  〜略〜

DomainKey-Signature:ヘッダが付加されている事、およびAuthentication-Results: ヘッダにdomainkeys=passと記録されている事を確認します。

問題点

DomainKeysを設定、運用しているうちにいくつか問題に出会う事があると思います。
例えば、以下のような問題が考えられます。

複数の鍵の対応
現在のバージョンのdkfilterの送信用フィルタでは、複数の鍵が扱えません。
そのため、ドメインによって署名に使う鍵を変えるという事ができません
(複数のドメインで一つの鍵を共用する事はできます。その場合は送信用フィルタの ドメインの指定で、--domain=example.org,example.comのように指定します)
対処としては、使用する鍵毎に送信用フィルタを起動し、submissionポートの smtpdで送信者によって振り分けるという方法が考えられます。
署名検証の失敗の扱い
現状ではさまざまな理由により、署名の検証が失敗する事があります。
例えば以下のような事が考えられます。 現状では、検証結果を元にした受信の拒否は行わない方が無難でしょう。
同じ理由により、公開鍵はテストモードで登録する方が無難だと思います。

TODO

この文書はまだ未完成です。 以下のような事についても、そのうちに書きたいと思います。

連絡先

ご意見、ご感想等が有りましたら、sue-nospam-dkfilter@nospam.postfix.jpまでメールを下さい。
スタイルシート未対応ブラウザ(もしくはスタイルシートを無効にした状態)では、 上記アドレスのspam避け部分も表示されてしまいます。
その場合は、上記アドレスのローカルパートから-nospam-dkfilterを、 ドメイン部からnospam.を削除したアドレス宛にメールを下さい
設定方法に関する質問等は、 Postfix-JP メーリングリスト の方がいいかも知れません。(私も読んでいますので、出来る限り答えるようにします)