クレジットカード決済システムで利用するデータセンターの選定

エンジニアの佐野です。今日はインフラの話です。主に物理インフラの話です。カンムがデータセンター(以下、DC)の選定や契約をした際の勘所について書きます。クラウドと DC の相互接続であったりネットワーク構成や機器のコンフィグレーションなどのテクニカルな話はまた別途書こうと思います。

カンムでは主に AWSGCP 上にインフラを展開して開発を行っています。メインは AWS機械学習やデータプロセッシングの一部は GCP です。そして先に書いたとおり DC 契約もしています。基本的にはクラウド中心のインフラ運用ですが DC はビジネスパートナーと専用線接続するための重要な拠点となっていて、シンガポール拠点の企業などと専用線で接続しています。DC と AWS 間は AWS Direct Connect で接続しています。

今や特にスタートアップは DC を自前契約することはほとんどないと思っています。大企業でも自社 DC を持っていてさらに強力なプラットフォームチームがなければクラウドを採択するケースの方が多いんじゃないかと思います。強力なプラットフォームチームがいたとしてもパブリッククラウドを選択した方が良いかもしれません。しかしながら歴史のある業種であったりシステム連携先が多い事業体の場合どうしても専用線接続が必要なケースがあります。早くクラウドに対応してくんねーかな〜、と手をこまねいていても何も始まりません。しかし「よしじゃあやるか」と腰を上げるにも筋力が必要かと思います。今まで物理インフラを触ったことがない、触ったことはあるけど DC の選定をやったことがないという人はどうしたらいいかわからないんじゃないかと思います。ニッチかもしれませんがそんな状況の参考になれば幸いです。

  • DC という事業体
  • DC 選定の勘所
  • 契約書には適切な修正要求を出す

あたりについて書きます。


1. DC という事業体

まずDCとはなんなのか?契約するとはどういうことなのか?について整理しておきます。

まず DC とはなんなのか?について簡単に書くと、サーバやその他機器を稼働させるための通信回線、電力、機器の設置場所を提供する施設になります。その施設のスペックはピンキリで、世界各地とのコネクティビティを売りにしている事業体もあれば、強力な電力供給能力を売りにしている事業体もあります。サービスも様々で、機器や備品のレンタルや販売も行っている事業体、機器の設置代行や業務代行を行っている事業体もあります。契約する際にはいくつかの DC とコンタクトを取って話すことになるのですがけっこう面白いです。私は DC 作業や物理設計は過去に仕事でやったことがありますが DC 契約からするのはカンムが初めてだったのでなかなか新鮮でした。

続いてDCとの契約です。「データセンター」を検索すると、大手から小規模事業者までたくさん出てきます。これらの企業と契約して始めて DC を利用することができるようになります。これら DC 事業者が提供しているサービスを非常にざっくりいうと次の3つです。

  • コロケーション: 平たく言うと場所貸しです。決まったスペースと電源設備と通信設備を利用することができます。ラック(これはご存じの人も多いと思いますが機器を収納するための鍵のついた棚)の設置は借りた側がやります。
  • ハウジング: すでに備え付けられたラックを借りて、借りた側がラック内に自由にサーバやネットワーク機器を置いて運用するようなイメージです。電力や通信設備はコロケーション同様に利用することができます。
  • ホスティング: 俗に言うレンサバです。既に備え付けのサーバや機器を借りるようなイメージです。

カンムはクラウドにメインのインフラを置きつつ、特別な対応が必要な企業とは専用線接続する必要がありました。メインのAWS とは AWS Direct Connect で DC に繋いで、接続先企業に回線を伸ばすという要件です。よって AWS と接続先企業との中継をするネットワーク機器を置くためのラックが1つあれば十分でした。つまり上記3択とするとハウジングで小さなラックを1つ借りるのが適切です。以下、電源・通信設備およびラックをハウジング契約する前提として書きます。

2. DC 選定の観点

選定の観点を箇条書きにすると次の通りとなります。DC自体のスペックや扱いやすさに加えて、カンムの事業と接続先の要件を加味する必要があるためけっこうアイテムがあります。

  • カンムの事業的な観点
    • PCI DSS に準拠しているか
    • PCI DSS の証明書は欲しいときにすぐに発行してもらえるか
    • マルチキャリア対応していてそのキャリア回線のポートに空きがあるか
  • 運用観点
    • AWS との Direct Connect のしやすさ
    • オフィス/家からのアクセスが良いか
    • オフィスからの接続
    • 入館のしやすさ
    • 機材の持ち込み
    • 備え付けの設備
  • 設備のスペックとコスト
    • ラックあたりの電力
    • 空調
    • 耐震構造
    • UPSの有無

2.1 カンムの事業的な観点

カンムが事業をするにあたっての前提条件です。DCのスペックやコストに関する事を多く箇条書きにしました。しかしどんなに低コストで高品質な DC と契約できたとしてもここをクリアしないとカンムが事業を行うことができません。

2.1.1 PCI DSS に準拠しているか

カンムはクレジットカード決済をしている事業体として、 PCI DSS という業界セキュリティ水準に準拠していることが求められます。その範囲はカンムが契約する DC も含まれます。もちろん AWS など利用しているクラウドサービスや提携先の企業も例外ではありません。例えば AWS はサービスごとに PCI DSS に準拠しているかどうかを公開しています(https://aws.amazon.com/jp/compliance/pci-dss-level-1-faqs/)。よって DC およびその運営体が PCI DSS に準拠しているかどうか?がまず最初の選定の観点になります。

2.1.2 PCI DSS の証明書は欲しいときにすぐに発行してもらえるか

監査機関から少なくとも年に1回 PCI DSS の証明書の提示を求められます。上に書いた通りカンム自体の証明書だけでなく利用している DC のものも提示を求められます。その取得の手続きが比較的簡単な事業体を選びます(ました)。

2.1.3 マルチキャリア対応していてそのキャリア回線のポートに空きがあるか

専用線接続する企業から要件として、指定の回線事業者 x 2 とのマルチキャリア対応が求められていました。カンム側のDCと先方の DC 間は指定の回線を使って冗長性を担保した上で接続しますよ、と。ここでいう回線事業者というのは例えば日本国内であれば NTT 系や KDDI 系などの企業が該当します。海外企業で著名な事業者であれば Verizon, Telstra ...などになります。だいたい大丈夫だとは思うのですが、DC に指定の回線事業者の回線が来ていないということになると NG になるのでそれの確認は選定の時点で行いました。 加えてその回線事業者が接続するDC内のポートの空きの確認です。回線が来ていたとしても物理的に空きがないと使うことができません。そうなった場合はどうにかして増設してもらう必要があります(増設の可否は不明)。もしここでそのような事案になった場合プロジェクトが足止めになります。よって指定されたキャリアの回線の引き込みがされていて、かつ、それらが利用可能であること、というのを選定の条件としました。

2.2 運用観点

実際に契約が完了して DC を使えるようになると機器の設置や検証を経て本番運用が始まります。セキュリティとトレードオフになる事柄も多いのですが、DC への入館のしやすさや持ち込みに制限があるか?などの観点が必要です。

2.2.3 AWS との Direct Connect のしやすさ

AWS の接続拠点は日本国内にいくつかあり、そこと接続しやすく、コンフィグレーションがしやすい事業者を選びます。 AWS Direct Connect は AWS 側の設定に加えて DC 側の設定も必要です。DC 側の設定はエクセルや文書で申請するような事業者もあります。初期設定だけならともかく、設定変更の際も文書でやりとりするのはあまりやりたくないと思います。DCの管理コンソールがあり、こちらの自由なタイミングで自由に設定できるところがベターです。

2.2.2 オフィス/家からのアクセスが良いか

機器の設置や接続検証を行うタイミングであったり、実際に運用が始まってからも DC に行く機会があります。物理的な距離を考慮します。のっぴきならぬ事情で遠いDCを使う必要がある場合は運用代行してくれる会社と別途契約するなど運用負荷を軽減する必要も出てきます。ただしこれはこれで PCI DSS 的な監査が少し手間になります。

2.2.3 入館のしやすさ

DC は入館証を持っていけばすぐ入れる施設もあれば、事前にウェブや電話で申請を行い、許可が下りるまで待つ必要がある施設もあります。運用開始後に障害発生時の対応を考慮し、入館が煩雑ではないDCを選びます。

2.2.4 機材の持ち込み

設置する機器とは別に、スマートデバイスなどの通信機器やラップトップを持ち込めるかどうか?です。厳しいDCでは持ち込み一切禁止で持ち物検査がある施設もあります。機器を持ち込んで現地で設定を変更するときであったり、運用開始後の対応が煩わしくなると大変なので、我々の考える最低限の機材(スマートフォン、ラップトップなど)が持ち込み可能なDCを選択します。

2.2.5 備え付けの設備

機材の持ち込みと同様にDC 内部での作業のしやすさに繋がります。施設内に wifi は飛んでいるか?ドライバーやナット、ネジなどの貸し出しはあるか?など。テザリングしてもいいかもしれませんが wifi は欲しいところです。

2.3 設備のスペックとコスト

一番気になるポイントであり、意識しやすいポイントかと思います。

2.3.1 ラックあたりの電力

ラックにどのくらいの電力供給があるのかを確認しておきます。お金を積めば供給電力が増える場合もあります。機器をたくさん置きたい、電力消費が激しい機器を置きたい場合は電力供給が不十分だとマシンがダウンするリスクがあります。

2.3.2 空調

熱対策を確認しておきます。今時は大丈夫だと思いますが、古めの DC だったりするとちょっと熱対策に不安があるような場所もあります。ラック内に機器を置きすぎるなど利用する側の運用にもよるのですがエアフローが滞ると機器は簡単に熱落ちします。

2.3.3 耐震構造

耐震構造含め災害時にどのような対策が取られているのかを確認しておきます。

2.3.4 UPSの有無

予備電力です。災害時の生命線になります。

3. 契約書には適切な修正要求を出す

さて、選定が完了したら契約書にサインして契約を締結するのですが、割とノールックサインしてしまう人も多いんじゃないかと思います。しかしよく読みましょう。例えばこちらが設備を破損したときであったり何かしでかしてしまったときの賠償金額に法外な金額(小さな会社からすると会社が飛ぶような金額)が設定されていたりします。また指定の保険会社の保険への加入が義務づけられていたりするケースもあります(ありました)。 そして基本的には借りる側が不利になるような条項が書かれています。もちろんこれは悪意があるようなものではなく貸す側の論理から考えると理解できる内容です。しかしながら適切に修正要求を出すべきです。例えば「一切の責任を負わないものとする」 -> 「一切の責任を負わないものとする。ただし乙の過失があった場合はその限りではない。」など。こうしておけば何かあったときにそこに過失はあったのか?と争うことができます。前者のままだと争う余地すらありません。私は門外漢なので法務担当者に協力してもらいながら契約書の修正を要求しました。契約書はただの紙ではないです。

今日はここまで。いずれ AWS との接続の検証やネットワーク設計について書きます。

おわり

「LayerXとKanmu FinTechスタートアップセキュリティ事情」を開催しました

バンドルカードのソフトウェアエンジニアをしている summerwind です。最近は社内で解体屋と呼ばれています。

2022/09/30に株式会社LayerXさまと合同で「LayerXとKanmu FinTechスタートアップセキュリティ事情」というイベントを開催しました。今回のイベントでは創業当時から決済金融系の事業を行ってきた2社が、セキュリティにまつわる事例や知見を共有しました。FinTechとスタートアップの組み合わせならではの苦労や思いなどの話が聞けて、とても有意義な時間になったのではと感じています。

kanmu.connpass.com

カンムからは自分を含めて2名のエンジニアが登壇しましたので、そのセッションについても簡単にご紹介します。

カンムにおけるプロダクトセキュリティのこれまでとこれから

このセッションでは自分がプロダクトセキュリティ領域でカンムへの入社時からこれまでにやってきたことなどを紹介しました。こういう話はだいたい泥臭い内容になりがちなのですが、Twitter やアンケートなどで共感してもらえる方からのフィードバックをいただけたので、発表をしてみてよかったと感じています。

PCI DSS運用とv4.0対応

社内で一緒に PCI DSS の対応をやってくれている liva さんによる PCI DSS 運用についての紹介セッションです。Policy as Code による運用の自動化やリファクタリングなどは自分としても今後の動きが楽しみなところです。

最後に

今回のイベントではセキュリティに対する考え方や tfsec の導入による改善、PCI DSS の運用の話まで、セキュリティをテーマに様々な興味深い話を聞くことができました。イベントにご参加いただいたみなさま、またご一緒させていただいたLayerXのみなさま、本当にありがとうございました!

そして、カンムではプロダクト開発の知見を生かしてセキュリティを継続的に改善していくソフトウェアエンジニアを募集しています。今回のイベントをきっかけにカンムに少しでも興味を持った方は、ぜひカジュアル面談でお話しましょう!

▼カジュアル面談申込みはこちらから team.kanmu.co.jp

「Tech Meetup 〜Goで作る決済サービス〜」を開催しました

こんにちは、カンムでCOOやってます achiku です。

2022/08/04に、株式会社UPSIDERさま・株式会社BASEさまと合同で「Tech Meetup 〜Goで作る決済サービス〜」というイベントを開催いたしました。200名近くの方からお申込いただき、ありがとうございました!

upsider.connpass.com

このイベントでは、Go言語を使って決済サービスを開発する3社が集まり、セッションやディスカッションで各社の事例や知見を共有しました。Go × 決済という共通点を持つ3社ならではの苦労や裏話を聞ける、とても有意義な時間となりました。ご一緒させていただいたUPSIDERさま、BASEさま、貴重なお話をありがとうございました!

セッションに登壇しました

セッションにはバックエンドエンジニアの @_pongzu が登壇しました。

speakerdeck.com

Goの大きな特徴の一つである並行/並列を扱いやすい標準ライブラリを利用し、どのようにTCPコネクションを多重化しているかというお話です。しっかり定義された課題から始まる良い解決策でGoっぽいな〜という発表になったかと思います。

パネルディスカッションに参加しました

3社合同でのパネルディスカッションでは、自分(achiku)がモデレーターとして、バックエンドエンジニアの @hiroakis_ がパネラーとして参加いたしました。当日は以下のトピックでディスカッションを行いました。

  • 各社のアーキテクチャ紹介
  • Goを書いていてよかったと思うこと
  • 残高管理や決済などコア機能のテスト戦略
  • 開発チーム構成や開発プロセスで気をつけている事

3社3様のアーキテクチャや工夫があるのですが、やはりそれは各社個別のコンテキストが存在しそれに対して最適な策を検討しているからだなぁとモデレーターしながら感じておりました。もう少しパネリスト間でのやり取りも発生させることができればよかったかなぁぁという反省点もありますが、ひとまず皆さんに楽しんでいただけたようで何よりです。


www.youtube.com

最後に

決済に関わるスタートアップはどんどん増加しており、その中でGoをバックエンドの言語として採用するケースも多くなってきているなと感じております。今後とも決済事業だけでなく、Goという言語のコミュニティーにも貢献出来るよう、引き続きアウトプット頑張っていこうと思いを新たにしました。

今回はUPSIDERさま・BASEさま、このような機会をいただき本当にありがとうございました!

そしてカンムでは一緒に決済サービスを開発するエンジニアを大募集です!今回のイベントをきっかけに金融業界に少しでもご興味を持った方、ぜひカジュアル面談でお話しましょう!

▼カジュアル面談申込みはこちらから team.kanmu.co.jp

バンドルカードと Pool のカードが 3D セキュアに対応しました

バンドルカードの SRE をしている summerwind です。最近は A Philosophy of Software Design を読んでいます。

タイトルの通り、2022年6月21日からバンドルカードPool のカードが 3D セキュアに対応しました。バンドルカードではアプリですぐに発行可能なバーチャルカードを含む全てのカードで対応しているので、気軽により多くの加盟店での決済にご利用いただけるようになりました。

いつもはバンドルカードのインフラやセキュリティといった領域を担当しているのですが、3D セキュアの対応では久しぶりにバックエンドエンジニアとして自分もプロダクト開発に関わったので、今回は 3D セキュアの仕組みとその開発に関する話を簡単に紹介したいと思います。

3D セキュアとは

3D セキュアは、オンラインなど非対面でクレジットカードを使用して決済をする際に、カードの所有者であることを事前に認証する仕組みです。3D セキュアを使用することで、カード情報の盗用によるオンライン上での不正利用を防止できます。ショッピングサイトなどでカード決済をする際に突然パスワードの入力を求められた、といった経験がある方も多いかもしれません。あれが 3D セキュアによる認証です。

3D というのは三次元のことではなく3つのドメイン (Domain) すなわち、加盟店/アクワイアラ、決済ネットワーク、イシュアの3つの立場を指しており、以下の図のようにそれぞれの立場にあるシステムが連携して本人認証をする仕組みになっています。カンムのようにカード発行をするイシュアの立場では Access Control Server (ACS) と呼ばれるシステムを用意することで 3D セキュアによる認証に対応できます。

3D セキュアの認証フロー

3D セキュアの最新の仕様は「EMV 3-D Secure」と呼ばれており、EMVCo の公式サイトで参照できるので、興味がある方はぜひ参照してみてください。

3D セキュアの認証フロー

EMV 3-D Secure の仕様では「Frictionless Flow」と「Challenge Flow」の2つの認証フローが定義されています。それぞれの認証フローについて簡単に説明します。

Frictionless Flow

従来の 3D セキュアの仕組みでは、本人認証時のパスワードの手間が大きく認証時に決済を諦めてしまう人が多いという課題がありました。この問題を軽減するため、EMV 3-D Secure では決済時の情報に基づくリスクベースの判断をして、パスワード入力などの認証ステップなしに認証を完了させるフローに対応しています。これが Frictionless Flow です。

カンムのようなイシュアの立場では、ACS で加盟店/アクワイアラから送られてきた決済の情報などを元にリスクを分析し、追加の認証は不要と判断した場合は認証を完了させる動きをとることになります。

Challenge Flow

Frictionless Flow を実行した結果、ACS が追加の認証が必要と判断した場合は Challenge Flow に移行します。

Challenge Flow では決済をしようとしているユーザーが本人であるかを確認する認証フローを実行します。認証フローではまずイシュアが持つカードの発行情報に基づいて、カードの所有者にワンタイムパスワードを送信します。次にカードの所有者はイシュアから送信されてきたワンタイムパスワードを決済画面に入力します。ワンタイムパスワードの一致が確認できたら認証は成功です。

バンドルカードでは Challenge Flow の場合に次のような認証画面を表示しています。

Challenge Flow の認証画面

バンドルカードと Pool における ACS

バンドルカードと Pool で 3D セキュアに対応するには、イシュアの立場として ACS を実装して用意する必要があります。しかし ACSフルスクラッチで実装するには多くの時間とコストがかかるため、今回の対応では Visa 様が提供する Visa Consumer Authentication Service (VCAS)ACS として導入しています。

VCAS では ACS としての機能が一通り提供されており、3D セキュアの認証フローの各種処理に必要となる情報を専用の API を介してイシュアから VCAS に提供することで、ACS を実装せずとも 3D セキュアに対応できる仕組みになっています。

VCAS を ACS として使用した認証フロー

VCAS の導入とその開発

VCAS の導入にあたっては、バンドルカードと Pool のそれぞれで VCAS と接続するための仕組みを開発する必要がありました。ここからは開発の流れなどを簡単にふりかえって気をつけた点などを紹介してみたいと思います。

前提の確認と方針決め

まず、今回の VCAS の導入では次のような前提や制約がありました。

  • Visa 様との共同プロジェクトによる開発
  • ローンチまでのスケジュールが比較的がっちりと決まっている
  • バンドルカードと Pool で同時に VCAS に対応する必要がある
  • 可能な限り早くユーザーが 3D セキュアを利用できるようにしたい

これらの前提と制約を受け入れつつスムーズに開発を進めるために、ある程度細かい部分までの仕様は先に決めて関係者の間で合意をとりつつ、実装はバンドルカードと Pool で順番に細かく進め、得られた知見をそれぞれのプロダクトの実装に相互にフィードバックしながら品質を高めていく方針としました。

設計と社内レビュー

制約と方針に基づいて、まずは設計を固めることとしましたが、この段階では「バンドルカードと Pool で同時に VCAS に対応する」という制約が最初の課題になりました。これは単純に Pool が新しいプロダクトであり、今まで2つのプロダクトで同時に同じ対応をする、という動きがこれまでの社内では無かったためです。そこでまずはバンドルカードを社内のリファレンス実装として先に実装することに決め、そこで得られた知見をもとに Pool での実装を進めることにしました。

次に「VCAS とは何か」「その対応のための要件と制約は何か」「そして2つのプロダクトで同時に対応するにはどういった構成パターンが考えられるか」「バンドルカードではどういった改修が必要か」といった情報を整理してドキュメントにまとめ、社内で「ぶつかり」と呼ばれているレビュー会を開催して設計を確定させることにしました。

設計ドキュメント

事前にいくつかの構成案の良し悪しや、個人的に迷っているポイントなどをドキュメントにまとめておいたおかげで、「ぶつかり」では各構成案に対する意見がスムーズに集約できました。また既存のシステムの設計思想の共有や今回の開発においてはその思想がどう適用しうるかといった議論もでき、純粋によい学びの時間になったなと感じています。

役割の分担

「ぶつかり」により設計がうまく確定できたので次のステップである実装に入っていくわけですが、開発規模がそこそこ大きくなってきたのでここでエンジニア間の役割分担をすることにしました。

3D セキュアに対応するには VCAS 向けの開発とは別に Processor と呼ばれるカード決済を処理するシステムも改修をして、Visa 様による事前の仕様確認テストをパスする必要がありました。Processor についてもバンドルカードと Pool のそれぞれで改修が必要だったため、社内の Processor の匠である hiroakis にその対応をお願いしました。

また、リファレンス実装として最初に実装をするバンドルカードの内部はいくつかのコンポーネントに分かれており、コンポーネントごとに改修が必要だったため、直近でゴリゴリにバンドルカードの改善を進めてくれている Shiba にも実装の手伝いをお願いして、最終的にバックエンドエンジニアは3人体制での開発となりました。

実装とフィードバックの反映

役割分担ができたので、それぞれの担当領域で実装を進めていきます。自分は 3D セキュアによる認証時に VCAS と連携する API をまずバンドルカード向けに実装しました。

設計と「ぶつかり」によりコードレビュー時に必要となる土台の知識は共有できていると感じていたので、ここからは比較的見やすい単位でコードレビューを進めていけるような流れを作ることにしました。具体的には次のような単位で順番に Pull Request を作成して実装とコードレビューを進めていきました。

  1. ただ起動するだけの API サーバーの実装
  2. VCAS と連携に必要な API エンドポイントを API サーバーに追加 (各エンドポイント単位で数回繰り返し)
  3. バグ対応やリファクタリングのための修正など

バンドルカードの実装がある程度の形になって VCAS と接続しての結合テストが可能な状態になった後は、並行して Pool 用 API の実装も同様の流れで進めました。

何かを新しく実装する時は何回か実装を繰り返すとより良いコードが見えてくる...といったことがよくあるかと思いますが、今回は幸いにも同じような API を2回実装する機会ができたので、Pool 用の API の実装時にはバンドルカード用の実装の課題をうまく改善する試みができました。また Pool 用の実装でうまくいった点は逆にバンドルカード用の実装にフィードバックする、といったことができたのもよかったなと感じています。

テストの実施

バンドルカードと Pool のそれぞれの実装が形になったあとは Visa 様の管理するシステムと接続しての結合テストを実施します。結合テストは以下の2つを観点でそれぞれ独立して実施されました。

  • VCAS とバンドルカードおよび Pool の API が連携して 3D セキュアの認証が正しく動作するか
  • 決済ネットワークである VisaNet とバンドルカードおよび Pool の Processor が 3D セキュア認証後のオーソリを正しく処理できるか

それぞれの結合テストではレスポンスにおける誤った値の指定や処理漏れなどの不具合が見つかりましたが、幸いにも軽微な修正で対応が可能なもので比較的スムーズにテストを終えることができました。

結合テストが完了すると、その後は本番環境の構築とリリース前の最終的なテストを経て、無事に機能リリースとなりました。

おわりに

今回は 3D セキュアの仕組みとその対応にあたっての開発の流れなどを簡単に紹介してみました。個人的には 3D セキュア対応により多くのユーザーの決済機会を増やすことができたのを嬉しいと思うと同時に、久々のプロダクト開発も楽しめてとてもよい経験になりました。

バンドルカードでは 3D セキュアへの対応をはじめとして、今後も自分の持っている価値をどこでも自由に交換出来るようにする「価値交換」、自分の持つ価値をより良く制御できるようサポートする「価値制御」、自分が持っている価値を未来まで拡張する「未来価値」の3つの領域にフォーカスしてプロダクトの改善を進めていく予定です。これらの3つの領域の詳細については、ぜひ COO である achiku が書いた以下のブログ記事もあわせて参照してみてください。また、先日新たに提供を開始した Pool でも資産形成を軸に様々な価値を提供していきます。

akirachiku.com

カンムではこれらのプロダクトの改善に加わってくれるメンバーを募集中です。ブログ記事の内容を深掘りするだけのカジュアル面談も大歓迎ですので、ぜひお声がけください。

kanmu.co.jp

バンドルカードの本人確認改善の取り組み

デザイナーの@torimizunoです。

この記事では、バンドルカードでの本人確認改善の取り組みについて、プロジェクトチームの活動の一部をご紹介します。

バンドルカードの本人確認とは

バンドルカードの本人確認
バンドルカードの本人確認

バンドルカードのバーチャルカードは本人確認不要で利用を開始できますが、リアル+カードを発行する場合は利用上限額が上がるため、本人確認手続きが必要になります。 本人確認手続きの詳細はお伝えできないのですが、手続きの一部として、本人確認書類と撮影した本人確認書類と本人情報をご提出いただき、本人であるかの確認を行います。(以降、「本人確認」と呼びます)

確認及び一定の審査が完了すると、カードの発行を行い、お客さまのもとへカードが送られます。 本人確認ができなかった場合は再度申請をお願いすることになり、お客さまのもとへカードが届くのにお時間がかかってしまいます。

本人確認でき発行へ進めたことを承認率と定義し、カスタマーサポートチームが日々状況を計測していました。 2021年5月にこの承認率が過去最低値を記録したことから、改善に注力したプロジェクトが発足し、私はデザイナーとして参加しました。

プロジェクトメンバー
取り組みの流れ
  1. 目的と目標の認識あわせ
  2. 一次情報による原因調査
  3. 課題と仮説立て
  4. イデアから検討
  5. プロトタイピングと実装とリリース
  6. 効果を振り返る
  7. 振り返りから次の打ち手を再検討し実施(2〜6を繰り返し)

目的と目標の認識あわせ

本人確認の承認率には、最初の申請で本人確認ができた初回承認率と、何度か再申請を重ねて本人確認ができた最終的な承認率と、ふたつの定義がありました。

初回の承認率が上がれば、最短でお客さまにリアル+カードが届くことにつながり、その後の承認率向上にもなるため、初回の承認率に的を絞ることを決めました。 過去の最高値を超える値を平均としていくために、過去の最高値を目標値として目指すとしました。

一次情報による原因調査

何が原因でお客さまは本人確認ができなかったのか、住所の間違いや氏名の間違いなど、理由は分類して計測できるしくみが既にできていました。

原因の割合でそれまでも傾向は見ていましたが、プロジェクトメンバーによって一次情報まで確認できている人とそうでない人で課題感の差がありました。 そこでひとりひとりが多角的に課題を知るために、プロジェクトメンバー全員が数百件ずつ申請情報を見に行き、本人確認できなかった理由を抽出していきました。

例えば、本人確認書類の厚みは「表面の厚みや内容が識別できる」を判断基準で真正性を確認していますが、本人確認ができないものとしては下記のような原因が抽出されました。

  • 撮影した書類の全体像が写っていない
  • 書類がぼやけて本人情報が読めない
  • 暗すぎる、反射で本人情報が読めない
  • 厚みが見えない
  • 厚みの角度が急すぎて本人確認情報が見えない
  • 書類の裏面の厚みを撮影してしまっている

本人確認書類の厚みの判断基準
本人確認書類の厚みの判断基準

また、自分たちで一次情報を確認していると「この申請は自分が本人確認ルールに則ってチェックすると承認だと思うけど、非承認になっている」と感じる内容があることにも気が付きました。

課題と仮説立て

本人確認できなかった原因を見ていくと、申請前 申請中 申請後のフローごとの課題と仮説が見えてきました。

フローごとの課題
フローごとの課題

申請前

そもそもの本人確認書類の情報と、住所や氏名等の現在の情報が合っていない場合が見られました。本人確認の趣旨が伝わっていない、住所確認でなく送付先住所と思われている等の仮説があげられそうです。

申請中

書類の撮影が上手くできなかったり、入力する情報を間違えてしまうなどの課題が見受けられました。撮影した書類の情報が他者にとって読み取れる必要があることが伝わっていない、書類の厚みの必要性が伝わっていない、などの仮説があげられそうです。

申請後

本人確認のチェックをしている方たちが、人によって申請内容の承認にぶれがあることが見えてきました。判断に悩む要因を一律本人確認できないとしている可能性がありそうです。

ここから各課題の割合と影響人数を出し、インパクトのシミュレーションを作成し、初回承認率の上昇に影響のある大きさの課題から取り組む優先度を検討していきました。

イデアから検討

課題によって、プロダクト側から申請前・申請中にできるアプローチと、確認オペレーションで申請後に解決できそうなものと両軸が考えられそうなため、平行して検討と実施を進めることにしました。

オペレーションについては カスタマーサポート・不正検知・コンプライアンスのメンバーを中心として、本人確認を実施しているメンバーが判断に迷ったものはプロジェクトメンバーにエスカレーションしてもらう流れを一時的に行いました。 そこでプロジェクトメンバーがチェックを行い、迷う判断基準を言語化し、確認マニュアルに落とし込んでいきます。

プロダクト方面からのアプローチは、Googleスプリントの一部のプロセスを採用し、アイデアだしはクレイジー8を活用しながら実施していきました。

クレイジー8とは

  • 課題解決の参考になる情報を集め案を考える(手書きでOK)…(10分)
  • その時点での案をひとり3分ずつプレゼンする…(3分✕人数)
  • ほかのメンバーのプレゼンを聞いたあと、アイデアを「ひとりで」練り、8つのマスに各1分で書く(8分)
  • 各メンバーのソリューションを匿名で批評・検討し、ベストを決める(20分)

カンムではmeetを利用しているので、ワークショップはMTG中に作成できるホワイトボード機能を利用して実施しました。 私以外のメンバーはクレイジー8をするのは始めてだったのですが、ワークショップ中にどんどんアイデアを書き起こしていってくれて、すごいなと感じました。

クレイジー8で出たアイデア例
クレイジー8で出たアイデア

まずは厚み・生年月日・氏名を取り組むことを決めたので、各課題について毎週のようにアイデアだしを行い、最終的にはどのアイデアが最も初回の本人確認の承認率を上昇させそうかを軸に投票し、ベストなアイデアを絞り込みつつ考慮すべき点もメンバーで洗い出していきました。

プロトタイピングと実装とリリース

イデアを絞り込んだ後は、ひたすらプロトタイプを作成してはチームメンバーで操作してブラッシュアップをしていきました。 例えば申請中の厚みの課題に対して、下記のような施策を実施しました。

①撮影前に注意ポイント画面を挟む 厚みは表面の撮影が必要なこと、厚みと情報が読める必要がある点を伝わりやすくする

撮影前
撮影前

②撮影時のガイドを読みとりやすいものへ 後ろ倒しの斜めから、手前斜め45度のガイドに変更して確認している本人情報が見えやすい角度での撮影を促す

撮影時
撮影時

③撮影後の確認画面の調整 厚み撮影後にチェックする箇所を確認する体験を挟み、注意点に気付けるようにする

撮影後
撮影後

ユーザーに表示される画面が増え完了までその分お時間が発生してしまうのですが、撮影で気をつけるポイントがわからず、撮影が上手くいかず申請後にやり直しが発生するほうがユーザーにとってもサービスにとってもデメリットが大きいと判断し、撮影前と撮影後のガイドを充実させました。 他にも、細やかにライティングの調整等を実施しています。

効果を振り返る

各アイデアを順にリリースしていき、それぞれで効果の振り返りをしていきます。 施策によっては仮説していた想定効果がでないものもあり、新たな仮説を立て直し次の施策へと回していきました。

仮説に対して効果があったものでいえば、例にもあげていた申請中の厚みに対する施策は効果が見られました。 リリース前とリリース後で厚みが原因の割合がどう変化したのか調査したところ、一時期は本人確認できなかった原因の平均4割を占めていましたが、施策以降は最小1割、平均2割以下にさがっていきました。

本人確認できなかった厚みの原因の100%のうち、改善前は「読み取れない&角度が急で読めない」が55%を占めていましたが、施策後は18%になり37%の減少が見られました。 裏面の厚みを撮影してしまう27%を占めていましたが、17%の割合に減少しました。

厚みが原因の内訳の変化
厚みが原因の内訳の変化

振り返りから次の打ち手を再検討し実施

振り返りでの分析で、数年前に申請して久しぶりに再申請を試み、本人確認できないユーザーが半数くらいいることもわかりました。 これに関しては、現状申請時にどの書類で申請したか情報を保存できておらず、一度でも再申請が発生すると、初回のガイドつき申請UIでなくガイドなしUIになってしまっている課題があるため、申請時に書類の情報の取得から順次取り組みをはじめています。

振り返りを重ねながら継続的に施策を実施した結果、2022年1月に初回承認率は目標値を達成し、その後数ヶ月安定して経過しています。

撮影時の厚みの課題以外に、入力時の間違いを減らす施策に関しても施策を繰り返しているため、また別の記事としてお伝えしていきたいです。

今回のプロジェクトから得た学び

実際に申請された情報をひとつひとつチームメンバーが見にいくことで、課題抽出の解像度があがりました。 それがメンバー間での意見活性化や、仮説とアイデアの立てやすさに繋がった感覚があります。 プロジェクトメンバー全員が一次情報をきちんと見ようとした意識で自然と動いていったのは、カンムの「事実と向き合う」文化がにじみ出ているのではないかと思います。

施策によっては効果がなかったものもありますが、実施したことでこの仮説ではない…という事実がわかったトライ自体に価値があるとカンムに来てから感じています。

一度施策を実施した上で再び一次情報を見にいった際、わかった事実を自分が持っているので、今まで見えてなかった観点の課題が見えるようになった時は学びが自分の中に入っている感覚がありました。その感覚が知れたことが嬉しいです。

引き続き、バンドルカードは「誰もがかんたんにわかる」プロダクトとしての品質を高めるアップデートを続けていきます。

採用リンク

カンムではプロダクトを一緒に磨いてくれる仲間を募集しています。

採用情報 team.kanmu.co.jp

社内イベント: エディタについて語る会で Vim script と ISO8583 の話をしました

エンジニアの佐野です。最近記事を書いていなかったので小ネタです。先日、菅原企画の社内イベント、エディタについて語る会が催されました。職種にもよりますがカンムでは多くの従業員はオンラインで業務を行っています。たまにはオフラインで交流も...ということで来れる人はオフィスに集まってエディタの話をしつつ軽食を楽しむというコンセプトです。

当日は Vim, Emacs, Visual Studio Code, nano... と様々なエディタのゆるい話から熱い話が語られました。私は VimVim script について話したので今日はそれを記事化します。


0. 私とエディタ

私は長らく Vim をエディタとして使っています。「エディタ」というものを意識したのは大学生の頃でしょうか。機械工学系だったのですがソフトウェア工学や C や C++ がカリキュラムにあり自分もそれらを履修しました。それらの演習では Microsoft Visual Studio (Visual C++だったかな...?)やメモ帳を使っていました。それにしてもなぜメモ帳だったのか...。

大学入学は2000年でした。ちょうどその頃に一般家庭にもインターネットが普及し始めました。IT革命という言葉が世間に踊った時期だった気がします。住んでいたアパートにもインターネット回線を導入して私はインターネットにどっぷりハマっていきました。インターネットに触れることでコンピュータに興味が出てきた私は Perl を独学したり PC を自作してサーバ構築の真似事のようなことを始めました。コードを書くときに前述の Visual Studio を使いたかったのですが有償(だったと思う)で手が出しにくい、メモ帳では機能が微妙すぎる、何かないだろうか?と本屋を物色していたら Vi (Vim), Emacs あたりの本を見つけました。どうやら Vi (Vim) というのがシンプルで良いらしいということで Vi を使い始めました。

使い始めた当初は「モードって何?」「なんでバックスペースで削除できないんだ?」という状態でした。普通の人からするとそんなものだと思います。設定ファイルの存在も知らず、毎回起動しては :set nu (行番号を表示する)を打っていました。

サラリーマンになって Java を書いていたときは Eclipse を使っていましたが、それ以外のプログラミング、サーバでのオペレーション、ファイル編集などはずっと Vim でした。これはほとんどのマシンに標準で入っていたというのと、大学からの流れで手に馴染んでいる、というのが理由でしょうか。

その後、新しいエディタやIDEが出る度に乗り換えを試みますが、結局は Vim に戻ってきました。私はロールとして長らくサーバサイドのアプリケーションエンジニアとインフラエンジニアをやっています。サーバに ssh してマシン上で作業したり手元でコードを書いたり...業務中は端末を操作している時間が長いため、多くの作業をターミナルで完結させたくなります。他のエディタを起動して時にターミナルに切り替えたり...というのがちょっとしたストレスになり、結局はVim に(ターミナルに)戻ってきてしまいます。

最近では GoLand や VSCode への乗り換えを試みましたがやはりダメでした。

1. Vim の設定

思い出話が長くなりました。学生の頃から使っていた Vim ですが、初期のころはいろいろなプラグインを入れたり設定をこねくり回したりして盆栽のように設定を育てていました。今はプラグインはだいぶ減って以下のもののみになっています。

これらのプラグインとともにいくつかの設定を施しています。 Vim を知らない人向けに少し書くと、Vim の設定ファイルは .vimrc で基本的な配置位置は $HOME/.vimrc になります。Vim のカスタマイズはこちらのファイルを編集することで行います。私の設定は以下のようになっています。

"=========================================================
" Basic Configuration
"=========================================================

set notitle
set nocompatible        "vi互換をoff
set nobackup            "バックアップファイルを作らない
set noswapfile          "スワップファイルを作らない
set number              "行番号を表示
set laststatus=2        "ステータスを常に表示
set showmode            "モードを表示する
set showcmd             "コマンドを表示
set noshowmatch
set display=uhex        "謎の文字を16進数で表示
set wildmenu            "補間候補を表示する
set wrap                "自動折返しを有効
set expandtab           "タブをスペースに変換
set tabstop=2           "タブをスペース4つ分として表示する
set shiftwidth=4        "シフトで移動する文字幅
set softtabstop=2       "タブキーを押したときに挿入する半角スペースの数
set noincsearch         "インクリメンタルサーチはしない
set wrapscan            "最後まで検索したら先頭へ戻る
set ignorecase          "大文字小文字無視
set smartcase           "検索文字列に大文字が含まれている場合は区別して検索する
set hlsearch            "検索文字をハイライト
set splitbelow          "新しいウィンドウを下に開く
set splitright          "新しいウィンドウを右に開く
set nocursorline        "カーソルのある行をハイライトしない
set nocursorcolumn      "カーソルのある列をハイライトしない
augroup Cursor
  autocmd WinLeave * setlocal nocursorline "カレントウィンドウから離れたらカーソルハイライトを消す
  highlight ZenkakuSpace cterm=underline ctermfg=lightblue guibg=#666666 "全角スペースを見えるようにする
  autocmd BufNewFile,BufRead * match ZenkakuSpace / /
augroup END

filetype plugin indent on

"=========================================================
" Private
"=========================================================

let mapleader = "\<Space>"
nnoremap <Leader>r :reg<CR>

"=========================================================
" Encode
"=========================================================
"表示するときの文字コード(ターミナルの設定と同じ)
set encoding=utf-8
"保存するときの文字コード
set fileencoding=utf-8
"文字コード自動判別の候補とする文字コード種を列挙
set fileencodings=iso-2022-jp,euc-jp,cp932,utf-8

"=========================================================
" Plugin management
"=========================================================

" プラグインインストールディレクトリ
let s:dein_dir = expand('~/.cache/dein')
let s:dein_repo_dir = s:dein_dir . '/repos/github.com/Shougo/dein.vim'

" dein.vim がなければ取得
if &runtimepath !~# '/dein.vim'
  if !isdirectory(s:dein_repo_dir)
    execute '!git clone https://github.com/Shougo/dein.vim' s:dein_repo_dir
  endif
  execute 'set runtimepath^=' . fnamemodify(s:dein_repo_dir, ':p')
endif

if dein#load_state(s:dein_dir)
  call dein#begin(s:dein_dir)

  let g:rc_dir    = expand('~/.vim/rc')
  let s:toml      = g:rc_dir . '/dein.toml'
  let s:lazy_toml = g:rc_dir . '/dein_lazy.toml'

  call dein#load_toml(s:toml,      {'lazy': 0})
  call dein#load_toml(s:lazy_toml, {'lazy': 1})

  call dein#end()
  call dein#save_state()
endif

if dein#check_install()
  call dein#install()
endif

"=========================================================
" Plugin configuration
"=========================================================
" gitgutter
set updatetime=250
let g:gitgutter_max_signs = 500

" goimports
let g:goimports = 1

" vim-quickhl
nmap <Space>m <Plug>(quickhl-manual-this)
xmap <Space>m <Plug>(quickhl-manual-this)
nmap <Space>M <Plug>(quickhl-manual-reset)
xmap <Space>M <Plug>(quickhl-manual-reset)

"=========================================================
" Color
"=========================================================
colorscheme cyberspace
set background=light

" 256色
set t_Co=256
" 背景色
set signcolumn=yes
hi SignColumn ctermbg=black
hi SignColumn guibg=black

"=========================================================
" Popup color
"=========================================================
hi NormalFloat guifg=#ffffff guibg=#191970
hi Pmenu guifg=#ffffff guibg=#191970

"=========================================================
" status line color
"=========================================================
set noshowmode
let g:lightline = { 'colorscheme': 'wombat' }

syntax enable

前半部分に set xxxx という設定が羅列されていますが、プラグインマネージャ dein の箇所には変数 ( let ... )や関数呼び出し ( call ... )、外部コマンドの呼び出し (execute '!git ...)、条件分岐 ( if ... ) が表れたりします。 set xxxx も含めこれらは Vim script と呼ばれます。つまり Vim の設定ファイルやプラグインの正体は Vim script の塊です。

ちなみに Vim script はオブジェクト指向プログラミングもサポートしています。 Vim script は単なる設定ではなくれっきとしたプログラミング言語です。

2. Vim script を書く

ではその Vim script を書いてみます。Vim の設定というよりは Hello, world のような簡単なプログラムを書いてみます。

Vim script の始め方は簡単で、 Vim を起動したらコマンドラインモードでそのまま Vim script が書けます。次の例ではテキストの2行目から4行目を取得して echo で結果を出力しています。結果はそのままステータスラインに表示されます。

map() などの便利な組み込み関数もあります。似たようなコードですが、2行目から4行目の数値をリストで取得して出力、続いてそれを map 関数を通して倍にして出力しています。

一連の処理は .vimrc に関数として定義することで call FuncName() で利用できます。次の関数は数値のリストを受け取ってそれを加算するものです。

3. ISO8583

ここまでで Vim script で普通のプログラミングができることを示しました。さて、カンムと言えば ISO8583 らしいのですが...先日の GoCon オフィスアワーで ISO8583 を Go で Parse してみましょうという出題がありました。これを Vim script で倒してみます。

ISO8583 自体の説明や GoCon の問題の説明や解説は下記のエントリを参照していただけると幸いです。が、少しだけ解説します。ISO8583というのはクレジットカードのデータ通信時に使われるプロトコルで、店頭でカードを切ったときにこのプロトコルにのっとってデータが飛んできます。カード会社はこのプロトコルを捌く必要があり、カンムでもこれを処理するサーバとアプリケーションが元気に稼働しています。これは Vim script ではなくは Go で書かれています。

tech.kanmu.co.jp

3.1 問題のファイルを読む

問題を解くにあたりまずやるべきことは問題のバイナリデータを読むことです。Vim script でもファイルシステムからファイルを読むことができます。次のようにファイルをバイナリモードで読み込んで1バイトずつ処理できます。

let inputfile = "/path/to/github.com/kanmu/gocon-2022-spring/message.bin"
for b in readfile(inputfile, 'B')
  " ISO8583 Processing
endfor

3.2 結果を送信する

さらに問題を解くには結果を送信する必要があるのですが Vim script でも当然できます。ソケットを開いてそこに HTTP を書き込むことで HTTP 通信ができます

let channel = ch_open("168.138.192.92:80",
            \ {"callback": "Callback", "mode": "raw", "waittime": "1000ms"})
let body = printf("{\"input\":{\"Type\":%d,\"PrimaryAccountNumber\":%x,\"ProcessingCode\":%x,\"  AmountTransaction\":%x,\"ExpirationDate\":%x}}",
            \ str2nr(mt), pan, pc, amount, ed)
let header = printf("POST /v1/data/iso8583/validation HTTP/1.1\r\nHost: 168.138.192.92\r\nConte  nt-Type: application/json\r\nContent-Length: %d\r\n\r\n",
            \ len(body))
let payload = header . body
call ch_sendraw(channel, payload)

3.3 ビット演算をする

ISO8583 を処理するにはビット演算を活用する必要があります。こちらについては詳細は上述したとおり GoCon の問題解説のエントリを見てください。and()論理積がとれます。 こちらはビットが立っている場所を調べて、立っていたら処理を行う...というコードになります。

" PAN
if and(bitmap, 0b0100000000000000000000000000000000000000000000000000000000000000) != 0
  if i == 10
    let panbyte = b/2
    let i = i + 1
    continue
  endif
  if i <= 10+panbyte
    let shift = 8 * (panbyte - (i - 10))
    let pan =  pan + b * float2nr(pow(2, shift))
    let i = i + 1
    continue
 endif
endif
" Processing Code
if and(bitmap, 0b0010000000000000000000000000000000000000000000000000000000000000) != 0
  if i <= 10+panbyte+pcbyte
    let shift = 8 * (pcbyte - (i - (10 + panbyte)))
    let pc =  pc + b * float2nr(pow(2, shift))
    let i = i + 1
    continue
  endif
endif

3.4 ビットシフトする

バイナリを処理する傍らビットシフトしたくなるんですが Vim script でも当然それはでき....なかった! Vim script にシフト演算子はありません(たぶん...)!よってここは2のべき乗で対応します。

if i <= 10+panbyte
  let shift = 8 * (panbyte - (i - 10))
  let pan =  pan + b * float2nr(pow(2, shift))
  let i = i + 1
  continue
endif

これは何をやっているのかというと、例えばカード番号 4019-2499-9999-9999 は ISO8583 では次のように BCD で表現されています。

0x40, 0x19, 0x24, 0x99, 0x99, 0x99, 0x99, 0x99

1バイトずつ処理していくため、まず 0x40 を左に7バイトシフトして 0x4000000000000000 とする、次に 0x19 を左に6バイトシフトして 0x19000000000000 とする... を行い、

 0x4000000000000000
   0x19000000000000
     0x240000000000
     ...
+
------------------------
4019249999999999

といった形でカード番号を取り出しています。

4. ISO8583() 関数を書く

重要な処理はだいたいここまでです。あとは一連の流れを組み立てていきます。結果、ISO8583 を処理する関数を書くと次のようになります。

function! ISO8583()
  let inputfile = "/path/to/github.com/kanmu/gocon-2022-spring/message.bin"

  let mt = ""
  let bitmap = ""
  let bitmapbyte = 8

  let panbyte = 0
  let pan = ""

  let pc = ""
  let pcbyte = 3

  let amount = ""
  let amountbyte = 6

  let ed = ""
  let edbyte = 2

  let i = 0
  for b in readfile(inputfile, 'B')
    " message type
    if i <= 1
      let mt = mt . printf("%02s", b)
    endif

    " bitmap
    if i >= 2 && i <= 9
      let shift = 8 * (bitmapbyte - (i - 1))
      let bitmap =  bitmap + b * float2nr(pow(2, shift))
    endif

    " data element
    if i >= 10
      " PAN
      if and(bitmap, 0b0100000000000000000000000000000000000000000000000000000000000000) != 0
        if i == 10
          let panbyte = b/2
          let i = i + 1
          continue
        endif
        if i <= 10+panbyte
          let shift = 8 * (panbyte - (i - 10))
          let pan =  pan + b * float2nr(pow(2, shift))
          let i = i + 1
          continue
        endif
      endif
      " Processing Code
      if and(bitmap, 0b0010000000000000000000000000000000000000000000000000000000000000) != 0
        if i <= 10+panbyte+pcbyte
          let shift = 8 * (pcbyte - (i - (10 + panbyte)))
          let pc =  pc + b * float2nr(pow(2, shift))
          let i = i + 1
          continue
        endif
      endif
      " Amount
      if and(bitmap, 0b0001000000000000000000000000000000000000000000000000000000000000) != 0
        if i <= 10+panbyte+pcbyte+amountbyte
          let shift = 8 * (amountbyte - (i - (10 + panbyte + pcbyte)))
          let amount =  amount + b * float2nr(pow(2, shift))
          let i = i + 1
          continue
        endif
      endif
      " Expiration date
      if and(bitmap, 0b0000000000000100000000000000000000000000000000000000000000000000) != 0
        if i <= 10+panbyte+pcbyte+amountbyte+edbyte
          let shift = 8 * (edbyte - (i - (10 + panbyte + pcbyte + amountbyte)))
          let ed =  ed + b * float2nr(pow(2, shift))
          let i = i + 1
          continue
        endif
      endif
    endif
    let i = i + 1
  endfor

  echo "--------------------"
  echo printf("Message Type: %s", mt)
  echo printf("Bitmap: %b", bitmap)
  echo printf("PAN: %x", pan)
  echo printf("ProcessingCode: %x", pc)
  echo printf("Amount Transaction: %x", amount)
  echo printf("Expiration Date: %x", ed)
  echo "--------------------"

  let channel = ch_open("168.138.192.92:80",
              \ {"callback": "Callback", "mode": "raw", "waittime": "1000ms"})
  let body = printf("{\"input\":{\"Type\":%d,\"PrimaryAccountNumber\":%x,\"ProcessingCode\":%x,\"AmountTransaction\":%x,\"ExpirationDate\":%x}}",
              \ str2nr(mt), pan, pc, amount, ed)
  let header = printf("POST /v1/data/iso8583/validation HTTP/1.1\r\nHost: 168.138.192.92\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n",
              \ len(body))
  let payload = header . body
  call ch_sendraw(channel, payload)
  echo payload
endfunction

function! Callback(handle, msg)
  echo a:msg
endfunction

ここで :call ISO8583() を呼び出してみます。クリアできました。

最後に

あまりドスの効いていない社内イベントの紹介記事になってしまいましたが...。Vim やカンムの社内イベントに興味を持っていただけたら幸いです。

Go Conference 2022 Spring: クイズ ISO 8583 Message Challange の紹介と解説

バンドルカードのバックエンドやインフラを担当しているエンジニアの summerwind です。最近は WebAssembly と JIT に興味があります。

4月23日に開催された Go Conference 2022 Spring ではカンムのメンバーが Go に関する内容で登壇しました。セッションで紹介したスライドは以下で参照できます。将棋プログラミングについては自分もまったく知らない世界の話だったのでとても興味深かったです。


今回のイベントにはカンムもスポンサーとして参加させていただき、今回もオフィスアワーの催しとして Go を使ったクイズ「ISO 8583 Message Challange」を公開しました。

クイズの問題

今回のクイズの問題は以下のリポジトリで参照できます。もし興味がありましたらぜひチャレンジしてみてください。

github.com

今回の問題のテーマは「バイナリ処理」です。インターネットでの通信やデータの保存などでは様々な形式のバイナリが使われていますが、一般的なプロダクト開発ではバイナリを直接扱うようなコードを書く機会は意外と少なかったりします。個人的には Go でバイナリを扱うコードを書くが好きなので、今回のクイズではバイナリのパースを通じてより多くの人にバイナリの操作を楽しんでもらえたらと思い、このテーマを設定してみました。

問題では、カンムの決済処理にも使われている ISO 8583 形式のメッセージをパースしてその中から答えとなる値を見つける、というゴールを設定しています。メッセージのバイナリファイルはリポジトリmessage.bin として保存されているので、これをパースしていくと答えが見つかる仕組みです。今回の問題では、より手軽にクイズにチャレンジしてもらえるよう以下のように parse.go ファイルに定義された Parse() 関数の中身だけを実装すれば回答を出せるようにしてみました。

問題の解説

ここからは実際にバイナリをパースしていく方法を解説してみたいと思います。

まず最初に、実装が必要な Parse() 関数の定義を見てみると次のようになっています。引数 buf にはバイナリの値が byte の配列として保存されており、*Message を返せばいいことが分かります。

// ISO 8583 メッセージのバイト列をパースして Message を返す関数
func Parse(buf []byte) *Message {
    var msg Message

    // TODO: バイト列 buf をパースして msg の各フィールドに値を設定してください

    return &msg
}

次に、戻り値である *Message の構造を見みてみると、次のようになっています。

// ISO 8583 メッセージ構造体
type Message struct {
    // メッセージ: 3桁の数字
    // ISO 8583 バージョンの 0 は含まないことに注意してください
    Type uint16

    // カード会員番号 (PAN): 16桁の数字
    PrimaryAccountNumber uint64

    // 処理コード: 6桁の数字
    ProcessingCode uint32

    // 取引金額: 任意の桁の数字
    AmountTransaction uint64

    // 有効期限: YYMM の4桁の数字
    ExpirationDate uint16
}

この構造から今回のクイズでは、バイナリから以下の5つのデータをパースして抽出すればよいことが分かります。

  • メッセージタイプ
  • カード会員番号
  • 処理コード
  • 取引金額
  • 有効期限

データの抽出には ISO 8583 メッセージのバイナリフォーマットの知識が必要になります。弊社の hiroakis が以前発表した資料にフォーマットの詳細と詳しい解説がありますので、ここからはこの資料とあわせて読み進めてみてください。

最初にメッセージタイプをパースします。これはメッセージの種類などを示す値でメッセージの先頭2バイトを4ビットのパック10進数 (Packed BCD) として扱う必要があります。今回の解説では処理を簡略化するため、BCD の処理については公開されているパッケージである https://github.com/albenik/bcd を利用していきます。

// メッセージタイプを取得
msg.Type = bcd.ToUint16(bin[0:2])

次にカード会員番号のパースに取り掛かりたいところですが、最初にビットマップと呼ばれる領域を取得しておく必要があります。

ビットマップはメッセージにどんな種類のデータが存在するかを示すビットを保存している64ビットの領域です (64ビットより長い場合もあります) 。この領域の各ビットの値が1だった場合はそのビット位置に対応するデータの存在を示しています。例えば、ビットマップの値が2進数で 01110010 であった場合、2、3、4、7番目のデータが存在していることを示しています。

// ビットマップ領域を取得
bitmap := binary.BigEndian.Uint64(bin[2:10])

ビットマップが取得できたので、カード会員番号を抽出します。カード会員番号の存在を示すのはビットマップの左から2ビット目の値なので AND 演算で確認します。ビットが存在した場合は値を抽出しますが、カード番号の値は可変長の値なので、最初に先頭1バイトの長さを抽出し、その長さ分のバイト数を Packed BCD として読み出します。

// バイナリのオフセット
offset := 10

// カード会員番号のビットの存在を確認
if (bitmap & 0x4000000000000000) > 0 {
    // カード会員番号の長さを取得
    length, _ := binary.Varint(buf[offset : offset+1])
    offset += 1

    // カード会員番号を取得
    msg.PrimaryAccountNumber = bcd.ToUint64(buf[offset : offset+int(length)])
    offset += int(length)
}

次に処理コードを抽出します。カード会員番号と同じようにビットマップで存在を確認してから、処理コードの値を取得します。処理コードの値は3バイトの固定長なので、そのまま読み出します。

// 処理コードのビットの存在を確認
if (bitmap & 0x2000000000000000) > 0 {
    // 処理コードを取得
    msg.ProcessingCode = bcd.ToUint32(buf[offset : offset+3])
    offset += 3
}

残りは取引金額と有効期限になりますが、これらは処理コードと同じ固定長のフィールドなので、次のように処理コードと同じように値を取得できます。

// 取引金額の取得
if (bitmap & 0x1000000000000000) > 0 {
    msg.AmountTransaction = bcd.ToUint64(buf[offset : offset+6])
    offset += 6
}

// 有効期限の取得
if (bitmap & 0x0004000000000000) > 0 {
    msg.ExpirationDate = bcd.ToUint16(buf[offset : offset+2])
    offset += 2
}

実はビットマップの存在確認に使用する各ビットの値は定数として事前に定義されているので、それらを使用すると最終的な Parse() の実装は次のようになります。

// ISO 8583 メッセージのバイト列をパースして Message を返す関数
func Parse(buf []byte) *Message {
    var msg Message

    // メッセージタイプを取得
    msg.Type = bcd.ToUint16(buf[0:2])

    // ビットマップ領域を取得
    bitmap := binary.BigEndian.Uint64(buf[2:10])

    // バイナリのオフセット
    offset := 10

    // カード会員番号を取得
    if (bitmap & BitPrimaryAccountNumber) > 0 {
        length, _ := binary.Varint(buf[offset : offset+1])
        offset += 1

        msg.PrimaryAccountNumber = bcd.ToUint64(buf[offset : offset+int(length)])
        offset += int(length)
    }

    // 処理コードを取得
    if (bitmap & BitProcessingCode) > 0 { 
        msg.ProcessingCode = bcd.ToUint32(buf[offset : offset+3])
        offset += 3
    }

    // 取引金額の取得
    if (bitmap & BitAmountTransaction) > 0 {
        msg.AmountTransaction = bcd.ToUint64(buf[offset : offset+6])
        offset += 6
    }

    // 有効期限の取得
    if (bitmap & BitExpirationDate) > 0 {
        msg.ExpirationDate = bcd.ToUint16(buf[offset : offset+2])
        offset += 2
    }

    return &msg
}

実装ができたのでクイズの README.md の記載に従いコードを実行してみると、バイナリファイルから正しい値を取得して正解が表示されました。

$ go run .
--------------------
Message Type: 100
PAN: 4019249999999999
Processing Code: 327327
Amount Transaction: 1138
Expiration Date: 2204
--------------------
Result: VALID: You have successfully parsed the ISO 8583 message! ...

回答判定の仕組み

問題が解けたところで、今回のクイズの回答判定の仕組みについても簡単に紹介します。

カンムではこれまでにも Go Conference の開催にあわせて CTF やクイズを公開してきました。これまでの問題では出題を担当するメンバーが独自に回答を判定する仕組みを実装していましたが、今回のクイズ作成にあたっては、より汎用的な回答判定をする仕組みを採用してみることにしました。

今回の回答判定に採用したのは Open Policy Agent (OPA) です。OPA を使用すれば正解判定を Rego で記述したポリシーとして扱うことができるため、非常に簡単に回答判定の仕組みが用意できました。今回の回答判定では次のようなポリシーを使用しています。

package iso8583

default validation = "INVALID"

validation = "VALID: You have successfully parsed the ISO 8583 message!" {
  input.Type == 100
  input.PrimaryAccountNumber == 4019249999999999
  input.ProcessingCode == 327327
  input.AmountTransaction = 1138
  input.ExpirationDate = 2204
}

目視によるパース

実は ISO 8583 形式のメッセージは16進数で見てみると、ある程度データの値を推測できます。実際に目視でパースして正解した、という方もいたようでした。

$ hexdump -C message.bin
00000000  01 00 70 04 00 00 00 00  00 00 10 40 19 24 99 99  |..p........@.$..|
00000010  99 99 99 32 73 27 00 00  00 00 11 38 22 04        |...2s'.....8".|
0000001e

おまけ

クイズのバイナリに含まれる 3271138 といった値はスターウォーズでたびたび登場するマジックナンバーに由来しています。これは Go のトリビアをリスペクトしてみました。

おわりに

Go Conference の開催中は、Twitter などでクイズの問題に実際に挑戦してバイナリパースの楽しさを感じてくれた方のコメントを見かけたりして嬉しかったです。

カンムでは実際に ISO 8583 のメッセージを処理するシステムを開発して決済サービスを提供しており、バイナリ処理に楽しさを感じるようなエンジニアを募集しています。カジュアル面談などは随時実施していますので、ぜひお気軽にお声がけください。

kanmu.co.jp

最後に、今回も素晴らしい Go Conference の場を提供してくれた運営のみなさま、参加者のみなさま、どうもありがとうございました!