こんにちは。エンジニアの山下です。
今回は Salesforce の Einstein Discovery と Einstein Next Best Action を連携させ、Lightning Page 上で統計手法ベースのレコメンデーションの出し分けを行う方法について書こうと思います。
続きを読むこんにちは。エンジニアの山下です。
今回は Salesforce の Einstein Discovery と Einstein Next Best Action を連携させ、Lightning Page 上で統計手法ベースのレコメンデーションの出し分けを行う方法について書こうと思います。
続きを読むみなさんこんにちは。エンジニアの佐藤です。今回はAWS製プロセッサGraviton3の強さを再確認した、というお話です。
AWS EC2にはさまざまな種類のインスタンスがあります。本稿執筆時点(2024年4月初旬)では、最新世代のCompute Optimizedタイプのインスタンスとして以下の3種類が利用可能です。
種類 | プロセッサ | 一般利用開始 | アーキテクチャ |
---|---|---|---|
C7g | AWS Graviton3 | 2022-03 | arm64 |
C7i | Intel 4th gen Xeon | 2023-09 | x86_64 |
C7a | AMD 4th gen EPYC | 2023-10 | x86_64 |
AWSは自社開発のGravitonプロセッサの利用を勧めています。他の2つに対して価格が低めに設定されており、電力効率も高いと宣伝されています。Amazonプライムデーなどの自社企画では、これらを活用して優れた電力効率を実現したことが発表されています。一方、Intelは業界一番の老舗であり、一昔前まではクラウド向けプロセッサをほぼ独占供給していました。しかし近年はAMD社製プロセッサの性能向上が著しく、シェアを伸ばしています。
続きを読むこんにちは。エンジニアの山下です。
昨今の LLM の隆盛は目を見張るものがあり、フレクトでも LLM を使った開発に必要な技術や知識の準備を進めています。その一環として Agents for Amazon Bedrock を使って模擬的に LLM アプリケーションを実装・調整してみたのですが、今回はそこで得た知見について書こうと思います。
本投稿では主に Agents for Amazon Bedrock を使ってみて得た所見について書きますが、前提を共有するため、最初に Agents for Amazon Bedrock の概要とアーキテクチャについても簡単に述べます。既に知っているという方は読み飛ばしていただいて OK です。
続きを読むこんにちは。エンジニアの山下です。
パスワードレス技術として Passkey が注目を集めていますが、この Passkey はフィッシング耐性を持つと言われています。今回は Passkey のフィッシング耐性を支える仕組みについて調査した結果を書きたいと思います。
そもそも Passkey は FIDO Alliance という団体によって定められた概念です。FIDO Alliance の 公式ページ では Passkey を以下のように定義しています:
Any passwordless FIDO credential is a passkey.
FIDO Alliance は FIDO (FIDO2) というパスワードレス認証の技術標準を定めており、この FIDO で使用されるパスワード以外のクレデンシャルを全て Passkey と呼ぶ、と定義は述べています。
実のところ、フィッシング耐性を持つように設計されているのは FIDO であり、単なるクレデンシャルに過ぎない Passkey にはいかなる力もありません。従って、以降は FIDO ではフィッシング耐性をどのように実現しているのかが話の主題となります。
続きを読む皆さんこんにちは。エンジニアの佐藤です。今回はメモリキャッシュのお話です。
Webアプリケーション開発者の方々なら、Redisを知らない人はいないでしょう。メモリストレージの定番として、筆者が認識し始めたのは10年ぐらい前でした。無料で利用可能で、Dockerコンテナで手軽にデプロイできます。今ではクラウドベンダーはほとんどRedisをSaas提供しており、ローカル開発環境からクラウドの本番環境にそのままま乗せ替えできる点も便利です。
しかし、問題はお値段です。Redisは当然、CPUを占有します。CPUは高価なリソースです。さらに頭が痛いのが、高可用性(HA)構成です。RedisをHA構成しようと思ったら、待機系を用意しなければならず、費用は2倍です。つまり、WebサーバーのHA構成と合わせてCPUを4つ運用する必要があります。昨今は様々な調達方法がありますので実際は単純ではありませんが(本稿執筆中にAWSからElastiCache Serverlessなるアナウンスもありました)、Redisが本質的に高価なストレージであることに変わりはありません。
そんな折、Infinispanなるものの存在を知りました。RedhatがJBossの一部としてオープンソースで開発しているもので、商用ライセンスはDataGridとして販売されています。業務要件でこのInfinispanを展開する機会があり、調べて試しているうちに、これはRedisの代わりに使えるんじゃないかと思い始めました。(ただし、仕様は異なります。InfinispanはRedis RESP3エンドポイントもありますが、サポートされるコマンドは限定的です。)
Infinispanの特徴は、HA構成をサポートしていながら、クラスタノード同士は対等な、マルチマスター構成である点です。(もちろん、主系待機系構成のRedisとどちらが良いという問題ではありません。)普段は調達リソースの100%のパフォーマンスを期待し、障害時は半分で我慢する、という構成が取れます。
そこで今回は、このInfinispanをWebサーバーのメモリストレージとしてHA構成する方法をご紹介したいと思います。何かの参考にしていただければ幸いです。
Infinispanは高機能なので、様々な構成が可能です(興味のある方は文末の余談「高機能だが知名度が低く、難解なInfinispan」をお読みください。)その分迷いやすく、結局どうすれば良いのかわからなくなりがちです。そこで今回は最初に作戦を立てて、参考知識を収集していきます。
今回立てた作戦は以下のようなものです。
Infinispanはポート11222をREST APIエンドポイントとして使います。このエンドポイントはノード内部専用で、認証は設定しません。ポイントは赤点線の中で、Infinispanノード同士は「ノード間通信」と「障害検知」の2つのコネクションでつながっています。前者はポート7800、後者はポート57800です。(文末の余談「Infinispanのポートはどうやって決まっているのか」参照)
この作戦自体は、特に不自然なものではなく、平易に理解していただけると思います。
この作戦を展開するもっとも簡便な方法は、KubernetesのSidecarの利用です。こうするとノード間通信はKubernetesクラスタの内部ネットワークに隔離され、WebサーバーコンテナとInfinispanノードがひとつのPodに配置されます。今回はKubernetes環境にはCanonical MicroK8sをUbuntu 22.04LTSにインストールして使いました。
次に、この作戦に従ってInfinispanを設定します。設定はXMLファイルを記述し、これをKubernetes ConfigMapでInfinispanコンテナに指定します。
まず設定のXMLファイルです。
<infinispan xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:infinispan:config:14.0 https://infinispan.org/schemas/infinispan-config-14.0.xsd urn:infinispan:server:14.0 https://infinispan.org/schemas/infinispan-server-14.0.xsd urn:org:jgroups http://www.jgroups.org/schema/jgroups-5.2.xsd" xmlns="urn:infinispan:config:14.0" xmlns:ispn="urn:infinispan:config:14.0" xmlns:server="urn:infinispan:server:14.0"> <server xmlns="urn:infinispan:server:14.0"> <interfaces> <interface name="public"> <inet-address value="${infinispan.bind.address:127.0.0.1}"/> </interface> </interfaces> <socket-bindings default-interface="public" port-offset="${infinispan.socket.binding.port-offset:0}"> <socket-binding name="default" port="${infinispan.bind.port:11222}"/> </socket-bindings> <endpoints> <endpoint socket-binding="default"> <rest-connector /> </endpoint> </endpoints> </server> <cache-container name="default"> <transport stack="kubernetes"/> <distributed-cache name="cache01" owners="2" /> <counters xmlns="urn:infinispan:config:counters:14.0" num-owners="2" reliability="CONSISTENT"> <strong-counter name="counter01" initial-value="0" storage="VOLATILE"/> </counters> </cache-container> </infinispan>
この内容は、infinispanの公開コンテナの内容から筆者が抜粋して作ったもので、以下のような内容です。
そしてcache-container要素の中でキャッシュストレージを定義していくわけですが、ここが難解です。
<transport stack="kubernetes"/>
これは、Infinispanノード間通信と障害検知に"kubernetes"という名称で既定で定義されている設定を使うという意味です。その定義はここにありますが、本ブログの範囲を超える内容になりますので、割愛します。
次のdistributed-cache要素は、クラスタ化された(つまり片方のInfinispanノードが障害となっても消えない)キーバリューストアです。本ブログでは、定義のみ紹介します。
次のcounters/strong-counterは、クラスタ化されたカウンターで、本ブログでこの後紹介していくものです。strong-counter(解説ブログはこちら)とは、クラスタ化されていて2ノード以上で維持されているストレージでありながら、その更新をトランザクションによって原子的に実行できるカウンターのことです。このカウンターの利用が、先の作戦図のような冗長構成でできることは、Infinispanクラスタが正常動作していることを確認する最も簡便な手段です。
このXML設定ファイルを利用して、作戦図にあるシステム構成を実現する定義ファイルは以下のようになりました。
apiVersion: v1 kind: ConfigMap metadata: name: ispn-config data: infinispan.conf: | (ここにXML設定ファイルを貼り付け) --- apiVersion: v1 kind: Service metadata: name: test01-service spec: type: NodePort selector: app: test01 ports: - name: http port: 5000 nodePort: 30001 targetPort: http --- apiVersion: v1 kind: Service metadata: name: infinispan-service spec: ports: - name: jgroups port: 7800 targetPort: jgroups - name: jgroups-fd port: 57800 targetPort: jrogups-fd selector: app: test01 type: ClusterIP clusterIP: None --- apiVersion: apps/v1 kind: Deployment metadata: name: test01 labels: app: test01 spec: replicas: 2 selector: matchLabels: app: test01 template: metadata: labels: app: test01 spec: containers: - name: test01 image: localhost:32000/test01:20231128_03 env: - name: INFINISPAN_REST_ENDPOINT value: http://localhost:11222 ports: - name: http containerPort: 5000 - name: infinispan image: infinispan/server:latest args: [ "-c", "/usr-config/infinispan.conf", "-Djgroups.dns.query=infinispan-service.default.svc.cluster.local", ] ports: - name: infinispan containerPort: 11222 - name: jgroups containerPort: 7800 - name: jgroups-fd containerPort: 57800 volumeMounts: - name: user-config mountPath: /usr-config volumes: - name: user-config configMap: name: ispn-config dnsConfig: options: - name: ndots value: "1"
一番重要なポイントは、Service「infninspan-service」を定めていることです。この定義により、Kubernetesの内部DNS名「infinispan-cluster-service.default.svc.cluster.local」により、Infinispanを含むPodのポート7800と57800の列挙ができるようになります。(KubernetesクラスタのCoreDNSプラグインを有効にしておきましょう。)
Webサーバー(test01コンテナ)はポート5000で着信待機し、localhost:11222で 手元の Infinispanノードと通信します。
InfinispanはConfigMapで作成されたvolumeをマウントし、これをinfinispan/serverコンテナの -c 起動オプションで指定することにより設定します。
Webサーバーの内容は、以下のような、Python Flaskを利用したごく簡単なものです。その機能は、ルートパスでアクセスされると、strong counter counter01を「原子的に参照し、加算する」処理を手元のInfinispanノードに対して行います。どのPodで実行されたかを示すために、Podのアドレスも付記することにします。このWebサーバーのエンドポイントはNodePort 30001で外部に公開します。
from flask import Flask import requests import os import socket INFINISPAN = os.getenv('INFINISPAN_REST_ENDPOINT', 'http://localhost:11222') app = Flask(__name__) @app.route("/") def index(): counter = requests.post(f'{INFINISPAN}/rest/v2/counters/counter01?action=increment').text addr = socket.gethostbyname(socket.gethostname()) return f'{addr},{counter}' if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)
MicroK8sにインポートして、以下のようにWebサーバーの( HA対応の! )Webサーバーのエンドポイントを連続アクセスしてみましょう。
cmd="curl http://localhost:30001"; for i in $(seq 50); do $cmd; echo ""; sleep 0; done
結果は以下の通りで、ごく短時間で別のPodにアクセスした場合でも、正しくカウントアップされています。期待通りに動作していると考えて良さそうです!
10.1.89.54,162 10.1.89.55,163 10.1.89.55,164 10.1.89.55,165 10.1.89.55,166 10.1.89.54,167 10.1.89.54,168 10.1.89.54,169 10.1.89.55,170 10.1.89.54,171 10.1.89.55,172 10.1.89.54,173 10.1.89.54,174 10.1.89.55,175 10.1.89.54,176 10.1.89.55,177 10.1.89.55,178 10.1.89.54,179 10.1.89.55,180 10.1.89.55,181 10.1.89.54,182 10.1.89.55,183 10.1.89.54,184 10.1.89.55,185 10.1.89.54,186 10.1.89.54,187 10.1.89.55,188 10.1.89.55,189 10.1.89.55,190 10.1.89.54,191 10.1.89.55,192 10.1.89.54,193 10.1.89.55,194 10.1.89.55,195 10.1.89.54,196 10.1.89.55,197 10.1.89.55,198 10.1.89.55,199 10.1.89.54,200 10.1.89.54,201 10.1.89.55,202 10.1.89.54,203 10.1.89.55,204 10.1.89.54,205 10.1.89.55,206 10.1.89.54,207 10.1.89.54,208 10.1.89.54,209 10.1.89.54,210 10.1.89.54,211
今度はPodを落として実験してみましょう。
curl http://localhost:30001/ 10.1.89.64,5
Podを確認します。
kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES test01-f9c589f59-6vkbt 2/2 Running 0 12m 10.1.89.64 ip-172-31-0-83 <none> <none> test01-f9c589f59-krh6p 2/2 Running 0 12m 10.1.89.63 ip-172-31-0-83 <none> <none>
先ほど返信した、IP 10.1.89.64のPodを落とします。
kubectl delete pod test01-f9c589f59-6vkbt pod "test01-f9c589f59-6vkbt" deleted
再びアクセスすると、カウンタは正しくカウントアップされています!
curl http://localhost:30001/ 10.1.89.65,6
ここまでお読みくださりありがとうございました。
うまく当初の目標を達成しました。Webサーバーにメモリキャッシュは追加したいが、HAでRedisを構成するのは予算的に難しそうなケースにうまくフィットするかもしれません。
ただしInfinispanの学習・設定の負担はそれなりにあります。本家マニュアルポータルはこちらで、Dockerイメージの説明はこちらで、本ブログもこれらを参照していますが、不足している情報は周辺調査やソースコードを見て補う必要がありました。
「で、結局ミニマムHA構成はどうすれば良いのか?」と思われた時に、本ブログを参考にしていただければ幸いです。
Infinispanがどうやってポートを決めているのかは、簡単に見つかりません。筆者の邪推ですが、その昔、クラスタ内の一群の機材がファイアウォールを定めず自由に通信していた時代の文化のようにも感じられます。
その定義は、以下の部分に書かれています。
障害検知通信ポートは、ノード間通信ポートからのオフセット(ポート番号の足し算)で定義されているので、既定では7800 + 50000 = 57800になります。
JGroupsのマニュアルには、この障害検知ポートは設定されたポート範囲を定めることもできるとあります。これもまた、通信ポートがファイアウォールで制限される前の時代の文化のように、筆者には感じられます。
Infinispanは非常に高機能で、継続開発されており、ドキュメントも詳しく書かれています。今回はWebサーバーに併設する方法を採りましたが、単体のストレージクラスタとして構成することも可能で、遠隔バックアップ構成も可能なようです。
今回はKubernetesの例を紹介しましたが、クラスタノードディスカバリーにはAmazon S3などのオブジェクトストレージやJDBCを経由する方法もあり、AWS ECSではこちらの方法でクラスタを構成できます。
今回紹介していない有用な機能に「Java Transaction APIサポート」があり、XA(2フェーズ)トランザクションにも対応します。これによりメモリストレージとJDBCの更新の両方にまたがるトランザクション処理が構成できます。
みなさんこんにちは。エンジニアの佐藤です。今回は年末調整のお話です。
年末調整の時節、会社からは「保険料控除ではXMLを奨励」というアナウンスがありました。「エックスエムエル!」という言葉と共に新鮮な驚きが頭の中に広がりました。ええっ、XMLだって?あいつらまだ生きていたのか!そう、コンピュータ同士の相互通信データフォーマットとして、XMLは時代遅れなのです。現在の主役はとっくにJSON。何を今更XML。
しかし、筆者はXMLは嫌いではありませんし、それなりに知識もあります。(昔話に興味のある方は、末尾の「昔話」をご覧ください。) 保険業社が発行するXMLがどんなものか、大きな興味をもってファイルを開いてみました。
ざっくり言うと、以下のような構造になっています。
<文書> <保険情報詳細/> <署名情報/> </文書>
その仕様は「源泉徴収票等オンライン化に関する仕様書」として国税庁が配布しています。これ自体は特に難解なわけではなく、保険業社や支払い金額など、年末調整の保険料控除の書類に記入していた項目が書かれているだけです。
難しいのは(そしておもしろいのは)、署名情報の部分です。概要は以下のようになっています。
<dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" Id="..."> <dsig:SignedInfo> <dsig:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/> <dsig:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/> <dsig:Reference URI="#(保険情報詳細データへのポインタ)"> <dsig:Transforms> <dsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/> <dsig:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/> </dsig:Transforms> <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> <dsig:DigestValue>(保険情報詳細データのダイジェスト)</dsig:DigestValue> </dsig:Reference> </dsig:SignedInfo> <dsig:SignatureValue>(署名データ)</dsig:SignatureValue> <dsig:KeyInfo> <dsig:X509Data> <dsig:X509Certificate>(署名者の証明書)</dsig:X509Certificate> </dsig:X509Data> </dsig:KeyInfo> </dsig:Signature>
内容はこうです。まず保険詳細情報の部分がXML文書としてバイナリデータになり、ダイジェストが計算されます。そしてこのダイジェストが署名者の秘密キーで署名され、署名データとなります。この署名データを署名者の証明書に含まれる公開キーで検証することで、証明書の発行者(=署名者)が署名した内容に改ざんがないことが確認できるのです。
証明書はbase64形式ですが、復号すると署名者は「Registrar of Tokyo Legal Affairs Bureau, Japanese Government」となっていました。この署名者として署名できるのは、署名者に対応する秘密キーを手にした者だけに、厳密に制限されます。 つまりこれは、東京法務局が署名した公文書で、法律上の扱いはともかく、その性質は住民票や車検証といった身近な公文書と完全に同じです。(たったこれだけのテキスト断片にそんな価値があるなんて、なんだかワクワクしてきませんか。)
この保険業社発行のデータは、もちろんその有効性を確認してから発行されていると思います。疑うわけではありません。しかし、エンジニアとしてその真正性を確認したいと思った筆者は、署名の検証を手元でやってみようと思い立ちました。
ただ、これが結構、険しい道だったのです。結論はこうです。署名検証は可能で、署名は正しかったです。しかしこれを確認する手順は、ライブラリで一発では決してなかったのです。Java言語とPython言語で、2つの保険料控除XMLの署名を確認しましたが、確認のためにはそれなりの知識とプログラム調整を要しました。 ひょっとしたら同じ問題に遭遇している方がいらっしゃるかもしれないと思い、ここにその顛末を紹介させていただきます。
Javaの場合は、JDKにXML署名検証のための機能が実装されており、その使い方はOracleの以下のサイトに書かれています。
ただし、結構な分量があり、その内容もかなり専門的です。幸い以下のGithub公開レポジトリに、この文書で解説されているサンプルコードが取りまとめられており、筆者もこれを動かしてみることから始めました。
https://github.com/jknecht/xml-signature-validation
しかし、、そのまま動かすと以下の例外になってしまいます。
javax.xml.crypto.MarshalException: It is forbidden to use algorithm http://www.w3.org/2000/09/xmldsig#rsa-sha1 when secure validation is enabled
そういえば署名情報を見た時から、悪い予感がしていたのです。SHA1アルゴリズムは、強行突破する計算量が現在のコンピュータの速度と比較して不十分と業界で認識され、非奨励となっていたのです。しかし今回はダイジェストがSHA1で作られているため、このアルゴリズムを動かさざるを得ません。
その手順は巷に幅広く解説されていますが、以下のような手順です。
まず既定のjava.securityファイルを回収します。筆者の場合は以下のディレクトリにありました。
/usr/lib/jvm/java-17-amazon-corretto/conf/security/java.security
次に、この内容を以下のように変更して、SHA1の禁止措置を解除します。
# 以下が禁止措置を解除した元々含まれていた項目 # disallowAlg http://www.w3.org/2000/09/xmldsig#sha1,\ # disallowAlg http://www.w3.org/2000/09/xmldsig#rsa-sha1,\ jdk.xml.dsig.secureValidationPolicy=\ disallowAlg http://www.w3.org/TR/1999/REC-xslt-19991116,\ disallowAlg http://www.w3.org/2001/04/xmldsig-more#rsa-md5,\ disallowAlg http://www.w3.org/2001/04/xmldsig-more#hmac-md5,\ disallowAlg http://www.w3.org/2001/04/xmldsig-more#md5,\ disallowAlg http://www.w3.org/2000/09/xmldsig#dsa-sha1,\ disallowAlg http://www.w3.org/2007/05/xmldsig-more#sha1-rsa-MGF1,\ disallowAlg http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1,\ maxTransforms 5,\ maxReferences 30,\ disallowReferenceUriSchemes file http https,\ minKeySize RSA 1024,\ minKeySize DSA 1024,\ minKeySize EC 224,\ noDuplicateIds,\ noRetrievalMethodLoops
これをプログラム開始時に以下のコマンドライン引数で指定すれば、SHA1が使えるようになります。
-Djava.security.properties=${workspaceFolder}/java.security
さあこれで動くようになったでしょうか?残念ながらそうではありません。続いて以下の例外となりました。
Caused by: com.sun.org.apache.xml.internal.security.utils.resolver.ResourceResolverException: Cannot resolve element with ID ...
もういきなりわかりません。IDって。。??デバッガでJDKのコードフローを辿っていくと、URIリファレンスの解決に失敗した、と言うふうに読めます。そこで元のXMLを眺めていると、該当しそうな場所が見つかりました。冒頭に紹介した署名情報の以下の部分です。
<dsig:Reference URI="#(保険情報詳細データへのポインタ)">
このURIをIDとして設定しているのは、ルート要素の以下の部分です。
<TEG810 xmlns="http://xml.e-tax.nta.go.jp/XSD/kyotsu" xmlns:gen="http://xml.e-tax.nta.go.jp/XSD/general" xmlns:kyo="http://xml.e-tax.nta.go.jp/XSD/kyotsu" VR="1.0" id="TEG810...">
ははぁ、これは保険情報のXMLノードクエリ失敗だなと思って調べてみると、XML DocumentにはID解決する属性を指定する必要があるという巷情報に行きつきました。
https://stackoverflow.com/questions/3423430/java-xml-dom-how-are-id-attributes-special
Document doc = dbf.newDocumentBuilder().parse( new FileInputStream(fileName)); Element x = doc.getDocumentElement(); // 追加部分 doc.getDocumentElement().setIdAttributeNS( null, "id", true);
すると、、、以下のように表示され、署名検証が成功しました!
Signature passed core validation
試しに保険詳細情報(金額など)や署名の内容を1文字変更して検証してみましたが、正しく検証エラーとなります。A社とB社両方のXMLで署名を確認できました。
Pythonの場合は。。と思って巷ライブラリを確認した筆者は、大きなショックを受けました。ほとんど以下の一択なのです。
https://xml-security.github.io/signxml/
しかも、サンプルコードがありません。公式ドキュメントに書かれているサンプルはわずかにこれだけです。
https://xml-security.github.io/signxml/#verifying-saml-assertions
XMLVerifier.verifyの説明を読み込むなどして、以下のコードを書き起こしました。(最終コードは本項目の末尾をご覧ください。)
from lxml import etree from signxml.verifier import XMLVerifier, SignatureConfiguration from signxml.algorithms import DigestAlgorithm, SignatureMethod xmlData = None with open("a.xml", "rb") as fh: xmlData = fh.read() x = etree.XML(xmlData, None) el = x.find(".//ns:X509Certificate", namespaces={"ns": "http://www.w3.org/2000/09/xmldsig#"}) cert = el.text verifier = XMLVerifier() assertion_data = verifier.verify( xmlData, x509_cert=cert ).signed_xml s = etree.tostring(assertion_data) print(s)
証明書データをあえて事前に回収しているのは、verifyメソッドの説明に以下のように書かれているからです。
If left set to None, requires that the signature supply a valid X.509 certificate chain that validates against the known certificate authorities.
東京法務局が今回の証明書に対応するルート証明書を発行しているのか、本稿執筆時点では確認できませんでしたので、システムCAにそれを収録する代わりにCAチェックを回避する方法を選びました。
実行すると、Javaの時にも遭遇したあのエラーです。
signxml.exceptions.InvalidInput: Signature method RSA_SHA1 forbidden by configuration
以下のようにして有効化します。
verifier.verify( xmlData, x509_cert=cert, expect_config=SignatureConfiguration( signature_methods=frozenset({ SignatureMethod.RSA_SHA1 }), digest_algorithms=frozenset({ DigestAlgorithm.SHA1 }) ) )
ここまで修正して、B社のXMLは検証成功しました!例によって内容を変更して確認しましたが、正しく検証できているようです。
しかし、、A社のXMLでは以下の例外になっていまいます。
signxml.exceptions.InvalidSignature: Signature verification failed: bad signature
こんなことって。。 (あるんですよね、こういうことが。これが言わば、ソフト開発技術者業務の難所だと筆者は思います。特に今回のように暗号アルゴリズムを使う場合は、数多ある設定が完全に想定通りでないと、このような冷たいエラーから抜け出すことができないのです。)
しかし、B社のXMLでは署名検証成功しているので、この差分を詰めていけば、絶対に正解に辿り着くはずです。まずXMLの中身の確認から入りました。署名情報部分に以下のような違いがありました。
A社(失敗)
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#" ...>
B社(成功)
<dsig:Signature xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" ...>
XMLに詳しい方なら分かると思いますが、この2つのXMLは、この部分だけを見れば、同値です。要素の名前が違うじゃないかと思われるかもしれませんが、B社XMLの「dsig」の部分は名前空間のプレフィクスであり、本当の名前空間識別子は「xmlns:dsig」属性で指定されている「http://www.w3.org/2000/09/xmldsig#」の方なのです。
念のためA社XMLの名前空間をB社形式に合わせてみましたが、エラーは解消しませんでした。
頭を総動員して考えられる可能性を探っていきます。
そもそもXMLの署名検証とは、以下のような過程で行われるものです。(他の方式もありますが、今回の場合です。また、今回はA社XMLとB社XMLで方式は共通です。)
SignedInfoの中にはダイジェストが含まれていますので、こうすることでXML全体の内容保証ができるのです。問題は、SignatureInfoや、Reference ID指定された要素はXMLテキストであり、先ほど紹介したように、その内容に表現の多様性があることです。一方でデジタル署名は内容が完全に一致するバイナリデータに対する署名ですので、XMLテキストはそのままでは署名データとしては使えないのです。そこでcanonicalizeというプロセスが登場します。CanonicalizationMethod要素を見ると、その手順の識別子が定義されています。
しかし、canonicalize手順の識別子はA社とB社で全く同じです。何が問題なのでしょうか。
次に筆者が思いついたのは、署名検証データとして持ち込まれるSignatureInfoの 完全な文字列情報の 確認です。これはPythonのデバッグ情報を出力させることで行えます。
出力した文字情報は、以下のようになっていました。(括弧内は固有情報なのでマスクしています。)
<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:gen="http://xml.e-tax.nta.go.jp/XSD/general" xmlns:kyo="http://xml.e-tax.nta.go.jp/XSD/kyotsu"><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></SignatureMethod><Reference URI="#(ID情報)"><Transforms xmlns=""><Transform xmlns="" Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform><Transform xmlns="" Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></Transform></Transforms><DigestMethod xmlns="" Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod><DigestValue xmlns="">(保険情報詳細データのダイジェスト)</DigestValue></Reference></SignedInfo>
A社のXMLはJavaでの署名情報検証には成功しています。その時のSignedInfo文字列はどうなっていたのでしょうか。
その出力手順は複雑で、まず以下のようなlogging.propertiesファイルを用意します。
handlers= java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.level = FINER java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter org.jcp.xml.dsig.internal.level = FINER com.sun.org.apache.xml.internal.security.level = FINER
次にこれをjava起動パラメターで指定します。
-Djava.util.logging.config.file=${workspaceFolder}/logging.properties
出力されたのは、以下のような文字列でした。
<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#" xmlns:gen="http://xml.e-tax.nta.go.jp/XSD/general" xmlns:kyo="http://xml.e-tax.nta.go.jp/XSD/kyotsu"><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></SignatureMethod><Reference URI="#(ID情報)"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></Transform></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod><DigestValue>(保険情報詳細データのダイジェスト)</DigestValue></Reference></SignedInfo>
Pythonの方にだけ、「xmlns=""」という、空のXML名前空間指定が付いています。これは、ライブラリの処理としては誤りで、バグと考えても良いかもしれません。
幸い、この空のXML名前空間指定を文字列編集で除去するオプションがライブラリにありました。これはどこにも解説されていない内容で、筆者がライブラリのソースコードを読んで掘り出したものです。
verifier = XMLVerifier() verifier.excise_empty_xmlns_declarations = True
最終的なPythonプログラムは以下のようになりました。
from lxml import etree from base64 import b64decode from signxml.verifier import XMLVerifier, SignatureConfiguration from signxml.algorithms import DigestAlgorithm, SignatureMethod import logging logging.basicConfig(encoding='utf-8', level=logging.DEBUG) xmlData = None with open("a.xml", "rb") as fh: xmlData = fh.read() x = etree.XML(xmlData, None) el = x.find(".//ns:X509Certificate", namespaces={"ns": "http://www.w3.org/2000/09/xmldsig#"}) cert = el.text verifier = XMLVerifier() verifier.excise_empty_xmlns_declarations = True assertion_data = verifier.verify( xmlData, x509_cert=cert, expect_config=SignatureConfiguration( signature_methods=frozenset({ SignatureMethod.RSA_SHA1 }), digest_algorithms=frozenset({ DigestAlgorithm.SHA1 }) ) ).signed_xml s = etree.tostring(assertion_data) print(s)
このプログラムで検証すると、A社のXMLも検証OKとなり、内容改ざんの検出も期待通りでした!
様々な問題がありましたが、なんとか目的を達成しました。最後までお読みいただきありがとうございます。
いろいろ調べながら感じたのは、XML署名が業界的にマイナー技術であり、とにかく文献が少なくて古いことです。時代の流れは着実にJSON/JWTであり、XMLは旧技術なのです。しかしXMLで決まっていることは仕方がないし、一度決まった業務仕様は長い間生きながらえます。知識の力で何とかつなぐのがエンジニアの仕事です。
今から20年ほど前、XMLが大きな注目を集めたことがありました。Microsoftが.NET Framework 1.0を発表し、その目玉機能としてアピールしたからです。商売の話はさておき、筆者も、あのとき、紙書類の階層構造と、バイナリメッセージフォーマットの中間にある、コンピュータでクエリできる構造化された共通テキストフォーマットであるXMLに大きな可能性を感じていました。
そんなXMLはどうして主流になれなかったのか?筆者の知る限り、現在XMLが使われているのは、Mavenなどの設定ファイルと、SOAPプロトコルと(Salesforce SOAP APIは現役です!)、SAML査証だけです。XSLなどのスキーマ定義やXSLTなどのHTMLへのデータ変換に至っては、ほとんど見かけなくなりました。その理由はおそらく、仕様を膨らませ過ぎたことにあるのではないかと筆者は思っています。そのため難解になり敷居が上がり、開発工数と戦うエンジニアの方々からは遠ざかってしまったのです。XMLが果たすはずだった役割は、今ではほぼJSONが代わりに果たしているように筆者には思えます。
このような経緯はあれど、ようやく当時の技術が実用化されて手元にあるのだと思うと、年末調整保険料控除XMLシステム開発に携わったエンジニアの方に感謝したい気持ちになりました。
こんにちは。エンジニアの佐藤です。今回はGraalVMのお話をさせていただきます。
先日他プロジェクトの方から「Webサーバーのリクエストが不思議なタイムアウトをしているが、どうしてだろうか?」と相談を受けました。話を聞いていると、メモリ(主記憶)利用超過が疑わしいと思いました。メモリは古来からある問題で、瞬間的にでも超過すると、サーバーはOS(通常はLinux)によって実行停止措置となってしまいます。今回の場合、そのリミットは1GB。仮想メモリはありませんので、この制限を踏み越えればアウトです。
インタビューによると、利用しているフレームワークはSpring Boot。長い歴史を持つJava言語のポピュラーなフレームワークです。しかし筆者はこのとき「どうしてNodeJSとかにしなかったのだろうか」と思っていましました。なぜならばこのSpring Boot、「Data JPA」を使うと、起動がものすごく重くなるからです。数年前に何とか速くならないものかと頑張ったこともあったのですが、どうしようもない降参だと思っていました。メモリ利用量も多く、ごく単純なアプリケーションでも300MB以上のメモリを消費するとされています。今回のリミットの1GB(=1000MB)と比較すると、何とも心配な消費量です。
そう言えば。。Java界隈での最近の話題と言えば、GraalVMです。これ自体は以前からあったのですが(2011~)、今年初夏にOracleが無償利用条件を緩和し、再び注目を集めています。Spring FrameworkでもSpring NativeとしてSpring Boot v3からサポートされています。GraalVMのホームページによると、起動時間の短縮とメモリ利用量 節約に効果があるとされています。今回のお悩みにミートしそうです。
しかし、(古い話ですが)Java言語は何といっても動的コンパイルの元祖です。四半世紀も続いたパラダイムを事前コンパイルにシフトするなど、可能なものでしょうか?そこはもちろんJavaの本家Oracleが頑張っているので大丈夫と信じたいですが、Javaの歴史は古いので、古いライブラリがサポートされていない可能性はどこまでもあります。一方、Javaと多くのアイディアを共有するMicrosoft .NETはネイティブイメージコンパイラNGenを10年以上前からサポートし、Microsoft製品のインストール経過で利用されているのを目にしていました。
この機会に、試してみることにしましょう。
最初に極小プログラムで試してみましょう。GraalVMサイトの以下のような内容です。なお、今回のテストはOracle Cloud A1インスタンス上のUbuntu 22.04 (arm64)で行いました。
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }
ネイティブイメージをビルドしてみると。。Javaクラスファイルに比べて非常に大きいのがわかります。(1000倍以上!)さらに、ビルドは非常に重い。4コアインスタンスでメモリを1GB近く消費して1分ほどかかりました。(たった4行のソースコードにです!)
ファイル | サイズ |
---|---|
HelloWorld.class | 427 bytes |
helloworld(ネイティブ) | 6781368 bytes (6MB!) |
実行時のメモリ消費量はどうでしょうか?このページに書かれているtimeコマンドで最大利用メモリ(Maximum resident set size)を調べてみると、以下のようになりました。
ファイル | Maximum resident set size (kbytes) |
---|---|
HelloWorld.class | 36044 (36MB) |
helloworld(ネイティブ) | 1220 (1MB!) |
利用メモリの劇的節約です!これは期待できそうです。
次に本丸のSpring Boot Web + DBアプリケーションで試してみましょう。Spring BootのGithubから以下のサンプルを選びました。
https://github.com/spring-guides/gs-accessing-data-mysql/tree/main/complete
以下のコマンドでネイティブイメージをビルドします。
mvn -Pnative native:compile
コンパイルは多量のリソースを必要とします。4コアインスタンスで10分ほどで、利用メモリは8GBに達しました。
すぐには成功しませんでしたが、何点か調整すると、成功しました。
遭遇したエラーと対処方法は以下のようなものです。
エラー | 対処方法 |
---|---|
MySQLドライバがロードできない | pom.xml依存関係にmysql-connector-jを追加。 |
実行時にクラス初期化エラー | pom.xmlのparentをspring-boot-starter-parent v3.1.4に最新化。Reachability Metadataサポート追加 |
出来上がったネイティブイメージのファイルサイズは非常に大きいです。
ファイル | サイズ |
---|---|
accessing-data-mysql-complete-0.0.1-SNAPSHOT.jar | 44MB |
accessing-data-mysql-complete(ネイティブ) | 157MB |
メモリ消費はどうでしょうか?サーバーが起動してから、最も単純なDB参照を伴うREST APIに1回レスポンスを返すまでの動作について、同様にtimeコマンドでメモリ消費量を比較してみると。。減りました!45%節約です!
ファイル | Maximum resident set size (kbytes) |
---|---|
Jarで実行 | 320568 (321MB) |
ネイティブで実行 | 175344 (175MB) |
今回の実験はここまでですが、これは期待できると思います。ただし、今回調査していて感じたことですが、開発コードを正しく動かす道のりは簡単ではない場合がありそうです。JVMから事前コンパイルへの方式変更の実行環境へのインパクトは大きく、ソリューションの品質保証のためには非互換の開発モジュールをどうやって動かすか?などの難局を乗り切る知識がきっと必要になるでしょう。
今回も最後までお読みいただきありがとうございました。