| // Copyright 2026 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| //go:build !fips140v1.0 |
| |
| package mldsa_test |
| |
| import ( |
| "bytes" |
| "crypto" |
| "crypto/internal/cryptotest/wycheproof" |
| internalmldsa "crypto/internal/fips140/mldsa" |
| "crypto/mldsa" |
| "slices" |
| "testing" |
| ) |
| |
| // TestVerifyWycheproof test signature verification using the public |
| // mldsa API. |
| func TestVerifyWycheproof(t *testing.T) { |
| for _, file := range []string{ |
| "mldsa_44_verify_test.json", |
| "mldsa_65_verify_test.json", |
| "mldsa_87_verify_test.json", |
| } { |
| var testdata wycheproof.MldsaVerifySchemaJson |
| wycheproof.LoadVectorFile(t, file, &testdata) |
| |
| params := paramsForAlg(testdata.Algorithm) |
| |
| for _, tg := range testdata.TestGroups { |
| publicKey := wycheproof.MustDecodeHex(tg.PublicKey) |
| |
| for _, tv := range tg.Tests { |
| t.Run(wycheproof.TestName(file, tv), func(t *testing.T) { |
| t.Parallel() |
| |
| shouldPass := wycheproof.ShouldPass(t, tv.Result, tv.Flags, nil) |
| |
| pub, err := mldsa.NewPublicKey(params, publicKey) |
| if err != nil { |
| if shouldPass { |
| t.Fatalf("NewPublicKey: %v", err) |
| } |
| return |
| } |
| |
| if !bytes.Equal(pub.Bytes(), publicKey) { |
| t.Errorf("public key roundtrip mismatch") |
| } |
| |
| msg := wycheproof.MustDecodeHex(tv.Msg) |
| sig := wycheproof.MustDecodeHex(tv.Sig) |
| opts := new(mldsa.Options) |
| if tv.Ctx != nil { |
| opts.Context = string(wycheproof.MustDecodeHex(*tv.Ctx)) |
| } |
| |
| err = mldsa.Verify(pub, msg, sig, opts) |
| if shouldPass && err != nil { |
| t.Errorf("Verify: %v", err) |
| } |
| if !shouldPass && err == nil { |
| t.Errorf("Verify should have failed") |
| } |
| }) |
| } |
| } |
| } |
| } |
| |
| // TestSignSeedWycheproof tests key generation and signature creation using |
| // the public mldsa API for seed private key inputs. |
| // |
| // It covers deterministic signature creation with and without pre-hashed mu. |
| func TestSignSeedWycheproof(t *testing.T) { |
| // We don't include the mldsa_*_sign_noseed_test.json test vector files. |
| // Semi-expanded keys are not supported with the public API. |
| for _, file := range []string{ |
| "mldsa_44_sign_seed_test.json", |
| "mldsa_65_sign_seed_test.json", |
| "mldsa_87_sign_seed_test.json", |
| } { |
| var testdata wycheproof.MldsaSignSeedSchemaJson |
| wycheproof.LoadVectorFile(t, file, &testdata) |
| |
| params := paramsForAlg(testdata.Algorithm) |
| |
| for _, tg := range testdata.TestGroups { |
| seed := wycheproof.MustDecodeHex(tg.PrivateSeed) |
| var expectedPublicKey []byte |
| if pk, ok := tg.PublicKey.(string); ok { |
| expectedPublicKey = wycheproof.MustDecodeHex(pk) |
| } |
| |
| for _, tv := range tg.Tests { |
| t.Run(wycheproof.TestName(file, tv), func(t *testing.T) { |
| t.Parallel() |
| |
| shouldPass := wycheproof.ShouldPass(t, tv.Result, tv.Flags, nil) |
| |
| priv, err := mldsa.NewPrivateKey(params, seed) |
| if err != nil { |
| if shouldPass { |
| t.Fatalf("NewPrivateKey: %v", err) |
| } |
| return |
| } |
| // By checking the derived public key is equal to the vector's |
| // provided public key the 'sign' vectors double as key |
| // generation vectors. |
| if expectedPublicKey != nil && !bytes.Equal(priv.PublicKey().Bytes(), expectedPublicKey) { |
| t.Fatalf("public key mismatch") |
| } |
| |
| if slices.Contains(tv.Flags, "Randomized") { |
| t.Skipf("randomized signatures not supported with public API") |
| } |
| |
| runSignTest(t, priv, tv, shouldPass) |
| }) |
| } |
| } |
| } |
| } |
| |
| func runSignTest(t *testing.T, priv *mldsa.PrivateKey, tv wycheproof.MlDsaSignTestVector, shouldPass bool) { |
| t.Helper() |
| |
| var msg, μ []byte |
| opts := new(mldsa.Options) |
| if tv.Msg != nil { |
| msg = wycheproof.MustDecodeHex(*tv.Msg) |
| if tv.Ctx != nil { |
| opts.Context = string(wycheproof.MustDecodeHex(*tv.Ctx)) |
| } |
| } |
| if tv.Mu != nil && *tv.Mu != "" { |
| μ = wycheproof.MustDecodeHex(*tv.Mu) |
| } |
| if msg == nil && μ == nil { |
| t.Fatalf("test vector has neither msg nor mu") |
| } |
| |
| var sigMsg, sigMu []byte |
| var errMsg, errMu error |
| if msg != nil { |
| sigMsg, errMsg = priv.SignDeterministic(msg, opts) |
| } |
| if μ != nil { |
| sigMu, errMu = priv.SignDeterministic(μ, crypto.MLDSAMu) |
| } |
| |
| for _, e := range []error{errMsg, errMu} { |
| if e != nil { |
| if shouldPass { |
| t.Fatalf("Sign: %v", e) |
| } |
| return |
| } |
| } |
| if !shouldPass { |
| t.Errorf("Sign unexpectedly succeeded") |
| return |
| } |
| |
| expectedSig := wycheproof.MustDecodeHex(tv.Sig) |
| sig := sigMsg |
| if sig == nil { |
| sig = sigMu |
| } |
| if sigMsg != nil && sigMu != nil && !bytes.Equal(sigMsg, sigMu) { |
| t.Errorf("Sign(msg, ctx) and SignExternalMu(mu) disagree") |
| } |
| if !bytes.Equal(sig, expectedSig) { |
| t.Errorf("signature mismatch") |
| } |
| |
| pub := priv.PublicKey() |
| if msg != nil { |
| if err := mldsa.Verify(pub, msg, sig, opts); err != nil { |
| t.Errorf("Verify of own signature failed: %v", err) |
| } |
| } |
| // note: we can't round-trip verify external-mu signatures with the public API. |
| // but if that capability were exposed in the future we could check here for |
| // mu != nil. |
| } |
| |
| // TestMLDSASignSeedRandomizedWycheproof tests randomized signing with the |
| // internal testing-only API. |
| // |
| // It covers randomized signature creation with and without pre-hashed mu. |
| func TestMLDSASignSeedRandomizedWycheproof(t *testing.T) { |
| for _, file := range []string{ |
| "mldsa_44_sign_seed_test.json", |
| "mldsa_65_sign_seed_test.json", |
| "mldsa_87_sign_seed_test.json", |
| } { |
| var testdata wycheproof.MldsaSignSeedSchemaJson |
| wycheproof.LoadVectorFile(t, file, &testdata) |
| |
| newPriv := newPrivateKeyFromSeedFn(t, testdata.Algorithm) |
| |
| for _, tg := range testdata.TestGroups { |
| seed := wycheproof.MustDecodeHex(tg.PrivateSeed) |
| var expectedPublicKey []byte |
| if pk, ok := tg.PublicKey.(string); ok { |
| expectedPublicKey = wycheproof.MustDecodeHex(pk) |
| } |
| |
| for _, tv := range tg.Tests { |
| t.Run(wycheproof.TestName(file, tv), func(t *testing.T) { |
| t.Parallel() |
| |
| shouldPass := wycheproof.ShouldPass(t, tv.Result, tv.Flags, nil) |
| priv, err := newPriv(seed) |
| if err != nil { |
| if shouldPass { |
| t.Fatalf("NewPrivateKey: %v", err) |
| } |
| return |
| } |
| |
| // By checking the derived public key is equal to the vector's |
| // provided public key the 'sign' vectors double as key |
| // generation vectors. |
| if expectedPublicKey != nil && !bytes.Equal(priv.PublicKey().Bytes(), expectedPublicKey) { |
| t.Fatalf("public key mismatch") |
| } |
| |
| runRandomizedSignTest(t, priv, tv, shouldPass) |
| }) |
| } |
| } |
| } |
| } |
| |
| func runRandomizedSignTest(t *testing.T, priv *internalmldsa.PrivateKey, tv wycheproof.MlDsaSignTestVector, shouldPass bool) { |
| t.Helper() |
| |
| var msg, μ []byte |
| var ctx string |
| rnd := make([]byte, 32) |
| |
| if tv.Msg != nil { |
| msg = wycheproof.MustDecodeHex(*tv.Msg) |
| if tv.Ctx != nil { |
| ctx = string(wycheproof.MustDecodeHex(*tv.Ctx)) |
| } |
| } |
| if tv.Mu != nil && *tv.Mu != "" { |
| μ = wycheproof.MustDecodeHex(*tv.Mu) |
| } |
| if tv.Rnd != nil && *tv.Rnd != "" { |
| rnd = wycheproof.MustDecodeHex(*tv.Rnd) |
| } |
| |
| if msg == nil && μ == nil { |
| t.Fatalf("test vector has neither msg nor mu") |
| } |
| |
| var sigMsg, sigMu []byte |
| var errMsg, errMu error |
| if msg != nil { |
| sigMsg, errMsg = internalmldsa.TestingOnlySignWithRandom(priv, msg, ctx, rnd) |
| } |
| if μ != nil { |
| sigMu, errMu = internalmldsa.TestingOnlySignExternalMuWithRandom(priv, μ, rnd) |
| } |
| |
| for _, e := range []error{errMsg, errMu} { |
| if e != nil { |
| if shouldPass { |
| t.Fatalf("Sign: %v", e) |
| } |
| return |
| } |
| } |
| if !shouldPass { |
| t.Errorf("Sign unexpectedly succeeded") |
| return |
| } |
| |
| expectedSig := wycheproof.MustDecodeHex(tv.Sig) |
| sig := sigMsg |
| if sig == nil { |
| sig = sigMu |
| } |
| if sigMsg != nil && sigMu != nil && !bytes.Equal(sigMsg, sigMu) { |
| t.Errorf("Sign(msg, ctx) and SignExternalMu(mu) disagree") |
| } |
| if !bytes.Equal(sig, expectedSig) { |
| t.Errorf("signature mismatch") |
| } |
| |
| pub := priv.PublicKey() |
| if msg != nil { |
| if err := internalmldsa.Verify(pub, msg, sig, ctx); err != nil { |
| t.Errorf("Verify of own signature failed: %v", err) |
| } |
| } |
| if μ != nil { |
| if err := internalmldsa.VerifyExternalMu(pub, μ, sig); err != nil { |
| t.Errorf("VerifyExternalMu of own signature failed: %v", err) |
| } |
| } |
| } |
| |
| // TestMLDSANoSeedWycheproof tests semi-expanded private key inputs |
| // derive the correct public key using the internal testing-only API. |
| // |
| // We don't perform further signature signing operations as this is covered |
| // by the seed-form TestSignSeedWycheproof. |
| func TestMLDSANoSeedWycheproof(t *testing.T) { |
| for _, file := range []string{ |
| "mldsa_44_sign_noseed_test.json", |
| "mldsa_65_sign_noseed_test.json", |
| "mldsa_87_sign_noseed_test.json", |
| } { |
| var testdata wycheproof.MldsaSignNoseedSchemaJson |
| wycheproof.LoadVectorFile(t, file, &testdata) |
| |
| for _, tg := range testdata.TestGroups { |
| privateKey := wycheproof.MustDecodeHex(tg.PrivateKey) |
| var expectedPublicKey []byte |
| if pk, ok := tg.PublicKey.(string); ok { |
| expectedPublicKey = wycheproof.MustDecodeHex(pk) |
| } |
| |
| for _, tv := range tg.Tests { |
| t.Run(wycheproof.TestName(file, tv), func(t *testing.T) { |
| t.Parallel() |
| |
| shouldPass := wycheproof.ShouldPass(t, tv.Result, tv.Flags, nil) |
| priv, err := internalmldsa.TestingOnlyNewPrivateKeyFromSemiExpanded(privateKey) |
| if err != nil { |
| if shouldPass { |
| t.Fatalf("TestingOnlyNewPrivateKeyFromSemiExpanded: %v", err) |
| } |
| return |
| } |
| |
| if expectedPublicKey != nil && !bytes.Equal(priv.PublicKey().Bytes(), expectedPublicKey) { |
| t.Fatalf("public key mismatch") |
| } |
| }) |
| } |
| } |
| } |
| } |
| |
| func paramsForAlg(algorithm string) mldsa.Parameters { |
| switch algorithm { |
| case "ML-DSA-44": |
| return mldsa.MLDSA44() |
| case "ML-DSA-65": |
| return mldsa.MLDSA65() |
| case "ML-DSA-87": |
| return mldsa.MLDSA87() |
| } |
| panic("unknown algorithm: " + algorithm) |
| } |
| |
| func newPrivateKeyFromSeedFn(t *testing.T, algorithm string) func([]byte) (*internalmldsa.PrivateKey, error) { |
| switch algorithm { |
| case "ML-DSA-44": |
| return internalmldsa.NewPrivateKey44 |
| case "ML-DSA-65": |
| return internalmldsa.NewPrivateKey65 |
| case "ML-DSA-87": |
| return internalmldsa.NewPrivateKey87 |
| } |
| t.Fatalf("unknown algorithm: %s", algorithm) |
| return nil |
| } |