crypto パッケージに、External μ ML-DSA 署名の入力が事前ハッシュ済みであることを示すセンチネル値 MLDSAMu が追加された。
crypto
概要
crypto パッケージに新しい Hash 値 MLDSAMu が追加された。これ自体は実装を持たないセンチネル値で、SignerOpts.HashFunc() が MLDSAMu を返すことによって、crypto.Signer に渡すメッセージが RFC 9881 の定める「事前ハッシュ済み External μ メッセージ代表値」であることをシグナルするために使う。主な利用先は crypto/mldsa の PrivateKey.Sign/SignDeterministic で、ハッシュ計算を内部で完結させたいハードウェア実装から External μ 方式の ML-DSA 署名を要求できるようになる。
導入経緯
この値は crypto/mldsa パッケージ新設のproposal(golang/go#77626)の一部として提案された。提案では、External μ に対応する crypto.Hash センチネル値を用意することで、crypto.Signer インターフェース経由でハードウェア実装(HSMなど)にも一貫した方法で事前ハッシュ済みの入力を渡せるようにする狙いが説明されている。
議論タイムライン
- 2026-02-25: Weekly Proposal Review Meeting で active へ移行。
- 2026-04-15: Proposal Review Meeting でトップコメントのAPIをそのまま進めることに合意し、likely accept へ移行。
- 2026-04-22: それ以降コンセンサスに変更がなかったため accepted。
議論のハイライト
- 「なぜ
Optionsのフィールドではなくcrypto.Hashのセンチネル値なのか」という設計判断について、issue内では①crypto.Signer実装がPSSOptionsのようにフィールドを無視してしまいがちなこと、②External μ の検証が安全かどうかまだ明確でないためサポートすべきでないこと、③Options.Contextと両立しないこと、の3点が理由として挙げられ、実装を持たないcrypto.Hash値という点でcrypto.MD5SHA1に前例があるとされた。 - レビューでは
MLDSAMuに対してSize()/Available()/HashFunc()/String()/New()の各メソッドの挙動が問われ、提案者は「Size()は64を返す、Available()は false、HashFunc()は自分自身、String()はML-DSA μ message representative、New()はpanicする」と回答した。また「crypto.RegisterHashで外部パッケージが実装を差し込めないようにすべきか」という指摘を受け、RegisterHashがMLDSAMuに対して明示的にpanicするよう変更された。 - 「pre-hashed μ message representative を
hash.Hashとして実装できないか」という質問には、公開鍵とコンテキストで初期化する必要があるため通常のfunc() hash.Hashのような構築子の形にはできない(func(*mldsa.PublicKey, *mldsa.Options) hash.Hashのような形になる)という回答があった。
使用例
Before
package main
import (
"crypto/mldsa"
"fmt"
)
func main() {
sk, err := mldsa.GenerateKey(mldsa.MLDSA44())
if err != nil {
panic(err)
}
message := []byte("hello, post-quantum world")
// crypto.Signer 経由でメッセージを渡す標準的な方法は、生のメッセージを
// そのまま署名するものだけだった。ハードウェア実装が内部で事前ハッシュ
// (External μ)を計算し終えている場合でも、それを crypto.SignerOpts で
// 汎用的にシグナルする手段がなかった。
sig, err := sk.Sign(nil, message, &mldsa.Options{Context: "doc-v1"})
if err != nil {
panic(err)
}
fmt.Printf("signature length: %d\n", len(sig))
}
After
package main
import (
"crypto"
"crypto/mldsa"
"fmt"
)
func main() {
sk, err := mldsa.GenerateKey(mldsa.MLDSA44())
if err != nil {
panic(err)
}
// mu はハードウェア実装などが RFC 9881 の External μ 計算に従って
// あらかじめ計算した「事前ハッシュ済みメッセージ代表値」で、長さは
// crypto.MLDSAMu.Size()(64バイト)と一致する必要がある。
mu := make([]byte, crypto.MLDSAMu.Size())
// crypto.Hash は HashFunc() で自分自身を返すため crypto.SignerOpts を
// 満たしており、crypto.MLDSAMu をそのまま opts として渡せる。これにより
// message が生のメッセージではなく事前ハッシュ済みの μ であることを
// crypto.Signer 経由で汎用的にシグナルできる。
sig, err := sk.Sign(nil, mu, crypto.MLDSAMu)
if err != nil {
panic(err)
}
fmt.Printf("signature length: %d\n", len(sig))
}
移行時の注意
MLDSAMu は実装を持たないセンチネル値なので、crypto.RegisterHash(crypto.MLDSAMu, ...) を呼ぶとpanicする。MLDSAMu.New() を呼び出した場合もpanicし、MLDSAMu.Available() は常に false を返す。crypto.MLDSAMu を SignerOpts.HashFunc() の戻り値として使う場合、Sign/SignDeterministic に渡すメッセージは生のメッセージではなく、事前計算済みのExternal μ(64バイト)である必要がある点に注意する。
実装解説
crypto パッケージでは MLDSAMu は既存の Hash の iota 列の末尾に追加された定数で、crypto.go#L91-L96で定義され、digestSizes テーブルで 64 を返すよう登録されている(crypto.go#L121)。String() メソッドは "ML-DSA μ message representative" を返す専用のcaseを持ち(crypto.go#L63)、RegisterHash は MLDSAMu が渡されると明示的にpanicするようガードされている(crypto.go#L160)。
利用側の crypto/mldsa では、PrivateKey.Sign/SignDeterministic が opts.HashFunc() の値で switch しており、戻り値が 0 なら通常のメッセージとして mldsa.Sign/mldsa.SignDeterministic に、crypto.MLDSAMu なら事前ハッシュ済みの μ として mldsa.SignExternalMu/mldsa.SignExternalMuDeterministic にディスパッチしている(mldsa_fips140v1.26.go#L105-L121)。