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/pem
のpem.Decode()
とcyrpt/x509
のx509.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
で、rsaPublicKey
がssh.PublicKey
インタフェースを実装しています。ssh
パッケージで手に入るオブジェクトと、欲しいオブジェクトは紙一重でした。でも、rsaPublicKey
がプライベートなので、ダウンキャストさせるコードが書けないんです。
- 前提5: 欲しい型のオブジェクトと、実際に手に入るオブジェクトの実体は同じ。でも実際に手に入るオブジェクトはパッケージプライベートで、公開されているインタフェースしかアクセスできない。
長かったですが、目的編はここまでです。
参考URL:
- https://socketloop.com/references/golang-crypto-rsa-encryptoaep-decryptoaep-functions-example
- http://stackoverflow.com/questions/14404757/how-to-encrypt-and-decrypt-plain-text-with-a-rsa-keys-in-go
解決編
解決したい課題をまとめます。
- 欲しいのは、
rsa.PublicKey
型のオブジェクト -
ssh
パッケージ内で、ssh.rsaPublicKey
型は、rsa.PublicKey
型へのエイリアスとして定義されている。プライベートなのが要注目ポイント -
ssh
パッケージを使ってパースすると、*ssh.rsaPublicKey
のオブジェクトになって帰ってくるが、外部からはrsa.PublicKey
インタフェースの「何か」が帰ってくるとしか分からない -
*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にするか・・・