x/crypto/ssh: refactor signers API
要約
概要
x/crypto/sshパッケージの署名者(Signer)関連APIが長年の拡張で複雑化・重複している問題を解消するため、V2サフィックス付きの新しい統一APIを導入する提案です。将来的にx/crypto/sshが標準ライブラリへ移行する際の橋渡しとしても位置づけられています。
ステータス変更
active → likely_accept
2026年2月から続いた複数回のproposal reviewを経て、2026年9月16日のミーティングで「likely accept」と判定されました。当初は「完全なv2パッケージを作るべきか」が議論されましたが、v1/v2間の循環依存が避けられず現実的でないと判断され、既存パッケージ内にV2サフィックスのAPIを段階的に追加する方針が採用されました。その後@rolandshoemaker氏によるAPIレビューを経て、命名(V2サフィックス)には引き続き難色を示しつつも「段階的にv2へ進む唯一の現実的な方法」として合意に至りました。
技術的背景
現状の問題点
現在のsshパッケージにはSigner・AlgorithmSigner・MultiAlgorithmSignerという3つの重複したインターフェースが存在し、RSA鍵の署名アルゴリズム選択のために場当たり的に拡張されてきました。またParsePrivateKey系関数はDSA鍵(OpenSSH 10.0で既に廃止)をサポートするためのinterface{}引数や、非推奨のx509.DecryptPEMBlockに依存したレガシーPEM暗号化方式も抱えています。
// 現状: 複数の型アサーションが必要になりがちな乱立したインターフェース
var s ssh.Signer
if as, ok := s.(ssh.AlgorithmSigner); ok {
sig, err := as.SignWithAlgorithm(rand, data, "rsa-sha2-256")
}
提案された解決策
単一のSignerV2インターフェースに統合し、Algorithms()で利用可能なアルゴリズム一覧を取得、SignContextでコンテキストとアルゴリズムを指定して署名できるようにします。
type SignerV2 interface {
PublicKey() PublicKey
Sign(rand io.Reader, data []byte) (*Signature, error)
SignContext(ctx context.Context, rand io.Reader, data []byte, algorithm string) (*Signature, error)
Algorithms() []string
Signer() (crypto.Signer, error)
}
func NewSignerV2(signer crypto.Signer) (SignerV2, error)
func NewSignerV2WithAlgorithms(signer SignerV2, algorithms []string) (SignerV2, error)
func NewCertificateSignerV2(cert *Certificate, signer SignerV2) (SignerV2, error)
func ParsePrivateKeyV2(pemBytes []byte, options *ParsePrivateKeyV2Options) (SignerV2, error)
func MarshalPrivateKeyV2(key SignerV2, options *MarshalPrivateKeyV2Options) (*pem.Block, error)
DSAサポートとレガシーPEM暗号化(RFC 1423)は廃止され、ssh-rsa(SHA-1)はデフォルトで無効化されます。SignerV2は既存のSignerインターフェースと互換性があり、ServerConfig.AddHostKeyなど既存APIにもそのまま渡せます。
これによって何ができるようになるか
- KMS/HSMバックエンドの鍵をコンテキスト伝播しながら安全に利用:
SignContextによりタイムアウトやトレーシング情報を署名処理まで伝えられ、ハードウェアセキュリティモジュール経由の署名者を扱いやすくなります。 - 署名アルゴリズムの一元管理:
NewSignerV2WithAlgorithmsにより、鍵の種類を意識せず安全なアルゴリズムのみを許可するフィルタリングが可能になります。 - legacy
ssh-rsa/SHA-1を明示的にのみ有効化: デフォルトで安全側に倒しつつ、必要な場合のみ明示的に古い相手先との互換性を確保できます。
コード例
// Before: MultiAlgorithmSigner への型アサーションが必要
signer, _ := ssh.ParsePrivateKey(pemBytes)
if mas, ok := signer.(ssh.MultiAlgorithmSigner); ok {
sig, _ := mas.SignWithAlgorithm(rand.Reader, data, "rsa-sha2-256")
}
// After: SignerV2 で統一的にコンテキスト付き署名
signer, _ := ssh.ParsePrivateKeyV2(pemBytes, &ssh.ParsePrivateKeyV2Options{})
sig, _ := signer.SignContext(ctx, rand.Reader, data, "rsa-sha2-256")
議論のハイライト
- v2パッケージ分離 vs 既存パッケージへの追加: レビュー会議で完全な別パッケージ化が議論されたが、v1/v2間の循環依存が避けられず現実的でないため、既存パッケージ内への段階的な
V2API追加に落ち着いた。 - レガシーPEM暗号化キーの扱い:
@imirkin氏がDES-CBC等の古い暗号化鍵を今後どう扱うかを懸念し、サードパーティ実装への流出リスクを指摘。提案者は他メンテナとの検討を継続する姿勢を示した。 ssh-rsa(SHA-1)のデフォルト無効化: セキュリティ上デフォルトでは無効化しつつ、ParsePrivateKeyV2Options.SignatureAlgorithmsで明示的に許可できるよう拡張し、既存の「不要なアルゴリズムを除外する」ワークフローとの互換性を確保した。context.Context対応の追加:@hslatman氏の指摘(crypto.Signer向けの同様の提案#56508が既に承認済み)を受け、SignContextメソッドが後から追加された。- メソッド名の変更(
PrivateKey()→Signer()): 返り値がcrypto.Signerであり実際の秘密鍵型ではないため、@hslatman氏の提案でSigner()に改名。ただし、標準ライブラリ移行後にSignerインターフェースのSigner()メソッドという命名になる点は「あまり格好良くない」と認識されつつも許容された。 - APIの一貫性改善: レビューア(
@meling氏)の指摘を受け、NewSignerV2はcrypto.Signerから生成、NewSignerV2WithAlgorithmsはSignerV2を制限する、という役割分担に整理された。