hgot07 Hotspot Blog

主に無線LANや認証連携などの技術についてまとめるブログです。ネコは見る専。

Edgecore EAP105のWireGuard VPN機能を試す

EdgecoreのWi-Fi 7対応機、EAP105に、待望のWireGuard VPN機能が付きました。3年ほど前から、無線LANアクセスポイントにはオープンで高速なVPN機能が欲しいよと、お願いしていたものです。 

なんで「オープンで高速」なのかというと、何度か書いてきたように、ケーブルの部分で通信を保護したいことと、大して速くもないのに割高になるのが不可避な専用コントローラ (WLC)ではなくLinux箱などで受けたいことが理由です。EAP101の記事から抜粋します。

hgot07.hatenablog.com

顧客 (オーナー) の店舗に転がすようなマネージドWi-Fiの用途では、オーナーに自前の回線への接続を任せることがあります。このような設置方法の場合、通信事業者の立場では、フリーWi-Fiトラフィックをオーナーや他の来客のいたずらから保護するために、ケーブルの部分をVPNで保護するなどの対策が欲しいものです。

 

なお、この記事ではWireGuardのみ扱い、無線LAN部分については一切触れません。

 

EAP105とは

とりあえず、バァーーン!

Edgecore EAP105

Edgecore EAP105

裏面はこう。全面がヒートシンクになっている感じです。

Edgecore EAP105 (裏面)

Edgecore EAP105 (裏面)

電源供給が従来の丸型DCジャックではなくPD3.0になっているところが目新しいです。もちろんPoEにも対応しています。

製品情報はこちら。なかなか魅力的なスペックです。

wifi.edge-core.com

 

プロセッサはQualcomm Dragonwing N7 Platform IPQ5332のようです。以前Immersive Home 3210 Platformと呼ばれていたものが、改名されたとのこと。

www.qualcomm.com

OSはいつものOpenWrtベースですが、例によってopkgによるパッケージマネージャは塞がれています。

 

EAP105のWireGuard VPN

Firmware v12.6.7から対応しています。

今のところ、ecCLOUDには設定項目がなく、スタンドアロンで使うことになります。ecCLOUDに登録した状態でクラウド側の設定がプッシュされると、WireGuardの設定が無効になってしまいます

設定画面はあっさりとしていて、こんな感じです。

WireGuardの設定画面

WireGuardの設定画面

ただし、ここだけではないので要注意です。設定項目が分散して分かりにくいのは、OpenWrtの悪いところですね。ファイアウォールの設定をしないと、パケットが流れません。

ファイアウォールの設定

ファイアウォールの設定

ip ruleで見ると、WireGuard VPNトラフィックを分離するようなルールが入っていました。

 

本体からWireGuardサーバに到達できない?

WireGuardの設定はサクサクと進んで、wg show で見るとリンクアップしているのですが、コマンドラインからサーバ側のIPアドレスを叩くとなぜか

ping: sendto: Operation not permitted

になってしまいました。これでは、RADIUSパケットを通したり、リモートから管理画面に入ったりという、本当に使いたかった機能がダメです。ip ruleが設定されているので、素直には通してくれないようです。

散々悩みました_(゚。3」∠)_

/etc/config/network を見ると、まずは一つ落とし穴が。route_allowed_ipsを '1' に設定しないといけません。(なんで '0' のままだったの?)

/etc/config/networkの設定

/etc/config/networkの設定

次に、/etc/config/firewall にも落とし穴が!変更前の値はこのとおりで、input/output/forwardが全部DROPになっていました。

/etc/config/firewallの設定

/etc/config/firewallの設定

ACCEPTに変更して、やっと開通ヽ(・∀・)ノ

これでVPNサーバにRADIUSの通信を集約したり、サーバを踏み台にして基地局の管理画面にアクセスできるようになります。

 

WireGuard VPNの性能

最近のCPUで、そこそこ上位の機種なので、500Mbpsぐらい出るものと期待していました。初めに、下りが300~400Mbpsぐらい出る自宅の光回線で試してみたところ、ありゃ?50Mbpsしか出ない……???どうも上りが遅いのに引っ張られているようです。あと、盆休みで回線が混雑していた兆候もあったので、とりあえず保留。

仕事場のLAN (GbE)で試してみたところ、予想通りの性能が得られました。

Speedtest結果

Speedtest結果

Speedtestで上下530Mbps出ています。

次に、VPNサーバのIPアドレスをめがけて iperf3 を試してみると、こんな感じ。

iperf3の結果

iperf3の結果

正直なところは、700Mbpsぐらい出るCPUだとよかったのですが、フリーWi-Fiの用途では十分かもしれません。Wi-Fi 7の速度はうたえないことになりますが、速度が欲しい場所は、いたずらされないようにガッチリとケーブルを固定して、VPN無しで設置しましょう。

 

他社も追従して、オープンなVPN機能を搭載してくれたらいいのになぁ

あと、Edgecore / IgniteNet さんは、EAP105以外のモデルにもVPN機能を追加して、ecCLOUDやecOPENから設定ができるようにしてほしいところです。VPN箱は自前で高速なものを用意するから。

欲を言えば L2TP/IPsecOpenVPN のソフトウェアも入れておいてほしい。

 

おしまい

無線LANローミングから見たNIST SP 800-63-4

先日、待望の Digital Identity Guidelines Revision 4 がリリースされました。

www.nist.gov

これが無線LANローミングにどう関係するのか、なんとなく気付いたら、認証連携をやり込んだ人ではないでしょうか。

 

無線LANローミングの本体は認証連携

無線LANローミングの中心は、認証連携と信頼関係 (トラスト) です。無線通信ではありません。

ローミング環境では、利用者にネットワークサービスを提供する組織はアクセスネットワークプロバイダ (ANP) と呼ばれ、利用者のアイデンティティを保持して認証処理を行う組織はアイデンティティプロバイダ (IdP) と呼ばれます。ANPはサービスプロバイダ (SP) と呼ばれることもあります (eduroamなど)。

ANPは、IdPが利用者をきちんと把握しているという前提の下に、ネットワークサービスを提供するという枠組みになっています。すなわち、ANP-IdP間に信頼関係が必要です。

例えば、利用者が何らかの不正利用を行った場合、外部から見えるのはANPのネットワークです。外部の者には、ANPの組織に不正利用者がいるようにしか見えません。しかし、ローミング環境にはプライバシー保護の仕組みがあり、利用者が実際に誰なのかをANPが把握できないのが通例です。苦情が届いた場合、ANPは自己の責任を回避したいので、当該利用者のIdPに捜査の目を向けさせなければなりません。

IdPは、どの人物に無線LANのアカウントを発行して、利用を許可したのかを、ある程度把握しておく必要があるでしょう。利用者が匿名ならば、不正利用の責任の一部をANPやIdPが負う恐れが出てきます。

もしIdPが無責任ならば、ANPはそのIdPの利用者に対して、サービスを提供したくないでしょう。そこで、ANPはIdPに対して、一定の Identity Assurance Level (IAL) というものを要求することになります。

 

NIST SP 800-63-3のIAL定義

Revision 3 では、IAL1, IAL2, IAL3と、3段階のIALが定義されていました。このうち、無線LANローミング基盤では IAL1, IAL2 が重要です。

csrc.nist.gov

Revision 3におけるIALの定義は、このようなものでした。

IAL1: There is no requirement to link the applicant to a specific real-life identity. Any attributes provided in conjunction with the authentication process are self-asserted or should be treated as such (including attributes a Credential Service Provider, or CSP, asserts to an RP).

IAL2: Evidence supports the real-world existence of the claimed identity and verifies that the applicant is appropriately associated with this real-world identity. IAL2 introduces the need for either remote or physically-present identity proofing. Attributes can be asserted by CSPs to RPs in support of pseudonymous identity with verified attributes.

IAL3: Physical presence is required for identity proofing. Identifying attributes must be verified by an authorized and trained representative of the CSP. As with IAL2, attributes can be asserted by CSPs to RPs in support of pseudonymous identity with verified attributes.

IAL2でも、利用者の実在性を確認する必要があり、様々なオンラインサービスでもなかなか達成が難しいものです。フリーWi-Fiの用途ではなおさらです。

IAL1は、要するに自己申告でしかなく、匿名利用と変わりないことから、フリーWi-Fiの登録に使うには物足りないということになります。不正利用者に辿り着くヒントがないのですから。従って、Revision 3ではザルのIAL1を要求しても意味がなく、OpenRoamingによるフリーWi-Fiでも、IAL2を目指すしかないという、厳しい状況でした。

 

NIST SP 800-63-4のIAL定義

無線LANローミングでも、責任の所在を明らかにするという観点で、利用者の紐づけを確実にするIAL2を目指したいという点は従来と変わりありません

しかしながら、フリーWi-Fiのサインアップを利用者に行わせる場合に、本人確認はなかなか難しいという現実があります。例えば、クレジットカードの情報まで確認できるならば、利用者の紐づけは間接的に実現できるでしょう。フリーWi-Fiを使うのにクレジットカードの登録まで要求したら、利用者は大いに躊躇することでしょう。

結局、現在のフリーWi-Fiでは、電話番号やメールアドレス、SNSアカウントを確認することで、お茶を濁しているという状況です。利用者の紐づけとしては不十分ながらも、多くの場合は利用者に辿り着く有力なヒントが得られるため、現在の落し所はこのレベルということになります。

さて、Revision 3のIAL1は先のとおりザルなので、ANPがIdPにIAL1を要求したところで無意味でしょう。このため、IAL2に近づけるように努力せよとしか言えなかったわけです。

Revision 4でIALの定義が変更され、多くのサービスが期待するものに近づきました。

IAL1: IAL1 supports the real-world existence of the claimed identity and provides some assurance that the applicant is associated with that identity. Core attributes are obtained from identity evidence or self-asserted by the applicant. All core attributes are validated against authoritative or credible sources, and steps are taken to link the attributes to the person undergoing the identity proofing process.

IAL2: IAL2 requires collecting additional evidence and a more rigorous process for validating the evidence and verifying the identity.

IAL3: IAL3 adds the requirement for a trained CSP representative (i.e., proofing agent) to interact directly with the applicant, as part of an on-site attended identity proofing session, and the collection of at least one biometric.

IAL1の定義を見ると、ある程度確実な情報とリンクすることが求められています。「ある程度」がどれぐらいのものか、微妙なところはありますが、従来のIAL2より達成しやすく、匿名同様だったIAL1と比べたら制約が明確になってきたと言えます。

無線LANローミングの世界で言うと、以前はIAL1を要求する意味がなかったところ、これからはIAL1を一つの基準として要求できるという、大きな進歩になりそうです。

 

電話番号やSNSアカウントで十分なのか?

ごもっともな疑問です。

実際、どの程度の紐づけができれば十分なのか、明確な基準がありません。色々な組織で話し合って、今のバランスに落ち着いているというのが現状でしょう。

Gmailのようなフリーメールなら即ダメかというとそうでもなくて、電話番号やクレジットカード番号が登録されているなら、そのアカウントはある程度信頼できるものでしょう。IdPが捜査に協力するかどうかは、また別の問題です。この辺もまだ無線LAN業界で議論の真っ最中です。

国によって、温度差があるのも事実です。国によっては、空港でパスポートを見せないとフリーWi-Fiのアカウントがもらえなかったりします。一方、そのような国でも、空港内のカフェでさえパスワードが貼り出されているなど、運用がかなり緩いことがあります。十数年前のドイツでは、ホテルで個別ID/PWが印刷された紙切れを渡されることがあったのですが、フロントのカゴから適当に拾えたりしました。カゴじゃなくてザルだったかも。

 

以上、発散してきましたが、少し進展があったという話でした。

おしまい

Apache 2.4 + mod_auth_openidc でJWT認証を実現してみる

APIの認証ではよくJSON Web Token (JWT)が使われている……」なんてあちこちに書かれているので、そんなに面倒なことはないだろうと思っていたら、結構な いばらの道 でした (2025年7月時点の話)。JWTの構造自体は単純で、そこそこ年数も経っているのに。

AlmaLinux 9のパッケージだけで、なんとか認証までたどり着くことができたので、自分用のメモとして残します。色々とウソもつかれたけど、調査を助けてくれたCopilotくんに感謝!

やりたかったこと:

  • 異なる組織、異なる計算機にまたがってアプリを連携させたいので、APIのための安全で軽量な認証がほしい。(Basic認証IPアドレス制限では怖い)
  • できればそこそこ枯れた新しい技術を使ってみたい。
  • 共通鍵ではなく、公開鍵暗号を使いたい。
  • いつも使っているのがApache HTTP Serverなので、これを使いたい。
  • 自前でビルドするのは避けたい。

つまずいたのは、ざっとこんなところです ↓

  • Apache用のJWT認証というと、まっさきに mod_auth_jwt が出てくる (Copilotくんも)。しかし、肝心のリポジトリが見当たらない。似たようなものがGitHubに幾つかあるものの、パッケージで提供されているようなものではない。
  • Apache 2.5には mod_autht_jwt というモジュールがあり、これがApacheの本命に見える。しかし、2.5はまだ出ていない。
  • とりあえずトークンを自前で作ってみようとしたら、署名がうまく行かない。

今回、本当に綱渡りで動かしたので、バージョンが違うと動かないことがあるかもしれません。それはそれで、ソフトウェアの品質としてどうなのという思うところはあります。Apacheには早く mod_autht_jwt を提供していただきたいところ。

以下、ノークレームで!

 

Apache 2.4でJWT認証できる簡単な方法を探す

まず、Copilotくんに聞いてみたら、「mod_auth_jwt というのがあるよ」とのこと。使い方も簡単に見えたので、いざAlmalinuxでdnf searchしてみたら、ない、ないよ……

Copilotくんが指し示したリポジトリは 404 でした。

他に使えそうなものを探しまくっていたら、Apache 2.5にはmod_autht_jwtがあるとのこと。ところが、2.5なんて出ていないんですよねー。

さらにCopilotくんと頑張って探し続けていたところ、ポロリと有用な回答が。

「それ、mod_auth_openidc でもできるよ!」

OIDC特有のリダイレクト機能などを使わず、JWTの認証部分だけを使えばよいとのこと

 

mod_auth_openidc の設定例では動かなかった

ドキドキしながら、AlmaLinux 9でdnf searchしてみたら、ありました、mod_auth_openidc-2.4.10-1 !

Copilotくんが示した Complete working example がこちら ↓ (httpdの設定ファイルの中に書くもの)

OIDCValidateJWT On
OIDCJWTIssuer https://your-issuer/
OIDCJWTVerifyJwksUri https://your-issuer/.well-known/jwks.json
OIDCJWTRequiredClaim aud your-audience

<Location /api>
    AuthType openid-connect
    Require valid-user
</Location>

さっそくhttpdを再起動してみたら……、立ち上がりません _('、3」∠)_

そもそも Syntax error なので、重症です。

 

色々と戦った後に得られたのがこちら ↓ 

OIDCOAuthVerifyJwksUri https://your-issuer.example.com/.well-known/jwks.json
OIDCOAuthRemoteUserClaim sub
OIDCOAuthVerifyClaims "iss=your-issuer" "aud=your-audience"

 

<Location /api>
    AuthType oauth20
    Require valid-user
</Location>

まだ、立ち上がりません _('、3」∠)_

OIDCOAuthVerifyClaimsがおかしいので、削除してみたところ、httpd が動き始めました。

(認可周りの調査は今後の課題)

 

ES256にチャレンジしてみる

結論から述べると、ES256で特に面倒が増えるという感じはなかったです。強いて言えば、情報が少ないぐらい。

さくっと鍵を作ってしまいます。

$ openssl ecparam -name prime256v1 -genkey -noout -out privkey.pem
$ openssl ec -in eckey-pair.pem -pubout -out pubkey.pem

JWTは、<ヘッダー>.<ペイロード>.<署名> というシンプルな構造をしているので、とりあえず簡単なヘッダーとペイロードを作って、BASE64URLエンコードして、ピリオドで連結しておきます。

$ echo -n "{\"alg\":\"ES256\", ...}" | basenc --base64url -w0 > tmp.txt
$ echo -n "." >> tmp.txt
$ echo -n "{ペイロード...}" | basenc --base64url -w0 >> tmp.txt

改行コードが入らないように細心の注意を払います。不安だったら hexdump -Cv で確認。

 

署名の罠

「あとは署名だから openssl dgst -sha256 -sign privkey.pem tmp.txt で簡単やろ」と思ったら、落とし穴がわんさか……

opensslで作った署名をBASE64URLエンコードしてくっつけても、JWTデコーダー に突っ込んでみると Invalid Signature になってしまいます。

ES256 (ECDSA P-256)

    The signature is a concatenation of two 32-byte numbers: R and S.
    Many libraries and tools (like OpenSSL) output ECDSA signatures in ASN.1 DER format, but JWT expects the raw format:
        Raw format: [R (32 bytes)] || [S (32 bytes)] (total 64 bytes)
        Then base64url-encoded without padding.

ぁー、形式が違うんですね。

Copilotくんに言われるまま、

$ openssl asn1parse -in signature.der
Error: offset out of range
$

ありゃ?

-inform der が要ります。Copilotくんの嘘つき!(

いや、この辺の処理が面倒くさいので、perlスクリプトを書いてしまった方が早いです。use Crypt::JWT qw(encode_jwt); して、秘密鍵を読み込ませて、以下の感じで行けます (なげやり)。keyを参照で渡さないといけないところに気付くまでさらにウン十分。

my $j = encode_jwt(
        payload => $payload,
        alg => 'ES256',
        key => \$privkey,
        extra_headers => {typ=>'JWT',kid=>'012389'},
);
print $j; 

やっとこさ、JWTデコーダー で Signature Verified になりました。

 

ウェブサーバに公開鍵を仕込む

jwks.json というファイルに、こんな風に書きます。

{
  "keys": [
    {
      "kty": "EC",
      "kid": "012389",
      "use": "sig",
      "crv": "P-256",
      "alg": "ES256",
      "x": "...",
      "y": "..."
    }
  ]
}

はて、xとyは?

以下のようにすると、公開鍵の中身が表示されます。

$ openssl ec -in pubkey.pem -pubin -text -noout

pub: の項目で、最初の 04 を除くと、残りが32バイトのx座標値と、32バイトのy座標値です (P-256/ES256なので)。コロンを削除して、16進数表記をBASE64URLに変換して、パディングを削除して……って、やってられんわー・・・・・(ノ`Д´)ノ彡┻━┻

(自動化すべき)

[2025/7/31追記] 以下のperlスクリプトで x, y 座標値が表示できます。

#!/usr/bin/perl

use Crypt::PK::ECC;
use MIME::Base64;
use JSON::PP;

# Read public key from PEM
my $pem = do { local $/; open my $fh, '<', 'pubkey.pem' or die $!; <$fh> };
my $pk = Crypt::PK::ECC->new(\$pem);

my $pub = $pk->export_key_jwk('public');

my $json=JSON::PP->new->pretty->canonical;

print $json->encode( $json->decode($pub) );

 

結果

できました!ヽ(・∀・)ノ

Hello, JWT!

こんな感じで動作確認できます ↓

$ curl -H GET 'https://api.example.com/api/index.txt' -H 'Content-Type:application/json;charset=utf-8' -H 'Authorization: Bearer <作成したトークン>'

念のため、JWTを送らなかったり、わざと別のJWTを送ってみたりして、アクセスが拒否されることを確認します (重要)。

ペイロードに exp を埋め込んでおくと、その時刻以降は"正しく"認証が通らなくなります (確認済み)。

 

おしまい

openssl v3互換の暗号化をPerlで実現する方法・高速化編

前回のこれの続きです。日が開くと面倒くさくなるのでサクサクと。

hgot07.hatenablog.com

前回課題になっていた部分です。

  • 頻繁に起動されるスクリプトなので、暗号強度が少し下がっても、それなりの速度が欲しい。
  • 一定時間は同じ入力に対して同じ暗号文を生成したい。つまり、saltとIV (Initialization Vector)を使いまわせるようにしたい。

openssl v3 のコマンドは、以下を想定しています。

$ openssl enc -pbkdf2 -salt -aes256 -base64 -k hogefugapiyopiyo -e -in plain.txt -out cipher.txt
$ openssl enc -pbkdf2 -salt -aes256 -base64 -k hogefugapiyopiyo -d -in cipher.txt -out decrypted.txt

暗号化のモードは、AES256 CBCモードです。同じsaltとIVが指定されると、同じ入力に対して同じ出力が得られます。つまり、処理時間のかかる PBKDF2 などの鍵導出は最初の一回だけにして、しばらくsaltとIVを使い回すことで、高速化できそうです。

 

[2025/7/1追記]

重要:IVの再利用は暗号化にとって致命的な問題を生むことがあります。モードによっては即時アウトです (AES-GCMなど)。CBCモードでも安全性が著しく低下するとされているので、用途を十分に検討すべきです。

saltとIVを明示的に与える暗号化

結論から言うと、正解例はこれです。PBKDF2の計算を先に行い、求めたsaltとIVを使ってCrypt::CBCで暗号化します

use MIME::Base64;
use Crypt::CBC;
use Crypt::PRNG;
use Crypt::PBKDF2;

my $salt = Crypt::PRNG::random_bytes(8);

my $pbkdf2 = Crypt::PBKDF2->new(
        hash_class => 'HMACSHA2',
        iterations => 10000,
        output_len => 32+16,
        salt_len => 8,
);
my $ekr = $pbkdf2->generate('hogefugapiyopiyo', $salt);
my @ek = split(/:/, $ekr);
my $key_iv = decode_base64($ek[3]);

my $key = substr($key_iv, 0, 32);
my $iv = substr($key_iv, 32, 16);

my $crypt = Crypt::CBC->new(
        -pass    => $key,
        -keysize => 32,
        -cipher => 'Crypt::Rijndael',
        -iv => $iv,
        -pbkdf  => 'none',
        -header => 'none',
);

my $cipher = "Salted\x5f\x5f".$salt.$crypt->encrypt( 'I love you, too!' );

前回と比べるとだいぶ長くなりました。

この出力 $cipher をBase64エンコードして cipher.txt に書き込めば、前述の openssl enc -d で復号できるはずです。

 

ハマったところの説明

ウェブ検索で見つかる記事や、GitHubのCopilot君では、なかなか解決のヒントに辿り着けませんでした。ハマりどころは以下のとおりです

まずCrypt::PBKDF2 の出力をそのまま Crypt::CBC に渡すコードをよく見かけるのですが、形式が違います。 Crypt::PBKDF2 は RFC 2307 形式で鍵を出力します。printしてみるとこんな感じです。

{X-PBKDF2}HMACSHA2+256:AAAnEA:aTLiFshBDHw=:33CUEBmlL9J42BVhbfH2i7X0NGtAwIQbkj0NJExNA0/nxnPYpNAHEbtfOzAt0FL1

コロンで区切られた最後のフィールドが、Base64エンコードされた鍵です。

次に、IVをどうしたらよいのかというところで、なかなか正解にたどり着けなくて苦労しました。ウェブには任意の乱数をIVとして使うコードばかりありますが、これではダメです。自前で生成した乱数saltと、前回のコードで Crypt::CBC の中で計算されたIVのペアを使うと、今回のコードの後半部分は正常に動きます。ところが、IVを別に与えると復号できないのです。

正解は「暗号化の鍵とIVをPBKDF2の中で一緒に求める」というものです。

わかるかーい!・・・・・(ノ`Д´)ノ彡┻━┻

AES256 (CBC) では、鍵長が32バイトです。これにIV分の16バイトを足した値を PBKDF2 で生成して、前半を鍵、後半をIVとして使います。

みっつめは、Crypt::CBC のencryptの出力にsaltが付いていないことです。仕方がないので自分でヘッダーを付けます。

 

PBKDF2::Tiny で高速化する

そうです。同じスクリプトの中で PBKDF2 の計算をしているので、前回のコードから高速化できていません。Ryzen 7 PRO 4750GのPCで動かしてみると、user時間が70~80 msもかかっています。realは100 msぐらいです。おっそ……

調べてみると、実は Crypt::PBKDF2 のモジュールをロードする部分でも時間を食われていることがわかりました。

手っ取り早く高速化するのに、PBKDF2::Tiny を使う方法があります。"Minimalist PBKDF2 (RFC 2898) with HMAC-SHA1 or HMAC-SHA2" だそうです。これを使うと、ロード時間が短く、コードもグッとシンプルになります。

require PBKDF2::Tiny;
        $key_iv = PBKDF2::Tiny::derive('SHA-256', 'hogefugapiyopiyo', $salt, 10000, 48);

user時間が40~50 ms、realが60 ms程度になりました。

 

saltとIVをKVSに保存して高速化してみる

スクリプトが呼ばれるたびにsalt/IVが新しくなってしまうので、当初の目的が達成できていません。毎回 PBKDF2 しなくても済むように、salt/IVを再利用することを考えます。といっても、ファイルに書き出すのでは遅いので、オンメモリの KVS (Key Value Store) などを使うのがよさそうです。Redis / Valkey でできそうですね。

というわけで、試してみたのですが、なんと Redis のモジュールをロードするのに60 msぐらいかかることが判明_('、3」∠)_ 本末転倒な結果に。

そこに神様がいました。Redis::Fast というモジュールを使うと、ロード時間が大幅に短縮されます。キャッシュしたsalt/IVを再利用することで、user時間が20 ms程度まで短縮できました。

 

(応用先が) 芽出たし、芽出たし。

openssl v3互換の暗号化をPerlで実現する方法

とある技術の実装 (インプリメンテーション) において、Perlスクリプトで暗号化を行う必要が生じました。opensslコマンドでも同じデータを扱えるようにしたかったので、色々と調べてみました。意外とハマりどころが多かったので、メモを残します。openssl 1系ではなく、3系です。

やりたいこと:

  • openssl enc -aes256 と互換性のある暗号化・復号をPerlスクリプトで行いたい。
  • もちろん既存ライブラリを使って楽をしたい、というより、ヘタに自前で実装するよりも安全なのでそうすべき。
  • 頻繁に起動されるスクリプトなので、暗号強度が少し下がっても、それなりの速度が欲しい。
  • 一定時間は同じ入力に対して同じ暗号文を生成したい。つまり、saltとIV (Initialization Vector)を使いまわせるようにしたい。(今回は省略、別記事を計画)

 

まずはopensslの使い方

暗号化と復号のコマンドは、こんな感じです。

$ openssl enc -pbkdf2 -salt -aes256 -base64 -k hogefugapiyopiyo -e -in plain.txt -out cipher.txt
$ openssl enc -pbkdf2 -salt -aes256 -base64 -k hogefugapiyopiyo -d -in cipher.txt -out decrypted.txt

ウェブ検索で見つかる古めの記事には -pbkdf2 がなかったりします。openssl v3では、-pbkdf2 がないとこんな風に怒られてしまいます

*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.

このたいそうな名前のオプションは一体何なのかというと、鍵導出関数 (KDF, Key Derivation Function) のアルゴリズムとして PBKDF2 を使えという意味です。パスワードをそのまま使うと安全性が低いので、ハッシュ値を新たな鍵として暗号化に使うというのが、現代的なやり方です

なお、Diffie-Hellman鍵共有で導出された鍵も、ハッシュ化してから使うことが推奨されています。

 

Perlで実装するには

Crypt::CBCを使うのがメジャーなようで、ウェブ検索でよく引っかかります。ところが、どれを試してみても、エラーになるか、openssl v3で展開できないデータが出てくるかで、結構苦労しました。

GitHubのCopilot君にも色々と聞いてみましたが、なかなか解決のヒントに辿り着けませんでした。

とりあえず、手っ取り早く暗号化するなら、正解例はこれです

use Crypt::CBC;
use Crypt::PRNG;

my $salt = Crypt::PRNG::random_bytes(8);

my $crypt = Crypt::CBC->new(
        -pass    => 'hogefugapiyopiyo',
        -keysize => 32,
        -cipher => 'Crypt::Rijndael',
        -salt   => $salt,
        -iter   => 10000,
        -pbkdf  => 'pbkdf2',
        -header => 'salt',
);

my $cipher = $crypt->encrypt( 'I love you!' );

 

出力の $cipher はバイナリなので、必要に応じてBase64に変換すればよいです。これを cipher.txt に書き込めば、opensslで復号できるはずです。

ivはどこへ行った? はい、モジュールの内部で自動的に生成されます。

 

以下、ハマったところを説明していきます

鍵導出のために Crypt::PBKDF2 を使うコードが、あちこちで例示されています。ところが、少なくとも現在の Crypt::CBC では、Crypt::PBKDF2 の出力をそのまま受け取ることができなくて、エラーになってしまいます。

次に、-cipher => 'Crypt::OpenSSL::AES' と書かれている解説もよくあるのですが、openssl v3互換にするなら Crypt::Rijndael にしなければなりません。

この例では、生のパスワードを -pass に渡していますが、心配には及びません。中できちんと PBKDF2 で鍵導出が行われます。

saltを求めるのに、Copilot君はrand関数と英数字だけを使った例を出してきました。saltはバイナリで与えることができるので、できるだけまともな乱数生成器を使って、バイナリデータで生成すべきでしょう。

 

高速化してみる

こいつ、遅いぞ!

そうです。100バイト程度のデータを暗号化するのに、スクリプト全体で100 ms以上かかりました。

暗号化の部分が遅いのではなくて、CPU時間を食っているのは PBKDF2 の計算です。10,000回 (暗号界の最低限の推奨回数で、opensslのデフォルト値) も回していれば、それは時間食うでしょうねぇ。

どうしても高速化したければ、強度の低下に目をつぶって、以前の実装で使われていた1000回ぐらいまで落とす方法があります。opensslコマンドでは -iter 1000 のように指定できます。自己責任で。

 

以上、openssl互換は実現できましたが、saltとIVを使いまわせるようにするには、さらに困難がありました。これについては次の機会に。

 

おしまい

Passpoint非対応の端末でEXPO 2025 大阪・関西万博のOpenRoamingに接続する方法

ご注意: 私は万博会場に行ったことがないので、自分で検証したわけではありません。おそらく接続できると思いますが、うまくいかなかったらごめんなさい。Passpoint対応の端末を持っていくのが絶対にお奨めです

 

EXPO 2025 大阪・関西万博の会場には、OpenRoamingに対応したフリーWi-Fiがあります。イベントでのOpenRoaming推しは、国内では最大規模でしょう。

www.expo2025.or.jp

OpenRoamingって何?という話は公式サイトにお任せするとして、ここではOpenRoaming / Passpointに非対応の端末で接続する方法を紹介します。規約外の使い方ではありません。 

通信クラスタ()向けなので、ふつうの人にはお奨めしません接続できるかどうか、自己責任でお願いします無線LANで悩んで貴重な時間を無駄にするのはもったいないです。

 

Passpoint非対応の端末とは?

OpenRoamingという無線LANローミングシステムは、Passpointと呼ばれる技術がベースになっています。Passpointを一言で説明しようとすると難しいのですが、「SSIDが変わっても同じ無線LANプロファイルのままWPA2/WPA3 Enterpriseで安全に接続できる仕組み」ぐらいでしょうか。

というわけで、OpenRoamingの基地局に接続するには、Passpointに対応した端末が必要です。最近のAndroidスマホiPhoneWindows 11などは、Passpointに対応しています。

しかーし、少し古め (Android 11以前とか)の端末で、Passpointに対応していない機種がまだ残っています。新しい端末でも、Passpointがうまく動かないバグ持ちのものがあります。

 

WPA2 Enterpriseでつないでみる

WPA2 Enterprise と WPA3 Enterprise 両方を含むのですが、面倒なので、以下ではWPA2 Enterpriseに統一して書きます。(実は技術的にも大差ありません)

Passpointは、WPA2 Enterpriseがベースになっていて、その前に「SSID自動選択機能」が付いたようなものです。接続先のSSIDさえ知っていれば、WPA2 Enterpriseの設定だけでも接続できたりします。

WPA2 Enterpriseって何よ?という人で、もし大学でeduroamを使っていたり、キャリアWi-Fiの1X認証という名前を聞いたことがあれば、そいつのことです。

まずは無線LANに接続するためのアカウントが必要です。万博では Osaka Free Wi-Fi無線LANプロファイルを紹介しているので、それを使うのが無難です。

ofw-oer.com

プロファイルのダウンロードに進んで、利用規約に同意、ソーシャルアカウントでサインインすると、プロファイルがダウンロードできるようになります。

ダウンロードの操作をすると、しばらくして、「プロファイルの設定が失敗される場合はこちらをお試しください」という表示が出ます。

プロファイルの設定に失敗する場合

プロファイルの設定に失敗する場合

これをずっと下までたどっていくと、「ユーザ名、パスワードを作成する」というボタンが現れます。ここでユーザ名とパスワードを確認できます。

ユーザ名、パスワードの作成

ユーザ名、パスワードの作成

あとは画面に従って設定してゆけばよいのですが、一つだけ変更点があります。

万博会場のOpenRoaming基地局 (Cisco提供)のSSIDは、Visitors-WiFi_Expo2025 です。ネットワーク名の欄に、cityroam の代わりに、Visitors-WiFi_Expo2025 と入力します

ワイヤ・アンド・ワイヤレスが提供する基地局には SSID cityroam があるので、Osaka Free Wi-Fiの解説にはそのように書かれているのです。これを万博会場のものに変更するだけです。

端末によっては、証明書のオンライン検証の欄で「検証しない」を選ぶと、うまく接続できないことがあります。「証明書のステータスをリクエストする」の方が成功率が高いようです。

 

iPhone / iPadはどう?

残念ながら、iOS / iPadOS は、Passpointの設定が付与されたSSIDがお嫌いなようです。SSIDをタップしても、ユーザ名とパスワードを入力する画面が出てきません。

Apple Configurator などを使って、自前でWPA2 Enterpriseのプロファイル (.mobileconfig) を作ると、うまく接続できるかもしれません。

 

おねがい

紹介した方法は、万博会場以外の基地局で、OpenRoamingに対応しているけどSSIDが異なる場合にも、応用できます。

参考までに、cityroamというSSIDは、国内のローミング基盤の「Cityroamフェデレーション」に参加している事業者が吹いているものです。

最後にお願いですが、ユーザ名を間違えないように入力してください。例えば wi2 が wl2 になっているなどの間違いがちらほらあります。ユーザ名を間違えていると、端末が再接続を繰り返してしまいます。

もしうまく接続できなかったら、そのネットワーク (プロファイル)はすみやかに削除してください。

せっかくの機会なので、ぜひPasspoint対応の端末を会場に持ち込んでみてください。もしうまく接続できなかったら、会場で無線LAN接続で悩むより、様々な出し物を存分に楽しんできましょう。OpenRoamingのお試しは、万博会場でなくてもできます。

 

おしまい

GL.iNet GL-MT2500AでOpenVPNクライアントを動かす

ちっちゃくないよ!

ちっちゃくないよ!

というわけで、まずは GL.iNet GL-MT2500A (Brume 2) の紹介を軽く。

製品ページの写真を見ると Mango ぐらいに錯覚しますが、結構大きいです。また、金属筐体なので、ずっしりと重い印象です。これ、ポケットVPNルータではなくて、VPNセキュリティゲートウェイの位置付けなんですね。初代 Brume GL-MV1000 よりはずっと小さいし、パワフルになっています。

 

 

型番に A の付かない、プラスチック筐体のモデルもあったのですが、Amazon では入手できなくなっています。もしかすると放熱対策かもしれません。

中身は MediaTek MT7981B @1.3GHz で、Beryl AX (GL-MT3000)と一緒です。ただし、 Wi-Fi 機能が省かれています。外付けアンテナでも良いので Wi-Fi に対応していたら、携帯用になったかもしれません。それだと Beryl AX と差が無くなるのでしょう。

なお、Beryl AX はファン内蔵で、負荷をかけない限りあまり回ることはないのですが、ファンがあるということはホコリが溜まるということです。据え置きでメンテナンスフリーにするなら Brume 2 の方が安心かもしれません。

 

ファームウェアは枯れてない

執筆時点の純正ファームウェアは 4.7.0 で、BETA版が 4.7.4 です。試しにBETA版を入れてみたら、LuCI のソフトウェア管理がうまく動かないという、致命的な問題がありました。要するに、opkg update がエラーになります。そのうち直るでしょうが、もう少しちゃんと作ってほしいところ。あと、OpenWrt 21.02 ベースなのは大いに不満です。

色々と新しいことを試したかったので、OpenWrt 版のファームウェアも試してみました。24.10.0-rc7 に GL-MT2500 用があったので、早速入れてみました。ところが様子が何かおかしい……。WAN 側のインタフェースが動いていません!手動でアドレスを付けようとしてもエラーになってしまいます。まだ青々としていますねぇ。

U-Boot を使って純正ファームウェアに戻そうとしたら、これまた問題を発見。アップロードまではできるのですが、いざファームウェアを焼こうとすると、衝撃の UPDATE FAILED のメッセージが!ここで死を覚悟しました。

検索してみると、同症状の方がいて、対策もありました。どういうわけか、Firefox ではうまくいかないとのことです。Chrome を使ったところ、うまく焼けました。

 

OpenVPNクライアントの設定

気を取り直して、OpenVPN のクライアントとして設定してみます。準備として、LuCI の OpenVPN 機能をインストールします。openvpn-openssl は既に入っています。再起動すれば OpenVPN 関係のメニューが生えているはずです。

LuCI の起動は、他のモデルと違っていて、https://192.168.8.1:8080 のようにポート番号を 8080 にする必要がありました。

Software メニューからパッケージを入れてもよいし、ssh でログインして CLI から

# opkg update
# opkg install luci-app-openvpn

としてもよいです。

OpenVPN サーバの設定は、ネットに色々とあるので、割愛します。必要なのは、サーバ証明書 (ca.crt) とクライアント証明書・鍵ペア (client.crt, client.key) です。.ovpn ファイルを作って設定する方法もありますが、私は PKCS #12 (.p12) 形式のファイルを作って、手動で設定しました。

参考までに、Linux 上で PKCS #12 形式のパスフレーズ無しのファイルを作るには、こうです。OpenSSL 3.xの場合は -legacy オプションを付けます。

# openssl pkcs12 -legacy -export -in client.crt -inkey client.key -out client.p12 -passin pass:"<パスフレーズ>" -passout pass:""

あとは、LuCI からちまちまと設定を入れていけばよいのですが、CLI からやりたい人のためにザックリと()

設定ファイル /etc/config/openvpn の中に、このように書きます。

config openvpn 'my-ovpn'
        option float '1'
        option client '1'
        option reneg_sec '0'
        option dev 'tun'
        option verb '3'
        option nobind '1'
        option persist_tun '1'
        option persist_key '1'
        option remote_cert_tls 'server'
        list remote '<VPNサーバのIPアドレス>'
        option pkcs12 '/etc/openvpn/cert/client.p12'
        option ca '/etc/openvpn/cert/ca.crt'
        option port '1194'
        option proto 'tcp-client'
        option enabled '1'

この例では OpenVPN でトンネルを掘っており、1194/tcp で通信するように設定しています。

末尾にある option enabled '1' が重要で、これがないと、LuCI のメニューで Start をクリックしても、OpenVPN が立ち上がりません。option comp_lzo の行がある場合、値が '0' でも動かなかったりするので、削除します (OpenVPNで comp-lzo はdeprecated)。

OpenWrt で忘れがちな設定の一つに、どの zone に所属させるかというものがあります。OpenVPNを起動してから、ifconfig などで見ると、おそらく tun0 が生えていると思います。/etc/config/network に追加する内容はこのとおり。

config interface 'tun0'
        option proto 'none'

/etc/config/firewall の wan zone セクションに、tun0 デバイスを追加します。こんな感じです。

config zone
        option name 'wan'
        option output 'ACCEPT'
        option mtu_fix '1'
        option input 'DROP'
        list device 'tun0'
        list network 'wan'
        list network 'wan6'
        option masq '1'
        option forward 'REJECT'

 

OpenVPNクライアントの性能

下り400Mbps、上り90Mbpsぐらい出る、自宅の光回線から、RTT 18.5 ms ほど離れたところにある Linux 箱の OpenVPN サーバに接続してみました。

OpenVPNのSpeedtest結果

OpenVPNのSpeedtest結果

カタログ値では 150 Mbps ですが、それよりもやや良好な結果です。Wi-Fi 6 基地局バックホールとして使うには物足りないですが、フリーWi-Fi用途ならなんとかなりそうです。

 

Wi-Fiを生やしてみる

Wi-Fi 機能がないのが少し残念なので、ちょっと試してみました。

手元にあった Wi-Fi 6E 対応の NETGEAR A8000 を挿してみましたが、残念ながら該当するドライバがありません。残念!

NETGEAR A8000を挿してみただけの図 (動かない)

NETGEAR A8000を挿してみただけの図 (動かない)

代わりに NETGEAR A6210 を挿してみたところ、kmod-mt76x2u パッケージの追加で認識させることができました。しかし、Beryl AX などの内蔵 phy と違って、あまり設定の自由度がありません。安定性もイマイチです。

というわけで、Wi-Fi 機能は諦めましょう。

 

おしまい