それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

sslhでSNI認識してサーバを振り分ける。(apln 有無)

sslh にはSNI機能があった

nginx で捌いても良いんですが、sslhはnginxより設定がシンプルだ。

443ポートを直接リッスンするならsslhでリッスンして各種ポートに透過プロキシしたほうが再利用性が高くて良い。

443 ポートで、SNIで接続先を選定

特定のホスト名のときだけ別のnginxへつなぎに行く。

protocols:
(
  {
    name: "ssh";
    service: "ssh";
    host: "192.168.2.5";
    port: "22";
    fork: true;
  },
  {
    name: "tls";
    alpn_protocols: [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ];
    host: "192.168.100.2";
    port: "443";
    sni_hostnames: [
      "example.tld",
      "example.com",
    ];
    log_level: 0;
    tfo_ok: true
  },
  {
    name: "tls";
    alpn_protocols : [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ];
    host: "192.168.100.5";
    port: "443";
    log_level: 0;
    tfo_ok: true
  },

起動してみる。

sslh -f -F /root/sslh.conf --listen 127.0.0.1:443

テスト

curl -v --resolve example.tld:443:127.0.0.1 https://example.tld/ # 200 ok from 100.2
curl -v --resolve example.com:443:127.0.0.1 https://example.com/  # 200 ok from 100.2
curl -v --resolve example.net:443:127.0.0.1 https://example.net/  # from 100.5

APLN とSNI

ALPNをサポートするかしないかでSNIの振り分けが変わってくる

次のようなALPN付きの条件を書いたら、SNIより先にALPNで仕分けられる

  {
    name: "tls";
    alpn_protocols: [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ];
    host: "192.168.100.2";
    port: "443";
    sni_hostnames: ["nginx.lan"]
  },

ALPNを条件にした振り分けの接続チェック

openssl s_client --connect router.lan:443 -alpn h2 --servername nginx.lan < /dev/null 

openssl に -alpn を条件に入れて、接続をチェックする。このとき、SNIは--servernameで指定している。

ALPN未指定の設定

  {
    name: "tls";
    # alpn_protocols: なし
    host: "192.168.100.4";
    port: "443";
    sni_hostnames: ["nginx2.lan"]
  },

上記のように、ALPNを未指定で残す設定で書いたとき。

以下のように接続をチェックする

openssl s_client --connect router.lan:443 --servername nginx2.lan < /dev/null 

SSLHではALPNがサポートされていて、ALPNつけることで絞り込みができるし、Nginxなどが対応しているのでつけてもつけなくても、ほとんど影響がない。まれにALPN非対応なWebサーバーがあったりするとこの条件を意識しないといけない。

また、curlの場合 --no-alpn を使うことでALPNの有無でSSLHの反応が変わることがわかる。

curl -vs  --http2 https://t.co > /dev/null
curl -vs --no-alpn https://t.co
curl -vs --no-alpn --http1.1 https://t.co > /dev/null

docker サーバーを分離できる。

docker マシンが重くなってきたので、nginxと別マシンに移動させようと思ったけど、ルーターで直接443をリッスンして振り分けることができて、レイヤ7のルーターとして動作するの便利だ。

nginxで書いても良いんだけど。nginxを入れるほどでもないし、nginxでopenvpn/ipsec/sshマッピングするのが面倒だったので、sslhを使うことにした

sslhのコマンド引数にはない。

sslh の「設定ファイル」を書く必要があり、コマンド引数ではALPN/SNIを指定できないのが注意。

443 ポート制限に負けない。

TCP/443ポート以外の通信を「不正」とパケット破棄されるような「接続サービス」をインターネット接続と呼んで欲しくない。

UDP/443ポートまで制限するのは流石にないと思ってたけど、最近、とある旅館で遭遇したのでとても恐ろしい。HTTP/3すら否定された。

2023-05-24

SNIとALPNについて追記

ポリシールーティング GWが複数時に、条件マッチで経路を変える。

特定の条件だけ経路を変えたい。

たとえば、通常のルーティングはすでにあるとして、特定PC(業務用)だけ経路を変えたい。

ルーティングはテーブルで

通常のルーティングは、ルーティングテーブルで行われる。ルーティングを書けばいい

特定のパケットだけGWを変えたい。

こうなると、途端に難しくなる。

特定の条件に合致したパケットだけを違うGWに転送したい。つまりルーティングテーブルをスキップしたい。

色々と考えられるが、面倒になる。別ルータを仮想マシンで用意するのが手っ取り早かったりする。

ポリシールーティング

これを実現する簡単な方法が、ルーティングテーブルの複数持ちである。

ルーティングテーブルは複数持ちできて、条件にマッチしたパケットを別のルーティングテーブルをに放り込むことができる。

これを「ポリシー・ベース・ルーティング」 という。

ルーティングテーブルを追加

ポリシールーティングのために ruleとマーキング(フラグ設定) を作成

ip rule add fwmark 101 table 101
ip route add 1.1.1.1 dev pppoe-isp2 table 101

ルーティングテーブルの選択。

iptables -I PREROUTING -t mangle  -d 1.1.1.1/32 -j MARK --set-mark 101

設定の確認

ip route show table 101
ip route get fibmatch 1.1.1.1 mark 101  

まとめ

たった3行です。

テーブルを追加して、経路を追加する。 条件にマッチしたパケットにMARKする。

ip rule add fwmark 101 table 101
ip route add 1.1.1.1 dev pppoe-isp2 table 101
iptables -I PREROUTING -t mangle  -d 1.1.1.1/32 -j MARK --set-mark 101

メインのテーブルはいじらないのでとても簡単で便利。

通常のパケットはVPN/IPsecで飛ばし、条件にマッチしたパケットをVPN/IPsecを経由させないみたいな。そういう事ができてネットワークのデバッグがすごく楽になる。

CAを作る(openssl.cnf )中間認証局を作る、サーバー証明書を作る。ローカルドメインをHTTPS化する。

CAを作る(openssl.cnf )

MyCAという名前でRoot CA を作る。

mkdir MyCA
cd MyCA

必要なディレクトリを作る。

mkdir ./newcerts
mkdir ./requests
touch index.txt
echo 00 > serial
echo 00 > crlnumber

openssl.cnf を作る。

/etc/ssl/openssl.cnf から必要な部分、[ ca ]セクション から [ tsa ]セクション手前 までを取り出す

cat /etc/ssl/openssl.cnf \
 | sed -n '/\[ ca \]/,$p' \
 | sed  '/\[ tsa/,$d' \
 > openssl.cnf

keyUsage(鍵用途)で証明書発行を許可した証明書(CAになれる)となるよう設定を変更

sed -i openssl.cnf  -e '/^keyUsage = nonRepudiation/s|^|# |'
sed -i openssl.cnf  -e '/keyUsage = cRLSign/s|^# ||'

有効期限を10年くらいにする(1年は365日と覚えてるだろうが、360日で計算してたほうが楽なので、1年は360日+誤差だと考えてたほうが計算が楽。)

sed -i openssl.cnf  -e 's|365|3600|' 

CAディレクトリをカレントにする

sed -i openssl.cnf -e 's|\./demoCA|\.|' 

設定ファイルを確定させる。

cp openssl.cnf myCA.openssl.cnf

秘密鍵作成から、自己署名証明書の作成まで

openssl genrsa > myCA.priv.key
openssl req -new -subj "/C=JP/ST=Kyoto/O=my/CN=MyCA" -key myCA.priv.key > myCA.csr
openssl req -text < myCA.csr 
openssl x509 -req  -in myCA.csr -signkey myCA.priv.key > myCA.crt

自己署名証明書を作る。

openssl ca -config myCA.openssl.cnf -extensions v3_ca  -in myCA.csr -keyfile myCA.priv.key  -selfsign

PEM をコピーする

openssl x509 < newcerts/00.pem  > myCA.crt

ここまでで、次のようになった

~/MyCA$ tree .
.
├── crlnumber
├── index.txt
├── index.txt.attr
├── index.txt.old
├── myCA.crt
├── myCA.csr
├── myCA.openssl.cnf
├── myCA.priv.key
├── newcerts
│   └── 00.pem
├── openssl.cnf
├── requests
├── serial
└── serial.old

2 directories, 12 files

中間認証局(intermediate)を作成してみる。

ディレクトリを確保する

mkdir MedCA
cd MedCA

初期化しておいて

mkdir ./newcerts
mkdir ./requests
touch index.txt
echo 00 > serial
echo 00 > crlnumber

設定ファイルをサクッと作る。

cat /etc/ssl/openssl.cnf  \
 | sed -n '/\[ ca \]/,$p' \
 | sed  '/\[ tsa/,$d'     \
 | sed -e 's|365|3600|'   \
 | sed -e '/^keyUsage = nonRepudiation/s|^|# |' \
 | sed -e '/keyUsage = cRLSign/s|^# ||' \
 | sed -e '/copy_extensions/s|^# ||' \
 | sed -e 's|\./demoCA|\.|' \
> medCA.openssl.cnf

中間認証局用の秘密鍵とリクエスト作成

openssl genrsa > medCA.priv.key
openssl req -new -subj "/C=JP/ST=Kyoto/O=my medium/CN=MedCA" -key medCA.priv.key > medCA.csr
openssl req -text < medCA.csr

Root CA(MyCA)にリクエストを送信(コピー)する

cp MedCA/medCA.csr  MyCA/requests/

Root CAに入り、中間認証局向けに証明書を発行してあげる

cd ../myCA
openssl ca \
  -config myCA.openssl.cnf \
  -extensions v3_ca \
  -policy policy_anything \
  -cert myCA.crt \
  -in  ./requests/medCA.csr \
  -keyfile myCA.priv.key 

結果を確認

openssl x509 -text < ./newcerts/01.pem

証明書PEMを払い出し(コピー)

openssl x509  < ./newcerts/01.pem > medCA.crt
mv medCA.crt ../MedCA/

一般利用者向けに証明書を中間認証局から発行する

nginx などで使えるように、DNS証明書(DV証明書を作る。)対象とするホスト名はraspi3.lan とする。IPアドレスでも使えるようにIP:xxx.xxx.xxx.xxx にも署名した証明書を作る。

今回は、SAN値(subject alternative name )を使って証明書を発行する。

SAN値を使うにはコンフィグ(openssl.cnf)の構成が必要

サーバー証明書を入れるディレクト

mkdir srv
cd srv

san.conf を作る。

cat << 'EOF' > san.cnf
[ req ]
default_bits            = 2048
distinguished_name      = req_distinguished_name
req_extensions          = req_ext


[ req_distinguished_name ]
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name (full name)
localityName                    = Locality Name (eg, city)
0.organizationName              = Organization Name (eg, company)
commonName                      = Common Name (e.g. server FQDN or YOUR name)


[ req_ext ]
subjectAltName = @alt_names

[alt_names]
# Write SANs(Subject Alternative Names)
DNS.1 = raspi3.lan
DNS.2 =  raspi3.local
IP.1 = 10.3.0.2
IP.2 = 127.0.0.1
EOF

SANつきでCSRを作る

openssl genrsa > server.priv.key
openssl req -new \
  -config san.cnf \
  -subj "/C=JP/ST=Kyoto/L=Kyoto City/O=acid/CN=raspi3.lan" \
  -key server.priv.key \
  > server.csr
  

リクエストを認証局(中間)へコピーする

cp server.csr ../MedCA/requests/

中間認証局で署名する。

cd ../MedCA
openssl ca \
  -config medCA.openssl.cnf \
  -policy policy_anything \
  -in requests/server.csr \
  -cert medCA.crt \
  -keyfile medCA.priv.key 

サーバーの証明書を払い出し(コピー)

openssl x509 < newcerts/00.pem > server.crt
mv server.crt  ../srv/

ここまでで、MyCA(root CA) → MedCA(中間CA) -> サーバー(証明書利用者)の3層構造が出来上がったわけである。

証明書の検証を行う。

証明書チェーンのファイルを作る

cat ../MedCA/medCA.crt   ../MyCA/myCA.crt > chain.pem    

verify すると。

openssl verify -verbose -CAfile chain.pem server.crt
# => server.crt: OK

これで使えることがわかる。

nginx で使ってみる

nginx で作った証明書と鍵を導入する。

/etc/nginx/sites-enabled/raspi3.lan.conf

server {

  server_name  raspi3.lan;

  add_header  X-Robots-Tag "noindex, nofollow, nosnippet, noarchive";

  listen 10.3.0.2:443 ssl http2;
  ssl_certificate_key /home/takuya/certs/srv/server.priv.key;
  ssl_certificate /home/takuya/certs/srv/chain.crt;

  if ($scheme = http) {
    return 301 https://$server_name$request_uri;
  }
  location / {
    add_header Content-Type "text/html";
    return 200 "<h1>It works.(nginx)</h1>\n";
  }
}

設定を反映

sudo nginx -t 
sudo nginx -s reload 

クライアントから接続してみる

HTTPSで証明書が使えるか検証してみる

curl --cacert ./MyCA/myCA.crt https://raspi3.lan
# => <h1>It works.(nginx)</h1>

-k の insecure オプションを使わなくても、curl が complainせずに暗号化通信が行えている

openssl でALPN/TLSプロトコルを見てみる。問題なさそう。

openssl s_client -connect 10.3.0.2:443  -crlf -servername raspi3.lan

Chromeブラウザ(Windows)でみてみる。

Windowsの証明書マネージャに作ったMyCAを「ルート証明書」として突っ込む

(これはとても危険行為です。通常はルート証明書を書き換えることは禁忌です。)

certmgr.msc

すべてのタスク→インポート

ウイザードの開始

ファイルを選ぶ

最終確認が出る

ルート証明書を触るのは非常時なので「通常やらないことやってるけどダイジョブか」と更に注意が出る。

インポートされた(後で削除を忘れないよう)

chromeブラウザで確認する。

ローカルドメイン ( .lan ) に対して証明書エラーが回避され、通常通りHTTPS通信になっている。

中間認証局の役割。

中間認証局を作って見るために、煩雑なことをやっている。

HTTPS化するだけなら

オレオレ・ルート証明書だけで十分である。

参考資料

openssl でオレオレ証明書(自己署名証明書)を作りCAを作り、他の証明書リクエストに署名する。

ネットを探してても時間が浪費するので、よく使いそうな機能をまとめておく

オレオレ証明書を作る。

この記事に書いたこと

オレオレ証明書を作る

オレオレ証明書とは、自己署名証明書(第三者の署名を持たない証明書)。自己署名証明書は自分で自分の身分を主張する。似たような事例に「振り込め詐欺」があり、これに因んで、オレオレ証明書という。

手順を書いた。

オレオレ証明書でAliceの証明書を発行する

Aliceの署名に、私が署名して証明書する。

手順は次の通り。

  • Alice の秘密鍵作成
  • Alice の署名リクエストを作成
  • Alice の署名リクエストに署名する

オレオレ証明書で認証基盤(認証局機能)を作る

PKIでのCAの役割をする。署名した発行済証明書を管理する。

  • CA を構成
  • CA として署名する
  • 署名した証明書一覧
  • Revokeを試す。

はじめに。

証明書を使って、「公開鍵」を証明する。逆に考えれば、証明書を発行したことを否定できない。

openssl の準備

現代では、ほとんどの環境で準備とインストールを気にせずに使えると思う。インストール必要なのは、Windowsくらいかも。

最初にバージョンを確認しておく。

openssl version

私が今回用いいたのは次のバージョンです。

$ openssl version
OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)

自己署名証明書オレオレ証明書)の作成

秘密鍵をつくり、公開鍵を導出する。公開鍵を秘密鍵で署名する。署名に秘密鍵で署名する。

秘密鍵作成

openssl genrsa 2048 > privkey.pem

署名リクエストを作成

openssl req -new -key privkey.pem -subj '/C=AU/ST=Some-State/O=OreOre/CN=Ore' -out csr.pem

秘密鍵を使って公開鍵を導出。公開鍵に秘密鍵で署名する。証明書を作る

署名リクエストのSubjectを確認

openssl req -in csr.pem -noout -subject
# -> subject=C = AU, ST = Some-State, O = OreOre, CN = Ore

署名リクエスト(証明書発行リクエスト)の中身を確認する

署名リクエストへ署名

openssl x509 -req -in csr.pem -signkey privkey.pem  -out crt.pem

発行者・被発行者を確認

openssl x509 -in crt.pem -noout -subject -issuer
# -> subject=C = AU, ST = Some-State, O = OreOre, CN = Ore
#    issuer=C = AU, ST = Some-State, O = OreOre, CN = Ore 

Subject(一般的には主体者と訳される)は、「主題・主語・容疑者・被験者・被写体・的(まと)」の意味があり、ここでは「被発行者・対象者・所有者・申請者」の意味で捉えておくといいと思う。

issuer は発行者と訳される。発行社・発行人・発行担当である。ここでは、「証人」と思っておくと良い。

Aliceの証明書を作る

Aliceの証明書を作成する。オレオレ証明書による署名とほぼ同等である。Subjectが変わる。

Aliceの秘密鍵を作成。

openssl genrsa 2048 > alice.privkey.pem

Aliceの署名リクエストを作成

openssl req -new -key alice.privkey.pem \
   -subj '/C=AL/ST=Ice/O=Alice on Ice/CN=Alice wonder'  \
   -out alice.csr.pem

Aliceの証明書に署名する

署名リクエスト(Aliceの自己署名証明書)に署名する

openssl x509 -req  \
  -CA crt.pem  -CAkey privkey.pem \
  -CAcreateserial -CAserial serial \
  -in alice.csr.pem  > alice.crt.pem

発行したAlice の証明書を確認する。

openssl x509 -issuer -subject -noout < alice.crt.pem
## => issuer=C = AU, ST = Some-State, O = OreOre, CN = Ore
##    subject=C = AL, ST = Ice, O = Alice on Ice, CN = Alice wonder

発行者(Issuer)がオレオレになっていることがわかる。

証明書の検証

openssl verify -verbose -CAfile crt.pem alice.crt.pem
# => alice.crt.pem: OK

Alice証明書を、発行者の証明書(オレオレ証明書)で検証する。

CA を構成

PKI 基盤として、オレオレ証明書で CA を構成

openssl ca ## エラーがでるので逐次対応する

対応

mkdir demoCA
mkdir demoCA/private
mkdir ./demoCA/newcerts
cp privkey.pem  demoCA/private/cakey.pem
cp crt.pem  demoCA/cacert.pem
touch demoCA/index.txt
echo 00 > demoCA/serial

対応結果

openssl ca ## エラーが出ないことを確認

この対応は Debian/Ubuntuのopensslのデフォルト設定(/usr/lib/ssl/openssl.cnf) を用いた場合の対応であり、実験を簡単にする目的で行っている。

設定ファイルを指定するのが正しい運用です。 ( $ openssl -config openssl.cnf )

CAとして 署名し証明書を発行

openssl ca -in alice.csr.pem  -policy policy_anything 

証明書に本当に署名するか尋ねられる。

Sign the certificate? [y/n]:

署名した証明書を取り出す

cp demoCA/newcerts/00.pem alice.ca_signed.crt.pem

Aliceの証明書を検証する

openssl verify -verbose -CAfile demoCA/cacert.pem alice.ca_signed.crt.pem
## => alice.ca_signed.crt.pem: OK

CA の発行済の証明書を確認する

証明書を発行したら、シリアル値と発行済の証明書が管理される。

cat demoCA/index.txt
#  V       240311012107Z           00      unknown /C=AL/ST=Ice/O=Alice on Ice/CN=Alice wonder
ls -l demoCA/newcerts/00.pem
# -> -rw-rw-r-- 1 takuya takuya 4381  3月 12 10:21 demoCA/newcerts/00.pem

単純にx509 で署名する場合と違うのは、この管理があるかどうか。

revoke してCRLに書き加える。

特定の証明書をRevokeする。

openssl ca -revoke alice.ca_signed.crt.pem
# Revoking Certificate 00.

結果を確認すると、V→R に変わっている。

cat demoCA/index.txt
# R       240311012107Z   230312012549Z   00      unknown /C=AL/ST=Ice/O=Alice on Ice/CN=Alice wonder

CRLを見る。

echo 00 > ./demoCA/crlnumber
openssl ca -gencrl > crl.txt
openssl crl -text < crl.txt

まとめ

他人の証明書リクエストに「自分の秘密鍵」で署名をつけることができる。

openssl x509 -req  \
  -CA crt.pem  -CAkey privkey.pem \
  -CAcreateserial -CAserial serial \
  -in alice.csr.pem  > alice.crt.pem

これが大事なんだと思う。

追記

オレオレ証明書といえども、振り込め詐欺のようなものではなく、「規格準拠」した正しい証明書である。

署名(リクエスト)に署名するだけとはいえCAとして振る舞う必要がある。

ここで作ったオレオレ証明書はOSへ導入するとそのまま使うことができるが。普通はやらない。

最近はルータの管理画面がhttp で困るので導入するかな。

参考資料

AppleのフォントをWindowsにインストール

プラグインテーマがmacOS用のアイコンを要求する

Joplin のmacOS theme が macOSのアイコンを要求する。

このテーマファイルをいれたらアイコンがすべて豆腐になった。なんで豆腐?理由が知りたくて調べた。アイコンが豆腐の原因はフォントが不足でした。フォントを入れたら本当に治るのかを調べた。

フォントをインストールしてみた。

原因はフォントのようです。

なぜフォントを入れたらアイコンが治るのかはまだ謎です。

windowsAppleのフォントをインストール

AppleiOS用のフォントをWEBサイトに置いています。

開発目的であればlicenceを満たすそうです。

今回は、壊れたアイコンが、フォントのインストールで本当に動くか検証したい。なので開発目的ですね。大丈夫。

配布サイトにアクセスしました。

dmg ファイルをダウンロードして windows 7-zip で閲覧します。

pkg ファイルを開き

Payloadを開き

カレント(.)を開く

フォントが出てくる(画像は SF Mono の場合)

SF Compact / SF Pro

SF Compact は WatchOS用で、小さいディスプレイでの視認性を高めて、全体的に直線的なフォント。SF ProはiOSmacOSで標準的に使われているフォントでCompactよりは丸みがある。

SF とは サンフランシスコ

SFは、サンフランシスコの略です。

SF mono

名前の通り、モノスペース(単一幅)です。等幅フォント・固定幅フォント・ノンプロポーショナルなどと呼ばれる、プログラマが好きなあれです。

手動でインストールしたと設定されました。を戻す。

debian/ubuntu で、インストールを手動マークされる

apt install をすると最初から入っていた。とかよくある

手動マークされた

次のように、「手動でインストールされた」(set to manually installed.)とメッセージが出てくる。

bsd-mailx は手動でインストールしたと設定されました。

元に戻す。

元に戻すには、どうするのか。

sudo apt-mark auto bsd-mailx

マークを[自動]にすると、もとに戻る。

bsd-mailx は自動でインストールしたと設定されました。

apt-mark コマンドを知らないと、気持ち悪い状態を抱えたことになる。

このまま放置しても殆ど変化はないので必要ないといえば必要ない。

違いは、アンインストールしたときに自動で消されないくらいだが、わざわざAptを叩いてるときは、そのコマンドを使っている状態なので、自動で消されたら困るわけです。使わなくなったら消せば良いわけです。消したらまた自動に戻るわけですし。

機密passwordを安全に保存する。コマンドで使う。保存する。keyringコマンド

開発用パスワード・キーの管理をしたい

env.txt だとか .git/config とか .gitconfig とか、パスワードとAPIキーが散らばる。どこかに保存したい。

keyring コマンドで管理する。

linux には keyring コマンドがある。これを使うと機密な文字列をマスターパスワードで管理できる、

インストール

Ubuntu Desktopなどは最初から入っている。(apt install python3-keyring )

コマンドからだけ使う場合には、次のコマンドをつかう

apt install python3-keyrings.alt

python3-keyring は保存先の設定が必要でDesktopならGNOMEキーリングになるのだが、ServerやMinmalはそれらを扱えないので、暗号化ファイルをバックエンドに使う。

バックエンド

バックエンドは、OSに依存する安全な保存先が使われる。alt を使った場合は暗号化ファイルになる。

macOSならKeychainなど、WindowsならCredentialsなどが選ばれる。それ以外もプラグインを使えば使える。

バックエンドを未設定ならエラーになる。

私が試したときには次のようなエラーになった。

keyring set system username
Password for 'username' in 'system':
Traceback (most recent call last):
  File "/usr/bin/keyring", line 33, in <module>
    sys.exit(load_entry_point('keyring==22.0.1', 'console_scripts', 'keyring')())
  File "/usr/lib/python3/dist-packages/keyring/cli.py", line 135, in main
    return cli.run(argv)
  File "/usr/lib/python3/dist-packages/keyring/cli.py", line 68, in run
    return method()
  File "/usr/lib/python3/dist-packages/keyring/cli.py", line 85, in do_set
    set_password(self.service, self.username, password)
  File "/usr/lib/python3/dist-packages/keyring/core.py", line 60, in set_password
    get_keyring().set_password(service_name, username, password)
  File "/usr/lib/python3/dist-packages/keyring/backends/fail.py", line 25, in get_password
    raise NoKeyringError(msg)
keyring.errors.NoKeyringError: No recommended backend was available. Install a recommended 3rd party backend package; or, install the keyrings.alt package if you want to use the non-recommended backends. See https://pypi.org/project/keyring for details.

パスワードを保存する

初回起動の場合はマスターパスが必要。

takuya@raspi-ubuntu:~$ keyring set system username
Password for 'username' in 'system': 保存したいパスワード
Please set a password for your new keyring: マスタ・パスワード
Please confirm the password: マスタ・パスワード

2回目以降はパスワードだけを保存する。

takuya@raspi-ubuntu:~$ keyring get system username
Please enter password for encrypted keyring:
password

保存するパスワードに「確認」がないのでミスタイプをしないように注意する。

保存したパスワードを確認

確認は、取得コマンドで行える。

keyring get system username

取得コマンドを使うと、ターミナルに表示されるので取り扱いには注意する。

pbcopy や /dev/nullをつかっていい感じに画面に表示させないように使う。

keyring get system username | pbcopy >/dev/null

windowsで使う場合。

これは、嬉しいことにWindowsでも使える。

python.exe -m pip pip install keyring

Power Shellで保存する。

windowsに保存したパスワードの例

WindowsではスタートメニューからCredentials を検索して

Windows Credentials Lockerを選んで

中身をみるとパスワードが保存されている。

Windows credentials は「ウインドウズ・資格情報」という面倒な日本語訳でとても検索しにくいためでしょうか、まったく情報がありません。Windows Credentialをキーチェインの代わりに使えないかと探していたらKeyringを見つけました。

WindowsはデフォルトでKeyringコマンドを搭載するべきだと思う。

Cmd.exe/PowerShell からは使えるが、MinGw/Cygwinからは使えない。WSL(v1)からは使える。Windows Logon情報と連携しないためだろうか。

バックエンドをBitwardenなどに

バックエンドをBitwardenなどのパスワード・マネージャーにすることもできる。

できるけど、bw-cli だけで事足りるので不要である。

セキュリティに関する愚痴

パスワード管理ソフトを課金するくらいならWindows資格情報を使えばいいし、公開鍵をZipパスワードで保護するような無意味なことをやってないで、ss-agentを使えばいいし。macOS keychains みたいにWindowsCredentialsをSSH-AGENTで使えたらいいのにね。

Null合体演算子(Null Coalescing)と三項演算子の短縮形(Short-ternary )をまとめておく。

Null合体演算子(Null Coalescing)と三項演算子の短縮形(Short-ternary )を使ってパニクったので整理。

<?php
$a[0]??null #-> この値を知りたい。
比較 結果
unset($a) $a[0] ?? null NULL
$a = null $a[0] ?? null NULL
$a = $a[0] ?? null NULL
$a = ['x'] $a[0] ?? null 'x'
$a = 'abc' $a[0] ?? null 'a'
$a = '' $a[0] ?? null NULL
$a = ['x'] $a[0] ?: null 'x'

更に、細かく調査した結果。

比較 結果
unset($a) $a NULL
unset($a) $a ?? null NULL
unset($a) $a[0] ?? null NULL
$a = null $a NULL
$a = null $a ?? null NULL
$a = null $a[0] ?? null NULL
$a = $a
$a = $a ?? null
$a = $a[0] ?? null NULL
$a = ['x'] $a ['x']
$a = ['x'] $a ?? null ['x']
$a = ['x'] $a[0] ?? null 'x'
$a = 'abc' $a 'abc'
$a = 'abc' $a ?? null 'abc'
$a = 'abc' $a[0] ?? null 'a'
$a = '' $a ''
$a = '' $a ?? null ''
$a = '' $a[0] ?? null NULL
$a = ['x'] $a ?: null ['x']
$a = ['x'] $a[0] ?: null 'x'

更にオブジェクトについても見ておく

比較 結果
$a = null $a->id?? null NULL
$a = [] $a->id?? null NULL
$a = 'x' $a->id?? null NULL
$a = '' $a->id?? null NULL
unset($a) $a->id?? null NULL

特にイメージが湧かないのが、次の場合で。

<?php
$a = [];
$a[0]??null #=>null
$a = null;
$a[0]??null #=>null

ミスってしまいがちなのが、次のように順番をミス。

<?
//Trying to access array offset on value of type null 
$a??$a[0]; // Error. 

正しくは次のように書く。

<?php
$a[0]??$a; // $a が返される。 

配列をAPIで返されるときに、適当にアクセスしたらNULLが山盛りになってパニクった。

参考資料

https://www.php.net/manual/ja/language.operators.comparison.php#language.operators.comparison.coalesce

ヌーラボ・バックログのAPIライブラリを作った

ヌーラボバックログAPIにアクセスしたかった。

Nulabのドキュメントをみていて、BacklogのAPIライブラリを探したのですが。PHPでサクッと使えそうなのがなかった。

https://developer.nulab.com/ja/docs/backlog/libraries/#%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA

Swaggerもないし、ドキュメントをスクレイピングして、全メソッドをマッピングして自動生成。

関数の自動補完が効かないので、ちょっとした作業でドキュメントを何度も開かないといけないので不便。

もう自分で作ったほうが気持ちよく作業できる。

コメントを探し回るのが不便で。

作った

https://github.com/takuya/php-nulab-backlog-api-client

ついでにDBに取り出すものも

バックログのデータをDBに取り出しておけば、アーカイブできるなと思ってバックアップとアーカイブ用のライブラリも作成した。

https://github.com/takuya/laravel-plugin-nulab-backlog-archiver

これで、データベースにデータ取り出した上でどこに何があるかよく分かるようになった。

php の foreach で配列の展開までいっきにやる。list と foreach をまとめて書く。

ふと、他人の書いたPHPコードgithub で眺めていると、次のような記述に遭遇した。

list() と foreach で配列を変数に展開する。

<?php

$a = [ 'a','b' ,'c'];
$arr = [$a, $a , $a ];
foreach($arr as list($x, $y, $z)){
  var_dump($x,$y,$z);
}

とても楽ちんに書ける。本当に楽。list 関数と foreachで代入までまとめてできる。

このような連想配列でも

<?php
foreach ($cols as $col){
  $name = $col['name'];
  $type = $col['type'];
  $constraint = $col['constraint'];
}

やばいぐらいスッキリする。

<?php
foreach ($cols as list('name'=>$name,'type'=>$type,'constraint'=>$constraint)){
    var_dump($name);
}
<?php
foreach ($cols as list($name,$type,$constraint)){
    var_dump($name);
}

この書き方は革命だわ。

ただし、配列の添字(キー)が必ず存在する必要があること、サイズが同じであること、が前提条件になる。

エラーは出やすくなるが、配列のタイプ型チェックができると考えると、メリットのほうが大きい。

知らなかったよ。気づかなかったよ。

連想配列を一気に変数に取り出す。

<?php
$a = [ 'a'=>1,'b'=>2 ,'c'=>4];
$arr = [$a, $a , $a ];
foreach($arr as list('a'=>$x, 'b'=>$y, 'c'=>$z)){
  var_dump([$x,$y,$z]);
}

list と foreach を組み合わせることで代入が必要なくなって、処理が明確になる。

なんで今まで気づかなかった。

Guzzle で name[]=1 のような添字なしのリクエストを組み立てる。

Guzzle さんは、内部的に http_build_query を使っている。

そのため、次のようなリクエストのパラメタを渡すと。

$gz->request('GET' , [ 'name'=>['a','b','c']] )

次のようなリクエストを作り出す。

?name[0]=a&name[1]=b&name[2]=c

これは、あらゆるHTTPサーバーが配列として解釈してくれるかといと、全然ダメ。

サーバーのよっては次のようなリクエストを欲しがる場合がある。

?name[]=a&name[]=b&name[]=c

また、サーバーによっては、チェックボックスを使ったフォームのようなHTTPリクエストを欲しがる場合もある。

?name=a&name=b&name=c

どうするのか。

結論 置換する。

あれこれ模索したが、単純に置換するのが早い。

<?php
// 配列が name[0]=value になるのを避けて、name[]=value にする
$str = http_build_query( $data, $numeric_prefix=false, $arg_separator = null, $encoding_type );
$str = preg_replace( '/%5B(?:[0-9]|[1-9][0-9]+)%5D=/', '%5B%5D=', $str );
$cli->request('GET', ['query'=>$str],$options );

POSTの場合はどうするのか

POSTの場合も同じである。

$cli->request('POST', ['form_data'=>$str],$options );

Guzzleに渡す配列のキーが異なる。HTTPリクエストのボディに入れるのか、クエリ文字列にするのか、この違いだけで使う文字列とエンコーディングは同じである。(application/x-www-form-urlencoded )

このような問題があるのでHTTPリクエストで配列を渡す仕様はとても不安定になりやすい。

この問題は、エンコーディングapplication/JSON が選ばれている理由でもあるのだろうと思います。

クエリ文字列やform-urlencoded (ボディ)でリクエストに配列を送信するのは、枯れてているHTTPリクエストにおいて解決しない問題ですよね。

参考資料

https://github.com/guzzle/guzzle/issues/2252

配列インデックスの連番に欠番が出たときに、欠番をnullで埋める。

php の配列はすべてが連想配列なので、連番に欠番が出る。

欠番を「除去」したいときは、array_values で欠番を除いて再度の採番をすればいいのだけど、「欠番」を「特定の値で埋めたい」ときにはどうすればいいか。

欠番を数字で埋める。

<?php
$a = [ 1=>'a', 3=>'c', 4=>'d', ];

$seq_keys = range(min(array_keys($a)),max(array_keys($a)) );
$b = array_replace(array_fill_keys($seq_keys,'未設定'),$a);
var_dump($a,$b,$c);

実行結果

array(3) {
  [1]=>
  string(1) "a"
  [3]=>
  string(1) "c"
  [4]=>
  string(1) "d"
}
array(4) {
  [1]=>
  string(1) "a"
  [2]=>
  string(9) "未設定"
  [3]=>
  string(1) "c"
  [4]=>
  string(1) "d"
}

array_map を使うともっと記述がシンプルかも

<?php
$c = array_map(fn($idx)=>$a[$idx]??'未設定',$seq_keys);

もっといい方法がないのだろうか。

WebAPIの無駄呼び出し回数をGeneratorで減らしつつ、コードの書き換えはできる限り減らしたい。

二律背反の要求を実現したい。

WebAPIの無駄な呼び出し回数を少しでも減らしたい。回数を減らし、プログラム応答速度を上げたい。

ただ、コードの書き換えは極小にしたい。

この目的では、Generatorを使うのが、鉄板だと思うんですよね。

WEBを呼び出すコードのサンプル。

次のようなAPIを叩くコードがあってさ。

<?php
function my_list_items(){
  foreach( $api->call('list_item') as $id){
    $list[]=$api->call('get_item', $id);
  }
  return $list;
}
$items = $my_list_items();
$item = $items[0];

要素を全部使わないのに、毎回全部取ってしまうんですよね。もったいないですよね。API呼び出し回数に時間あたり制限とかあったり、高頻度呼び出しが攻撃判定されちゃったりして辛いんですよね。

Generator ( yield ) で解決する。

yield を使えば、だいたい解決する。

<?php
function my_list_items(){
  foreach(  $api->call('list_item') as $id){
    $item  = $api->call('get_item', $id);
    yield $item; //<=yield を使って書き換える。
  }
}
//
$items = $my_list_items();
$item = $items[0];//<= ここがエラーになる。

yield を使えばいいの。だいたい解決する。だけど、yieldの結果はGeneratorである。GeneratorだとArrayとはアクセス方法が異なる。foreach互換性(ある程度)がある。ただ、Arrayとの互換性が全くない。コードの修正箇所が増えてしまう。修正箇所を探すだけでも困難だ。

<?php
$item = $items[0];//<= 配列のアクセスが使えない
$item = $items->corrent();//<= メソッドを使う必要がある。

とくに、current()のようなメソッドを使って先頭を取るのが、ソースコードの可読性を著しく損なうというか。逆に型がわかりにくい。

CachingIterarorを使う。

php の場合。CachingIteraror を使うのが鉄板だが、CachingIterarorには問題がある。

<?php
function my_list_items(){
  return new \CachingIteraror((function(){
  foreach(  $api->call('list_item') as $id){
    $item  = $api->call('get_item', $id);
    yield $item;
  }  
  })());
}
$items = $my_list_items();
$item = $items[0];//<= BadMethodCallException になる。

CachingIterarorだと、一見すると解決しそうなんだけど、キャッシュするまでは、BadMethodCallException になる。( php 8.2 でも)

一度キャッシュする必要がある。

<?php
$items = $my_list_items();
$item = $items[0];//<= BadMethodCallException になる。

キャッシュするまでアクセス不能で、キャッシュ後は気軽にアクセスできるって、コードの見通しが良くない。配列でアクセスしたいのか、手続きに他の要因が関連してるように見えてしまい、コードのメンテナンス性能を喪失してしまう。

<?php
$item = $items[0];//<= BadMethodCallException になる。
$items->corrent();//<=キャッシュする。
$item = $items[0];//<= アクセス可能になる。

こんな問題があってGeneratorは避けてたんだけど、APIコールが遅いのでなにか手を考える必要があって。やっぱりGeneratorしか無い。となるが、Generatorを見せないで使いたい。

そのうち、公式で解決しそうなんだけど。解決しそうなんだけど、って思ってたのだが、公式では解決しそうに無い。今後の更新も型システムについてばかり。*1

中間クラスがほしい。

ArrayAccessをサポートしつつCachingもサポートしたそんなクラスがほしい。

<?php
function my_list_items(){
  return new \MySomeClass((function(){
  foreach(  $api->call('list_item') as $id){
    $item  = $api->call('get_item', $id);
    yield $item;
  }  
  })());
}
$items = $my_list_items();
$item = $items[0];//<= コードの書き換えがない。

とりあえず作った。

yieldを使うときの、このような問題を手軽に解決したくて、コードを書きました。

github.com

もっといい解決方法はないのだろうか。

*1:PhpStromのようなIDEを使わないカジュアルなPHP'erの存在が裾野の広さで、そこがエコシステムの根底なのに、その裾野をバッサリ切る感じはちょっとどうかと思う。正直言ってPHPは型の硬さが入ることで相当数の利用者が離脱し、便利さが再発見されるまでに相当時間がかかりそう。

trait で関数名が衝突した場合。名前かぶりを別名にする。

trait を use するときに名前がかぶった。

同じ名前の関数・変数があって、名前が衝突してエラーになる。

対応するには、use を使うか、優先順位を変える。

名前の衝突する trait の例

次の例では say() が衝突してしまう。

<?php

trait  A {
  public function say () {
    echo "I'm A\n";
  }
}

trait  B {
  public function say () {
    echo "I'm B\n";
  }
}

use の中で衝突を回避する。

A::say insteadof B を書けば無事に動作する。

<?php
class MyUSer {
  use A, B{
    // A::say insteadof B::say の意味。B::say が冗長なので省略されている。初見殺し。
    A::say insteadof B;
    B::say as b_say;
  }
}

$u = new MyUSer();
$u->say();
$u->b_say();

?>

instead of は英語で、代替するという意味になる。なので、このUse句は、次のような意味だとわかる。

use A::say insteadof B::say

これはあまりに冗長なので、

use A,B{
  A::say insteadof B
}

と表現しているのでしょう。

別解、上書き。

B::say が不要なのであれば、Trait中でTraitを上書きしてまえば良い。

<?php

trait  A {
  use B
  public function say () {
    echo "I'm A\n";
  }
}

trait  B {
  public function say () {
    echo "I'm B\n";
  }
}

こうすれば、上書きで解決する。意外とスッキリするのでこの方法もありかもしれない。

ただし、Reflection時にgetTraitできなくなるので、Traitの関係を辿る必要がある。

ping のタイムアウトを明示する。(応答なしを明示)

送ったパケに対して、応答があったのかなかったのか。

ping -O 192.168.1.1

-o は次のようになっている。

  -O                 report outstanding replies

これは、BSD/macOSpingタイムアウト表示に近くなるので好きな人はこっちのほうが好きだと思う。

1つずつ表示してくれるので信頼できるかもしれない。経路のルータがUnreachableを返すまで待ってられないというか。

ちゃんと、PINGは送ってる。というのがわかるし、いま何番まで送ったかがよく分かる。

実例

-O がないとき

何も表示されない。

一定タイミングでRouterからUnreachableがまとめて届く。

まとめて届くまで、待ってられないというか。状況を逐次知りたい。そういうときに-O をつけておくと安心できます。