LoginSignup
27
6

More than 1 year has passed since last update.

株式会社ゆめみ所属メンバの SSH 鍵強度調査

Last updated at Posted at 2021-12-08

この記事は YUMEMI Advent Calendar 2021 9 日目の記事です。

──── SSH鍵強度は意識の高さ

ということで、弊社の GitHub Organization に所属しているメンバを対象に SSH 鍵の強度を調べてみました。

元ネタ

調査結果

調査方法

  1. GitHub API で Organization に所属するメンバ一覧を取得
    • 非 Public な メンバについても取得する場合は、 read:org な scope の Personal access token が必要
  2. メンバのIDを元に、登録されている公開鍵を取得
    • https://github.com/${メンバーのID}.keys or GitHub API で取得可能
    • 1 人で複数個登録可能なため、登録されている公開鍵すべてを対象とする
  3. 公開鍵のアルゴリズム、鍵長を確認

といった手順で調査を実施しました。

結果

鍵種別 個数 割合 [%]
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 の並列呼び出しについては、

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 ディレクティブを持つ別名ファイルを用意したほうがよさそう

といった地味な嵌まりどころはあったのですが、なんとかアドベントカレンダーには間に合いました。

27
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
6