Skip to content

Instantly share code, notes, and snippets.

@salrashid123
Last active March 16, 2026 11:27
Show Gist options
  • Select an option

  • Save salrashid123/69bcb59cacffcede684faf07e23cca12 to your computer and use it in GitHub Desktop.

Select an option

Save salrashid123/69bcb59cacffcede684faf07e23cca12 to your computer and use it in GitHub Desktop.
Cosign MLDSA signatures (experimental)

Cosign local sign-blob with MLDSA

Simple demo of using MLDSA signatures with cosign.

Before you do anything, please note this is just a POC, nothing more, nothing less. Do not use other than just to test.

plase note

go does not support MLDSA but it will in crypto/mldsa after golang/go#77626

once thats done and there is built in support in the crypto/x509 module, i would guess cosign, sigstore and dependencies like letsencrypt/boulder will also need to support it.

For now, i just did a hack/patch to those packages to generate a basic signature.

The following will sign-blob locally and not upload

The steps below creates a local go version with crypto/mldsa and then patches to the various dependencies

cd cosign_mldsa/
git clone --branch go1.26.0 --single-branch --depth 1 https://github.com/golang/go.git goroot
cd goroot
git apply ../go.diff

cd src/
./make.bash

cd ../../
export COSIGN_PASSWORD=
export GOROOT=`pwd`/goroot
export PATH=$GOROOT/bin:$PATH:

$ go version
go version go1.26.0-test linux/amd64

git clone https://github.com/letsencrypt/boulder.git
cd boulder
git fetch origin d2c1c53f7834a50c407a89bce08d3b76620b0c65
git apply ../boulder.diff

cd ../
git clone https://github.com/sigstore/sigstore.git
cd sigstore
git fetch origin c73103207d9c5dc8271d0c431f5b863785a016e1
git apply ../sigstore.diff

cd ../
git clone https://github.com/sigstore/cosign
cd cosign
git fetch origin a09afa97480a0a4a20ad6314600598b7bddc8c0c
git apply ../cosign.diff


cd cosign/

echo -n "foo" > /tmp/message.txt

go run cmd/cosign/main.go import-key-pair --key ../mldsa-private.pem -o ../cosign-mldsa

go run cmd/cosign/main.go sign-blob  --key ../cosign-mldsa.key --signing-config=../signing_config.json --bundle /tmp/artifact.sigstore.json /tmp/message.txt

go run cmd/cosign/main.go  verify-blob --key ../cosign-mldsa.pub --insecure-ignore-tlog=true --insecure-ignore-sct=true  --bundle /tmp/artifact.sigstore.json /tmp/message.txt

The signing config i used is

  • signing_config.json
{"mediaType":"application/vnd.dev.sigstore.signingconfig.v0.2+json","rekorTlogConfig":{},"tsaConfig":{}}

The sample signature output is

  • /tmp/artifact.sigstore.json
{
  "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
  "verificationMaterial": {
    "publicKey": {
      "hint": "YGv8Bfe40wme5ZXynQWUdhFY9I5sNEWYMPIOdBzWbKA="
    }
  },
  "messageSignature": {
    "messageDigest": {
      "algorithm": "SHA3_384",
      "digest": "ZlVRko0Tt9hO4Cc0UCsBjYlqD7h+7VrbTIe6kbvWSJQQ4RsPvMBu19DrrVWeXTu1"
    },
    "signature": "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"
  }
}

mldsa-private.pem

-----BEGIN PRIVATE KEY-----
MDICAQAwCwYJYIZIAWUDBAMSBCAHowT4EhhrJFM1kSB4hf6N625NY91/dx/sAaen
iPJ+nQ==
-----END PRIVATE KEY-----

cosign-mldsa.key

-----BEGIN ENCRYPTED SIGSTORE PRIVATE KEY-----
eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjo2NTUzNiwiciI6
OCwicCI6MX0sInNhbHQiOiJuNDVGZDQvcm1RbWgxbUVoa3lLY0pMNXBVT3hlOFVK
YmFQSmVvTUp5bk5vPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
Iiwibm9uY2UiOiJKOFNPL0Y1MkZacHJSWk5sUVI1NVduMVJZbDQ1OGxEcCJ9LCJj
aXBoZXJ0ZXh0IjoiRjFMOHdoMzhIR1pPakhXRXJKSlloeVJvWUNqM1h3Nlk0NGNh
SmlWakM3am1OU3R1QUpvRnd0MHdsL295K3pMS3h0Y09pQ3hsNlJ2aFBiQXlzV0pa
UUYwVUtYTT0ifQ==
-----END ENCRYPTED SIGSTORE PRIVATE KEY-----

cosign-mldsa.pub mldsa-public.pem

-----BEGIN PUBLIC KEY-----
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
QEpUHgTn
-----END PUBLIC KEY-----

go.diff

diff --git a/VERSION b/VERSION
index ca44274..e9e5f9d 100644
--- a/VERSION
+++ b/VERSION
@@ -1,2 +1,2 @@
-go1.26.0
+go1.26.0-test
 time 2026-02-10T01:22:00Z
diff --git a/src/crypto/crypto.go b/src/crypto/crypto.go
index 0bf9ec8..fbdcacf 100644
--- a/src/crypto/crypto.go
+++ b/src/crypto/crypto.go
@@ -60,6 +60,8 @@ func (h Hash) String() string {
 		return "BLAKE2b-384"
 	case BLAKE2b_512:
 		return "BLAKE2b-512"
+	case MLDSAMu:
+		return "MLDSA-Mu"
 	default:
 		return "unknown hash value " + strconv.Itoa(int(h))
 	}
@@ -86,6 +88,14 @@ const (
 	BLAKE2b_384                 // import golang.org/x/crypto/blake2b
 	BLAKE2b_512                 // import golang.org/x/crypto/blake2b
 	maxHash
+	MLDSAMu
+
+	// MLDSAMu is a function that produces a [pre-hashed μ message representative].
+	// It has no implementation, but is used a [crypto.SignerOpts.HashFunc] return
+	// value for [mldsa.PrivateKey.Sign].
+	//
+	// [pre-hashed μ message representative]: https://www.rfc-editor.org/rfc/rfc9881.html#externalmu
+	//MLDSAMu Hash = 0xABCDEF12
 )
 
 var digestSizes = []uint8{
diff --git a/src/crypto/x509/parser.go b/src/crypto/x509/parser.go
index e255f7d..86db76c 100644
--- a/src/crypto/x509/parser.go
+++ b/src/crypto/x509/parser.go
@@ -10,6 +10,7 @@ import (
 	"crypto/ecdh"
 	"crypto/ecdsa"
 	"crypto/ed25519"
+	"crypto/mldsa"
 	"crypto/rsa"
 	"crypto/x509/pkix"
 	"encoding/asn1"
@@ -336,6 +337,11 @@ func parsePublicKey(keyData *publicKeyInfo) (any, error) {
 			return nil, errors.New("x509: zero or negative DSA parameter")
 		}
 		return pub, nil
+	case oid.Equal(oidPublicKeyMLDSA65):
+		if len(params.FullBytes) != 0 {
+			return nil, errors.New("x509: X25519 key encoded with illegal parameters")
+		}
+		return mldsa.NewPublicKey(mldsa.MLDSA65(), data)
 	default:
 		return nil, errors.New("x509: unknown public key algorithm")
 	}
diff --git a/src/crypto/x509/pkcs8.go b/src/crypto/x509/pkcs8.go
index d0ab573..9b351e8 100644
--- a/src/crypto/x509/pkcs8.go
+++ b/src/crypto/x509/pkcs8.go
@@ -8,6 +8,8 @@ import (
 	"crypto/ecdh"
 	"crypto/ecdsa"
 	"crypto/ed25519"
+
+	"crypto/mldsa"
 	"crypto/rsa"
 	"crypto/x509/pkix"
 	"encoding/asn1"
@@ -88,7 +90,8 @@ func ParsePKCS8PrivateKey(der []byte) (key any, err error) {
 			return nil, fmt.Errorf("x509: invalid X25519 private key: %v", err)
 		}
 		return ecdh.X25519().NewPrivateKey(curvePrivateKey)
-
+	case privKey.Algo.Algorithm.Equal(oidPublicKeyMLDSA65):
+		return mldsa.NewPrivateKey(mldsa.MLDSA65(), privKey.PrivateKey)
 	default:
 		return nil, fmt.Errorf("x509: PKCS#8 wrapping contained private key with unknown algorithm: %v", privKey.Algo.Algorithm)
 	}
@@ -175,6 +178,11 @@ func MarshalPKCS8PrivateKey(key any) ([]byte, error) {
 				return nil, errors.New("x509: failed to marshal EC private key while building PKCS#8: " + err.Error())
 			}
 		}
+	case *mldsa.PrivateKey:
+		privKey.Algo = pkix.AlgorithmIdentifier{
+			Algorithm: oidPublicKeyMLDSA65,
+		}
+		privKey.PrivateKey = k.Bytes()
 
 	default:
 		return nil, fmt.Errorf("x509: unknown key type while marshaling PKCS#8: %T", key)
diff --git a/src/crypto/x509/x509.go b/src/crypto/x509/x509.go
index 7953b61..1c1bbfb 100644
--- a/src/crypto/x509/x509.go
+++ b/src/crypto/x509/x509.go
@@ -27,6 +27,7 @@ import (
 	"crypto/ecdsa"
 	"crypto/ed25519"
 	"crypto/elliptic"
+	"crypto/mldsa"
 	"crypto/rsa"
 	"crypto/sha1"
 	"crypto/sha256"
@@ -132,6 +133,9 @@ func marshalPublicKey(pub any) (publicKeyBytes []byte, publicKeyAlgorithm pkix.A
 			}
 			publicKeyAlgorithm.Parameters.FullBytes = paramBytes
 		}
+	case *mldsa.PublicKey:
+		publicKeyBytes = pub.Bytes()
+		publicKeyAlgorithm.Algorithm = oidPublicKeyMLDSA65
 	default:
 		return nil, pkix.AlgorithmIdentifier{}, fmt.Errorf("x509: unsupported public key type: %T", pub)
 	}
@@ -268,6 +272,7 @@ const (
 	DSA // Only supported for parsing.
 	ECDSA
 	Ed25519
+	MLDSA65
 )
 
 var publicKeyAlgoName = [...]string{
@@ -275,6 +280,7 @@ var publicKeyAlgoName = [...]string{
 	DSA:     "DSA",
 	ECDSA:   "ECDSA",
 	Ed25519: "Ed25519",
+	MLDSA65: "MLDSA65",
 }
 
 func (algo PublicKeyAlgorithm) String() string {
@@ -492,6 +498,8 @@ var (
 	//	id-Ed25519   OBJECT IDENTIFIER ::= { 1 3 101 112 }
 	oidPublicKeyX25519  = asn1.ObjectIdentifier{1, 3, 101, 110}
 	oidPublicKeyEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112}
+
+	oidPublicKeyMLDSA65 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 18}
 )
 
 // getPublicKeyAlgorithmFromOID returns the exposed PublicKeyAlgorithm
@@ -507,6 +515,8 @@ func getPublicKeyAlgorithmFromOID(oid asn1.ObjectIdentifier) PublicKeyAlgorithm
 		return ECDSA
 	case oid.Equal(oidPublicKeyEd25519):
 		return Ed25519
+	case oid.Equal(oidPublicKeyMLDSA65):
+		return MLDSA65
 	}
 	return UnknownPublicKeyAlgorithm
 }
diff --git a/src/crypto/mldsa/mldsa.go b/src/crypto/mldsa/mldsa.go
new file mode 100644
index 0000000..6402565
--- /dev/null
+++ b/src/crypto/mldsa/mldsa.go
@@ -0,0 +1,268 @@
+// Package mldsa implements the post-quantum ML-DSA signature scheme specified
+// in FIPS 204.
+package mldsa
+
+import (
+	"crypto"
+	"crypto/subtle"
+	"errors"
+	"io"
+
+	"crypto/internal/fips140/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 {
+	name       string
+	pubKeySize int
+	sigSize    int
+}
+
+var (
+	mldsa44 = &Parameters{"ML-DSA-44", MLDSA44PublicKeySize, MLDSA44SignatureSize}
+	mldsa65 = &Parameters{"ML-DSA-65", MLDSA65PublicKeySize, MLDSA65SignatureSize}
+	mldsa87 = &Parameters{"ML-DSA-87", MLDSA87PublicKeySize, MLDSA87SignatureSize}
+)
+
+// 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 { return mldsa44 }
+
+// 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 { return mldsa65 }
+
+// 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 { return mldsa87 }
+
+// PublicKeySize returns the size of public keys for this parameter set, in bytes.
+func (params *Parameters) PublicKeySize() int { return params.pubKeySize }
+
+// SignatureSize returns the size of signatures for this parameter set, in bytes.
+func (params *Parameters) SignatureSize() int { return params.sigSize }
+
+// String returns the name of the parameter set, e.g. "ML-DSA-44".
+func (params *Parameters) String() string { return params.name }
+
+// 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 {
+	key *mldsa.PrivateKey
+}
+
+// GenerateKey generates a new random ML-DSA private key.
+func GenerateKey(params *Parameters) (*PrivateKey, error) {
+	switch params {
+	case mldsa44:
+		return &PrivateKey{mldsa.GenerateKey44()}, nil
+	case mldsa65:
+		return &PrivateKey{mldsa.GenerateKey65()}, nil
+	case mldsa87:
+		return &PrivateKey{mldsa.GenerateKey87()}, nil
+	default:
+		return nil, errors.New("mldsa: invalid parameters")
+	}
+}
+
+// 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) {
+	var key *mldsa.PrivateKey
+	var err error
+	switch params {
+	case mldsa44:
+		key, err = mldsa.NewPrivateKey44(seed)
+	case mldsa65:
+		key, err = mldsa.NewPrivateKey65(seed)
+	case mldsa87:
+		key, err = mldsa.NewPrivateKey87(seed)
+	default:
+		return nil, errors.New("mldsa: invalid parameters")
+	}
+	if err != nil {
+		return nil, err
+	}
+	return &PrivateKey{key}, nil
+}
+
+// Public returns the corresponding [PublicKey] for this private key.
+//
+// It implements the [crypto.Signer] interface.
+func (sk *PrivateKey) Public() crypto.PublicKey {
+	return sk.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 {
+	other, ok := x.(*PrivateKey)
+	if !ok {
+		return false
+	}
+	return subtle.ConstantTimeCompare(sk.Bytes(), other.Bytes()) == 1
+}
+
+// PublicKey returns the corresponding [PublicKey] for this private key.
+func (sk *PrivateKey) PublicKey() *PublicKey {
+	return &PublicKey{sk.key.PublicKey()}
+}
+
+// Bytes returns the private key seed.
+func (sk *PrivateKey) Bytes() []byte {
+	return sk.key.Bytes()
+}
+
+// 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 crypto.SignerOpts) (signature []byte, err error) {
+	switch {
+	case opts == nil || opts.HashFunc() == 0:
+		// Sign the message directly.
+		var context string
+		if opts, ok := opts.(*Options); ok {
+			context = opts.Context
+		}
+		return mldsa.Sign(sk.key, message, context)
+	case opts.HashFunc() == crypto.MLDSAMu:
+		// Sign the pre-hashed μ message representative.
+		return mldsa.SignExternalMu(sk.key, message)
+	default:
+		return nil, errors.New("mldsa: invalid SignerOpts.HashFunc")
+	}
+}
+
+// SignDeterministic works like [PrivateKey.Sign], but the signature is
+// deterministic.
+func (sk *PrivateKey) SignDeterministic(message []byte, opts crypto.SignerOpts) (signature []byte, err error) {
+	switch {
+	case opts == nil || opts.HashFunc() == 0:
+		// Sign the message directly.
+		var context string
+		if opts, ok := opts.(*Options); ok {
+			context = opts.Context
+		}
+		return mldsa.SignDeterministic(sk.key, message, context)
+	case opts.HashFunc() == crypto.MLDSAMu:
+		// Sign the pre-hashed μ message representative.
+		return mldsa.SignExternalMuDeterministic(sk.key, message)
+	default:
+		return nil, errors.New("mldsa: invalid SignerOpts.HashFunc")
+	}
+}
+
+// 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 {
+	key *mldsa.PublicKey
+}
+
+// NewPublicKey creates a new ML-DSA public key from the given encoding.
+func NewPublicKey(params *Parameters, seed []byte) (*PublicKey, error) {
+	var key *mldsa.PublicKey
+	var err error
+	switch params {
+	case mldsa44:
+		key, err = mldsa.NewPublicKey44(seed)
+	case mldsa65:
+		key, err = mldsa.NewPublicKey65(seed)
+	case mldsa87:
+		key, err = mldsa.NewPublicKey87(seed)
+	default:
+		return nil, errors.New("mldsa: invalid parameters")
+	}
+	if err != nil {
+		return nil, err
+	}
+	return &PublicKey{key}, nil
+}
+
+// Bytes returns the public key encoding.
+func (pk *PublicKey) Bytes() []byte {
+	return pk.key.Bytes()
+}
+
+// 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 {
+	other, ok := x.(*PublicKey)
+	if !ok {
+		return false
+	}
+	return subtle.ConstantTimeCompare(pk.Bytes(), other.Bytes()) == 1
+}
+
+// Parameters returns the parameters associated with this public key.
+func (pk *PublicKey) Parameters() *Parameters {
+	switch pk.key.Parameters() {
+	case "ML-DSA-44":
+		return mldsa44
+	case "ML-DSA-65":
+		return mldsa65
+	case "ML-DSA-87":
+		return mldsa87
+	default:
+		panic("mldsa: internal error: invalid parameters")
+	}
+}
+
+// Verify reports whether signature is a valid signature of message by pk.
+func Verify(pk *PublicKey, message []byte, signature []byte, opts *Options) error {
+	var context string
+	if opts != nil {
+		context = opts.Context
+	}
+	return mldsa.Verify(pk.key, message, signature, context)
+}
+
+// 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.Hash {
+	return 0
+}

sigstore.diff

diff --git a/go.mod b/go.mod
index 27c0599..d917f82 100644
--- a/go.mod
+++ b/go.mod
@@ -20,6 +20,8 @@ require (
 	golang.org/x/term v0.40.0
 )
 
+replace github.com/letsencrypt/boulder => ../boulder
+
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
diff --git a/go.sum b/go.sum
index 7cdeff4..7f66c79 100644
--- a/go.sum
+++ b/go.sum
@@ -54,8 +54,6 @@ github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/letsencrypt/boulder v0.20260223.0 h1:xdS2OnJNUasR6TgVIOpqqcvdkOu47+PQQMBk9ThuWBw=
-github.com/letsencrypt/boulder v0.20260223.0/go.mod h1:r3aTSA7UZ7dbDfiGK+HLHJz0bWNbHk6YSPiXgzl23sA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
diff --git a/pkg/cryptoutils/goodkey/publickey.go b/pkg/cryptoutils/goodkey/publickey.go
index 8c23157..a56faf5 100644
--- a/pkg/cryptoutils/goodkey/publickey.go
+++ b/pkg/cryptoutils/goodkey/publickey.go
@@ -21,6 +21,7 @@ import (
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/ed25519"
+	"crypto/mldsa"
 	"crypto/rsa" // nolint:gosec
 	"errors"
 	"fmt"
@@ -49,6 +50,7 @@ func ValidatePubKey(pub crypto.PublicKey) error {
 		ECDSAP256: true,
 		ECDSAP384: true,
 		ECDSAP521: true,
+		MLDSA65:   true,
 	}
 	cfg := &goodkey.Config{
 		FermatRounds: 100,
@@ -61,6 +63,7 @@ func ValidatePubKey(pub crypto.PublicKey) error {
 		return errors.New("unable to initialize key policy")
 	}
 
+	fmt.Println()
 	switch pk := pub.(type) {
 	case *rsa.PublicKey:
 		// ctx is unused
@@ -70,6 +73,8 @@ func ValidatePubKey(pub crypto.PublicKey) error {
 		return p.GoodKey(context.Background(), pub)
 	case ed25519.PublicKey:
 		return validateEd25519Key(pk)
+	case *mldsa.PublicKey:
+		return nil //validateEd25519Key(pk)
 	}
 	return fmt.Errorf("unsupported public key type: %T", pub)
 }
diff --git a/pkg/signature/algorithm_registry.go b/pkg/signature/algorithm_registry.go
index a348051..2f01a1a 100644
--- a/pkg/signature/algorithm_registry.go
+++ b/pkg/signature/algorithm_registry.go
@@ -20,6 +20,7 @@ import (
 	"crypto/ecdsa"
 	"crypto/ed25519"
 	"crypto/elliptic"
+	"crypto/mldsa"
 	"crypto/rsa"
 	"errors"
 	"fmt"
@@ -37,6 +38,8 @@ const (
 	ECDSA
 	// ED25519 public key
 	ED25519
+	// MLDSA65 public key
+	MLDSA65
 )
 
 // RSAKeySize represents the size of an RSA public key in bits.
@@ -165,6 +168,8 @@ var supportedAlgorithms = []AlgorithmDetails{
 	{v1.PublicKeyDetails_PKIX_ECDSA_P521_SHA_256, ECDSA, crypto.SHA256, v1.HashAlgorithm_SHA2_256, elliptic.P521(), "ecdsa-sha2-256-nistp521"}, //nolint:staticcheck
 	{v1.PublicKeyDetails_PKIX_ED25519, ED25519, crypto.Hash(0), v1.HashAlgorithm_HASH_ALGORITHM_UNSPECIFIED, nil, "ed25519"},
 	{v1.PublicKeyDetails_PKIX_ED25519_PH, ED25519, crypto.SHA512, v1.HashAlgorithm_SHA2_512, nil, "ed25519-ph"},
+
+	{v1.PublicKeyDetails_ML_DSA_65, MLDSA65, crypto.SHA3_256, v1.HashAlgorithm_SHA3_256, nil, "mldsa65-sha3-256"},
 }
 
 // AlgorithmRegistryConfig represents a set of permitted algorithms for a given Sigstore service or component.
@@ -293,6 +298,9 @@ func GetDefaultPublicKeyDetails(publicKey crypto.PublicKey, opts ...LoadOption)
 			return v1.PublicKeyDetails_PKIX_ED25519_PH, nil
 		}
 		return v1.PublicKeyDetails_PKIX_ED25519, nil
+
+	case *mldsa.PublicKey:
+		return v1.PublicKeyDetails_ML_DSA_65, nil
 	}
 	return v1.PublicKeyDetails_PUBLIC_KEY_DETAILS_UNSPECIFIED, errors.New("unsupported public key type")
 }
diff --git a/pkg/signature/mldsa.go b/pkg/signature/mldsa.go
new file mode 100644
index 0000000..7cfc053
--- /dev/null
+++ b/pkg/signature/mldsa.go
@@ -0,0 +1,148 @@
+//
+// Copyright 2021 The Sigstore Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package signature
+
+import (
+	"crypto"
+	"crypto/mldsa"
+	"crypto/sha3"
+	"errors"
+	"fmt"
+	"io"
+)
+
+type MLDSA65Signer struct {
+	hashFunc crypto.Hash
+	priv     *mldsa.PrivateKey
+}
+
+type MLDSA65Verifier struct {
+	publicKey *mldsa.PublicKey
+	hashFunc  crypto.Hash
+}
+
+type MLDSA65SignerVerifier struct {
+	*MLDSA65Signer
+	*MLDSA65Verifier
+}
+
+func (r MLDSA65SignerVerifier) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+	return r.publicKey, nil
+}
+
+// computeMu computes the μ message representative as specified in FIPS 204.
+// μ = SHAKE256(tr || 0x00 || len(ctx) || ctx || msg), where
+// tr = SHAKE256(publicKeyBytes) is 64 bytes.
+func computeMu(pk *mldsa.PublicKey, msg []byte, context string) []byte {
+	H := sha3.NewSHAKE256()
+	H.Write(pk.Bytes())
+	var tr [64]byte
+	H.Read(tr[:])
+
+	H.Reset()
+	H.Write(tr[:])
+	H.Write([]byte{0x00}) // ML-DSA domain separator
+	H.Write([]byte{byte(len(context))})
+	H.Write([]byte(context))
+	H.Write(msg)
+	mu := make([]byte, 64)
+	H.Read(mu)
+	return mu
+}
+
+func (r MLDSA65Signer) SignMessage(message io.Reader, opts ...SignOption) ([]byte, error) {
+
+	messageBytes, err := io.ReadAll(message)
+	if err != nil {
+		return nil, fmt.Errorf("error reading bytes for signature: %w", err)
+	}
+	mu := computeMu(r.priv.PublicKey(), messageBytes, "")
+
+	return r.priv.Sign(nil, mu, SignerOpts{
+		Hash: crypto.MLDSAMu,
+	})
+}
+
+func (r MLDSA65Verifier) PublicKey(_ ...PublicKeyOption) (crypto.PublicKey, error) {
+	return r.publicKey, nil
+}
+
+func (r MLDSA65Verifier) VerifySignature(signature, message io.Reader, opts ...VerifyOption) error {
+
+	if signature == nil {
+		return errors.New("nil signature passed to VerifySignature")
+	}
+
+	messageBytes, err := io.ReadAll(message)
+	if err != nil {
+		return fmt.Errorf("reading signature: %w", err)
+	}
+
+	sigBytes, err := io.ReadAll(signature)
+	if err != nil {
+		return fmt.Errorf("reading signature: %w", err)
+	}
+
+	return mldsa.Verify(r.publicKey, messageBytes, sigBytes, &mldsa.Options{
+		Context: "",
+	})
+}
+
+func LoadMLDSA65Signer(priv *mldsa.PrivateKey, hf crypto.Hash) (*MLDSA65Signer, error) {
+	if priv == nil {
+		return nil, errors.New("invalid MLDSA65 private key specified")
+	}
+
+	// if !isSupportedAlg(hf, mldsaSupportedHashFuncs) {
+	// 	return nil, errors.New("invalid hash function specified")
+	// }
+
+	return &MLDSA65Signer{
+		priv:     priv,
+		hashFunc: hf,
+	}, nil
+}
+
+func LoadMLDSA65Verifier(pub *mldsa.PublicKey, hashFunc crypto.Hash) (*MLDSA65Verifier, error) {
+	if pub == nil {
+		return nil, errors.New("invalid MLDSA65 public key specified")
+	}
+
+	// if !isSupportedAlg(hashFunc, rsaSupportedHashFuncs) {
+	// 	return nil, errors.New("invalid hash function specified")
+	// }
+
+	return &MLDSA65Verifier{
+		publicKey: pub,
+		hashFunc:  hashFunc,
+	}, nil
+}
+
+func LoadMLDSA65SignerVerifier(priv *mldsa.PrivateKey, hf crypto.Hash) (*MLDSA65SignerVerifier, error) {
+	signer, err := LoadMLDSA65Signer(priv, hf)
+	if err != nil {
+		return nil, fmt.Errorf("initializing signer: %w", err)
+	}
+	verifier, err := LoadMLDSA65Verifier(priv.PublicKey(), hf)
+	if err != nil {
+		return nil, fmt.Errorf("initializing verifier: %w", err)
+	}
+
+	return &MLDSA65SignerVerifier{
+		MLDSA65Signer:   signer,
+		MLDSA65Verifier: verifier,
+	}, nil
+}
diff --git a/pkg/signature/signerverifier.go b/pkg/signature/signerverifier.go
index 9ff9342..ef6693e 100644
--- a/pkg/signature/signerverifier.go
+++ b/pkg/signature/signerverifier.go
@@ -19,8 +19,10 @@ import (
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/ed25519"
+	"crypto/mldsa"
 	"crypto/rsa"
 	"errors"
+
 	"os"
 	"path/filepath"
 
@@ -68,6 +70,9 @@ func LoadSignerVerifierWithOpts(privateKey crypto.PrivateKey, opts ...LoadOption
 			return LoadED25519phSignerVerifier(pk)
 		}
 		return LoadED25519SignerVerifier(pk)
+	case *mldsa.PrivateKey:
+		hashFunc = crypto.SHA3_256
+		return LoadMLDSA65SignerVerifier(pk, hashFunc)
 	}
 	return nil, errors.New("unsupported public key type")
 }
diff --git a/pkg/signature/verifier.go b/pkg/signature/verifier.go
index 0b5a1bb..742baf3 100644
--- a/pkg/signature/verifier.go
+++ b/pkg/signature/verifier.go
@@ -19,6 +19,7 @@ import (
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/ed25519"
+	"crypto/mldsa"
 	"crypto/rsa"
 	"errors"
 	"io"
@@ -69,8 +70,10 @@ func LoadVerifierWithOpts(publicKey crypto.PublicKey, opts ...LoadOption) (Verif
 			return LoadED25519phVerifier(pk)
 		}
 		return LoadED25519Verifier(pk)
+	case *mldsa.PublicKey:
+		return LoadMLDSA65Verifier(pk, hashFunc)
 	}
-	return nil, errors.New("unsupported public key type")
+	return nil, errors.New("vv unsupported public key type")
 }
 
 // LoadUnsafeVerifier returns a signature.Verifier based on the algorithm of the public key

boulder.diff

diff --git a/goodkey/good_key.go b/goodkey/good_key.go
index d8efd703d..d17ffb65b 100644
--- a/goodkey/good_key.go
+++ b/goodkey/good_key.go
@@ -5,6 +5,7 @@ import (
 	"crypto"
 	"crypto/ecdsa"
 	"crypto/elliptic"
+	"crypto/mldsa"
 	"crypto/rsa"
 	"errors"
 	"fmt"
@@ -66,6 +67,8 @@ type AllowedKeys struct {
 	ECDSAP256 bool
 	ECDSAP384 bool
 	ECDSAP521 bool
+
+	MLDSA65 bool
 }
 
 // LetsEncryptCPS encodes the five key algorithms and sizes allowed by the Let's
@@ -80,6 +83,7 @@ func LetsEncryptCPS() AllowedKeys {
 		RSA4096:   true,
 		ECDSAP256: true,
 		ECDSAP384: true,
+		MLDSA65:   true,
 	}
 }
 
@@ -165,6 +169,8 @@ func (policy *KeyPolicy) GoodKey(ctx context.Context, key crypto.PublicKey) erro
 		return policy.goodKeyRSA(t)
 	case *ecdsa.PublicKey:
 		return policy.goodKeyECDSA(t)
+	case *mldsa.PublicKey:
+		return nil //policy.goodKeyECDSA(t)
 	default:
 		return badKey("unsupported key type %T", key)
 	}

cosign.diff

diff --git a/cmd/cosign/cli/options/signature_digest.go b/cmd/cosign/cli/options/signature_digest.go
index dda0ddd0..d13c8653 100644
--- a/cmd/cosign/cli/options/signature_digest.go
+++ b/cmd/cosign/cli/options/signature_digest.go
@@ -18,6 +18,7 @@ package options
 import (
 	"crypto"
 	_ "crypto/sha256" // for sha224 + sha256
+	_ "crypto/sha3"   // for sha3-256 + sha3-384 + sha3-512
 	_ "crypto/sha512" // for sha384 + sha512
 	"fmt"
 	"sort"
@@ -27,10 +28,11 @@ import (
 )
 
 var supportedSignatureAlgorithms = map[string]crypto.Hash{
-	"sha224": crypto.SHA224,
-	"sha256": crypto.SHA256,
-	"sha384": crypto.SHA384,
-	"sha512": crypto.SHA512,
+	"sha224":   crypto.SHA224,
+	"sha256":   crypto.SHA256,
+	"sha384":   crypto.SHA384,
+	"sha512":   crypto.SHA512,
+	"sha3_256": crypto.SHA3_256,
 }
 
 func supportedSignatureAlgorithmNames() []string {
diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go
index b84cdc93..9c933f0e 100644
--- a/cmd/cosign/cli/sign/sign_blob.go
+++ b/cmd/cosign/cli/sign/sign_blob.go
@@ -85,7 +85,6 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt
 	if err != nil {
 		return nil, fmt.Errorf("getting keypair and token: %w", err)
 	}
-
 	hashFunction := protoHashAlgoToHash(keypair.GetHashAlgorithm())
 	payload, closePayload, err := getPayload(ctx, payloadPath, hashFunction)
 	if err != nil {
@@ -289,6 +288,9 @@ func hashFuncToProtoBundle(hashFunc crypto.Hash) protocommon.HashAlgorithm {
 		return protocommon.HashAlgorithm_SHA2_384
 	case crypto.SHA512:
 		return protocommon.HashAlgorithm_SHA2_512
+	case crypto.SHA3_384:
+		return protocommon.HashAlgorithm_SHA3_384
+
 	default:
 		return protocommon.HashAlgorithm_HASH_ALGORITHM_UNSPECIFIED
 	}
@@ -302,6 +304,10 @@ func protoHashAlgoToHash(hashFunc protocommon.HashAlgorithm) crypto.Hash {
 		return crypto.SHA384
 	case protocommon.HashAlgorithm_SHA2_512:
 		return crypto.SHA512
+	case protocommon.HashAlgorithm_SHA3_256:
+		return crypto.SHA3_256
+	case protocommon.HashAlgorithm_SHA3_384:
+		return crypto.SHA3_384
 	default:
 		return crypto.Hash(0)
 	}
diff --git a/go.mod b/go.mod
index 032ef826..09752b87 100644
--- a/go.mod
+++ b/go.mod
@@ -66,6 +66,11 @@ require (
 	sigs.k8s.io/release-utils v0.12.3
 )
 
+replace (
+	github.com/letsencrypt/boulder => ../boulder
+	github.com/sigstore/sigstore => ../sigstore
+)
+
 require (
 	cloud.google.com/go v0.123.0 // indirect
 	cloud.google.com/go/auth v0.18.1 // indirect
@@ -217,7 +222,7 @@ require (
 	github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect
 	github.com/lestrrat-go/jwx/v3 v3.0.13 // indirect
 	github.com/lestrrat-go/option/v2 v2.0.0 // indirect
-	github.com/letsencrypt/boulder v0.20251110.0 // indirect
+	github.com/letsencrypt/boulder v0.20260223.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
diff --git a/go.sum b/go.sum
index 4e4f482b..3ac358db 100644
--- a/go.sum
+++ b/go.sum
@@ -519,8 +519,6 @@ github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0
 github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
 github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
 github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
-github.com/letsencrypt/boulder v0.20251110.0 h1:J8MnKICeilO91dyQ2n5eBbab24neHzUpYMUIOdOtbjc=
-github.com/letsencrypt/boulder v0.20251110.0/go.mod h1:ogKCJQwll82m7OVHWyTuf8eeFCjuzdRQlgnZcCl0V+8=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
@@ -529,8 +527,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
-github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
+github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
+github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
 github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
 github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ=
 github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
@@ -644,8 +642,6 @@ github.com/sigstore/rekor v1.5.0 h1:rL7SghHd5HLCtsCrxw0yQg+NczGvM75EjSPPWuGjaiQ=
 github.com/sigstore/rekor v1.5.0/go.mod h1:D7JoVCUkxwQOpPDNYeu+CE8zeBC18Y5uDo6tF8s2rcQ=
 github.com/sigstore/rekor-tiles/v2 v2.2.1 h1:UmV1CBQ3SjxxPGpFmwDoOhoIwiKpM2Qm1pU5tPGmvNk=
 github.com/sigstore/rekor-tiles/v2 v2.2.1/go.mod h1:z8n6l6oidpaLjjE6rJERuQqY9X38ulnHZCXyL+DEL7U=
-github.com/sigstore/sigstore v1.10.4 h1:ytOmxMgLdcUed3w1SbbZOgcxqwMG61lh1TmZLN+WeZE=
-github.com/sigstore/sigstore v1.10.4/go.mod h1:tDiyrdOref3q6qJxm2G+JHghqfmvifB7hw+EReAfnbI=
 github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg=
 github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg=
 github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.4 h1:VZ+L6SKVWbLPHznIF0tBuO7qKMFdJiJMVwFKu9DlY5o=
diff --git a/internal/key/svkeypair.go b/internal/key/svkeypair.go
index eee7d2ea..5c7c97b1 100644
--- a/internal/key/svkeypair.go
+++ b/internal/key/svkeypair.go
@@ -27,6 +27,8 @@ import (
 	"errors"
 	"fmt"
 
+	"crypto/mldsa"
+
 	"github.com/sigstore/cosign/v3/pkg/cosign"
 	protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
@@ -64,6 +66,8 @@ func NewSignerVerifierKeypair(sv signature.SignerVerifier, defaultLoadOptions *[
 		keyAlg = "RSA"
 	case ed25519.PublicKey:
 		keyAlg = "ED25519"
+	case *mldsa.PublicKey:
+		keyAlg = "MLDSA65"
 	default:
 		return nil, errors.New("unsupported key type")
 	}
@@ -72,7 +76,6 @@ func NewSignerVerifierKeypair(sv signature.SignerVerifier, defaultLoadOptions *[
 	if err != nil {
 		return nil, fmt.Errorf("getting default algorithm details: %w", err)
 	}
-
 	return &SignerVerifierKeypair{
 		sv:     sv,
 		hint:   hint,
diff --git a/pkg/cosign/keys.go b/pkg/cosign/keys.go
index 7493782c..b196c321 100644
--- a/pkg/cosign/keys.go
+++ b/pkg/cosign/keys.go
@@ -22,6 +22,7 @@ import (
 	"crypto/rand"
 	"crypto/rsa"
 	_ "crypto/sha256" // for `crypto.SHA256`
+	_ "crypto/sha3"   // for sha3-256 + sha3-384 + sha3-512
 	"crypto/x509"
 	"encoding/pem"
 	"errors"
@@ -30,6 +31,8 @@ import (
 	"path/filepath"
 	"sort"
 
+	"crypto/mldsa" //"filippo.io/mldsa"
+
 	"github.com/secure-systems-lab/go-securesystemslib/encrypted"
 	"github.com/sigstore/cosign/v3/pkg/oci/static"
 	v1 "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
@@ -59,6 +62,7 @@ var SupportedKeyDetails = []v1.PublicKeyDetails{
 	v1.PublicKeyDetails_PKIX_RSA_PKCS1V15_2048_SHA256,
 	v1.PublicKeyDetails_PKIX_RSA_PKCS1V15_3072_SHA256,
 	v1.PublicKeyDetails_PKIX_RSA_PKCS1V15_4096_SHA256,
+	v1.PublicKeyDetails_ML_DSA_65,
 	// Ed25519ph is not supported by Fulcio, so we don't support it here for now.
 	// v1.PublicKeyDetails_PKIX_ED25519_PH,
 }
@@ -187,6 +191,11 @@ func ImportKeyPair(keyPath string, pf PassFunc) (*KeysBytes, error) {
 				return nil, fmt.Errorf("error validating ed25519 key: %w", err)
 			}
 			pk = k
+		case *mldsa.PrivateKey:
+			if err = goodkey.ValidatePubKey(k.Public()); err != nil {
+				return nil, fmt.Errorf("error validating mldsa key: %w", err)
+			}
+			pk = k
 		default:
 			return nil, fmt.Errorf("unexpected private key")
 		}

Note that the mldsa will read the entire message into memory to do the signing. MLDSA does allow you to provide an externally generated hash which happens to be implemented inline in this sample.

// computeMu computes the μ message representative as specified in FIPS 204.
// μ = SHAKE256(tr || 0x00 || len(ctx) || ctx || msg), where
// tr = SHAKE256(publicKeyBytes) is 64 bytes.
func computeMu(pk *mldsa.PublicKey, msg []byte, context string) []byte {
	H := sha3.NewSHAKE256()
	H.Write(pk.Bytes())
	var tr [64]byte
	H.Read(tr[:])

	H.Reset()
	H.Write(tr[:])
	H.Write([]byte{0x00}) // ML-DSA domain separator
	H.Write([]byte{byte(len(context))})
	H.Write([]byte(context))
	H.Write(msg)
	mu := make([]byte, 64)
	H.Read(mu)
	return mu
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment