-
Notifications
You must be signed in to change notification settings - Fork 18.8k
Description
Go 1.26 added an internal implementation of the ML-DSA post-quantum signature algorithm, specified in FIPS 204. I propose we expose a public crypto/mldsa in Go 1.27 with the following API.
// Package mldsa implements the post-quantum ML-DSA signature scheme specified
// in FIPS 204.
//
// This package is unavailable if using the FIPS 140-3 Go Cryptographic Module
// v1.0.0, in which case [GenerateKey], [NewPrivateKey], [NewPublicKey], and
// [Verify] will return an error.
package mldsa
const (
PrivateKeySize = 32
MLDSA44PublicKeySize = 1312
MLDSA65PublicKeySize = 1952
MLDSA87PublicKeySize = 2592
MLDSA44SignatureSize = 2420
MLDSA65SignatureSize = 3309
MLDSA87SignatureSize = 4627
)
// Parameters represents one of the fixed parameter sets defined in FIPS 204.
//
// Most applications should use [MLDSA44].
type Parameters struct{}
// MLDSA44 returns the ML-DSA-44 parameter set defined in FIPS 204.
//
// Multiple invocations of this function will return the same value, which can
// be used for equality checks and switch statements. The returned value is safe
// for concurrent use.
func MLDSA44() *Parameters
// MLDSA65 returns the ML-DSA-65 parameter set defined in FIPS 204.
//
// Multiple invocations of this function will return the same value, which can
// be used for equality checks and switch statements. The returned value is safe
// for concurrent use.
func MLDSA65() *Parameters
// MLDSA87 returns the ML-DSA-87 parameter set defined in FIPS 204.
//
// Multiple invocations of this function will return the same value, which can
// be used for equality checks and switch statements. The returned value is safe
// for concurrent use.
func MLDSA87() *Parameters
// PublicKeySize returns the size of public keys for this parameter set, in bytes.
func (params *Parameters) PublicKeySize() int
// SignatureSize returns the size of signatures for this parameter set, in bytes.
func (params *Parameters) SignatureSize() int
// String returns the name of the parameter set, e.g. "ML-DSA-44".
func (params *Parameters) String() string
// PrivateKey is an in-memory ML-DSA private key. It implements [crypto.Signer]
// and the informal extended [crypto.PrivateKey] interface.
//
// A PrivateKey is safe for concurrent use.
type PrivateKey struct {}
// GenerateKey generates a new random ML-DSA private key.
func GenerateKey(params *Parameters) (*PrivateKey, error)
// NewPrivateKey creates a new ML-DSA private key from the given seed.
//
// The seed must be exactly [PrivateKeySize] bytes long.
func NewPrivateKey(params *Parameters, seed []byte) (*PrivateKey, error)
// Public returns the corresponding [PublicKey] for this private key.
//
// It implements the [crypto.Signer] interface.
func (sk *PrivateKey) Public() crypto.PublicKey
// Equal reports whether sk and x are the same key (i.e. they are derived from
// the same seed).
//
// If x is not a *PrivateKey, Equal returns false.
func (sk *PrivateKey) Equal(x crypto.PrivateKey) bool
// PublicKey returns the corresponding [PublicKey] for this private key.
func (sk *PrivateKey) PublicKey() *PublicKey
// Bytes returns the private key seed.
func (sk *PrivateKey) Bytes() []byte
// Sign returns a signature of the given message using this private key.
//
// If opts is nil or opts.HashFunc returns zero, the message is signed directly.
// If opts.HashFunc returns [crypto.MLDSAMu], the provided message must be a
// [pre-hashed μ message representative]. opts can be of type *[Options].
// The io.Reader argument is ignored.
//
// [pre-hashed μ message representative]: https://www.rfc-editor.org/rfc/rfc9881.html#externalmu
func (sk *PrivateKey) Sign(_ io.Reader, message []byte, opts SignerOpts) (signature []byte, err error)
// SignDeterministic works like [PrivateKey.Sign], but the signature is
// deterministic.
func (sk *PrivateKey) SignDeterministic(message []byte, opts SignerOpts) (signature []byte, err error)
// PublicKey is an ML-DSA public key. It implements the informal extended
// [crypto.PublicKey] interface.
//
// A PublicKey is safe for concurrent use.
type PublicKey struct {}
// NewPublicKey creates a new ML-DSA public key from the given encoding.
func NewPublicKey(params *Parameters, seed []byte) (*PublicKey, error)
// Bytes returns the public key encoding.
func (pk *PublicKey) Bytes() []byte
// Equal reports whether pk and x are the same key (i.e. they have the same
// encoding).
//
// If x is not a *PublicKey, Equal returns false.
func (pk *PublicKey) Equal(x crypto.PublicKey) bool
// Parameters returns the parameters associated with this public key.
func (pk *PublicKey) Parameters() *Parameters
// Verify reports whether signature is a valid signature of message by pk.
func Verify(pk *PublicKey, message []byte, signature []byte, opts *Options) error
// Options contains additional options for signing and verifying ML-DSA signatures.
type Options struct {
// Context can be used to distinguish signatures created for different
// purposes. It must be at most 255 bytes long, and it is empty by default.
//
// The same context must be used when signing and verifying a signature.
Context string
}
// HashFunc returns zero, to implement the [crypto.SignerOpts] interface.
func (opts *Options) HashFunc() crypto.HashTo signal inputs that are pre-hashed μ message representatives, I propose we also assign a new crypto.Hash iota value.
package crypto
// MLDSAMu is a function that produces a [pre-hashed μ message representative].
// It has no implementation, but is used a [SignerOpts.HashFunc] return value
// for [mldsa.PrivateKey.Sign].
//
// [pre-hashed μ message representative]: https://www.rfc-editor.org/rfc/rfc9881.html#externalmu
const MLDSAMu Hash
Finally, I propose we add support for parsing and marshaling ML-DSA keys in PKIX and PKCS#8 formats, according to RFC 9881, by extending MarshalPKCS8PrivateKey, ParsePKCS8PrivateKey, MarshalPKIXPublicKey, and ParsePKIXPublicKey to accept/return *mldsa.PrivateKey/*mldsa.PublicKey. For private keys, we will support only the recommended seed format. Note that I am not (yet?) proposing adding a PublicKeyAlgorithm and SignatureAlgorithm, nor any support in X.509 certificates.
Discussion
Why a crypto.Hash for External μ? μ is H(H(pubkey) || 0x00 || len(context) || context || message). See “Pre-Hashing (Externalμ-ML-DSA)” in RFC 9881. We could have an Options field to specify the message is a μ, but 1. it’s more likely crypto.Signer implementations will ignore it like RSA ones ignore PSSOptions, 2. it’s not clear External μ verification is safe so we should not support it, and 3. it would be mutually exclusive with Context. We could have a separate SignExternalMu method, but then there would be no established way to use a hardware implementation through crypto.Signer, which is what External μ is for. In a sense, μ computation is a weird, specific hash function which preprocesses the crypto.Signer input, and that’s what crypto.Hash represents. There is precedent in sentinel crypto.Hash values without implementation in crypto.MD5SHA1.
Why not support HashML-DSA? There are too many ML-DSA, and the consensus that emerged is that external hashing can be done with External μ instead. See “Rationale for Disallowing HashML-DSA” in RFC 9881.
Why a SignDeterministic method instead of an Options field? Because deterministic signing is only relevant to Sign, not to Verify, and Options is shared. The downside is that there is no way to request deterministic signing through a crypto.Signer implementation. That might be ok, because often the client of e.g. hardware implementations doesn’t get to ask for deterministic signatures?
Why not a package-level Sign function? We need the PrivateKey.Sign method for crypto.Signer, and we might as well have only one way to do things.
Why a package-level Verify function? It matches all other signature packages, and functions can get omitted by the linker, unlike methods. I don’t love the asymmetry with Sign, so not a strong opinion.
Why an Options struct with one field instead of a context parameter? We need the struct for crypto.SignerOpts, and we might as well make Verify consistent. Also, we might want to add a hash function for HashML-DSA in the future if we change our mind.
Why no X.509 certificate support? We might get there, but let’s start with the core functionality. The X.509 landscape is still in flux and major players, including the WebPKI and browsers, haven’t picked a route yet.
Why only seeds? Semi-expanded keys are a terrible format which is larger and slower to load than seeds, but on the other hand is more dangerous. They have only downsides. https://cs.opensource.google/go/go/+/master:src/crypto/internal/fips140/mldsa/semiexpanded.go
/cc @golang/security
Metadata
Metadata
Assignees
Labels
Type
Projects
Status