SVX日記
2023-08-11(Fri) コンテナ上にリモートデスクトップ環境の構築に成功
だいぶ間が開いてしまったが、東北から戻ってから、自宅の外壁工事が始まったり、ロードスターを外の駐車場に退避しておいたらブツけられたり、筋トレを緩めたせいなのか体が緩んだり、なんやかんやで、どうも気分的に不安定になっていたりする。
男性にも更年期があるとかで、そのせいなのか、自宅やクルマというテリトリが侵され気味のせいなのか、仕事の環境が微妙なせいなのか、暑い中を動きすぎて塩分が足りてないのか、どうも気分は曇り空である。時々、晴れ間が覗くような、そうでもないような。
何かに取り組んでいないと気がすまない性格なのに、どうも取り組む気が起きず、ダラダラとゲームしたりしてしまう。ゲームが悪いわけではないのだが、そういう気分の時にゲームすると罪悪感のようなものを感じてしまって、ますます気が滅入る。我ながら面倒くさい性格とは思うのだが。
そんな日々の中、今日はすっかり存在を忘れていた休日なのだが、なんとなく気分に晴れ間が覗いたからか、以前から作ってみたいと思いつつ、ディスクの容量などの都合で断念していた「リモートデスクトップコンテナ」の構築を始めてみた。
自分はFedoraでMATEの人なのだが、コンテナのビルドで「MATEデスクトップ」をインストールしようとすると、なぜかディスクの容量制限にかかって失敗してしまうのだ。そんなら、ということで、姑息ながら小分けインストールしたみたところ、そんな方法でインストールに成功してしまった。
突き詰めていくと「MATE」と「MATE Desktop」に分けるだけで十分なようだ。何度かつながらない状況をトラブルシュートしながらアレコレした程度で、それほど苦労することもなく、アッサリと「リモートデスクトップ接続(RDP)」からの接続に成功してしまった。
# cat Dockerfile
FROM fedora:38
LABEL maintainer="Furutanian <furutanian@gmail.com>"
ARG http_proxy
ARG https_proxy
RUN set -x \
&& dnf groupinstall -y 'MATE' \
&& dnf groupinstall -y 'MATE Desktop' \
&& dnf install -y xrdp \
&& rpm -e thunderbird thunderbird-librnp-rnp \
&& rm -rf /var/cache/dnf/* \
&& dnf clean all
RUN set -x \
&& ln -sv /usr/lib/systemd/system/multi-user.target /etc/systemd/system/default.target \
&& systemctl enable xrdp \
&& systemctl disable firewalld
EXPOSE 3389
ENTRYPOINT ["/sbin/init"]
# cat docker-compose.yml
version: '3'
services:
crd:
image:
docker.io/furutanian/crd
container_name:
crd-alpha
build:
context:
.
# args:
# http_proxy: http://user_abc:password@proxy.example.com:8080/
# https_proxy: http://user_abc:password@proxy.example.com:8080/
ports:
- "13389:3389"
restart:
always
stop_grace_period:
1s
privileged:
true
environment:
TZ: Asia/Tokyo
# http_proxy: http://user_abc:password@proxy.example.com:8080/
# https_proxy: http://user_abc:password@proxy.example.com:8080/
volumes:
- pv:/home
volumes:
pv:
driver: local
# データを永続的に保持する領域として
# mkdir -pv pv しておくこと
driver_opts:
type: none
o: bind
device: $PWD/pv
一応、/homeはPVに出してあるが、コンテナなので再起動すると、良くも悪くもほとんどの設定がブッ飛んでしまうし、現状、コンテナを上げる都度、ユーザを作らなければならないし、英語環境だし、キーボードはヘンだし、タイムゾーンはUTCだしで、まだ詰めは甘い。が、そのへんの直しとか、愛用のメーラであるMAVEの導入とかは、このコンテナを継承する形にするべきで、これはこれで完成形かな。
しかし、これが完全に実用になったならば、常に最新のFedoraに乗り換え続けることも容易になるな。これは、Windowsを捨てFedoraに移行して以来のデスクトップ環境の革命かもしれん。
2023-08-18(Fri) RubyでOAuth2.0認証でGmailをPOP/SMTPする
コンテナ上のリモートデスクトップ環境の整備の一環で、愛用の自作メーラであるMaveも動き出したのだが、PVである/home/userの下で動かす分にはDockerは関係しないことに気づいた。そらそうか。
で、Maveの動作確認の一環でGmailアカウントにPOP/SMTPアクセスしようと思ったら、認証エラー。以前はできていたはずなのだが、と、しばらく止めていたMaverickコンテナを再起動したのだが、認証エラー。なんだ? 環境の問題ではないっぽい?
いろいろ調べていると、なんでも2022年5月をもって通常の認証手段によるPOP/SMTPアクセスは廃止になっていたらしい。トンと知らなかった。職場でメールチェックするのに、外部のVPS上にMaverickコンテナを上げてPOPさせていたのだが、テレワークで不要になったので止めていた間にそんなことになっていたとは。
何か以前にも似たようなことあったような、と思い返すと、会社のメールがマイクソソフトExchangeに変わった時だ。MAPIとかいう独自プロトコルらしい。知ろうという気にもならなかった。DavMailという救世主のようなアプリで難を逃れられている。別に比較できる事象ではないけれど、実にマイクソソフトらしい。ホント近づきたくもない。臭うんだから、寄ってくんな、シッシッ。
というわけで、ダメモトで「OAuth2.0」を学び始める。が、何だろうこのひさびさのワクワク感は。このところの不安定な気分がすっ飛んでいくのを感じる。結局、しばらくの間、新しく歯ごたえのある問題に出会えなかったのが原因だったのだろうか。
AUTH XOAUTH2 dXNlcj1zb21ldXNlckBleGFtcGxlLmNvbQFhdXRoPUJlY...
こんな認証コマンドあったっけ? と、思ったら、RFC1734として定義があるらしい。だいぶ古い。そんならRubyのnet/popライブラリにあるのかしらん? と、思ってコードを見ると、それは実装されていないようだ。ダメじゃん。ほんじゃnet/smtpライブラリには? と、確認すると「AUTH PLAIN」や「AUTH LOGIN」はあるが「AUTH XOAUTH2」や「AUTH xxx」という仕組みはないようだ。これも、ダメじゃん。
ほぼ、というのは、POP/SMTPに必要なスコープは、「https://www.googleapis.com/auth/gmail.modify」や「https://www.googleapis.com/auth/gmail.readonly」ではなく、フルコントロールっぽい「https://mail.google.com/」だというところ。面倒なので細かく試していないが、たぶんそう。これが原因でだいぶ悩まされた。
#!/usr/bin/env ruby
require 'net/http'
require 'json'
# setup GCP
# https://blog.ver001.com/gmail-api-oauth2-credential-key/
if(ARGV.size == 0)
warn <<USAGE
Usage:
$ ./oauth2.rb client_secret_xxxx.json > auth.html
$ google-chrome auth.html
$ ./oauth2.rb client_secret_xxxx.json 'http://localhost/?code=xxxx'
USAGE
exit(1)
end
cs = nil; if((it = ARGV[0]) =~ /^client/)
open(it) {|fh|
cs = JSON.parse(fh.read)
}
end
if(ARGV.size == 1 and cs)
auth_uri = 'https://accounts.google.com/o/oauth2/auth'
auth_params = {
response_type: 'code',
scope: 'https://mail.google.com/',
client_id: cs['installed']['client_id'],
redirect_uri: 'http://localhost',
}
auth_response = Net::HTTP.post_form(URI(auth_uri), auth_params)
puts(auth_response.body)
elsif(ARGV.size == 2 and cs and (it = ARGV[1]) =~ /^http:/)
ps = URI.decode_www_form(URI(it).query)
auth_uri = 'https://accounts.google.com/o/oauth2/token'
auth_params = {
client_id: cs['installed']['client_id'],
client_secret: cs['installed']['client_secret'],
redirect_uri: 'http://localhost',
grant_type: 'authorization_code',
code: ps[0][1],
}
auth_response = Net::HTTP.post_form(URI(auth_uri), auth_params)
puts(auth_response.body)
else
warn('Invalid.')
end
$ ./oauth2.rb client_secret_xxxx.json > auth.html
$ google-chrome auth.html
$ ./oauth2.rb client_secret_xxxx.json 'http://localhost/?code=xxxx'
なんと、途中でブラウザを立ち上げて承認するという奇妙な手順を経て、さらに飛び先のURLを、再びコマンドに食わせるという奇妙な手順を経る。以下のようなJSONが返ればウッシッシ。アクセストークン、ゲットだぜ。
{
"access_token": "ya29.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"expires_in": 3599,
"refresh_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"scope": "https://mail.google.com/",
"token_type": "Bearer"
}
--- /usr/share/ruby/net/smtp.rb.org 2018-11-03 02:52:33.000000000 +0900
+++ /usr/share/ruby/net/smtp.rb 2023-08-18 16:40:20.249910652 +0900
@@ -760,6 +760,15 @@
res
end
+ def auth_oauth2(user, secret)
+ check_auth_args user, secret
+ res = critical {
+ get_response('AUTH XOAUTH2 ' + base64_encode("user=#{user}\1auth=#{secret}\1\1"))
+ }
+ check_auth_response res
+ res
+ end
+
private
def check_auth_method(type)
#!/usr/bin/env ruby
require 'net/smtp'
smtp = Net::SMTP.new('smtp.gmail.com', 587)
ssl = OpenSSL::SSL::SSLContext.new
ssl.verify_mode = OpenSSL::SSL::VERIFY_PEER
ssl.ca_file = '/etc/pki/tls/certs/ca-bundle.crt'
smtp.enable_starttls(ssl)
token = 'ya29.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
smtp.start('example.com', 'example@gmail.com', 'Bearer ' + token, :oauth2) {|smtp|
p smtp
}
$ ./smtp_test.rb
#<Net::SMTP smtp.gmail.com:587 started=true>
--- /usr/share/ruby/net/pop.rb.org 2018-11-03 02:52:33.000000000 +0900
+++ /usr/share/ruby/net/pop.rb 2023-08-18 16:38:38.149686063 +0900
@@ -415,11 +415,12 @@
# to use APOP authentication; it defaults to +false+.
#
# This method does *not* open the TCP connection.
- def initialize(addr, port = nil, isapop = false)
+ def initialize(addr, port = nil, isapop = false, isoauth2 = false)
@address = addr
@ssl_params = POP3.ssl_params
@port = port
@apop = isapop
+ @oauth2 = isoauth2
@command = nil
@socket = nil
@@ -438,6 +439,10 @@
@apop
end
+ def oauth2?
+ @oauth2
+ end
+
# does this instance use SSL?
def use_ssl?
return !@ssl_params.nil?
@@ -563,6 +568,8 @@
@command = POP3Command.new(@socket)
if apop?
@command.apop account, password
+ elsif oauth2?
+ @command.auth_oauth2 account, password
else
@command.auth account, password
end
@@ -918,6 +925,13 @@
})
end
+ def auth_oauth2(account, password)
+ check_response_auth(critical {
+ get_response('AUTH XOAUTH2 ' + ["user=#{account}\1auth=#{password}\1\1"].pack('m0'))
+ })
+ end
+
def list
critical {
getok 'LIST'
#!/usr/bin/env ruby
require 'net/pop'
pop = Net::POP3.new('pop.gmail.com', 995, false, true)
pop.enable_ssl(OpenSSL::SSL::VERIFY_NONE, '/etc/pki/tls/certs/ca-bundle.crt')
token = 'ya29.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
pop.start('example@gmail.com', 'Bearer ' + token) {|pop|
p pop
}
$ ./pop_test.rb
#<Net::POP3 pop.gmail.com:995 open=true>
2023-08-21(Mon) MaveでOAuth2.0認証でGmailをPOP/SMTPする
アクセストークンでPOP/SMTPアクセスできたが、有効期限が1時間しかないので、そのためにリフレッシュトークンを使うのではないか? ということで、アレコレしてみたら、実際そうだった。
*** oauth2.rb.1st 2023-08-18 22:02:21.385069207 +0900
--- oauth2 2023-08-21 23:02:08.486091221 +0900
***************
*** 9,17 ****
if(ARGV.size == 0)
warn <<USAGE
Usage:
! $ ./oauth2.rb client_secret_xxxx.json > auth.html
$ google-chrome auth.html
! $ ./oauth2.rb client_secret_xxxx.json 'http://localhost/?code=xxxx'
USAGE
exit(1)
end
--- 9,18 ----
if(ARGV.size == 0)
warn <<USAGE
Usage:
! $ ./oauth2 client_secret_xxxx.json > auth.html
$ google-chrome auth.html
! $ ./oauth2 client_secret_xxxx.json 'http://localhost/?code=xxxx' > refresh_token.json
! $ ./oauth2 client_secret_xxxx.json refresh_token.json > access_token.json
USAGE
exit(1)
end
***************
*** 21,26 ****
--- 22,32 ----
cs = JSON.parse(fh.read)
}
end
+ rt = nil; if((it = ARGV[1]) =~ /^refresh/)
+ open(it) {|fh|
+ rt = JSON.parse(fh.read)
+ }
+ end
if(ARGV.size == 1 and cs)
auth_uri = 'https://accounts.google.com/o/oauth2/auth'
***************
*** 45,50 ****
--- 51,67 ----
}
auth_response = Net::HTTP.post_form(URI(auth_uri), auth_params)
puts(auth_response.body)
+
+ elsif(ARGV.size == 2 and cs and rt)
+ auth_uri = 'https://accounts.google.com/o/oauth2/token'
+ auth_params = {
+ client_id: cs['installed']['client_id'],
+ client_secret: cs['installed']['client_secret'],
+ refresh_token: rt['refresh_token'],
+ grant_type: 'refresh_token',
+ }
+ auth_response = Net::HTTP.post_form(URI(auth_uri), auth_params)
+ puts(auth_response.body)
else
warn('Invalid.')
$ ./oauth2 client_secret_xxxx.json refresh_token.json > access_token.json
#---------------------------------------------------------------
#
# メールアカウント「gmail」の設定
#
account = {}
account[:NAME] = 'Gmail' # アカウント名(必須)
account[:ENABLE] = false # 有効/無効(必須)
account[:USER_ADDRESS] = 'taro-yamada@gmail.com' # メールアドレス(必須)
account[:POP_SERVER] = 'pop.gmail.com' # メール受信(POP)サーバ
account[:POP_PORT] = 995
account[:POP_ACCOUNT] = 'taro-yamada' # 受信アカウント
account[:POP_PASSWORD] = 'Bearer ' + `./get_access_token access_token.json`.chomp
account[:POP_AUTHTYPE] = :oauth2
account[:POP_OVER_SSL] = true # SSL を使う
account[:POP_SSL_VERIFY] = OpenSSL::SSL::VERIFY_PEER # 証明書を検証する
account[:POP_SSL_CERTS] = '/etc/pki/tls/certs/ca-bundle.crt'
account[:SMTP_SERVER] = 'smtp.gmail.com' # メール送信(SMTP)サーバ
account[:SMTP_PORT] = 587 # 587(submission), 465(SMTP over SSL)
account[:SMTP_ACCOUNT] = account[:POP_ACCOUNT]
account[:SMTP_PASSWORD] = account[:POP_PASSWORD]
account[:SMTP_AUTHTYPE] = :oauth2
account[:SMTP_STARTTLS] = true # STARTTLS を使う
account[:SMTP_OVER_TLS] = false # TLS を使う
account[:SMTP_TLS_VERIFY] = OpenSSL::SSL::VERIFY_PEER # 証明書を検証する
account[:SMTP_TLS_CERTS] = '/etc/pki/tls/certs/ca-bundle.crt'
@configs[:ACCOUNTS] << account
# 定期的にアクセストークンをリフレッシュする必要がある
# ./oauth2 client_secret_xxxx.json refresh_token.json > access_token.json
config内にファイルから読む処理が入っているので、cron等でaccess_token.jsonを更新してもMaveの起動中は更新されず、再起動する必要があるが、再起動は瞬時にできるので特段の対策はなし。そもそも、Maverickとして使う場合はmave_fetchからpopされるので問題ないし。
def initialize(addr, port = nil, isapop = false)
@address = addr
@ssl_params = POP3.ssl_params
@port = port
@apop = isapop
+ if(isapop == :oauth2)
+ @apop = false
+ @oauth2 = true
+ end
2023-08-28(Mon) Rubyでデジタル署名付きメールを生成する
いまだどうも気分が晴れない日が続いている。が、何かに取り組んでいないと、ますます気分が沈むので、とりあえずさほど必要でもないようなプログラミングに取り組むのであった。先日に引き続き、自作のメーラであるMaveに電子署名(S/MIME)機能を搭載するのである。
まず、最初に必要なのが「メール署名用の証明書と秘密鍵」の生成だ。自分は「Easy-RSA」が好きなのだが「メール署名用証明書」の情報が見当たらない。まぁ、その用途は「サーバ証明書」よりもだいぶ少ないわけで、さらにそれを「オレオレ証明書」で済まそうというのだから、そら見当たらないわな。でも、できる。今回、最新版のEasy-RSAを使うために、素のFedora38のコンテナを上げて作業した。いやぁ、コンテナって本当にいいもんですね。方法はざっと以下だ。
・作業ディレクトリを準備する
# useradd t-yamada
# su - t-yamada
$ mkdir make_email_cert
$ cd make_email_cert
$ cp -a /usr/share/easy-rsa .
$ cd easy-rsa/3.1.5
・環境を初期化する
$ ./easyrsa init-pki
・新たに認証局を立てる(認証局証明書/秘密鍵を生成する)
$ ./easyrsa build-ca
Enter New CA Key Passphrase: ca_passphrase
Confirm New CA Key Passphrase: ca_passphrase
Common Name (eg: your user, host, or server name) [Easy-RSA CA]: OreOreSign
・認証局証明書/秘密鍵の内容を確認する
$ ./easyrsa show-ca
$ openssl x509 -in ./pki/ca.crt -text
$ openssl rsa -in ./pki/private/ca.key -text
Enter pass phrase for ./pki/private/ca.key: ca_passphrase
・メール用の証明書署名要求(秘密鍵)を生成する
$ ./easyrsa --dn-mode=org gen-req t-yamada ★ --dn-mode=org がキモ
Enter PEM pass phrase: yamada_key_passphrase
Verifying - Enter PEM pass phrase: yamada_key_passphrase
Country Name (2 letter code) [US]: JP
State or Province Name (full name) [California]: Tokyo
Locality Name (eg, city) [San Francisco]: Chiyoda
Organization Name (eg, company) [Copyleft Certificate Co]: Example Corp.
Organizational Unit Name (eg, section) [My Organizational Unit]: . ★ OU は略す
Common Name (eg: your user, host, or server name) [t-yamada]: Taro Yamada
Email Address [me@example.net]: t-yamada@example.com
Serial-number (eg, device serial-number) []:
・証明書署名要求(秘密鍵)の内容を確認する
$ ./easyrsa show-req t-yamada
$ openssl req -in ./pki/reqs/t-yamada.req -text
$ openssl rsa -in ./pki/private/t-yamada.key -text
Enter pass phrase for ./pki/private/t-yamada.key: yamada_key_passphrase
・認証局として証明書署名要求に署名する
$ ./easyrsa sign-req email t-yamada ★ email がキモ
subject=
countryName = JP
stateOrProvinceName = Tokyo
localityName = Chiyoda
organizationName = Example Corp.
commonName = Taro Yamada
emailAddress = t-yamada@example.com
Confirm request details: yes
Enter pass phrase for /home/t-yamada/make_email_cert/easy-rsa/3.1.5/pki/private/ca.key: ca_passphrase
・署名済みのメール用の証明書の内容を確認する
$ ./easyrsa show-cert t-yamada
$ openssl x509 -in ./pki/issued/t-yamada.crt -text
・PKCS#12(証明書と秘密鍵などをまとめた形式)に変換する
$ ./easyrsa export-p12 t-yamada
Enter pass phrase for /home/t-yamada/make_email_cert/easy-rsa/3.1.5/pki/private/t-yamada.key: yamada_key_passphrase
Enter Export Password: yamada_p12_passphrase
Verifying - Enter Export Password: yamada_p12_passphrase
・PKCS#12の内容を確認する
$ openssl x509 -in ./pki/private/t-yamada.p12 -text
Enter pass phrase for PKCS12 import pass phrase: yamada_p12_passphrase
$ openssl rsa -in ./pki/private/t-yamada.p12 -text
Enter pass phrase for PKCS12 import pass phrase: yamada_p12_passphrase
ひとつめに重要なのが「./easyrsa --dn-mode=org gen-req t-yamada」の「--dn-mode=org」。これを指定しないと、証明書内のSubjectにメールアドレスが入らず「メール署名用の証明書」にならない。もうひとつ重要なのが「./easyrsa sign-req email t-yamada」の「email」。これを指定しないと、証明書内の「Extended Key Usage」に「E-mail Protection」が入らず、やはり「メール署名用の証明書」にならない。
で、上記の作業の成果物が「pki/private/t-yamada.p12」だ。オレオレながら、認証局の署名済みの「メール署名用の証明書」である。普通にシステムの証明書ストアに置いたりしてメーラから使えるものだ。だが、今回はS/MIMEを自分で実装するのが目的だから、普通じゃないのである。以下のようなコードで扱うのである。
require 'openssl'
pkcs12 = OpenSSL::PKCS12.new(open('t-yamada.p12').read, 'yamada_p12_passphrase')
pkcs12.certificate.subject.to_a.each {|name|
puts("Subject: %s:\n\t%s" % name)
}
pkcs12.certificate.extensions.each {|ext|
puts("Extension: %s:\n\t%s" % [ext.oid, ext.value])
}
Subject: C:
JP
Subject: ST:
Tokyo
Subject: L:
Chiyoda
Subject: O:
Example Corp.
Subject: CN:
Taro Yamada
Subject: emailAddress:
t-yamada@example.com
Extension: basicConstraints:
CA:FALSE
Extension: extendedKeyUsage:
E-mail Protection
Extension: keyUsage:
Digital Signature, Non Repudiation, Key Encipherment
data = 'This is Mail text.'
pkcs7 = OpenSSL::PKCS7.sign(pkcs12.certificate, pkcs12.key, data, [], OpenSSL::PKCS7::DETACHED)
smime = OpenSSL::PKCS7.write_smime(pkcs7)
puts(smime)
MIME-Version: 1.0
Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha-256"; boundary="----0123ABCD0123ABCD0123ABCD0123ABCD"
This is an S/MIME signed message
------0123ABCD0123ABCD0123ABCD0123ABCD
This is Mail text.
------0123ABCD0123ABCD0123ABCD0123ABCD
Content-Type: application/x-pkcs7-signature; name="smime.p7s"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="smime.p7s"
XXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxx
xxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXX
:
XXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxx
xxxxXXXXxxxxXXXXxxxxXXXXxxxxXX==
------0123ABCD0123ABCD0123ABCD0123ABCD--
2023-08-29(Tue) Maveでデジタル署名付きメールを送信する
自作のメーラであるMaveに電子署名(S/MIME)機能を実装したい、というわけで、先の試行を経て、実装をするのである。
一応、MVCスタイル(Model/View/Controller)で記述してあるので、それに沿って実装する。といっても、署名とは、つまり添付ファイルの一種なわけで、ファイルを添付する処理を実行後に、さらに署名を添付する処理を実行する形になる。
MaveControllerの送信直前のファイルの添付処理の直後に、署名の添付処理を追加。MaveAccountモデルに、証明書ハンドリング処理を追加。MaveFolderモデルに、下書きメールを署名付きメールに上書きする処理を追加。MavePseudoMailモデルに、下書きメールから署名付きメールを生成する処理を追加。
diff --git a/mave.config.sample b/mave.config.sample
@@ -27,6 +27,9 @@ account[:POP_KEEP_TIME] = 24 * 60 * 60 # サーバに残す時間(秒)
account[:SMTP_SERVER] = 'smtp.example.com' # メール送信(SMTP)サーバ
+account[:PKCS12] = 't-yamada.p12'
+account[:PKCS12_PASSPHRASE] = 'yamada_p12_passphrase'
+
# インポート設定
#account[:IMPORT_COMMAND] = %Q!/usr/bin/find /home/old_user/mave.mails -name '*.eml' | grep -E '/Inbox/' | sed 's/\\/.*\\//& /'| sort -k 2 | sed 's/ //'!
diff --git a/mave_controller.rb b/mave_controller.rb
@@ -310,7 +310,8 @@ class MaveController
}
@models[:STATUS].log(['rcpt to=%s', rcpt_to.inspect]) if(debug = false) # 送信先のデバッグ
outbox_folder.enclose_attachments(mail) # 必要なら、メールに添付ファイルを入れ込む
+ outbox_folder.attach_sign(mail, account) # 必要なら、メールに署名する
result = smtp.ready(account.mail_from, rcpt_to) {|fw|
mail.header_each(nobcc = true) {|line|
fw.write(line + "\r\n")
diff --git a/mave_models.rb b/mave_models.rb
@@ -189,6 +189,7 @@ class MaveAccount < MaveBaseModel
attr_reader :smtp_server
attr_reader :import_command
attr_reader :hash_id
+ attr_reader :pkcs12
def initialize(params)
super
@@ -220,6 +221,8 @@ class MaveAccount < MaveBaseModel
@smtp_tls_verify= @account[:SMTP_TLS_VERIFY]
@smtp_tls_certs = @account[:SMTP_TLS_CERTS]
+ (it = @account[:PKCS12]) and @pkcs12 = OpenSSL::PKCS12.new(open(it).read, @account[:PKCS12_PASSPHRASE])
+
@import_command = @account[:IMPORT_COMMAND]
@hash_id = Digest::MD5.hexdigest(@account[:USER_ADDRESS])[0, 8]
@@ -1134,6 +1137,23 @@ class MaveFolder < MaveDirectory
@dirty += 1 ####
end
+ #------------------------------------------- MaveFolder ----
+ #
+ # メールに署名する
+ #
+ def attach_sign(source_mail, account)
+ return unless(account.pkcs12)
+ halfname = create_mailfile {|fh| # 一時ファイルに書き出す
+ MavePseudoMail.new({:CONFIGS => @configs, :MODE => :SIGN, :MAIL => source_mail, :ACCOUNT => account}).pseudo_each {|line|
+ fh.write(line + "\n")
+ }
+ }
+ mail = MavePseudoMail.new({:CONFIGS => @configs, :FILE => (xmail = File.new(path + '/' + halfname))})
+ overwrite_mail(xmail, source_mail)
+ delete(halfname) unless(RUBY_PLATFORM =~ /i.86-mswin32/) ####
+ @dirty += 1 ####
+ end
+
#------------------------------------------- MaveFolder ----
#
# 任意の内容のメールを内部生成する
@@ -1723,6 +1743,7 @@ class MavePseudoMail < MaveMail
:VIEW => method(:view_message_each),
:VIEW_RAW => method(:view_raw_message_each),
:ENCLOSE => method(:enclose_attachments_each),
+ :SIGN => method(:attach_sign_each),
}
@each_func = @formtype[params[:MODE]] || nil
@through_date = params[:THROUGH_DATE]
@@ -2135,6 +2156,46 @@ class MavePseudoMail < MaveMail
yield("--#{boundary}--")
end
+
+ #--------------------------------------- MavePseudoMail ----
+ #
+ # 署名付きメールの作成
+ #
+ def attach_sign_each
+ data = ''
+ @mail.header_each {|line|
+ data << (line + "\n") if(line =~ /^Content-Type:/)
+ data << (line + "\n") if(line =~ /^Content-Transfer-Encoding:/)
+ } if(@mail)
+ data << "\n"
+ @mail.raw_body_each {|line|
+ line =~ /^This is a multi-part/ and next
+ data << (line + "\n")
+ } if(@mail)
+ pkcs12 = @account.pkcs12
+ pkcs7 = OpenSSL::PKCS7.sign(pkcs12.certificate, pkcs12.key, data, [], OpenSSL::PKCS7::DETACHED)
+ smime = OpenSSL::PKCS7.write_smime(pkcs7) # 署名付きメール(crlf, lf が混じっている)
+
+ @mail.header_each {|line|
+ if(line =~ /^(\S+?):/)
+ header = $1.downcase
+ if(header == 'mime-version')
+ # smime 中にあるので捨てる
+ elsif(header == 'content-type')
+ # smime 中にあるので捨てる
+ elsif(header == 'content-transfer-encoding')
+ # マルチパートになるので捨てる
+ else
+ yield(line); header = false
+ end
+ else
+ yield(line) unless(header)
+ end
+ } if(@mail)
+ smime.split(/\r?\n/).each {|line|
+ yield(line)
+ }
+ end
end
#===============================================================================
自分で言うのも何だが、エラく美しく追加できた。既存のコードの修正は一切なく、最初から用意されていた実装すべき場所に、必要なコードを加えただけ、という感じ。実に気分がいいなぁ。これも、たぶん使わないんだけど。
実装が終わったところでテスト。テストは、ちゃんとS/MIMEが実装されているメーラを使って行う。手元のFedoraに標準添付のThunderbirdだ。Maveから送信して、Thunderbirdで受信させる。
署名付きのメールを受信すると、封筒マークに赤い×が付いたアイコンが現れ、それをクリックすると「デジタル署名が正しくありません」「メッセージは暗号化されていません」と出る。だが、どちらも期待通りだ。前者は、オレオレ認証局による署名だから、信頼されていないという意味だし、後者は、そもそも暗号化を実装していないし、実装するつもりもないから。
署名の実装自体が正しいことの確認に、本文を改ざんしてみた。すると「メッセージ内容に対して署名が一致しません」と出る。これも期待通り。逆に言えば、改ざん前の状態では正しく署名されていた、ということになるから。
オレオレ認証局の証明書をThunderbirdに追加するとこうなる。封筒マークから赤い×が消え「メッセージは署名されています」となる。これが正しい状態だ。認める認証局が署名した相手からのメールで、本文も改ざんされていないことが保証された状態。