heyheytower

日々のメモです。誰かのお役に立てれば幸いです。

Gmail などのWEBメールで適切に処理されない S/MIME受信メールに対して、Linux 上で改ざん・デジタル署名の確認などを行う

OpenSSL_logo.png

目次

背景

表題の件に関係し、銀行から送られてくるメールの添付ファイル smime.p7s にいつも疑問を感じていたため、何であるか調査を行い、Linux 上にて適切な処理ができるか確認しました。

smime.p7s とは

S/MIMEを扱えない電子メールソフトもあるため、"smime.p7m"という名の本文や、"smime.p7s"という名の添付ファイルに困惑する人が多い。

S/MIME - Wikipedia

S/MIME に対応していないメーラでは、S/MIME証明書を添付ファイルとして表示しているのですね。

S/MIME(エスマイム、Secure / Multipurpose Internet Mail Extensions)とは、MIMEカプセル化した電子メールの公開鍵方式による暗号化とデジタル署名に関する標準規格である。

S/MIME - Wikipedia

"公開鍵方式による" ということで、openssl*1コマンドでどうにかできそうです。

smime.p7s を openssl コマンドで処理してみる(下調べ)

自分は Gmail を使っているので、まずはsmime.p7sをダウンロードして処理してみます。

環境

$  cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=12.04
DISTRIB_CODENAME=precise
DISTRIB_DESCRIPTION="Ubuntu 12.04.5 LTS"
$ openssl version
OpenSSL 1.0.1 14 Mar 2012

証明書の有効性確認

openssl の smime コマンドで Verify も簡単にできるかと思ったのですが、一度デコードしないといけないようです。

How to handle OpenSSL and not get hurt using the CLI - GridWiki

RSA鍵、証明書のファイルフォーマットについて - Qiita

$ openssl pkcs7 -in smime.p7s -inform DER -print_certs -out /tmp/output.crt

そして、メールに添付された当該証明書の信頼性を、インストール済みのルート証明書から確認します。

・/usr/share/ca-certificates/ 以下にバラバラのファイルとして CA cert が置かれる
・/etc/ssl/certs/ にはそのファイルへのシンボリックリンクと c_rehash で生成されたファイルが置かれる
・上記のファイルを一つにまとめたもの(だと思う)が /etc/ssl/certs/ca-certificates.crt に置かれる

Debian の SSL 認証局証明書 (CA cert.) を最新に保つ - Lazy Diary @ はてな

openssl コマンドでのSSL証明書の検証 - なんだそのカオは -_-

openssl でSSL/TLSと戯れてみる - いますぐ実践! Linuxシステム管理 / Vol.252

$ openssl verify -verbose -x509_strict -CAfile /etc/ssl/certs/ca-certificates.crt /tmp/output.crt
/tmp/output.crt: OK 

返り値チェックで、成功・失敗が分かるかと思ったのですが、error 出力でも返り値0だったりしたので、"OK" という文字列出現を確認することとしました。

加えて、ここでエラーが発生する場合、後述するのですが一部の証明書で「サーバー証明書・中間証明書などを逆順に記載しているもの」があったため、その可能性を疑ってみると良いと思います。

補足 : SSL 認証局証明書の更新

自分の場合は、NG の場合のエラーメッセージで "ルート証明書が古い" 的なことを言われたので(内容はコピーし忘れました…)、下記を参考に証明書を更新しました。

Debian の SSL 認証局証明書 (CA cert.) を最新に保つ - Lazy Diary @ はてな

sudo update-ca-certificates --fresh

サーバー証明書の失効を確認する

下記にも記載があるが、サーバー証明書失効リスト (CRL) を使うのは現実的ではないので、OCSP(Online Certificate Status Protocol)*2を使います。

PKI基礎講座(5):証明書の有効性 - @IT

OCSPリクエストによるサーバー証明書の失効確認

ここではメール送信者の証明書だけを、その上位の認証局に失効していないか確認します。(手順が複雑になるため、証明書パス全体の確認は行わない。)

私が愛した openssl (PKI 編 その 2) - してみむとて

$ OCSP_URI=`openssl x509 -in /tmp/output.crt -noout -text | egrep ocsp | sed -e "s/.*\(http.*\)$/\1/"`
$ awk 'BEGIN{RS="";FS="\n"};{a[NR]=$0}END{print a[2]}' /tmp/outpu.crt > /tmp/intermediate.crt
$ openssl ocsp -issuer /tmp/intermediate.crt -cert /tmp/output.crt -url $OCSP_URI -resp_text -respout resp.der -no_nonce -CAfile /tmp/intermediate.crt
〜略〜
-----END CERTIFICATE-----
Response verify OK
/tmp/output_reverse.crt: good
        This Update: Mar 30 09:02:34 2016 GMT
        Next Update: Apr  6 09:02:34 2016 GMT

OCSP 記載が無い場合は失効確認は行わない。OCSP記載が無く、CRL記載だけのものは少ないと思いますし、運用してみて改修するか考えようと思います。

【別解】サーバー証明書失効リスト (CRL) を取得する

証明書に記載のある CRL をダウンロードし、それとの突き合わせを行います。

opensslによるサーバー証明書失効リスト (CRL) 確認 - IKB: 雑記帖

自堕落な技術者の日記 : OpenSSL 1.0.0 beta1 タイムスタンプ検証機能 - livedoor Blog(ブログ)

メール送信者の証明書中から CRL 配布ポイントを確認・ダウンロードを行い、デジタル署名の確認に合わせて CRL との突き合わせを行なっています。

mkdir ~/.crl
pushd ~/.crl
curl -O ` openssl x509 -noout -text -in output.crt | grep .crl | sed -e "s/.*\(http.*\.crl\)$/\1/" `
c_rehash .
popd
openssl verify -verbose -crl_check -x509_strict -CApath ~/.crl -CAfile /etc/ssl/certs/ca-certificates.crt /tmp/output.crt

送信元の確認

メールに記載された送信元と、下記コマンド実行で出力される証明書に記載された署名者アドレスを突き合わせます。

grep emailAddress /tmp/output.crt | sed -e "s/.*emailAddress=\(.*\)/\1/"

メール内容の改ざん有無の確認

これは下記の記事を参考にしましたが、簡単なようです。

openssl - smime.p7sからメッセージダイジェストを求めたい - スタック・オーバーフロー

Gmail で "メッセージのソースを表示" を行い、ここではgmail.emlと名前をつけて保存し、下記コマンドを実施します。

openssl smime -verify -in gmail.eml

改ざん無し

  • 返り値 : 0
  • 標準出力 : Verification successful

※メッセージボディの sha1 ハッシュを確認し改ざんを確認しているので、試しにメッセージヘッダを改ざんしても上記結果となります。

メッセージボディの改ざん有り

  • 返り値 : 4
  • 標準出力 : Verification failure
  • エラー出力 : 140465929549472:error:21071065:PKCS7 routines:PKCS7_signatureVerify:digest failure:pk7_doit.c:1158:
  • エラー出力 : 140465929549472:error:21075069:PKCS7 routines:PKCS7_verify:signature failure:pk7_smime.c:410:

電子証明書の期限切れ

  • 返り値 : 4
  • 標準出力 : Verification failure
  • エラー出力 : 139838430467744:error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error:pk7_smime.c:342:Verify error:certificate has expired

S/MIME証明書チェックスクリプトを作成

これまでの内容では、smime.p7sを操作していましたが、Gmail の "メッセージのソースを表示" からメールを保存したものは eml 形式なので、そこからS/MIME電子証明書をパースします。

Gmail からメールをローカルに保存する

いちいちメールを閲覧時に "メッセージのソースを表示" するのは面倒なので、メッセージのダウンロードを簡単にできるようにします。

メールを普通に表示している時のURLを下に記します。("YYYYYYYYYYYYYYYY" はおそらくユーザ毎のメールID)

https://mail.google.com/mail/u/0/?zx=XXXXXXXXXXXX#inbox/YYYYYYYYYYYYYYYY

メッセージのソースを表示した時のURLを下に記します。("ZZZZZZZZZZ" はおそらくユーザID)

https://mail.google.com/mail/u/0/?ui=2&ik=ZZZZZZZZZZ&view=om&th=YYYYYYYYYYYYYYYY

上記URL構造から、メール閲覧時に下記 JavaScript にてダウンロードを実施します。("ZZZZZZZZZZ" は適宜置き換えてください。)

javascript:(function(){var str=""+document.location;num=str.slice(-16);
    url="https://mail.google.com/mail/u/0/?ui=2&ik=ZZZZZZZZZZ&view=om&th="+num;
    var a = document.createElement('a');
    a.href = url;
  a.setAttribute('download', "gmail.eml");
  a.dispatchEvent(new CustomEvent('click'));
})()

JavaScript でのダウンロードについては、下記サイトを参考にさせていただきました。

javascriptからファイル保存ダイアログを出す - Qiita

改ざん確認

先程のテストしたコマンドの返り値から、改ざん判定を行います。

if `openssl smime -verify -in gmail.eml 1>/dev/null` ; then
    echo 'NoPolute: OK'
else
    exit 1
fi

emlファイルよりS/MIME証明書部分を切り出す

  1. ダウンロードした eml ファイルの改行が0d0aなので、awk利用のため0aに変換する
  2. 最終行(付近)の multi-part 終了を示すハイフンを含む部分を削除する
  3. awk で、S/MIME証明書部分を切り取る
  4. BASE64デコードを実行する
perl -pe 's/\r\n/\n/' gmail.eml | sed 's/------.*--//g' | awk 'BEGIN{RS="";FS="\n"};{a[NR]=$0}END{print a[NR]}' | base64 -d > /tmp/smime.p7s

上記手順により添付ファイルとして見えていたsmime.p7sと同じものが取り出せました。

切り出した S/MIME証明書の確認

ここでは確認として、取り出した/tmp配下のファイルと、ダウンロードしたsmime.p7sファイルのハッシュ値を比較します。

$ md5sum /tmp/smime.p7s
ca49c2ed84c7d108f23bc66157766e88  /tmp/smime.p7s
$ md5sum smime.p7s
ca49c2ed84c7d108f23bc66157766e88  smime.p7s

ダウンロードしたsmime.p7sファイルと同じものを、うまく eml ファイルから切り出せたようです。

証明書の Verify を行う (一部のS/MIME証明書など、サーバー証明書・中間証明書などを逆順に記載しているものにも対応)

いざサーバー証明書の Verify を行う段になりまして、一部 S/MIME証明書で Verify が失敗する事象に出くわしました。

これは "三菱東京UFJダイレクト" さんの S/MIME証明書の場合で生じた事象だったのですが、どうも連結されているサーバー証明書の順番が一般的なものと逆だったようです。

(ELB に)中間証明書とクロスルート証明書の連結する順番に注意 - tkuchikiの日記

SSLサーバー証明書に中間証明書を結合する [(全部俺)何でも Advent Calendar 2013 8日目] | maruTA(Bis5)'s Weblog – Side D:iary

したがいまして、連結されたサーバー証明書の中の証明書を逆順にしてチェックを行うのがopenssl的に正しいのかなと思うのですが、正順(?)の場合も合わせてチェックすることにします。

  1. S/MIME証明書のデコード
  2. サーバー証明書(中身は正順)の Verify
  3. 連結されたサーバー証明書の中の証明書を逆順にする
  4. サーバー証明書(中身は逆順)の Verify
openssl pkcs7 -in /tmp/smime.p7s -inform DER -print_certs -out /tmp/output.crt
openssl verify -verbose -x509_strict -CAfile /etc/ssl/certs/ca-certificates.crt /tmp/output.crt
cat /tmp/output.crt | awk 'BEGIN{RS="";FS="\n"};{a[NR]=$0}END{for(i=NR;i>0;i--)print a[i]"\n"}' > /tmp/output_reverse.crt
openssl verify -verbose -x509_strict -CAfile /etc/ssl/certs/ca-certificates.crt /tmp/output_reverse.crt
補足 : 証明書の中身を見てみる
openssl x509 -text -noout -in /tmp/output.crt

OCSPリクエストによるサーバー証明書の失効確認

OCSP記載があるか確認し、OCSPリクエスト結果の返り値で失効しているか判定しています。

OCSP_URI=`openssl x509 -in /tmp/output_reverse.crt -noout -text | egrep ocsp | sed -e "s/.*\(http.*\)$/\1/"`
if [ -n "$OCSP_URI" ]; then
    awk 'BEGIN{RS="";FS="\n"};{a[NR]=$0}END{print a[2]}' /tmp/output_reverse.crt > /tmp/intermediate.crt
    if `openssl ocsp -issuer /tmp/intermediate.crt -cert /tmp/output_reverse.crt -url $OCSP_URI -resp_text -no_nonce -CAfile /tmp/intermediate.crt 1>/dev/null` ; then
        echo 'Status  : OK'
    else
        echo 'Status  : NG'
    fi
    rm /tmp/intermediate.crt
else
    echo 'OCSP_URI: None'
fi

送信元の確認

証明書に記載されたアドレスと、メールに記載された送信元を突き合わせます。

FROM_ADDRESS=`egrep "^From:" $TARGET_MAIL | perl -pe 's/.*?([a-zA-Z0-9!$&\*\.=^\`|~#%\+\/?_{}\-]+@[a-zA-Z0-9_\-\.]+).*/$1/' | perl -pe 's/\r\n/\n/'`
CERT_ADDRESS=`grep emailAddress /tmp/output.crt | sed -e "s/.*emailAddress=\(.*\)/\1/" | perl -pe 's/\r\n/\n/'`
if [ $FROM_ADDRESS = $CERT_ADDRESS ] ; then
    echo 'Address : OK'
else
    echo 'Address : NG'
    echo "FROM_ADDRESS = $FROM_ADDRESS"
    echo "CERT_ADDRESS = $CERT_ADDRESS"
    exit 1
fi

S/MIME証明書チェックスクリプト(完成版)

gist.github.com

例:成功

$ smime gmail.eml
Verification successful
NoPolute: OK
Certify : OK
Response verify OK
Status  : OK
Address : OK

例:改ざんされている

$ smime gmail_replace.eml 
Verification failure
139750278760096:error:21071065:PKCS7 routines:PKCS7_signatureVerify:digest failure:pk7_doit.c:1158:
139750278760096:error:21075069:PKCS7 routines:PKCS7_verify:signature failure:pk7_smime.c:410:

例:証明書が古い

$ smime gmail.eml 
Verification failure
139844339644064:error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error:pk7_smime.c:342:Verify error:certificate has expired

例:送信者が署名者と違う

$ smime gmail_bad_sender.eml 
Verification successful
NoPolute: OK
Certify : OK
Response verify OK
Status  : OK
Address : NG
FROM_ADDRESS = notice@hogemail.ocn.ne.jp
CERT_ADDRESS = notice@infomail.ocn.ne.jp

所感

サーバ証明書まわりの知識があまり無かったので勉強になりました。そして、S/MIMEの世間での扱われ方もわかりましたし、在り方として普及が難しいのもわかりました。WEBメール、つまるところブラウザからローカルの資源にアクセスするという意味でもWEBメールとは相性が悪いですし、署名をGmailサーバ上でやる場合の秘密鍵の取り扱いは…などなど。

本件は、MUA(Mail User Agent)を利用すれば簡単に代替できるのですが*3、会社ならまだしも、プライベートではどこからでもアクセスできるWEBメールが楽なんですよね。そして、どうしてもE2E(End-to-End)で暗号化したい場合の手段ではS/MIMEも必要かもしれませんが、それなら今時ならメールでなくとも…という感じはありますね。

なにはともあれ、「smime.p7sを見て何だろうなと思わなくて良くなり、いざという時はそれを適切に処理できるようになった」というのが、精神安定上的な意味で一番の成果ですね。

追記 ( 2016/04/04 )

Gmail では、タイトルの件とは別に2011年位から "送信ドメイン認証" は行なっているようですね。(だから S/MIME は…)

メール認証 - Gmail ヘルプ

"Google Apps" で DKIM+SPF 設定を行い、 Gmail は "送信ドメイン認証" チェックもできるわけですし、そもそもメールに限らず Google サービス内 (・間) のやり取りは google プライベートクラウドともいうべき中で行われているわけでして…囲い込まれ感が凄いですね。

通信時のメールの暗号化(TLS) - Gmail ヘルプ

社会が Google に取り込まれていくようです。

以上