この記事は YUMEMI Advent Calendar 2021 9 日目の記事です。
──── SSH鍵強度は意識の高さ
ということで、弊社の GitHub Organization に所属しているメンバを対象に SSH 鍵の強度を調べてみました。
元ネタ
調査結果
調査方法
- GitHub API で Organization に所属するメンバ一覧を取得
- 非 Public な メンバについても取得する場合は、
read:org
な scope の Personal access token が必要
- メンバのIDを元に、登録されている公開鍵を取得
-
https://github.com/${メンバーのID}.keys
or GitHub API で取得可能 - 1 人で複数個登録可能なため、登録されている公開鍵すべてを対象とする
- 公開鍵のアルゴリズム、鍵長を確認
といった手順で調査を実施しました。
結果
鍵種別 | 個数 | 割合 [%] |
---|---|---|
DSA 1024bit | 1 | 0.3 |
ECDSA 521bit | 1 | 0.3 |
ECDSA-SK 256bit | 2 | 0.6 |
ED25519 256bit | 30 | 8.7 |
RSA 1024bit | 4 | 1.2 |
RSA 2048bit | 119 | 34.6 |
RSA 3072bit | 77 | 22.4 |
RSA 4096bit | 109 | 31.7 |
Unknown | 1 | 0.3 |
合計:344個
※ Unknown については、ssh-keygen
に渡した際にエラーとなったもの
感想
少数とはいえ、 DSA 1024bit と RSA 1024bit 鍵が存在してしまったのが、うーんといった感じです。
逆に ED25519 や ECDSA 521bit 、 ECDSA-SK 256bit といったあたりの利用は光るところです。
特に ECDSA-SK 鍵については、今回の調査で初めて知ったのですが、 Yubikey 等の Security Key デバイスを利用したGitHub利用を目的としたもののようで、意識の高さを感じます。
全体的には RSA 鍵が多いのですが、その中でも 4096bit 指定が多めなので、 ssh-keygen
等で鍵ペアを作成する際にデフォルト設定で作るのではなく、きちんと強度を意識して作られている雰囲気はありました。
調査用の CLI ツール
調査手順自体は上述の通りなのですが、手動で実施するのもアレだったので、今回は Swift を利用して CLI ツールを作成、調査を実施しました。
以下、その CLI ツールの作成に関して簡単に記述していこうかと思います。
コード全体は以下のリポジトリに上げています。
GitHub API
必要となってくるのは、以下 2 本の REST API です。
両方とも最大 100 件までしか取得できず、それ以上の全件取得には複数回の API 呼び出しを伴うページング処理が必要となってきます。
また、 SSH 鍵取得のための API はユーザ毎に呼び出す必要があるため、並列処理できないと処理時間的に辛くなる部分となってきます。
Swift Concurrency
上述の API 呼び出しでの問題点を解消するため、 Swift Concurrency (async/await) を利用した実装を行なっています。
ページング処理については、
func fetchUserNames(organization: String) async throws -> [String] {
var userNames: [String] = []
var loadMore = true
var page = 1
while loadMore {
let request = makeMembersRequest(organization: organization, page: page, perPage: 100)
guard let (data, _) = try? await URLSession.shared.data(for: request),
let json = try? JSONSerialization.jsonObject(with: data, options: []),
let members = json as? [[String: Any]] else {
throw ToolError.apiCall
}
userNames += members.compactMap { $0["login"] as? String }
loadMore = members.count == 100
page += 1
}
return userNames
}
のように、 while
+ await
でページングしながらの API の直列呼び出しを記述。
API の並列呼び出しについては、
let userNames = try await client.fetchUserNames(organization: organization)
let userPemKeys: [UserPemKey] =
try await withThrowingTaskGroup(of: (userName: String, keys: [String]).self) { group in
for userName in userNames {
group.addTask {
try await (userName, client.fetchUserKeys(userName: userName))
}
}
return try await group.reduce(into: [UserPemKey]()) { acc, cur in
acc += cur.keys.map { UserPemKey(userName: cur.userName, pemKeyString: $0) }
}
}
のように、TaskGroup
を利用しての並列呼び出しの待ち合わせを実現しています。(エラー周りにナイーブな実装です)
公開鍵種別
Swift の Security Framework 利用で、公開鍵種別の判断もできそうだったのですが、 PEM (テキスト) 形式のフォーマットだと扱いが難しそうだったため、 ssh-keygen
コマンドを呼び出す形式を採用しました。
シェル形式で記述すると、以下のような処理を Swift 側から呼び出しています。
$ echo '${公開鍵}' | ssh-keygen -lf - | sed -E 's/^([0-9]+).+\((.+)\)$/\2 \1bit/'
- 正規表現での抽出処理も任せるのか
- この部分も Swift Concurrency で並列化できそう
等の問題は残っているのですが、とりあえずはこの処理を公開鍵リストにそのまま適用することで、欲しいフォーマットの公開鍵種別情報を取得しています。
まとめ
SSH 鍵強度の調査というお題で Swift Concurrency の素振りといった感じだったのですが、まあまあ書き心地はよかったです。
- macOS ターゲットで動作させる場合に、 macOS Monterey (12.0) 以上が必要
- top-level await ができないので、エントリポイントに関して
main.swift
ではなく、@main
ディレクティブを持つ別名ファイルを用意したほうがよさそう
といった地味な嵌まりどころはあったのですが、なんとかアドベントカレンダーには間に合いました。