Simple minds think alike

より多くの可能性を

【Golang】for-rangeループの中でポインタを使うと同じに値がなる

Go言語を使っているとfor-rangeループを多用します。その際、ポインタが絡むと予期しない結果になることがあるので、その事象の発生理由と対策をまとめてみたいと思います。

事象

Aさん(56歳)、Bさん(33歳)、Cさん(41歳)、Dさん(22歳)が配列で定義されていたとして、年齢が50歳以下の人だけを別の配列に入れるというコードを以下のように書いてみたとします。

package main

import "fmt"

type Person struct {
    name string
    age  int
}

func main() {
    people := []Person{
        Person{"Aさん", 56},
        Person{"Bさん", 33},
        Person{"Cさん", 41},
        Person{"Dさん", 22},
    }
    personPtrs := []*Person{}

    for _, person := range people {
        fmt.Printf("人の名前: <%s>\n", person.name)
        if person.age <= 50 {
            personPtrs = append(personPtrs, &person)
        }
    }

    for _, personPtr := range personPtrs {
        fmt.Printf("ポインタの参照先の名前 <%s>\n", personPtr.name)
    }
}

このコードを実行して期待する結果は、ポインタの参照先の名前に年齢が50歳以下のBさん、Cさん、Dさんの3名が表示されることですが、実際にコードを実行してみるとDさんの名前だけが3回表示されます。

人の名前: <Aさん>
人の名前: <Bさん>
人の名前: <Cさん>
人の名前: <Dさん>

ポインタの参照先の名前 <Dさん>
ポインタの参照先の名前 <Dさん>
ポインタの参照先の名前 <Dさん>

何が問題か

このコードの挙動を把握するために、変数のポインタを一緒に表示するようにコードを直してみます。

package main

import "fmt"

type Person struct {
    name string
    age  int
}

func main() {
    people := []Person{
        Person{"Aさん", 56},
        Person{"Bさん", 33},
        Person{"Cさん", 41},
        Person{"Dさん", 22},
    }
    personPtrs := []*Person{}

    for _, person := range people {
        fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", 
            person.name, &person)
        if person.age < 50 {
            personPtrs = append(personPtrs, &person)
        }
    }

    for _, personPtr := range personPtrs {
        fmt.Printf("ポインタの参照先の名前 <%s>、ポインタ: <%p>\n", 
            personPtr.name, personPtr)
    }
}

実行してみると以下のように出力されました。

人の名前: <Aさん>、ポインタ: <0xc0000a8018>
人の名前: <Bさん>、ポインタ: <0xc0000a8018>
人の名前: <Cさん>、ポインタ: <0xc0000a8018>
人の名前: <Dさん>、ポインタ: <0xc0000a8018>

ポインタの参照先の名前 <Dさん>、ポインタ: <0xc0000a8018>
ポインタの参照先の名前 <Dさん>、ポインタ: <0xc0000a8018>
ポインタの参照先の名前 <Dさん>、ポインタ: <0xc0000a8018>

この出力内容から for _, person := range people { のループの度に変数person を新しく生成しメモリ領域を確保しているのではなく、以下のように1つの変数に代入し、内容を上書いているだけだということが分かります。

package main

import "fmt"

type Person struct {
    name string
    age  int
}

func main() {
    people := []Person{
        Person{"Aさん", 56},
        Person{"Bさん", 33},
        Person{"Cさん", 41},
        Person{"Dさん", 22},
    }

    // 変数(入れ物)は1個だけ
    var person Person

    // 1回目のループ
    person = people[0]
    fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", person.name, &person)

    // 2回目のループ
    person = people[1]
    fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", person.name, &person)

    // 3回目のループ
    person = people[2]
    fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", person.name, &person)

    // 4回目のループ
    person = people[3]
    fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n", person.name, &person)
}
人の名前: <Aさん>、ポインタ: <0xc0000a8018>
人の名前: <Bさん>、ポインタ: <0xc0000a8018>
人の名前: <Cさん>、ポインタ: <0xc0000a8018>
人の名前: <Dさん>、ポインタ: <0xc0000a8018>

最終的に変数 person (ポインタ 0xc0000a8018) には"Dさん"の情報しか入っていないので、最初のコードのpersonPtrs の各値である personのポインタ(0xc0000a8018) を参照しても "Dさん"の情報しか出力されなかったというわけです。

対処方法

期待する結果(Bさん、Cさん、Dさんの3名の名前が表示される)にするにはコードをどのように修正すれば良いでしょうか。

対処方法は2つあるかと思います。ループ変数( person )のポインタを使わないという点はどちらも共通しています。

①元の配列の要素のポインタを使う

package main

import "fmt"

type Person struct {
    name string
    age  int
}

func main() {
    people := []Person{
        Person{"Aさん", 56},
        Person{"Bさん", 33},
        Person{"Cさん", 41},
        Person{"Dさん", 22},
    }
    personPtrs := []*Person{}

    for i := 0; i < len(people); i++ {
        var person *Person
        person = &people[i] // 元の配列の要素のポインタを代入
        fmt.Printf("人の名前: <%s>、ポインタ: <%p>\n",
            person.name, person)
        if person.age <= 50 {
            personPtrs = append(personPtrs, person)
        }
    }

    for _, personPtr := range personPtrs {
        fmt.Printf("ポインタの参照先の名前 <%s>、ポインタ: <%p>\n",
            personPtr.name, personPtr)
    }
}

以下の実行結果を見ると personPtrs の各値が元の配列( people )の各要素のポインタと同じ(0xc000062198, 0xc0000621b0, 0xc0000621c8)になっていることが分かります。

人の名前: <Aさん>、ポインタ: <0xc000062180>
人の名前: <Bさん>、ポインタ: <0xc000062198>
人の名前: <Cさん>、ポインタ: <0xc0000621b0>
人の名前: <Dさん>、ポインタ: <0xc0000621c8>

ポインタの参照先の名前 <Bさん>、ポインタ: <0xc000062198>
ポインタの参照先の名前 <Cさん>、ポインタ: <0xc0000621b0>
ポインタの参照先の名前 <Dさん>、ポインタ: <0xc0000621c8>
メリット・デメリット

この方法のメリット・デメリットは以下の通りです。

  • メリット
    • 新しい変数のメモリ領域を確保しないので処理が速い
  • デメリット
    • 新しい変数のメモリ領域を確保しないのでpersonPtrs の参照先の値を書き換えると元の配列の値まで変わってしまう

②ループ毎に別の変数を定義し、そのポインタを使う

別の変数を定義することによりループ毎にメモリ領域を確保し、そのポインタを personPtrs に入れます。

package main

import "fmt"

type Person struct {
    name string
    age  int
}

func main() {
    people := []Person{
        Person{"Aさん", 56},
        Person{"Bさん", 33},
        Person{"Cさん", 41},
        Person{"Dさん", 22},
    }
    personPtrs := []*Person{}

    for i, person := range people {
        personTmp := person // ループ毎に別の変数を定義
        fmt.Printf("人の名前: <%s>、ポインタ: <%p>、元の配列の要素のポインタ: <%p>\n",
            personTmp.name, &personTmp, &people[i])
        if personTmp.age <= 50 {
            personPtrs = append(personPtrs, &personTmp)
        }
    }

    for _, personPtr := range personPtrs {
        fmt.Printf("ポインタの参照先の名前 <%s>、ポインタ: <%p>\n",
            personPtr.name, personPtr)
    }
}

以下の実行結果を見ると、ループ毎に変数のポインタが変っていることが分かります。

人の名前: <Aさん>、別変数のポインタ: <0xc000010030>、元の配列の要素のポインタ: <0xc000062180>
人の名前: <Bさん>、別変数のポインタ: <0xc000010048>、元の配列の要素のポインタ: <0xc000062198>
人の名前: <Cさん>、別変数のポインタ: <0xc000010060>、元の配列の要素のポインタ: <0xc0000621b0>
人の名前: <Dさん>、別変数のポインタ: <0xc000010078>、元の配列の要素のポインタ: <0xc0000621c8>

ポインタの参照先の名前 <Bさん>、ポインタ: <0xc000010048>
ポインタの参照先の名前 <Cさん>、ポインタ: <0xc000010060>
ポインタの参照先の名前 <Dさん>、ポインタ: <0xc000010078>
メリット・デメリット

この方法のメリット・デメリットは以下の通りです。

  • メリット
    • 新しい変数のメモリ領域を確保するので処理が遅い
  • デメリット
    • 新しい変数のメモリ領域を確保するのでpersonPtrsの参照先の値を書き換えても元の配列の値は変わらない

結論

ループの回数がさほど多くない場合には、実行速度はさほど気にしなくても良いので ②ループ毎に別の変数を定義し、そのポインタを使う という方法が良さそうです。

実行速度に対してセンシティブな用途やループ回数が多い場合は、 ①元の配列の要素のポインタを使う という方法が良さそうです。

参考資料

【Golang】Twilio VerifyでSMS送信、パスコード検証してみる

直近仕事でTwilio Verifyに関して調査した時に、SMS送信、パスコード検証のサンプルアプリを作ってみたので手順を記事にまとめてみます。

サンプルアプリはGo言語のプロジェクトで作っていて、ライブラリはtwilio-goを使いました。

以下の公式記事を参考にしました。

www.twilio.com

前提

  • Twilioアカウント作成済み
  • direnvインストール済み

Twilio Verifyとは

Twilio Verify は SMS、電話、プッシュ通知、TOTP を利用して二要素認証を簡単に実現する仕組みです。

本記事では、SMSでワンタイムパスコードを送信し、二段階認証を実現します。Programmable Messaging (SMS)を使用する場合は、テキストを特定の電話番号SMS送信するだけですが、Verifyを使うと認証コードの発行や発行したコードの検証まで行ってくれます。

Programmable Messaging (SMS)との違い

まず特徴的な違いとしては以下の通りです。(2022年8月時点)

  • 違いがないところでは、国際網・国内網のどちらでも送信可能
  • SMSを送信する電話番号として共用番号のみ選択可能
  • 国内網で送信する場合 Twilio Verifyの方が1通あたり$0.01 高い
    • Twilio Verifyの場合、1通送信$0.8+認証成功$0.5=$0.13
    • Programmable Messagingの場合、$0.12
  • 認証コードのカスタマイズ性が低い
  • Twilio Verifyの場合は、メッセージテンプレートを使うことになるので送信メッセージの自由度が低い(参考)
    • Publicテンプレート: 全ユーザー共通のテンプレート
    • Privateテンプレート: 顧客毎に設定するテンプレート。どこまで自由に設定できるのかは不明。

Verify Serviceの作成

Twilioのアカウントを作成後、Verify Serviceを作成します。

表示されるダイアログ上で、SMSだけを選択し、Createを押します。

Verify Serviceを作成するとService SIDが発行されるためそれをコピーして控えておきます。

Account SIDとAuth token

また、Verify APIを使うにはAccount SIDとAuth tokenも必要になるため、console上からこれらもコピーして控えておきます。

サンプルプロジェクト作成

$ mkdir verify-go
$ cd verify-go

.envrcを作成し、先ほど控えておいて環境変数を設定します。

$ direnv edit .
export TWILIO_ACCOUNT_SID=XXXXXXXXXXXXXXXXXXXXXXXXX
export TWILIO_AUTH_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXX
export VERIFY_SERVICE_SID=XXXXXXXXXXXXXXXXXXXXXXXXX

go.mod を作っておきます。

$ go mod init verify

twilio-goをインストール

$ go get github.com/twilio/twilio-go

ここからはGoのコードを書いていきます。

まずは環境変数を読み込み、Twilioのクライアントを生成します。

package main

import (
   "fmt"
   "os"

   "github.com/twilio/twilio-go"
   openapi "github.com/twilio/twilio-go/rest/verify/v2"
)

var TWILIO_ACCOUNT_SID string = os.Getenv("TWILIO_ACCOUNT_SID")
var TWILIO_AUTH_TOKEN string = os.Getenv("TWILIO_AUTH_TOKEN")
var VERIFY_SERVICE_SID string = os.Getenv("VERIFY_SERVICE_SID")
var client *twilio.RestClient = twilio.NewRestClientWithParams(twilio.ClientParams{
   Username: TWILIO_ACCOUNT_SID,
   Password: TWILIO_AUTH_TOKEN,
})

次にワンタイムパスコード(OTP)を送る機能と送ったOTPを検証する機能を追加していきます。

以下のコードではVerificationのエンドポイントを呼び出してOTPを送信します。

func sendOtp(to string) {
   params := &openapi.CreateVerificationParams{}
   params.SetTo(to)
   params.SetChannel("sms")

   resp, err := client.VerifyV2.CreateVerification(VERIFY_SERVICE_SID, params)

   if err != nil {
       fmt.Println(err.Error())
   } else {
       fmt.Printf("Sent verification '%s'\n", *resp.Sid)
   }
}

以下のコードではユーザー入力受け付け、受け取った文字列パスコードとしてVerification Checkのエンドポイントに送信し、OTPを検証します。

func checkOtp(to string) {
   var code string
   fmt.Println("Please check your phone and enter the code:")
   fmt.Scanln(&code)

   params := &openapi.CreateVerificationCheckParams{}
   params.SetTo(to)
   params.SetCode(code)

   resp, err := client.VerifyV2.CreateVerificationCheck(VERIFY_SERVICE_SID, params)

   if err != nil {
       fmt.Println(err.Error())
   } else if *resp.Status == "approved" {
       fmt.Println("Correct!")
   } else {
       fmt.Println("Incorrect!")
   }
}

最後にmain関数を実装します。

func main() {
   to := "【SMSを送信する電話番号 (例: +818011112222)】"

   sendOtp(to)
   checkOtp(to)
}

実行すると、main関数に書いた電話番号にSMSが送信され、認証コードが届きます。受信した認証コードを入力し、検証結果が表示されます。

$ go run .
Sent verification 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
Please check your phone and enter the code:
XXXXXX
Correct!

curl実行する

参考までにcurlで実行するコマンドも書いておきます。もしよろしければ動かない時のトラブルシューティングとしてお使いください!

$ curl -X POST https://verify.twilio.com/v2/Services/{Service SID}/Verifications \
--data-urlencode "To=+81xxxxxxxxx" \
--data-urlencode "Channel=sms" \
-u your_account_sid:your_auth_token
$ curl -X POST https://verify.twilio.com/v2/Services/{Service SID}/VerificationCheck \
--data-urlencode "To=+81xxxxxxxxx" \
--data-urlencode "Code=123456" \
-u your_account_sid:your_auth_token

感想

費用としてはProgrammable Messaging (SMS)よりもちょっと高いくらいでSMSを経由した2段階認証機能が簡単に作れるので、あまり工数に余裕がない状況で予算は十分にある、という場合には採用検討の余地がありそうと思いました。

参考記事

APDUプロトコルを通じてマイナンバーカードで電子署名する

以下の記事では、OpenSCというツールを使ってマイナンバーカードにアクセスしましたが、今回はOpenSCの内部で使われているAPDUプロトコルを使って電子署名をやってみたいと思います。

simple-minds-think-alike.moritamorie.com

まずは、ADPUプロトコルに関してまとめ、実際にAPDU命令をマイナンバーカードに送って結果をみていきたいと思います。

APDUプロトコルとは

APDU (Application Protocol Data Unit) プロトコルとは、接触ICカードが準拠しているISO/IEC 7816の中で定められている通信規格で、この規格に準じてICカードとカードリーダー間の通信が行われることによって様々なICカードの間の相互互換性を維持できています。

APDUの構成・構成ユニット・パターン

構成

APDUは、以下の2つから構成されています。

  • Command APDU (C-APDU) - カードリーダーからICカードに送る
  • Response APDU (R-APDU) - ICカードからカードリーダに送る

構成ユニット

それぞれの構成ユニットは以下のようになっています。

Command APDU(C-APDU)

Response APDU(R-APDU)

パターン

C-APDUとR-APDU、それぞれの構成ユニットの組み合わせによって4パターンに分類されます。

使用するAPDUコマンド

本記事では以下3つのAPDUコマンドを使用します。

コマンド CLS(命令クラス) INS(命令コード) 概要 パターン
SELECT FILE 00 A4 ファイルなどの論理チャネルを開く パターン3
VERIFY 00 20 PINなどの確認 パターン3
COMPUTE DIGITAL SIGNATURE 80 2A 電子署名の計算 パターン4

各コマンドのパラメータに関しては大体以下のサイトに纏まっていました。COMPUTE DIGITAL SIGNATUREのパラメータに関してはマイナンバーカード 独自の仕様のようです。

また、Response CommandのStatus Wordに関しては以下を参照しました。

APDUプロトコルで署名

前記事で使用した opensc-tool というツールを使うとAPDUコマンドを送れるので今回も使います。

紹介する検証手順では opensc-tool の実行を1回ずつ実行しているように記載していますが、実行すると途中でエラーが返ってきます。実際には以下のように && で繋げて実行しています。

$ opensc-tool -s 00:A4:04:0C:0A:D3:92:F0:00:26:01:00:00:00:01 && opensc-tool -s 00:A4:02:0C:02:00:1B && 〜〜〜

署名フロー

左側にフロー、右側にマイナンバーカードのデータ構造とした図を掲載しておきます。

公的個人認証APをSELECT FILEする

  • CLS(命令クラス): 00
  • INS(命令コード): A4
  • P1(引数1): 04
  • P2(引数2): 0C
  • Lc(データフィールド長): 0A (10進だと10。)
  • Data: DF名 (D3 92 F0 00 26 01 00 00 00 01)
$ opensc-tool -s 00:A4:04:0C:0A:D3:92:F0:00:26:01:00:00:00:01
Using reader with a card: SCR3310 Smart Card Reader
Sending: 00 A4 04 0C 0A D3 92 F0 00 26 01 00 00 00 01
Received (SW1=0x90, SW2=0x00)

②署名用PINをSELECT FILEする

  • CLS(命令クラス): 00
  • INS(命令コード): A4
  • P1(引数1): 02
  • P2(引数2): 0C
  • Lc(データフィールド長): 02
  • Data: ファイル識別子 (00 1B)
$ opensc-tool -s 00:A4:02:0C:02:00:1B
Using reader with a card: SCR3310 Smart Card Reader
Sending: 00 A4 02 0C 02 00 18
Received (SW1=0x90, SW2=0x00)

③署名用PINをVERIFYする

  • CLS(命令クラス): 00
  • INS(命令コード): 20
  • P1(引数1): 00
  • P2(引数2): 80
  • Lc(データフィールド長): 05
  • Data: パスワード(31 32 33 34 35)
    • パスワードは12345を仮定しています。
    • パスワードはアスキーコードで送ります。
$ opensc-tool -s 00:20:00:80:05:31:32:33:34:35
Sending: 00 A4 02 0C 02 00 0A
Received (SW1=0x90, SW2=0x00)

④署名用秘密鍵をSELECT FILEする

  • CLS(命令クラス): 00
  • INS(命令コード): A4
  • P1(引数1): 02
  • P2(引数2): 0C
  • Lc(データフィールド長): 02
  • Data: ファイル識別子(00 1A)
$ opensc-tool -s 00:A4:02:0C:02:00:1A
Received (SW1=0x90, SW2=0x00)

電子署名する

まず、前記事で作成した署名対象のテキストデータファイルの中身を16進数で確認します。

$ xxd message.txt
00000000: 6865 6c6c 6f0a                           hello.

データは 6865 6c6c 6f0a であることが分かりました。

この値をDataとして、COMPUTE DIGITAL SIGNATUREを実行します。このコマンドによって署名用秘密鍵マイナンバーカード の外に送らずに、署名を行うことができます。

  • CLS(命令クラス): 80
  • INS(命令コード): 2A
  • P1(引数1): 00
  • P2(引数2): 80
  • Lc(データフィールド長): 06
  • Data: 署名対象(68 65 6C 6C 6F 0A)
  • Le(期待するレスポンスデータの長さ): 00
$ opensc-tool -s 80:2A:00:80:06:68:65:6C:6C:6F:0A:00
Received (SW1=0x90, SW2=0x00):
5C 9F CA FD 42 46 E3 A0 7B 34 19 B2 5F 5D 19 DB \...BF..{4.._]..
BD F7 13 34 D0 1A 9D 96 29 15 02 4B 61 6A DA D4 ...4....)..Kaj..
CF 77 5C D0 A7 6D 80 F7 4E 8C FC BF A8 66 74 1F .w\..m..N....ft.
9A 2C ED 49 21 72 BD 74 86 16 73 D7 02 EC 4E C8 .,.I!r.t..s...N.
E5 EB FE D1 EC 7D 76 18 4D D3 21 E6 2C 90 D2 70 .....}v.M.!.,..p
1F 2A 6B 79 DF 8E D3 0A 99 DB 46 61 7D 1C 3B AC .*ky......Fa}.;.
C4 65 B2 BA 1B 76 CA FF 02 1A 7A D5 B1 47 30 4B .e...v....z..G0K
2B 20 0A 3C CE 06 9D D3 AB BD C4 89 A3 2F 7F 09 + .<........./..
86 D9 C7 BE 8A 14 29 F7 6E A8 11 E9 1D 7D AA 01 ......).n....}..
E5 EE D7 F8 5A DC F8 61 ED ED 95 50 59 13 97 0C ....Z..a...PY...
A6 71 22 F8 1E 3C 80 A2 A4 B0 BD 8A F3 F0 DF CF .q"..<..........
BB 65 A7 CD B9 84 2E 07 D8 6E 89 26 31 0C 46 91 .e.......n.&1.F.
AD 1B 07 6A 1C 7F 2B 9B 42 CC AC CF F4 9A 86 23 ...j..+.B......#
9D 96 9D AE 87 D3 82 9A 7B CC C3 C5 91 4B A4 EF ........{....K..
1A CA CC 14 BA 7B A7 CA B4 1E A7 EC 26 5B 49 0C .....{......&[I.
EC 5F 85 92 7B 71 FE D0 6F 6A 40 75 D9 9E 9E 4D ._..{q..oj@u...M

署名付きデータ(5C〜4Dまで)を任意のバイナリエディタで保存し、text.sined のように名前をつけて保存します。

署名検証してみる

前記事で作成した公開鍵(sign.pub)で検証すると同様に内容を確認できます。

$ openssl rsautl -verify -pubin -inkey sign.pub -in text.signed
hello

参考資料

OpenSCを使ってマイナンバーカードで電子署名・検証してみた

マイナンバーカードを使って電子署名できるアプリを作ってみたくて、マイナンバーの仕様に関して調べてまとめてみました。また、実際にOpenSCというツールを使って電子署名・検証し、マイナンバーカードの内部構造の理解を深めました。

前提

  • macOS: 12.4
  • OpenSSL: LibreSSL 2.8.3
  • OpenSC: 0.22.0-rc1-74

マイナンバーカードのインタフェース

まずは、マイナンバーカードの外部からアクセスするためのインタフェースとその規格を確認し、接続に使った機器に関して説明します。

規格

マイナンバーカードは接触・非接触両方のインタフェースを持ち、それぞれ以下の規格に準拠しています。

  • 接触インターフェース
  • 接触インターフェース(NFC)
    • ISO/IEC 14443 TypeB

接触インターフェースであるNFCの代表的の規格は以下の3つであり、マイナンバーカードは2番目のType Bを採用しています。

分類 規格 特徴 使用例
Type-A ISO/IEC 14443 Type A (MIFARE) 比較的安価で低機能 taspo
Type-B ISO/IEC 14443 Type B 高セキュリティ、高処理速度 パスポート、住民基本台帳カード、免許証
Type-F NFC-F ‐ JIS X 6319-4 (FeliCa) 高セキュリティ、高処理速度 SuicaEdy、iD

使用機器・環境

今回の検証では、接触インターフェース(ISO 7816 準拠) ICカードに対応した以下のカードリーダーを使用し、電子署名・検証してみました。

PCからマイナンバーカードの裏面にあるICチップにアクセスできれば接触/非接触どちらのインタフェースでもよかったのですが、2,000円台という安価で壊れても買い直しやすそうだったのでこちらのICカードリーダーで試してみました。

Amazon.co.jp: SCM ICカードリーダー/ライター B-CAS・住基カード対応 SCR3310/v2.0 : パソコン・周辺機器

マイナンバーカードの内部構造

アプリ

総務省のマイナンナーカードの説明によるとマイナンバーカードのICチップには、券面AP・公的個人認証AP(JPKI-AP)・券面入力補助AP・住基APという4つのアプリケーションがあり、以下の概要図のようになっています。

https://www.soumu.go.jp/main_content/000379924.jpg

本記事の検証では、OpenSCというツールを使って電子署名をしたいので公的個人認証AP(JPKI-AP)の部分を使っていきます。

データ構造

また、ICチップ内部のデータ構造は公開されていませんが、ICチップの内部にアクセスして確認すると以下のようなデータ構造になっていることがわかります。

本記事では署名用のデータを使います。

電子署名・検証

いよいよOpenSCとマイナンバーカードにある署名証明書を使って電子署名を行なっていきます。

OpenSCとは

OpenSCは、クロスプラットフォームで動作するスマートカード用のライブラリ・ユーティリティ郡です。主に暗号操作をサポートするカードに対して、認証、メール暗号化、デジタル署名などをするアプリケーションを実装しやすくしてくれるものです。

OpenSCのインストール

以下から環境にあったインストーラーをダウンロードして、OpenSCをインストールします。 https://github.com/OpenSC/OpenSC/wiki/#download

インストール後、pkcs15-crypt, pkcs15-tool の両コマンドが使えるようになっていることを確認します。

$ pkcs15-crypt --version
OpenSC-0.22.0-rc1-74-gc902e199, rev: c902e199, commit-time: 2021-08-10 11:09:03 +0200
$ pkcs15-tool  --version
OpenSC-0.22.0-rc1-74-gc902e199, rev: c902e199, commit-time: 2021-08-10 11:09:03 +0200

これらのツールの用途はそれぞれ以下の通りです。

  • pksc15-creypt
    • ICカードに保存された鍵を用いて、電子署名の計算やデータの復号化などの暗号処理を実行するためのツールです。
  • pkcs15-tool
    • PIN・証明書・公開鍵など、カード内部のデータにアクセスするためのツールです。

電子署名

以下のコマンドで署名対象のファイル( plain.txt )を作成します。(RSA署名できるのは254バイトまでなので、実際には署名対象自体ではなくダイジェストに対して署名します)

$ echo "hello" > plain.txt

次に以下のコマンドで、PKCS#15オブジェクトを全て表示し、署名用の秘密鍵のIDを確認します。

$ pkcs15-tool --dump
〜〜〜 他のPKCS#15オブジェクト 〜〜〜
Private RSA Key [Digital Signature Key]
    Object Flags   : [0x01], private
    Usage          : [0x204], sign, nonRepudiation
    Access Flags   : [0x1D], sensitive, alwaysSensitive, neverExtract, local
    Algo_refs      : 0
    ModLength      : 2048
    Key ref        : 2 (0x02)
    Native         : yes
    Auth ID        : 02
    ID             : 02
    MD:guid        : xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
〜〜〜 他のPKCS#15オブジェクト 〜〜〜

署名用の秘密鍵のIDが 2 だということがわかりました。また、コマンドの結果から、マイナンバーカード内には以下の10個のオブジェクトがあることが分かります。

  • PIN [User Authentication PIN] - 認証用PIN
  • PIN [Digital Signature PIN] - 署名用PIN
  • Private RSA Key [User Authentication Key] - 認証用秘密鍵
  • Private RSA Key [Digital Signature Key] - 署名用秘密鍵
  • Public RSA Key [User Authentication Public Key] - 認証用公開鍵
  • Public RSA Key [Digital Signature Public Key] - 署名用公開鍵
  • X.509 Certificate [User Authentication Certificate] - 認証証明書
  • X.509 Certificate [Digital Signature Certificate] - 署名証明書
  • X.509 Certificate [User Authentication Certificate CA] - 認証用CA証明書
  • X.509 Certificate [Digital Signature Certificate CA] - 署名用CA証明書

pkcs15-crypt-k オプションに署名用の秘密鍵のID 2 を指定して、作成したテキストファイルを署名します。また、署名用パスワード(英数字6-16文字)の入力が求められるので入力します。(※英字のアルファベットは大文字を入力する必要があります。)

$ pkcs15-crypt -s -k 2 --pkcs1 -R -i plain.txt -o text.signed

pkcs15-crypt はデフォルトだと入力データが正しい長さでパディングされたものと見なして扱うので --pkcs1 オプションを指定して入力データをパディングしてあげています。(参考)

パスワードを間違えてしまった場合、以下のコマンドでPINがブロックされるまでの残り回数を確認できます。PINがブロックされると役所に行ってリセットを依頼する必要があるのでご注意を。

$ pkcs15-tool --list-pins

署名済みのファイルtext.signedができました。

署名検証

次に署名証明書の公開鍵を使って、署名済みのファイルの署名検証を行います。

以下のコマンドで、署名用のPINと証明書のIDを確認します。それぞれ 2 であることが分かります。

$ pkcs15-tool --dump
PIN [Digital Signature PIN]
    Object Flags   : [0x12], modifiable
    ID             : 02
    Flags          : [0x12], local, initialized
    Length         : min_len:6, max_len:16, stored_len:0
    Pad char       : 0x00
    Reference      : 2 (0x02)
    Type           : ascii-numeric
    Tries left     : 5

X.509 Certificate [Digital Signature Certificate]
    Object Flags   : [0x01], private
    Authority      : no
    Path           : 0001
    ID             : 02
    Encoded serial : 00 00 XXXXXXXXX

pkcs15-tool--read-certificate オプションに署名証明書のID 2--auth-id オプションにPINの auth-id02 指定して、署名証明書を取得します。再度、署名用パスワードの入力が求められるので入力します。

$ pkcs15-tool --read-certificate 2 --verify-pin --auth-id 02
Using reader with a card: SCR3310 Smart Card Reader
Please enter PIN [Digital Signature PIN]:
-----BEGIN CERTIFICATE-----
xxxxx
-----END CERTIFICATE-----

出力された-----BEGIN CERTIFICATE-----から-----END CERTIFICATE-----の箇所をファイル名 sign.crt として保存します。

以下のコマンドで署名証明書(sign.crt)から公開鍵の部分を取り出して、ファイル sign.pubに保存します。

$ openssl x509 -in sign.crt -noout -pubkey > sign.pub

先ほど作成した署名済みファイル(text.signed)を公開鍵(sign.pub)で検証すると内容を確認できます。

$ openssl rsautl -verify -pubin -inkey sign.pub -in text.signed
hello

参考資料

関連記事

simple-minds-think-alike.moritamorie.com

【Golang】GoLandにstaticcheckを設定する

Go言語のlinter staticcheck をGoLandでも有効にする方法を調べてみました。 staticcheck.io

プラグインとして提供されていると楽そうですが、2022年6月現在 staticcheckプラグインはないので、IntelliJ IDEA製品の機能であるFile Watchersを使って有効にしています。

前提

  • staticcheck 2022.1.2 (v0.3.2)
  • GoLand 2022.1.3

導入

staticcheckインストール

$ go install honnef.co/go/tools/cmd/staticcheck@latest

インストールしたらバイナリのパスを確認します。

$ which staticcheck
/Users/t.morita/.anyenv/envs/goenv/shims/staticcheck

GoLandの設定

カスタムテンプレートを選択します。

プログラムの欄に、先ほど確認したstaticcheckのバイナリのパスを指定します。

最初ファイルタイプ「Goファイル」を見つけられなかったので、あれ!?どうやって設定するんだろうと思ったのですが、デフォルトで設定されているファイルタイプ「不明」の上の方にありました。

すべてのプロジェクトで staticcheck を有効にするために、レベルをグローバルに変更します。

確認

設定が終わると、ファイルタイプに該当するファイルを編集すると自動的に staticcheck を実行し、結果を出力してくれます。

以下のようなコードに対して staticcheck を実行して結果を確認してみます。

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    f, err := os.Open("hoge.txt")
    fmt.Printf("%s\n", f.Name())

    if err != nil {
        log.Fatal(err)
    }
    f, err = os.Open("hoge.txt")
    fmt.Printf("%s\n", f.Name())
}

初回実行時、実行の出力タブに以下のように表示されていました。

「プロジェクトを信頼して実行」を選択すると、staticcheck が実行され、以下のようにエラーになった内容が表示されます。

エラーの内容を読むと err が使われていないことがわかるのでエラーチェックが漏れていることに気がつけそうです。

また、エラーのファイル:行数をクリックすると、エディタの該当行にフォーカスが当たってくれるのも便利です。

参考資料

【Golang】Cookieを認証・暗号化するライブラリ gorilla/cookie を試してみた

最近、仕事でCookie使ったGo言語コードを見かけた際に gorilla/securecookie が使われていたので、このライブラリに関してを調べてみました。その時に調べた内容を書きたいと思います。

ginecho といったGo言語でよく使われているWebフレームワークの内部でも gorilla/securecookie を使っているので、gorilla/securecookie の役割/仕組みを理解することで、goのWebアプリケーションのCookie管理の仕組みを把握できると思います。

前提

概要

公式のREADMEを読むと以下のように書いてありました。

securecookie encodes and decodes authenticated and optionally encrypted cookie values.

securecookie は認証され、オプションで暗号化されたCookieの値をエンコードおよびデコードします。

Secure cookies can't be forged, because their values are validated using HMAC. When encrypted, the content is also inaccessible to malicious eyes. It is still recommended that sensitive data not be stored in cookies, and that HTTPS be used to prevent cookie replay attacks.

セキュアCookieは、その値がHMACを使用して認証されるため、偽造することができません。暗号化された場合、コンテンツは悪意のある目からアクセスすることもできません。

securecookie は、標準パッケージの net/httpのcookie にはない認証と暗号化の仕組みを提供してくれるようです。

標準パッケージの挙動を確認

標準パッケージを使ってCookieを設定するだけのシンプルなコードを試してみました。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/set-cookie", func(w http.ResponseWriter, 
        r *http.Request) {

        cookie := &http.Cookie{
            Name:  "title",
            Value: "SPY x FAMILY",
        }
        http.SetCookie(w, cookie)

        fmt.Fprintf(w, "Cookieをセットしました")
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Webブラウザでパス: /set-cookie、ポート: 8080にアクセスすると以下のようになり、想定したCookieが設定されていることが確認できます。

Cookieはこのようにブラウザツールで簡単に参照、改変できるためセキュリティが低い状態になっています。

securecookieの認証の挙動を確認

securecookieを使ってコードを書き直す

gorilla/securecookie を使ってコードを書き直すと、セキュリティを向上できることを確認します。

securecookie.Newの第1引数にはHMACのハッシュキーを指定します。第2引数には暗号化のためのブロックキーを指定できますが、今回は nil を指定し、暗号化しないようにしてみます。

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/securecookie"
)

func main() {
    hashKey := []byte("hash-key")
    s := securecookie.New(hashKey, nil)

    http.HandleFunc("/set-cookie", func(w http.ResponseWriter,
        r *http.Request) {

        values := map[string]string{
            "title": "SPY x FAMILY",
        }
        encoded, err := s.Encode("cookie-name", values)
        if err == nil {
            cookie := &http.Cookie{
                Name:     "cookie-name",
                Value:    encoded,
            }
            http.SetCookie(w, cookie)

            fmt.Fprintf(w, "Cookieをセットしました")
        }
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

同じようにWebブラウザでアクセスするとCookieの値が変わり、そのままでは読めなくなっていることが分かります。

以下のハンドラーを追加し、Cookie内のハッシュ値を検証し、通れば値をデシリアライズしてCookieの値を表示します。

http.HandleFunc("/show-cookie", func(w http.ResponseWriter,
    r *http.Request) {

    if cookie, err := r.Cookie("cookie-name"); err == nil {
        value := make(map[string]string)

        err = s.Decode("cookie-name", cookie.Value, &value)
        if err == nil {
            fmt.Fprintf(w, "値は%qです。", value["title"])
        } else {
            fmt.Fprintf(w, err.Error())
        }
    }
})

WebブラウザでアクセスするとCookieから取得した値が表示されることを確認できます。

認証の仕組み

上記のコードと同様に、Cookieの名前を"cookie-name"、Cookieのデータ(Name: "title", Value: "SPY x FAMILY")とした場合の認証のフローです。

このフローではオプションである暗号化は考慮していないので機密性が上がるわけではありません。自身のサーバーがハッシュキーを使って保存したものか認証できるようになるだけです。

Cookieの値作成・保存

以下の図のフローでCookieの値を作成、保存します。

ハッシュ値を算出

基本文字列を"cookie-name|【タイムスタンプ】|Cookieデータ"とし、ハッシュキーと合わせてHMACハッシュ値(mac)を算出します。

Cookieに設定する値を作り、保存

次に "【タイムスタンプ】|Cookieデータ|ハッシュ値(mac)"をbase64エンコードし、Cookieとして保存します。

Cookieの取得・検証

以下の図のフローでCookieの値を取得・検証します。

ハッシュ値を取り出し

Cookieから取り出した"【タイムスタンプ】|Cookieデータ|ハッシュ値(mac)"をbase64デコードし、ハッシュ値(mac)を取り出します。

Cookieの検証

次に、"cookie-name"を追加して"cookie-name|タイムスタンプ|Cookieデータ"というデータを作り、ハッシュキーと合わせてHMACでハッシュ値(mac)を算出し、取り出したハッシュ値と値が一致するかで検証できます。

Cookieの内容を改変し、わざと検証失敗させてみる

開発者ツールを使ってCookieの値を改変してみます。

再度Webブラウザ/show-cookie にアクセスすると、検証エラーになるかことを確認できます。

securecookieの暗号化の挙動を確認

securecookieを使ってコードを書き直し、暗号化してみる

securecookie.Newの第2引数には暗号化のためのブロックキーを指定できます。

hashKey := []byte("hash-key")
blockKey := []byte("blocoooooock-key")
s := securecookie.New(hashKey, blockKey)

ぱっと見では、認証だけの場合と何も変わっていないように見えます。

暗号化/複合化の仕組み

オプションである暗号化を有効にすることで機密性が上がります。

以下の図のように認証処理の前に、Cookieのデータに対してAES(CTRモード)で暗号化を行うことで、Webブラウザには平文が保存されないようになります。

複合時は、Cookieの署名検証が問題なければ暗号化されたCookieデータをブロックキーで複合化します。

参考記事

【Golang】slack-goを使ってSlackのリクエスト署名を検証する<解説篇>

以下の記事ではGo言語のライブラリ slack-go を使ってSlackのリクエスト署名検証を行うサンプルアプリを作成しましたが、今回はその検証処理の詳細を確認していきたいと思います。

simple-minds-think-alike.moritamorie.com

なお、Slackのリクエスト署名に関しては以下の公式記事を参考にしました。

前提

  • go: v1.17
  • slack-go: v0.10.3

リクエスト署名の概要

公式の記事によると

リクエスト署名は、Slack がアプリにデータを送信する API メソッドでサポートされています。これには、スラッシュコマンド、Events API リクエスト、インタラクティブ・メッセージ、アクション、メッセージボタン、メッセージメニュー、レガシー Outgoing Webhook が含まれます。

ということで、基本的にSlackからアプリに送信されるHTTPリクエストはリクエスト署名検証できるようになっているようです。

リクエスト署名検証の仕方(理論)

また、リクエスト署名検証には以下の3つが必要なようです。

  • X-Slack-Signature HTTP ヘッダー
  • アプリ固有のシークレットキー( Signing Secret )
  • 基本文字列
    • バージョン番号 (現在は常に v0)
    • X-Slack-Request-Timestamp HTTP ヘッダーの値
    • リクエスト自体のメッセージ本文

X-Slack-Signature HTTP ヘッダー

公式記事に以下のように記載されていました。

このヘッダーには、HMAC-SHA256 キー付きハッシュが設定されています。このハッシュは、アプリ固有のシークレットをキーとして使って計算されます。

具体的にはv0=c11b919c9ea488395441d317....のような値になります。基本文字列とアプリ固有のシークレットでSHA256 ハッシュ計算されたものです。アプリ側でも同じ計算を行い、ハッシュが同じ値になることを確認することで署名検証できます。

アプリ固有のシークレットキー( Signing Secret )

xoxb-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxのような値がアプリ毎に発行され、Slack APIOAuth & Permissionsから取得できます。

基本文字列

バージョン番号 (現在は常に v0)、X-Slack-Request-Timestamp HTTP ヘッダーの値、リクエスト自体のメッセージ本文の順で、コロン (:) で区切って連結し、基本文字列を作成します。

例えば、基本文字列は次のようになります v0:1538352000:token=abc123def456xyz....

リクエスト署名検証の仕方(実装)

上記の検証を行なっている アプリとslack-go の実装を確認していきます。

アプリ側の実装

作成したサンプルアプリmiddleware.goから署名検証を行なっている箇所を抜粋し、処理内容をコメントで追記しています。

以下の①〜③の番号は次の slack-go の実装と関連する箇所を明示しています。

// ①リクエスト署名検証に必要な情報(基本文字列のメッセージ本文以外)を設定し、
//    シークレット検証機を生成
sv, err := slack.NewSecretsVerifier(r.Header,
    os.Getenv("SLACK_SIGNING_SECRET"))
if err != nil {
    w.WriteHeader(http.StatusBadRequest)
    return
}

// ②シークレット検証機に不足情報(基本情報の文字列のメッセージ本文)を追加
if _, err := sv.Write(body); err != nil {
    w.WriteHeader(http.StatusInternalServerError)
    return
}

// ③SHA256 ハッシュ計算し、ハッシュが同じ値になることを確認
if err := sv.Ensure(); err != nil {
    w.WriteHeader(http.StatusUnauthorized)
    return
}

slack-goの実装

署名検証は、security.goの実装に含まれており、そこからの抜粋になります。

①リクエスト署名検証に必要な情報(基本文字列のメッセージ本文以外)を設定し、シークレット検証機を生成

シークレット検証機生成とタイムスタンプのチェックをしています。

func NewSecretsVerifier(header http.Header,
    secret string) (sv SecretsVerifier, err error) {
    var (
        timestamp int64
    )

    stimestamp := header.Get(hTimestamp)

    // シークレット検証機生成
    if sv, err = unsafeSignatureVerifier(header, secret); err != nil {
        return SecretsVerifier{}, err
    }

    // タイムスタンプを10進数(64ビット長)に変換
    if timestamp, err = strconv.ParseInt(stimestamp, 10, 64); err != nil {
        return SecretsVerifier{}, err
    }

    // リクエストヘッダーのタイムスタンプ(X-Slack-Request-Timestamp HTTP ヘッダーの値)と現在時刻を比較し、
    // 5分より長ければエラーにする
    diff := absDuration(time.Since(time.Unix(timestamp, 0)))
    if diff > 5*time.Minute {
        return SecretsVerifier{}, ErrExpiredTimestamp
    }

    return sv, err
}

NewSecretsVerifier の中から呼ばれている unsafeSignatureVerifier の実態は以下の通りです。

const (
    hSignature = "X-Slack-Signature"
    hTimestamp = "X-Slack-Request-Timestamp"
)

func unsafeSignatureVerifier(header http.Header, 
    secret string) (_ SecretsVerifier, err error) {
    var (
        bsignature []byte
    )

    // HTTPヘッダーから署名とリクエストタイムスタンプを取得
    signature := header.Get(hSignature)
    stimestamp := header.Get(hTimestamp)

    if signature == "" || stimestamp == "" {
        return SecretsVerifier{}, ErrMissingHeaders
    }

    // 署名をデコード
    if bsignature, err = hex.DecodeString(
        strings.TrimPrefix(signature, "v0=")); err != nil {
        return SecretsVerifier{}, err
    }

    // シークレットをキーとしてHMACハッシュを生成する
    hash := hmac.New(sha256.New, []byte(secret))

    // ハッシュに"v0:【リクエストタイムスタンプ】"を書き込む
    if _, err = hash.Write(
        []byte(fmt.Sprintf("v0:%s:", stimestamp))); err != nil {
        return SecretsVerifier{}, err
    }

    return SecretsVerifier{
        signature: bsignature,
        hmac:      hash,
    }, nil
}

http.Headerとsecretを引数にとり、検証機を生成して返します。なお、ここで使用している HMAC-SHA-256というアルゴリズムに関しては、別の記事で概要を記載していますので、もしよろしければ参考にしてみてください。

simple-minds-think-alike.moritamorie.com

②シークレット検証機に不足情報(基本情報の文字列のメッセージ本文)を追加
func (v *SecretsVerifier) Write(body []byte) (n int, err error) {
    return v.hmac.Write(body)
}
③SHA256 ハッシュ計算し、ハッシュが同じ値になることを確認
func (v SecretsVerifier) Ensure() error {
    // ハッシュ値を算出
    computed := v.hmac.Sum(nil)

    // 署名と算出したハッシュ値が同じになることを確認
    if hmac.Equal(computed, v.signature) {
        return nil
    }

    if v.d != nil && v.d.Debug() {
        v.d.Debugln(fmt.Sprintf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed)))
    }
    return fmt.Errorf("Computed unexpected signature of: %s", hex.EncodeToString(computed))
}

Best Plactice for security

他のSlack APIに関するセキュリティの対応は Best Plactice for security という公式の記事にまとめられています。

api.slack.com

2022年6月時点での目次を転記しておきます。

関連記事

simple-minds-think-alike.moritamorie.com

simple-minds-think-alike.moritamorie.com