SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|06|07|08|09|10|11|

2023-08-11(Fri) コンテナ上にリモートデスクトップ環境の構築に成功

  だいぶ間が開いてしまったが、東北から戻ってから、自宅の外壁工事が始まったり、ロードスターを外の駐車場に退避しておいたらブツけられたり、筋トレを緩めたせいなのか体が緩んだり、なんやかんやで、どうも気分的に不安定になっていたりする。

  男性にも更年期があるとかで、そのせいなのか、自宅やクルマというテリトリが侵され気味のせいなのか、仕事の環境が微妙なせいなのか、暑い中を動きすぎて塩分が足りてないのか、どうも気分は曇り空である。時々、晴れ間が覗くような、そうでもないような。

  何かに取り組んでいないと気がすまない性格なのに、どうも取り組む気が起きず、ダラダラとゲームしたりしてしまう。ゲームが悪いわけではないのだが、そういう気分の時にゲームすると罪悪感のようなものを感じてしまって、ますます気が滅入る。我ながら面倒くさい性格とは思うのだが。

  そんな日々の中、今日はすっかり存在を忘れていた休日なのだが、なんとなく気分に晴れ間が覗いたからか、以前から作ってみたいと思いつつ、ディスクの容量などの都合で断念していた「リモートデスクトップコンテナ」の構築を始めてみた。

  自分はFedoraでMATEの人なのだが、コンテナのビルドで「MATEデスクトップ」をインストールしようとすると、なぜかディスクの容量制限にかかって失敗してしまうのだ。そんなら、ということで、姑息ながら小分けインストールしたみたところ、そんな方法でインストールに成功してしまった。

  突き詰めていくと「MATE」と「MATE Desktop」に分けるだけで十分なようだ。何度かつながらない状況をトラブルシュートしながらアレコレした程度で、それほど苦労することもなく、アッサリと「リモートデスクトップ接続(RDP)」からの接続に成功してしまった。

  画像の説明

  以下にDocker関連ファイルを示す。

 # 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させていたのだが、テレワークで不要になったので止めていた間にそんなことになっていたとは。

  別にGmailアクセスは必須じゃないのだが、できないのもシャクに障る。なんでも「OAuth2.0」という認証方式に変わったらしいのだが、別にGoogleの独自規格ではなくRFCが出ているらしい。

  何か以前にも似たようなことあったような、と思い返すと、会社のメールがマイクソソフトExchangeに変わった時だ。MAPIとかいう独自プロトコルらしい。知ろうという気にもならなかった。DavMailという救世主のようなアプリで難を逃れられている。別に比較できる事象ではないけれど、実にマイクソソフトらしい。ホント近づきたくもない。臭うんだから、寄ってくんな、シッシッ。

  というわけで、ダメモトで「OAuth2.0」を学び始める。が、何だろうこのひさびさのワクワク感は。このところの不安定な気分がすっ飛んでいくのを感じる。結局、しばらくの間、新しく歯ごたえのある問題に出会えなかったのが原因だったのだろうか。

  まず、一番重要な情報が以下。

  POP/SMTPアクセスが廃止になったわけではない、ということだ。さすがにそれができなくなると機能追加というレベルになってしまうので、Maveの対応をあきらめざるを得ない。

  しかし、SMTPはともかく、POPに、

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"
}

  アクセストークンが得られたらこっちのもんだ。Rubyのnet/smtpライブラリにOAuth2.0認証機能を足す。といっても、以下を足すだけ。

--- /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)

  アクセストークンを使ってSMTPアクセスするテストプログラムを書いてドン。

#!/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>

  net/popライブラリにもOAuth2.0認証機能を足す。元のAPIの設計が微妙なせいでちょっと美しくないし、net/smtpよりちょっと量があるが、以下の変更を加える。

--- /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'

  アクセストークンを使ってPOPアクセスするテストプログラムを書いてドン。

#!/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>

  んがしかし、これでPOP/SMTPアクセスができる目処はついたのだが、このアクセストークン、有効期限が1時間しかないので、パスワードの替わりという形で実装すると、まったく実用にならない。

  どうも「refresh_token」というのがカギを握っていそうだが……本日はコレまで。あー、しかし、ひさびさにエキサイティングだったわッ!


2023-08-21(Mon) MaveでOAuth2.0認証でGmailをPOP/SMTPする

  リフレッシュ機能を、前回作ったoauth2.rbに加える。

 *** 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

  そして、Maveへの修正は恐ろしいほどに小規模だった。ほぼconfigだけで済んだようなものだ。

 #---------------------------------------------------------------
 #
 #   メールアカウント「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されるので問題ないし。

  あ、net/popライブラリへのOAuth2.0認証機能の追加は、後方互換のため、パラメータを増やすのでなく、既存のapopを指定するパラメータに押し込む形に変更した。

      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

  つうわけで、最初はダメモトで「OAuth2.0」を学んでみるか、くらいのつもりだったが、ほぼ実用に足る形で実装が完了してしまった。実に気分がいいなぁ。たぶん使わないんだけど。

  つうわけで、リポジトリにコミットして完了である。


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

  さらに、pkcs12インスタンスをPKCS7クラスに食わせれば……

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--

  メール本文は「'This is Mail text.'」の部分だ。ヘッダまで自動生成ですぞ。うぅむ、こんなに簡単にできてしまうと、ちょっと過保護じゃね? と思うくらいだな。


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に追加するとこうなる。封筒マークから赤い×が消え「メッセージは署名されています」となる。これが正しい状態だ。認める認証局が署名した相手からのメールで、本文も改ざんされていないことが保証された状態。

  実際には、この署名の確認は相手側によって行われるものなので、オレオレ証明書を使っている限りはバツということになるが、実装としてはちゃんとS/MIMEが実現できているといえる。試しに会社から発行されているちゃんとした証明書を使ってテストしても、正しい結果が得られている。

  つうわけで、リポジトリにコミットして完了である。