gRPCで独自の認証機構を利用する(SSH公開鍵認証編)
前回はgRPCのWithPerRPCCredentialsを用いた独自認証機構について試した。今回はWithTransportCredentialsを試すよ。
WithTransportCredentials
これを使うことで、ハンドシェイクを行うような認証方式を実装できる。TLS/SSLや、少し前に話題になったALTSもWithTransportCredentialsを使っている。go-grpcのこのあたりにコードがある。今回実装するにあたり、参考にした。
TransportCredentials
インタフェースとしてTransportCredentialsが用意されている。今回は、SSHの認証で使う公開鍵を用いる、というものを作ってみた。TransportCredentialとハンドシェイク部分は以下の通り。公開鍵・秘密鍵の扱いについてはこのあたりを参照。ClientHandshakeとServerHandshakeを実装することで、net.Connを使ってサーバーとクライアント間でやりとりし、認証する。
type sshTC struct {
info *credentials.ProtocolInfo
publicKeyPath string
privateKeyPath string
}
func (tc *sshTC) ClientHandshake(ctx context.Context, addr string, rawConn net.Conn) (_ net.Conn, _ credentials.AuthInfo, err error) {
// サーバーから暗号化された乱数を受信
buf := make([]byte, 2014)
n, err := rawConn.Read(buf)
if err != nil {
log.Printf("[ERROR] Read error: %s\n", err)
return nil, nil, err
}
// 復号
key, err := tc.readPrivateKey(tc.privateKeyPath)
decrypted, err := tc.Decrypt(string(buf[:n]), key)
if err != nil {
log.Printf("[ERROR] Failed to decrypt: %s\n", err)
return nil, nil, err
}
// 復号結果からハッシュ値を生成し、サーバーに送信
h := sha256.Sum256([]byte(decrypted))
rawConn.Write([]byte(fmt.Sprintf("%x\n", h)))
// 認証結果をサーバーから受信
r := make([]byte, 64)
n, err = rawConn.Read(r)
if err != nil {
log.Printf("[ERROR] Read error: %s\n", err)
return nil, nil, err
}
r = r[:n]
if string(r) != "ok" {
log.Println("[ERROR] Failed to authenticate")
return nil, nil, errors.New("Failed to authenticate")
}
return rawConn, nil, err
}
func (tc *sshTC) ServerHandshake(rawConn net.Conn) (_ net.Conn, _ credentials.AuthInfo, err error) {
// 乱数を生成する
s := tc.randString()
// 乱数のハッシュ値を生成
h := sha256.Sum256([]byte(s))
// 乱数を暗号化してクライアントに送信
encrypted, err := tc.Encrypt(s, tc.publicKeyPath)
if err != nil {
return nil, nil, errors.New(fmt.Sprintf("Failed to encrypt: %s\n", err))
}
//fmt.Printf("encrypted: %s\n", encrypted)
rawConn.Write([]byte(encrypted))
// クライアントからハッシュ値を受け取る
buf := make([]byte, 2014)
n, err := rawConn.Read(buf)
if err != nil {
return nil, nil, errors.New(fmt.Sprintf("Read error: %s\n", err))
}
// 事前に生成したハッシュ値とクライアントから受け取ったハッシュ値を比較する
// 一致していれば正しいキーペアを使用していることがわかる
if strings.TrimRight(string(buf[:n]), "\n") == fmt.Sprintf("%x", h) {
rawConn.Write([]byte("ok"))
log.Println("[INFO] Authenticate Success!!!")
} else {
rawConn.Write([]byte("ng"))
log.Println("[ERROR] Authenticate Failed...")
return nil, nil, errors.New(fmt.Sprintf("Failed to authenticate: invalid key"))
}
return rawConn, nil, err
}
サンプル
takaishi/hello2018においてある。
サーバー:
$ go run ./main.go server -p ./test_rsa.pub
クライアント:
$ go run ./main.go client -i ./test_rsa add foo 1
$ go run ./main.go client -i ./test_rsa list
name:"foo" age:1
間違った秘密鍵を指定した場合:
$ go run ./main.go client -i ./invalid_rsa list
2018/04/10 21:42:47 [ERROR] Failed to decrypt: crypto/rsa: decryption error
2018/04/10 21:42:47 rpc error: code = Unavailable desc = all SubConns are in TransientFailure, latest connection error: connection error: desc = "transport: authentication handshake failed: crypto/rsa: decryption error"
exit status 1
所感
認証はできたが、通信内容の暗号化はどうやるんだろう?というところ。SSHだと認証の後に共通鍵を発行し、それを用いて暗号化するわけだが…。TLS/SSLの実装部分を読み込めばわかりそうな気はする。