Windows用のプログラムを開発していると、XMLデータに署名が必要ということがまれによくあります。ウェブサイトからWi-Fi設定を流し込むプロビジョニングでも、設定用のXMLデータに署名が必要で、まぁなんとも面倒くさいことです。たかがWi-Fi設定にコード署名用のEV証明書(*1)が必要だとか、いったい何を考えて設計したのやら……。
さて、Wi-Fiプロビジョニング用のウェブサイトを作るにあたって、この署名が一つの難関になり、ネット上の情報が乏しいので結構苦戦しました。備忘録を残しておきます。
*1 今のところ……(意味深)
[2023/5/19追記] Windows 11 22H2から、DV証明書でも署名できるように"改善"されました。
XML署名ツール
署名付きXML (SignedXML) を生成するためのツールは色々とあるようです。
- WindowsのConvertTo-SignedXml (Windows 8.1 SDKなどに入っている)
- xmlsec1などのフリーのコマンド
- 有償ツール
- 自作品 (!)
今回、オープンソース化が前提で無償でLinuxサーバ上に実装したかったので、ConvertTo-SignedXmlや有償ツールは選外となります。頑張れば自作もできますが、正しく署名できるか検証する必要があるので、まずは既存のツールを探します。
xmlsec1 という署名ツールがありました! (・∀・)
ただし、Linuxのインストール時に自動的に入るようなメジャーなツールではないので、追加のパッケージを探すことになります。開発元はこちら ↓
このxmlsec1、ちょっとクセがあって、使うのがちょっと面倒です。今回、代わりになるものが見つけられなかったのですが、他に良いものがあれば知りたいです。
XMLデータに署名してみる
Windows の ms-settings: URI スキームで、ウェブサイトから Wi-Fi 設定を行うためには、XMLデータに署名が必要です。この署名の手続きを示します。
元のXMLデータの中身は、こんな感じです。
<?xml version="1.0"?>
<CarrierProvisioning xmlns="http://www.microsoft.com/networking/CarrierControl/v1" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Global>
<CarrierId>{df909a2c-511f-c769-b424-11333792de13}</CarrierId>
<SubscriberId>1234567890</SubscriberId>
</Global>
<WLANProfiles>
<WLANProfile xmlns="http://www.microsoft.com/networking/CarrierControl/WLAN/v1">
<name>Example IdP</name>
<SSIDConfig>
<SSID>
<name>GreatAP</name>
</SSID>
</SSIDConfig>
<MSM>
<security>
<authEncryption>
<authentication>WPA2</authentication>
<encryption>AES</encryption>
<useOneX>true</useOneX>
</authEncryption>
<OneX xmlns="http://www.microsoft.com/networking/OneX/v1">
<authMode>user</authMode>
<EAPConfig>
<EapHostConfig xmlns="http://www.microsoft.com/provisioning/EapHostConfig">
<EapMethod>
<Type xmlns="http://www.microsoft.com/provisioning/EapCommon">21</Type>
<VendorId xmlns="http://www.microsoft.com/provisioning/EapCommon">0</VendorId>
<VendorType xmlns="http://www.microsoft.com/provisioning/EapCommon">0</VendorType>
<AuthorId xmlns="http://www.microsoft.com/provisioning/EapCommon">311</AuthorId>
</EapMethod>
<Config>
<EapTtls xmlns="http://www.microsoft.com/provisioning/EapTtlsConnectionPropertiesV1">
<ServerValidation>
<ServerNames>idp.example.jp</ServerNames>
<TrustedRootCAHash>ca bd 2a 79 a1 07 6a 31 f2 1d 25 36 35 cb 03 9d 43 29 a5 e8</TrustedRootCAHash>
<DisablePrompt>false</DisablePrompt>
</ServerValidation>
<Phase2Authentication>
<MSCHAPAuthentication/>
</Phase2Authentication>
<Phase1Identity>
<IdentityPrivacy>true</IdentityPrivacy>
<AnonymousIdentity>anonymous@example.jp</AnonymousIdentity>
</Phase1Identity>
</EapTtls>
</Config>
</EapHostConfig>
</EAPConfig>
</OneX>
<EapHostUserCredentials xmlns="http://www.microsoft.com/provisioning/EapHostUserCredentials" xmlns:baseEap="http://www.microsoft.com/provisioning/BaseEapMethodUserCredentials" xmlns:eapCommon="http://www.microsoft.com/provisioning/EapCommon">
<EapMethod>
<eapCommon:Type>21</eapCommon:Type>
<eapCommon:AuthorId>311</eapCommon:AuthorId>
</EapMethod>
<Credentials>
<EapTtls xmlns="http://www.microsoft.com/provisioning/EapTtlsUserPropertiesV1">
<Username>user001@example.jp</Username>
<Password>piyopiyo123</Password>
</EapTtls>
</Credentials>
</EapHostUserCredentials>
</security>
</MSM>
</WLANProfile>
</WLANProfiles>
</CarrierProvisioning>
この内容が、wifi.xml という名前のファイルに格納されていると想定します。何やらだらだらと長いですが、肝心なのは最後の二行だけなので安心しましょう。1行目のXML宣言を除いた部分が署名の対象になります。
「これを xmlsec1 に読み込ませて署名すればよいだけでは?」
ざんねん!下準備が必要です。
末尾にある </CarrierProvisioning> というタグの前に、以下のようにテンプレートを挿入します。
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" /><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" /><Reference URI=""><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" /><DigestValue></DigestValue></Reference></SignedInfo><SignatureValue></SignatureValue><KeyInfo><X509Data><X509Certificate></X509Certificate></X509Data></KeyInfo></Signature></CarrierProvisioning>
<Signature>...</Signature> の部分が署名です。Signatureブロックの内側は、無理に一行にまとめないで、改行を入れても大丈夫です。元データの最後のタグが、改行無しに書かれていることに注意します。もしここに改行があると、元データに余計な一行が入ったデータに対して署名が生成されることになります。(今回はどちらでもよいのですが、署名の範囲を注意するという意味で)
署名用の鍵と証明書、必要に応じてCA証明書を束ねた、PKCS #12形式のファイルを用意します。以下の例では、パスワードを空にしています。
$ cat cert.pem ca.pem | openssl pkcs12 -export \
-inkey privkey.pem -password "pass:" -out signercert.pfx
いざ、署名!
$ xmlsec1 --sign --pkcs12 signercert.pfx --pwd "" wifi.xml > signed-wifi.xml
出来上がった署名付きXMLデータを覗いてみると、Signatureブロックの中で、DigestValueやSignatureValueに値が入っていることが分かります。また、末尾にはPKCS #12形式ファイルに埋め込んだ証明書のデータがBase64エンコードされて埋め込まれています。パブリックCAを使うなら、Root CA証明書は不要のはずです。
[2023/5/19追記] xmlsec1で署名するには、RSA形式の証明書が必要です。ECDSA形式には非対応で、署名しようとコマンドを打つと、ずらずらとエラーが表示されます (しかも原因が判りにくい)。
おしまい