hgot07 Hotspot Blog

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

Windowsで使うXMLデータに署名する (xmlsec1編)

Windows用のプログラムを開発していると、XMLデータに署名が必要ということがまれによくあります。ウェブサイトからWi-Fi設定を流し込むプロビジョニングでも、設定用のXMLデータに署名が必要で、まぁなんとも面倒くさいことです。たかがWi-Fi設定にコード署名用のEV証明書(*1)が必要だとか、いったい何を考えて設計したのやら……。

さて、Wi-Fiプロビジョニング用のウェブサイトを作るにあたって、この署名が一つの難関になり、ネット上の情報が乏しいので結構苦戦しました。備忘録を残しておきます。

*1 今のところ……(意味深)
    [2023/5/19追記] Windows 11 22H2から、DV証明書でも署名できるように"改善"されました。

 

XML署名ツール

署名付きXML (SignedXML) を生成するためのツールは色々とあるようです。

  1. WindowsのConvertTo-SignedXml (Windows 8.1 SDKなどに入っている)
  2. xmlsec1などのフリーのコマンド
  3. 有償ツール
  4. 自作品 (!)

今回、オープンソース化が前提で無償でLinuxサーバ上に実装したかったので、ConvertTo-SignedXmlや有償ツールは選外となります。頑張れば自作もできますが、正しく署名できるか検証する必要があるので、まずは既存のツールを探します。

xmlsec1 という署名ツールがありました! (・∀・)

ただし、Linuxのインストール時に自動的に入るようなメジャーなツールではないので、追加のパッケージを探すことになります。開発元はこちら ↓

www.aleksey.com

このxmlsec1、ちょっとクセがあって、使うのがちょっと面倒です。今回、代わりになるものが見つけられなかったのですが、他に良いものがあれば知りたいです。

 

XMLデータに署名してみる

Windowsms-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形式には非対応で、署名しようとコマンドを打つと、ずらずらとエラーが表示されます (しかも原因が判りにくい)。

 

おしまい