Go

golangでprivateなエイリアスのポインタを元の型に戻す

More than 1 year has passed since last update.

ssh-keygenで作成したsshの公開鍵を読み込みたかったのに、技術的にうまい方法が見つからずにreflectを使いましたという話です。

技術的な解は「解決編」に書いていますが、解法だけだと「なんでわざわざそんなことしているの?アホなの?」と思われるかもしれないので、「目的編」にそのあたりのことを具体的に書きます。結果だけ知りたい人は目的編は飛ばしてください。

目的編

公開鍵暗号方式でファイルを暗号化しようとしました。golangにはデフォルトのAPI(crypt/rsa)で暗号化もできちゃうのでこれを使おうと思います。セキュリティがより堅い楕円暗号もありますが、まあsshの鍵としてみんな作るのはrsaの方がまだ多いですよね?

  • 前提1: golangの標準ライブラリでサポートされているRSAを使った暗号化が使いたいと思った

とりあえず、ssh-keygenコマンドを使って公開鍵と秘密鍵を作りました。今どきはこのあたりはみんな当たり前ですよね?ちゃちゃっと作っちゃいます。一応、普段のgit操作用の鍵のペアと一緒にするのは良くないと思ったので別に作ります。id_rsa, id_rsa.pubができました。

  • 前提2: みんなが慣れているssh-keygenを使うことにした

で、サンプルコードをあたると、最終的には*rsa.PublicKey 形式のオブジェクトが作れれば、rsa.EncryptOAEP 関数を使えば暗号化できそうです *1。ですが、どうもssh-keygenの公開鍵ファイルを使った暗号化について説明しているものがあまりないです。

ssh-keygenは公開鍵は ssh-rsa なんかバイナリっぽい文字列 みたいな感じのファイルになります。、秘密鍵はpem形式と呼ばれるファイルです。pem方式は-----BEGIN RSA PRIVATE KEY-----みたいなブロックで囲まれた中にbase64エンコードされたデータが格納されています。pemそのものはコンテナで、暗号鍵データはPKCSという形式で・・・みたいなのは興味ある方は調べてください。

サンプルで見つかったのはrsa.GenerateKey()で毎回キーを作成するか、crypt/pempem.Decode()cyrpt/x509x509.ParsePKCS1PrivateKey()を使って秘密鍵の方をパースして、rsa.PrivateKey構造体のPublicKeyメンバーを使う方法*2ぐらいです。毎回生成しちゃうのは暗号化通信でサーバクライアント間の暗号化にはいいかもしれないけど、単純にファイルの暗号化をしたいユースケースだと合いません。秘密鍵はなるべくばらまきたくないので、秘密鍵を使う方法もやめておきます。

  • 前提3: 秘密鍵を使う方法は禁じ手にしておく

ssh形式の公開鍵のパースの方法について調べてみると、準標準ライブラリのgolang.org/x/crypt/sshを使うとできそうです。ファイルのパースをして、その後、元の形式に戻すというのが以下のコードでできました。

src, err := ioutil.ReadFile("data/id_rsa.pub")
pub, _, _, _, err := ssh.ParseAuthorizedKey(src)
fmt.Println(ssh.MarshalAuthorizedKey(pub))

落とし穴が一個あって、このsshパッケージで作った公開鍵は、ssh.PublicKeyインタフェースを持ったプライベートなオブジェクトです。*rsa.PublicKeyとは違うのです。*rsa.PublicKeyからssh.PublicKeyへは変換間関数を使うと移行可能ですが(ssh.NewPublicKey(publicKey))、ssh.PublicKeyから*rsa.PublicKeyへの変換はできません。ssh.PublicKeyは署名には使えますが、暗号化には使えません。

もちろん、ssh.ParseAuthorizedKey()は内部でプライベートな関数をいくつも呼んでいるので、それらのコードを全部そのままコピーしてくれば目的は達成できますですが、それはやめておきます。特にセキュリティ関連はアップデートを忘れるとセキュリティ・ホールが残り続けることにもなるので、専業で暗号関連をやっている人でなければ、アップストリームのコードをそのまま使う方が安全です。

  • 前提4: プライベートなコードのコピーはしない

sshパッケージのコードを見ると、以下のようなコードがあります。

type rsaPublicKey rsa.PublicKey

で、rsaPublicKeyssh.PublicKeyインタフェースを実装しています。sshパッケージで手に入るオブジェクトと、欲しいオブジェクトは紙一重でした。でも、rsaPublicKeyがプライベートなので、ダウンキャストさせるコードが書けないんです。

  • 前提5: 欲しい型のオブジェクトと、実際に手に入るオブジェクトの実体は同じ。でも実際に手に入るオブジェクトはパッケージプライベートで、公開されているインタフェースしかアクセスできない。

長かったですが、目的編はここまでです。

参考URL:
1. https://socketloop.com/references/golang-crypto-rsa-encryptoaep-decryptoaep-functions-example
2. http://stackoverflow.com/questions/14404757/how-to-encrypt-and-decrypt-plain-text-with-a-rsa-keys-in-go

解決編

解決したい課題をまとめます。

  1. 欲しいのは、rsa.PublicKey型のオブジェクト
  2. sshパッケージ内で、ssh.rsaPublicKey型は、rsa.PublicKey型へのエイリアスとして定義されている。プライベートなのが要注目ポイント
  3. sshパッケージを使ってパースすると、 *ssh.rsaPublicKey のオブジェクトになって帰ってくるが、外部からはrsa.PublicKeyインタフェースの「何か」が帰ってくるとしか分からない
  4. *ssh.rsaPublicKeyからrsa.PublicKeyへの変換のメソッドは公開されていない

もし、rsaPublicKey型が公開された型であれば話は簡単です。以下の様に2回キャストすれば変換できます。

// ダウンキャスト
pk1, ok := publicKey.(*ssh.RsaPublicKey)
// ふつうのキャスト
pk2 := (*rsa.PublicKey)(pk1)

golangでは、インタフェースだけを公開して、実際の実装型を非公開にするということができて、sshパッケージはまさにそれをしています。

インタフェースが非公開型のオブジェクトを持っている場合、確実に実体が互換性のある型とわかっているなら次の方法が取れます。reflectパッケージとunsafeパッケージを使います。

v := reflect.ValueOf(publicKey)
pk := (*rsa.PublicKey)(unsafe.Pointer(v.Elem().UnsafeAddr()))

実装型が非公開でも無理やり変換できました。なお、本番コードではssh.PublicKey.Type()メソッドを使って確実にその型かどうかの確認をしてからにすべきですね。

オチ編

そもそもRSAの暗号化だと、暗号化できるデータサイズがすごく小さくて使いにくいのでした。openpgpにするか・・・