Gmail などのWEBメールで適切に処理されない S/MIME受信メールに対して、Linux 上で改ざん・デジタル署名の確認などを行う
目次
- 背景
- smime.p7s とは
- smime.p7s を openssl コマンドで処理してみる(下調べ)
- 所感
- 追記 ( 2016/04/04 )
背景
表題の件に関係し、銀行から送られてくるメールの添付ファイル 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を使います。
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証明書部分を切り出す
- ダウンロードした eml ファイルの改行が
0d0a
なので、awk利用のため0a
に変換する - 最終行(付近)の multi-part 終了を示すハイフンを含む部分を削除する
- awk で、S/MIME証明書部分を切り取る
- 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
的に正しいのかなと思うのですが、正順(?)の場合も合わせてチェックすることにします。
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証明書チェックスクリプト(完成版)
例:成功
$ 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 は…)
"Google Apps" で DKIM+SPF 設定を行い、 Gmail は "送信ドメイン認証" チェックもできるわけですし、そもそもメールに限らず Google サービス内 (・間) のやり取りは google プライベートクラウドともいうべき中で行われているわけでして…囲い込まれ感が凄いですね。
社会が Google に取り込まれていくようです。
以上