【Tips】Go言語の x/crypto/ssh パッケージで パスフレーズで保護された秘密鍵を扱う

こんにちは。DI部の春田です。

表題の件について、備忘録です。

問題

GolangでSSHクライアントを実装で、パスフレーズで保護された秘密鍵を扱いたいとき。

保護されたままパースすると、ParseRawPrivateKey の encryptedBlock でエラーを吐き出してくれます。

crypto/keys.go at master · golang/crypto · GitHub

// encryptedBlock tells whether a private key is
// encrypted by examining its Proc-Type header
// for a mention of ENCRYPTED
// according to RFC 1421 Section 4.6.1.1.
func encryptedBlock(block *pem.Block) bool {
	return strings.Contains(block.Headers["Proc-Type"], "ENCRYPTED")
}

// ParseRawPrivateKey returns a private key from a PEM encoded private key. It
// supports RSA (PKCS#1), PKCS#8, DSA (OpenSSL), and ECDSA private keys.
func ParseRawPrivateKey(pemBytes []byte) (interface{}, error) {
	block, _ := pem.Decode(pemBytes)
	if block == nil {
		return nil, errors.New("ssh: no key found")
	}

	if encryptedBlock(block) {
		return nil, errors.New("ssh: cannot decode encrypted private keys")
	}

	switch block.Type {
	case "RSA PRIVATE KEY":
		return x509.ParsePKCS1PrivateKey(block.Bytes)
	// RFC5208 - https://tools.ietf.org/html/rfc5208
	case "PRIVATE KEY":
		return x509.ParsePKCS8PrivateKey(block.Bytes)
	case "EC PRIVATE KEY":
		return x509.ParseECPrivateKey(block.Bytes)
	case "DSA PRIVATE KEY":
		return ParseDSAPrivateKey(block.Bytes)
	case "OPENSSH PRIVATE KEY":
		return parseOpenSSHPrivateKey(block.Bytes)
	default:
		return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type)
	}
}

解決策

直下に便利な関数がありました。

ssh - GoDoc

// ParseRawPrivateKeyWithPassphrase returns a private key decrypted with
// passphrase from a PEM encoded private key. If wrong passphrase, return
// x509.IncorrectPasswordError.
func ParseRawPrivateKeyWithPassphrase(pemBytes, passPhrase []byte) (interface{}, error) {
	block, _ := pem.Decode(pemBytes)
	if block == nil {
		return nil, errors.New("ssh: no key found")
	}
	buf := block.Bytes

	if encryptedBlock(block) {
		if x509.IsEncryptedPEMBlock(block) {
			var err error
			buf, err = x509.DecryptPEMBlock(block, passPhrase)
			if err != nil {
				if err == x509.IncorrectPasswordError {
					return nil, err
				}
				return nil, fmt.Errorf("ssh: cannot decode encrypted private keys: %v", err)
			}
		}
	}

	switch block.Type {
	case "RSA PRIVATE KEY":
		return x509.ParsePKCS1PrivateKey(buf)
	case "EC PRIVATE KEY":
		return x509.ParseECPrivateKey(buf)
	case "DSA PRIVATE KEY":
		return ParseDSAPrivateKey(buf)
	case "OPENSSH PRIVATE KEY":
		return parseOpenSSHPrivateKey(buf)
	default:
		return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type)
	}
}

引数にパスフレーズを追加してあげるだけです。2年前くらいに追加された機能みたいですね。

追記

OpenSSH 7.8/7.8p1 (2018-08-24) で、ssh-keygenで作成する秘密鍵のヘッダの形式が変更されました。
このバージョン以降で作成したパスフレーズ付きの秘密鍵は、上のパッケージでまだ対応できていないようなので注意してください。

OpenSSH: Release Notes 7.8/7.8p1 (2018-08-24) 

  • ssh-keygen(1): write OpenSSH format private keys by default instead of using OpenSSL's PEM format. The OpenSSH format, supported in OpenSSH releases since 2014 and described in the PROTOCOL.key file in the source distribution, offers substantially better protection against offline password guessing and supports key comments in private keys. If necessary, it is possible to write old PEM-style keys by adding "-m PEM" to ssh-keygen's arguments when generating or updating a key.

例)RSA 旧ヘッダ -----BEGIN RSA PRIVATE KEY-----
新ヘッダ -----BEGIN OPENSSH PRIVATE KEY-----

解決策として、ssh-keygenのオプションに "-m PEM" を加えると、旧ヘッダで出力できます。

最後に

ソースコード見れば済む話ですが、ググると情報が古かったので書きました。ご参考までに!