Source file src/crypto/mldsa/mldsa_wycheproof_test.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build !fips140v1.0
     6  
     7  package mldsa_test
     8  
     9  import (
    10  	"bytes"
    11  	"crypto"
    12  	"crypto/internal/cryptotest/wycheproof"
    13  	internalmldsa "crypto/internal/fips140/mldsa"
    14  	"crypto/mldsa"
    15  	"slices"
    16  	"testing"
    17  )
    18  
    19  // TestVerifyWycheproof test signature verification using the public
    20  // mldsa API.
    21  func TestVerifyWycheproof(t *testing.T) {
    22  	for _, file := range []string{
    23  		"mldsa_44_verify_test.json",
    24  		"mldsa_65_verify_test.json",
    25  		"mldsa_87_verify_test.json",
    26  	} {
    27  		var testdata wycheproof.MldsaVerifySchemaJson
    28  		wycheproof.LoadVectorFile(t, file, &testdata)
    29  
    30  		params := paramsForAlg(testdata.Algorithm)
    31  
    32  		for _, tg := range testdata.TestGroups {
    33  			publicKey := wycheproof.MustDecodeHex(tg.PublicKey)
    34  
    35  			for _, tv := range tg.Tests {
    36  				t.Run(wycheproof.TestName(file, tv), func(t *testing.T) {
    37  					t.Parallel()
    38  
    39  					shouldPass := wycheproof.ShouldPass(t, tv.Result, tv.Flags, nil)
    40  
    41  					pub, err := mldsa.NewPublicKey(params, publicKey)
    42  					if err != nil {
    43  						if shouldPass {
    44  							t.Fatalf("NewPublicKey: %v", err)
    45  						}
    46  						return
    47  					}
    48  
    49  					if !bytes.Equal(pub.Bytes(), publicKey) {
    50  						t.Errorf("public key roundtrip mismatch")
    51  					}
    52  
    53  					msg := wycheproof.MustDecodeHex(tv.Msg)
    54  					sig := wycheproof.MustDecodeHex(tv.Sig)
    55  					opts := new(mldsa.Options)
    56  					if tv.Ctx != nil {
    57  						opts.Context = string(wycheproof.MustDecodeHex(*tv.Ctx))
    58  					}
    59  
    60  					err = mldsa.Verify(pub, msg, sig, opts)
    61  					if shouldPass && err != nil {
    62  						t.Errorf("Verify: %v", err)
    63  					}
    64  					if !shouldPass && err == nil {
    65  						t.Errorf("Verify should have failed")
    66  					}
    67  				})
    68  			}
    69  		}
    70  	}
    71  }
    72  
    73  // TestSignSeedWycheproof tests key generation and signature creation using
    74  // the public mldsa API for seed private key inputs.
    75  //
    76  // It covers deterministic signature creation with and without pre-hashed mu.
    77  func TestSignSeedWycheproof(t *testing.T) {
    78  	// We don't include the mldsa_*_sign_noseed_test.json test vector files.
    79  	// Semi-expanded keys are not supported with the public API.
    80  	for _, file := range []string{
    81  		"mldsa_44_sign_seed_test.json",
    82  		"mldsa_65_sign_seed_test.json",
    83  		"mldsa_87_sign_seed_test.json",
    84  	} {
    85  		var testdata wycheproof.MldsaSignSeedSchemaJson
    86  		wycheproof.LoadVectorFile(t, file, &testdata)
    87  
    88  		params := paramsForAlg(testdata.Algorithm)
    89  
    90  		for _, tg := range testdata.TestGroups {
    91  			seed := wycheproof.MustDecodeHex(tg.PrivateSeed)
    92  			var expectedPublicKey []byte
    93  			if pk, ok := tg.PublicKey.(string); ok {
    94  				expectedPublicKey = wycheproof.MustDecodeHex(pk)
    95  			}
    96  
    97  			for _, tv := range tg.Tests {
    98  				t.Run(wycheproof.TestName(file, tv), func(t *testing.T) {
    99  					t.Parallel()
   100  
   101  					shouldPass := wycheproof.ShouldPass(t, tv.Result, tv.Flags, nil)
   102  
   103  					priv, err := mldsa.NewPrivateKey(params, seed)
   104  					if err != nil {
   105  						if shouldPass {
   106  							t.Fatalf("NewPrivateKey: %v", err)
   107  						}
   108  						return
   109  					}
   110  					// By checking the derived public key is equal to the vector's
   111  					// provided public key the 'sign' vectors double as key
   112  					// generation vectors.
   113  					if expectedPublicKey != nil && !bytes.Equal(priv.PublicKey().Bytes(), expectedPublicKey) {
   114  						t.Fatalf("public key mismatch")
   115  					}
   116  
   117  					if slices.Contains(tv.Flags, "Randomized") {
   118  						t.Skipf("randomized signatures not supported with public API")
   119  					}
   120  
   121  					runSignTest(t, priv, tv, shouldPass)
   122  				})
   123  			}
   124  		}
   125  	}
   126  }
   127  
   128  func runSignTest(t *testing.T, priv *mldsa.PrivateKey, tv wycheproof.MlDsaSignTestVector, shouldPass bool) {
   129  	t.Helper()
   130  
   131  	var msg, μ []byte
   132  	opts := new(mldsa.Options)
   133  	if tv.Msg != nil {
   134  		msg = wycheproof.MustDecodeHex(*tv.Msg)
   135  		if tv.Ctx != nil {
   136  			opts.Context = string(wycheproof.MustDecodeHex(*tv.Ctx))
   137  		}
   138  	}
   139  	if tv.Mu != nil && *tv.Mu != "" {
   140  		μ = wycheproof.MustDecodeHex(*tv.Mu)
   141  	}
   142  	if msg == nil && μ == nil {
   143  		t.Fatalf("test vector has neither msg nor mu")
   144  	}
   145  
   146  	var sigMsg, sigMu []byte
   147  	var errMsg, errMu error
   148  	if msg != nil {
   149  		sigMsg, errMsg = priv.SignDeterministic(msg, opts)
   150  	}
   151  	if μ != nil {
   152  		sigMu, errMu = priv.SignDeterministic(μ, crypto.MLDSAMu)
   153  	}
   154  
   155  	for _, e := range []error{errMsg, errMu} {
   156  		if e != nil {
   157  			if shouldPass {
   158  				t.Fatalf("Sign: %v", e)
   159  			}
   160  			return
   161  		}
   162  	}
   163  	if !shouldPass {
   164  		t.Errorf("Sign unexpectedly succeeded")
   165  		return
   166  	}
   167  
   168  	expectedSig := wycheproof.MustDecodeHex(tv.Sig)
   169  	sig := sigMsg
   170  	if sig == nil {
   171  		sig = sigMu
   172  	}
   173  	if sigMsg != nil && sigMu != nil && !bytes.Equal(sigMsg, sigMu) {
   174  		t.Errorf("Sign(msg, ctx) and SignExternalMu(mu) disagree")
   175  	}
   176  	if !bytes.Equal(sig, expectedSig) {
   177  		t.Errorf("signature mismatch")
   178  	}
   179  
   180  	pub := priv.PublicKey()
   181  	if msg != nil {
   182  		if err := mldsa.Verify(pub, msg, sig, opts); err != nil {
   183  			t.Errorf("Verify of own signature failed: %v", err)
   184  		}
   185  	}
   186  	// note: we can't round-trip verify external-mu signatures with the public API.
   187  	//  but if that capability were exposed in the future we could check here for
   188  	//  mu != nil.
   189  }
   190  
   191  // TestMLDSASignSeedRandomizedWycheproof tests randomized signing with the
   192  // internal testing-only API.
   193  //
   194  // It covers randomized signature creation with and without pre-hashed mu.
   195  func TestMLDSASignSeedRandomizedWycheproof(t *testing.T) {
   196  	for _, file := range []string{
   197  		"mldsa_44_sign_seed_test.json",
   198  		"mldsa_65_sign_seed_test.json",
   199  		"mldsa_87_sign_seed_test.json",
   200  	} {
   201  		var testdata wycheproof.MldsaSignSeedSchemaJson
   202  		wycheproof.LoadVectorFile(t, file, &testdata)
   203  
   204  		newPriv := newPrivateKeyFromSeedFn(t, testdata.Algorithm)
   205  
   206  		for _, tg := range testdata.TestGroups {
   207  			seed := wycheproof.MustDecodeHex(tg.PrivateSeed)
   208  			var expectedPublicKey []byte
   209  			if pk, ok := tg.PublicKey.(string); ok {
   210  				expectedPublicKey = wycheproof.MustDecodeHex(pk)
   211  			}
   212  
   213  			for _, tv := range tg.Tests {
   214  				t.Run(wycheproof.TestName(file, tv), func(t *testing.T) {
   215  					t.Parallel()
   216  
   217  					shouldPass := wycheproof.ShouldPass(t, tv.Result, tv.Flags, nil)
   218  					priv, err := newPriv(seed)
   219  					if err != nil {
   220  						if shouldPass {
   221  							t.Fatalf("NewPrivateKey: %v", err)
   222  						}
   223  						return
   224  					}
   225  
   226  					// By checking the derived public key is equal to the vector's
   227  					// provided public key the 'sign' vectors double as key
   228  					// generation vectors.
   229  					if expectedPublicKey != nil && !bytes.Equal(priv.PublicKey().Bytes(), expectedPublicKey) {
   230  						t.Fatalf("public key mismatch")
   231  					}
   232  
   233  					runRandomizedSignTest(t, priv, tv, shouldPass)
   234  				})
   235  			}
   236  		}
   237  	}
   238  }
   239  
   240  func runRandomizedSignTest(t *testing.T, priv *internalmldsa.PrivateKey, tv wycheproof.MlDsaSignTestVector, shouldPass bool) {
   241  	t.Helper()
   242  
   243  	var msg, μ []byte
   244  	var ctx string
   245  	rnd := make([]byte, 32)
   246  
   247  	if tv.Msg != nil {
   248  		msg = wycheproof.MustDecodeHex(*tv.Msg)
   249  		if tv.Ctx != nil {
   250  			ctx = string(wycheproof.MustDecodeHex(*tv.Ctx))
   251  		}
   252  	}
   253  	if tv.Mu != nil && *tv.Mu != "" {
   254  		μ = wycheproof.MustDecodeHex(*tv.Mu)
   255  	}
   256  	if tv.Rnd != nil && *tv.Rnd != "" {
   257  		rnd = wycheproof.MustDecodeHex(*tv.Rnd)
   258  	}
   259  
   260  	if msg == nil && μ == nil {
   261  		t.Fatalf("test vector has neither msg nor mu")
   262  	}
   263  
   264  	var sigMsg, sigMu []byte
   265  	var errMsg, errMu error
   266  	if msg != nil {
   267  		sigMsg, errMsg = internalmldsa.TestingOnlySignWithRandom(priv, msg, ctx, rnd)
   268  	}
   269  	if μ != nil {
   270  		sigMu, errMu = internalmldsa.TestingOnlySignExternalMuWithRandom(priv, μ, rnd)
   271  	}
   272  
   273  	for _, e := range []error{errMsg, errMu} {
   274  		if e != nil {
   275  			if shouldPass {
   276  				t.Fatalf("Sign: %v", e)
   277  			}
   278  			return
   279  		}
   280  	}
   281  	if !shouldPass {
   282  		t.Errorf("Sign unexpectedly succeeded")
   283  		return
   284  	}
   285  
   286  	expectedSig := wycheproof.MustDecodeHex(tv.Sig)
   287  	sig := sigMsg
   288  	if sig == nil {
   289  		sig = sigMu
   290  	}
   291  	if sigMsg != nil && sigMu != nil && !bytes.Equal(sigMsg, sigMu) {
   292  		t.Errorf("Sign(msg, ctx) and SignExternalMu(mu) disagree")
   293  	}
   294  	if !bytes.Equal(sig, expectedSig) {
   295  		t.Errorf("signature mismatch")
   296  	}
   297  
   298  	pub := priv.PublicKey()
   299  	if msg != nil {
   300  		if err := internalmldsa.Verify(pub, msg, sig, ctx); err != nil {
   301  			t.Errorf("Verify of own signature failed: %v", err)
   302  		}
   303  	}
   304  	if μ != nil {
   305  		if err := internalmldsa.VerifyExternalMu(pub, μ, sig); err != nil {
   306  			t.Errorf("VerifyExternalMu of own signature failed: %v", err)
   307  		}
   308  	}
   309  }
   310  
   311  // TestMLDSANoSeedWycheproof tests semi-expanded private key inputs
   312  // derive the correct public key using the internal testing-only API.
   313  //
   314  // We don't perform further signature signing operations as this is covered
   315  // by the seed-form TestSignSeedWycheproof.
   316  func TestMLDSANoSeedWycheproof(t *testing.T) {
   317  	for _, file := range []string{
   318  		"mldsa_44_sign_noseed_test.json",
   319  		"mldsa_65_sign_noseed_test.json",
   320  		"mldsa_87_sign_noseed_test.json",
   321  	} {
   322  		var testdata wycheproof.MldsaSignNoseedSchemaJson
   323  		wycheproof.LoadVectorFile(t, file, &testdata)
   324  
   325  		for _, tg := range testdata.TestGroups {
   326  			privateKey := wycheproof.MustDecodeHex(tg.PrivateKey)
   327  			var expectedPublicKey []byte
   328  			if pk, ok := tg.PublicKey.(string); ok {
   329  				expectedPublicKey = wycheproof.MustDecodeHex(pk)
   330  			}
   331  
   332  			for _, tv := range tg.Tests {
   333  				t.Run(wycheproof.TestName(file, tv), func(t *testing.T) {
   334  					t.Parallel()
   335  
   336  					shouldPass := wycheproof.ShouldPass(t, tv.Result, tv.Flags, nil)
   337  					priv, err := internalmldsa.TestingOnlyNewPrivateKeyFromSemiExpanded(privateKey)
   338  					if err != nil {
   339  						if shouldPass {
   340  							t.Fatalf("TestingOnlyNewPrivateKeyFromSemiExpanded: %v", err)
   341  						}
   342  						return
   343  					}
   344  
   345  					if expectedPublicKey != nil && !bytes.Equal(priv.PublicKey().Bytes(), expectedPublicKey) {
   346  						t.Fatalf("public key mismatch")
   347  					}
   348  				})
   349  			}
   350  		}
   351  	}
   352  }
   353  
   354  func paramsForAlg(algorithm string) mldsa.Parameters {
   355  	switch algorithm {
   356  	case "ML-DSA-44":
   357  		return mldsa.MLDSA44()
   358  	case "ML-DSA-65":
   359  		return mldsa.MLDSA65()
   360  	case "ML-DSA-87":
   361  		return mldsa.MLDSA87()
   362  	}
   363  	panic("unknown algorithm: " + algorithm)
   364  }
   365  
   366  func newPrivateKeyFromSeedFn(t *testing.T, algorithm string) func([]byte) (*internalmldsa.PrivateKey, error) {
   367  	switch algorithm {
   368  	case "ML-DSA-44":
   369  		return internalmldsa.NewPrivateKey44
   370  	case "ML-DSA-65":
   371  		return internalmldsa.NewPrivateKey65
   372  	case "ML-DSA-87":
   373  		return internalmldsa.NewPrivateKey87
   374  	}
   375  	t.Fatalf("unknown algorithm: %s", algorithm)
   376  	return nil
   377  }
   378  

View as plain text