From 9e364cb1d857e7d9dff5b55d004c2811bd97445f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:52:26 +0800 Subject: [PATCH] Release v0.33.0 * build(deps): bump github/codeql-action from 3.29.11 to 3.30.0 (#361) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.11 to 3.30.0. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/3c3833e0f8c1c83d449a7478aa59c036a9165498...2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.30.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump codecov/codecov-action from 5.5.0 to 5.5.1 (#362) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.0 to 5.5.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/fdcc8476540edceab3de004e990f80d881c6cc00...5a1091511ad55cbe89839c7260b706298ca349f7) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: 5.5.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump actions/setup-go from 5.5.0 to 6.0.0 (#363) Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5.5.0 to 6.0.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/d35c59abb061a4a6fb18e82ac0862c26744d6ab5...44694675825211faa026b3c33043df3e48a5fa00) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github/codeql-action from 3.30.0 to 3.30.1 (#364) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.0 to 3.30.1. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d...f1f6e5f6af878fb37288ce1c627459e94dbf7d01) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.30.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump step-security/harden-runner from 2.13.0 to 2.13.1 (#367) Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.13.0 to 2.13.1. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/ec9f2d5744a09debf3a187a3f4f675c53b671911...f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-version: 2.13.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump github/codeql-action from 3.30.1 to 3.30.2 (#368) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.1 to 3.30.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f1f6e5f6af878fb37288ce1c627459e94dbf7d01...d3678e237b9c32a6c9bffb3315c335f976f3549f) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.30.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(mlkem): initialize mlkem from golang standard library * chore(mlkem): refactoring, reduce alloc times * build(deps): bump github/codeql-action from 3.30.2 to 3.30.3 (#369) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.2 to 3.30.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/d3678e237b9c32a6c9bffb3315c335f976f3549f...192325c86100d080feab897ff886c34abd4c83a3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.30.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * doc(README): include MLKEM * mldsa: refactor the implementation of key and sign/verify * mldsa,slhdsa: crypto.Signer assertion * fix(slhdsa): GenerateKey slice issue #72 * fix(slhdsa): copy/paste issue --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sun Yimin --- mldsa/field.go | 16 +++ mldsa/mldsa44.go | 228 +++++++++++++++++++++------------------- mldsa/mldsa44_test.go | 16 ++- mldsa/mldsa65.go | 235 ++++++++++++++++++++++-------------------- mldsa/mldsa65_test.go | 15 ++- mldsa/mldsa87.go | 235 ++++++++++++++++++++++-------------------- mldsa/mldsa87_test.go | 15 ++- mldsa/prehash.go | 10 ++ slhdsa/dsa.go | 32 +++++- slhdsa/dsa_test.go | 4 +- slhdsa/key.go | 5 +- slhdsa/key_test.go | 11 ++ 12 files changed, 469 insertions(+), 353 deletions(-) diff --git a/mldsa/field.go b/mldsa/field.go index 3a9e70f..759269f 100644 --- a/mldsa/field.go +++ b/mldsa/field.go @@ -243,3 +243,19 @@ func vectorCountOnes(a []ringElement) int { } return oneCount } + +func constantTimeEqualRingElement(a, b ringElement) int { + var res int32 + for i := range a { + res |= int32(a[i] ^ b[i]) + } + return subtle.ConstantTimeByteEq(byte(res|(-res)>>31), 0) +} + +func constantTimeEqualRingElementArray(a, b []ringElement) int { + eq := 1 + for i := range a { + eq &= constantTimeEqualRingElement(a[i], b[i]) + } + return eq +} diff --git a/mldsa/mldsa44.go b/mldsa/mldsa44.go index eb8980a..34840c3 100644 --- a/mldsa/mldsa44.go +++ b/mldsa/mldsa44.go @@ -101,6 +101,9 @@ const ( sigEncodedLen87 = lambda256/4 + encodingSize20*l87 + omega75 + k87 ) +var _ crypto.Signer = (*PrivateKey44)(nil) +var _ crypto.Signer = (*Key44)(nil) + // A PrivateKey44 is the private key for the ML-DSA-44 signature scheme. type PrivateKey44 struct { rho [32]byte // public random seed @@ -109,11 +112,26 @@ type PrivateKey44 struct { s1 [l44]ringElement // private secret of size L with short coefficients (-4..4) or (-2..2) s2 [k44]ringElement // private secret of size K with short coefficients (-4..4) or (-2..2) t0 [k44]ringElement // the Polynomial encoding of the 13 LSB of each coefficient of the uncompressed public key polynomial t. This is saved as part of the private key. + t1 [k44]ringElement // the Polynomial encoding of the 10 MSB of each coefficient of the uncompressed public key polynomial t. This is saved as part of the public key. s1NTTCache [l44]nttElement s2NTTCache [k44]nttElement t0NTTCache [k44]nttElement a [k44 * l44]nttElement // a is generated and stored in NTT representation nttOnce sync.Once + t1Once sync.Once +} + +// Public returns the public key corresponding to the private key. +// Although we can derive the public key from the private key, +// but we do NOT need to derive it at most of the time. +func (sk *PrivateKey44) Public() crypto.PublicKey { + sk.ensureT1() + return &PublicKey44{ + rho: sk.rho, + t1: sk.t1, + tr: sk.tr, + a: sk.a, + } } func (sk *PrivateKey44) ensureNTT() { @@ -130,11 +148,36 @@ func (sk *PrivateKey44) ensureNTT() { }) } +func (sk *PrivateKey44) ensureT1() { + sk.ensureNTT() + sk.t1Once.Do(func() { + // t = NTT_inv(A' * NTT(s1)) + s2 + s1NTT := sk.s1NTTCache + A := sk.a + s2 := sk.s2 + var nttT [k44]nttElement + + for i := range nttT { + for j := range s1NTT { + nttT[i] = polyAdd(nttT[i], nttMul(s1NTT[j], A[i*l44+j])) + } + } + var t [k44]ringElement + t1 := &sk.t1 + for i := range nttT { + t[i] = polyAdd(inverseNTT(nttT[i]), s2[i]) + // compress t + for j := range n { + t1[i][j], _ = power2Round(t[i][j]) + } + } + }) +} + // A Key44 is the key pair for the ML-DSA-44 signature scheme. type Key44 struct { PrivateKey44 - xi [32]byte // input seed - t1 [k44]ringElement // the Polynomial encoding of the 10 MSB of each coefficient of the uncompressed public key polynomial t. This is saved as part of the public key. + xi [32]byte // input seed } // A PublicKey44 is the public key for the ML-DSA-44 signature scheme. @@ -147,9 +190,9 @@ type PublicKey44 struct { nttOnce sync.Once } -// PublicKey generates and returns the corresponding public key for the given +// Public generates and returns the corresponding public key for the given // Key44 instance. -func (sk *Key44) PublicKey() *PublicKey44 { +func (sk *Key44) Public() crypto.PublicKey { return &PublicKey44{ rho: sk.rho, t1: sk.t1, @@ -158,12 +201,21 @@ func (sk *Key44) PublicKey() *PublicKey44 { } } +// Seed returns a byte slice of the secret key's seed value. +func (sk *Key44) Seed() []byte { + var b [SeedSize]byte + copy(b[:], sk.xi[:]) + return b[:] +} + func (pk *PublicKey44) Equal(x crypto.PublicKey) bool { xx, ok := x.(*PublicKey44) if !ok { return false } - return pk.rho == xx.rho && pk.t1 == xx.t1 + eq := subtle.ConstantTimeCompare(pk.rho[:], xx.rho[:]) & + constantTimeEqualRingElementArray(pk.t1[:], xx.t1[:]) + return eq == 1 } // Bytes converts the PublicKey44 instance into a byte slice. @@ -194,15 +246,6 @@ func (pk *PublicKey44) ensureNTT() { }) } -// Bytes returns the byte representation of the PrivateKey44. -// It copies the internal seed (xi) into a fixed-size byte array -// and returns it as a slice. -func (sk *Key44) Bytes() []byte { - var b [SeedSize]byte - copy(b[:], sk.xi[:]) - return b[:] -} - // Bytes converts the PrivateKey44 instance into a byte slice. // See FIPS 204, Algorithm 24, skEncode() func (sk *PrivateKey44) Bytes() []byte { @@ -231,8 +274,13 @@ func (sk *PrivateKey44) Equal(x any) bool { if !ok { return false } - return sk.rho == xx.rho && sk.k == xx.k && sk.tr == xx.tr && - sk.s1 == xx.s1 && sk.s2 == xx.s2 && sk.t0 == xx.t0 + eq := subtle.ConstantTimeCompare(sk.rho[:], xx.rho[:]) & + subtle.ConstantTimeCompare(sk.k[:], xx.k[:]) & + subtle.ConstantTimeCompare(sk.tr[:], xx.tr[:]) & + constantTimeEqualRingElementArray(sk.s1[:], xx.s1[:]) & + constantTimeEqualRingElementArray(sk.s2[:], xx.s2[:]) & + constantTimeEqualRingElementArray(sk.t0[:], xx.t0[:]) + return eq == 1 } // GenerateKey44 generates a new Key44 (ML-DSA-44) using the provided random source. @@ -284,17 +332,17 @@ func dsaKeyGen44(sk *Key44, xi *[32]byte) { s1 := &sk.s1 s2 := &sk.s2 // Algorithm 33, ExpandS - for s := byte(0); s < l44; s++ { + for s := range byte(l44) { s1[s] = rejBoundedPoly(rho1, eta2, 0, s) } - for r := byte(0); r < k44; r++ { + for r := range byte(k44) { s2[r] = rejBoundedPoly(rho1, eta2, 0, r+l44) } // Using rho generate A' = A in NTT form A := &sk.a // Algorithm 32, ExpandA - for r := byte(0); r < k44; r++ { + for r := range byte(k44) { for s := byte(0); s < l44; s++ { A[r*l44+s] = rejNTTPoly(rho, s, r) } @@ -322,7 +370,7 @@ func dsaKeyGen44(sk *Key44, xi *[32]byte) { } } H.Reset() - ek := sk.PublicKey().Bytes() + ek := sk.Public().(*PublicKey44).Bytes() H.Write(ek) H.Read(sk.tr[:]) } @@ -355,8 +403,8 @@ func parsePublicKey44(pk *PublicKey44, b []byte) (*PublicKey44, error) { A := &pk.a rho := pk.rho[:] // Algorithm 32, ExpandA - for r := byte(0); r < k44; r++ { - for s := byte(0); s < l44; s++ { + for r := range byte(k44) { + for s := range byte(l44) { A[r*l44+s] = rejNTTPoly(rho, s, r) } } @@ -404,32 +452,42 @@ func parsePrivateKey44(sk *PrivateKey44, b []byte) (*PrivateKey44, error) { A := &sk.a rho := sk.rho[:] // Algorithm 32, ExpandA - for r := byte(0); r < k44; r++ { - for s := byte(0); s < l44; s++ { + for r := range byte(k44) { + for s := range byte(l44) { A[r*l44+s] = rejNTTPoly(rho, s, r) } } return sk, nil } -// Sign generates a digital signature for the given message and context using the private key. -// It uses a random seed generated from the provided random source. +// Sign signs the provided digest using the private key. It is a wrapper around SignMessage. +// It satisfies the crypto.Signer interface. +func (sk *PrivateKey44) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + return sk.SignMessage(rand, digest, opts) +} + +// SignMessage signs a message with the private key. +// It satisfies the crypto.MessageSigner interface. // -// Parameters: -// - rand: An io.Reader used to generate a random seed for signing. -// - message: The message to be signed. Must not be empty. -// - context: An optional context for domain separation. Must not exceed 255 bytes. -// -// Returns: -// - A byte slice containing the generated signature. -// - An error if the message is empty, the context is too long, or if there is an issue -// reading from the random source. -// -// Note: -// - The function uses SHAKE256 from the SHA-3 family for hashing. -// - The signing process involves generating a unique seed and a hash-based -// message digest (mu) before delegating to the internal signing function. -func (sk *PrivateKey44) Sign(rand io.Reader, message, context []byte) ([]byte, error) { +// The function supports pre-hashing the message by providing a hash OID in the options. +// Context data can also be provided, but is limited to 255 bytes. +func (sk *PrivateKey44) SignMessage(rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error) { + var ( + context []byte + hashOID asn1.ObjectIdentifier + indicator byte = 0 + ) + if opts, ok := opts.(*Options); ok { + context = opts.Context + hashOID = opts.PrehashOID + } + if len(hashOID) != 0 { + var err error + if message, err = preHash(hashOID, message); err != nil { + return nil, err + } + indicator = 1 + } if len(message) == 0 { return nil, errors.New("mldsa: empty message") } @@ -442,7 +500,7 @@ func (sk *PrivateKey44) Sign(rand io.Reader, message, context []byte) ([]byte, e } H := sha3.NewSHAKE256() H.Write(sk.tr[:]) - H.Write([]byte{0, byte(len(context))}) + H.Write([]byte{indicator, byte(len(context))}) if len(context) > 0 { H.Write(context) } @@ -453,39 +511,6 @@ func (sk *PrivateKey44) Sign(rand io.Reader, message, context []byte) ([]byte, e return sk.signInternal(seed[:], mu[:]) } -// SignWithPreHash generates a digital signature for the given message -// using the private key and additional context. It uses a given hashing algorithm -// from the OID to pre-hash the message before signing. -// It is similar to Sign but allows for pre-hashing the message. -func (sk *PrivateKey44) SignWithPreHash(rand io.Reader, message, context []byte, oid asn1.ObjectIdentifier) ([]byte, error) { - if len(message) == 0 { - return nil, errors.New("mldsa: empty message") - } - if len(context) > 255 { - return nil, errors.New("mldsa: context too long") - } - preHashValue, err := preHash(oid, message) - if err != nil { - return nil, err - } - var seed [SeedSize]byte - if _, err := io.ReadFull(rand, seed[:]); err != nil { - return nil, err - } - - H := sha3.NewSHAKE256() - H.Write(sk.tr[:]) - H.Write([]byte{1, byte(len(context))}) - if len(context) > 0 { - H.Write(context) - } - H.Write(preHashValue) - var mu [64]byte - H.Read(mu[:]) - - return sk.signInternal(seed[:], mu[:]) -} - // See FIPS 204, Algorithm 7 ML-DSA.Sign_internal() func (sk *PrivateKey44) signInternal(seed, mu []byte) ([]byte, error) { var rho2 [64 + 2]byte @@ -596,9 +621,25 @@ func (sk *PrivateKey44) signInternal(seed, mu []byte) ([]byte, error) { } } -// Verify checks the validity of a given signature for a message and context -// using the public key. -func (pk *PublicKey44) Verify(sig []byte, message, context []byte) bool { +// VerifyWithOptions verifies a signature against a message using the public key with additional options. +func (pk *PublicKey44) VerifyWithOptions(sig []byte, message []byte, opts crypto.SignerOpts) bool { + var ( + context []byte + hashOID asn1.ObjectIdentifier + indicator byte = 0 + ) + if opts, ok := opts.(*Options); ok { + context = opts.Context + hashOID = opts.PrehashOID + } + if len(hashOID) != 0 { + var err error + if message, err = preHash(hashOID, message); err != nil { + return false + } + indicator = 1 + } + if len(message) == 0 { return false } @@ -610,7 +651,7 @@ func (pk *PublicKey44) Verify(sig []byte, message, context []byte) bool { } H := sha3.NewSHAKE256() H.Write(pk.tr[:]) - H.Write([]byte{0, byte(len(context))}) + H.Write([]byte{indicator, byte(len(context))}) if len(context) > 0 { H.Write(context) } @@ -621,35 +662,6 @@ func (pk *PublicKey44) Verify(sig []byte, message, context []byte) bool { return pk.verifyInternal(sig, mu[:]) } -// VerifyWithPreHash verifies a signature using a message and additional context. -// It uses a given hashing algorithm from the OID to pre-hash the message before verifying. -func (pk *PublicKey44) VerifyWithPreHash(sig []byte, message, context []byte, oid asn1.ObjectIdentifier) bool { - if len(message) == 0 { - return false - } - if len(context) > 255 { - return false - } - if len(sig) != sigEncodedLen44 { - return false - } - preHashValue, err := preHash(oid, message) - if err != nil { - return false - } - H := sha3.NewSHAKE256() - H.Write(pk.tr[:]) - H.Write([]byte{1, byte(len(context))}) - if len(context) > 0 { - H.Write(context) - } - H.Write(preHashValue) - var mu [64]byte - H.Read(mu[:]) - - return pk.verifyInternal(sig, mu[:]) -} - // See FIPS 204, Algorithm 8 ML-DSA.Verify_internal() func (pk *PublicKey44) verifyInternal(sig, mu []byte) bool { // Decode the signature diff --git a/mldsa/mldsa44_test.go b/mldsa/mldsa44_test.go index 7dbb7e2..5057f98 100644 --- a/mldsa/mldsa44_test.go +++ b/mldsa/mldsa44_test.go @@ -46,7 +46,7 @@ func TestKeyGen44(t *testing.T) { if err != nil { t.Fatalf("NewPrivateKey44 failed: %v", err) } - pub := priv.PublicKey() + pub := priv.Public().(*PublicKey44) pubBytes := pub.Bytes() if !bytes.Equal(pubBytes, pk) { t.Errorf("Public key mismatch: got %x, want %x", pubBytes, pk) @@ -70,6 +70,10 @@ func TestKeyGen44(t *testing.T) { if !priv.Equal(priv2) { t.Errorf("Private key not equal: got %x, want %x", privBytes, priv2.Bytes()) } + pub3 := priv2.Public() + if !pub.Equal(pub3) { + t.Errorf("Public key from private key not equal") + } } } @@ -123,6 +127,7 @@ func TestSign44(t *testing.T) { if err != nil { t.Fatalf("NewPrivateKey44 failed: %v", err) } + sig2, err := priv.signInternal(seed[:], mu) if err != nil { t.Fatalf("failed to sign: %v", err) @@ -232,7 +237,7 @@ func TestSignWithPreHash44(t *testing.T) { if err != nil { t.Fatalf("NewPrivateKey44 failed: %v", err) } - sig2, err := priv.SignWithPreHash(zeroReader, msg, context, c.oid) + sig2, err := priv.Sign(zeroReader, msg, &Options{context, c.oid}) if err != nil { t.Fatalf("failed to sign: %v", err) } @@ -249,7 +254,7 @@ func TestSignWithPreHash44(t *testing.T) { if err != nil { t.Fatalf("NewPublicKey44 failed: %v", err) } - if !pub.VerifyWithPreHash(sig, msg, context, c.oid) { + if !pub.VerifyWithOptions(sig, msg, &Options{context, c.oid}) { t.Error("signature verification failed") } } @@ -294,7 +299,7 @@ func TestVerify44(t *testing.T) { if err != nil { t.Fatalf("NewPublicKey44 failed: %v", err) } - if pub.Verify(sig, msg, ctx) != c.passed { + if pub.VerifyWithOptions(sig, msg, &Options{Context: ctx}) != c.passed { t.Errorf("Verify failed") } } @@ -351,10 +356,11 @@ func BenchmarkVerify44(b *testing.B) { if err != nil { b.Fatalf("NewPublicKey44 failed: %v", err) } + opts := &Options{Context: ctx} b.ReportAllocs() b.ResetTimer() for b.Loop() { - if !pub.Verify(sig, msg, ctx) { + if !pub.VerifyWithOptions(sig, msg, opts) { b.Errorf("Verify failed") } } diff --git a/mldsa/mldsa65.go b/mldsa/mldsa65.go index 0a5b584..6222699 100644 --- a/mldsa/mldsa65.go +++ b/mldsa/mldsa65.go @@ -17,6 +17,9 @@ import ( "sync" ) +var _ crypto.Signer = (*PrivateKey65)(nil) +var _ crypto.Signer = (*Key65)(nil) + // A PrivateKey65 is the private key for the ML-DSA-65 signature scheme. type PrivateKey65 struct { rho [32]byte // public random seed @@ -25,11 +28,26 @@ type PrivateKey65 struct { s1 [l65]ringElement // private secret of size L with short coefficients (-4..4) or (-2..2) s2 [k65]ringElement // private secret of size K with short coefficients (-4..4) or (-2..2) t0 [k65]ringElement // the Polynomial encoding of the 13 LSB of each coefficient of the uncompressed public key polynomial t. This is saved as part of the private key. + t1 [k65]ringElement // the Polynomial encoding of the 10 MSB of each coefficient of the uncompressed public key polynomial t. This is saved as part of the public key. s1NTTCache [l65]nttElement s2NTTCache [k65]nttElement t0NTTCache [k65]nttElement a [k65 * l65]nttElement // a is generated and stored in NTT representation nttOnce sync.Once + t1Once sync.Once +} + +// Public returns the public key corresponding to the private key. +// Although we can derive the public key from the private key, +// but we do NOT need to derive it at most of the time. +func (sk *PrivateKey65) Public() crypto.PublicKey { + sk.ensureT1() + return &PublicKey65{ + rho: sk.rho, + t1: sk.t1, + tr: sk.tr, + a: sk.a, + } } func (sk *PrivateKey65) ensureNTT() { @@ -46,11 +64,36 @@ func (sk *PrivateKey65) ensureNTT() { }) } +func (sk *PrivateKey65) ensureT1() { + sk.ensureNTT() + sk.t1Once.Do(func() { + // t = NTT_inv(A' * NTT(s1)) + s2 + s1NTT := sk.s1NTTCache + A := sk.a + s2 := sk.s2 + var nttT [k65]nttElement + + for i := range nttT { + for j := range s1NTT { + nttT[i] = polyAdd(nttT[i], nttMul(s1NTT[j], A[i*l65+j])) + } + } + var t [k65]ringElement + t1 := &sk.t1 + for i := range nttT { + t[i] = polyAdd(inverseNTT(nttT[i]), s2[i]) + // compress t + for j := range n { + t1[i][j], _ = power2Round(t[i][j]) + } + } + }) +} + // A Key65 is the key pair for the ML-DSA-65 signature scheme. type Key65 struct { PrivateKey65 - xi [32]byte // input seed - t1 [k65]ringElement // the Polynomial encoding of the 10 MSB of each coefficient of the uncompressed public key polynomial t. This is saved as part of the public key. + xi [32]byte // input seed } // A PublicKey65 is the public key for the ML-DSA-65 signature scheme. @@ -63,9 +106,9 @@ type PublicKey65 struct { nttOnce sync.Once } -// PublicKey generates and returns the corresponding public key for the given +// Public generates and returns the corresponding public key for the given // Key65 instance. -func (sk *Key65) PublicKey() *PublicKey65 { +func (sk *Key65) Public() crypto.PublicKey { return &PublicKey65{ rho: sk.rho, t1: sk.t1, @@ -74,12 +117,21 @@ func (sk *Key65) PublicKey() *PublicKey65 { } } +// Seed returns a byte slice of the secret key's seed value. +func (sk *Key65) Seed() []byte { + var b [SeedSize]byte + copy(b[:], sk.xi[:]) + return b[:] +} + func (pk *PublicKey65) Equal(x crypto.PublicKey) bool { xx, ok := x.(*PublicKey65) if !ok { return false } - return pk.rho == xx.rho && pk.t1 == xx.t1 + eq := subtle.ConstantTimeCompare(pk.rho[:], xx.rho[:]) & + constantTimeEqualRingElementArray(pk.t1[:], xx.t1[:]) + return eq == 1 } // Bytes converts the PublicKey65 instance into a byte slice. @@ -110,15 +162,6 @@ func (pk *PublicKey65) ensureNTT() { }) } -// Bytes returns the byte representation of the PrivateKey65. -// It copies the internal seed (xi) into a fixed-size byte array -// and returns it as a slice. -func (sk *Key65) Bytes() []byte { - var b [SeedSize]byte - copy(b[:], sk.xi[:]) - return b[:] -} - // Bytes converts the PrivateKey65 instance into a byte slice. // See FIPS 204, Algorithm 24, skEncode() func (sk *PrivateKey65) Bytes() []byte { @@ -147,8 +190,13 @@ func (sk *PrivateKey65) Equal(x any) bool { if !ok { return false } - return sk.rho == xx.rho && sk.k == xx.k && sk.tr == xx.tr && - sk.s1 == xx.s1 && sk.s2 == xx.s2 && sk.t0 == xx.t0 + eq := subtle.ConstantTimeCompare(sk.rho[:], xx.rho[:]) & + subtle.ConstantTimeCompare(sk.k[:], xx.k[:]) & + subtle.ConstantTimeCompare(sk.tr[:], xx.tr[:]) & + constantTimeEqualRingElementArray(sk.s1[:], xx.s1[:]) & + constantTimeEqualRingElementArray(sk.s2[:], xx.s2[:]) & + constantTimeEqualRingElementArray(sk.t0[:], xx.t0[:]) + return eq == 1 } // GenerateKey65 generates a new Key65 (ML-DSA-65) using the provided random source. @@ -188,8 +236,7 @@ func dsaKeyGen65(sk *Key65, xi *[32]byte) { sk.xi = *xi H := sha3.NewSHAKE256() H.Write(xi[:]) - H.Write([]byte{k65}) - H.Write([]byte{l65}) + H.Write([]byte{k65, l65}) K := make([]byte, 128) H.Read(K) rho, rho1 := K[:32], K[32:96] @@ -201,17 +248,17 @@ func dsaKeyGen65(sk *Key65, xi *[32]byte) { s1 := &sk.s1 s2 := &sk.s2 // Algorithm 33, ExpandS - for s := byte(0); s < l65; s++ { + for s := range byte(l65) { s1[s] = rejBoundedPoly(rho1, eta4, 0, s) } - for r := byte(0); r < k65; r++ { + for r := range byte(k65) { s2[r] = rejBoundedPoly(rho1, eta4, 0, r+l65) } // Using rho generate A' = A in NTT form A := &sk.a // Algorithm 32, ExpandA - for r := byte(0); r < k65; r++ { + for r := range byte(k65) { for s := byte(0); s < l65; s++ { A[r*l65+s] = rejNTTPoly(rho, s, r) } @@ -239,7 +286,7 @@ func dsaKeyGen65(sk *Key65, xi *[32]byte) { } } H.Reset() - ek := sk.PublicKey().Bytes() + ek := sk.Public().(*PublicKey65).Bytes() H.Write(ek) H.Read(sk.tr[:]) } @@ -272,8 +319,8 @@ func parsePublicKey65(pk *PublicKey65, b []byte) (*PublicKey65, error) { A := &pk.a rho := pk.rho[:] // Algorithm 32, ExpandA - for r := byte(0); r < k65; r++ { - for s := byte(0); s < l65; s++ { + for r := range byte(k65) { + for s := range byte(l65) { A[r*l65+s] = rejNTTPoly(rho, s, r) } } @@ -321,32 +368,42 @@ func parsePrivateKey65(sk *PrivateKey65, b []byte) (*PrivateKey65, error) { A := &sk.a rho := sk.rho[:] // Algorithm 32, ExpandA - for r := byte(0); r < k65; r++ { - for s := byte(0); s < l65; s++ { + for r := range byte(k65) { + for s := range byte(l65) { A[r*l65+s] = rejNTTPoly(rho, s, r) } } return sk, nil } -// Sign generates a digital signature for the given message and context using the private key. -// It uses a random seed generated from the provided random source. +// Sign signs the provided digest using the private key. It is a wrapper around SignMessage. +// It satisfies the crypto.Signer interface. +func (sk *PrivateKey65) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + return sk.SignMessage(rand, digest, opts) +} + +// SignMessage signs a message with the private key. +// It satisfies the crypto.MessageSigner interface. // -// Parameters: -// - rand: An io.Reader used to generate a random seed for signing. -// - message: The message to be signed. Must not be empty. -// - context: An optional context for domain separation. Must not exceed 255 bytes. -// -// Returns: -// - A byte slice containing the generated signature. -// - An error if the message is empty, the context is too long, or if there is an issue -// reading from the random source. -// -// Note: -// - The function uses SHAKE256 from the SHA-3 family for hashing. -// - The signing process involves generating a unique seed and a hash-based -// message digest (mu) before delegating to the internal signing function. -func (sk *PrivateKey65) Sign(rand io.Reader, message, context []byte) ([]byte, error) { +// The function supports pre-hashing the message by providing a hash OID in the options. +// Context data can also be provided, but is limited to 255 bytes. +func (sk *PrivateKey65) SignMessage(rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error) { + var ( + context []byte + hashOID asn1.ObjectIdentifier + indicator byte = 0 + ) + if opts, ok := opts.(*Options); ok { + context = opts.Context + hashOID = opts.PrehashOID + } + if len(hashOID) != 0 { + var err error + if message, err = preHash(hashOID, message); err != nil { + return nil, err + } + indicator = 1 + } if len(message) == 0 { return nil, errors.New("mldsa: empty message") } @@ -359,7 +416,7 @@ func (sk *PrivateKey65) Sign(rand io.Reader, message, context []byte) ([]byte, e } H := sha3.NewSHAKE256() H.Write(sk.tr[:]) - H.Write([]byte{0, byte(len(context))}) + H.Write([]byte{indicator, byte(len(context))}) if len(context) > 0 { H.Write(context) } @@ -370,39 +427,6 @@ func (sk *PrivateKey65) Sign(rand io.Reader, message, context []byte) ([]byte, e return sk.signInternal(seed[:], mu[:]) } -// SignWithPreHash generates a digital signature for the given message -// using the private key and additional context. It uses a given hashing algorithm -// from the OID to pre-hash the message before signing. -// It is similar to Sign but allows for pre-hashing the message. -func (sk *PrivateKey65) SignWithPreHash(rand io.Reader, message, context []byte, oid asn1.ObjectIdentifier) ([]byte, error) { - if len(message) == 0 { - return nil, errors.New("mldsa: empty message") - } - if len(context) > 255 { - return nil, errors.New("mldsa: context too long") - } - preHashValue, err := preHash(oid, message) - if err != nil { - return nil, err - } - var seed [SeedSize]byte - if _, err := io.ReadFull(rand, seed[:]); err != nil { - return nil, err - } - - H := sha3.NewSHAKE256() - H.Write(sk.tr[:]) - H.Write([]byte{1, byte(len(context))}) - if len(context) > 0 { - H.Write(context) - } - H.Write(preHashValue) - var mu [64]byte - H.Read(mu[:]) - - return sk.signInternal(seed[:], mu[:]) -} - // See FIPS 204, Algorithm 7 ML-DSA.Sign_internal() func (sk *PrivateKey65) signInternal(seed, mu []byte) ([]byte, error) { var rho2 [64 + 2]byte @@ -418,7 +442,7 @@ func (sk *PrivateKey65) signInternal(seed, mu []byte) ([]byte, error) { r0NormThreshold := int(gamma2QMinus1Div32 - beta65) // rejection sampling loop - for kappa := 0; ; kappa = kappa + l65 { + for kappa := 0; ; kappa += l65 { // expand mask var ( y [l65]ringElement @@ -513,9 +537,25 @@ func (sk *PrivateKey65) signInternal(seed, mu []byte) ([]byte, error) { } } -// Verify checks the validity of a given signature for a message and context -// using the public key. -func (pk *PublicKey65) Verify(sig []byte, message, context []byte) bool { +// VerifyWithOptions verifies a signature against a message using the public key with additional options. +func (pk *PublicKey65) VerifyWithOptions(sig []byte, message []byte, opts crypto.SignerOpts) bool { + var ( + context []byte + hashOID asn1.ObjectIdentifier + indicator byte = 0 + ) + if opts, ok := opts.(*Options); ok { + context = opts.Context + hashOID = opts.PrehashOID + } + if len(hashOID) != 0 { + var err error + if message, err = preHash(hashOID, message); err != nil { + return false + } + indicator = 1 + } + if len(message) == 0 { return false } @@ -527,7 +567,7 @@ func (pk *PublicKey65) Verify(sig []byte, message, context []byte) bool { } H := sha3.NewSHAKE256() H.Write(pk.tr[:]) - H.Write([]byte{0, byte(len(context))}) + H.Write([]byte{indicator, byte(len(context))}) if len(context) > 0 { H.Write(context) } @@ -538,35 +578,6 @@ func (pk *PublicKey65) Verify(sig []byte, message, context []byte) bool { return pk.verifyInternal(sig, mu[:]) } -// VerifyWithPreHash verifies a signature using a message and additional context. -// It uses a given hashing algorithm from the OID to pre-hash the message before verifying. -func (pk *PublicKey65) VerifyWithPreHash(sig []byte, message, context []byte, oid asn1.ObjectIdentifier) bool { - if len(message) == 0 { - return false - } - if len(context) > 255 { - return false - } - if len(sig) != sigEncodedLen65 { - return false - } - preHashValue, err := preHash(oid, message) - if err != nil { - return false - } - H := sha3.NewSHAKE256() - H.Write(pk.tr[:]) - H.Write([]byte{1, byte(len(context))}) - if len(context) > 0 { - H.Write(context) - } - H.Write(preHashValue) - var mu [64]byte - H.Read(mu[:]) - - return pk.verifyInternal(sig, mu[:]) -} - // See FIPS 204, Algorithm 8 ML-DSA.Verify_internal() func (pk *PublicKey65) verifyInternal(sig, mu []byte) bool { // Decode the signature @@ -622,5 +633,5 @@ func (pk *PublicKey65) verifyInternal(sig, mu []byte) bool { var cTilde1 [lambda192 / 4]byte H.Read(cTilde1[:]) return subtle.ConstantTimeLessOrEq(int(gamma1TwoPower19-beta65), zNorm) == 0 && - subtle.ConstantTimeCompare(cTilde[:], cTilde1[:]) == 1 + subtle.ConstantTimeCompare(cTilde, cTilde1[:]) == 1 } diff --git a/mldsa/mldsa65_test.go b/mldsa/mldsa65_test.go index 56d1f06..3355aca 100644 --- a/mldsa/mldsa65_test.go +++ b/mldsa/mldsa65_test.go @@ -46,7 +46,7 @@ func TestKeyGen65(t *testing.T) { if err != nil { t.Fatalf("NewPrivateKey65 failed: %v", err) } - pub := priv.PublicKey() + pub := priv.Public().(*PublicKey65) pubBytes := pub.Bytes() if !bytes.Equal(pubBytes, pk) { t.Errorf("Public key mismatch: got %x, want %x", pubBytes, pk) @@ -70,6 +70,10 @@ func TestKeyGen65(t *testing.T) { if !priv.Equal(priv2) { t.Errorf("Private key not equal: got %x, want %x", privBytes, priv2.Bytes()) } + pub3 := priv2.Public() + if !pub.Equal(pub3) { + t.Errorf("Public key from private key not equal") + } } } @@ -232,7 +236,7 @@ func TestSignWithPreHash65(t *testing.T) { if err != nil { t.Fatalf("NewPrivateKey65 failed: %v", err) } - sig2, err := priv.SignWithPreHash(zeroReader, msg, context, c.oid) + sig2, err := priv.Sign(zeroReader, msg, &Options{context, c.oid}) if err != nil { t.Fatalf("failed to sign: %v", err) } @@ -249,7 +253,7 @@ func TestSignWithPreHash65(t *testing.T) { if err != nil { t.Fatalf("NewPublicKey65 failed: %v", err) } - if !pub.VerifyWithPreHash(sig, msg, context, c.oid) { + if !pub.VerifyWithOptions(sig, msg, &Options{context, c.oid}) { t.Error("signature verification failed") } } @@ -294,7 +298,7 @@ func TestVerify65(t *testing.T) { if err != nil { t.Fatalf("NewPublicKey65 failed: %v", err) } - if pub.Verify(sig, msg, ctx) != c.passed { + if pub.VerifyWithOptions(sig, msg, &Options{Context: ctx}) != c.passed { t.Errorf("Verify failed") } } @@ -341,10 +345,11 @@ func BenchmarkVerify65(b *testing.B) { if err != nil { b.Fatalf("NewPublicKey65 failed: %v", err) } + opts := &Options{Context: ctx} b.ReportAllocs() b.ResetTimer() for b.Loop() { - if !pub.Verify(sig, msg, ctx) { + if !pub.VerifyWithOptions(sig, msg, opts) { b.Errorf("Verify failed") } } diff --git a/mldsa/mldsa87.go b/mldsa/mldsa87.go index 742751f..d5edae9 100644 --- a/mldsa/mldsa87.go +++ b/mldsa/mldsa87.go @@ -17,6 +17,9 @@ import ( "sync" ) +var _ crypto.Signer = (*PrivateKey87)(nil) +var _ crypto.Signer = (*Key87)(nil) + // A PrivateKey87 is the private key for the ML-DSA-87 signature scheme. type PrivateKey87 struct { rho [32]byte // public random seed @@ -25,11 +28,26 @@ type PrivateKey87 struct { s1 [l87]ringElement // private secret of size L with short coefficients (-4..4) or (-2..2) s2 [k87]ringElement // private secret of size K with short coefficients (-4..4) or (-2..2) t0 [k87]ringElement // the Polynomial encoding of the 13 LSB of each coefficient of the uncompressed public key polynomial t. This is saved as part of the private key. + t1 [k87]ringElement // the Polynomial encoding of the 10 MSB of each coefficient of the uncompressed public key polynomial t. This is saved as part of the public key. s1NTTCache [l87]nttElement s2NTTCache [k87]nttElement t0NTTCache [k87]nttElement a [k87 * l87]nttElement // a is generated and stored in NTT representation nttOnce sync.Once + t1Once sync.Once +} + +// Public returns the public key corresponding to the private key. +// Although we can derive the public key from the private key, +// but we do NOT need to derive it at most of the time. +func (sk *PrivateKey87) Public() crypto.PublicKey { + sk.ensureT1() + return &PublicKey87{ + rho: sk.rho, + t1: sk.t1, + tr: sk.tr, + a: sk.a, + } } func (sk *PrivateKey87) ensureNTT() { @@ -46,11 +64,36 @@ func (sk *PrivateKey87) ensureNTT() { }) } +func (sk *PrivateKey87) ensureT1() { + sk.ensureNTT() + sk.t1Once.Do(func() { + // t = NTT_inv(A' * NTT(s1)) + s2 + s1NTT := sk.s1NTTCache + A := sk.a + s2 := sk.s2 + var nttT [k87]nttElement + + for i := range nttT { + for j := range s1NTT { + nttT[i] = polyAdd(nttT[i], nttMul(s1NTT[j], A[i*l87+j])) + } + } + var t [k87]ringElement + t1 := &sk.t1 + for i := range nttT { + t[i] = polyAdd(inverseNTT(nttT[i]), s2[i]) + // compress t + for j := range n { + t1[i][j], _ = power2Round(t[i][j]) + } + } + }) +} + // A Key87 is the key pair for the ML-DSA-87 signature scheme. type Key87 struct { PrivateKey87 - xi [32]byte // input seed - t1 [k87]ringElement // the Polynomial encoding of the 10 MSB of each coefficient of the uncompressed public key polynomial t. This is saved as part of the public key. + xi [32]byte // input seed } // A PublicKey87 is the public key for the ML-DSA-87 signature scheme. @@ -63,9 +106,9 @@ type PublicKey87 struct { nttOnce sync.Once } -// PublicKey generates and returns the corresponding public key for the given +// Public generates and returns the corresponding public key for the given // Key87 instance. -func (sk *Key87) PublicKey() *PublicKey87 { +func (sk *Key87) Public() crypto.PublicKey { return &PublicKey87{ rho: sk.rho, t1: sk.t1, @@ -74,12 +117,21 @@ func (sk *Key87) PublicKey() *PublicKey87 { } } +// Seed returns a byte slice of the secret key's seed value. +func (sk *Key87) Seed() []byte { + var b [SeedSize]byte + copy(b[:], sk.xi[:]) + return b[:] +} + func (pk *PublicKey87) Equal(x crypto.PublicKey) bool { xx, ok := x.(*PublicKey87) if !ok { return false } - return pk.rho == xx.rho && pk.t1 == xx.t1 + eq := subtle.ConstantTimeCompare(pk.rho[:], xx.rho[:]) & + constantTimeEqualRingElementArray(pk.t1[:], xx.t1[:]) + return eq == 1 } // Bytes converts the PublicKey87 instance into a byte slice. @@ -110,15 +162,6 @@ func (pk *PublicKey87) ensureNTT() { }) } -// Bytes returns the byte representation of the PrivateKey87. -// It copies the internal seed (xi) into a fixed-size byte array -// and returns it as a slice. -func (sk *Key87) Bytes() []byte { - var b [SeedSize]byte - copy(b[:], sk.xi[:]) - return b[:] -} - // Bytes converts the PrivateKey87 instance into a byte slice. // See FIPS 204, Algorithm 24, skEncode() func (sk *PrivateKey87) Bytes() []byte { @@ -147,8 +190,13 @@ func (sk *PrivateKey87) Equal(x any) bool { if !ok { return false } - return sk.rho == xx.rho && sk.k == xx.k && sk.tr == xx.tr && - sk.s1 == xx.s1 && sk.s2 == xx.s2 && sk.t0 == xx.t0 + eq := subtle.ConstantTimeCompare(sk.rho[:], xx.rho[:]) & + subtle.ConstantTimeCompare(sk.k[:], xx.k[:]) & + subtle.ConstantTimeCompare(sk.tr[:], xx.tr[:]) & + constantTimeEqualRingElementArray(sk.s1[:], xx.s1[:]) & + constantTimeEqualRingElementArray(sk.s2[:], xx.s2[:]) & + constantTimeEqualRingElementArray(sk.t0[:], xx.t0[:]) + return eq == 1 } // GenerateKey87 generates a new Key87 (ML-DSA-87) using the provided random source. @@ -188,8 +236,7 @@ func dsaKeyGen87(sk *Key87, xi *[32]byte) { sk.xi = *xi H := sha3.NewSHAKE256() H.Write(xi[:]) - H.Write([]byte{k87}) - H.Write([]byte{l87}) + H.Write([]byte{k87, l87}) K := make([]byte, 128) H.Read(K) rho, rho1 := K[:32], K[32:96] @@ -201,17 +248,17 @@ func dsaKeyGen87(sk *Key87, xi *[32]byte) { s1 := &sk.s1 s2 := &sk.s2 // Algorithm 33, ExpandS - for s := byte(0); s < l87; s++ { + for s := range byte(l87) { s1[s] = rejBoundedPoly(rho1, eta2, 0, s) } - for r := byte(0); r < k87; r++ { + for r := range byte(k87) { s2[r] = rejBoundedPoly(rho1, eta2, 0, r+l87) } // Using rho generate A' = A in NTT form A := &sk.a // Algorithm 32, ExpandA - for r := byte(0); r < k87; r++ { + for r := range byte(k87) { for s := byte(0); s < l87; s++ { A[r*l87+s] = rejNTTPoly(rho, s, r) } @@ -239,7 +286,7 @@ func dsaKeyGen87(sk *Key87, xi *[32]byte) { } } H.Reset() - ek := sk.PublicKey().Bytes() + ek := sk.Public().(*PublicKey87).Bytes() H.Write(ek) H.Read(sk.tr[:]) } @@ -272,8 +319,8 @@ func parsePublicKey87(pk *PublicKey87, b []byte) (*PublicKey87, error) { A := &pk.a rho := pk.rho[:] // Algorithm 32, ExpandA - for r := byte(0); r < k87; r++ { - for s := byte(0); s < l87; s++ { + for r := range byte(k87) { + for s := range byte(l87) { A[r*l87+s] = rejNTTPoly(rho, s, r) } } @@ -321,32 +368,42 @@ func parsePrivateKey87(sk *PrivateKey87, b []byte) (*PrivateKey87, error) { A := &sk.a rho := sk.rho[:] // Algorithm 32, ExpandA - for r := byte(0); r < k87; r++ { - for s := byte(0); s < l87; s++ { + for r := range byte(k87) { + for s := range byte(l87) { A[r*l87+s] = rejNTTPoly(rho, s, r) } } return sk, nil } -// Sign generates a digital signature for the given message and context using the private key. -// It uses a random seed generated from the provided random source. +// Sign signs the provided digest using the private key. It is a wrapper around SignMessage. +// It satisfies the crypto.Signer interface. +func (sk *PrivateKey87) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + return sk.SignMessage(rand, digest, opts) +} + +// SignMessage signs a message with the private key. +// It satisfies the crypto.MessageSigner interface. // -// Parameters: -// - rand: An io.Reader used to generate a random seed for signing. -// - message: The message to be signed. Must not be empty. -// - context: An optional context for domain separation. Must not exceed 255 bytes. -// -// Returns: -// - A byte slice containing the generated signature. -// - An error if the message is empty, the context is too long, or if there is an issue -// reading from the random source. -// -// Note: -// - The function uses SHAKE256 from the SHA-3 family for hashing. -// - The signing process involves generating a unique seed and a hash-based -// message digest (mu) before delegating to the internal signing function. -func (sk *PrivateKey87) Sign(rand io.Reader, message, context []byte) ([]byte, error) { +// The function supports pre-hashing the message by providing a hash OID in the options. +// Context data can also be provided, but is limited to 255 bytes. +func (sk *PrivateKey87) SignMessage(rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error) { + var ( + context []byte + hashOID asn1.ObjectIdentifier + indicator byte = 0 + ) + if opts, ok := opts.(*Options); ok { + context = opts.Context + hashOID = opts.PrehashOID + } + if len(hashOID) != 0 { + var err error + if message, err = preHash(hashOID, message); err != nil { + return nil, err + } + indicator = 1 + } if len(message) == 0 { return nil, errors.New("mldsa: empty message") } @@ -359,7 +416,7 @@ func (sk *PrivateKey87) Sign(rand io.Reader, message, context []byte) ([]byte, e } H := sha3.NewSHAKE256() H.Write(sk.tr[:]) - H.Write([]byte{0, byte(len(context))}) + H.Write([]byte{indicator, byte(len(context))}) if len(context) > 0 { H.Write(context) } @@ -370,39 +427,6 @@ func (sk *PrivateKey87) Sign(rand io.Reader, message, context []byte) ([]byte, e return sk.signInternal(seed[:], mu[:]) } -// SignWithPreHash generates a digital signature for the given message -// using the private key and additional context. It uses a given hashing algorithm -// from the OID to pre-hash the message before signing. -// It is similar to Sign but allows for pre-hashing the message. -func (sk *PrivateKey87) SignWithPreHash(rand io.Reader, message, context []byte, oid asn1.ObjectIdentifier) ([]byte, error) { - if len(message) == 0 { - return nil, errors.New("mldsa: empty message") - } - if len(context) > 255 { - return nil, errors.New("mldsa: context too long") - } - preHashValue, err := preHash(oid, message) - if err != nil { - return nil, err - } - var seed [SeedSize]byte - if _, err := io.ReadFull(rand, seed[:]); err != nil { - return nil, err - } - - H := sha3.NewSHAKE256() - H.Write(sk.tr[:]) - H.Write([]byte{1, byte(len(context))}) - if len(context) > 0 { - H.Write(context) - } - H.Write(preHashValue) - var mu [64]byte - H.Read(mu[:]) - - return sk.signInternal(seed[:], mu[:]) -} - // See FIPS 204, Algorithm 7 ML-DSA.Sign_internal() func (sk *PrivateKey87) signInternal(seed, mu []byte) ([]byte, error) { var rho2 [64 + 2]byte @@ -418,7 +442,7 @@ func (sk *PrivateKey87) signInternal(seed, mu []byte) ([]byte, error) { r0NormThreshold := int(gamma2QMinus1Div32 - beta87) // rejection sampling loop - for kappa := 0; ; kappa = kappa + l87 { + for kappa := 0; ; kappa += l87 { // expand mask var ( y [l87]ringElement @@ -513,9 +537,25 @@ func (sk *PrivateKey87) signInternal(seed, mu []byte) ([]byte, error) { } } -// Verify checks the validity of a given signature for a message and context -// using the public key. -func (pk *PublicKey87) Verify(sig []byte, message, context []byte) bool { +// VerifyWithOptions verifies a signature against a message using the public key with additional options. +func (pk *PublicKey87) VerifyWithOptions(sig []byte, message []byte, opts crypto.SignerOpts) bool { + var ( + context []byte + hashOID asn1.ObjectIdentifier + indicator byte = 0 + ) + if opts, ok := opts.(*Options); ok { + context = opts.Context + hashOID = opts.PrehashOID + } + if len(hashOID) != 0 { + var err error + if message, err = preHash(hashOID, message); err != nil { + return false + } + indicator = 1 + } + if len(message) == 0 { return false } @@ -527,7 +567,7 @@ func (pk *PublicKey87) Verify(sig []byte, message, context []byte) bool { } H := sha3.NewSHAKE256() H.Write(pk.tr[:]) - H.Write([]byte{0, byte(len(context))}) + H.Write([]byte{indicator, byte(len(context))}) if len(context) > 0 { H.Write(context) } @@ -538,35 +578,6 @@ func (pk *PublicKey87) Verify(sig []byte, message, context []byte) bool { return pk.verifyInternal(sig, mu[:]) } -// VerifyWithPreHash verifies a signature using a message and additional context. -// It uses a given hashing algorithm from the OID to pre-hash the message before verifying. -func (pk *PublicKey87) VerifyWithPreHash(sig []byte, message, context []byte, oid asn1.ObjectIdentifier) bool { - if len(message) == 0 { - return false - } - if len(context) > 255 { - return false - } - if len(sig) != sigEncodedLen87 { - return false - } - preHashValue, err := preHash(oid, message) - if err != nil { - return false - } - H := sha3.NewSHAKE256() - H.Write(pk.tr[:]) - H.Write([]byte{1, byte(len(context))}) - if len(context) > 0 { - H.Write(context) - } - H.Write(preHashValue) - var mu [64]byte - H.Read(mu[:]) - - return pk.verifyInternal(sig, mu[:]) -} - // See FIPS 204, Algorithm 8 ML-DSA.Verify_internal() func (pk *PublicKey87) verifyInternal(sig, mu []byte) bool { // Decode the signature @@ -622,5 +633,5 @@ func (pk *PublicKey87) verifyInternal(sig, mu []byte) bool { var cTilde1 [lambda256 / 4]byte H.Read(cTilde1[:]) return subtle.ConstantTimeLessOrEq(int(gamma1TwoPower19-beta87), zNorm) == 0 && - subtle.ConstantTimeCompare(cTilde[:], cTilde1[:]) == 1 + subtle.ConstantTimeCompare(cTilde, cTilde1[:]) == 1 } diff --git a/mldsa/mldsa87_test.go b/mldsa/mldsa87_test.go index 95554d6..4dce505 100644 --- a/mldsa/mldsa87_test.go +++ b/mldsa/mldsa87_test.go @@ -46,7 +46,7 @@ func TestKeyGen87(t *testing.T) { if err != nil { t.Fatalf("NewPrivateKey65 failed: %v", err) } - pub := priv.PublicKey() + pub := priv.Public().(*PublicKey87) pubBytes := pub.Bytes() if !bytes.Equal(pubBytes, pk) { t.Errorf("Public key mismatch: got %x, want %x", pubBytes, pk) @@ -70,6 +70,10 @@ func TestKeyGen87(t *testing.T) { if !priv.Equal(priv2) { t.Errorf("Private key not equal: got %x, want %x", privBytes, priv2.Bytes()) } + pub3 := priv2.Public() + if !pub.Equal(pub3) { + t.Errorf("Public key from private key not equal") + } } } @@ -184,7 +188,7 @@ func TestSignWithPreHash87(t *testing.T) { if err != nil { t.Fatalf("NewPrivateKey87 failed: %v", err) } - sig2, err := priv.SignWithPreHash(zeroReader, msg, context, c.oid) + sig2, err := priv.Sign(zeroReader, msg, &Options{context, c.oid}) if err != nil { t.Fatalf("failed to sign: %v", err) } @@ -201,7 +205,7 @@ func TestSignWithPreHash87(t *testing.T) { if err != nil { t.Fatalf("NewPublicKey87 failed: %v", err) } - if !pub.VerifyWithPreHash(sig, msg, context, c.oid) { + if !pub.VerifyWithOptions(sig, msg, &Options{context, c.oid}) { t.Error("signature verification failed") } } @@ -254,7 +258,7 @@ func TestVerify87(t *testing.T) { if err != nil { t.Fatalf("NewPublicKey87 failed: %v", err) } - if pub.Verify(sig, msg, ctx) != c.passed { + if pub.VerifyWithOptions(sig, msg, &Options{Context: ctx}) != c.passed { t.Errorf("Verify failed") } } @@ -301,10 +305,11 @@ func BenchmarkVerify87(b *testing.B) { if err != nil { b.Fatalf("NewPublicKey87 failed: %v", err) } + opts := &Options{Context: ctx} b.ReportAllocs() b.ResetTimer() for b.Loop() { - if !pub.Verify(sig, msg, ctx) { + if !pub.VerifyWithOptions(sig, msg, opts) { b.Errorf("Verify failed") } } diff --git a/mldsa/prehash.go b/mldsa/prehash.go index 9ea55d5..abb63c4 100644 --- a/mldsa/prehash.go +++ b/mldsa/prehash.go @@ -7,6 +7,7 @@ package mldsa import ( + "crypto" "crypto/sha256" "crypto/sha3" "crypto/sha512" @@ -90,3 +91,12 @@ func preHash(oid asn1.ObjectIdentifier, data []byte) ([]byte, error) { oidBytes, _ := asn1.Marshal(oid) return h.Sum(oidBytes), nil } + +type Options struct { + Context []byte + PrehashOID asn1.ObjectIdentifier +} + +func (opts *Options) HashFunc() crypto.Hash { + return crypto.Hash(0) +} diff --git a/slhdsa/dsa.go b/slhdsa/dsa.go index b011a16..d8766f8 100644 --- a/slhdsa/dsa.go +++ b/slhdsa/dsa.go @@ -7,18 +7,42 @@ package slhdsa import ( + "crypto" "errors" + "io" ) +var _ crypto.Signer = (*PrivateKey)(nil) + +type Options struct { + Context []byte + AddRand []byte // optional randomness to be added to the signature. If nil, the signature is deterministic. +} + +func (opts *Options) HashFunc() crypto.Hash { + return crypto.Hash(0) +} + +// Sign produces a signature of the message using the private key. +// It is a wrapper around the SignMessage method, implementing the crypto.Signer interface. +func (sk *PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error) { + return sk.SignMessage(rand, message, opts) +} + // Sign generates a pure SLH-DSA signature for the given message. // The signature is deterministic if the addRand parameter is nil. // If addRand is not nil, it must be of the same length as n. // // See FIPS 205 Algorithm 22 slh_sign -func (sk *PrivateKey) Sign(message, context, addRand []byte) ([]byte, error) { +func (sk *PrivateKey) SignMessage(rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error) { if len(message) == 0 { return nil, errors.New("slhdsa: empty message") } + var context, addRand []byte + if opts, ok := opts.(*Options); ok { + context = opts.Context + addRand = opts.AddRand + } if len(addRand) > 0 && len(addRand) != int(sk.params.n) { return nil, errors.New("slhdsa: addrnd should be nil (deterministic variant) or of length n") } @@ -85,10 +109,14 @@ func (sk *PrivateKey) signInternal(msgPrefix, message, addRand []byte) ([]byte, // Verify verifies a pure SLH-DSA signature for the given message. // // See FIPS 205 Algorithm 24 slh_verify -func (pk *PublicKey) Verify(signature, message, context []byte) bool { +func (pk *PublicKey) VerifyWithOptions(signature, message []byte, opts crypto.SignerOpts) bool { if len(message) == 0 { return false } + var context []byte + if opts, ok := opts.(*Options); ok { + context = opts.Context + } if len(context) > maxContextLen { return false } diff --git a/slhdsa/dsa_test.go b/slhdsa/dsa_test.go index 83a658b..5852b59 100644 --- a/slhdsa/dsa_test.go +++ b/slhdsa/dsa_test.go @@ -75,7 +75,7 @@ func testData(t *testing.T, filename string, tc *slhtest) { if err != nil { t.Fatalf("%v NewPrivateKey(%x) = %v", filename, skBytes, err) } - sig2, err := privKey.Sign(message, context, addRand) + sig2, err := privKey.Sign(nil, message, &Options{context, addRand}) if err != nil { t.Fatalf("%v Sign(%x,%x) = %v", filename, message, context, err) } @@ -104,7 +104,7 @@ func testData(t *testing.T, filename string, tc *slhtest) { if err != nil { t.Fatalf("%v NewPublicKey(%x) = %v", filename, pkBytes, err) } - if !pub.Verify(sigOriginal, message, context) { + if !pub.VerifyWithOptions(sigOriginal, message, &Options{Context: context}) { t.Errorf("%v Verify() = false, want true", filename) } } diff --git a/slhdsa/key.go b/slhdsa/key.go index 07d2855..12bb8c7 100644 --- a/slhdsa/key.go +++ b/slhdsa/key.go @@ -7,6 +7,7 @@ package slhdsa import ( + "crypto" "crypto/sha256" "crypto/sha3" "crypto/sha512" @@ -69,7 +70,7 @@ func (sk *PrivateKey) Bytes() []byte { } // Public returns the public key of the private key. -func (sk *PrivateKey) Public() *PublicKey { +func (sk *PrivateKey) Public() crypto.PublicKey { return &sk.PublicKey } @@ -101,7 +102,7 @@ func GenerateKey(rand io.Reader, params *params) (*PrivateKey, error) { if _, err := io.ReadFull(rand, priv.PublicKey.seed[:params.n]); err != nil { return nil, err } - return generateKeyInernal(priv.seed[:], priv.prf[:], priv.PublicKey.seed[:], params) + return generateKeyInernal(priv.seed[:params.n], priv.prf[:params.n], priv.PublicKey.seed[:params.n], params) } // NewPrivateKey creates a new PrivateKey instance from the provided priv.seed||priv.prf||pub.seed||pub.root and parameters. diff --git a/slhdsa/key_test.go b/slhdsa/key_test.go index 7a02e47..6ee3d9d 100644 --- a/slhdsa/key_test.go +++ b/slhdsa/key_test.go @@ -8,6 +8,7 @@ package slhdsa import ( "bytes" + "crypto/rand" "encoding/hex" "testing" ) @@ -137,3 +138,13 @@ func TestGenerateKeyInternal(t *testing.T) { } } } + +func TestGenerateKey(t *testing.T) { + for _, tc := range keyCases { + _, err := tc.params.GenerateKey(rand.Reader) + if err != nil { + t.Errorf("params.GenerateKey() = %v", err) + continue + } + } +}