Postfixでオレオレ認証を作る

Postfix で SMTP 認証するには、Cyrus SASL と Dovecot SASL のどちらかを使用できる。 Cyrus SASL は、Postfix のビルド時に SASL ライブラリを組み込む必要があるが、Dovecot SASL は特殊なライブラリは必要ない。 Dovecot SASL はネットワーク通信で、通信相手として Dovecot サーバーが必要なのだが、シンプルなテキストベースのプロトコルなので、簡単に独自の認証プログラムを作ることもできる。

Dovecot 認証プロトコルについての詳細は https://doc.dovecot.org/developer_manual/design/auth_protocol/ に説明があるが、Postfix に対応するだけであれば、完全な実装は必要ない。

以下、Postfix がしゃべる Dovecot 認証プロトコルの説明。

プロトコル

接続

以下、クライアント(C)は Postfix(smtpd)、サーバー(S)は認証プログラム。

smtpd 起動時:

C: VERSION<tab>1<tab>0
C: CPID<tab>$pid
  • $pid : Postfix のプロセス番号。

応答

S: VERSION<tab>1<tab>$minor
S: MECH<tab>$name<tab>$params
S: SPID<tab>$pid
S: DONE
  • $minor
    • プロトコルのマイナーバージョン。無視されるので数値であればなんでもいい。なおメジャーバージョンは 1 でないと Postfix がエラーになる。
  • $name
    • SASL mechanism 名。PLAINLOGINCRAM-MD5 等。
  • $params
    • SASL mechanism の属性。有効な値は anonymous, plaintext, dictionary, active, forward-secrecy, mutual-authprivate は無視される。タブ区切りで複数指定可。 これは Postfix の smtpd_sasl_tls_security_options の値と関連する。たとえば、smtpd_sasl_tls_security_options=noplaintext の場合、plaintext 属性の mechanism は Postfix は使用しない(EHLO の応答にもでてこない)。
  • $pid
    • サーバープロセスのプロセス番号。無視される。

MECH は複数行可。MECH で返した名前が EHLO の応答の AUTH に現れる。

SPID は Postfix には無視されるだけなのでなくてもよい。けど、MECH よりも前に返すとエラーになる(これはプロトコルに合ってないような気がする…)。

認証

SMTP の AUTH 命令時:

C: AUTH<tab>$id<tab>$mech<tab>service=$service<tab>$params
  • $id : 接続識別子。AUTH 命令の度にインクリメントされる。応答の $id と一致しなければエラー。
  • $mech : SASL mechanism 名。
  • $service : Postfix の smtpd_sasl_service の値。

Postfix から渡される $params は次の通り:

  • nologin
    • なにこれ?謎…。
  • lip=$localip
    • SMTP 接続のサーバー側IPアドレス。
  • rip=$remoteip
    • SMTP 接続のクライアント側IPアドレス。
  • secured
    • SMTP 接続が TLS の場合に付加。
  • resp=$resp
    • SMTP AUTH 命令に引数があれば付加。たとえば AUTH PLAIN xxxx の場合は xxxx の部分。

応答

認証成功時:

S: OK<tab>$id<tab>user=$userid
  • $userid : ユーザー名

認証失敗時:

S: FAIL<tab>$id<tab>reason=$reason
  • $reason : 認証失敗の理由

認証のために続きのデータが必要な場合:

S: CONT<tab>$id<tab>$data
  • $data SMTP クライアントに 334 で送られるデータ。LOGIN 認証時のプロンプト文字列(Username:)等。

認証の続き

C: CONT<tab>$id<tab>$data
  • $id : AUTH 時の $id と同じもの。
  • $data : 認証に必要なデータ。

切断

特に命令はない。クライアント(Postfix)からネットワークを切断するだけ。

オレオレ認証

Ruby でオレオレ認証を作ってみる。

ネットワーク通信するデーモンプログラムを作って自前で起動する仕組みを作るのは面倒なので、Postfix に管理をまかせる。 spawn を使うと標準入出力を使って簡単に書ける(inetd みたいな感じ)。

etc/postfix/main.cf

smtpd_sasl_path = private/saslauth

/etc/postfix/master.cf

saslauth unix - n n - - spawn user=daemon argv=/usr/local/bin/saslauth.rb

/usr/local/bin/saslauth.rb

#!/usr/bin/ruby

def main
  gets  # VERSION
  gets  # CPID
  puts "VERSION\t1\t0"
  puts "MECH\tOREORE\tplaintext"
  puts "DONE"

  while line = gets
    cmd, id, mech, *args = line.chomp.split(/\t/)  # AUTH
    params = args.map{|arg| (arg.split(/=/, 2)+[nil])[0, 2]}.to_h
    case mech
    when 'OREORE'
      oreore(id, params)
    else
      puts "FAIL\t#{id}\treason=unknown mechanism"
    end
  end
end

# オレオレ認証
# ユーザー名だけで信じちゃう
def oreore(id, params)
  unless params['resp']
    puts "FAIL\t#{id}\treason=parameter required"
    return
  end
  user = params['resp'].unpack1('m')
  puts "OK\t#{id}\tuser=#{user}"
end

$stdout.sync = true
main

テスト

% nc localhost 25
220 servername.localdomain ESMTP Postfix
EHLO client
250-servername.localdomain
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-AUTH OREORE
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250 CHUNKING
AUTH OREORE aG9nZWhvZ2U=       ← "hogehoge" をBase64化したもの
235 2.7.0 Authentication successful
QUIT
221 2.0.0 Bye

うまくいった。