OpenSSHのプロトコル拡張「UpdateHostKeys」の仕組みと実装

Backlog課Gitチームの@vvatanabeです。

先日、BacklogのGitリポジトリへSSHでアクセスする機能を提供するサーバーが、ECDSAとEd25519のホスト鍵をサポートしました。

GitリポジトリへのSSHアクセスに関連するセキュリティアップデートのお知らせ

その際、OpenSSHのSSHプロトコル拡張の一つである「UpdateHostKeys」と呼ばれる機能を、Goで書かれたSSHサーバーに実装したので、その経験をもとに機能の仕組みと実装について解説します。

はじめに

SSHにおける「なりすまし」を防止する仕組み

UpdateHostKeysを説明する前に、SSHにおける「なりすまし」を防止するための重要な要素として、「ホスト鍵」と「known hosts」という仕組みについて説明します。

識別子としてのホスト鍵

ホスト鍵とは、SSHでコンピュータを認証するために使用する公開鍵と秘密鍵のペアです。通常、RSA、DSA、ECDSA、Ed25519といったアルゴリズムを使用します。SSHサーバーは秘密鍵を使って自分自身を証明して、SSHクライアントは公開鍵を利用してサーバーを識別します。

ホスト鍵を検証するknown hosts

SSHクライアントはホスト鍵を識別するために、「known hosts」という仕組みを使います。

SSHクライアントはSSHサーバーへの接続する際に、SSHサーバーのホスト鍵の公開鍵を受け取ります。その公開鍵をローカルマシン上の known_hosts ファイルと照合します。 known_hosts ファイルにはこれまでに接続したことがあるSSHサーバーの公開鍵を保持しており、SSHサーバーがこの公開鍵に対応する秘密鍵をもっているかどうか、デジタル署名を検証することで確認します。

対象の公開鍵が known_hosts ファイルに存在しなければ、対象のSSHサーバーに対して初回の接続と見なして次のようなメッセージを表示します。

RSA key fingerprint is SHA256:vUgu+kx0TNpk/3Za77SIWd/+3nNsamN/C/+ncbV/20g.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? 

SSHクライアントは、フィンガープリントと呼ばれる接続先のSSHサーバーが持つ公開鍵のハッシュ値を表示して、本当に接続していいのか確認します。このフィンガープリントを表示して対象のSSHサーバーの識別子として間違いないかシステム管理者に確認することで、システム管理者は接続先のサーバーが本物なのか判断できます。

フィンガープリントに問題なければ、プロンプトに yes と入力することで次のようなメッセージを表示して処理を継続します。

Warning: Permanently added 'nulab.git.backlog.jp' (RSA) to the list of known hosts.

SSHサーバーに正常に接続できたら、SSHクライアントはローカルマシンの known_hosts ファイルに接続したSSHサーバーの公開鍵を次のように記録します。

nulab.git.backlog.jp ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAxZMoHkzkq4S0cimvumMSDmPjs2Q5E+MiZvwy1uiW9ci4/lLz2A5FflmKpsVZq7b1TpURgAWOdIObxv29FqRdCYR7vazff4F2HWhd4byTSYctgyivWB+oYxYctn8EITjM6qRU3OXmAEJvrEAySWTZdsQndkh9wGkbokfeKRsGc5KkteOiKfsJG4LdhmP3kWTCpxydfQ9GfazqbQknLBaqmC9obbDpR1ze4hL90O2PUozA6+Iyl73zpnLZvEtnO9qe5zS+MlutyPm01ndCyxfN42fLaYPRtWoCQf1WVI7bCwilL+CBLHdsJdjKuyyBSWeU5pabHP7BlSW6tCGakgHGKQ==

次回同じSSHサーバーに接続する際は、 known_hosts ファイルに記録された公開鍵で照合できるため、信頼できるSSHサーバーと見なして最初のメッセージは表示されません。

ホスト鍵を更新する際の留意事項

SSHサーバーが持つホスト鍵をECDSAやEd25519といった新しい暗号タイプへ更新する場合、留意しなければならないことがあります。単純にSSHサーバーのホスト鍵(公開鍵と秘密鍵のペア)を変更してしまうと、SSHクライアントが照会する known_hosts ファイルに記録している公開鍵と一致しないため、SSHクライアント側で次のような中間者攻撃(Man In The Middle Attack)を警告するメッセージを表示して、接続を中断してしまいます。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.

これでは、ユーザーは自分自身の判断で known_hosts ファイルを編集しなければならず、何よりユーザーの不安を煽ります。いずれにせよ事前にユーザーへホスト鍵を変更するといったアナウンスが必要になります。

この問題をスマートに解決するのが本題の「UpdateHostKeys」オプションです。

UpdateHostKeysとは

本題のUpdateHostKeysは、どのような機能を提供するのか、そしてどのような仕組みなのか、順を追って解説します。

ホスト鍵をグレイスフルに追加・削除する

UpdateHostKeysはOpenSSHのオプションの一つです。このオプションを有効にすると、SSH接続時にSSHサーバーが持つ複数のホスト鍵の公開鍵(以下、公開鍵)をすべてSSHクライアントへ通知します。この通知を受け取ったSSHクライアントは、提供された公開鍵のうちどの鍵が known_hosts ファイルに存在するかを確認します。これにより、SSHクライアントはSSHサーバーの複数のホスト鍵の中から、それまで認知していない暗号タイプの公開鍵を知ることができ、 known_hosts ファイルに記録している公開鍵を自動で更新できるようになります。

この機能はOpenSSH 6.8で提供されており、OpenSSH 8.5からデフォルトで有効になっています。

参考:

UpdateHostKeysの仕組み

UpdateHostKeysの仕組みについて、OpenSSHのリポジトリのPROTOCOLファイルに定義されている規約をもとに解説します。

OpenSSHはSSHプロトコルの拡張をサポートしており、その拡張機能の一つに「UpdateHostKeys」があります。

SSHサーバーはユーザ認証が完了した後に、持っているすべての公開鍵をクライアントにグローバルリクエストを介して通知できます。グローバルリクエストについては後述の「[補足] グローバルリクエスト」で解説します。SSHメッセージのフォーマットは次のとおりです。

byte		SSH_MSG_GLOBAL_REQUEST
string	"hostkeys-00@openssh.com"
char		0 /* want-reply */
string[]	hostkeys

このSSHメッセージを受信したSSHクライアントは、提供された公開鍵が known_hosts ファイルに既に記録されているかどうか確認する必要があります。

SSHサーバーが、SSHクライアントではサポートしない鍵タイプを送信するかもしれないので注意してください。SSHクライアントはサポートしない鍵を受信した場合、無視する必要があります。

SSHクライアントは、 known_hosts ファイルに記録されていない(知らない)公開鍵を特定したら、「hostkeys-prove@openssh.com」メッセージを送信して、SSHサーバーにその公開鍵の所有権の証明をリクエストする必要があります。SSHメッセージのフォーマットは次のとおりです。

byte SSH_MSG_GLOBAL_REQUEST
string "hostkeys-prove-00@openssh.com"
char 1 /* want-reply */
string[] hostkeys

このSSHメッセージを受け取ったSSHサーバーは、証明をリクエストされた公開鍵のペアとなる秘密鍵を用いて、デジタル署名(注1)を生成する必要があります。

(注1)デジタル署名とは,あるメッセージがその作者によって作られたことを検証する仕組みです。

その署名のフォーマットは次のとおりです。

リクエストタイプの文字列(hostkeys-prove-00@openssh.com)、識別子のSSHのセッションID、対象の公開鍵が含まれます。

string "hostkeys-prove-00@openssh.com"
string session identifier
string hostkey

これらの署名は、リクエストのホスト鍵と一致する順序でリプライに含めるべきです。SSHメッセージのフォーマットは次のとおりです。

byte SSH_MSG_REQUEST_SUCCESS
string[] signatures

SSHクライアントはこの応答を正常に受け取ると、署名を検証し、known_hosts ファイルを更新して、新たな公開鍵を追加し、SSHサーバーで提供しなくなった公開鍵を削除することができます。

この拡張機能を使って、SSHクライアントはSSHサーバーの新しいホスト鍵の種類を知れるようになり、より優れたホスト鍵へアップグレードできるようになります。また、SSHサーバーがサポートするホスト鍵から非推奨のホスト鍵を取り除く前の段階で、新しく追加したホスト鍵の公開鍵をSSHクライアントの known_hosts ファイルに記録する機会を与えれるようになるので、一定期間複数の公開鍵を提供して自動でにローテーションできるようになります。

UpdateHostKeysのシーケンス

次の図は、前述の「UpdateHostKeysの仕組み」で解説した規約をシーケンスに書き起こした図です。

UpdateHostKeysにおける署名の流れ

次の図は、UpdateHostKeysにおけるデジタル署名の処理の流れを時系列に表したものです。

  1. SSHサーバーは持っている全てのホスト鍵の公開鍵を含む情報をクライアントに送信します。
  2. SSHクライアントはSSHサーバーから送られてきた公開鍵からknown_hostsファイルにない(知らない)ものをSSHサーバーに送信します。
  3. SSHサーバーは送られてきた公開鍵の対となる秘密鍵を使って署名を作成します。
  4. SSHサーバーは作成した署名を含む情報をSSHクライアントへ送信します。
  5. SSHクライアントは受信した署名を2で送信した公開鍵で検証します。
  6. SSHクライアントは5で検証した公開鍵をknown_hostsファイルに追加・更新します。

[補足] グローバルリクエスト

前述の「UpdateHostKeysの仕組み」で触れたグローバルリクエストについて、「RFC4254」をもとに解説します。

グローバルリクエストとチャンネルリクエスト

SSHは「RFC4254」に定義されているコネクションプロトコルで、多重化(マルチプレキシング)されたチャンネルと呼ばれる通信路を提供しています。SSHクライアントとSSHサーバーは、このチャンネルを通してSSHリクエストを送受信します。

SSHリクエストには大きく分けて次の2種類があります。

  • グローバルリクエスト
  • チャンネルリクエスト

グローバルリクエストはSSHコネクション全体に影響するリクエストです。チャンネルリクエストは特定のチャンネルだけに影響するリクエストです。

この2種類のリクエストは用途によってさらに分別されます。

グローバルリクエストの規約

SSHクライアントとSSHサーバーはいつでもグローバルリクエストを送信することができます。受信者はリクエストに対して適切にリプライしなければなりません

グローバルリクエストのフォーマットは次のとおりです。

      byte      SSH_MSG_GLOBAL_REQUEST
      string    request name in US-ASCII only
      boolean   want reply
      ....      request-specific data follows

リクエスト名(SSH_MSG_GLOBAL_REQUEST)の値は [SSH-ARCH] で概説されているDNS拡張の命名規則に従っています。

受信者はこのメッセージに対してwant replyがTRUEならば、 次のいずれかのリクエスト名でリプライする必要があります。

  • SSH_MSG_REQUEST_SUCCESS
  • SSH_MSG_REQUEST_FAILURE

リプライのフォーマットは次のとおりです。

      byte      SSH_MSG_REQUEST_SUCCESS
      ....      response specific data

通常「response specific data」は使われません。

もし受信者が対象のグローバルリクエストを認識しない、もしくはサポートしない場合、SSH_MSG_REQUEST_FAILUREで応答します。

      byte      SSH_MSG_REQUEST_FAILURE

注意点として、通常グローバルリクエストへのリプライはリクエストの識別子を含んでいません。そのため、グローバルリクエストの発信元が、各リプライがどのリクエストを指しているかを識別できるようにするために、対応するリクエストのメッセージと同じ順序で送られなければなりません。

GoでUpdateHostKeysを実装する

UpdateHostKeysを実装したコードの例を次のGitHubリポジトリで公開しています。

https://github.com/nulab/sshext 

かなり用途は限られますが、GoでSSHサーバーを実装して複数のホスト鍵を自動で追加・削除したい時に使ってみてください。

このsshextパッケージは140行程度の小さなコードで「UpdateHostKeys関数」を提供しています。ここでは実装の例として紹介します。

SSH接続を確立する

UpdateHostKeys関数を実行する前に、SSH接続を確立して認証処理を完了させる必要があります。そのため、SSH接続を確立する簡易的なコードから解説します。SSHプロトコルの実装にはgolang.org/x/crypto/sshを使用します。

ホスト鍵を登録する

ssh.ServerConfig(SSHサーバーの設定)を作成します。ここでは公開鍵認証の処理を関数でPublicKeyCallbackフィールドに代入しています。

cfg := &ssh.ServerConfig{
    PublicKeyCallback: func(c ssh.ConnMetadata, k ssh.PublicKey) (*ssh.Permissions, error) {
        // implements a public key for authentication
        return &ssh.Permissions{}, nil
    },
}

SSHのホスト鍵となる秘密鍵のパスを読み取り、ssh.ParsePrivateKey関数でssh.Signer(署名者)を取得します。同時にssh.ServerConfigが持つAddHostKey関数でssh.Signerをホスト鍵として登録します。

filenames := []string{
    "/etc/ssh/id_rsa",
    "/etc/ssh/id_ecdsa",
    "/etc/ssh/id_ed25519",
})

var signers []ssh.Signer
for _, filename := range filenames {
    pem, err := ioutil.ReadFile(filename)
    if err != nil {
        return
    }
    singer, err := ssh.ParsePrivateKey(pem)
    if err != nil {
        return
    }
    signers = append(signers, singer)
    cfg.AddHostKey(key)
}

TCP接続を確立する

net.Listen関数で22番ポートをリッスンするnet.Listener(TCPのリスナー)を作成して、net.Listenerが持つAccept関数でTCP接続が確立されるのを待ちます。TCP接続が確立されたら接続の実態としてnet.Connを返します。次にhandleConn関数を実行してSSH接続を確立するフェーズに入ります。後続のリクエストをブロッキングしないようにgoroutineを作成して非同期にhandleConn関数を実行しています。

l, err := net.Listen("tcp", fmt.Sprintf(":%d", 22))
if err != nil {
    return
}
for {
    conn, err := l.Accept()
    if err != nil {
        return
    }
    go handleConn(conn, cfg, signers)
}

ssh.NewServerConn関数は、net.Connとssh.ServerConfigをもとにSSH接続の確立・認証を行います。問題なければssh.ServerConn(認証されたSSH接続)とssh.NewChannel(SSHのチャンネル)のchan、ssh.Request(SSHのグローバルリクエスト)のchanを返します。

最後に、sshext.UpdateHostKeys関数を実行するとUpdateHostKeyの規約に沿った処理が実行されます。

func handleConn(conn net.Conn, cfg *ssh.ServerConfig, signers []ssh.Signer) {
    sconn, chans, reqs, err := ssh.NewServerConn(conn, cfg)
    if err != nil {
        return
    }

    reqs, err = sshext.UpdateHostKeys(sconn, reqs, signers)

    // 省略...
}

sshext.UpdateHostKeys関数

インターフェース

sshextパッケージが提供するAPI「UpdateHostKeys」関数は次のようなインターフェースになっています。引数で、ssh.ServerConn(認証されたSSH接続)、ssh.Request(SSHのグローバルリクエスト)のchan、ssh.Signer(署名者)の可変長配列(以下、スライス)を受け取り、UpdateHostKeys以外のssh.Request(SSHのグローバルリクエスト)を別のchanに入れ替えて返却します。

func UpdateHostKeys(conn *ssh.ServerConn, reqs <-chan *ssh.Request, signers []ssh.Signer) (<-chan *ssh.Request, error)

処理の流れ

次のコードはUpdateHostKeys関数の実装です。処理内容を上から順を追って説明します。

  1. sendHostKeys関数でクライアントへホスト鍵の公開鍵を送信します。
  2. UpdateHostKeys以外のssh.Request(SSHのグローバルリクエスト)を中継するためのチャンネルを作成します。チャンネルのサイズはcrypt/sshパッケージに定義されているデフォルトのサイズを元に16で設定しています。
  3. グローバルリクエストのチャンネルの読み取りにブロッキングされないように、goroutineを生成します。
  4. グローバルリクエストのタイプがhostkeys-00@openssh.comであれば、proveOwnership関数を実行して公開鍵の所有権の証明を行います。
  5. SSHコネクションが破棄されたらssh.Requestのチャンネルもcloseされるので、そのタイミングでforのループを抜けます。
  6. ループを抜けたら中継用のチャンネルもcloseします。
const (
    chanSize                 = 16
    requestTypeHostKeys      = "hostkeys-00@openssh.com"
    requestTypeHostKeysProve = "hostkeys-prove-00@openssh.com"
)

func UpdateHostKeys(conn *ssh.ServerConn, reqs <-chan *ssh.Request, signers []ssh.Signer) (<-chan *ssh.Request, error) {
    err := sendHostKeys(conn, signers)
    if err != nil {
        return reqs, err
    }
    id := conn.SessionID()
    relayed := make(chan *ssh.Request, chanSize)
    go func() {
        for req := range reqs {
            if req.Type == requestTypeHostKeysProve {
                proveOwnership(signers, id, req)
            } else {
                relayed <- req
            }
        }
        close(relayed)
    }()
    return relayed, nil
}

クライアントへホスト鍵を送信する

次のコードはクライアントへホスト鍵の公開鍵を送信するsendHostKeys関数の実装です。公開鍵をSSHメッセージのフォーマットに変換して、グローバルリクエストでクライアントへ公開鍵を送信します。

func sendHostKeys(conn *ssh.ServerConn, signers []ssh.Signer) error {
    payload := marshalPublicKeys(signers)
    _, _, err := conn.SendRequest(requestTypeHostKeys, false, payload)
    if err != nil {
        return fmt.Errorf("failed to send request for %s: %s", requestTypeHostKeys, err)
    }
    return nil
}

公開鍵をSSHメッセージのフォーマットに変換する

次のコードは、公開鍵をSSHメッセージのフォーマットに変換すmarshalPublicKeys関数の実装です。まず、[]ssh.Signerから公開鍵のバイト列を取得します。次に、公開鍵のバイト列をssh.Marshal関数を使ってSSHの規定のワイヤーフォーマットのバイト列に変換しています。ssh.Marshal関数の引数は構造体である必要があるため、wrapStruct関数で公開鍵のバイト列を無名の構造体でラップしています。

func marshalPublicKeys(signers []ssh.Signer) []byte {
    var buf bytes.Buffer
    for _, s := range signers {
        raw := s.PublicKey().Marshal()
        msg := wrapStruct(raw)
        buf.Write(ssh.Marshal(msg))
    }
    return buf.Bytes()
}

func wrapStruct(p []byte) struct{ string } {
    return struct {
        string
    }{string: string(p)}
}

クライアントへ公開鍵を送信する

次のコードはsendHostKeys関数内でクライアントへ公開鍵を送信する部分です。ssh.ServerConnのSendRequest関数でグローバルリクエストを送信します。この関数にリクエスト名としてhostkeys-00@openssh.comと1のバイト列を渡します。また、このリクエストに対してクライアントからのリプライは不要なので第二引数のwant-replyはfalseを渡しています。

_, _, err := conn.SendRequest(requestTypeHostKeys, false, payload)
if err != nil {
    return fmt.Errorf("failed to send request for %s: %s",requestTypeHostKeys, err)
}
return nil

ホスト鍵の所有権を証明する

次のコードは、ホスト鍵の所有権を証明するproveOwnership関数の実装です。処理内容を上から順を追って説明します。

  1. parsePublicKeys関数でクライアントからのリクエストのペイロードを公開鍵に変換します。
  2. 変換した公開鍵を検査するために、findKnown関数でサーバーが持つホスト鍵のペアと一致しているものを探します。
  3. 一致した場合、signKey関数で一致した公開鍵の署名を作成します。
  4. 作成した署名はmarshalSignatures関数でバイト列に変換します。
  5. そのバイト列をssh.RequestのReply関数にでクライアントへ送信します。
func proveOwnership(signers []ssh.Signer, sessionID []byte, req *ssh.Request) {
    keys, err := parsePublicKeys(req.Payload)
    if err != nil {
        _ = req.Reply(false, nil)
        return
    }

    var sigs []*ssh.Signature
    for _, key := range keys {
        known := findKnown(signers, key)
        if known == nil {
            _ = req.Reply(false, nil)
        return
        }

        sig, err := signHostKey(known, key, sessionID)
        if err != nil {
            _ = req.Reply(false, nil)
            return
        }
        sigs = append(sigs, sig)
    }
    _ = req.Reply(true, marshalSignatures(sigs))
}

リクエストのペイロードを公開鍵に復元する

次のコードは、クライアントからのリクエストのペイロードを公開鍵に変換するparsePublicKeys関数の実装です。処理内容を上から順を追って説明します。

  1. 引数で受け取ったリクエストのペイロードにはクライアントから送られてきた複数の公開鍵が含まれています。このペイロードのバイト列を、ssh.Unmarshal関数で関数内スコープのmsg構造体に復元します。
  2. 復元したmsg構造体はフィールドのBlobに公開鍵の文字列を持っており、このBlobをバイト列に変換してssh.ParsePublicKey関数に渡すことでssh.PublicKey構造体に変換します。
  3. 変換したssh.PublicKey構造体をスライスに追加します。
  4. 一度にmsg構造体へ復元できなかった残りのバイト列はRestフィールドに含まれているので、Restを変数pに再代入することで、全ての公開鍵を復元するまで処理を繰り返します。
func parsePublicKeys(p []byte) ([]ssh.PublicKey, error) {
    var keys []ssh.PublicKey
    for len(p) > 0 {
        var msg struct {
            Blob string
            Rest []byte `ssh:"rest"`
        }
        if err := ssh.Unmarshal(p, &msg); err != nil {
            return nil, fmt.Errorf("failed to unmarshal payload: %s", err)
        }
        key, err := ssh.ParsePublicKey([]byte(msg.Blob))
        if err != nil {
            return nil, fmt.Errorf("failed to parse public key: %s", err)
        }
        keys = append(keys, key)
        p = msg.Rest
    }
    return keys, nil
}

サーバーが認知している公開鍵か確認する

次のコードはサーバーが認知している公開鍵か確認するfindKnown関数の実装です。findKnown関数は、署名者(ssh.Signer)のスライスと、公開鍵(ssh.PublicKey)を受け取り、一致する署名者(ssh.Signer)を返します。

func findKnown(signers []ssh.Signer, key ssh.PublicKey) ssh.Signer {
    wire := key.Marshal()
    for _, s := range signers {
        if bytes.Equal(s.PublicKey().Marshal(), wire) {
            return s
        }
    }
    return nil
}

公開鍵の署名を作成する

次のコードは公開鍵の署名を作成するsignHostKey関数の実装です。signHostKey関数はssh.Signerとssh.PublicKeyとセッションIDのバイト列を受け取り、署名(ssh.Signature)を返します。

まず、次の値を元にhostKeysProveMsg構造体を生成します。

次に、hostKeysProveMsg構造体を、ssh.Marshal関数でSSHメッセージのバイト列に変換します。

最後に、ssh.SignerのSign関数に乱数生成器のrand.Readerと上記のバイト列を渡して署名します。rand.Reader(crypto/rand)はLinuxではgetrandom(2)関数かデバイスファイルの/dev/urandomが使用されます。

type hostKeysProveMsg struct {
    RequestType string
    SessionID   []byte
    Key         []byte
}

func signHostKey(signer ssh.Signer, key ssh.PublicKey, sessionID []byte) (*ssh.Signature, error) {
    msg := hostKeysProveMsg{
        RequestType: requestTypeHostKeysProve,
        SessionID:   sessionID,
        Key:         key.Marshal(),
    }
    return signer.Sign(rand.Reader, ssh.Marshal(msg))
}

署名をSSHメッセージのフォーマットに変換する

次のコードはmarshalSignatures関数の実装です。署名(ssh.Signature)のスライスを受け取り、SSHメッセージのワイヤーフォーマット(Wire Encoding [RFC4251])に従ったバイト列を返します。

func marshalSignatures(signatures []*ssh.Signature) []byte {
    var buf bytes.Buffer
    for _, s := range signatures {
        raw := ssh.Marshal(s)
        msg := wrapStruct(raw)
        buf.Write(ssh.Marshal(msg))
    }
    return buf.Bytes()
}

クライアントへ署名を送信する

最後に、ssh.Request(グローバルリクエスト)が持つReply関数で作成した署名をクライアントへ送信します。第一引数をtrueにすることで、グローバルリクエストの規約通りSSH_MSG_REQUEST_SUCCESSとして応答します。

_ = req.Reply(true, marshalSignatures(sigs))

まとめ

SSHのなりすましを防止する仕組みとして、「識別子としてのホスト鍵」「ホスト鍵を検証するknown hosts」が挙げられます。そのホスト鍵をグレイスフルに追加・削除するための機能として、OpenSSHのプロトコル拡張「UpdateHostKeys」という機能があります。本記事では、その仕組みとGoを用いた具体的な実装方法、さらに、補足として「グローバルリクエスト」について解説しました。

一般的に興味を抱かれにくい分野を深堀りした内容でしたが、GoでSSHサーバーを書く上で、ちょっとした参考になれば幸いです。

参考文献

改訂履歴

2022/08/01:

デジタル署名に関する記述の誤りを次のとおり訂正しました。フィードバックをいただきありがとうございました!

  1. 『秘密鍵で暗号化する/公開鍵で復号する』といった表現を『秘密鍵で署名を作成する/公開鍵で署名を検証する』といった表現に訂正。
  2. 1の訂正に伴い「UpdateHostKeysにおける署名の流れ」のシーケンス図を訂正。
  3. デジタル署名の概要説明を訂正。
開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる