mirror of
https://github.com/emmansun/gmsm.git
synced 2025-05-10 19:16:18 +08:00
mldsa: initial implementation NIST FIPS 204
This commit is contained in:
parent
07bf6835b7
commit
90522392f1
97
mldsa/compress.go
Normal file
97
mldsa/compress.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
)
|
||||||
|
|
||||||
|
// power2Round decomposes r into (r1, r0) such that r == r1 * 2^13 + r0 mod q, See FIPS 204, Algorithm 35, Power2Round()
|
||||||
|
//
|
||||||
|
// Note: that this code is more complex than the FIPS 204 spec since it keeps
|
||||||
|
// r0 as a positive number
|
||||||
|
//
|
||||||
|
// r mod +- 2^13 is defined as having a range of -4095..4096
|
||||||
|
//
|
||||||
|
// i.e for r = 0..4096 r1 = 0 and r0 = 0..4096
|
||||||
|
// at r = 4097..8191 r1 = 1 and r0 = -4095..-1
|
||||||
|
// (but since r0 is kept positive it effectively adds q and then reduces by q if needed)
|
||||||
|
// Similarly for the range r = 8192..8192+4096 r1=1 and r0=0..4096
|
||||||
|
// & 12289..16383 r1=2 and r0=-4095..-1
|
||||||
|
func power2Round(r fieldElement) (r1, r0 fieldElement) {
|
||||||
|
r1 = r >> d
|
||||||
|
r0 = r - r1<<d
|
||||||
|
|
||||||
|
const (
|
||||||
|
dv = 1 << d
|
||||||
|
halfDV = dv >> 1
|
||||||
|
)
|
||||||
|
|
||||||
|
r0Adjusted := fieldSub(r0, dv)
|
||||||
|
r1Adjusted := r1 + 1
|
||||||
|
|
||||||
|
// mask is set iff r0 <= (2^(dropped_bits))/2
|
||||||
|
mask := subtle.ConstantTimeLessOrEq(int(r0), halfDV)
|
||||||
|
r0 = fieldElement(subtle.ConstantTimeSelect(mask, int(r0), int(r0Adjusted)))
|
||||||
|
r1 = fieldElement(subtle.ConstantTimeSelect(mask, int(r1), int(r1Adjusted)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// compressHighBits decomposes r into r1 and r0 such that r == r1 * (2 * gamma2) + r0 mod q.
|
||||||
|
// See FIPS 204, Algorithm 37, HighBits.
|
||||||
|
//
|
||||||
|
// r: The input value to decompose, in the range [0, q-1].
|
||||||
|
// gamma2: Depending on the algorithm, gamma2 is either (q-1)/32 or (q-1)/88.
|
||||||
|
// Returns: r1 (the high-order bits).
|
||||||
|
func compressHighBits(r fieldElement, gamma2 uint32) uint32 {
|
||||||
|
// Initial computation of r1
|
||||||
|
r1 := int32((r + 127) >> 7)
|
||||||
|
|
||||||
|
if gamma2 == gamma2QMinus1Div32 {
|
||||||
|
// returns ((ceil(r / 2^7) * (2^10 + 1) + 2^21) / 2^22) mod 2^4
|
||||||
|
r1 = (r1*1025 + (1 << 21)) >> 22
|
||||||
|
r1 &= 15 // r1 mod 2^4
|
||||||
|
return uint32(r1)
|
||||||
|
} else {
|
||||||
|
// Adjust r1 for gamma2 = (q-1)/88
|
||||||
|
r1 = (r1*11275 + (1 << 23)) >> 24
|
||||||
|
// Ensure r1 is within the valid range
|
||||||
|
r1 ^= ((43 - r1) >> 31) & r1
|
||||||
|
return uint32(r1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decompose(r fieldElement, gamma2 uint32) (r1 uint32, r0 int32) {
|
||||||
|
r1 = compressHighBits(r, gamma2)
|
||||||
|
r0 = int32(r) - int32(r1)*int32(gamma2)*2
|
||||||
|
r0 -= ((int32(qMinus1Div2) - r0) >> 31) & q
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// See FIPS 204, Algorithm 40, UseHint(h, r)
|
||||||
|
func useHint(h, r fieldElement, gamma2 uint32) fieldElement {
|
||||||
|
r1, r0 := decompose(r, gamma2)
|
||||||
|
if int(h) == 0 {
|
||||||
|
return fieldElement(r1)
|
||||||
|
}
|
||||||
|
if gamma2 == gamma2QMinus1Div32 {
|
||||||
|
// m = 16, thus |mod m| in the spec turns into |& 15|
|
||||||
|
if r0 > 0 {
|
||||||
|
return fieldElement((r1 + 1) & 15)
|
||||||
|
}
|
||||||
|
return fieldElement((r1 - 1) & 15)
|
||||||
|
} else {
|
||||||
|
// m = 44 if gamma2 = ((q - 1) / 88)
|
||||||
|
if r0 > 0 {
|
||||||
|
if r1 == 43 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return fieldElement(r1 + 1)
|
||||||
|
} else if r1 == 0 {
|
||||||
|
return 43
|
||||||
|
}
|
||||||
|
return fieldElement(r1 - 1)
|
||||||
|
}
|
||||||
|
}
|
87
mldsa/compress_test.go
Normal file
87
mldsa/compress_test.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func _power2Round(r uint32) (r1 uint32, r0 int32) {
|
||||||
|
const rd = 1 << d
|
||||||
|
r = r % q
|
||||||
|
r0 = int32(r % rd)
|
||||||
|
if r0 > int32(rd/2) {
|
||||||
|
r0 -= int32(rd)
|
||||||
|
}
|
||||||
|
r1 = uint32((int32(r) - r0) / int32(rd))
|
||||||
|
if r0 < 0 {
|
||||||
|
r0 += int32(q)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func _decompse(r uint32, gamma2 uint32) (r1 uint32, r0 int32) {
|
||||||
|
r = r % q
|
||||||
|
r0 = int32(r % (2 * gamma2))
|
||||||
|
if r0 > int32(gamma2) {
|
||||||
|
r0 -= 2 * int32(gamma2)
|
||||||
|
}
|
||||||
|
if int32(r)-r0 == q-1 {
|
||||||
|
r1 = 0
|
||||||
|
r0--
|
||||||
|
} else {
|
||||||
|
r1 = uint32(int32(r)-r0) / (2 * gamma2)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPower2Round(t *testing.T) {
|
||||||
|
for i := 0; i <= 1000; i++ {
|
||||||
|
r1, r0 := power2Round(fieldElement(i))
|
||||||
|
expectedR1, expectedR0 := _power2Round(uint32(i))
|
||||||
|
if r1 != fieldElement(expectedR1) {
|
||||||
|
t.Errorf("power2Round(%d) = %d, want %d", i, r1, expectedR1)
|
||||||
|
}
|
||||||
|
if r0 != fieldElement(expectedR0) {
|
||||||
|
t.Errorf("power2Round(%d) = %d, want %d", i, r0, expectedR0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := q - 1001; i < q; i++ {
|
||||||
|
r1, r0 := power2Round(fieldElement(i))
|
||||||
|
expectedR1, expectedR0 := _power2Round(uint32(i))
|
||||||
|
if r1 != fieldElement(expectedR1) {
|
||||||
|
t.Errorf("power2Round(%d) = %d, want %d", i, r1, expectedR1)
|
||||||
|
}
|
||||||
|
if r0 != fieldElement(expectedR0) {
|
||||||
|
t.Errorf("power2Round(%d) = %d, want %d", i, r0, expectedR0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecompose(t *testing.T) {
|
||||||
|
gammas := []uint32{gamma2QMinus1Div32, gamma2QMinus1Div88}
|
||||||
|
for _, gamma := range gammas {
|
||||||
|
for i := 0; i <= 1000; i++ {
|
||||||
|
r1, r0 := decompose(fieldElement(i), gamma)
|
||||||
|
expectedR1, expectedR0 := _decompse(uint32(i), gamma)
|
||||||
|
if r1 != expectedR1 {
|
||||||
|
t.Errorf("decompose(%d/%d) r1 = %d, want %d", i, gamma, r1, expectedR1)
|
||||||
|
}
|
||||||
|
if r0 != expectedR0 {
|
||||||
|
t.Errorf("decompose(%d/%d) r0 = %d, want %d", i, gamma, r0, expectedR0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := q - 1001; i < q; i++ {
|
||||||
|
r1, r0 := decompose(fieldElement(i), gamma)
|
||||||
|
expectedR1, expectedR0 := _decompse(uint32(i), gamma)
|
||||||
|
if r1 != expectedR1 {
|
||||||
|
t.Errorf("decompose(%d/%d) r1 = %d, want %d", i, gamma, r1, expectedR1)
|
||||||
|
}
|
||||||
|
if r0 != expectedR0 {
|
||||||
|
t.Errorf("decompose(%d/%d) r0 = %d, want %d", i, gamma, r0, expectedR0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
435
mldsa/encoder.go
Normal file
435
mldsa/encoder.go
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/emmansun/gmsm/internal/alias"
|
||||||
|
)
|
||||||
|
|
||||||
|
// simpleBitPack10Bits encodes a polynomial f into a byte slice
|
||||||
|
//
|
||||||
|
// See FIPS 204, Algorithm 16, SimpleBitPack(w, b) where b = 10 bits
|
||||||
|
// i.e. Use 10 bits from each coefficient and pack them into bytes
|
||||||
|
// So every 4 coefficients (c0..c3) fit into 5 bytes.
|
||||||
|
//
|
||||||
|
// |c0||c1||c2||c3|
|
||||||
|
// |\ |\ |\ |\
|
||||||
|
// |8|2 6|4 4|6 2|8|
|
||||||
|
func simpleBitPack10Bits(s []byte, f ringElement) []byte {
|
||||||
|
s, b := alias.SliceForAppend(s, encodingSize10)
|
||||||
|
for i := 0; i < n; i += 4 {
|
||||||
|
var x uint64
|
||||||
|
x |= uint64(f[i])
|
||||||
|
x |= uint64(f[i+1]) << 10
|
||||||
|
x |= uint64(f[i+2]) << 20
|
||||||
|
x |= uint64(f[i+3]) << 30
|
||||||
|
b[0] = uint8(x)
|
||||||
|
b[1] = uint8(x >> 8)
|
||||||
|
b[2] = uint8(x >> 16)
|
||||||
|
b[3] = uint8(x >> 24)
|
||||||
|
b[4] = uint8(x >> 32)
|
||||||
|
b = b[5:]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// simpleBitUnpack10Bits decodes a byte slice into a polynomial f
|
||||||
|
// See FIPS 204, Algorithm 18, SimpleBitUnpack(w, b) where b = 10 bits
|
||||||
|
func simpleBitUnpack10Bits(b []byte, f *ringElement) {
|
||||||
|
const mask = 0x3FF
|
||||||
|
for i := 0; i < n; i += 4 {
|
||||||
|
x := uint64(b[0]) | (uint64(b[1]) << 8) | (uint64(b[2]) << 16) | (uint64(b[3]) << 24) | (uint64(b[4]) << 32)
|
||||||
|
b = b[5:]
|
||||||
|
f[i] = fieldElement(x & mask)
|
||||||
|
f[i+1] = fieldElement((x >> 10) & mask)
|
||||||
|
f[i+2] = fieldElement((x >> 20) & mask)
|
||||||
|
f[i+3] = fieldElement((x >> 30) & mask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// simpleBitPack4Bits encodes a polynomial into a byte string, assuming that all coefficients are
|
||||||
|
// in the range 0..15 (4 bits).
|
||||||
|
//
|
||||||
|
// See FIPS 204, Algorithm 16, SimpleBitPack(w, b) where b = 4 bits
|
||||||
|
//
|
||||||
|
// i.e. Use 4 bits from each coefficient and pack them into bytes
|
||||||
|
// So every 2 coefficients fit into 1 byte.
|
||||||
|
//
|
||||||
|
// This is used to encode w1 when signing with ML-DSA-65 and ML-DSA-87
|
||||||
|
func simpleBitPack4Bits(s []byte, f ringElement) []byte {
|
||||||
|
s, b := alias.SliceForAppend(s, encodingSize4)
|
||||||
|
for i := 0; i < n; i += 2 {
|
||||||
|
b[0] = uint8(f[i]) | (uint8(f[i+1]) << 4)
|
||||||
|
b = b[1:]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// simpleBitPack6Bits encodes a polynomial into a byte string, assuming that all coefficients are
|
||||||
|
// in the range 0..43 (6 bits).
|
||||||
|
//
|
||||||
|
// See FIPS 204, Algorithm 16, SimpleBitPack(w, b) where b = 43
|
||||||
|
//
|
||||||
|
// i.e. Use 6 bits from each coefficient and pack them into bytes
|
||||||
|
// So every 4 coefficients fit into 3 bytes.
|
||||||
|
//
|
||||||
|
// |c0||c1||c2||c3|
|
||||||
|
// | /| /\ /
|
||||||
|
// |6 2|4 4|2 6|
|
||||||
|
//
|
||||||
|
// This is used to encode w1 when signing with ML-DSA-44
|
||||||
|
func simpleBitPack6Bits(s []byte, f ringElement) []byte {
|
||||||
|
s, b := alias.SliceForAppend(s, encodingSize6)
|
||||||
|
for i := 0; i < n; i += 4 {
|
||||||
|
var x uint64
|
||||||
|
x = uint64(f[i])
|
||||||
|
x |= uint64(f[i+1]) << 6
|
||||||
|
x |= uint64(f[i+2]) << 12
|
||||||
|
x |= uint64(f[i+3]) << 18
|
||||||
|
b[0] = uint8(x)
|
||||||
|
b[1] = uint8(x >> 8)
|
||||||
|
b[2] = uint8(x >> 16)
|
||||||
|
|
||||||
|
b = b[3:]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitPackSigned2 encodes a polynomial f into a byte slice, assuming that all
|
||||||
|
// coefficients are in the range -2..2.
|
||||||
|
// See FIPS 204, Algorithm 17, BitPack(w, a, b). where a = b = 2.
|
||||||
|
//
|
||||||
|
// This is used to encode the private key polynomial elements of s1 and s2
|
||||||
|
// for ML-DSA-44 and ML-DSA-87 (i.e. eta = 2)
|
||||||
|
// Use 3 bits from each coefficient and pack them into bytes
|
||||||
|
// So every 8 coefficients fit into 3 bytes.
|
||||||
|
//
|
||||||
|
// |c0 c1 c2 c3 c4 c5 c6 c7|
|
||||||
|
// | / / | | / / | | /
|
||||||
|
// |3 3 2| 1 3 3 1| 2 3 3|
|
||||||
|
func bitPackSigned2(s []byte, f ringElement) []byte {
|
||||||
|
s, b := alias.SliceForAppend(s, encodingSize3)
|
||||||
|
for i := 0; i < n; i += 8 {
|
||||||
|
var x uint32
|
||||||
|
x |= uint32(fieldSub(2, f[i]))
|
||||||
|
x |= uint32(fieldSub(2, f[i+1])) << 3
|
||||||
|
x |= uint32(fieldSub(2, f[i+2])) << 6
|
||||||
|
x |= uint32(fieldSub(2, f[i+3])) << 9
|
||||||
|
x |= uint32(fieldSub(2, f[i+4])) << 12
|
||||||
|
x |= uint32(fieldSub(2, f[i+5])) << 15
|
||||||
|
x |= uint32(fieldSub(2, f[i+6])) << 18
|
||||||
|
x |= uint32(fieldSub(2, f[i+7])) << 21
|
||||||
|
b[0] = uint8(x)
|
||||||
|
b[1] = uint8(x >> 8)
|
||||||
|
b[2] = uint8(x >> 16)
|
||||||
|
b = b[3:]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitUnpackSigned2 decodes a byte slice into a polynomial f
|
||||||
|
// See FIPS 204, Algorithm 19, BitUnpack(w, a, b). where a = b = 2.
|
||||||
|
func bitUnpackSigned2(b []byte) (ringElement, error) {
|
||||||
|
const bitsMask = 0x7
|
||||||
|
var f ringElement
|
||||||
|
for i := 0; i < n; i += 8 {
|
||||||
|
x := uint32(b[0]) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16)
|
||||||
|
msbs := x & 0o44444444
|
||||||
|
mask := (msbs >> 1) | (msbs >> 2)
|
||||||
|
if subtle.ConstantTimeEq(int32(mask&x), 0) == 0 {
|
||||||
|
return ringElement{}, errors.New("mldsa: invalid encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
b = b[3:]
|
||||||
|
f[i] = fieldSub(2, fieldElement(x&bitsMask))
|
||||||
|
f[i+1] = fieldSub(2, fieldElement((x>>3)&bitsMask))
|
||||||
|
f[i+2] = fieldSub(2, fieldElement((x>>6)&bitsMask))
|
||||||
|
f[i+3] = fieldSub(2, fieldElement((x>>9)&bitsMask))
|
||||||
|
f[i+4] = fieldSub(2, fieldElement((x>>12)&bitsMask))
|
||||||
|
f[i+5] = fieldSub(2, fieldElement((x>>15)&bitsMask))
|
||||||
|
f[i+6] = fieldSub(2, fieldElement((x>>18)&bitsMask))
|
||||||
|
f[i+7] = fieldSub(2, fieldElement((x>>21)&bitsMask))
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// bitPackSigned4 encodes a polynomial into a byte string, assuming that all
|
||||||
|
// coefficients are in the range -4..4.
|
||||||
|
// See FIPS 204, Algorithm 17, BitPack(w, a, b). (a = 4, b = 4)
|
||||||
|
//
|
||||||
|
// It uses a nibble from each coefficient and packs them into bytes
|
||||||
|
// So every 2 coefficients fit into 1 byte.
|
||||||
|
//
|
||||||
|
// This is used to encode the private key polynomial elements of s1 and s2
|
||||||
|
// for ML-DSA-65 (i.e. eta = 4)
|
||||||
|
func bitPackSigned4(s []byte, f ringElement) []byte {
|
||||||
|
s, b := alias.SliceForAppend(s, encodingSize4)
|
||||||
|
for i := 0; i < n; i += 2 {
|
||||||
|
b[0] = uint8(fieldSub(4, f[i])) | (uint8(fieldSub(4, f[i+1])) << 4)
|
||||||
|
b = b[1:]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitUnpackSigned4 reverses the procedure of bitPackSigned4().
|
||||||
|
// See FIPS 204, Algorithm 19, BitUnpack(v, a, b) where a = b = 4.
|
||||||
|
func bitUnpackSigned4(b []byte) (ringElement, error) {
|
||||||
|
const bitsMask = 0xF
|
||||||
|
var f ringElement
|
||||||
|
for i := 0; i < n; i += 8 {
|
||||||
|
x := uint32(b[0]) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24)
|
||||||
|
// None of the nibbles may be >= 9. So if the MSB of any nibble is set,
|
||||||
|
// none of the other bits may be set. First, select all the MSBs.
|
||||||
|
msbs := x & 0x88888888
|
||||||
|
// For each nibble where the MSB is set, form a mask of all the other bits.
|
||||||
|
mask := (msbs >> 1) | (msbs >> 2) | (msbs >> 3)
|
||||||
|
if subtle.ConstantTimeEq(int32(mask&x), 0) == 0 {
|
||||||
|
return ringElement{}, errors.New("mldsa: invalid encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
b = b[4:]
|
||||||
|
f[i] = fieldSub(4, fieldElement(x&bitsMask))
|
||||||
|
f[i+1] = fieldSub(4, fieldElement((x>>4)&bitsMask))
|
||||||
|
f[i+2] = fieldSub(4, fieldElement((x>>8)&bitsMask))
|
||||||
|
f[i+3] = fieldSub(4, fieldElement((x>>12)&bitsMask))
|
||||||
|
f[i+4] = fieldSub(4, fieldElement((x>>16)&bitsMask))
|
||||||
|
f[i+5] = fieldSub(4, fieldElement((x>>20)&bitsMask))
|
||||||
|
f[i+6] = fieldSub(4, fieldElement((x>>24)&bitsMask))
|
||||||
|
f[i+7] = fieldSub(4, fieldElement((x>>28)&bitsMask))
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// bitPackSigned4196 encodes a polynomial f into a byte slice, assuming that all
|
||||||
|
// coefficients are in the range (-2^12 + 1)..2^12.
|
||||||
|
// See FIPS 204, Algorithm 17, BitPack(w, a, b). where a = 2^12 - 1, b = 2^12.
|
||||||
|
//
|
||||||
|
// This is used to encode the LSB of the public key polynomial elements of t0
|
||||||
|
// which are encoded as part of the encoded private key.
|
||||||
|
//
|
||||||
|
// The code below packs them into 2 64 bits blocks by doing..
|
||||||
|
//
|
||||||
|
// z0 z1 z2 z3 z4 z5 z6 z7 0
|
||||||
|
// | | | | / \ | | | |
|
||||||
|
//
|
||||||
|
// |13 13 13 13 12 |1 13 13 13 24
|
||||||
|
func bitPackSigned4096(s []byte, f ringElement) []byte {
|
||||||
|
const r = 4096 // 2^12
|
||||||
|
s, b := alias.SliceForAppend(s, encodingSize13)
|
||||||
|
for i := 0; i < n; i += 8 {
|
||||||
|
var x1, x2, a uint64
|
||||||
|
x1 = uint64(fieldSub(r, f[i]))
|
||||||
|
x1 |= uint64(fieldSub(r, f[i+1])) << 13
|
||||||
|
x1 |= uint64(fieldSub(r, f[i+2])) << 26
|
||||||
|
x1 |= uint64(fieldSub(r, f[i+3])) << 39
|
||||||
|
a = uint64(fieldSub(r, f[i+4]))
|
||||||
|
x1 |= a << 52
|
||||||
|
x2 = a >> 12
|
||||||
|
x2 |= uint64(fieldSub(r, f[i+5])) << 1
|
||||||
|
x2 |= uint64(fieldSub(r, f[i+6])) << 14
|
||||||
|
x2 |= uint64(fieldSub(r, f[i+7])) << 27
|
||||||
|
b[0] = uint8(x1)
|
||||||
|
b[1] = uint8(x1 >> 8)
|
||||||
|
b[2] = uint8(x1 >> 16)
|
||||||
|
b[3] = uint8(x1 >> 24)
|
||||||
|
b[4] = uint8(x1 >> 32)
|
||||||
|
b[5] = uint8(x1 >> 40)
|
||||||
|
b[6] = uint8(x1 >> 48)
|
||||||
|
b[7] = uint8(x1 >> 56)
|
||||||
|
b[8] = uint8(x2)
|
||||||
|
b[9] = uint8(x2 >> 8)
|
||||||
|
b[10] = uint8(x2 >> 16)
|
||||||
|
b[11] = uint8(x2 >> 24)
|
||||||
|
b[12] = uint8(x2 >> 32)
|
||||||
|
|
||||||
|
b = b[13:]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitUnpackSigned4096 decodes a byte slice into a polynomial f
|
||||||
|
// See FIPS 204, Algorithm 19, BitUnpack(w, a, b). where a = 2^12 - 1, b = 2^12.
|
||||||
|
func bitUnpackSigned4096(b []byte, f *ringElement) error {
|
||||||
|
const bitsMask = 0x1FFF // 2^13-1
|
||||||
|
const r = 4096 // 2^12
|
||||||
|
for i := 0; i < n; i += 8 {
|
||||||
|
x1 := uint64(b[0]) | (uint64(b[1]) << 8) | (uint64(b[2]) << 16) | (uint64(b[3]) << 24) | (uint64(b[4]) << 32) | (uint64(b[5]) << 40) | (uint64(b[6]) << 48) | (uint64(b[7]) << 56)
|
||||||
|
x2 := uint64(b[8]) | (uint64(b[9]) << 8) | (uint64(b[10]) << 16) | (uint64(b[11]) << 24) | (uint64(b[12]) << 32)
|
||||||
|
b = b[13:]
|
||||||
|
f[i] = fieldSub(r, fieldElement(x1&bitsMask))
|
||||||
|
f[i+1] = fieldSub(r, fieldElement((x1>>13)&bitsMask))
|
||||||
|
f[i+2] = fieldSub(r, fieldElement((x1>>26)&bitsMask))
|
||||||
|
f[i+3] = fieldSub(r, fieldElement((x1>>39)&bitsMask))
|
||||||
|
f[i+4] = fieldSub(r, fieldElement((x1>>52 | (x2 << 12 & bitsMask))))
|
||||||
|
f[i+5] = fieldSub(r, fieldElement((x2>>1)&bitsMask))
|
||||||
|
f[i+6] = fieldSub(r, fieldElement((x2>>14)&bitsMask))
|
||||||
|
f[i+7] = fieldSub(r, fieldElement((x2>>27)&bitsMask))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitPackSignedTwoPower17 encodes a polynomial into a byte string, assuming that all
|
||||||
|
// coefficients are in the range (-2^17 + 1)..2^17.
|
||||||
|
// See FIPS 204, Algorithm 17, BitPack(w, a, b). where a = 2^17 - 1, b = 2^17.
|
||||||
|
//
|
||||||
|
// This is used to encode signatures for ML-DSA-44 (where gamma1 = 2^17)
|
||||||
|
//
|
||||||
|
// # Use 18 bits from each coefficient and pack them into bytes
|
||||||
|
//
|
||||||
|
// The code below packs every 4 (18 bit) coefficients into 9 bytes
|
||||||
|
//
|
||||||
|
// z0 z1 z2 z3
|
||||||
|
// | |\ | | \
|
||||||
|
//
|
||||||
|
// |18 14|4 18 10| 8
|
||||||
|
func bitPackSignedTwoPower17(s []byte, f ringElement) []byte {
|
||||||
|
const r = 131072 // 2^17
|
||||||
|
s, b := alias.SliceForAppend(s, encodingSize18)
|
||||||
|
for i := 0; i < n; i += 4 {
|
||||||
|
var x1, x2 uint64
|
||||||
|
x1 = uint64(fieldSub(r, f[i]))
|
||||||
|
x1 |= uint64(fieldSub(r, f[i+1])) << 18
|
||||||
|
x1 |= uint64(fieldSub(r, f[i+2])) << 36
|
||||||
|
x2 = uint64(fieldSub(r, f[i+3]))
|
||||||
|
x1 |= x2 << 54
|
||||||
|
x2 >>= 10
|
||||||
|
b[0] = uint8(x1)
|
||||||
|
b[1] = uint8(x1 >> 8)
|
||||||
|
b[2] = uint8(x1 >> 16)
|
||||||
|
b[3] = uint8(x1 >> 24)
|
||||||
|
b[4] = uint8(x1 >> 32)
|
||||||
|
b[5] = uint8(x1 >> 40)
|
||||||
|
b[6] = uint8(x1 >> 48)
|
||||||
|
b[7] = uint8(x1 >> 56)
|
||||||
|
b[8] = uint8(x2)
|
||||||
|
|
||||||
|
b = b[9:]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitUnpackSignedTwoPower17 decodes a byte slice into a polynomial f
|
||||||
|
// See FIPS 204, Algorithm 19, BitUnpack(w, a, b). where a = 2^17 - 1, b = 2^17.
|
||||||
|
func bitUnpackSignedTwoPower17(b []byte, f *ringElement) {
|
||||||
|
const bitsMask = 0x3FFFF // 2^18-1
|
||||||
|
const r = 131072 // 2^17
|
||||||
|
for i := 0; i < n; i += 4 {
|
||||||
|
x1 := uint64(b[0]) | (uint64(b[1]) << 8) | (uint64(b[2]) << 16) | (uint64(b[3]) << 24) | (uint64(b[4]) << 32) | (uint64(b[5]) << 40) | (uint64(b[6]) << 48) | (uint64(b[7]) << 56)
|
||||||
|
x2 := uint64(b[8])
|
||||||
|
b = b[9:]
|
||||||
|
f[i] = fieldSub(r, fieldElement(x1&bitsMask))
|
||||||
|
f[i+1] = fieldSub(r, fieldElement((x1>>18)&bitsMask))
|
||||||
|
f[i+2] = fieldSub(r, fieldElement((x1>>36)&bitsMask))
|
||||||
|
f[i+3] = fieldSub(r, fieldElement((x1>>54 | (x2 << 10 & bitsMask))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitPackSignedTwoPower19 encodes a polynomial into a byte string, assuming that all
|
||||||
|
// coefficients are in the range (-2^19 + 1)..2^19.
|
||||||
|
// See FIPS 204, Algorithm 17, BitPack(w, a, b). where a = 2^19 - 1, b = 2^19.
|
||||||
|
//
|
||||||
|
// This is used to encode signatures for ML-DSA-65 & ML-DSA-87 (gamma1 = 2^19)
|
||||||
|
//
|
||||||
|
// # Use 20 bits from each coefficient and pack them into bytes
|
||||||
|
//
|
||||||
|
// The code below packs every 4 (20 bit) coefficients into 10 bytes
|
||||||
|
//
|
||||||
|
// z0 z1 z2 z3
|
||||||
|
// | |\ | | \
|
||||||
|
//
|
||||||
|
// |20 12|8 20 4|16
|
||||||
|
func bitPackSignedTwoPower19(s []byte, f ringElement) []byte {
|
||||||
|
const r = 524288 // 2^19
|
||||||
|
s, b := alias.SliceForAppend(s, encodingSize20)
|
||||||
|
for i := 0; i < n; i += 4 {
|
||||||
|
var x1, x2 uint64
|
||||||
|
x1 = uint64(fieldSub(r, f[i]))
|
||||||
|
x1 |= uint64(fieldSub(r, f[i+1])) << 20
|
||||||
|
x1 |= uint64(fieldSub(r, f[i+2])) << 40
|
||||||
|
x2 = uint64(fieldSub(r, f[i+3]))
|
||||||
|
x1 |= x2 << 60
|
||||||
|
x2 >>= 4
|
||||||
|
b[0] = uint8(x1)
|
||||||
|
b[1] = uint8(x1 >> 8)
|
||||||
|
b[2] = uint8(x1 >> 16)
|
||||||
|
b[3] = uint8(x1 >> 24)
|
||||||
|
b[4] = uint8(x1 >> 32)
|
||||||
|
b[5] = uint8(x1 >> 40)
|
||||||
|
b[6] = uint8(x1 >> 48)
|
||||||
|
b[7] = uint8(x1 >> 56)
|
||||||
|
b[8] = uint8(x2)
|
||||||
|
b[9] = uint8(x2 >> 8)
|
||||||
|
|
||||||
|
b = b[10:]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitUnpackSignedTwoPower19 decodes a byte slice into a polynomial f
|
||||||
|
// See FIPS 204, Algorithm 19, BitUnpack(w, a, b). where a = 2^19 - 1, b = 2^19.
|
||||||
|
// The coefficients are in the range (-2^19 + 1)..2^19
|
||||||
|
// and are represented as 20 bits.
|
||||||
|
func bitUnpackSignedTwoPower19(b []byte, f *ringElement) {
|
||||||
|
const bitsMask = 0xFFFFF // 2^20-1
|
||||||
|
const r = 524288 // 2^19
|
||||||
|
for i := 0; i < n; i += 4 {
|
||||||
|
x1 := uint64(b[0]) | (uint64(b[1]) << 8) | (uint64(b[2]) << 16) | (uint64(b[3]) << 24) | (uint64(b[4]) << 32) | (uint64(b[5]) << 40) | (uint64(b[6]) << 48) | (uint64(b[7]) << 56)
|
||||||
|
x2 := uint64(b[8]) | (uint64(b[9]) << 8)
|
||||||
|
b = b[10:]
|
||||||
|
f[i] = fieldSub(r, fieldElement(x1&bitsMask))
|
||||||
|
f[i+1] = fieldSub(r, fieldElement((x1>>20)&bitsMask))
|
||||||
|
f[i+2] = fieldSub(r, fieldElement((x1>>40)&bitsMask))
|
||||||
|
f[i+3] = fieldSub(r, fieldElement((x1>>60 | (x2 << 4 & bitsMask))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// See FIPS 204, Algorithm 20, HintBitPack().
|
||||||
|
func hintBitPack(s []byte, hint []ringElement, omega int) []byte {
|
||||||
|
k := len(hint)
|
||||||
|
s, b := alias.SliceForAppend(s, omega+k)
|
||||||
|
index := 0
|
||||||
|
for i := range k {
|
||||||
|
for j := 0; j < n; j++ {
|
||||||
|
if hint[i][j] != 0 {
|
||||||
|
b[index] = byte(j)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b[omega+i] = byte(index)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// See FIPS 204, Algorithm 21, HintBitUnpack().
|
||||||
|
func hintBitUnpack(b []byte, hint []ringElement, omega int) bool {
|
||||||
|
k := len(hint)
|
||||||
|
index := 0
|
||||||
|
first := 0
|
||||||
|
for i := range k {
|
||||||
|
limit := int(b[omega+i])
|
||||||
|
if limit < index || limit > omega {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
first = index
|
||||||
|
for ; index < limit; index++ {
|
||||||
|
bi := b[index]
|
||||||
|
if index > first && b[index-1] >= bi {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hint[i][bi] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := index; i < omega; i++ {
|
||||||
|
if b[i] != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
254
mldsa/field.go
Normal file
254
mldsa/field.go
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fieldElement is an integer modulo q, an element of ℤ_q. It is always reduced.
|
||||||
|
type fieldElement uint32
|
||||||
|
|
||||||
|
// fieldCheckReduced checks that a value a is < q.
|
||||||
|
//func fieldCheckReduced(a uint32) (fieldElement, error) {
|
||||||
|
// if a >= q {
|
||||||
|
// return 0, errors.New("unreduced field element")
|
||||||
|
// }
|
||||||
|
// return fieldElement(a), nil
|
||||||
|
//}
|
||||||
|
|
||||||
|
// fieldReduceOnce reduces a value a < 2q.
|
||||||
|
func fieldReduceOnce(a uint32) fieldElement {
|
||||||
|
x := a - q
|
||||||
|
// If x underflowed, then x >= 2^32 - q > 2^31, so the top bit is set.
|
||||||
|
x += (x >> 31) * q
|
||||||
|
return fieldElement(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldAdd(a, b fieldElement) fieldElement {
|
||||||
|
x := uint32(a + b)
|
||||||
|
return fieldReduceOnce(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldSub(a, b fieldElement) fieldElement {
|
||||||
|
x := uint32(a - b + q)
|
||||||
|
return fieldReduceOnce(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
qInv = 58728449
|
||||||
|
qNegInv = 4236238847
|
||||||
|
r = 4193792 // 2^32 mod q
|
||||||
|
)
|
||||||
|
|
||||||
|
func fieldReduce(a uint64) fieldElement {
|
||||||
|
t := uint32(a) * qNegInv
|
||||||
|
return fieldReduceOnce(uint32((a + uint64(t)*q) >> 32))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldMul(a, b fieldElement) fieldElement {
|
||||||
|
x := uint64(a) * uint64(b)
|
||||||
|
return fieldReduce(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldMulSub returns a * (b - c). This operation is fused to save a
|
||||||
|
// fieldReduceOnce after the subtraction.
|
||||||
|
func fieldMulSub(a, b, c fieldElement) fieldElement {
|
||||||
|
x := uint64(a) * uint64(b-c+q)
|
||||||
|
return fieldReduce(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ringElement is a polynomial, an element of R_q, represented as an array.
|
||||||
|
type ringElement [n]fieldElement
|
||||||
|
|
||||||
|
// polyAdd adds two ringElements or nttElements.
|
||||||
|
func polyAdd[T ~[n]fieldElement](a, b T) (s T) {
|
||||||
|
for i := range s {
|
||||||
|
s[i] = fieldAdd(a[i], b[i])
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// polySub subtracts two ringElements or nttElements.
|
||||||
|
func polySub[T ~[n]fieldElement](a, b T) (s T) {
|
||||||
|
for i := range s {
|
||||||
|
s[i] = fieldSub(a[i], b[i])
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// nttElement is an NTT representation, an element of T_q, represented as an array.
|
||||||
|
type nttElement [n]fieldElement
|
||||||
|
|
||||||
|
// The table in FIPS 204 Appendix B uses the following formula
|
||||||
|
// zeta[k]= 1753^bitrev(k) mod q for (k = 1..255) (The first value is not used).
|
||||||
|
//
|
||||||
|
// As this implementation uses montgomery form with a multiplier of 2^32.
|
||||||
|
// The values need to be transformed i.e.
|
||||||
|
//
|
||||||
|
// zetasMontgomery[k] = fieldReduce(zeta[k] * (2^32 * 2^32 mod(q)))
|
||||||
|
var zetasMontgomery = [n]fieldElement{
|
||||||
|
4193792, 25847, 5771523, 7861508, 237124, 7602457, 7504169, 466468,
|
||||||
|
1826347, 2353451, 8021166, 6288512, 3119733, 5495562, 3111497, 2680103,
|
||||||
|
2725464, 1024112, 7300517, 3585928, 7830929, 7260833, 2619752, 6271868,
|
||||||
|
6262231, 4520680, 6980856, 5102745, 1757237, 8360995, 4010497, 280005,
|
||||||
|
2706023, 95776, 3077325, 3530437, 6718724, 4788269, 5842901, 3915439,
|
||||||
|
4519302, 5336701, 3574422, 5512770, 3539968, 8079950, 2348700, 7841118,
|
||||||
|
6681150, 6736599, 3505694, 4558682, 3507263, 6239768, 6779997, 3699596,
|
||||||
|
811944, 531354, 954230, 3881043, 3900724, 5823537, 2071892, 5582638,
|
||||||
|
4450022, 6851714, 4702672, 5339162, 6927966, 3475950, 2176455, 6795196,
|
||||||
|
7122806, 1939314, 4296819, 7380215, 5190273, 5223087, 4747489, 126922,
|
||||||
|
3412210, 7396998, 2147896, 2715295, 5412772, 4686924, 7969390, 5903370,
|
||||||
|
7709315, 7151892, 8357436, 7072248, 7998430, 1349076, 1852771, 6949987,
|
||||||
|
5037034, 264944, 508951, 3097992, 44288, 7280319, 904516, 3958618,
|
||||||
|
4656075, 8371839, 1653064, 5130689, 2389356, 8169440, 759969, 7063561,
|
||||||
|
189548, 4827145, 3159746, 6529015, 5971092, 8202977, 1315589, 1341330,
|
||||||
|
1285669, 6795489, 7567685, 6940675, 5361315, 4499357, 4751448, 3839961,
|
||||||
|
2091667, 3407706, 2316500, 3817976, 5037939, 2244091, 5933984, 4817955,
|
||||||
|
266997, 2434439, 7144689, 3513181, 4860065, 4621053, 7183191, 5187039,
|
||||||
|
900702, 1859098, 909542, 819034, 495491, 6767243, 8337157, 7857917,
|
||||||
|
7725090, 5257975, 2031748, 3207046, 4823422, 7855319, 7611795, 4784579,
|
||||||
|
342297, 286988, 5942594, 4108315, 3437287, 5038140, 1735879, 203044,
|
||||||
|
2842341, 2691481, 5790267, 1265009, 4055324, 1247620, 2486353, 1595974,
|
||||||
|
4613401, 1250494, 2635921, 4832145, 5386378, 1869119, 1903435, 7329447,
|
||||||
|
7047359, 1237275, 5062207, 6950192, 7929317, 1312455, 3306115, 6417775,
|
||||||
|
7100756, 1917081, 5834105, 7005614, 1500165, 777191, 2235880, 3406031,
|
||||||
|
7838005, 5548557, 6709241, 6533464, 5796124, 4656147, 594136, 4603424,
|
||||||
|
6366809, 2432395, 2454455, 8215696, 1957272, 3369112, 185531, 7173032,
|
||||||
|
5196991, 162844, 1616392, 3014001, 810149, 1652634, 4686184, 6581310,
|
||||||
|
5341501, 3523897, 3866901, 269760, 2213111, 7404533, 1717735, 472078,
|
||||||
|
7953734, 1723600, 6577327, 1910376, 6712985, 7276084, 8119771, 4546524,
|
||||||
|
5441381, 6144432, 7959518, 6094090, 183443, 7403526, 1612842, 4834730,
|
||||||
|
7826001, 3919660, 8332111, 7018208, 3937738, 1400424, 7534263, 1976782,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ntt maps a ringElement to its nttElement representation.
|
||||||
|
//
|
||||||
|
// It implements NTT, according to FIPS 204, Algorithm 41.
|
||||||
|
func ntt(f ringElement) nttElement {
|
||||||
|
k := 1
|
||||||
|
// len: 128, 64, 32, ..., 1
|
||||||
|
for len := 128; len >= 1; len /= 2 {
|
||||||
|
// start
|
||||||
|
for start := 0; start < 256; start += 2 * len {
|
||||||
|
zeta := zetasMontgomery[k]
|
||||||
|
k++
|
||||||
|
// Bounds check elimination hint.
|
||||||
|
f, flen := f[start:start+len], f[start+len:start+len+len]
|
||||||
|
for j := range len {
|
||||||
|
t := fieldMul(zeta, flen[j])
|
||||||
|
flen[j] = fieldSub(f[j], t)
|
||||||
|
f[j] = fieldAdd(f[j], t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nttElement(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inverseNTT maps a nttElement back to the ringElement it represents.
|
||||||
|
//
|
||||||
|
// It implements NTT⁻¹, according to FIPS 204, Algorithm 42.
|
||||||
|
func inverseNTT(f nttElement) ringElement {
|
||||||
|
k := 255
|
||||||
|
for len := 1; len < 256; len *= 2 {
|
||||||
|
for start := 0; start < 256; start += 2 * len {
|
||||||
|
zeta := q - zetasMontgomery[k]
|
||||||
|
k--
|
||||||
|
// Bounds check elimination hint.
|
||||||
|
f, flen := f[start:start+len], f[start+len:start+len+len]
|
||||||
|
for j := range len {
|
||||||
|
t := f[j]
|
||||||
|
f[j] = fieldAdd(t, flen[j])
|
||||||
|
flen[j] = fieldMulSub(zeta, t, flen[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range f {
|
||||||
|
f[i] = fieldMul(f[i], 41978) // 41978 = ((256⁻¹ mod q) * (2^64 mode q)) mode q
|
||||||
|
}
|
||||||
|
return ringElement(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nttMul(f, g nttElement) nttElement {
|
||||||
|
var ret nttElement
|
||||||
|
for i, v := range f {
|
||||||
|
ret[i] = fieldMul(v, g[i])
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// infinityNorm returns the absolute value modulo q in constant time
|
||||||
|
//
|
||||||
|
// i.e return x > (q - 1) / 2 ? q - x : x;
|
||||||
|
func infinityNorm(a fieldElement) uint32 {
|
||||||
|
ret := subtle.ConstantTimeLessOrEq(int(a), qMinus1Div2)
|
||||||
|
return uint32(subtle.ConstantTimeSelect(ret, int(a), int(q-a)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func polyInfinityNorm[T ~[n]fieldElement](a T, norm int) int {
|
||||||
|
for i := range a {
|
||||||
|
left := int(infinityNorm(a[i]))
|
||||||
|
right := int(norm)
|
||||||
|
norm = subtle.ConstantTimeSelect(subtle.ConstantTimeLessOrEq(left, right), right, left)
|
||||||
|
}
|
||||||
|
return norm
|
||||||
|
}
|
||||||
|
|
||||||
|
func vectorInfinityNorm[T ~[n]fieldElement](a []T, norm int) int {
|
||||||
|
for i := range a {
|
||||||
|
left := int(polyInfinityNorm(a[i], norm))
|
||||||
|
right := int(norm)
|
||||||
|
norm = subtle.ConstantTimeSelect(subtle.ConstantTimeLessOrEq(left, right), right, left)
|
||||||
|
}
|
||||||
|
return norm
|
||||||
|
}
|
||||||
|
|
||||||
|
func infinityNormSigned(a int32) int {
|
||||||
|
ret := subtle.ConstantTimeLessOrEq(0x80000000, int(a))
|
||||||
|
return subtle.ConstantTimeSelect(ret, int(-a), int(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
func polyInfinityNormSigned(a []int32, norm int) int {
|
||||||
|
for i := range a {
|
||||||
|
left := int(infinityNormSigned(a[i]))
|
||||||
|
right := norm
|
||||||
|
norm = subtle.ConstantTimeSelect(subtle.ConstantTimeLessOrEq(left, right), right, left)
|
||||||
|
}
|
||||||
|
return norm
|
||||||
|
}
|
||||||
|
|
||||||
|
func vectorInfinityNormSigned(a [][n]int32, norm int) int {
|
||||||
|
for i := range a {
|
||||||
|
left := int(polyInfinityNormSigned(a[i][:], norm))
|
||||||
|
right := norm
|
||||||
|
norm = subtle.ConstantTimeSelect(subtle.ConstantTimeLessOrEq(left, right), right, left)
|
||||||
|
}
|
||||||
|
return norm
|
||||||
|
}
|
||||||
|
|
||||||
|
func vectorCountOnes(a []ringElement) int {
|
||||||
|
var oneCount int
|
||||||
|
for i := range a {
|
||||||
|
for j := range a[i] {
|
||||||
|
oneCount += int(a[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oneCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func vectorMakeHint(ct0, cs2, w, hint []ringElement, gamma2 uint32) {
|
||||||
|
for i := range ct0 {
|
||||||
|
for j := range ct0[i] {
|
||||||
|
hint[i][j] = makeHint(ct0[i][j], cs2[i][j], w[i][j], gamma2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHint(ct0, cs2, w fieldElement, gamma2 uint32) fieldElement {
|
||||||
|
rPulusZ := fieldSub(w, cs2)
|
||||||
|
r := fieldAdd(rPulusZ, ct0)
|
||||||
|
|
||||||
|
return fieldElement(1 ^ uint32(subtle.ConstantTimeEq(int32(compressHighBits(r, gamma2)), int32(compressHighBits(rPulusZ, gamma2)))))
|
||||||
|
}
|
112
mldsa/field_barrett.go
Normal file
112
mldsa/field_barrett.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import "math/bits"
|
||||||
|
|
||||||
|
const (
|
||||||
|
barrettMultiplier = 8396807 // 2^23 * 2^23 / q
|
||||||
|
barrettShift = 46 // log₂(2^23 * 2^23)
|
||||||
|
)
|
||||||
|
|
||||||
|
func fieldBarrettReduce(a uint64) fieldElement {
|
||||||
|
hi, low := bits.Mul64(a, barrettMultiplier)
|
||||||
|
quotient := hi<<18 | low>>barrettShift
|
||||||
|
return fieldReduceOnce(uint32(a - quotient*q))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldBarrettMul(a, b fieldElement) fieldElement {
|
||||||
|
x := uint64(a) * uint64(b)
|
||||||
|
return fieldBarrettReduce(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldBarrettMulSub(a, b, c fieldElement) fieldElement {
|
||||||
|
x := uint64(a) * uint64(b-c+q)
|
||||||
|
return fieldBarrettReduce(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zetas = [n]fieldElement{
|
||||||
|
1, 4808194, 3765607, 3761513, 5178923, 5496691, 5234739, 5178987,
|
||||||
|
7778734, 3542485, 2682288, 2129892, 3764867, 7375178, 557458, 7159240,
|
||||||
|
5010068, 4317364, 2663378, 6705802, 4855975, 7946292, 676590, 7044481,
|
||||||
|
5152541, 1714295, 2453983, 1460718, 7737789, 4795319, 2815639, 2283733,
|
||||||
|
3602218, 3182878, 2740543, 4793971, 5269599, 2101410, 3704823, 1159875,
|
||||||
|
394148, 928749, 1095468, 4874037, 2071829, 4361428, 3241972, 2156050,
|
||||||
|
3415069, 1759347, 7562881, 4805951, 3756790, 6444618, 6663429, 4430364,
|
||||||
|
5483103, 3192354, 556856, 3870317, 2917338, 1853806, 3345963, 1858416,
|
||||||
|
3073009, 1277625, 5744944, 3852015, 4183372, 5157610, 5258977, 8106357,
|
||||||
|
2508980, 2028118, 1937570, 4564692, 2811291, 5396636, 7270901, 4158088,
|
||||||
|
1528066, 482649, 1148858, 5418153, 7814814, 169688, 2462444, 5046034,
|
||||||
|
4213992, 4892034, 1987814, 5183169, 1736313, 235407, 5130263, 3258457,
|
||||||
|
5801164, 1787943, 5989328, 6125690, 3482206, 4197502, 7080401, 6018354,
|
||||||
|
7062739, 2461387, 3035980, 621164, 3901472, 7153756, 2925816, 3374250,
|
||||||
|
1356448, 5604662, 2683270, 5601629, 4912752, 2312838, 7727142, 7921254,
|
||||||
|
348812, 8052569, 1011223, 6026202, 4561790, 6458164, 6143691, 1744507,
|
||||||
|
1753, 6444997, 5720892, 6924527, 2660408, 6600190, 8321269, 2772600,
|
||||||
|
1182243, 87208, 636927, 4415111, 4423672, 6084020, 5095502, 4663471,
|
||||||
|
8352605, 822541, 1009365, 5926272, 6400920, 1596822, 4423473, 4620952,
|
||||||
|
6695264, 4969849, 2678278, 4611469, 4829411, 635956, 8129971, 5925040,
|
||||||
|
4234153, 6607829, 2192938, 6653329, 2387513, 4768667, 8111961, 5199961,
|
||||||
|
3747250, 2296099, 1239911, 4541938, 3195676, 2642980, 1254190, 8368000,
|
||||||
|
2998219, 141835, 8291116, 2513018, 7025525, 613238, 7070156, 6161950,
|
||||||
|
7921677, 6458423, 4040196, 4908348, 2039144, 6500539, 7561656, 6201452,
|
||||||
|
6757063, 2105286, 6006015, 6346610, 586241, 7200804, 527981, 5637006,
|
||||||
|
6903432, 1994046, 2491325, 6987258, 507927, 7192532, 7655613, 6545891,
|
||||||
|
5346675, 8041997, 2647994, 3009748, 5767564, 4148469, 749577, 4357667,
|
||||||
|
3980599, 2569011, 6764887, 1723229, 1665318, 2028038, 1163598, 5011144,
|
||||||
|
3994671, 8368538, 7009900, 3020393, 3363542, 214880, 545376, 7609976,
|
||||||
|
3105558, 7277073, 508145, 7826699, 860144, 3430436, 140244, 6866265,
|
||||||
|
6195333, 3123762, 2358373, 6187330, 5365997, 6663603, 2926054, 7987710,
|
||||||
|
8077412, 3531229, 4405932, 4606686, 1900052, 7598542, 1054478, 7648983,
|
||||||
|
}
|
||||||
|
|
||||||
|
func barrettNTT(f ringElement) nttElement {
|
||||||
|
k := 1
|
||||||
|
// len: 128, 64, 32, ..., 1
|
||||||
|
for len := 128; len >= 1; len /= 2 {
|
||||||
|
// start
|
||||||
|
for start := 0; start < 256; start += 2 * len {
|
||||||
|
zeta := zetas[k]
|
||||||
|
k++
|
||||||
|
// Bounds check elimination hint.
|
||||||
|
f, flen := f[start:start+len], f[start+len:start+len+len]
|
||||||
|
for j := range len {
|
||||||
|
t := fieldBarrettMul(zeta, flen[j])
|
||||||
|
flen[j] = fieldSub(f[j], t)
|
||||||
|
f[j] = fieldAdd(f[j], t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nttElement(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func inverseBarrettNTT(f nttElement) ringElement {
|
||||||
|
k := 255
|
||||||
|
for len := 1; len < 256; len *= 2 {
|
||||||
|
for start := 0; start < 256; start += 2 * len {
|
||||||
|
zeta := q - zetas[k]
|
||||||
|
k--
|
||||||
|
// Bounds check elimination hint.
|
||||||
|
f, flen := f[start:start+len], f[start+len:start+len+len]
|
||||||
|
for j := range len {
|
||||||
|
t := f[j]
|
||||||
|
f[j] = fieldAdd(t, flen[j])
|
||||||
|
flen[j] = fieldBarrettMulSub(zeta, t, flen[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range f {
|
||||||
|
f[i] = fieldBarrettMul(f[i], 8347681) // 8347681 = 256⁻¹ mod q
|
||||||
|
}
|
||||||
|
return ringElement(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
//func nttBarrettMul(f, g nttElement) nttElement {
|
||||||
|
// var ret nttElement
|
||||||
|
// for i, v := range f {
|
||||||
|
// ret[i] = fieldBarrettMul(v, g[i])
|
||||||
|
// }
|
||||||
|
// return ret
|
||||||
|
//}
|
179
mldsa/field_test.go
Normal file
179
mldsa/field_test.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
mathrand "math/rand/v2"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFieldAdd(t *testing.T) {
|
||||||
|
for a := fieldElement(q - 1000); a < q; a++ {
|
||||||
|
for b := fieldElement(q - 1000); b < q; b++ {
|
||||||
|
got := fieldAdd(a, b)
|
||||||
|
exp := (a + b) % q
|
||||||
|
if got != exp {
|
||||||
|
t.Fatalf("%d + %d = %d, expected %d", a, b, got, exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldSub(t *testing.T) {
|
||||||
|
for a := fieldElement(0); a < 2000; a++ {
|
||||||
|
for b := fieldElement(q - 1000); b < q; b++ {
|
||||||
|
got := fieldSub(a, b)
|
||||||
|
exp := (a - b + q) % q
|
||||||
|
if got != exp {
|
||||||
|
t.Fatalf("%d - %d = %d, expected %d", a, b, got, exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldMul(t *testing.T) {
|
||||||
|
for a := fieldElement(q - 1000); a < q; a++ {
|
||||||
|
for b := fieldElement(q - 1000); b < q; b++ {
|
||||||
|
got := fieldMul(fieldElement((uint64(a)*uint64(r))%q), b)
|
||||||
|
exp := fieldElement((uint64(a) * uint64(b)) % q)
|
||||||
|
if got != exp {
|
||||||
|
t.Fatalf("%d * %d = %d, expected %d", a, b, got, exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, z := range zetasMontgomery {
|
||||||
|
fmt.Printf("%v, ", fieldReduce(uint64(z)))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldBarrettMul(t *testing.T) {
|
||||||
|
for a := fieldElement(q - 1000); a < q; a++ {
|
||||||
|
for b := fieldElement(q - 1000); b < q; b++ {
|
||||||
|
got := fieldBarrettMul(a, b)
|
||||||
|
exp := fieldElement((uint64(a) * uint64(b)) % q)
|
||||||
|
if got != exp {
|
||||||
|
t.Fatalf("%d * %d = %d, expected %d", a, b, got, exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomRingElement() ringElement {
|
||||||
|
var r ringElement
|
||||||
|
for i := range r {
|
||||||
|
r[i] = fieldElement(mathrand.IntN(q))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNTT(t *testing.T) {
|
||||||
|
r := randomRingElement()
|
||||||
|
r1 := r
|
||||||
|
r2 := ntt(r)
|
||||||
|
r3 := barrettNTT(r1)
|
||||||
|
for i, v := range r3 {
|
||||||
|
if v != r2[i] {
|
||||||
|
t.Errorf("expected %v, got %v", v, r2[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInverseNTT(t *testing.T) {
|
||||||
|
r := randomRingElement()
|
||||||
|
r1 := r
|
||||||
|
r2 := ntt(r1)
|
||||||
|
r3 := inverseNTT(r2)
|
||||||
|
for i, v := range r {
|
||||||
|
if v != fieldReduce(uint64(r3[i])) {
|
||||||
|
t.Errorf("expected %v, got %v", v, fieldReduce(uint64(r3[i])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInverseBarrettNTT(t *testing.T) {
|
||||||
|
r := randomRingElement()
|
||||||
|
r1 := r
|
||||||
|
r2 := barrettNTT(r1)
|
||||||
|
r3 := inverseBarrettNTT(r2)
|
||||||
|
for i, v := range r {
|
||||||
|
if v != r3[i] {
|
||||||
|
t.Errorf("expected %v, got %v", v, r3[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInfinityNorm(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input fieldElement
|
||||||
|
expected uint32
|
||||||
|
}{
|
||||||
|
{0, 0},
|
||||||
|
{1, 1},
|
||||||
|
{(q - 1) / 2, (q - 1) / 2},
|
||||||
|
{(q-1)/2 + 1, q - 1 - (q-1)/2},
|
||||||
|
{q - 1, 1},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got := infinityNorm(c.input)
|
||||||
|
if got != c.expected {
|
||||||
|
t.Fatalf("infinityNorm(%d) = %d, expected %d", c.input, got, c.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolyInfinityNorm(t *testing.T) {
|
||||||
|
r := randomRingElement()
|
||||||
|
got := polyInfinityNorm(r, 0)
|
||||||
|
var expected int
|
||||||
|
|
||||||
|
for _, v := range r {
|
||||||
|
if v > qMinus1Div2 {
|
||||||
|
v = q - v
|
||||||
|
}
|
||||||
|
if int(v) > expected {
|
||||||
|
expected = int(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got != expected {
|
||||||
|
t.Fatalf("polyInfinityNorm(%v) = %d, expected %d", r, got, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInfinityNormSigned(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input int32
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{0, 0},
|
||||||
|
{1, 1},
|
||||||
|
{-1, 1},
|
||||||
|
{-2, 2},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got := infinityNormSigned(c.input)
|
||||||
|
if got != c.expected {
|
||||||
|
t.Fatalf("infinityNormSigned(%d) = %d, expected %d", c.input, got, c.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolyInfinityNormSigned(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input []int32
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{[]int32{0, 0, 0}, 0},
|
||||||
|
{[]int32{1, 2, 3}, 3},
|
||||||
|
{[]int32{0, -1, -2, -3, 2}, 3},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got := polyInfinityNormSigned(c.input, 0)
|
||||||
|
if got != c.expected {
|
||||||
|
t.Fatalf("polyInfinityNormSigned(%v) = %d, expected %d", c.input, got, c.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
585
mldsa/mldsa44.go
Normal file
585
mldsa/mldsa44.go
Normal file
@ -0,0 +1,585 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go1.24
|
||||||
|
|
||||||
|
// Package mldsa implements the quantum-resistant digital signature algorithm
|
||||||
|
// ML-DSA (Module-Lattice-Based Digital Signature Standard) as specified in [NIST FIPS 204].
|
||||||
|
//
|
||||||
|
// [NIST FIPS 204]: https://doi.org/10.6028/NIST.FIPS.204
|
||||||
|
//
|
||||||
|
// This implementations referenced OpenSSL's implementation of ML-DSA and part of Golang ML-KEM
|
||||||
|
// [OpenSSL ML-DSA]: https://github.com/openssl/openssl/blob/master/crypto/ml_dsa
|
||||||
|
// [Golang ML-KEM]: https://github.com/golang/go/blob/master/src/crypto/internal/fips140/mlkem
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/sha3"
|
||||||
|
"crypto/subtle"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ML-DSA global constants.
|
||||||
|
n = 256
|
||||||
|
q = 8380417 // 2^23 - 2^13 + 1
|
||||||
|
qMinus1Div2 = (q - 1) / 2
|
||||||
|
d = 13 // # of dropped bits from t
|
||||||
|
|
||||||
|
encodingSize10 = n * 10 / 8 // encoding size for bitlen=10
|
||||||
|
encodingSize3 = n * 3 / 8 // encoding size for bitlen=3
|
||||||
|
encodingSize4 = n * 4 / 8 // encoding size for bitlen=4
|
||||||
|
encodingSize6 = n * 6 / 8 // encoding size for bitlen=6
|
||||||
|
encodingSize13 = n * 13 / 8 // encoding size for bitlen=13
|
||||||
|
encodingSize18 = n * 18 / 8 // encoding size for bitlen=18
|
||||||
|
encodingSize20 = n * 20 / 8 // encoding size for bitlen=20
|
||||||
|
|
||||||
|
SeedSize = 32
|
||||||
|
|
||||||
|
gamma2QMinus1Div88 = (q - 1) / 88 // low-order rounding range for ML-DSA-44
|
||||||
|
gamma2QMinus1Div32 = (q - 1) / 32 // low-order rounding range for ML-DSA-65 and ML-DSA-87
|
||||||
|
|
||||||
|
gamma1TwoPower17 = 1 << 17 // coefficient range of y for ML-DSA-44
|
||||||
|
gamma1TwoPower19 = 1 << 19 // coefficient range of y for ML-DSA-65 and ML-DSA-87
|
||||||
|
|
||||||
|
eta2 = 2 // private key range for ML-DSA-44 and ML-DSA-87
|
||||||
|
bitLenOfETA2 = 3
|
||||||
|
eta4 = 4 // private key range for ML-DSA-65
|
||||||
|
bitLenOfETA4 = 4
|
||||||
|
|
||||||
|
lambda128 = 128 // collision strengh of c tilde for ML-DSA-44
|
||||||
|
lambda192 = 192 // collision strength of c tilde for ML-DSA-65
|
||||||
|
lambda256 = 256 // collision strength of c tilde for ML-DSA-87
|
||||||
|
|
||||||
|
tau39 = 39 // security parameter for ML-DSA-44
|
||||||
|
tau49 = 49 // security parameter for ML-DSA-65
|
||||||
|
tau60 = 60 // security parameter for ML-DSA-87
|
||||||
|
|
||||||
|
omega80 = 80 // max# of 1 in the hint for ML-DSA-44
|
||||||
|
omega55 = 55 // max# of 1 in the hint for ML-DSA-65
|
||||||
|
omega75 = 75 // max# of 1 in the hint for ML-DSA-87
|
||||||
|
)
|
||||||
|
|
||||||
|
// ML-DSA-44 parameters.
|
||||||
|
const (
|
||||||
|
k44 = 4
|
||||||
|
l44 = 4
|
||||||
|
beta44 = eta2 * tau39
|
||||||
|
|
||||||
|
PublicKeySize44 = 32 + 32*k44*10
|
||||||
|
PrivateKeySize44 = 32 + 32 + 64 + 32*((k44+l44)*bitLenOfETA2+d*k44)
|
||||||
|
|
||||||
|
sigEncodedLen44 = lambda128/4 + encodingSize18*l44 + omega80 + k44
|
||||||
|
)
|
||||||
|
|
||||||
|
// ML-DSA-65 parameters.
|
||||||
|
const (
|
||||||
|
k65 = 6
|
||||||
|
l65 = 5
|
||||||
|
beta65 = eta4 * tau49
|
||||||
|
|
||||||
|
PublicKeySize65 = 32 + 32*k65*10
|
||||||
|
PrivateKeySize65 = 32 + 32 + 64 + 32*((k65+l65)*bitLenOfETA4+d*k65)
|
||||||
|
|
||||||
|
sigEncodedLen65 = lambda192/4 + encodingSize20*l65 + omega55 + k65
|
||||||
|
)
|
||||||
|
|
||||||
|
// ML-DSA-87 parameters.
|
||||||
|
const (
|
||||||
|
k87 = 8
|
||||||
|
l87 = 7
|
||||||
|
beta87 = eta2 * tau60
|
||||||
|
|
||||||
|
PublicKeySize87 = 32 + 32*k87*10
|
||||||
|
PrivateKeySize87 = 32 + 32 + 64 + 32*((k87+l87)*bitLenOfETA2+d*k87)
|
||||||
|
|
||||||
|
sigEncodedLen87 = lambda256/4 + encodingSize20*l87 + omega75 + k87
|
||||||
|
)
|
||||||
|
|
||||||
|
// A PrivateKey44 is the private key for the ML-DSA-44 signature scheme.
|
||||||
|
type PrivateKey44 struct {
|
||||||
|
rho [32]byte // public random seed
|
||||||
|
k [32]byte // private random seed for signing
|
||||||
|
tr [64]byte // pre-cached public key Hash, H(pk, 64)
|
||||||
|
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.
|
||||||
|
a [k44 * l44]nttElement // a is generated and stored in NTT representation
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
// A PublicKey44 is the public key for the ML-DSA-44 signature scheme.
|
||||||
|
type PublicKey44 struct {
|
||||||
|
rho [32]byte
|
||||||
|
t1 [k44]ringElement
|
||||||
|
tr [64]byte // H(pk, 64), need to further check if public key requires it
|
||||||
|
a [k44 * l44]nttElement // a is generated and stored in NTT representation
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey generates and returns the corresponding public key for the given
|
||||||
|
// Key44 instance.
|
||||||
|
func (sk *Key44) PublicKey() *PublicKey44 {
|
||||||
|
return &PublicKey44{
|
||||||
|
rho: sk.rho,
|
||||||
|
t1: sk.t1,
|
||||||
|
tr: sk.tr,
|
||||||
|
a: sk.a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes converts the PublicKey44 instance into a byte slice.
|
||||||
|
// See FIPS 204, Algorithm 22, pkEncode()
|
||||||
|
func (pk *PublicKey44) Bytes() []byte {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
b := make([]byte, 0, PublicKeySize44)
|
||||||
|
return pk.bytes(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey44) bytes(b []byte) []byte {
|
||||||
|
b = append(b, pk.rho[:]...)
|
||||||
|
for _, f := range pk.t1 {
|
||||||
|
b = simpleBitPack10Bits(b, f)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
b := make([]byte, 0, PrivateKeySize44)
|
||||||
|
return sk.bytes(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey44) bytes(b []byte) []byte {
|
||||||
|
b = append(b, sk.rho[:]...)
|
||||||
|
b = append(b, sk.k[:]...)
|
||||||
|
b = append(b, sk.tr[:]...)
|
||||||
|
for _, f := range sk.s1 {
|
||||||
|
b = bitPackSigned2(b, f)
|
||||||
|
}
|
||||||
|
for _, f := range sk.s2 {
|
||||||
|
b = bitPackSigned2(b, f)
|
||||||
|
}
|
||||||
|
for _, f := range sk.t0 {
|
||||||
|
b = bitPackSigned4096(b, f)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey44) Equal(x any) bool {
|
||||||
|
xx, ok := x.(*PrivateKey44)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKey44 generates a new Key44 (ML-DSA-44) using the provided random source.
|
||||||
|
func GenerateKey44(rand io.Reader) (*Key44, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
sk := &Key44{}
|
||||||
|
return generateKey44(sk, rand)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateKey44(sk *Key44, rand io.Reader) (*Key44, error) {
|
||||||
|
// Generate a random seed.
|
||||||
|
var seed [SeedSize]byte
|
||||||
|
if _, err := io.ReadFull(rand, seed[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dsaKeyGen44(sk, &seed)
|
||||||
|
return sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKey44 creates a new instance of Key44 using the provided seed.
|
||||||
|
func NewKey44(seed []byte) (*Key44, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
sk := &Key44{}
|
||||||
|
return newPrivateKey44FromSeed(sk, seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPrivateKey44FromSeed(sk *Key44, seed []byte) (*Key44, error) {
|
||||||
|
if len(seed) != SeedSize {
|
||||||
|
return nil, errors.New("mldsa: invalid seed length")
|
||||||
|
}
|
||||||
|
xi := (*[32]byte)(seed)
|
||||||
|
dsaKeyGen44(sk, xi)
|
||||||
|
return sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dsaKeyGen44(sk *Key44, xi *[32]byte) {
|
||||||
|
sk.xi = *xi
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(xi[:])
|
||||||
|
H.Write([]byte{k44})
|
||||||
|
H.Write([]byte{l44})
|
||||||
|
K := make([]byte, 128)
|
||||||
|
H.Read(K)
|
||||||
|
rho, rho1 := K[:32], K[32:96]
|
||||||
|
K = K[96:]
|
||||||
|
|
||||||
|
sk.rho = [32]byte(rho)
|
||||||
|
sk.k = [32]byte(K)
|
||||||
|
|
||||||
|
s1 := &sk.s1
|
||||||
|
s2 := &sk.s2
|
||||||
|
// Algorithm 33, ExpandS
|
||||||
|
for s := byte(0); s < l44; s++ {
|
||||||
|
s1[s] = rejBoundedPoly(rho1, eta2, 0, s)
|
||||||
|
}
|
||||||
|
for r := byte(0); r < k44; r++ {
|
||||||
|
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 s := byte(0); s < l44; s++ {
|
||||||
|
A[r*l44+s] = rejNTTPoly(rho, s, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// t = NTT_inv(A' * NTT(s1)) + s2
|
||||||
|
var s1NTT [l44]nttElement
|
||||||
|
var nttT [k44]nttElement
|
||||||
|
for i := range s1 {
|
||||||
|
s1NTT[i] = ntt(s1[i])
|
||||||
|
}
|
||||||
|
for i := range nttT {
|
||||||
|
for j := range s1NTT {
|
||||||
|
nttT[i] = polyAdd(nttT[i], nttMul(s1NTT[j], A[i*l44+j]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var t [k44]ringElement
|
||||||
|
t0 := &sk.t0
|
||||||
|
t1 := &sk.t1
|
||||||
|
for i := range nttT {
|
||||||
|
t[i] = polyAdd(inverseNTT(nttT[i]), s2[i])
|
||||||
|
// compress t
|
||||||
|
for j := range n {
|
||||||
|
t1[i][j], t0[i][j] = power2Round(t[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
H.Reset()
|
||||||
|
ek := sk.PublicKey().Bytes()
|
||||||
|
H.Write(ek)
|
||||||
|
H.Read(sk.tr[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPublicKey44 decode an public key from its encoded form.
|
||||||
|
// See FIPS 204, Algorithm 23 pkDecode()
|
||||||
|
func NewPublicKey44(b []byte) (*PublicKey44, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
pk := &PublicKey44{}
|
||||||
|
return parsePublicKey44(pk, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See FIPS 204, Algorithm 23 pkDecode()
|
||||||
|
func parsePublicKey44(pk *PublicKey44, b []byte) (*PublicKey44, error) {
|
||||||
|
if len(b) != PublicKeySize44 {
|
||||||
|
return nil, errors.New("mldsa: invalid public key length")
|
||||||
|
}
|
||||||
|
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(b)
|
||||||
|
H.Read(pk.tr[:])
|
||||||
|
|
||||||
|
copy(pk.rho[:], b[:32])
|
||||||
|
b = b[32:]
|
||||||
|
for i := range k44 {
|
||||||
|
simpleBitUnpack10Bits(b, &pk.t1[i])
|
||||||
|
b = b[encodingSize10:]
|
||||||
|
}
|
||||||
|
|
||||||
|
A := &pk.a
|
||||||
|
rho := pk.rho[:]
|
||||||
|
// Algorithm 32, ExpandA
|
||||||
|
for r := byte(0); r < k44; r++ {
|
||||||
|
for s := byte(0); s < l44; s++ {
|
||||||
|
A[r*l44+s] = rejNTTPoly(rho, s, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrivateKey44 decode an private key from its encoded form.
|
||||||
|
// See FIPS 204, Algorithm 25 skDecode()
|
||||||
|
func NewPrivateKey44(b []byte) (*PrivateKey44, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
sk := &PrivateKey44{}
|
||||||
|
return parsePrivateKey44(sk, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See FIPS 204, Algorithm 25 skDecode()
|
||||||
|
// Decode a private key from its encoded form.
|
||||||
|
func parsePrivateKey44(sk *PrivateKey44, b []byte) (*PrivateKey44, error) {
|
||||||
|
if len(b) != PrivateKeySize44 {
|
||||||
|
return nil, errors.New("mldsa: invalid private key length")
|
||||||
|
}
|
||||||
|
copy(sk.rho[:], b[:32])
|
||||||
|
copy(sk.k[:], b[32:64])
|
||||||
|
copy(sk.tr[:], b[64:128])
|
||||||
|
b = b[128:]
|
||||||
|
for i := range l44 {
|
||||||
|
f, err := bitUnpackSigned2(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sk.s1[i] = f
|
||||||
|
b = b[encodingSize3:]
|
||||||
|
}
|
||||||
|
for i := range k44 {
|
||||||
|
f, err := bitUnpackSigned2(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sk.s2[i] = f
|
||||||
|
b = b[encodingSize3:]
|
||||||
|
}
|
||||||
|
for i := range k44 {
|
||||||
|
bitUnpackSigned4096(b, &sk.t0[i])
|
||||||
|
b = b[encodingSize13:]
|
||||||
|
}
|
||||||
|
A := &sk.a
|
||||||
|
rho := sk.rho[:]
|
||||||
|
// Algorithm 32, ExpandA
|
||||||
|
for r := byte(0); r < k44; r++ {
|
||||||
|
for s := byte(0); s < l44; s++ {
|
||||||
|
A[r*l44+s] = rejNTTPoly(rho, s, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey44) Sign(rand io.Reader, message, context []byte) ([]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")
|
||||||
|
}
|
||||||
|
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{0, byte(len(context))})
|
||||||
|
if len(context) > 0 {
|
||||||
|
H.Write(context)
|
||||||
|
}
|
||||||
|
H.Write(message)
|
||||||
|
var mu [64]byte
|
||||||
|
H.Read(mu[:])
|
||||||
|
|
||||||
|
return sk.signInternal(seed[:], mu[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey44) signInternal(seed, mu []byte) ([]byte, error) {
|
||||||
|
var s1NTT [l44]nttElement
|
||||||
|
var s2NTT [k44]nttElement
|
||||||
|
var t0NTT [k44]nttElement
|
||||||
|
for i := range s1NTT {
|
||||||
|
s1NTT[i] = ntt(sk.s1[i])
|
||||||
|
}
|
||||||
|
for i := range s2NTT {
|
||||||
|
s2NTT[i] = ntt(sk.s2[i])
|
||||||
|
}
|
||||||
|
for i := range t0NTT {
|
||||||
|
t0NTT[i] = ntt(sk.t0[i])
|
||||||
|
}
|
||||||
|
var rho2 [64 + 2]byte
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(sk.k[:])
|
||||||
|
H.Write(seed[:])
|
||||||
|
H.Write(mu[:])
|
||||||
|
H.Read(rho2[:64])
|
||||||
|
A := &sk.a
|
||||||
|
|
||||||
|
// rejection sampling loop
|
||||||
|
for kappa := 0; ; kappa = kappa + l44 {
|
||||||
|
// expand mask
|
||||||
|
var y [l44]ringElement
|
||||||
|
for i := range l44 {
|
||||||
|
index := kappa + i
|
||||||
|
rho2[64] = byte(index)
|
||||||
|
rho2[65] = byte(index >> 8)
|
||||||
|
y[i] = expandMask(rho2[:], gamma1TwoPower17)
|
||||||
|
}
|
||||||
|
// compute w and w1
|
||||||
|
var w, w1 [k44]ringElement
|
||||||
|
var wNTT [k44]nttElement
|
||||||
|
for i := range w {
|
||||||
|
for j := range y {
|
||||||
|
wNTT[i] = polyAdd(wNTT[i], nttMul(ntt(y[j]), A[i*l44+j]))
|
||||||
|
}
|
||||||
|
w[i] = inverseNTT(wNTT[i])
|
||||||
|
// high bits
|
||||||
|
for j := range w[i] {
|
||||||
|
w1[i][j] = fieldElement(compressHighBits(w[i][j], gamma2QMinus1Div88))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// commitment hash
|
||||||
|
var cTilde [lambda128 / 4]byte
|
||||||
|
var w1Encoded [encodingSize6]byte
|
||||||
|
H.Reset()
|
||||||
|
H.Write(mu[:])
|
||||||
|
for i := range k44 {
|
||||||
|
simpleBitPack6Bits(w1Encoded[:0], w1[i])
|
||||||
|
H.Write(w1Encoded[:])
|
||||||
|
}
|
||||||
|
H.Read(cTilde[:])
|
||||||
|
// verifier's challenge
|
||||||
|
cNTT := ntt(sampleInBall(cTilde[:], tau39))
|
||||||
|
|
||||||
|
var cs1 [l44]ringElement
|
||||||
|
var cs2 [k44]ringElement
|
||||||
|
var z [l44]ringElement
|
||||||
|
var r0 [k44][n]int32
|
||||||
|
// compute <<cs1>> and z = <<cs1>> + y
|
||||||
|
for i := range l44 {
|
||||||
|
cs1[i] = inverseNTT(nttMul(cNTT, s1NTT[i]))
|
||||||
|
z[i] = polyAdd(cs1[i], y[i])
|
||||||
|
}
|
||||||
|
// compute <<cs2>> and r0 = LowBits(w - <<cs2>>)
|
||||||
|
for i := range k44 {
|
||||||
|
cs2[i] = inverseNTT(nttMul(cNTT, s2NTT[i]))
|
||||||
|
for j := range cs2[i] {
|
||||||
|
_, r0[i][j] = decompose(fieldSub(w[i][j], cs2[i][j]), gamma2QMinus1Div88)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zNorm := vectorInfinityNorm(z[:], 0)
|
||||||
|
r0Norm := vectorInfinityNormSigned(r0[:], 0)
|
||||||
|
|
||||||
|
// if zNorm >= gamma1 - beta || r0Norm >= gamma2 - beta, then continue
|
||||||
|
if subtle.ConstantTimeLessOrEq(int(gamma1TwoPower17-beta44), zNorm)|subtle.ConstantTimeLessOrEq(int(gamma2QMinus1Div88-beta44), r0Norm) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// compute <<ct0>>
|
||||||
|
var ct0 [k44]ringElement
|
||||||
|
for i := range k44 {
|
||||||
|
ct0[i] = inverseNTT(nttMul(cNTT, t0NTT[i]))
|
||||||
|
}
|
||||||
|
// compute infinity norm of <<ct0>>
|
||||||
|
ct0Norm := vectorInfinityNorm(ct0[:], 0)
|
||||||
|
// make hint
|
||||||
|
var hints [k44]ringElement
|
||||||
|
vectorMakeHint(ct0[:], cs2[:], w[:], hints[:], gamma2QMinus1Div88)
|
||||||
|
// if the number of 1 in the hint is greater than omega or the infinity norm of <<ct0>> >= gamma2, then continue
|
||||||
|
if (subtle.ConstantTimeLessOrEq(int(omega80+1), vectorCountOnes(hints[:])) | subtle.ConstantTimeLessOrEq(gamma2QMinus1Div88, ct0Norm)) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// signature encoding
|
||||||
|
sig := make([]byte, 0, sigEncodedLen44)
|
||||||
|
sig = append(sig, cTilde[:]...)
|
||||||
|
for i := range l44 {
|
||||||
|
sig = bitPackSignedTwoPower17(sig, z[i])
|
||||||
|
}
|
||||||
|
return hintBitPack(sig, hints[:], omega80), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey44) Verify(sig []byte, message, context []byte) bool {
|
||||||
|
if len(message) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(context) > 255 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(sig) != sigEncodedLen44 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(pk.tr[:])
|
||||||
|
H.Write([]byte{0, byte(len(context))})
|
||||||
|
if len(context) > 0 {
|
||||||
|
H.Write(context)
|
||||||
|
}
|
||||||
|
H.Write(message)
|
||||||
|
var mu [64]byte
|
||||||
|
H.Read(mu[:])
|
||||||
|
|
||||||
|
return pk.verifyInternal(sig, mu[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey44) verifyInternal(sig, mu []byte) bool {
|
||||||
|
// Decode the signature
|
||||||
|
cTilde := sig[:lambda128/4]
|
||||||
|
sig = sig[lambda128/4:]
|
||||||
|
var z [l44]ringElement
|
||||||
|
for i := range l44 {
|
||||||
|
bitUnpackSignedTwoPower17(sig, &z[i])
|
||||||
|
sig = sig[encodingSize18:]
|
||||||
|
}
|
||||||
|
zNorm := vectorInfinityNorm(z[:], 0)
|
||||||
|
var hints [k44]ringElement
|
||||||
|
if !hintBitUnpack(sig, hints[:], omega80) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// verifier's challenge
|
||||||
|
cNTT := ntt(sampleInBall(cTilde[:], tau39))
|
||||||
|
|
||||||
|
// t = t1 * 2^d
|
||||||
|
// tNTT = NTT(t)*cNTT
|
||||||
|
var tNTT [k44]nttElement
|
||||||
|
t := pk.t1
|
||||||
|
for i := range k44 {
|
||||||
|
for j := range t[i] {
|
||||||
|
t[i][j] <<= d
|
||||||
|
}
|
||||||
|
tNTT[i] = nttMul(ntt(t[i]), cNTT)
|
||||||
|
}
|
||||||
|
|
||||||
|
var w1, wApprox [k44]ringElement
|
||||||
|
var zNTT [k44]nttElement
|
||||||
|
for i := range k44 {
|
||||||
|
for j := 0; j < l44; j++ {
|
||||||
|
zNTT[i] = polyAdd(zNTT[i], nttMul(ntt(z[j]), pk.a[i*l44+j]))
|
||||||
|
}
|
||||||
|
zNTT[i] = polySub(zNTT[i], tNTT[i])
|
||||||
|
wApprox[i] = inverseNTT(zNTT[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(mu[:])
|
||||||
|
var w1Encoded [encodingSize6]byte
|
||||||
|
for i := range k44 {
|
||||||
|
for j := range wApprox[i] {
|
||||||
|
w1[i][j] = useHint(hints[i][j], wApprox[i][j], gamma2QMinus1Div88)
|
||||||
|
}
|
||||||
|
simpleBitPack6Bits(w1Encoded[:0], w1[i])
|
||||||
|
H.Write(w1Encoded[:])
|
||||||
|
}
|
||||||
|
var cTilde1 [lambda128 / 4]byte
|
||||||
|
H.Read(cTilde1[:])
|
||||||
|
return subtle.ConstantTimeLessOrEq(int(gamma1TwoPower17-beta44), zNorm) == 0 &&
|
||||||
|
subtle.ConstantTimeCompare(cTilde[:], cTilde1[:]) == 1
|
||||||
|
}
|
190
mldsa/mldsa44_test.go
Normal file
190
mldsa/mldsa44_test.go
Normal file
File diff suppressed because one or more lines are too long
498
mldsa/mldsa65.go
Normal file
498
mldsa/mldsa65.go
Normal file
@ -0,0 +1,498 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go1.24
|
||||||
|
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/sha3"
|
||||||
|
"crypto/subtle"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A PrivateKey65 is the private key for the ML-DSA-65 signature scheme.
|
||||||
|
type PrivateKey65 struct {
|
||||||
|
rho [32]byte // public random seed
|
||||||
|
k [32]byte // private random seed for signing
|
||||||
|
tr [64]byte // pre-cached public key Hash, H(pk, 64)
|
||||||
|
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.
|
||||||
|
a [k65 * l65]nttElement // a is generated and stored in NTT representation
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
// A PublicKey65 is the public key for the ML-DSA-65 signature scheme.
|
||||||
|
type PublicKey65 struct {
|
||||||
|
rho [32]byte
|
||||||
|
t1 [k65]ringElement
|
||||||
|
tr [64]byte // H(pk, 64), need to further check if public key requires it
|
||||||
|
a [k65 * l65]nttElement // a is generated and stored in NTT representation
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey generates and returns the corresponding public key for the given
|
||||||
|
// Key65 instance.
|
||||||
|
func (sk *Key65) PublicKey() *PublicKey65 {
|
||||||
|
return &PublicKey65{
|
||||||
|
rho: sk.rho,
|
||||||
|
t1: sk.t1,
|
||||||
|
tr: sk.tr,
|
||||||
|
a: sk.a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes converts the PublicKey65 instance into a byte slice.
|
||||||
|
// See FIPS 204, Algorithm 22, pkEncode()
|
||||||
|
func (pk *PublicKey65) Bytes() []byte {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
b := make([]byte, 0, PublicKeySize65)
|
||||||
|
return pk.bytes(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey65) bytes(b []byte) []byte {
|
||||||
|
b = append(b, pk.rho[:]...)
|
||||||
|
for _, f := range pk.t1 {
|
||||||
|
b = simpleBitPack10Bits(b, f)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
b := make([]byte, 0, PrivateKeySize65)
|
||||||
|
return sk.bytes(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey65) bytes(b []byte) []byte {
|
||||||
|
b = append(b, sk.rho[:]...)
|
||||||
|
b = append(b, sk.k[:]...)
|
||||||
|
b = append(b, sk.tr[:]...)
|
||||||
|
for _, f := range sk.s1 {
|
||||||
|
b = bitPackSigned4(b, f)
|
||||||
|
}
|
||||||
|
for _, f := range sk.s2 {
|
||||||
|
b = bitPackSigned4(b, f)
|
||||||
|
}
|
||||||
|
for _, f := range sk.t0 {
|
||||||
|
b = bitPackSigned4096(b, f)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey65) Equal(x any) bool {
|
||||||
|
xx, ok := x.(*PrivateKey65)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKey65 generates a new Key65 (ML-DSA-65) using the provided random source.
|
||||||
|
func GenerateKey65(rand io.Reader) (*Key65, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
sk := &Key65{}
|
||||||
|
return generateKey65(sk, rand)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateKey65(sk *Key65, rand io.Reader) (*Key65, error) {
|
||||||
|
// Generate a random seed.
|
||||||
|
var seed [SeedSize]byte
|
||||||
|
if _, err := io.ReadFull(rand, seed[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dsaKeyGen65(sk, &seed)
|
||||||
|
return sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKey65 creates a new instance of Key65 using the provided seed.
|
||||||
|
func NewKey65(seed []byte) (*Key65, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
sk := &Key65{}
|
||||||
|
return newPrivateKey65FromSeed(sk, seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPrivateKey65FromSeed(sk *Key65, seed []byte) (*Key65, error) {
|
||||||
|
if len(seed) != SeedSize {
|
||||||
|
return nil, errors.New("mldsa: invalid seed length")
|
||||||
|
}
|
||||||
|
xi := (*[32]byte)(seed)
|
||||||
|
dsaKeyGen65(sk, xi)
|
||||||
|
return sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dsaKeyGen65(sk *Key65, xi *[32]byte) {
|
||||||
|
sk.xi = *xi
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(xi[:])
|
||||||
|
H.Write([]byte{k65})
|
||||||
|
H.Write([]byte{l65})
|
||||||
|
K := make([]byte, 128)
|
||||||
|
H.Read(K)
|
||||||
|
rho, rho1 := K[:32], K[32:96]
|
||||||
|
K = K[96:]
|
||||||
|
|
||||||
|
sk.rho = [32]byte(rho)
|
||||||
|
sk.k = [32]byte(K)
|
||||||
|
|
||||||
|
s1 := &sk.s1
|
||||||
|
s2 := &sk.s2
|
||||||
|
// Algorithm 33, ExpandS
|
||||||
|
for s := byte(0); s < l65; s++ {
|
||||||
|
s1[s] = rejBoundedPoly(rho1, eta4, 0, s)
|
||||||
|
}
|
||||||
|
for r := byte(0); r < k65; r++ {
|
||||||
|
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 s := byte(0); s < l65; s++ {
|
||||||
|
A[r*l65+s] = rejNTTPoly(rho, s, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// t = NTT_inv(A' * NTT(s1)) + s2
|
||||||
|
var s1NTT [l65]nttElement
|
||||||
|
var nttT [k65]nttElement
|
||||||
|
for i := range s1 {
|
||||||
|
s1NTT[i] = ntt(s1[i])
|
||||||
|
}
|
||||||
|
for i := range nttT {
|
||||||
|
for j := range s1NTT {
|
||||||
|
nttT[i] = polyAdd(nttT[i], nttMul(s1NTT[j], A[i*l65+j]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var t [k65]ringElement
|
||||||
|
t0 := &sk.t0
|
||||||
|
t1 := &sk.t1
|
||||||
|
for i := range nttT {
|
||||||
|
t[i] = polyAdd(inverseNTT(nttT[i]), s2[i])
|
||||||
|
// compress t
|
||||||
|
for j := range n {
|
||||||
|
t1[i][j], t0[i][j] = power2Round(t[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
H.Reset()
|
||||||
|
ek := sk.PublicKey().Bytes()
|
||||||
|
H.Write(ek)
|
||||||
|
H.Read(sk.tr[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPublicKey65 decode an public key from its encoded form.
|
||||||
|
// See FIPS 204, Algorithm 23 pkDecode()
|
||||||
|
func NewPublicKey65(b []byte) (*PublicKey65, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
pk := &PublicKey65{}
|
||||||
|
return parsePublicKey65(pk, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See FIPS 204, Algorithm 23 pkDecode()
|
||||||
|
func parsePublicKey65(pk *PublicKey65, b []byte) (*PublicKey65, error) {
|
||||||
|
if len(b) != PublicKeySize65 {
|
||||||
|
return nil, errors.New("mldsa: invalid public key length")
|
||||||
|
}
|
||||||
|
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(b)
|
||||||
|
H.Read(pk.tr[:])
|
||||||
|
|
||||||
|
copy(pk.rho[:], b[:32])
|
||||||
|
b = b[32:]
|
||||||
|
for i := range k65 {
|
||||||
|
simpleBitUnpack10Bits(b, &pk.t1[i])
|
||||||
|
b = b[encodingSize10:]
|
||||||
|
}
|
||||||
|
|
||||||
|
A := &pk.a
|
||||||
|
rho := pk.rho[:]
|
||||||
|
// Algorithm 32, ExpandA
|
||||||
|
for r := byte(0); r < k65; r++ {
|
||||||
|
for s := byte(0); s < l65; s++ {
|
||||||
|
A[r*l65+s] = rejNTTPoly(rho, s, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrivateKey65 decode an private key from its encoded form.
|
||||||
|
// See FIPS 204, Algorithm 25 skDecode()
|
||||||
|
func NewPrivateKey65(b []byte) (*PrivateKey65, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
sk := &PrivateKey65{}
|
||||||
|
return parsePrivateKey65(sk, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See FIPS 204, Algorithm 25 skDecode()
|
||||||
|
// Decode a private key from its encoded form.
|
||||||
|
func parsePrivateKey65(sk *PrivateKey65, b []byte) (*PrivateKey65, error) {
|
||||||
|
if len(b) != PrivateKeySize65 {
|
||||||
|
return nil, errors.New("mldsa: invalid private key length")
|
||||||
|
}
|
||||||
|
copy(sk.rho[:], b[:32])
|
||||||
|
copy(sk.k[:], b[32:64])
|
||||||
|
copy(sk.tr[:], b[64:128])
|
||||||
|
b = b[128:]
|
||||||
|
for i := range l65 {
|
||||||
|
f, err := bitUnpackSigned4(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sk.s1[i] = f
|
||||||
|
b = b[encodingSize4:]
|
||||||
|
}
|
||||||
|
for i := range k65 {
|
||||||
|
f, err := bitUnpackSigned4(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sk.s2[i] = f
|
||||||
|
b = b[encodingSize4:]
|
||||||
|
}
|
||||||
|
for i := range k65 {
|
||||||
|
bitUnpackSigned4096(b, &sk.t0[i])
|
||||||
|
b = b[encodingSize13:]
|
||||||
|
}
|
||||||
|
A := &sk.a
|
||||||
|
rho := sk.rho[:]
|
||||||
|
// Algorithm 32, ExpandA
|
||||||
|
for r := byte(0); r < k65; r++ {
|
||||||
|
for s := byte(0); s < l65; s++ {
|
||||||
|
A[r*l65+s] = rejNTTPoly(rho, s, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey65) Sign(rand io.Reader, message, context []byte) ([]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")
|
||||||
|
}
|
||||||
|
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{0, byte(len(context))})
|
||||||
|
if len(context) > 0 {
|
||||||
|
H.Write(context)
|
||||||
|
}
|
||||||
|
H.Write(message)
|
||||||
|
var mu [64]byte
|
||||||
|
H.Read(mu[:])
|
||||||
|
|
||||||
|
return sk.signInternal(seed[:], mu[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey65) signInternal(seed, mu []byte) ([]byte, error) {
|
||||||
|
var s1NTT [l65]nttElement
|
||||||
|
var s2NTT [k65]nttElement
|
||||||
|
var t0NTT [k65]nttElement
|
||||||
|
for i := range s1NTT {
|
||||||
|
s1NTT[i] = ntt(sk.s1[i])
|
||||||
|
}
|
||||||
|
for i := range s2NTT {
|
||||||
|
s2NTT[i] = ntt(sk.s2[i])
|
||||||
|
}
|
||||||
|
for i := range t0NTT {
|
||||||
|
t0NTT[i] = ntt(sk.t0[i])
|
||||||
|
}
|
||||||
|
var rho2 [64 + 2]byte
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(sk.k[:])
|
||||||
|
H.Write(seed[:])
|
||||||
|
H.Write(mu[:])
|
||||||
|
H.Read(rho2[:64])
|
||||||
|
A := &sk.a
|
||||||
|
|
||||||
|
// rejection sampling loop
|
||||||
|
for kappa := 0; ; kappa = kappa + l65 {
|
||||||
|
// expand mask
|
||||||
|
var y [l65]ringElement
|
||||||
|
for i := range l65 {
|
||||||
|
index := kappa + i
|
||||||
|
rho2[64] = byte(index)
|
||||||
|
rho2[65] = byte(index >> 8)
|
||||||
|
y[i] = expandMask(rho2[:], gamma1TwoPower19)
|
||||||
|
}
|
||||||
|
// compute w and w1
|
||||||
|
var w, w1 [k65]ringElement
|
||||||
|
var wNTT [k65]nttElement
|
||||||
|
for i := range w {
|
||||||
|
for j := range y {
|
||||||
|
wNTT[i] = polyAdd(wNTT[i], nttMul(ntt(y[j]), A[i*l65+j]))
|
||||||
|
}
|
||||||
|
w[i] = inverseNTT(wNTT[i])
|
||||||
|
// high bits
|
||||||
|
for j := range w[i] {
|
||||||
|
w1[i][j] = fieldElement(compressHighBits(w[i][j], gamma2QMinus1Div32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// commitment hash
|
||||||
|
var cTilde [lambda192 / 4]byte
|
||||||
|
var w1Encoded [encodingSize4]byte
|
||||||
|
H.Reset()
|
||||||
|
H.Write(mu[:])
|
||||||
|
for i := range k65 {
|
||||||
|
simpleBitPack4Bits(w1Encoded[:0], w1[i])
|
||||||
|
H.Write(w1Encoded[:])
|
||||||
|
}
|
||||||
|
H.Read(cTilde[:])
|
||||||
|
// verifier's challenge
|
||||||
|
cNTT := ntt(sampleInBall(cTilde[:], tau49))
|
||||||
|
|
||||||
|
var cs1 [l65]ringElement
|
||||||
|
var cs2 [k65]ringElement
|
||||||
|
var z [l65]ringElement
|
||||||
|
var r0 [k65][n]int32
|
||||||
|
// compute <<cs1>> and z = <<cs1>> + y
|
||||||
|
for i := range l65 {
|
||||||
|
cs1[i] = inverseNTT(nttMul(cNTT, s1NTT[i]))
|
||||||
|
z[i] = polyAdd(cs1[i], y[i])
|
||||||
|
}
|
||||||
|
// compute <<cs2>> and r0 = LowBits(w - <<cs2>>)
|
||||||
|
for i := range k65 {
|
||||||
|
cs2[i] = inverseNTT(nttMul(cNTT, s2NTT[i]))
|
||||||
|
for j := range cs2[i] {
|
||||||
|
_, r0[i][j] = decompose(fieldSub(w[i][j], cs2[i][j]), gamma2QMinus1Div32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zNorm := vectorInfinityNorm(z[:], 0)
|
||||||
|
r0Norm := vectorInfinityNormSigned(r0[:], 0)
|
||||||
|
|
||||||
|
// if zNorm >= gamma1 - beta || r0Norm >= gamma2 - beta, then continue
|
||||||
|
if subtle.ConstantTimeLessOrEq(int(gamma1TwoPower19-beta65), zNorm)|subtle.ConstantTimeLessOrEq(int(gamma2QMinus1Div32-beta65), r0Norm) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// compute <<ct0>>
|
||||||
|
var ct0 [k65]ringElement
|
||||||
|
for i := range k65 {
|
||||||
|
ct0[i] = inverseNTT(nttMul(cNTT, t0NTT[i]))
|
||||||
|
}
|
||||||
|
// compute infinity norm of <<ct0>>
|
||||||
|
ct0Norm := vectorInfinityNorm(ct0[:], 0)
|
||||||
|
// make hint
|
||||||
|
var hints [k65]ringElement
|
||||||
|
vectorMakeHint(ct0[:], cs2[:], w[:], hints[:], gamma2QMinus1Div32)
|
||||||
|
// if the number of 1 in the hint is greater than omega or the infinity norm of <<ct0>> >= gamma2, then continue
|
||||||
|
if (subtle.ConstantTimeLessOrEq(int(omega55+1), vectorCountOnes(hints[:])) | subtle.ConstantTimeLessOrEq(gamma2QMinus1Div32, ct0Norm)) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// signature encoding
|
||||||
|
sig := make([]byte, 0, sigEncodedLen65)
|
||||||
|
sig = append(sig, cTilde[:]...)
|
||||||
|
for i := range l65 {
|
||||||
|
sig = bitPackSignedTwoPower19(sig, z[i])
|
||||||
|
}
|
||||||
|
return hintBitPack(sig, hints[:], omega55), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey65) Verify(sig []byte, message, context []byte) bool {
|
||||||
|
if len(message) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(context) > 255 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(sig) != sigEncodedLen65 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(pk.tr[:])
|
||||||
|
H.Write([]byte{0, byte(len(context))})
|
||||||
|
H.Write(context)
|
||||||
|
H.Write(message)
|
||||||
|
var mu [64]byte
|
||||||
|
H.Read(mu[:])
|
||||||
|
|
||||||
|
return pk.verifyInternal(sig, mu[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey65) verifyInternal(sig, mu []byte) bool {
|
||||||
|
// Decode the signature
|
||||||
|
cTilde := sig[:lambda192/4]
|
||||||
|
sig = sig[lambda192/4:]
|
||||||
|
var z [l65]ringElement
|
||||||
|
for i := range l65 {
|
||||||
|
bitUnpackSignedTwoPower19(sig, &z[i])
|
||||||
|
sig = sig[encodingSize20:]
|
||||||
|
}
|
||||||
|
zNorm := vectorInfinityNorm(z[:], 0)
|
||||||
|
var hints [k65]ringElement
|
||||||
|
if !hintBitUnpack(sig, hints[:], omega55) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// verifier's challenge
|
||||||
|
cNTT := ntt(sampleInBall(cTilde[:], tau49))
|
||||||
|
|
||||||
|
// t = t1 * 2^d
|
||||||
|
// tNTT = NTT(t)*cNTT
|
||||||
|
var tNTT [k65]nttElement
|
||||||
|
t := pk.t1
|
||||||
|
for i := range k65 {
|
||||||
|
for j := range t[i] {
|
||||||
|
t[i][j] <<= d
|
||||||
|
}
|
||||||
|
tNTT[i] = nttMul(ntt(t[i]), cNTT)
|
||||||
|
}
|
||||||
|
|
||||||
|
var w1, wApprox [k65]ringElement
|
||||||
|
var zNTT [k65]nttElement
|
||||||
|
for i := range k65 {
|
||||||
|
for j := 0; j < l65; j++ {
|
||||||
|
zNTT[i] = polyAdd(zNTT[i], nttMul(ntt(z[j]), pk.a[i*l65+j]))
|
||||||
|
}
|
||||||
|
zNTT[i] = polySub(zNTT[i], tNTT[i])
|
||||||
|
wApprox[i] = inverseNTT(zNTT[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(mu[:])
|
||||||
|
var w1Encoded [encodingSize4]byte
|
||||||
|
for i := range k65 {
|
||||||
|
for j := range wApprox[i] {
|
||||||
|
w1[i][j] = useHint(hints[i][j], wApprox[i][j], gamma2QMinus1Div32)
|
||||||
|
}
|
||||||
|
simpleBitPack4Bits(w1Encoded[:0], w1[i])
|
||||||
|
H.Write(w1Encoded[:])
|
||||||
|
}
|
||||||
|
var cTilde1 [lambda192 / 4]byte
|
||||||
|
H.Read(cTilde1[:])
|
||||||
|
return subtle.ConstantTimeLessOrEq(int(gamma1TwoPower19-beta65), zNorm) == 0 &&
|
||||||
|
subtle.ConstantTimeCompare(cTilde[:], cTilde1[:]) == 1
|
||||||
|
}
|
190
mldsa/mldsa65_test.go
Normal file
190
mldsa/mldsa65_test.go
Normal file
File diff suppressed because one or more lines are too long
500
mldsa/mldsa87.go
Normal file
500
mldsa/mldsa87.go
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go1.24
|
||||||
|
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/sha3"
|
||||||
|
"crypto/subtle"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A PrivateKey87 is the private key for the ML-DSA-87 signature scheme.
|
||||||
|
type PrivateKey87 struct {
|
||||||
|
rho [32]byte // public random seed
|
||||||
|
k [32]byte // private random seed for signing
|
||||||
|
tr [64]byte // pre-cached public key Hash, H(pk, 64)
|
||||||
|
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.
|
||||||
|
a [k87 * l87]nttElement // a is generated and stored in NTT representation
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
// A PublicKey87 is the public key for the ML-DSA-87 signature scheme.
|
||||||
|
type PublicKey87 struct {
|
||||||
|
rho [32]byte
|
||||||
|
t1 [k87]ringElement
|
||||||
|
tr [64]byte // H(pk, 64), need to further check if public key requires it
|
||||||
|
a [k87 * l87]nttElement // a is generated and stored in NTT representation
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey generates and returns the corresponding public key for the given
|
||||||
|
// Key87 instance.
|
||||||
|
func (sk *Key87) PublicKey() *PublicKey87 {
|
||||||
|
return &PublicKey87{
|
||||||
|
rho: sk.rho,
|
||||||
|
t1: sk.t1,
|
||||||
|
tr: sk.tr,
|
||||||
|
a: sk.a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes converts the PublicKey87 instance into a byte slice.
|
||||||
|
// See FIPS 204, Algorithm 22, pkEncode()
|
||||||
|
func (pk *PublicKey87) Bytes() []byte {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
b := make([]byte, 0, PublicKeySize87)
|
||||||
|
return pk.bytes(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey87) bytes(b []byte) []byte {
|
||||||
|
b = append(b, pk.rho[:]...)
|
||||||
|
for _, f := range pk.t1 {
|
||||||
|
b = simpleBitPack10Bits(b, f)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
b := make([]byte, 0, PrivateKeySize87)
|
||||||
|
return sk.bytes(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey87) bytes(b []byte) []byte {
|
||||||
|
b = append(b, sk.rho[:]...)
|
||||||
|
b = append(b, sk.k[:]...)
|
||||||
|
b = append(b, sk.tr[:]...)
|
||||||
|
for _, f := range sk.s1 {
|
||||||
|
b = bitPackSigned2(b, f)
|
||||||
|
}
|
||||||
|
for _, f := range sk.s2 {
|
||||||
|
b = bitPackSigned2(b, f)
|
||||||
|
}
|
||||||
|
for _, f := range sk.t0 {
|
||||||
|
b = bitPackSigned4096(b, f)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey87) Equal(x any) bool {
|
||||||
|
xx, ok := x.(*PrivateKey87)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKey87 generates a new Key87 (ML-DSA-87) using the provided random source.
|
||||||
|
func GenerateKey87(rand io.Reader) (*Key87, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
sk := &Key87{}
|
||||||
|
return generateKey87(sk, rand)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateKey87(sk *Key87, rand io.Reader) (*Key87, error) {
|
||||||
|
// Generate a random seed.
|
||||||
|
var seed [SeedSize]byte
|
||||||
|
if _, err := io.ReadFull(rand, seed[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dsaKeyGen87(sk, &seed)
|
||||||
|
return sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKey87 creates a new instance of Key87 using the provided seed.
|
||||||
|
func NewKey87(seed []byte) (*Key87, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
sk := &Key87{}
|
||||||
|
return newPrivateKey87FromSeed(sk, seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPrivateKey87FromSeed(sk *Key87, seed []byte) (*Key87, error) {
|
||||||
|
if len(seed) != SeedSize {
|
||||||
|
return nil, errors.New("mldsa: invalid seed length")
|
||||||
|
}
|
||||||
|
xi := (*[32]byte)(seed)
|
||||||
|
dsaKeyGen87(sk, xi)
|
||||||
|
return sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dsaKeyGen87(sk *Key87, xi *[32]byte) {
|
||||||
|
sk.xi = *xi
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(xi[:])
|
||||||
|
H.Write([]byte{k87})
|
||||||
|
H.Write([]byte{l87})
|
||||||
|
K := make([]byte, 128)
|
||||||
|
H.Read(K)
|
||||||
|
rho, rho1 := K[:32], K[32:96]
|
||||||
|
K = K[96:]
|
||||||
|
|
||||||
|
sk.rho = [32]byte(rho)
|
||||||
|
sk.k = [32]byte(K)
|
||||||
|
|
||||||
|
s1 := &sk.s1
|
||||||
|
s2 := &sk.s2
|
||||||
|
// Algorithm 33, ExpandS
|
||||||
|
for s := byte(0); s < l87; s++ {
|
||||||
|
s1[s] = rejBoundedPoly(rho1, eta2, 0, s)
|
||||||
|
}
|
||||||
|
for r := byte(0); r < k87; r++ {
|
||||||
|
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 s := byte(0); s < l87; s++ {
|
||||||
|
A[r*l87+s] = rejNTTPoly(rho, s, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// t = NTT_inv(A' * NTT(s1)) + s2
|
||||||
|
var s1NTT [l87]nttElement
|
||||||
|
var nttT [k87]nttElement
|
||||||
|
for i := range s1 {
|
||||||
|
s1NTT[i] = ntt(s1[i])
|
||||||
|
}
|
||||||
|
for i := range nttT {
|
||||||
|
for j := range s1NTT {
|
||||||
|
nttT[i] = polyAdd(nttT[i], nttMul(s1NTT[j], A[i*l87+j]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var t [k87]ringElement
|
||||||
|
t0 := &sk.t0
|
||||||
|
t1 := &sk.t1
|
||||||
|
for i := range nttT {
|
||||||
|
t[i] = polyAdd(inverseNTT(nttT[i]), s2[i])
|
||||||
|
// compress t
|
||||||
|
for j := range n {
|
||||||
|
t1[i][j], t0[i][j] = power2Round(t[i][j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
H.Reset()
|
||||||
|
ek := sk.PublicKey().Bytes()
|
||||||
|
H.Write(ek)
|
||||||
|
H.Read(sk.tr[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPublicKey87 decode an public key from its encoded form.
|
||||||
|
// See FIPS 204, Algorithm 23 pkDecode()
|
||||||
|
func NewPublicKey87(b []byte) (*PublicKey87, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
pk := &PublicKey87{}
|
||||||
|
return parsePublicKey87(pk, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See FIPS 204, Algorithm 23 pkDecode()
|
||||||
|
func parsePublicKey87(pk *PublicKey87, b []byte) (*PublicKey87, error) {
|
||||||
|
if len(b) != PublicKeySize87 {
|
||||||
|
return nil, errors.New("mldsa: invalid public key length")
|
||||||
|
}
|
||||||
|
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(b)
|
||||||
|
H.Read(pk.tr[:])
|
||||||
|
|
||||||
|
copy(pk.rho[:], b[:32])
|
||||||
|
b = b[32:]
|
||||||
|
for i := range k87 {
|
||||||
|
simpleBitUnpack10Bits(b, &pk.t1[i])
|
||||||
|
b = b[encodingSize10:]
|
||||||
|
}
|
||||||
|
|
||||||
|
A := &pk.a
|
||||||
|
rho := pk.rho[:]
|
||||||
|
// Algorithm 32, ExpandA
|
||||||
|
for r := byte(0); r < k87; r++ {
|
||||||
|
for s := byte(0); s < l87; s++ {
|
||||||
|
A[r*l87+s] = rejNTTPoly(rho, s, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrivateKey87 decode an private key from its encoded form.
|
||||||
|
// See FIPS 204, Algorithm 25 skDecode()
|
||||||
|
func NewPrivateKey87(b []byte) (*PrivateKey87, error) {
|
||||||
|
// The actual logic is in a separate function to outline this allocation.
|
||||||
|
sk := &PrivateKey87{}
|
||||||
|
return parsePrivateKey87(sk, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See FIPS 204, Algorithm 25 skDecode()
|
||||||
|
// Decode a private key from its encoded form.
|
||||||
|
func parsePrivateKey87(sk *PrivateKey87, b []byte) (*PrivateKey87, error) {
|
||||||
|
if len(b) != PrivateKeySize87 {
|
||||||
|
return nil, errors.New("mldsa: invalid private key length")
|
||||||
|
}
|
||||||
|
copy(sk.rho[:], b[:32])
|
||||||
|
copy(sk.k[:], b[32:64])
|
||||||
|
copy(sk.tr[:], b[64:128])
|
||||||
|
b = b[128:]
|
||||||
|
for i := range l87 {
|
||||||
|
f, err := bitUnpackSigned2(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sk.s1[i] = f
|
||||||
|
b = b[encodingSize3:]
|
||||||
|
}
|
||||||
|
for i := range k87 {
|
||||||
|
f, err := bitUnpackSigned2(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sk.s2[i] = f
|
||||||
|
b = b[encodingSize3:]
|
||||||
|
}
|
||||||
|
for i := range k87 {
|
||||||
|
bitUnpackSigned4096(b, &sk.t0[i])
|
||||||
|
b = b[encodingSize13:]
|
||||||
|
}
|
||||||
|
A := &sk.a
|
||||||
|
rho := sk.rho[:]
|
||||||
|
// Algorithm 32, ExpandA
|
||||||
|
for r := byte(0); r < k87; r++ {
|
||||||
|
for s := byte(0); s < l87; s++ {
|
||||||
|
A[r*l87+s] = rejNTTPoly(rho, s, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey87) Sign(rand io.Reader, message, context []byte) ([]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")
|
||||||
|
}
|
||||||
|
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{0, byte(len(context))})
|
||||||
|
if len(context) > 0 {
|
||||||
|
H.Write(context)
|
||||||
|
}
|
||||||
|
H.Write(message)
|
||||||
|
var mu [64]byte
|
||||||
|
H.Read(mu[:])
|
||||||
|
|
||||||
|
return sk.signInternal(seed[:], mu[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sk *PrivateKey87) signInternal(seed, mu []byte) ([]byte, error) {
|
||||||
|
var s1NTT [l87]nttElement
|
||||||
|
var s2NTT [k87]nttElement
|
||||||
|
var t0NTT [k87]nttElement
|
||||||
|
for i := range s1NTT {
|
||||||
|
s1NTT[i] = ntt(sk.s1[i])
|
||||||
|
}
|
||||||
|
for i := range s2NTT {
|
||||||
|
s2NTT[i] = ntt(sk.s2[i])
|
||||||
|
}
|
||||||
|
for i := range t0NTT {
|
||||||
|
t0NTT[i] = ntt(sk.t0[i])
|
||||||
|
}
|
||||||
|
var rho2 [64 + 2]byte
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(sk.k[:])
|
||||||
|
H.Write(seed[:])
|
||||||
|
H.Write(mu[:])
|
||||||
|
H.Read(rho2[:64])
|
||||||
|
A := &sk.a
|
||||||
|
|
||||||
|
// rejection sampling loop
|
||||||
|
for kappa := 0; ; kappa = kappa + l87 {
|
||||||
|
// expand mask
|
||||||
|
var y [l87]ringElement
|
||||||
|
for i := range l87 {
|
||||||
|
index := kappa + i
|
||||||
|
rho2[64] = byte(index)
|
||||||
|
rho2[65] = byte(index >> 8)
|
||||||
|
y[i] = expandMask(rho2[:], gamma1TwoPower19)
|
||||||
|
}
|
||||||
|
// compute w and w1
|
||||||
|
var w, w1 [k87]ringElement
|
||||||
|
var wNTT [k87]nttElement
|
||||||
|
for i := range w {
|
||||||
|
for j := range y {
|
||||||
|
wNTT[i] = polyAdd(wNTT[i], nttMul(ntt(y[j]), A[i*l87+j]))
|
||||||
|
}
|
||||||
|
w[i] = inverseNTT(wNTT[i])
|
||||||
|
// high bits
|
||||||
|
for j := range w[i] {
|
||||||
|
w1[i][j] = fieldElement(compressHighBits(w[i][j], gamma2QMinus1Div32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// commitment hash
|
||||||
|
var cTilde [lambda256 / 4]byte
|
||||||
|
var w1Encoded [encodingSize4]byte
|
||||||
|
H.Reset()
|
||||||
|
H.Write(mu[:])
|
||||||
|
for i := range k87 {
|
||||||
|
simpleBitPack4Bits(w1Encoded[:0], w1[i])
|
||||||
|
H.Write(w1Encoded[:])
|
||||||
|
}
|
||||||
|
H.Read(cTilde[:])
|
||||||
|
// verifier's challenge
|
||||||
|
cNTT := ntt(sampleInBall(cTilde[:], tau60))
|
||||||
|
|
||||||
|
var cs1 [l87]ringElement
|
||||||
|
var cs2 [k87]ringElement
|
||||||
|
var z [l87]ringElement
|
||||||
|
var r0 [k87][n]int32
|
||||||
|
// compute <<cs1>> and z = <<cs1>> + y
|
||||||
|
for i := range l87 {
|
||||||
|
cs1[i] = inverseNTT(nttMul(cNTT, s1NTT[i]))
|
||||||
|
z[i] = polyAdd(cs1[i], y[i])
|
||||||
|
}
|
||||||
|
// compute <<cs2>> and r0 = LowBits(w - <<cs2>>)
|
||||||
|
for i := range k87 {
|
||||||
|
cs2[i] = inverseNTT(nttMul(cNTT, s2NTT[i]))
|
||||||
|
for j := range cs2[i] {
|
||||||
|
_, r0[i][j] = decompose(fieldSub(w[i][j], cs2[i][j]), gamma2QMinus1Div32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zNorm := vectorInfinityNorm(z[:], 0)
|
||||||
|
r0Norm := vectorInfinityNormSigned(r0[:], 0)
|
||||||
|
|
||||||
|
// if zNorm >= gamma1 - beta || r0Norm >= gamma2 - beta, then continue
|
||||||
|
if subtle.ConstantTimeLessOrEq(int(gamma1TwoPower19-beta87), zNorm)|subtle.ConstantTimeLessOrEq(int(gamma2QMinus1Div32-beta87), r0Norm) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// compute <<ct0>>
|
||||||
|
var ct0 [k87]ringElement
|
||||||
|
for i := range k87 {
|
||||||
|
ct0[i] = inverseNTT(nttMul(cNTT, t0NTT[i]))
|
||||||
|
}
|
||||||
|
// compute infinity norm of <<ct0>>
|
||||||
|
ct0Norm := vectorInfinityNorm(ct0[:], 0)
|
||||||
|
// make hint
|
||||||
|
var hints [k87]ringElement
|
||||||
|
vectorMakeHint(ct0[:], cs2[:], w[:], hints[:], gamma2QMinus1Div32)
|
||||||
|
// if the number of 1 in the hint is greater than omega or the infinity norm of <<ct0>> >= gamma2, then continue
|
||||||
|
if (subtle.ConstantTimeLessOrEq(int(omega75+1), vectorCountOnes(hints[:])) | subtle.ConstantTimeLessOrEq(gamma2QMinus1Div32, ct0Norm)) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// signature encoding
|
||||||
|
sig := make([]byte, 0, sigEncodedLen87)
|
||||||
|
sig = append(sig, cTilde[:]...)
|
||||||
|
for i := range l87 {
|
||||||
|
sig = bitPackSignedTwoPower19(sig, z[i])
|
||||||
|
}
|
||||||
|
return hintBitPack(sig, hints[:], omega75), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey87) Verify(sig []byte, message, context []byte) bool {
|
||||||
|
if len(message) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(context) > 255 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(sig) != sigEncodedLen87 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(pk.tr[:])
|
||||||
|
H.Write([]byte{0, byte(len(context))})
|
||||||
|
if len(context) > 0 {
|
||||||
|
H.Write(context)
|
||||||
|
}
|
||||||
|
H.Write(message)
|
||||||
|
var mu [64]byte
|
||||||
|
H.Read(mu[:])
|
||||||
|
|
||||||
|
return pk.verifyInternal(sig, mu[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pk *PublicKey87) verifyInternal(sig, mu []byte) bool {
|
||||||
|
// Decode the signature
|
||||||
|
cTilde := sig[:lambda256/4]
|
||||||
|
sig = sig[lambda256/4:]
|
||||||
|
var z [l87]ringElement
|
||||||
|
for i := range l87 {
|
||||||
|
bitUnpackSignedTwoPower19(sig, &z[i])
|
||||||
|
sig = sig[encodingSize20:]
|
||||||
|
}
|
||||||
|
zNorm := vectorInfinityNorm(z[:], 0)
|
||||||
|
var hints [k87]ringElement
|
||||||
|
if !hintBitUnpack(sig, hints[:], omega75) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// verifier's challenge
|
||||||
|
cNTT := ntt(sampleInBall(cTilde[:], tau60))
|
||||||
|
|
||||||
|
// t = t1 * 2^d
|
||||||
|
// tNTT = NTT(t)*cNTT
|
||||||
|
var tNTT [k87]nttElement
|
||||||
|
t := pk.t1
|
||||||
|
for i := range k87 {
|
||||||
|
for j := range t[i] {
|
||||||
|
t[i][j] <<= d
|
||||||
|
}
|
||||||
|
tNTT[i] = nttMul(ntt(t[i]), cNTT)
|
||||||
|
}
|
||||||
|
|
||||||
|
var w1, wApprox [k87]ringElement
|
||||||
|
var zNTT [k87]nttElement
|
||||||
|
for i := range k87 {
|
||||||
|
for j := 0; j < l87; j++ {
|
||||||
|
zNTT[i] = polyAdd(zNTT[i], nttMul(ntt(z[j]), pk.a[i*l87+j]))
|
||||||
|
}
|
||||||
|
zNTT[i] = polySub(zNTT[i], tNTT[i])
|
||||||
|
wApprox[i] = inverseNTT(zNTT[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(mu[:])
|
||||||
|
var w1Encoded [encodingSize4]byte
|
||||||
|
for i := range k87 {
|
||||||
|
for j := range wApprox[i] {
|
||||||
|
w1[i][j] = useHint(hints[i][j], wApprox[i][j], gamma2QMinus1Div32)
|
||||||
|
}
|
||||||
|
simpleBitPack4Bits(w1Encoded[:0], w1[i])
|
||||||
|
H.Write(w1Encoded[:])
|
||||||
|
}
|
||||||
|
var cTilde1 [lambda256 / 4]byte
|
||||||
|
H.Read(cTilde1[:])
|
||||||
|
return subtle.ConstantTimeLessOrEq(int(gamma1TwoPower19-beta87), zNorm) == 0 &&
|
||||||
|
subtle.ConstantTimeCompare(cTilde[:], cTilde1[:]) == 1
|
||||||
|
}
|
198
mldsa/mldsa87_test.go
Normal file
198
mldsa/mldsa87_test.go
Normal file
File diff suppressed because one or more lines are too long
168
mldsa/sample.go
Normal file
168
mldsa/sample.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go1.24
|
||||||
|
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha3"
|
||||||
|
"crypto/subtle"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Algorithm 30
|
||||||
|
func rejNTTPoly(rho []byte, s, r byte) nttElement {
|
||||||
|
G := sha3.NewSHAKE128()
|
||||||
|
G.Write(rho)
|
||||||
|
G.Write([]byte{s, r})
|
||||||
|
|
||||||
|
//TODO: optimize to read a block once
|
||||||
|
var buf [3]byte
|
||||||
|
|
||||||
|
var a nttElement
|
||||||
|
var j int
|
||||||
|
|
||||||
|
for {
|
||||||
|
G.Read(buf[:])
|
||||||
|
// Algorithm 14, CoeffFromThreeBytes()
|
||||||
|
d := uint32(buf[0]) | uint32(buf[1])<<8 | ((uint32(buf[2]) & 0x7f) << 16)
|
||||||
|
if d < q {
|
||||||
|
a[j] = fieldElement(d)
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
if j >= len(a) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a constant time version of n % 5
|
||||||
|
// Note that 0xFFFF / 5 = 0x3333, 2 is added to make an over-estimate of 1/5
|
||||||
|
// and then we divide by (0xFFFF + 1)
|
||||||
|
//
|
||||||
|
// from openssl
|
||||||
|
func constantMod5(n uint32) uint32 {
|
||||||
|
return ((n) - 5*(0x3335*(n)>>16))
|
||||||
|
}
|
||||||
|
|
||||||
|
// rejBoundedPoly uses a seed value to generate a polynomial with coefficients in the
|
||||||
|
// range of ((q-eta)..0..eta) using rejection sampling. eta is either 2 or 4.
|
||||||
|
// SHAKE256 is used to absorb the seed, and then samples are squeezed.
|
||||||
|
// See FIPS 204, Algorithm 31, RejBoundedPoly()
|
||||||
|
func rejBoundedPoly(rho []byte, eta int, highByte, lowByte byte) ringElement {
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(rho)
|
||||||
|
H.Write([]byte{lowByte, highByte})
|
||||||
|
|
||||||
|
//TODO: optimize to read a block once
|
||||||
|
var buf [1]byte
|
||||||
|
var a ringElement
|
||||||
|
var j int
|
||||||
|
|
||||||
|
for {
|
||||||
|
H.Read(buf[:])
|
||||||
|
z0 := buf[0] & 0xf
|
||||||
|
z1 := buf[0] >> 4
|
||||||
|
|
||||||
|
if eta == 2 {
|
||||||
|
if subtle.ConstantTimeByteEq(z0, 15) == 0 {
|
||||||
|
a[j] = fieldSub(2, fieldElement(constantMod5(uint32(z0))))
|
||||||
|
j++
|
||||||
|
if j >= len(a) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeByteEq(z1, 15) == 0 {
|
||||||
|
a[j] = fieldSub(2, fieldElement(constantMod5(uint32(z1))))
|
||||||
|
j++
|
||||||
|
if j >= len(a) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if eta == 4 {
|
||||||
|
if subtle.ConstantTimeLessOrEq(int(z0), 8) == 1 {
|
||||||
|
a[j] = fieldSub(4, fieldElement(z0))
|
||||||
|
j++
|
||||||
|
if j >= len(a) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeLessOrEq(int(z1), 8) == 1 {
|
||||||
|
a[j] = fieldSub(4, fieldElement(z1))
|
||||||
|
j++
|
||||||
|
if j >= len(a) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// See FIPS 204, Algorithm 34, ExpandMask()
|
||||||
|
func expandMask(derivedSeed []byte, gamma1 int) (f ringElement) {
|
||||||
|
var nu [32 * 20]byte
|
||||||
|
l := len(nu)
|
||||||
|
if gamma1 == gamma1TwoPower17 {
|
||||||
|
l = 32 * 18
|
||||||
|
}
|
||||||
|
v := nu[:l]
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(derivedSeed)
|
||||||
|
H.Read(v)
|
||||||
|
|
||||||
|
switch gamma1 {
|
||||||
|
case gamma1TwoPower17:
|
||||||
|
bitUnpackSignedTwoPower17(v, &f)
|
||||||
|
case gamma1TwoPower19:
|
||||||
|
bitUnpackSignedTwoPower19(v, &f)
|
||||||
|
default:
|
||||||
|
panic("mldsa: invalid gamma1 value")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// samples a polynomial with coefficients in the range {-1..1}.
|
||||||
|
// The number of non zero values (hamming weight) is given by tau
|
||||||
|
//
|
||||||
|
// See FIPS 204, Algorithm 29, SampleInBall()
|
||||||
|
// This function is assumed to not be constant time.
|
||||||
|
// The algorithm is based on Durstenfeld's version of the Fisher-Yates shuffle.
|
||||||
|
//
|
||||||
|
// Note that the coefficients returned by this implementation are positive
|
||||||
|
// i.e one of q-1, 0, or 1.
|
||||||
|
func sampleInBall(seed []byte, tao int) (f ringElement) {
|
||||||
|
H := sha3.NewSHAKE256()
|
||||||
|
H.Write(seed)
|
||||||
|
|
||||||
|
var buf [64]byte
|
||||||
|
var index byte
|
||||||
|
var signs uint64
|
||||||
|
|
||||||
|
H.Read(buf[:])
|
||||||
|
offset := 8
|
||||||
|
signs = uint64(buf[0]) | uint64(buf[1])<<8 | uint64(buf[2])<<16 | uint64(buf[3])<<24
|
||||||
|
signs |= uint64(buf[4])<<32 | uint64(buf[5])<<40 | uint64(buf[6])<<48 | uint64(buf[7])<<56
|
||||||
|
|
||||||
|
for end := 256 - tao; end < 256; end++ {
|
||||||
|
for {
|
||||||
|
if offset == 64 {
|
||||||
|
H.Read(buf[:])
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
index = buf[offset]
|
||||||
|
offset++
|
||||||
|
if index <= byte(end) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f[end] = f[index]
|
||||||
|
f[index] = fieldSub(1, fieldElement(2*(signs&1)))
|
||||||
|
signs >>= 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
33
mldsa/sample_test.go
Normal file
33
mldsa/sample_test.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2025 Sun Yimin. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build go1.24
|
||||||
|
|
||||||
|
package mldsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSampleInBall(t *testing.T) {
|
||||||
|
var seed [32]byte
|
||||||
|
rand.Reader.Read(seed[:])
|
||||||
|
var count int
|
||||||
|
taus := []int{tau39, tau49, tau60}
|
||||||
|
for _, tau := range taus {
|
||||||
|
for range 1000 {
|
||||||
|
count = 0
|
||||||
|
f := sampleInBall(seed[:], tau)
|
||||||
|
for _, v := range f {
|
||||||
|
if v != 0 {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count != tau {
|
||||||
|
t.Errorf("sampleInBall(%x) = %d, expected %d", seed[:], count, tau)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user