aws-sdk-goを通してlocal-kmsを使う

# この記事を書いた経緯
aws-sdk-goを用いた案件ソース中にkmsを用いた暗号・復号化の実装があり
kmsという概念すらわからなかった私が、ローカル環境での検証に四苦八苦した時の覚書を認めたものです


# コマンドだけ知りたい人へ
## local-kmsコンテナを立ち上げる
**そのままdev環境・本番環境のKMSに作られている鍵を使おうとしても、IAMの認証をかけられてしまうので
権限のないローカル環境では暗号化・復号操作を行うことができません。**

ローカル環境においてkmsを用いた処理をデバッグしたい場合、[local-kms](https://hub.docker.com/r/nsmithuk/local-kms)というdocker imageを用いて実現することが可能です。
他コンテナとポートが重複する場合は、環境変数``PORT``を用いることで競合を回避できます

```bash
$ docker run -p 8080:8081 -e PORT=8081 nsmithuk/local-kms
```

## aws configureの作成
local-kmsを使用する際も通常のインスタンスを用いる場合と同様にクレデンシャル情報が参照されますが
適当なダミーデータで問題ありません
すでに何かしらのconfigureがローカルに作成されていればスルーで問題ないと思います

```bash

$ aws configure
AWS Access Key ID [None]: dummy
AWS Secret Access Key [None]: dummy
Default region name [None]: ap-northeast-1
Default output format [None]: 
```

## create-key コマンドによりCMKを作成する
暗号・復号化に必要な、いわゆるCMK(Customer Master Key)と呼ばれるものを作成します。
この時出力されるAccountID・KeyIDなどは適当なダミー値になります

```bash

$ aws kms create-key --endpoint-url http://localhost:8080
{
    "KeyMetadata": {
        "AWSAccountId": "111122223333",
        "KeyId": "23aeebbd-cb83-43a3-8870-a6f9ee5a4ace",
        "Arn": "arn:aws:kms:eu-west-2:111122223333:key/23aeebbd-cb83-43a3-8870-a6f9ee5a4ace",
        "CreationDate": 1590853469,
        "Enabled": true,
        "KeyUsage": "ENCRYPT_DECRYPT",
        "KeyState": "Enabled",
        "Origin": "AWS_KMS",
        "KeyManager": "CUSTOMER",
        "CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
        "EncryptionAlgorithms": [
            "SYMMETRIC_DEFAULT"
        ]
    }
}
``` 

ここで出力された``KeyID``を用いて、kms encryptを実行します

## aws-sdk-goからlocal-kmsを介してkms encryptとdecryptを使う
aws-sdk-goの[こちら](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/kms-example-encrypt-data.html)のドキュメントに記載されているサンプルソースを拝借し、若干加筆しました。
``WithEndPoint()``へlocal-kmsのコンテナのアドレスを持たせ、``WithDisableSSL(true)``を使ってhttpへのアクセスを前提とさせたものを
aws.NewConfig()へメソッドチェーンさせることで、local-kmsを用いた暗号・復号化を可能とします

```go:localkms.go
package main

import (
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/kms"

	"fmt"
	"os"
)

func main() {
	sess, err := session.NewSession()
	if err != nil {
		fmt.Println("aws newsession error: ", err)
		os.Exit(1)
	}

	svc := kms.New(sess, aws.NewConfig().WithRegion("ap-northeast-1").WithEndpoint("localhost:8080").WithDisableSSL(true))

	keyId := "arn:aws:kms:eu-west-2:111122223333:key/23aeebbd-cb83-43a3-8870-a6f9ee5a4ace"
	text := "1234567890"

	//kms encrypt
	encResult, err := svc.Encrypt(&kms.EncryptInput{
			KeyId: aws.String(keyId),
			Plaintext: []byte(text),
	})

	if err != nil {
			fmt.Println("got error encrypting data: ", err)
			os.Exit(1)
	}

	//kms decrypt
	decResult, err := svc.Decrypt(&kms.DecryptInput{
		CiphertextBlob: encResult.CiphertextBlob,
	})

	if err != nil {
		fmt.Println("got error decrypting data: ", err)
		os.Exit(1)
	}

	fmt.Println("decoded Plaintext:")
	fmt.Println(string(decResult.Plaintext)) 
}
```

ソースのビルド

```
$ go build localkms.go 
```

ソースの実行

```
$ go run localkms.go 
decoded Plaintext: 1234567890
```

## Dockerfileでgoのソースをビルドしたい場合
このような階層の簡素なプロジェクトがあることを想定します

```bash
tree -L 3 localkms/
localkms/
├── Dockerfile
└── src
    ├── go.mod
    ├── go.sum
    ├── localkms
    ├── localkms.go
    └── vendor
        ├── github.com
        └── modules.txt
```

### DockerFile
マルチステージビルドの書き方が望ましいですが、とりあえず簡素に作ってみます

```dockerfile:Dockerfile
FROM golang:1.13.0
 
ADD ./src /go/src/local-kms
WORKDIR /go/src/local-kms
RUN cd /go/src/local-kms

ENV AWS_ACCESS_KEY_ID=dummy
ENV AWS_SECRET_ACCESS_KEY=dummy

RUN go build -mod=vendor localkms.go

ENTRYPOINT ["./localkms"]
```

### go.mod
ファイルを作成後、go mod vendorを実行し、srcディレクトリの下にvendorディレクトリを作成します

```bash:go.mod
module src/localkms

go 1.13

require github.com/aws/aws-sdk-go v1.25.14-0.20200528180948-645efefb5bce
```

### ソースの修正
コンテナの中からは``localhost``でlocal-kmsを参照できなくなるので
WithEndPointの記述を下記のように変更します(コンテナ名を記載することでそのコンテナのIPアドレスにリクエストが飛ぶようになります)

```diff:19行目
- svc := kms.New(sess, aws.NewConfig().WithRegion("ap-northeast-1").WithEndpoint("localhost:8080").WithDisableSSL(true))
+ svc := kms.New(sess, aws.NewConfig().WithRegion("ap-northeast-1").WithEndpoint("local-kms:8081").WithDisableSSL(true))
```

### ソースのビルド

```bash
$ docker build ./ -t go-container
```

### ソースの実行
local-kmsコンテナと連携するため``--link``オプションを利用します

```
$ docker run --link local-kms:local-kms go-container
decoded Plaintext: 1234567890
```

---

# ここから参考文献など

## aws cliのインストール
aws kms コマンドの実行にはaws cliのインストールが必要となります
[AWS CLI バージョン 1 のインストール](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv1.html)


## kmsとは？
データの暗号化に利用されるCMK(Customer Master Key)の作成と管理を容易にするマネージド型サービスです。

- [AWS Key Management Service の概念](https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/overview.html)
- [AWS KMS Management 開発者ガイド](https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/kms-dg.pdf)

### kms encryot
CMKのKeyIDを用いて平文を暗号化します
[encryptコマンドリファレンス](https://docs.aws.amazon.com/cli/latest/reference/kms/encrypt.html)

暗号化されたテキストは、``EncryptionAlgorithm``でのエンクリプションを行った結果ががさらに
base64エンコードされて出力されます。

```bash:local-kmsを使ったencryptの一例
$ aws kms encrypt --key-id arn:aws:kms:eu-west-2:111122223333:key/23aeebbd-cb83-43a3-8870-a6f9ee5a4ace --plaintext "hoge" --endpoint-url http://localhost:8080
{
    "CiphertextBlob": "S2Fybjphd3M6a21zOmV1LXdlc3QtMjoxMTExMjIyMjMzMzM6a2V5LzIzYWVlYmJkLWNiODMtNDNhMy04ODcwLWE2ZjllZTVhNGFjZQAAAADmlRx1OQCCTk5LQoIkNseoCofCRhLXo3iPpQ2lwx8E5A==",
    "KeyId": "arn:aws:kms:eu-west-2:111122223333:key/23aeebbd-cb83-43a3-8870-a6f9ee5a4ace",
    "EncryptionAlgorithm": "SYMMETRIC_DEFAULT"
}
```

### kms decrypt
CMKにより暗号化されたCiphertextを復号します。
encryptコマンドと異なり、この時keyIDを指定する必要はありません
(ciphertextblobに含まれているメタデータからこの情報を取り込むため、明示的に指定する必要がありません)

また、この時の復号結果の出力である``Plaintext``はbase64エンコードされて出てくるため
本来の平文を得るためにはbase64デコードする必要があります

```bash:local-kmsを使ったdecryptの一例
$ aws kms decrypt --ciphertext-blob fileb://<(echo 'S2Fybjphd3M6a21zOmV1LXdlc3QtMjoxMTExMjIyMjMzMzM6a2V5LzIzYWVlYmJkLWNiODMtNDNhMy04ODcwLWE2ZjllZTVhNGFjZQAAAADmlRx1OQCCTk5LQoIkNseoCofCRhLXo3iPpQ2lwx8E5A==' | base64 --decode) --endpoint-url http://localhost:8080 | jq .Plaintext --raw-output | base64 --decode
hoge
```

[decryptコマンドリファレンス](https://docs.aws.amazon.com/cli/latest/reference/kms/decrypt.html)