Simple minds think alike

より多くの可能性を

【GitHub Actions】configuration variablesで機密性の無い変数を設定する

本記事は以下のGitHub公式のGitHub Docsを参照して記載しました。なお、configuration variablesの機能は現在ベータ版のため、仕様が変更される恐れがあります。最新の情報は公式をご参照ください。

docs.github.com

Configuration variables in workflowsとは

以下のGitHubからのAnnouncementの引用によると、非機密データをプレーンテキストとして格納し、GitHub Actionsワークフロー内から参照できる変数のようです。また、組織、環境、リポジトリレベルで変数を定義できるようです。

Configuration variables allows you to store your non sensitive data as plain text variables that can be reused across your workflows in your repository or organization. You can define variables at Organization, Repository or Environment level based on your requirement.

なお、環境(environments)というのは、本番、ステージング、開発などの一般的なデプロイメント対象を表すために使用される機能で、GitHub Actionsワークフローから環境を指定してデプロイが可能になります。この機能は、2023年1月現在公開リポジトリまたは有償プランで利用可能です。

Configuration variablesの特徴

以下の引用によると組織、環境、リポジトリの順番でより狭い設定が優先されるようです。

You can create configuration variables for use across multiple workflows, and can define them at either the organization, repository, or environment level.

For example, you can use configuration variables to set default values for parameters passed to build tools at an organization level, but then allow repository owners to override these parameters on a case-by-case basis.

イメージとしては以下のような状態です。レポジトリの変数userには"repouser"が設定されていて、3つある環境のうちdevelopmentにだけ変数の設定がない状態を示しています。

適用優先度のイメージ図

もし、組織にしか設定されていなければそれが、全リポジトリ、全環境に反映されるという具合です。(ここは未検証です。)

この特徴が、単一リポジトリにしか設定できない環境変数とは明確に異なる点かと思います。

上記の特徴から、機密性の高い情報ではなく環境変数として設定するほどでもないが、大体設定同じでレポジトリ全部に入れて管理するには手間が掛かる。組織全体に変数設定して、少ない個別のレポジトリにだけ別の値を設定したい、という場合に向いていそうです。

Configuration Variablesを参照するには

GitHub Actions のワークフローファイルで、以下のようにvar contextを使うと参照できます。

steps:
- name: Use variables
  run: |
    echo "variable : ${{ vars.REPOSITORY_VAR }}"

Configuration variablesの制限

組織、リポジトリ変数、環境変数の保存可能な個数はそれぞれ、組織変数は1,000個まで、リポジトリ変数は100個まで、環境変数は100個まで保存可能です。

また、変数名にはデフォルトの環境変数の名前は使用できません。

試してみた

上記のイメージと同じように設定してみました。

環境の設定

以下のように3つの環境(development, staging, production)を作ります。 環境一覧

一例として、prduction環境に以下のように変数USER(値はprduser)を設定しています。

変数の追加

リポジトリの設定

リポジトリには変数USER(値はrepouser)を設定しています。 リポジトリの変数設定画面

GitHub Actionsワークフロー作成

以下のworkflow.ymlを追加し、ワークフローを手動実行します。

on:
  workflow_dispatch:
env:
  # Setting an environment variable with the value of a configuration variable
  env_var: ${{ vars.USER }}

jobs:
  display-variables-development:
    runs-on: ubuntu-latest
    environment:
      name: development
    steps:
    - name: Use variables
      run: |
        echo "configuration variable : ${{ vars.USER }}"
        echo "variable from shell environment : $env_var"

  display-variables-staging:
    runs-on: ubuntu-latest
    environment:
      name: staging
    steps:
    - name: Use variables
      run: |
        echo "configuration variable : ${{ vars.USER }}"
        echo "variable from shell environment : $env_var"

  display-variables-production:
    runs-on: ubuntu-latest
    environment:
      name: production
    steps:
    - name: Use variables
      run: |
        echo "configuration variable : ${{ vars.USER }}"
        echo "variable from shell environment : $env_var"

実行結果

development環境だけ、リポジトリで設定した変数が表示されていることを確認できます。

development

環境developmentのワークフロージョブ実行結果

staging

環境stagingのワークフロージョブ実行結果

production

環境productionのワークフロージョブ実行結果

検証リポジトリ

github.com

参考資料

【Rails】範囲オブジェクト(Range)を使ったActiveRecordのwhere比較、範囲検索のコードの書き方

ActiveRecordのwhereを使って、色々な書き方で比較演算(>, >=, <, <=)・範囲抽出(Beetween、◯以上□未満)を実装できますが、最近では範囲オブジェクト (Range (例: 10..30)) を使ってほとんど実装することができるようになっています。

ただ、範囲オブジェクトを使った実装は

  • Ruby2.7から導入されたbeginless range
    • 範囲オブジェクトの開始の値を省略できる書き方(例: ..30)
  • Ruby 2.6から導入されたendless range
    • 範囲オブジェクトの終わりの値を省略できる書き方(例: 10..)

を使うと綺麗に書けるので、可能であれば対応したRuby, Railsバージョンにすると良いと思います。

範囲オブジェクトを使った書き方のメリット

範囲オブジェクトを使った書き方

個人的には範囲オブジェクトを使った書き方が

  • コード量が少なく可読性が高い(コードを読んで挙動が分かりやすい)
    • 保守面でのメリットが高い
  • エラーになりにくい
    • 開発面でのメリットが高い

ので、一番良いと思っています。具体的には、

# Rails(ActiveRecord)
User.where(age: ...30)

という書き方で

-- SQL
SELECT "users".* FROM "users" WHERE "users"."age" < 30"

というSQLクエリが発行されます。

以下のような書き方で以前から範囲オブジェクトを使って実現できましたが、開始の値に意味の無い値を書く必要があったので、使っていませんでした。

# Rails(ActiveRecord)
User.where(age: - Float::INFINITY...30)

範囲オブジェクトを使わない書き方

範囲オブジェクトが使うようになるまでは、よく以下のような書き方をしていました。

# Rails(ActiveRecord)
User.where('age < ?', 30)

無駄がない書き方ではあるのですが、テーブルを結合して両方のテーブルにある同じ名前のカラムを抽出条件にする場合、エラーになるというデメリットがありました。

User.joins(:family_members).where('age < ? ', 30)
=> SELECT "users".* FROM "users" INNER JOIN "family_members" ON 
    "family_members"."user_id" = "users"."id" 
   WHERE (age < 30)

PG::AmbiguousColumn: ERROR:  column reference "age" is ambiguous

範囲オブジェクトを使った書き方では、この問題が解決されています。

User.joins(:family_members).where(age: ...30)
=> SELECT "users".* FROM "users" INNER JOIN "family_members" ON 
    "family_members"."user_id" = "users"."id" 
   WHERE "family_members"."age" < 30
User.joins(:family_members).where(family_members: { age: ...30 })
=> SELECT "users".* FROM "users" INNER JOIN 
    "family_members" ON "family_members"."user_id" = "users"."id" 
   WHERE "users"."age" < 30

範囲オブジェクトを使った書き方の注意点

ケース別の書き方

それでは、比較機能(>, >=, <, <=)・範囲抽出(Beetween)それぞれのケース別の書き方をご紹介します。以下の例は抽出条件が、整数のみですが、時間や日付型のカラムでも大丈夫です。

◯◯より小さい (less than <)

# Rails(ActiveRecord)
User.where(age: ...30)
-- SQL
SELECT "users".* FROM "users" WHERE "users"."age" < 30"

◯◯以下 (less than or equal <=)

# Rails(ActiveRecord)
User.where(age: ..30)
-- SQL
SELECT "users".* FROM "users" WHERE "users"."age" <= 30"

◯◯以上 (greater than or equal >=)

# Rails(ActiveRecord)
User.where(age: 11..)
-- SQL
SELECT "users".* FROM "users" WHERE "users"."age" >= 11"

◯◯よりも大きい (greater than >)

範囲オブジェクトを使った書き方では、今のところ以下のSQLクエリは発行できません。

SELECT "users".* FROM "users" WHERE "users"."age" > 10"

なので、範囲オブジェクトを使う場合は◯◯以上 (greater than or equal >=)というSQLクエリに置き換える必要があります。ですが、あまり困ることもなさそうです。

範囲抽出(Beetween)

今まで通り

# Rails(ActiveRecord)
User.where(age: 10..30)
-- SQL
SELECT "users".* FROM "users" WHERE "users"."age" BETWEEN 10 AND 30

と書くことができます。

範囲抽出 (◯以上□未満)

# Rails(ActiveRecord)
User.where(age: 10...30)
-- SQL
SELECT "users".* FROM "users" 
  WHERE "users"."age" >= 10 AND "users"."age" < 30

Go 1.20 crypto/ecdh導入の理由

本記事は以下の記事を参照、一部引用して記載しています。出典の記載がない引用に関しては以下のいずれかからの引用になります。

words.filippo.io

github.com

ECDHとは

楕円曲線ディフィー・ヘルマン鍵共有(だえんきょくせんディフィー・ヘルマンかぎきょうゆう、英: Elliptic curve Diffie–Hellman key exchange, ECDH)は、事前の秘密の共有無しに、盗聴の可能性のある通信路を使って、暗号鍵の共有を可能にする、公開鍵暗号方式の暗号プロトコルである。両者で共有した秘密の値はそのまま、あるいは何かしらの変換をかけて、共通鍵暗号の鍵として用いることができる。 ディフィー・ヘルマン鍵共有を楕円曲線を使うように変更した、楕円曲線暗号の一つである。 出典:Wikipedia(楕円曲線ディフィー・ヘルマン鍵共有)

crypto/ecdh導入の目的

The goal of the package is to replace the major use case for the now-deprecated crypto/elliptic API, which has a hardcoded dependency on the variable-time, large, and complex math/big package.

引用によると、このパッケージの目的は可変時間で大規模かつ複雑なmath/bigパッケージにハードコードされた依存性を持つ crypto/elliptic APIの主なユースケースを置き換えることを目的としているとのことです。crypto/elliptic APIはGo言語標準パッケージの楕円曲線パッケージです。

現在2023年1月時点で crypto/elliptic パッケージはdeprecatedではないため、おそらくnow-deprecatedでは誤記であり、正しくはnon-deprecatedと記載したかったのだと思われます。

GitHub Issueの方で言及されている crypto/elliptic APIのメソッドを見ると、確かに *big.Int に依存してます。

type CurveParams struct {
    P       *big.Int // the order of the underlying field
    N       *big.Int // the order of the base point
    B       *big.Int // the constant of the curve equation
    Gx, Gy  *big.Int // (x,y) of the base point
    BitSize int      // the size of the underlying field
    Name    string   // the canonical name of the curve
}

https://pkg.go.dev/crypto/elliptic#CurveParams

math/big に依存しているとなぜ問題なのか

math/big is a general-purpose big integer library, it's not constant time, and it's full of complex code that while unnecessary for cryptography has repeatedly led to security vulnerabilities in crypto packages.

math/big は汎用のbig integerライブラリで、定数時間ではなく暗号パッケージのセキュリティ脆弱性を繰り返し引き起こすような複雑なコードがたくさんあるからという理由のようです。

この問題に関しては、以前以下のissueでも定数時間サポートに関して提起されており、また今後コードコメントにも注意書きが追加されるようです。 https://github.com/golang/go/issues/20654 https://go-review.googlesource.com/c/go/+/455135/1/src/math/big/int.go

ECDHはどんなところに使われるのか(影響範囲はどの程度なのか)

ECDH is used in TLS, SSH, JOSE, OpenPGP, PIV, and HPKE, as well as a component of other various ECIES schemes.

この定数時間サポートの問題をどのように解決するのか

ざっくりまとめると以下のようにして問題が解決されることが分かります。

  • Go 1.17からGo 1.19の間に標準ライブラリの楕円曲線実装(crypt/eliptic APIの実装のことだと思われます)のほとんどの重要なコードは crypto/internal/nistecrypto/internal/edwards25519 に移動した。
  • math/big の代わりとなる定数時間をサポートするbigmod を実装した。
  • crypto/rsacrypto/ecdsa を移植し math/big依存をなくすことで、bit.Int が暗号パッケージから到達できなくなる。

crypto/ellipticはいつ非推奨になるのか

The full crypto/elliptic deprecation will actually have to wait until Go 1.22, because of the deprecation policy:

Go 1.22

現在使っているcrypto/elipticをより高度に使用する場合はどのようにすれば良いか

Any more advanced uses of crypto/elliptic can switch to filippo.io/nistec which is an exported version of crypto/internal/nistec, or filippo.io/edwards25519 which is an exported version of crypto/internal/edwards25519.

crypto/internal/nistec のエクスポートバージョンである filippo.io/nistec、または crypto/internal/edwards25519 のエクスポートバージョンである filippo.io/edwards25519 へ移行することが可能。

参考・引用資料

【Golang】AWS LambdaをSAMでデバッグする方法

Go言語で実装したAWS Lambda関数をVSCodeデバッグ実行できるようになるまでの手順を書いてみようと思います。

Lambda関数をローカル環境でデバッグ実行できるようにすることで、AWS上でのデプロイやテストをしなくても、開発を円滑に進めることができます。

また、AWS Lambdaをローカル環境で実行するにはSAM(Serverless Application Model)を使用します。実行時にデバッグポートを割り当てらることができ、VSCodeからデバッガをアタッチしてデバッグ実行を実現します。

SAMとは

SAM(Serverless Application Model)は、AWSが提供するサーバレスアプリケーションを開発、デプロイ、管理するためのフレームワークです。SAMを使用することで、AWS LambdaやAmazon API Gatewayなどのサーバレスサービスを使用したアプリケーションを簡単に構築したり、管理したりすることができます。

また、SAMはAWS CloudFormationのサブセットであり、CloudFormationのテンプレートを使用してサーバレスアプリケーションを定義します。

前提

  • 検証環境: macOS Monterey 12.6.2
  • Homebrewインストール済み
  • AWS CLIインストール、設定済み

※ 本記事で紹介している情報は2022年12月時点のものであり、閲覧時には古くなっている場合があります。最新の情報に関してはAWSの公式ドキュメントにしてください。

AWS SAM CLIインストール

ローカル環境でLambda関数を実行できるようにするために、公式ドキュメントを参考にAWS SAM CLIをインストールします。

$ brew tap aws/tap
$ brew install aws-sam-cli

正常にSAM CLIがインストールされていることを確認します。

$ sam --version
SAM CLI, version 1.66.0

実行するGoプロジェクトを作成する

まずディレクトリを作り、その中にデバッグ実行するLamba関数である main.go、SAMテンプレートファイル、go.modを作成します。

$ mkdir lambda-debug-sample
$ cd lambda-debug-sample
$ touch main.go
$ touch template.yml
$ go mod init lambda-debug-sample

Lambda関数(Go言語)の作成

main.goAWS公式のサンプルコードをそのまま書いてみます。
MyEvent構造体の値のNameを出力するだけのシンプルなコードです。

package main

import (
        "fmt"
        "context"
        "github.com/aws/aws-lambda-go/lambda"
)

type MyEvent struct {
        Name string `json:"name"`
}

func HandleRequest(ctx context.Context, name MyEvent) (string, error) {
        return fmt.Sprintf("Hello %s!", name.Name ), nil
}

func main() {
        lambda.Start(HandleRequest)
}

テンプレートファイルの作成

作成した main.go をLambda関数として動かすためのシンプルなテンプレートファイルを記載してみます。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  lambda-debug-sample

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 5

Resources:
  HelloGopher: # リソース名を指定
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ./
      Handler: hello-world
      Runtime: go1.x
      Architectures:
        - x86_64

Resourcesの部分で、HelloGopher というリソース名を指定しています。他の箇所から参照する時に、このリソース名を使用できます。
Typeに AWS::Serverless::Function を指定し、CloudFormationがLambdaを生成できるようにします。

また、Propertiesでは以下の4つを指定しています。

※その他のオプションに関しては公式ドキュメントをご確認ください。

試しにSAM CLIを使って実行してみる

今の状態で作ったLambda関数を実行してみます。

$ sam local invoke
Invoking hello-world (go1.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-go1.x:rapid-1.66.0-x86_64.

START RequestId: b35b26b2-bfa1-4973-9c55-188775457e2d Version: $LATEST
fork/exec /var/task/hello-world: no such file or directory: PathError
null
END RequestId: b35b26b2-bfa1-4973-9c55-188775457e2d

すると、template.yml ファイルのHandlerで指定したバイナリファイル hello-world が存在しないので、PathError が発生することを確認できました。

ビルドする

ビルドすると.aws-samにビルド結果が出力されます。

$ go mod tidy
$ sam build

Running GoModulesBuilder:Build

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

再度SAM CLIで実行してみる

実行すると Hello ! と出力され、正常に実行できることを確認できます。

$ sam local invoke
Invoking hello-world (go1.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-go1.x:rapid-1.66.0-x86_64.

START RequestId: 16b172ed-6023-4ee8-999b-89d907784099 Version: $LATEST
END RequestId: 16b172ed-6023-4ee8-999b-89d907784099
REPORT RequestId: 16b172ed-6023-4ee8-999b-89d907784099  Init Duration: 0.20 ms    Duration: 191.46 msBilled Duration: 192 ms   Memory Size: 128 MB    Max Memory Used: 128 MB
"Hello !"

イベントを渡して実行してみる

ローカル環境でLambda関数が動いたので、次は色々なやり方でLambda関数を起動してみようと思います。

まずはイベントを作成します。イベントは本来S3やSQSで作成・更新のイベントが発生した際に、Lambda関数が呼ばれる際に渡されるものですが、今回は仮のシンプルなデータを作成します。

$ touch event.json
{
    "name": "HogeHoge"
}

作成したイベント event.json を指定して関数を実行すると、Hello HogeHoge! と表示され、event.jsonからの値を確認できます。

$  sam local invoke -e event.json
Invoking hello-world (go1.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-go1.x:rapid-1.66.0-x86_64.

START RequestId: a54238e1-41d4-4c15-b6aa-c3b36f20c426 Version: $LATEST
END RequestId: a54238e1-41d4-4c15-b6aa-c3b36f20c426
REPORT RequestId: a54238e1-41d4-4c15-b6aa-c3b36f20c426  Init Duration: 0.24 ms    Duration: 201.85 msBilled Duration: 202 ms   Memory Size: 128 MB    Max Memory Used: 128 MB
"Hello HogeHoge!"

リソース名を指定して実行してみる

リソース名を指定して起動することもできます。実行内容は同じです。

$ sam local invoke HelloGopher -e event.json

AWS Lambda をエミュレートするローカルエンドポイントを起動して実行してみる

次にsam local start-lambda で、ローカルエンドポイントを起動して実行してみます。

まずは、ローカルエンドポイントの起動からです。デフォルトURLは http://127.0.0.1:3001です。

$ sam local start-lambda
Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint.
2022-12-31 17:28:26  * Running on http://127.0.0.1:3001/ (Press CTRL+C to quit)

AWS CLIを使って起動したエンドポイント叩きます。

$ aws lambda invoke --function-name HelloGopher --endpoint "http://127.0.0.1:3001" --payload '{"name": "HogeHoge"}' --cli-binary-format raw-in-base64-out response.json
{
    "StatusCode": 200
}

response.json にLambdaの実行結果が出力されます。

"Hello HogeHoge!"

VSCodeデバッグ実行できるようにする

delveのインストール

Goのデバッグツールである delve をインストールします。

$ GOARCH=amd64 GOOS=linux go install github.com/go-delve/delve/cmd/dlv@latest

Lambda関数及び delve が実行されるのはLambdaコンテナ内(Amazon Linux)なので、GOOSに linux を指定し、インストールします。(参考github issueコメント).

デバッガーパスを指定してLambda関数を実行

delve$GOPATH/bin/linux_amd64 されるので、このパスをデバッガーパスに指定、デバッグポートに 8099 を指定して、Lambda関数を実行します。

API server (8099 ポートリッスン)で起動することが確認できます。

$  sam local invoke HelloGopher -e event.json  -d 8099 --debugger-path=$GOPATH/bin/linux_amd64 --debug-args="-delveAPI=2"
Invoking hello-world (go1.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-go1.x:rapid-1.66.0-x86_64.

START RequestId: 909cfcd8-e0cc-448c-8b55-e8afe022b5b8 Version: $LATEST
API server listening at: [::]:8099
2022-12-31T08:47:59Z warning layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
2022-12-31T08:47:59Z info layer=debugger launching process with args: [/var/task/hello-world]

Visual Studio Codeからアタッチする

最後にVSCodeからアタッチするための情報を設定します。

プロジェクト( main.go と同じところ)に以下の .vscode/launch.json を作成します。

{
    "configurations": [

        {
            "name": "SAMでデバッグ",
            "type": "go",
            "request": "attach",
            "mode": "remote",
            "port": 8099,
            "host": "localhost",
            "env": {},
            "args": []
        }
    ]
}

main.goVSCodeで開き、ブレイクポイントを設定したら、実行とデバッグを押します。 SAMでデバッグする際のVSCodeデバッグ画面

ブレイクポイントに止まることを確認できます。 SAMでデバッグ中のVSCodeデバッグ画面

1ステップでデバッグ実行できるようにする

以下の2ステップでデバッグ実行を行いましたが、launch.jsonの書き方を変えると1ステップでできるようになり、デバッグポートを指定する必要がなくなります。

  • デバッガーパスを指定してLambda関数を実行
  • Visual Studio Codeからアタッチする

先ほどの launch.json を以下のように変更し、実行とデバッグから2つ目の方を指定すると同様にブレイクポイントに止まることが確認できます。

{
    "configurations": [
        {
            "name": "SAMでデバッグ",
            "type": "go",
            "request": "attach",
            "mode": "remote",
            "port": 8099,
            "host": "localhost",
            "env": {},
            "args": []
        },
        // ↓↓↓ここの部分を追記↓↓↓
        {
            "name": "SAMでデバッグ2",
            "type": "aws-sam",
            "request": "direct-invoke",
            "invokeTarget": {
                "target": "template",
                "templatePath": "${workspaceFolder}/template.yml",
                "logicalId": "HelloGopher"
            },
            "lambda": {
                "payload": {
                    "path": "event.json"
                    // "json": { "name": "HogeHoge" },
                },
                "environmentVariables": {}
            }
        },
        // ↑↑↑ここの部分を追記↑↑↑
    ]
}

サンプルプロジェクト

github.com

参考記事

【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を作成します。

Twilioのダッシュボード画面でVerify Serviceを作成

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

TwilioダッシュボードのVerify Service新規作成画面

Verify Serviceを作成するとService SIDが発行されるためそれをコピーして控えておきます。 TwilioダッシュボードのServicesからService SIDを確認

Account SIDとAuth token

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

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

$ 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!

"スマートフォンで送られてきたSMSの認証コードを確認"

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)

Command APDU(C-APDU)の概要図

Response APDU(R-APDU)

Response APDU(R-APDU)の概要図

パターン

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

C-APDUとR-APDUそれぞれの構成ユニットの組み合わせ

使用する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 && 〜〜〜

署名フロー

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

APDUプロトコルを使った署名フロー

公的個人認証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

参考資料