Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,27 @@
|
||||
Copyright 2025 The Go Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,33 @@
|
||||
# mldsa
|
||||
|
||||
Pure V implementation of [ML-DSA](https://csrc.nist.gov/pubs/fips/204/final) (FIPS 204), a post-quantum digital signature algorithm. Supports all three parameter sets (ML-DSA-44, ML-DSA-65, ML-DSA-87).
|
||||
|
||||
> **This is still experimental**
|
||||
> It is verified against NIST ACVP test vectors for [keygen](./nist_keygen_test.v),
|
||||
> [signing](./nist_siggen_test.v), and [verification](./nist_sigver_test.v),
|
||||
> but not yet production-ready.
|
||||
|
||||
## Example
|
||||
|
||||
```v ignore
|
||||
import x.crypto.mldsa
|
||||
|
||||
fn main() {
|
||||
// generate a new ML-DSA-65 key pair
|
||||
sk := mldsa.PrivateKey.generate(.ml_dsa_65)!
|
||||
pk := sk.public_key()
|
||||
|
||||
// sign a message (with an optional context string)
|
||||
msg := 'Hello ML-DSA'.bytes()
|
||||
sig := sk.sign(msg, context: 'not-a-drill')!
|
||||
|
||||
// verify the signature with the same context
|
||||
verified := pk.verify(msg, sig, context: 'not-a-drill')!
|
||||
assert verified // true
|
||||
|
||||
// deterministic signing is also available
|
||||
sig2 := sk.sign(msg, context: 'not-a-drill', deterministic: true)!
|
||||
verified2 := pk.verify(msg, sig2, context: 'not-a-drill')!
|
||||
assert verified2 // true
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,594 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// Ported to V from Go's crypto/internal/fips140/mldsa.
|
||||
module mldsa
|
||||
|
||||
import crypto.internal.subtle
|
||||
import crypto.sha3
|
||||
|
||||
// algo. 22: pkEncode (s. 7.2)
|
||||
@[direct_array_access]
|
||||
fn pk_encode(rho []u8, t1 [][]u16, p Params) []u8 {
|
||||
mut pk := rho.clone()
|
||||
for i in 0 .. p.k {
|
||||
w := t1[i]
|
||||
mut j := 0
|
||||
for j < n {
|
||||
c0 := w[j]
|
||||
c1 := w[j + 1]
|
||||
c2 := w[j + 2]
|
||||
c3 := w[j + 3]
|
||||
pk << u8(c0)
|
||||
pk << u8((c0 >> 8) | (c1 << 2))
|
||||
pk << u8((c1 >> 6) | (c2 << 4))
|
||||
pk << u8((c2 >> 4) | (c3 << 6))
|
||||
pk << u8(c3 >> 2)
|
||||
j += 4
|
||||
}
|
||||
}
|
||||
return pk
|
||||
}
|
||||
|
||||
// algo. 23: pkDecode (s. 7.2)
|
||||
@[direct_array_access]
|
||||
fn pk_decode(pk []u8, p Params) !([]u8, [][]u16) {
|
||||
expected := pub_key_size(p)
|
||||
if pk.len != expected {
|
||||
return error('invalid public key length')
|
||||
}
|
||||
rho := pk[..32].clone()
|
||||
// avoid cloning on each iteration
|
||||
mut data := unsafe { pk[32..] }
|
||||
mut t1 := [][]u16{len: p.k, init: []u16{len: n}}
|
||||
for r in 0 .. p.k {
|
||||
mut j := 0
|
||||
for j < n {
|
||||
b0 := data[0]
|
||||
b1 := data[1]
|
||||
b2 := data[2]
|
||||
b3 := data[3]
|
||||
b4 := data[4]
|
||||
t1[r][j] = u16(b0) | (u16(b1 & 0x03) << 8)
|
||||
t1[r][j + 1] = u16(b1 >> 2) | (u16(b2 & 0x0f) << 6)
|
||||
t1[r][j + 2] = u16(b2 >> 4) | (u16(b3 & 0x3f) << 4)
|
||||
t1[r][j + 3] = u16(b3 >> 6) | (u16(b4) << 2)
|
||||
data = unsafe { data[5..] }
|
||||
j += 4
|
||||
}
|
||||
}
|
||||
return rho, t1
|
||||
}
|
||||
|
||||
fn compute_pk_hash(pk []u8) [64]u8 {
|
||||
return slice_to_64(sha3.shake256(pk, 64))
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn compute_t1_hat(t1 [][]u16) []NttElement {
|
||||
mut result := []NttElement{len: t1.len}
|
||||
for i in 0 .. t1.len {
|
||||
mut w := RingElement{}
|
||||
for j in 0 .. n {
|
||||
// panics if (t1[i][j] << d) >= q, c.f field.v
|
||||
// only called from pk_decode, which produces 10bit vals (<= 1023)
|
||||
// so 1023 << 13 = 8_380_416 = q - 1
|
||||
z := field_to_montgomery(u32(t1[i][j]) << d) or { panic(err) }
|
||||
w[j] = z
|
||||
}
|
||||
result[i] = ntt(w)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// algo. 35: Power2Round (s. 7.4)
|
||||
fn power2_round(r FieldElement) (u16, FieldElement) {
|
||||
rr_ := field_from_montgomery(r)
|
||||
r1 := (rr_ + (1 << 12) - 1) >> d
|
||||
r0 := field_sub_to_montgomery(rr_, r1 << d)
|
||||
return u16(r1), r0
|
||||
}
|
||||
|
||||
// algo. 37: HighBits (s. 7.4)
|
||||
@[direct_array_access]
|
||||
fn high_bits(r RingElement, p Params) [256]u8 {
|
||||
mut w := [256]u8{}
|
||||
match p.gamma2 {
|
||||
32 {
|
||||
for i in 0 .. n {
|
||||
w[i] = high_bits_32(field_from_montgomery(r[i]))
|
||||
}
|
||||
}
|
||||
88 {
|
||||
for i in 0 .. n {
|
||||
w[i] = high_bits_88(field_from_montgomery(r[i]))
|
||||
}
|
||||
}
|
||||
else {
|
||||
panic('mldsa: unsupported gamma2') // unreachable
|
||||
}
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
fn high_bits_32(x u32) u8 {
|
||||
mut r1 := (x + 127) >> 7 // approx div by 2*gamma2
|
||||
r1 = (r1 * 1025 + (1 << 21)) >> 22
|
||||
r1 &= 0xf
|
||||
return u8(r1)
|
||||
}
|
||||
|
||||
fn high_bits_88(x u32) u8 {
|
||||
mut r1 := (x + 127) >> 7 // approx div by 2*gamma2
|
||||
r1 = (r1 * 11275 + (1 << 23)) >> 24
|
||||
r1 = ct_select_eq(r1, 44, 0, r1)
|
||||
return u8(r1)
|
||||
}
|
||||
|
||||
// algo. 36: Decompose, gamma2 = (q-1)/32 (s. 7.4)
|
||||
fn decompose_32(r FieldElement) (u8, i32) {
|
||||
x := field_from_montgomery(r)
|
||||
r1 := high_bits_32(x)
|
||||
r0 := i32(x) - i32(r1) * 2 * i32((q - 1) / 32)
|
||||
r0_adj := ct_select_leq(i32(q / 2 + 1), r0, r0 - i32(q), r0)
|
||||
return r1, r0_adj
|
||||
}
|
||||
|
||||
// algo. 36: Decompose, gamma2 = (q-1)/88 (s. 7.4)
|
||||
fn decompose_88(r FieldElement) (u8, i32) {
|
||||
x := field_from_montgomery(r)
|
||||
r1 := high_bits_88(x)
|
||||
r0 := i32(x) - i32(r1) * 2 * i32((q - 1) / 88)
|
||||
r0_adj := ct_select_leq(i32(q / 2 + 1), r0, r0 - i32(q), r0)
|
||||
return r1, r0_adj
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn low_bits_exceed_bound(w RingElement, bound u32, p Params) bool {
|
||||
match p.gamma2 {
|
||||
32 {
|
||||
for i in 0 .. n {
|
||||
_, r0 := decompose_32(w[i])
|
||||
if ct_abs(r0) >= bound {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
88 {
|
||||
for i in 0 .. n {
|
||||
_, r0 := decompose_88(w[i])
|
||||
if ct_abs(r0) >= bound {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
panic('mldsa: unsupported gamma2') // unreachable
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// algo. 40: UseHint (s. 7.4)
|
||||
@[direct_array_access]
|
||||
fn use_hint(r RingElement, h [256]u8, p Params) [256]u8 {
|
||||
mut w := [256]u8{}
|
||||
match p.gamma2 {
|
||||
32 {
|
||||
for i in 0 .. n {
|
||||
w[i] = use_hint_32(r[i], h[i])
|
||||
}
|
||||
}
|
||||
88 {
|
||||
for i in 0 .. n {
|
||||
w[i] = use_hint_88(r[i], h[i])
|
||||
}
|
||||
}
|
||||
else {
|
||||
panic('mldsa: unsupported gamma2') // unreachable
|
||||
}
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
fn use_hint_32(r FieldElement, hint u8) u8 {
|
||||
mut r1, r0 := decompose_32(r)
|
||||
if hint == 1 {
|
||||
if r0 > 0 {
|
||||
r1 = (r1 + 1) % 16
|
||||
} else {
|
||||
r1 = (r1 - 1) % 16
|
||||
}
|
||||
}
|
||||
return r1
|
||||
}
|
||||
|
||||
fn use_hint_88(r FieldElement, hint u8) u8 {
|
||||
mut r1, r0 := decompose_88(r)
|
||||
if hint == 1 {
|
||||
if r0 > 0 {
|
||||
if r1 == 43 {
|
||||
r1 = 0
|
||||
} else {
|
||||
r1++
|
||||
}
|
||||
} else {
|
||||
if r1 == 0 {
|
||||
r1 = 43
|
||||
} else {
|
||||
r1--
|
||||
}
|
||||
}
|
||||
}
|
||||
return r1
|
||||
}
|
||||
|
||||
// algo. 39: MakeHint (s. 7.4)
|
||||
@[direct_array_access]
|
||||
fn make_hint(ct0 RingElement, w RingElement, cs2 RingElement, p Params) ([256]u8, int) {
|
||||
mut h := [256]u8{}
|
||||
mut count := 0
|
||||
match p.gamma2 {
|
||||
32 {
|
||||
for i in 0 .. n {
|
||||
h[i] = make_hint_32(ct0[i], w[i], cs2[i])
|
||||
count += int(h[i])
|
||||
}
|
||||
}
|
||||
88 {
|
||||
for i in 0 .. n {
|
||||
h[i] = make_hint_88(ct0[i], w[i], cs2[i])
|
||||
count += int(h[i])
|
||||
}
|
||||
}
|
||||
else {
|
||||
panic('mldsa: unsupported gamma2') // unreachable
|
||||
}
|
||||
}
|
||||
|
||||
return h, count
|
||||
}
|
||||
|
||||
fn make_hint_32(ct0 FieldElement, w FieldElement, cs2 FieldElement) u8 {
|
||||
r_plus_z := field_sub(w, cs2)
|
||||
v1 := high_bits_32(field_from_montgomery(r_plus_z))
|
||||
r1 := high_bits_32(field_from_montgomery(field_add(r_plus_z, ct0)))
|
||||
return u8(1 - subtle.constant_time_byte_eq(v1, r1))
|
||||
}
|
||||
|
||||
fn make_hint_88(ct0 FieldElement, w FieldElement, cs2 FieldElement) u8 {
|
||||
r_plus_z := field_sub(w, cs2)
|
||||
v1 := high_bits_88(field_from_montgomery(r_plus_z))
|
||||
r1 := high_bits_88(field_from_montgomery(field_add(r_plus_z, ct0)))
|
||||
return u8(1 - subtle.constant_time_byte_eq(v1, r1))
|
||||
}
|
||||
|
||||
fn w1_encode_len(p Params) int {
|
||||
return match p.gamma2 {
|
||||
32 { 4 * n / 8 }
|
||||
88 { 6 * n / 8 }
|
||||
else { panic('mldsa: unsupported gamma2') } // unreachable
|
||||
}
|
||||
}
|
||||
|
||||
// algo. 28: w1Encode (s. 7.2)
|
||||
@[direct_array_access]
|
||||
fn w1_encode(w [256]u8, p Params, mut buf []u8) {
|
||||
match p.gamma2 {
|
||||
32 {
|
||||
for i := 0; i < n; i += 2 {
|
||||
buf[i / 2] = w[i] | (w[i + 1] << 4)
|
||||
}
|
||||
}
|
||||
88 {
|
||||
for i := 0; i < n; i += 4 {
|
||||
buf[3 * i / 4] = w[i] | (w[i + 1] << 6)
|
||||
buf[3 * i / 4 + 1] = (w[i + 1] >> 2) | (w[i + 2] << 4)
|
||||
buf[3 * i / 4 + 2] = (w[i + 2] >> 4) | (w[i + 3] << 2)
|
||||
}
|
||||
}
|
||||
else {
|
||||
panic('mldsa: unsupported gamma2') // unreachable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// algo. 26: sigEncode (s. 7.2)
|
||||
fn sig_encode(ch []u8, z []RingElement, h [][256]u8, p Params) []u8 {
|
||||
mut sig := ch.clone()
|
||||
for i in 0 .. z.len {
|
||||
sig << bit_pack(z[i], p)
|
||||
}
|
||||
sig << hint_encode(h, p)
|
||||
return sig
|
||||
}
|
||||
|
||||
// algo. 27: sigDecode (s. 7.2)
|
||||
@[direct_array_access]
|
||||
fn sig_decode(sig []u8, p Params) !([]u8, []RingElement, [][256]u8) {
|
||||
expected := sig_size(p)
|
||||
if sig.len != expected {
|
||||
return error('invalid signature length')
|
||||
}
|
||||
ch_len := p.lambda / 4
|
||||
ch := sig[..ch_len].clone()
|
||||
mut offset := ch_len
|
||||
mut z := []RingElement{len: p.l}
|
||||
for i in 0 .. p.l {
|
||||
length := (p.gamma1 + 1) * n / 8
|
||||
z[i] = bit_unpack(sig[offset..offset + length], p)
|
||||
offset += length
|
||||
}
|
||||
h := hint_decode(sig[offset..], p)!
|
||||
return ch, z, h
|
||||
}
|
||||
|
||||
// algo. 17: BitPack (s. 7.1)
|
||||
fn bit_pack(r RingElement, p Params) []u8 {
|
||||
match p.gamma1 {
|
||||
17 { return bit_pack_18(r) }
|
||||
19 { return bit_pack_20(r) }
|
||||
else { panic('mldsa: unsupported gamma1') } // unreachable
|
||||
}
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn bit_pack_18(r RingElement) []u8 {
|
||||
mut v := []u8{len: 18 * n / 8}
|
||||
mut pos := 0
|
||||
for i := 0; i < n; i += 4 {
|
||||
w0 := u32(1 << 17) - u32(field_centered_mod(r[i]))
|
||||
w1 := u32(1 << 17) - u32(field_centered_mod(r[i + 1]))
|
||||
w2 := u32(1 << 17) - u32(field_centered_mod(r[i + 2]))
|
||||
w3 := u32(1 << 17) - u32(field_centered_mod(r[i + 3]))
|
||||
v[pos] = u8(w0)
|
||||
v[pos + 1] = u8(w0 >> 8)
|
||||
v[pos + 2] = u8(w0 >> 16)
|
||||
v[pos + 2] |= u8(w1 << 2)
|
||||
v[pos + 3] = u8(w1 >> 6)
|
||||
v[pos + 4] = u8(w1 >> 14)
|
||||
v[pos + 4] |= u8(w2 << 4)
|
||||
v[pos + 5] = u8(w2 >> 4)
|
||||
v[pos + 6] = u8(w2 >> 12)
|
||||
v[pos + 6] |= u8(w3 << 6)
|
||||
v[pos + 7] = u8(w3 >> 2)
|
||||
v[pos + 8] = u8(w3 >> 10)
|
||||
pos += 9
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn bit_pack_20(r RingElement) []u8 {
|
||||
mut v := []u8{len: 20 * n / 8}
|
||||
mut pos := 0
|
||||
for i := 0; i < n; i += 2 {
|
||||
w0 := u32(1 << 19) - u32(field_centered_mod(r[i]))
|
||||
w1 := u32(1 << 19) - u32(field_centered_mod(r[i + 1]))
|
||||
v[pos] = u8(w0)
|
||||
v[pos + 1] = u8(w0 >> 8)
|
||||
v[pos + 2] = u8(w0 >> 16)
|
||||
v[pos + 2] |= u8(w1 << 4)
|
||||
v[pos + 3] = u8(w1 >> 4)
|
||||
v[pos + 4] = u8(w1 >> 12)
|
||||
pos += 5
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// algo. 19: BitUnpack (s. 7.1)
|
||||
fn bit_unpack(v []u8, p Params) RingElement {
|
||||
match p.gamma1 {
|
||||
17 { return bit_unpack_18(v) }
|
||||
19 { return bit_unpack_20(v) }
|
||||
else { panic('mldsa: unsupported gamma1') } // unreachable
|
||||
}
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn bit_unpack_18(v []u8) RingElement {
|
||||
mut r := RingElement{}
|
||||
mut pos := 0
|
||||
for i := 0; i < n; i += 4 {
|
||||
w0 := u32(v[pos]) | (u32(v[pos + 1]) << 8) | (u32(v[pos + 2]) << 16)
|
||||
r[i] = field_sub_to_montgomery(u32(1 << 17), w0 & 0x3ffff)
|
||||
w1 := (u32(v[pos + 2]) >> 2) | (u32(v[pos + 3]) << 6) | (u32(v[pos + 4]) << 14)
|
||||
r[i + 1] = field_sub_to_montgomery(u32(1 << 17), w1 & 0x3ffff)
|
||||
w2 := (u32(v[pos + 4]) >> 4) | (u32(v[pos + 5]) << 4) | (u32(v[pos + 6]) << 12)
|
||||
r[i + 2] = field_sub_to_montgomery(u32(1 << 17), w2 & 0x3ffff)
|
||||
w3 := (u32(v[pos + 6]) >> 6) | (u32(v[pos + 7]) << 2) | (u32(v[pos + 8]) << 10)
|
||||
r[i + 3] = field_sub_to_montgomery(u32(1 << 17), w3 & 0x3ffff)
|
||||
pos += 9
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn bit_unpack_20(v []u8) RingElement {
|
||||
mut r := RingElement{}
|
||||
mut pos := 0
|
||||
for i := 0; i < n; i += 2 {
|
||||
w0 := u32(v[pos]) | (u32(v[pos + 1]) << 8) | (u32(v[pos + 2]) << 16)
|
||||
r[i] = field_sub_to_montgomery(u32(1 << 19), w0 & 0xfffff)
|
||||
w1 := (u32(v[pos + 2]) >> 4) | (u32(v[pos + 3]) << 4) | (u32(v[pos + 4]) << 12)
|
||||
r[i + 1] = field_sub_to_montgomery(u32(1 << 19), w1 & 0xfffff)
|
||||
pos += 5
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// algo. 20: HintBitPack (s. 7.2)
|
||||
@[direct_array_access]
|
||||
fn hint_encode(h [][256]u8, p Params) []u8 {
|
||||
mut out := []u8{len: p.omega + p.k}
|
||||
mut idx := 0
|
||||
for i in 0 .. p.k {
|
||||
for j in 0 .. n {
|
||||
if h[i][j] != 0 {
|
||||
out[idx] = u8(j)
|
||||
idx++
|
||||
}
|
||||
}
|
||||
out[p.omega + i] = u8(idx)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// algo. 21: HintBitUnpack (s. 7.2)
|
||||
@[direct_array_access]
|
||||
fn hint_decode(y []u8, p Params) ![][256]u8 {
|
||||
if y.len != p.omega + p.k {
|
||||
return error('invalid hint length')
|
||||
}
|
||||
mut h := [][256]u8{len: p.k, init: [256]u8{}}
|
||||
mut idx := u8(0)
|
||||
for i in 0 .. p.k {
|
||||
limit := y[p.omega + i]
|
||||
if limit < idx || limit > u8(p.omega) {
|
||||
return error('invalid hint limits')
|
||||
}
|
||||
first := idx
|
||||
for idx < limit {
|
||||
if idx > first && y[idx - 1] >= y[idx] {
|
||||
return error('invalid hint index order')
|
||||
}
|
||||
h[i][y[idx]] = 1
|
||||
idx++
|
||||
}
|
||||
}
|
||||
for i := int(idx); i < p.omega; i++ {
|
||||
if y[i] != 0 {
|
||||
return error('invalid hint padding')
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn bit_pack_slow(r RingElement, a int, b int) []u8 {
|
||||
bitlen := bits_len(u32(a + b))
|
||||
mut out := []u8{len: n * bitlen / 8}
|
||||
mut acc := u32(0)
|
||||
mut acc_bits := u32(0)
|
||||
mut vi := 0
|
||||
for i in 0 .. n {
|
||||
w := u32(int(b) - int(field_centered_mod(r[i])))
|
||||
acc |= w << acc_bits
|
||||
acc_bits += u32(bitlen)
|
||||
for acc_bits >= 8 {
|
||||
out[vi] = u8(acc)
|
||||
vi++
|
||||
acc >>= 8
|
||||
acc_bits -= 8
|
||||
}
|
||||
}
|
||||
if acc_bits > 0 {
|
||||
out[vi] = u8(acc)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn bit_unpack_slow(v []u8, a int, b int) !RingElement {
|
||||
bitlen := bits_len(u32(a + b))
|
||||
if v.len != n * bitlen / 8 {
|
||||
return error('mldsa: invalid input length for bit_unpack_slow')
|
||||
}
|
||||
|
||||
mask := u32((1 << bitlen) - 1)
|
||||
max_value := u32(a + b)
|
||||
|
||||
mut r := RingElement{}
|
||||
mut acc := u32(0)
|
||||
mut acc_bits := u32(0)
|
||||
mut vi := 0
|
||||
|
||||
for i in 0 .. n {
|
||||
for acc_bits < u32(bitlen) {
|
||||
if vi < v.len {
|
||||
acc |= u32(v[vi]) << acc_bits
|
||||
vi++
|
||||
acc_bits += 8
|
||||
}
|
||||
}
|
||||
w := acc & mask
|
||||
if w > max_value {
|
||||
return error('mldsa: coefficient out of range')
|
||||
}
|
||||
r[i] = field_sub_to_montgomery(u32(b), w)
|
||||
acc >>= u32(bitlen)
|
||||
acc_bits -= u32(bitlen)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
fn bits_len(x u32) int {
|
||||
if x == 0 {
|
||||
return 0
|
||||
}
|
||||
mut v := x
|
||||
mut n_ := 0
|
||||
for v > 0 {
|
||||
v >>= 1
|
||||
n_++
|
||||
}
|
||||
return n_
|
||||
}
|
||||
|
||||
// algo. 24: skEncode (s. 7.2)
|
||||
fn sk_encode(rho []u8, capital_k [32]u8, tr [64]u8, s1 []NttElement, s2 []NttElement, t0 []NttElement, p Params) []u8 {
|
||||
mut out := []u8{}
|
||||
out << rho[..32]
|
||||
out << capital_k[..]
|
||||
out << tr[..]
|
||||
eta := int(p.eta)
|
||||
for i in 0 .. p.l {
|
||||
out << bit_pack_slow(inverse_ntt(s1[i]), eta, eta)
|
||||
}
|
||||
for i in 0 .. p.k {
|
||||
out << bit_pack_slow(inverse_ntt(s2[i]), eta, eta)
|
||||
}
|
||||
for i in 0 .. p.k {
|
||||
out << bit_pack_slow(inverse_ntt(t0[i]), 4095, 4096)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// algo. 25: skDecode (s. 7.2)
|
||||
fn sk_decode(sk []u8, p Params) !([]u8, [32]u8, [64]u8, []RingElement, []RingElement, []RingElement) {
|
||||
k, l, eta := p.k, p.l, p.eta
|
||||
if sk.len != priv_key_size(p) {
|
||||
return error('mldsa: invalid private key size')
|
||||
}
|
||||
rho := sk[..32].clone()
|
||||
capital_k := slice_to_32(sk[32..64])
|
||||
tr := slice_to_64(sk[64..128])
|
||||
mut offset := 128
|
||||
|
||||
eta_len := n * bits_len(u32(eta * 2)) / 8
|
||||
mut s1 := []RingElement{len: l}
|
||||
for i in 0 .. l {
|
||||
s1[i] = bit_unpack_slow(sk[offset..offset + eta_len], eta, eta)!
|
||||
offset += eta_len
|
||||
}
|
||||
|
||||
mut s2 := []RingElement{len: k}
|
||||
for i in 0 .. k {
|
||||
s2[i] = bit_unpack_slow(sk[offset..offset + eta_len], eta, eta)!
|
||||
offset += eta_len
|
||||
}
|
||||
|
||||
t0_len := n * 13 / 8
|
||||
mut t0 := []RingElement{len: k}
|
||||
for i in 0 .. k {
|
||||
t0[i] = bit_unpack_slow(sk[offset..offset + t0_len], 4095, 4096)!
|
||||
offset += t0_len
|
||||
}
|
||||
|
||||
return rho, capital_k, tr, s1, s2, t0
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// Ported to V from Go's crypto/internal/fips140/mldsa.
|
||||
module mldsa
|
||||
|
||||
import crypto.internal.subtle
|
||||
|
||||
// s. 2.3, appendix a
|
||||
const q = u32(8380417) // 2^23 - 2^13 + 1
|
||||
const rr = u32(2365951) // R^2 mod q (R = 2^32)
|
||||
const q_neg_inv = u32(4236238847) // -q^-1 mod R (appendix a: QINV = 58728449)
|
||||
const mont_one = u32(4193792) // R mod q
|
||||
const mont_minus_one = u32(4186625) // (q-1)*R mod q
|
||||
const n = 256
|
||||
const d = 13
|
||||
|
||||
// signing uses rejection sampling that succeeds with p =~ 1/5.22
|
||||
// per attempt (worst case: ML-DSA-65, over 10k sigs)
|
||||
// P(no converg. after 512 iters) = (1 - 1/5.22)^512 =~ 2^-157
|
||||
// for ref. ML-DSA-44 security is 2^-128
|
||||
const max_sign_attempts = 512
|
||||
|
||||
type FieldElement = u32
|
||||
type RingElement = [256]u32
|
||||
type NttElement = [256]u32
|
||||
|
||||
fn field_to_montgomery(a u32) !FieldElement {
|
||||
if a >= q {
|
||||
return error('unreduced field element ${a}')
|
||||
}
|
||||
return field_montgomery_mul(FieldElement(a), rr)
|
||||
}
|
||||
|
||||
fn field_sub_to_montgomery(a u32, b u32) FieldElement {
|
||||
x := a - b + q
|
||||
return field_montgomery_mul(FieldElement(x), rr)
|
||||
}
|
||||
|
||||
fn field_from_montgomery(a FieldElement) u32 {
|
||||
return u32(field_montgomery_reduce(u64(a)))
|
||||
}
|
||||
|
||||
fn field_centered_mod(r FieldElement) i32 {
|
||||
x := i32(field_from_montgomery(r))
|
||||
return ct_select_leq(x, i32(q / 2), x, x - i32(q))
|
||||
}
|
||||
|
||||
fn field_infinity_norm(r FieldElement) u32 {
|
||||
x := i32(field_from_montgomery(r))
|
||||
return u32(ct_select_leq(x, i32(q / 2), x, i32(q) - x))
|
||||
}
|
||||
|
||||
fn field_reduce_once(a u32) FieldElement {
|
||||
if a >= q {
|
||||
return FieldElement(a - q)
|
||||
}
|
||||
return FieldElement(a)
|
||||
}
|
||||
|
||||
fn field_add(a FieldElement, b FieldElement) FieldElement {
|
||||
return field_reduce_once(u32(a) + u32(b))
|
||||
}
|
||||
|
||||
fn field_sub(a FieldElement, b FieldElement) FieldElement {
|
||||
return field_reduce_once(u32(a) - u32(b) + q)
|
||||
}
|
||||
|
||||
fn field_montgomery_mul(a FieldElement, b FieldElement) FieldElement {
|
||||
x := u64(a) * u64(b)
|
||||
return field_montgomery_reduce(x)
|
||||
}
|
||||
|
||||
// algo. 49: MontgomeryReduce
|
||||
fn field_montgomery_reduce(x u64) FieldElement {
|
||||
t := u32(x) * q_neg_inv
|
||||
u_ := (x + u64(t) * u64(q)) >> 32
|
||||
return field_reduce_once(u32(u_))
|
||||
}
|
||||
|
||||
fn field_montgomery_mul_sub(a FieldElement, b FieldElement, c FieldElement) FieldElement {
|
||||
x := u64(a) * u64(u32(b) - u32(c) + q)
|
||||
return field_montgomery_reduce(x)
|
||||
}
|
||||
|
||||
fn field_montgomery_add_mul(a FieldElement, b FieldElement, c FieldElement, d_ FieldElement) FieldElement {
|
||||
x := u64(a) * u64(b) + u64(c) * u64(d_)
|
||||
return field_montgomery_reduce(x)
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn poly_add_ring(a RingElement, b RingElement) RingElement {
|
||||
mut s := RingElement{}
|
||||
for i in 0 .. n {
|
||||
s[i] = field_add(a[i], b[i])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn poly_add_ntt(a NttElement, b NttElement) NttElement {
|
||||
mut s := NttElement{}
|
||||
for i in 0 .. n {
|
||||
s[i] = field_add(a[i], b[i])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn poly_sub_ring(a RingElement, b RingElement) RingElement {
|
||||
mut s := RingElement{}
|
||||
for i in 0 .. n {
|
||||
s[i] = field_sub(a[i], b[i])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn poly_sub_ntt(a NttElement, b NttElement) NttElement {
|
||||
mut s := NttElement{}
|
||||
for i in 0 .. n {
|
||||
s[i] = field_sub(a[i], b[i])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// algo. 45: MultiplyNTT
|
||||
@[direct_array_access]
|
||||
fn ntt_mul(a NttElement, b NttElement) NttElement {
|
||||
mut p := NttElement{}
|
||||
for i in 0 .. n {
|
||||
p[i] = field_montgomery_mul(a[i], b[i])
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn coefficients_exceed_bound(w RingElement, bound u32) bool {
|
||||
for i in 0 .. n {
|
||||
if field_infinity_norm(w[i]) >= bound {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fn ct_select_leq(a i32, b i32, yes i32, no i32) i32 {
|
||||
return if subtle.constant_time_less_or_eq(int(a), int(b)) == 1 { yes } else { no }
|
||||
}
|
||||
|
||||
fn ct_select_eq(a u32, b u32, yes u32, no u32) u32 {
|
||||
return u32(subtle.constant_time_select(subtle.constant_time_eq(int(a), int(b)), int(yes),
|
||||
int(no)))
|
||||
}
|
||||
|
||||
fn ct_abs(x i32) u32 {
|
||||
return u32(ct_select_leq(0, x, x, -x))
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) V contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
module mldsa
|
||||
|
||||
// TODO: remove this when OpenSSL 3.5 is more widely available by default.
|
||||
#flag linux -I/opt/ssl/include/openssl
|
||||
#flag linux -I/opt/ssl/include/crypto
|
||||
#flag linux -I/opt/ssl/include
|
||||
#flag linux -L/opt/ssl/lib64
|
||||
|
||||
// Standard path
|
||||
#flag linux -I/usr/local/include/openssl
|
||||
#flag linux -I/usr/local/include/crypto
|
||||
#flag linux -L/usr/local/lib64
|
||||
|
||||
#flag darwin -L /opt/homebrew/opt/openssl/lib -I /opt/homebrew/opt/openssl/include
|
||||
|
||||
#flag -I/usr/include/openssl
|
||||
#flag -lcrypto
|
||||
|
||||
#flag darwin -I/usr/local/opt/openssl/include
|
||||
#flag darwin -L/usr/local/opt/openssl/lib
|
||||
|
||||
#flag openbsd -I/usr/local/include/eopenssl35
|
||||
#flag openbsd -L/usr/local/lib/eopenssl35 -Wl,-rpath,/usr/local/lib/eopenssl35
|
||||
|
||||
// Installed through choco:
|
||||
#flag windows -IC:/Program Files/OpenSSL-Win64/include
|
||||
#flag windows -LC:/Program Files/OpenSSL-Win64/lib/VC/x64/MD
|
||||
// Installed on the CI:
|
||||
#flag windows -IC:/Program Files/OpenSSL/include
|
||||
#flag windows -LC:/Program Files/OpenSSL/lib/VC/x64/MD
|
||||
#flag windows -l libcrypto
|
||||
|
||||
#include <openssl/obj_mac.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/param_build.h>
|
||||
|
||||
@[typedef]
|
||||
struct C.EVP_PKEY {}
|
||||
|
||||
@[typedef]
|
||||
struct C.EVP_PKEY_CTX {}
|
||||
|
||||
@[typedef]
|
||||
struct C.EVP_SIGNATURE {}
|
||||
|
||||
@[typedef]
|
||||
struct C.OSSL_PARAM {}
|
||||
|
||||
@[typedef]
|
||||
struct C.OSSL_PARAM_BLD {}
|
||||
|
||||
fn C.OpenSSL_version_num() u64
|
||||
|
||||
fn C.EVP_PKEY_new() &C.EVP_PKEY
|
||||
fn C.EVP_PKEY_Q_keygen(ctx voidptr, propq &char, tipe &char) &C.EVP_PKEY
|
||||
fn C.EVP_PKEY_dup(key &C.EVP_PKEY) &C.EVP_PKEY
|
||||
fn C.EVP_PKEY_free(key &C.EVP_PKEY)
|
||||
fn C.EVP_PKEY_size(key &C.EVP_PKEY) i32
|
||||
fn C.EVP_PKEY_get0_type_name(key &C.EVP_PKEY) &char
|
||||
fn C.EVP_PKEY_get_octet_string_param(pkey &C.EVP_PKEY, key_name &char, buf &u8, max_buf_sz usize, out_len &usize) i32
|
||||
fn C.EVP_PKEY_set_octet_string_param(key &C.EVP_PKEY, key_name &char, buf voidptr, bsize usize) i32
|
||||
fn C.EVP_PKEY_keygen_init(ctx &C.EVP_PKEY_CTX) i32
|
||||
fn C.EVP_PKEY_keygen(ctx &C.EVP_PKEY_CTX, ppkey &&C.EVP_PKEY) i32
|
||||
fn C.EVP_PKEY_CTX_set_params(ctx &C.EVP_PKEY_CTX, params &C.OSSL_PARAM) i32
|
||||
fn C.EVP_PKEY_fromdata_init(ctx &C.EVP_PKEY_CTX) i32
|
||||
fn C.EVP_PKEY_fromdata(ctx &C.EVP_PKEY_CTX, ppkey &&C.EVP_PKEY, selection i32, params &C.OSSL_PARAM) i32
|
||||
fn C.EVP_PKEY_sign_message_init(ctx &C.EVP_PKEY_CTX, algo &C.EVP_SIGNATURE, params &C.OSSL_PARAM) i32
|
||||
fn C.EVP_PKEY_sign(ctx &C.EVP_PKEY_CTX, sig &u8, siglen &usize, tbs &u8, tbslen usize) i32
|
||||
fn C.EVP_PKEY_verify_message_init(ctx &C.EVP_PKEY_CTX, algo &C.EVP_SIGNATURE, params &C.OSSL_PARAM) i32
|
||||
fn C.EVP_PKEY_verify(ctx &C.EVP_PKEY_CTX, sig &u8, siglen usize, tbs &u8, tbslen usize) i32
|
||||
|
||||
fn C.EVP_PKEY_CTX_new_from_name(libctx voidptr, name &char, pq voidptr) &C.EVP_PKEY_CTX
|
||||
fn C.EVP_PKEY_CTX_new_from_pkey(libctx voidptr, pkey &C.EVP_PKEY, pq voidptr) &C.EVP_PKEY_CTX
|
||||
fn C.EVP_PKEY_CTX_free(ctx &C.EVP_PKEY_CTX)
|
||||
|
||||
fn C.EVP_SIGNATURE_free(signature &C.EVP_SIGNATURE)
|
||||
fn C.EVP_SIGNATURE_fetch(ctx voidptr, algorithm &char, properties voidptr) &C.EVP_SIGNATURE
|
||||
|
||||
fn C.OSSL_PARAM_free(params &C.OSSL_PARAM)
|
||||
fn C.OSSL_PARAM_BLD_free(param_bld &C.OSSL_PARAM_BLD)
|
||||
fn C.OSSL_PARAM_BLD_new() &C.OSSL_PARAM_BLD
|
||||
fn C.OSSL_PARAM_BLD_push_int(bld &C.OSSL_PARAM_BLD, key &char, val i32) i32
|
||||
fn C.OSSL_PARAM_BLD_push_octet_string(bld &C.OSSL_PARAM_BLD, key &char, buf voidptr, bsize usize) i32
|
||||
fn C.OSSL_PARAM_BLD_to_param(bld &C.OSSL_PARAM_BLD) &C.OSSL_PARAM
|
||||
@@ -0,0 +1,518 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// Ported to V from Go's crypto/internal/fips140/mldsa.
|
||||
|
||||
// ML-DSA (Module-Lattice-Based Digital Signature Algorithm) per FIPS 204
|
||||
// https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.204.pdf
|
||||
|
||||
module mldsa
|
||||
|
||||
import crypto.rand
|
||||
import crypto.sha3
|
||||
import crypto.internal.subtle
|
||||
|
||||
@[direct_array_access]
|
||||
fn slice_to_32(s []u8) [32]u8 {
|
||||
mut a := [32]u8{}
|
||||
for i in 0 .. 32 {
|
||||
a[i] = s[i]
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
@[direct_array_access]
|
||||
fn slice_to_64(s []u8) [64]u8 {
|
||||
mut a := [64]u8{}
|
||||
for i in 0 .. 64 {
|
||||
a[i] = s[i]
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
pub struct PrivateKey {
|
||||
seed [32]u8
|
||||
pk PublicKey
|
||||
s1 []NttElement // len = l
|
||||
s2 []NttElement // len = k
|
||||
t0 []NttElement // len = k
|
||||
k [32]u8
|
||||
}
|
||||
|
||||
pub struct PublicKey {
|
||||
raw []u8
|
||||
p Params
|
||||
a []NttElement // k*l matrix in NTT domain
|
||||
t1 []NttElement // len = k, NTT(t1 * 2^d)
|
||||
tr [64]u8
|
||||
}
|
||||
|
||||
// algo. 1: ML-DSA.KeyGen (s. 5.1)
|
||||
pub fn PrivateKey.generate(kind Kind) !PrivateKey {
|
||||
return new_private_key(slice_to_32(rand.read(32)!), kind.params())
|
||||
}
|
||||
|
||||
pub fn PrivateKey.from_seed(seed []u8, kind Kind) !PrivateKey {
|
||||
if seed.len != 32 {
|
||||
return error('invalid seed length')
|
||||
}
|
||||
return new_private_key(slice_to_32(seed), kind.params())
|
||||
}
|
||||
|
||||
// from FIPS 204 semi-expanded encoding. seed() and equal() are
|
||||
// meaningless on the result — use from_seed when possible.
|
||||
pub fn PrivateKey.from_bytes(raw []u8, kind Kind) !PrivateKey {
|
||||
return new_private_key_from_bytes(raw, kind.params())
|
||||
}
|
||||
|
||||
pub fn PublicKey.from_bytes(raw []u8, kind Kind) !PublicKey {
|
||||
return new_public_key(raw, kind.params())
|
||||
}
|
||||
|
||||
pub fn (sk &PrivateKey) public_key() &PublicKey {
|
||||
return &sk.pk
|
||||
}
|
||||
|
||||
pub fn (sk &PrivateKey) seed() []u8 {
|
||||
mut s := []u8{len: 32}
|
||||
for i in 0 .. 32 {
|
||||
s[i] = sk.seed[i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
pub fn (sk &PrivateKey) bytes() []u8 {
|
||||
return sk_encode(sk.pk.raw[..32], sk.k, sk.pk.tr, sk.s1, sk.s2, sk.t0, sk.pk.p)
|
||||
}
|
||||
|
||||
// seed-based constant-time comparison. not meaningful for from_bytes keys.
|
||||
pub fn (sk &PrivateKey) equal(other &PrivateKey) bool {
|
||||
mut a := []u8{len: 32}
|
||||
mut b := []u8{len: 32}
|
||||
for i in 0 .. 32 {
|
||||
a[i] = sk.seed[i]
|
||||
b[i] = other.seed[i]
|
||||
}
|
||||
return sk.pk.p == other.pk.p && subtle.constant_time_compare(a, b) == 1
|
||||
}
|
||||
|
||||
// constant-time comparison of the serialized key material. slower but works for from_bytes keys.
|
||||
pub fn (sk &PrivateKey) equal_bytes(other &PrivateKey) bool {
|
||||
return sk.pk.p == other.pk.p && subtle.constant_time_compare(sk.bytes(), other.bytes()) == 1
|
||||
}
|
||||
|
||||
// algo. 2/4: ML-DSA.Sign / HashML-DSA.Sign (s. 5.2, 5.4.1)
|
||||
pub fn (sk &PrivateKey) sign(msg []u8, opts SignerOpts) ![]u8 {
|
||||
if opts.context.len > 255 {
|
||||
return error('context too long')
|
||||
}
|
||||
mu := if opts.prehash != .none {
|
||||
compute_mu_prehash(sk.pk.tr[..], msg, opts.context, opts.prehash)
|
||||
} else {
|
||||
compute_mu(sk.pk.tr[..], msg, opts.context)
|
||||
}
|
||||
if opts.deterministic {
|
||||
return sign_internal(sk, mu, [32]u8{})
|
||||
}
|
||||
return sign_internal(sk, mu, slice_to_32(rand.read(32)!))
|
||||
}
|
||||
|
||||
// sign_mu signs a precomputed mu value with explicit randomness.
|
||||
// mu must be 64 bytes. rnd must be 32 bytes (use all zeros for deterministic signing).
|
||||
pub fn (sk &PrivateKey) sign_mu(mu []u8, rnd []u8) ![]u8 {
|
||||
if mu.len != 64 {
|
||||
return error('mu must be 64 bytes')
|
||||
}
|
||||
if rnd.len != 32 {
|
||||
return error('rnd must be 32 bytes')
|
||||
}
|
||||
return sign_internal(sk, slice_to_64(mu), slice_to_32(rnd))
|
||||
}
|
||||
|
||||
pub fn (pk &PublicKey) bytes() []u8 {
|
||||
return pk.raw.clone()
|
||||
}
|
||||
|
||||
// tr returns the 64-byte transcript hash (H(pk)) used in mu computation.
|
||||
pub fn (pk &PublicKey) tr() []u8 {
|
||||
return pk.tr[..]
|
||||
}
|
||||
|
||||
pub fn (pk &PublicKey) equal(other &PublicKey) bool {
|
||||
return pk.p == other.p && subtle.constant_time_compare(pk.raw, other.raw) == 1
|
||||
}
|
||||
|
||||
// algo. 3/5: ML-DSA.Verify / HashML-DSA.Verify (s. 5.3, 5.4.1)
|
||||
pub fn (pk &PublicKey) verify(msg []u8, sig []u8, opts SignerOpts) !bool {
|
||||
if opts.context.len > 255 {
|
||||
return error('context too long')
|
||||
}
|
||||
mu := if opts.prehash != .none {
|
||||
compute_mu_prehash(pk.tr[..], msg, opts.context, opts.prehash)
|
||||
} else {
|
||||
compute_mu(pk.tr[..], msg, opts.context)
|
||||
}
|
||||
return verify_internal(pk, mu, sig)
|
||||
}
|
||||
|
||||
pub fn (pk &PublicKey) verify_mu(mu []u8, sig []u8) !bool {
|
||||
if mu.len != 64 {
|
||||
return error('mu must be exactly 64 bytes')
|
||||
}
|
||||
return verify_internal(pk, slice_to_64(mu), sig)
|
||||
}
|
||||
|
||||
// algo. 6: ML-DSA.KeyGen_internal (s. 6.1)
|
||||
fn new_private_key(seed [32]u8, p Params) PrivateKey {
|
||||
k, l := p.k, p.l
|
||||
|
||||
// expand seed into rho, rho', K
|
||||
mut xi := sha3.new_shake256()
|
||||
xi.write(seed[..])
|
||||
xi.write([u8(k), u8(l)])
|
||||
rho := xi.read(32)
|
||||
rho_s := xi.read(64)
|
||||
k_bytes := xi.read(32)
|
||||
|
||||
a := compute_matrix_a(rho, p)
|
||||
|
||||
mut s1 := []NttElement{len: l}
|
||||
for r in 0 .. l {
|
||||
s1[r] = ntt(sample_bounded_poly(rho_s, u8(r), p))
|
||||
}
|
||||
mut s2 := []NttElement{len: k}
|
||||
for r in 0 .. k {
|
||||
s2[r] = ntt(sample_bounded_poly(rho_s, u8(l + r), p))
|
||||
}
|
||||
|
||||
// t_hat = A_hat * s1_hat + s2_hat
|
||||
mut t_hat := []NttElement{len: k}
|
||||
for i in 0 .. k {
|
||||
t_hat[i] = s2[i]
|
||||
for j in 0 .. l {
|
||||
t_hat[i] = poly_add_ntt(t_hat[i], ntt_mul(a[i * l + j], s1[j]))
|
||||
}
|
||||
}
|
||||
|
||||
mut t1 := [][]u16{len: k, init: []u16{len: n}}
|
||||
mut t0 := []NttElement{len: k}
|
||||
for i in 0 .. k {
|
||||
t_i := inverse_ntt(t_hat[i])
|
||||
mut w := RingElement{}
|
||||
for j in 0 .. n {
|
||||
t1[i][j], w[j] = power2_round(t_i[j])
|
||||
}
|
||||
t0[i] = ntt(w)
|
||||
}
|
||||
|
||||
pk_bytes := pk_encode(rho, t1, p)
|
||||
tr := compute_pk_hash(pk_bytes)
|
||||
t1_hat := compute_t1_hat(t1)
|
||||
|
||||
k_arr := slice_to_32(k_bytes)
|
||||
|
||||
return PrivateKey{
|
||||
seed: seed
|
||||
pk: PublicKey{
|
||||
raw: pk_bytes
|
||||
p: p
|
||||
a: a
|
||||
t1: t1_hat
|
||||
tr: tr
|
||||
}
|
||||
s1: s1
|
||||
s2: s2
|
||||
t0: t0
|
||||
k: k_arr
|
||||
}
|
||||
}
|
||||
|
||||
fn new_private_key_from_bytes(sk []u8, p Params) !PrivateKey {
|
||||
k, l := p.k, p.l
|
||||
|
||||
rho, capital_k, tr, s1_ring, s2_ring, t0_ring := sk_decode(sk, p)!
|
||||
|
||||
a := compute_matrix_a(rho, p)
|
||||
|
||||
mut s1 := []NttElement{len: l}
|
||||
for r in 0 .. l {
|
||||
s1[r] = ntt(s1_ring[r])
|
||||
}
|
||||
mut s2 := []NttElement{len: k}
|
||||
for r in 0 .. k {
|
||||
s2[r] = ntt(s2_ring[r])
|
||||
}
|
||||
mut t0 := []NttElement{len: k}
|
||||
for r in 0 .. k {
|
||||
t0[r] = ntt(t0_ring[r])
|
||||
}
|
||||
|
||||
// recompute t1 from rho, s1, s2 to verify consistency
|
||||
mut t1 := [][]u16{len: k, init: []u16{len: n}}
|
||||
for i in 0 .. k {
|
||||
mut t_hat := s2[i]
|
||||
for j in 0 .. l {
|
||||
t_hat = poly_add_ntt(t_hat, ntt_mul(a[i * l + j], s1[j]))
|
||||
}
|
||||
t_i := inverse_ntt(t_hat)
|
||||
for j in 0 .. n {
|
||||
r1, r0 := power2_round(t_i[j])
|
||||
t1[i][j] = r1
|
||||
if r0 != t0_ring[i][j] {
|
||||
return error('mldsa: private key inconsistent with t0')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pk_bytes := pk_encode(rho, t1, p)
|
||||
computed_tr := compute_pk_hash(pk_bytes)
|
||||
if computed_tr != tr {
|
||||
return error('mldsa: private key inconsistent with public key hash')
|
||||
}
|
||||
t1_hat := compute_t1_hat(t1)
|
||||
|
||||
// use random bytes for seed since the semi-expanded format doesn't contain it
|
||||
seed := slice_to_32(rand.read(32)!)
|
||||
|
||||
return PrivateKey{
|
||||
seed: seed
|
||||
pk: PublicKey{
|
||||
raw: pk_bytes
|
||||
p: p
|
||||
a: a
|
||||
t1: t1_hat
|
||||
tr: tr
|
||||
}
|
||||
s1: s1
|
||||
s2: s2
|
||||
t0: t0
|
||||
k: capital_k
|
||||
}
|
||||
}
|
||||
|
||||
fn new_public_key(raw []u8, p Params) !PublicKey {
|
||||
k, l := p.k, p.l
|
||||
|
||||
rho, t1 := pk_decode(raw, p)!
|
||||
a := compute_matrix_a(rho, p)
|
||||
tr := compute_pk_hash(raw)
|
||||
t1_hat := compute_t1_hat(t1)
|
||||
|
||||
return PublicKey{
|
||||
raw: raw.clone()
|
||||
p: p
|
||||
a: a[..k * l].clone()
|
||||
t1: t1_hat[..k].clone()
|
||||
tr: tr
|
||||
}
|
||||
}
|
||||
|
||||
// algo. 2, lines 10-11: M' = 0x00 || |ctx| || ctx || M; mu = H(tr || M', 64)
|
||||
// compute_mu computes mu = H(tr || M', 64) where M' = 0x00 || |ctx| || ctx || msg.
|
||||
pub fn compute_mu(tr []u8, msg []u8, context string) [64]u8 {
|
||||
mut h := sha3.new_shake256()
|
||||
h.write(tr)
|
||||
h.write([u8(0)]) // pure mode domain sep
|
||||
h.write([u8(context.len)])
|
||||
h.write(context.bytes())
|
||||
h.write(msg)
|
||||
return slice_to_64(h.read(64))
|
||||
}
|
||||
|
||||
// algo. 7: ML-DSA.Sign_internal (s. 6.2)
|
||||
@[direct_array_access]
|
||||
fn sign_internal(sk &PrivateKey, mu [64]u8, random [32]u8) ![]u8 {
|
||||
p := sk.pk.p
|
||||
k, l := p.k, p.l
|
||||
a := sk.pk.a
|
||||
s1 := sk.s1
|
||||
s2 := sk.s2
|
||||
t0 := sk.t0
|
||||
|
||||
beta := u32(p.tau * p.eta)
|
||||
gamma1 := u32(1) << p.gamma1
|
||||
gamma1_beta := gamma1 - beta
|
||||
gamma2 := (q - 1) / u32(p.gamma2)
|
||||
gamma2_beta := gamma2 - beta
|
||||
|
||||
// line 7: rho'' = H(K || rnd || mu, 64)
|
||||
mut h_nonce := sha3.new_shake256()
|
||||
h_nonce.write(sk.k[..])
|
||||
h_nonce.write(random[..])
|
||||
h_nonce.write(mu[..])
|
||||
nonce := h_nonce.read(64)
|
||||
|
||||
mut kappa := 0
|
||||
|
||||
mut y := []RingElement{len: l}
|
||||
mut y_hat := []NttElement{len: l}
|
||||
mut w := []RingElement{len: k}
|
||||
mut cs1 := []RingElement{len: l}
|
||||
mut cs2 := []RingElement{len: k}
|
||||
mut z := []RingElement{len: l}
|
||||
mut ct0 := []RingElement{len: k}
|
||||
mut h := [][256]u8{len: k, init: [256]u8{}}
|
||||
mut w1_buf := []u8{len: w1_encode_len(p)}
|
||||
|
||||
// lines 10-32: rejection sampling loop (bounded by max_sign_attempts)
|
||||
for _ in 0 .. max_sign_attempts {
|
||||
// line 11: y = ExpandMask(rho'', kappa) (algo. 34)
|
||||
for r in 0 .. l {
|
||||
counter := [u8(kappa & 0xff), u8(kappa >> 8)]
|
||||
kappa++
|
||||
|
||||
mut h_y := sha3.new_shake256()
|
||||
h_y.write(nonce)
|
||||
h_y.write(counter)
|
||||
v_bytes := h_y.read((p.gamma1 + 1) * n / 8)
|
||||
y[r] = bit_unpack(v_bytes, p)
|
||||
}
|
||||
|
||||
// line 12: w = NTT^-1(A_hat * NTT(y))
|
||||
for i in 0 .. l {
|
||||
y_hat[i] = ntt(y[i])
|
||||
}
|
||||
for i in 0 .. k {
|
||||
mut w_hat := NttElement{}
|
||||
for j in 0 .. l {
|
||||
w_hat = poly_add_ntt(w_hat, ntt_mul(a[i * l + j], y_hat[j]))
|
||||
}
|
||||
w[i] = inverse_ntt(w_hat)
|
||||
}
|
||||
|
||||
// line 13-14: w1 = HighBits(w); c_tilde = H(mu || w1Encode(w1), lambda/4)
|
||||
mut h_ch := sha3.new_shake256()
|
||||
h_ch.write(mu[..])
|
||||
for i in 0 .. k {
|
||||
w1_encode(high_bits(w[i], p), p, mut w1_buf)
|
||||
h_ch.write(w1_buf)
|
||||
}
|
||||
ch := h_ch.read(p.lambda / 4)
|
||||
|
||||
// line 15-16: c = SampleInBall(c_tilde); c_hat = NTT(c)
|
||||
c := ntt(sample_in_ball(ch, p))
|
||||
|
||||
// lines 17-20: cs1 = NTT^-1(c_hat * s1_hat); z = y + cs1
|
||||
for i in 0 .. l {
|
||||
cs1[i] = inverse_ntt(ntt_mul(c, s1[i]))
|
||||
}
|
||||
for i in 0 .. k {
|
||||
cs2[i] = inverse_ntt(ntt_mul(c, s2[i]))
|
||||
}
|
||||
|
||||
// line 23: ||z||_inf >= gamma1 - beta
|
||||
mut reject := false
|
||||
for i in 0 .. l {
|
||||
z[i] = poly_add_ring(y[i], cs1[i])
|
||||
if coefficients_exceed_bound(z[i], gamma1_beta) {
|
||||
reject = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if reject {
|
||||
continue
|
||||
}
|
||||
|
||||
// line 23: ||r0||_inf >= gamma2 - beta
|
||||
reject = false
|
||||
for i in 0 .. k {
|
||||
r0 := poly_sub_ring(w[i], cs2[i])
|
||||
if low_bits_exceed_bound(r0, gamma2_beta, p) {
|
||||
reject = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if reject {
|
||||
continue
|
||||
}
|
||||
|
||||
// line 25, 28: ct0 = NTT^-1(c_hat * t0_hat); ||ct0||_inf >= gamma2
|
||||
reject = false
|
||||
for i in 0 .. k {
|
||||
ct0[i] = inverse_ntt(ntt_mul(c, t0[i]))
|
||||
if coefficients_exceed_bound(ct0[i], gamma2) {
|
||||
reject = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if reject {
|
||||
continue
|
||||
}
|
||||
|
||||
// line 26, 28: h = MakeHint(-ct0, w - cs2 + ct0); count(h) > omega
|
||||
mut count1s := 0
|
||||
for i in 0 .. k {
|
||||
hint_result, count := make_hint(ct0[i], w[i], cs2[i], p)
|
||||
h[i] = hint_result
|
||||
count1s += count
|
||||
}
|
||||
if count1s > p.omega {
|
||||
continue
|
||||
}
|
||||
|
||||
return sig_encode(ch, z, h, p) // line 33: sigEncode(c_tilde, z, h)
|
||||
}
|
||||
return error('signing failed: rejection sampling did not converge after ${max_sign_attempts} attempts')
|
||||
}
|
||||
|
||||
// algo. 8: ML-DSA.Verify_internal (s. 6.3)
|
||||
@[direct_array_access]
|
||||
fn verify_internal(pk &PublicKey, mu [64]u8, sig []u8) !bool {
|
||||
p := pk.p
|
||||
k, l := p.k, p.l
|
||||
t1 := pk.t1
|
||||
a := pk.a
|
||||
|
||||
beta := u32(p.tau * p.eta)
|
||||
gamma1 := u32(1) << p.gamma1
|
||||
gamma1_beta := gamma1 - beta
|
||||
|
||||
ch, z, h := sig_decode(sig, p) or { return false }
|
||||
|
||||
c := ntt(sample_in_ball(ch, p))
|
||||
|
||||
// line 9: w'_approx = NTT^-1(A_hat * NTT(z) - NTT(c) * NTT(t1 * 2^d))
|
||||
mut z_hat := []NttElement{len: l}
|
||||
for i in 0 .. l {
|
||||
z_hat[i] = ntt(z[i])
|
||||
}
|
||||
mut w := []RingElement{len: k}
|
||||
for i in 0 .. k {
|
||||
mut w_hat := NttElement{}
|
||||
for j in 0 .. l {
|
||||
w_hat = poly_add_ntt(w_hat, ntt_mul(a[i * l + j], z_hat[j]))
|
||||
}
|
||||
w_hat = poly_sub_ntt(w_hat, ntt_mul(c, t1[i]))
|
||||
w[i] = inverse_ntt(w_hat)
|
||||
}
|
||||
|
||||
// line 10: w'1 = UseHint(h, w'_approx)
|
||||
mut w1 := [][256]u8{len: k, init: [256]u8{}}
|
||||
for i in 0 .. k {
|
||||
w1[i] = use_hint(w[i], h[i], p)
|
||||
}
|
||||
|
||||
// line 12: c_tilde' = H(mu || w1Encode(w'1), lambda/4)
|
||||
mut h_ch := sha3.new_shake256()
|
||||
h_ch.write(mu[..])
|
||||
mut w1_buf := []u8{len: w1_encode_len(p)}
|
||||
for i in 0 .. k {
|
||||
w1_encode(w1[i], p, mut w1_buf)
|
||||
h_ch.write(w1_buf)
|
||||
}
|
||||
computed_ch := h_ch.read(p.lambda / 4)
|
||||
|
||||
// line 13: ||z||_inf < gamma1 - beta and c_tilde == c_tilde'
|
||||
for i in 0 .. l {
|
||||
if coefficients_exceed_bound(z[i], gamma1_beta) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if subtle.constant_time_compare(ch, computed_ch) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// vtest build: has_modern_openssl?
|
||||
// regenerate go test vecs: v run testdata/gen.vsh [go-source-path]
|
||||
|
||||
module mldsa
|
||||
|
||||
import crypto.sha256
|
||||
import encoding.hex
|
||||
import json
|
||||
import os
|
||||
|
||||
struct TestVec {
|
||||
kind string
|
||||
seed string
|
||||
msg string
|
||||
pk_sha256 string
|
||||
sig_sha256 string
|
||||
context string
|
||||
}
|
||||
|
||||
const vecs_json = os.read_file(os.real_path(os.join_path(os.dir(@FILE), 'testdata', 'vectors.json'))) or {
|
||||
panic(err)
|
||||
}
|
||||
const test_vecs = json.decode([]TestVec, vecs_json) or { panic(err) }
|
||||
|
||||
fn parse_kind(s string) Kind {
|
||||
return match s {
|
||||
'ml_dsa_44' { Kind.ml_dsa_44 }
|
||||
'ml_dsa_65' { Kind.ml_dsa_65 }
|
||||
'ml_dsa_87' { Kind.ml_dsa_87 }
|
||||
else { panic('unknown kind: ${s}') }
|
||||
}
|
||||
}
|
||||
|
||||
fn test_keygen_sign_verify() {
|
||||
assert test_vecs.len > 0, 'no test vectors loaded'
|
||||
|
||||
for tv in test_vecs {
|
||||
kind := parse_kind(tv.kind)
|
||||
seed := hex.decode(tv.seed) or { panic(err) }
|
||||
msg := hex.decode(tv.msg) or { panic(err) }
|
||||
expected_pk_hash := hex.decode(tv.pk_sha256) or { panic(err) }
|
||||
expected_sig_hash := hex.decode(tv.sig_sha256) or { panic(err) }
|
||||
|
||||
sk := PrivateKey.from_seed(seed, kind) or { panic(err) }
|
||||
pk := sk.public_key()
|
||||
|
||||
pk_hash := sha256.sum(pk.bytes())
|
||||
assert pk_hash[..] == expected_pk_hash, 'pk hash mismatch for ${tv.kind} seed=${tv.seed[..16]}...'
|
||||
|
||||
sig := sk.sign(msg, deterministic: true, context: tv.context) or { panic(err) }
|
||||
sig_hash := sha256.sum(sig)
|
||||
assert sig_hash[..] == expected_sig_hash, 'sig hash mismatch for ${tv.kind} seed=${tv.seed[..16]}...'
|
||||
|
||||
verified := pk.verify(msg, sig, context: tv.context) or { panic(err) }
|
||||
assert verified, 'verify returned false for ${tv.kind} seed=${tv.seed[..16]}...'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_verify_rejects_bad_signature() {
|
||||
for kind in [Kind.ml_dsa_44, .ml_dsa_65, .ml_dsa_87] {
|
||||
seed := []u8{len: 32, init: 0x00}
|
||||
sk := PrivateKey.from_seed(seed, kind) or { panic(err) }
|
||||
pk := sk.public_key()
|
||||
msg := 'deadbeef'.bytes()
|
||||
|
||||
sig := sk.sign(msg, deterministic: true) or { panic(err) }
|
||||
|
||||
mut bad_sig := sig.clone()
|
||||
bad_sig[10] ^= 0xff
|
||||
|
||||
result := pk.verify(msg, bad_sig) or { false }
|
||||
assert result == false, 'verify should reject tampered sig for ${kind}'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_verify_rejects_wrong_message() {
|
||||
for kind in [Kind.ml_dsa_44, .ml_dsa_65, .ml_dsa_87] {
|
||||
seed := []u8{len: 32, init: 0x01}
|
||||
sk := PrivateKey.from_seed(seed, kind) or { panic(err) }
|
||||
pk := sk.public_key()
|
||||
msg := 'the beef is alive'.bytes()
|
||||
|
||||
sig := sk.sign(msg, deterministic: true) or { panic(err) }
|
||||
|
||||
result := pk.verify('I love strawberries'.bytes(), sig) or { false }
|
||||
assert result == false, 'verify should reject wrong message for ${kind}'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_verify_rejects_wrong_context() {
|
||||
for kind in [Kind.ml_dsa_44, .ml_dsa_65, .ml_dsa_87] {
|
||||
seed := []u8{len: 32, init: 0x02}
|
||||
sk := PrivateKey.from_seed(seed, kind) or { panic(err) }
|
||||
pk := sk.public_key()
|
||||
msg := 'very cool message'.bytes()
|
||||
|
||||
sig := sk.sign(msg, deterministic: true, context: 'some context a') or { panic(err) }
|
||||
|
||||
result := pk.verify(msg, sig, context: 'another context b') or { false }
|
||||
assert result == false, 'verify should reject wrong context for ${kind}'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_public_key_roundtrip() {
|
||||
for kind in [Kind.ml_dsa_44, .ml_dsa_65, .ml_dsa_87] {
|
||||
seed := []u8{len: 32, init: 0x03}
|
||||
sk := PrivateKey.from_seed(seed, kind) or { panic(err) }
|
||||
pk := sk.public_key()
|
||||
msg := 'pk roundtrip'.bytes()
|
||||
|
||||
sig := sk.sign(msg, deterministic: true) or { panic(err) }
|
||||
|
||||
pk2 := PublicKey.from_bytes(pk.bytes(), kind) or { panic(err) }
|
||||
assert pk.equal(&pk2), 'roundtripped public key not equal'
|
||||
|
||||
verified := pk2.verify(msg, sig) or { panic(err) }
|
||||
assert verified, 'verify failed after public key roundtrip for ${kind}'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_prehash_sign_verify() {
|
||||
prehashes := [
|
||||
PreHash.sha2_256,
|
||||
.sha2_384,
|
||||
.sha2_512,
|
||||
.sha3_256,
|
||||
.sha3_512,
|
||||
.shake_128,
|
||||
.shake_256,
|
||||
]
|
||||
for kind in [Kind.ml_dsa_44, .ml_dsa_65, .ml_dsa_87] {
|
||||
seed := []u8{len: 32, init: 0x05}
|
||||
sk := PrivateKey.from_seed(seed, kind) or { panic(err) }
|
||||
pk := sk.public_key()
|
||||
msg := 'prehash test message'.bytes()
|
||||
|
||||
for ph in prehashes {
|
||||
sig := sk.sign(msg, deterministic: true, prehash: ph) or { panic(err) }
|
||||
verified := pk.verify(msg, sig, prehash: ph) or { panic(err) }
|
||||
assert verified, 'prehash verify failed for ${kind} ${ph}'
|
||||
|
||||
// pure verify must reject a prehashed sig
|
||||
pure_result := pk.verify(msg, sig) or { false }
|
||||
assert pure_result == false, 'pure verify should reject prehash signature for ${kind} ${ph}'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_field_to_montgomery_roundtrip() {
|
||||
for val in [u32(0), 1, 2, 100, 1000, q - 1] {
|
||||
m := field_to_montgomery(val) or { panic(err) }
|
||||
back := field_from_montgomery(m)
|
||||
assert back == val, 'roundtrip failed for ${val}: got ${back}'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_field_add_sub() {
|
||||
a := field_to_montgomery(100) or { panic(err) }
|
||||
b := field_to_montgomery(200) or { panic(err) }
|
||||
sum := field_add(a, b)
|
||||
assert field_from_montgomery(sum) == 300
|
||||
|
||||
diff := field_sub(sum, b)
|
||||
assert field_from_montgomery(diff) == 100
|
||||
}
|
||||
|
||||
fn test_field_mul() {
|
||||
a := field_to_montgomery(1000) or { panic(err) }
|
||||
b := field_to_montgomery(2000) or { panic(err) }
|
||||
prod := field_montgomery_mul(a, b)
|
||||
assert field_from_montgomery(prod) == (1000 * 2000) % q
|
||||
}
|
||||
|
||||
fn test_ntt_inverse_ntt_roundtrip() {
|
||||
mut f := RingElement{}
|
||||
for i in 0 .. n {
|
||||
f[i] = field_to_montgomery(u32(i % 100)) or { panic(err) }
|
||||
}
|
||||
ntt_f := ntt(f)
|
||||
back := inverse_ntt(ntt_f)
|
||||
for i in 0 .. n {
|
||||
assert field_from_montgomery(back[i]) == field_from_montgomery(f[i]), 'NTT roundtrip failed at index ${i}'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_ntt_mul_is_polynomial_product() {
|
||||
// (1 + x)^2 ?= x^2 + 2x + 1
|
||||
mut a := RingElement{}
|
||||
a[0] = field_to_montgomery(1) or { panic(err) }
|
||||
a[1] = field_to_montgomery(1) or { panic(err) }
|
||||
|
||||
a_ntt := ntt(a)
|
||||
prod_ntt := ntt_mul(a_ntt, a_ntt)
|
||||
prod := inverse_ntt(prod_ntt)
|
||||
|
||||
assert field_from_montgomery(prod[0]) == 1, 'expected x^2'
|
||||
assert field_from_montgomery(prod[1]) == 2, 'expected 2x'
|
||||
assert field_from_montgomery(prod[2]) == 1, 'expected 1'
|
||||
|
||||
for i in 3 .. n {
|
||||
assert field_from_montgomery(prod[i]) == 0, 'expected 0 at index ${i}, got ${field_from_montgomery(prod[i])}'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_power2_round() {
|
||||
for val in [u32(0), 1, 100, 1000, q / 2, q - 1] {
|
||||
r := field_to_montgomery(val) or { panic(err) }
|
||||
hi, lo := power2_round(r)
|
||||
reconstructed := field_add(field_to_montgomery(u32(hi) << d) or { panic(err) }, lo)
|
||||
assert field_from_montgomery(reconstructed) == val, 'power2_round failed for ${val}'
|
||||
}
|
||||
}
|
||||
|
||||
fn test_private_key_roundtrip() {
|
||||
for kind in [Kind.ml_dsa_44, .ml_dsa_65, .ml_dsa_87] {
|
||||
seed := []u8{len: 32, init: 0x04}
|
||||
sk := PrivateKey.from_seed(seed, kind) or { panic(err) }
|
||||
sk2 := PrivateKey.from_seed(sk.seed(), kind) or { panic(err) }
|
||||
assert sk.equal(&sk2), 'roundtripped private key not equal for ${kind}'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// Ported to V from Go's crypto/internal/fips140/mldsa.
|
||||
module mldsa
|
||||
|
||||
// appendix b: zeta^BitRev8(k) mod q in montgomery domain
|
||||
const zetas = [u32(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]!
|
||||
|
||||
// algo. 41: NTT (s. 7.5)
|
||||
@[direct_array_access]
|
||||
fn ntt(f_ RingElement) NttElement {
|
||||
mut f := unsafe { f_ } // workaround for false mutability notice
|
||||
mut m := u8(0)
|
||||
|
||||
mut len := 128
|
||||
for len >= 8 {
|
||||
mut start := 0
|
||||
for start < 256 {
|
||||
m++
|
||||
zeta := FieldElement(zetas[m])
|
||||
|
||||
mut j := 0
|
||||
for j < len {
|
||||
t := field_montgomery_mul(zeta, f[start + len + j])
|
||||
f[start + len + j] = field_sub(f[start + j], t)
|
||||
f[start + j] = field_add(f[start + j], t)
|
||||
|
||||
t2 := field_montgomery_mul(zeta, f[start + len + j + 1])
|
||||
f[start + len + j + 1] = field_sub(f[start + j + 1], t2)
|
||||
f[start + j + 1] = field_add(f[start + j + 1], t2)
|
||||
|
||||
j += 2
|
||||
}
|
||||
start += 2 * len
|
||||
}
|
||||
len /= 2
|
||||
}
|
||||
|
||||
for start := 0; start < 256; start += 8 {
|
||||
m++
|
||||
zeta := FieldElement(zetas[m])
|
||||
|
||||
mut t := field_montgomery_mul(zeta, f[start + 4])
|
||||
f[start + 4] = field_sub(f[start], t)
|
||||
f[start] = field_add(f[start], t)
|
||||
|
||||
t = field_montgomery_mul(zeta, f[start + 5])
|
||||
f[start + 5] = field_sub(f[start + 1], t)
|
||||
f[start + 1] = field_add(f[start + 1], t)
|
||||
|
||||
t = field_montgomery_mul(zeta, f[start + 6])
|
||||
f[start + 6] = field_sub(f[start + 2], t)
|
||||
f[start + 2] = field_add(f[start + 2], t)
|
||||
|
||||
t = field_montgomery_mul(zeta, f[start + 7])
|
||||
f[start + 7] = field_sub(f[start + 3], t)
|
||||
f[start + 3] = field_add(f[start + 3], t)
|
||||
}
|
||||
|
||||
for start := 0; start < 256; start += 4 {
|
||||
m++
|
||||
zeta := FieldElement(zetas[m])
|
||||
|
||||
mut t := field_montgomery_mul(zeta, f[start + 2])
|
||||
f[start + 2] = field_sub(f[start], t)
|
||||
f[start] = field_add(f[start], t)
|
||||
|
||||
t = field_montgomery_mul(zeta, f[start + 3])
|
||||
f[start + 3] = field_sub(f[start + 1], t)
|
||||
f[start + 1] = field_add(f[start + 1], t)
|
||||
}
|
||||
|
||||
for start := 0; start < 256; start += 2 {
|
||||
m++
|
||||
zeta := FieldElement(zetas[m])
|
||||
|
||||
t := field_montgomery_mul(zeta, f[start + 1])
|
||||
f[start + 1] = field_sub(f[start], t)
|
||||
f[start] = field_add(f[start], t)
|
||||
}
|
||||
|
||||
return NttElement(f)
|
||||
}
|
||||
|
||||
// algo. 42: NTT^-1 (s. 7.5)
|
||||
@[direct_array_access]
|
||||
fn inverse_ntt(f_ NttElement) RingElement {
|
||||
mut f := unsafe { f_ } // workaround for false mutability notice
|
||||
mut m := u8(255)
|
||||
|
||||
for start := 0; start < 256; start += 2 {
|
||||
zeta := FieldElement(zetas[m])
|
||||
m--
|
||||
|
||||
t := f[start]
|
||||
f[start] = field_add(t, f[start + 1])
|
||||
f[start + 1] = field_montgomery_mul_sub(zeta, f[start + 1], t)
|
||||
}
|
||||
|
||||
for start := 0; start < 256; start += 4 {
|
||||
zeta := FieldElement(zetas[m])
|
||||
m--
|
||||
|
||||
mut t := f[start]
|
||||
f[start] = field_add(t, f[start + 2])
|
||||
f[start + 2] = field_montgomery_mul_sub(zeta, f[start + 2], t)
|
||||
|
||||
t = f[start + 1]
|
||||
f[start + 1] = field_add(t, f[start + 3])
|
||||
f[start + 3] = field_montgomery_mul_sub(zeta, f[start + 3], t)
|
||||
}
|
||||
|
||||
for start := 0; start < 256; start += 8 {
|
||||
zeta := FieldElement(zetas[m])
|
||||
m--
|
||||
|
||||
mut t := f[start]
|
||||
f[start] = field_add(t, f[start + 4])
|
||||
f[start + 4] = field_montgomery_mul_sub(zeta, f[start + 4], t)
|
||||
|
||||
t = f[start + 1]
|
||||
f[start + 1] = field_add(t, f[start + 5])
|
||||
f[start + 5] = field_montgomery_mul_sub(zeta, f[start + 5], t)
|
||||
|
||||
t = f[start + 2]
|
||||
f[start + 2] = field_add(t, f[start + 6])
|
||||
f[start + 6] = field_montgomery_mul_sub(zeta, f[start + 6], t)
|
||||
|
||||
t = f[start + 3]
|
||||
f[start + 3] = field_add(t, f[start + 7])
|
||||
f[start + 7] = field_montgomery_mul_sub(zeta, f[start + 7], t)
|
||||
}
|
||||
|
||||
mut len2 := 8
|
||||
for len2 < 256 {
|
||||
mut start := 0
|
||||
for start < 256 {
|
||||
zeta := FieldElement(zetas[m])
|
||||
m--
|
||||
|
||||
mut j := 0
|
||||
for j < len2 {
|
||||
mut t := f[start + j]
|
||||
f[start + j] = field_add(t, f[start + len2 + j])
|
||||
f[start + len2 + j] = field_montgomery_mul_sub(zeta, f[start + len2 + j], t)
|
||||
|
||||
t = f[start + j + 1]
|
||||
f[start + j + 1] = field_add(t, f[start + len2 + j + 1])
|
||||
f[start + len2 + j + 1] = field_montgomery_mul_sub(zeta, f[start + len2 + j + 1], t)
|
||||
|
||||
j += 2
|
||||
}
|
||||
start += 2 * len2
|
||||
}
|
||||
len2 *= 2
|
||||
}
|
||||
|
||||
// algo. 42, line 21: f = 8347681 = 256^-1 mod q; in montgomery: 16382
|
||||
for i in 0 .. 256 {
|
||||
f[i] = field_montgomery_mul(f[i], 16382)
|
||||
}
|
||||
return RingElement(f)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// Ported to V from Go's crypto/internal/fips140/mldsa.
|
||||
module mldsa
|
||||
|
||||
// s. 4, table 1
|
||||
pub enum Kind {
|
||||
ml_dsa_44
|
||||
ml_dsa_65
|
||||
ml_dsa_87
|
||||
}
|
||||
|
||||
fn (k Kind) params() Params {
|
||||
return match k {
|
||||
.ml_dsa_44 { params_44 }
|
||||
.ml_dsa_65 { params_65 }
|
||||
.ml_dsa_87 { params_87 }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (k Kind) public_key_size() int {
|
||||
return pub_key_size(k.params())
|
||||
}
|
||||
|
||||
pub fn (k Kind) private_key_size() int {
|
||||
return priv_key_size(k.params())
|
||||
}
|
||||
|
||||
pub fn (k Kind) signature_size() int {
|
||||
return sig_size(k.params())
|
||||
}
|
||||
|
||||
// FIPS 204 s. 5.4: approved pre-hash functions for HashML-DSA.
|
||||
pub enum PreHash {
|
||||
none // pure ML-DSA (default)
|
||||
sha2_224
|
||||
sha2_256
|
||||
sha2_384
|
||||
sha2_512
|
||||
sha2_512_224
|
||||
sha2_512_256
|
||||
sha3_224
|
||||
sha3_256
|
||||
sha3_384
|
||||
sha3_512
|
||||
shake_128
|
||||
shake_256
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct SignerOpts {
|
||||
pub:
|
||||
context string
|
||||
deterministic bool
|
||||
prehash PreHash
|
||||
}
|
||||
|
||||
struct Params {
|
||||
k int
|
||||
l int
|
||||
eta int
|
||||
gamma1 int
|
||||
gamma2 int
|
||||
lambda int
|
||||
tau int
|
||||
omega int
|
||||
}
|
||||
|
||||
// s. 4, table 1
|
||||
const params_44 = Params{
|
||||
k: 4
|
||||
l: 4
|
||||
eta: 2
|
||||
gamma1: 17
|
||||
gamma2: 88
|
||||
lambda: 128
|
||||
tau: 39
|
||||
omega: 80
|
||||
}
|
||||
|
||||
const params_65 = Params{
|
||||
k: 6
|
||||
l: 5
|
||||
eta: 4
|
||||
gamma1: 19
|
||||
gamma2: 32
|
||||
lambda: 192
|
||||
tau: 49
|
||||
omega: 55
|
||||
}
|
||||
|
||||
const params_87 = Params{
|
||||
k: 8
|
||||
l: 7
|
||||
eta: 2
|
||||
gamma1: 19
|
||||
gamma2: 32
|
||||
lambda: 256
|
||||
tau: 60
|
||||
omega: 75
|
||||
}
|
||||
|
||||
pub const seed_size = 32
|
||||
|
||||
// s. 4, table 2
|
||||
pub const public_key_size_44 = 32 + 4 * n * 10 / 8
|
||||
pub const public_key_size_65 = 32 + 6 * n * 10 / 8
|
||||
pub const public_key_size_87 = 32 + 8 * n * 10 / 8
|
||||
|
||||
// s. 4, table 2
|
||||
pub const signature_size_44 = 128 / 4 + 4 * n * (17 + 1) / 8 + 80 + 4
|
||||
pub const signature_size_65 = 192 / 4 + 5 * n * (19 + 1) / 8 + 55 + 6
|
||||
pub const signature_size_87 = 256 / 4 + 7 * n * (19 + 1) / 8 + 75 + 8
|
||||
|
||||
fn pub_key_size(p Params) int {
|
||||
return 32 + p.k * n * 10 / 8
|
||||
}
|
||||
|
||||
fn priv_key_size(p Params) int {
|
||||
eta_bitlen := bits_len(u32(p.eta * 2))
|
||||
// rho + K + tr + l*n*eta-bit s1 + k*n*eta-bit s2 + k*n*13-bit t0
|
||||
return 32 + 32 + 64 + p.l * n * eta_bitlen / 8 + p.k * n * eta_bitlen / 8 + p.k * n * 13 / 8
|
||||
}
|
||||
|
||||
fn sig_size(p Params) int {
|
||||
return (p.lambda / 4) + p.l * n * (p.gamma1 + 1) / 8 + p.omega + p.k
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// FIPS 204 s. 5.4: Pre-Hash ML-DSA
|
||||
// signs PH(M) instead of M directly, prefer using pure ml-dsa when possible
|
||||
|
||||
module mldsa
|
||||
|
||||
import crypto.sha256
|
||||
import crypto.sha512
|
||||
import crypto.sha3
|
||||
|
||||
// algo. 4/5: DER-encoded OID for each pre-hash function
|
||||
// all under arc 2.16.840.1.101.3.4.2
|
||||
// joint-iso-itu-t(2) country(16) us(840) organization(1) gov(101) csor(3) nistAlgorithm(4) hashAlgs(2)
|
||||
fn prehash_oid(ph PreHash) []u8 {
|
||||
suffix := match ph {
|
||||
.sha2_256 { u8(0x01) }
|
||||
.sha2_384 { u8(0x02) }
|
||||
.sha2_512 { u8(0x03) }
|
||||
.sha2_224 { u8(0x04) }
|
||||
.sha2_512_224 { u8(0x05) }
|
||||
.sha2_512_256 { u8(0x06) }
|
||||
.sha3_224 { u8(0x07) }
|
||||
.sha3_256 { u8(0x08) }
|
||||
.sha3_384 { u8(0x09) }
|
||||
.sha3_512 { u8(0x0a) }
|
||||
.shake_128 { u8(0x0b) }
|
||||
.shake_256 { u8(0x0c) }
|
||||
// unreachable, called from mu_prehash, which is called from sign when ph != .none
|
||||
.none { panic('mldsa: prehash_oid called with .none') }
|
||||
}
|
||||
|
||||
return [u8(0x06), 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, suffix]
|
||||
}
|
||||
|
||||
// algo. 4, lines 10-22: PH(M) for the given hash function
|
||||
fn prehash_message(msg []u8, ph PreHash) []u8 {
|
||||
return match ph {
|
||||
.sha2_224 { sha256.sum224(msg) }
|
||||
.sha2_256 { sha256.sum256(msg) }
|
||||
.sha2_384 { sha512.sum384(msg) }
|
||||
.sha2_512 { sha512.sum512(msg) }
|
||||
.sha2_512_224 { sha512.sum512_224(msg) }
|
||||
.sha2_512_256 { sha512.sum512_256(msg) }
|
||||
.sha3_224 { sha3.sum224(msg) }
|
||||
.sha3_256 { sha3.sum256(msg) }
|
||||
.sha3_384 { sha3.sum384(msg) }
|
||||
.sha3_512 { sha3.sum512(msg) }
|
||||
.shake_128 { sha3.shake128(msg, 32) }
|
||||
.shake_256 { sha3.shake256(msg, 64) }
|
||||
// unreachable, called from mu_prehash, which is called from sign when ph != .none
|
||||
.none { panic('mldsa: prehash_message called with .none') }
|
||||
}
|
||||
}
|
||||
|
||||
// algo. 4, line 23: M' = 0x01 || |ctx| || ctx || OID || PH(M)
|
||||
// algo. 7, line 6: mu = H(tr || M')
|
||||
// compute_mu_prehash computes mu for prehash mode: H(tr || 0x01 || |ctx| || ctx || OID || PH(msg), 64).
|
||||
pub fn compute_mu_prehash(tr []u8, msg []u8, context string, ph PreHash) [64]u8 {
|
||||
mut h := sha3.new_shake256()
|
||||
h.write(tr)
|
||||
h.write([u8(0x01)]) // domain sep
|
||||
h.write([u8(context.len)])
|
||||
h.write(context.bytes())
|
||||
h.write(prehash_oid(ph))
|
||||
h.write(prehash_message(msg, ph))
|
||||
return slice_to_64(h.read(64))
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2025 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// Ported to V from Go's crypto/internal/fips140/mldsa.
|
||||
module mldsa
|
||||
|
||||
import crypto.sha3
|
||||
|
||||
// algo. 30: RejNTTPoly (s. 7.3)
|
||||
@[direct_array_access]
|
||||
fn sample_ntt(rho []u8, s u8, r u8) NttElement {
|
||||
mut g := sha3.new_shake128()
|
||||
g.write(rho)
|
||||
g.write([s, r])
|
||||
|
||||
mut a := NttElement{}
|
||||
mut j := 0
|
||||
mut buf := g.read(168)
|
||||
mut off := 0
|
||||
for j < n {
|
||||
if off + 2 >= buf.len {
|
||||
buf = g.read(168)
|
||||
off = 0
|
||||
}
|
||||
v := u32(buf[off]) | (u32(buf[off + 1]) << 8) | (u32(buf[off + 2]) << 16)
|
||||
off += 3
|
||||
candidate := v & 0x7fffff
|
||||
if candidate < q {
|
||||
a[j] = field_to_montgomery(candidate) or { continue }
|
||||
j++
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// algo. 31: RejBoundedPoly (s. 7.3)
|
||||
@[direct_array_access]
|
||||
fn sample_bounded_poly(rho []u8, r u8, p Params) RingElement {
|
||||
mut h := sha3.new_shake256()
|
||||
h.write(rho)
|
||||
h.write([r, 0])
|
||||
|
||||
mut a := RingElement{}
|
||||
mut j := 0
|
||||
mut buf := h.read(136)
|
||||
mut off := 0
|
||||
for {
|
||||
if off >= buf.len {
|
||||
buf = h.read(136)
|
||||
off = 0
|
||||
}
|
||||
z0 := buf[off] & 0x0f
|
||||
z1 := buf[off] >> 4
|
||||
off++
|
||||
|
||||
coeff, ok := coeff_from_half_byte(z0, p)
|
||||
if ok {
|
||||
a[j] = coeff
|
||||
j++
|
||||
}
|
||||
if j >= n {
|
||||
break
|
||||
}
|
||||
|
||||
coeff2, ok2 := coeff_from_half_byte(z1, p)
|
||||
if ok2 {
|
||||
a[j] = coeff2
|
||||
j++
|
||||
}
|
||||
if j >= n {
|
||||
break
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// algo. 29: SampleInBall (s. 7.3)
|
||||
@[direct_array_access]
|
||||
fn sample_in_ball(rho []u8, p Params) RingElement {
|
||||
mut h := sha3.new_shake256()
|
||||
h.write(rho)
|
||||
s := h.read(8)
|
||||
|
||||
// pre-read ~2x expected bytes to avoid per-byte allocations
|
||||
mut buf := h.read(p.tau * 2)
|
||||
mut off := 0
|
||||
|
||||
mut c := RingElement{}
|
||||
for i := 256 - p.tau; i < 256; i++ {
|
||||
for {
|
||||
if off >= buf.len {
|
||||
buf = h.read(256)
|
||||
off = 0
|
||||
}
|
||||
j := buf[off]
|
||||
off++
|
||||
if j <= u8(i) {
|
||||
c[i] = c[j]
|
||||
bit_idx := i + p.tau - 256
|
||||
bit := (s[bit_idx / 8] >> (bit_idx % 8)) & 1
|
||||
if bit == 0 {
|
||||
c[j] = mont_one
|
||||
} else {
|
||||
c[j] = mont_minus_one
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// algo. 15: CoeffFromHalfByte (s. 7.1)
|
||||
fn coeff_from_half_byte(b u8, p Params) (FieldElement, bool) {
|
||||
match p.eta {
|
||||
2 {
|
||||
if b > 14 {
|
||||
return FieldElement(0), false
|
||||
}
|
||||
quotient := (u32(b) * 0x3334) >> 16 // barrett b % 5
|
||||
remainder := u32(b) - quotient * 5
|
||||
return field_sub_to_montgomery(2, remainder), true
|
||||
}
|
||||
4 {
|
||||
if b > 8 {
|
||||
return FieldElement(0), false
|
||||
}
|
||||
return field_sub_to_montgomery(4, u32(b)), true
|
||||
}
|
||||
else {
|
||||
panic('mldsa: unsupported eta') // unreachable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// algo. 32: ExpandA (s. 7.3)
|
||||
fn compute_matrix_a(rho []u8, p Params) []NttElement {
|
||||
mut a := []NttElement{len: p.k * p.l}
|
||||
for r in 0 .. p.k {
|
||||
for s in 0 .. p.l {
|
||||
a[r * p.l + s] = sample_ntt(rho, u8(s), u8(r))
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
// generates ML-DSA test vectors using Go's crypto/internal/fips140/mldsa
|
||||
// run from within the Go source tree because mldsa is not public yet
|
||||
// see https://github.com/golang/go/issues/77626
|
||||
//
|
||||
// this is normally ran via gen.vsh
|
||||
//
|
||||
// GOROOT=. ./bin/go run ./src/crypto/internal/fips140/mldsa/gen
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/internal/fips140/mldsa"
|
||||
"crypto/internal/fips140/sha3"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Vector struct {
|
||||
Kind string `json:"kind"`
|
||||
Seed string `json:"seed"`
|
||||
Msg string `json:"msg"`
|
||||
PkSha256 string `json:"pk_sha256"`
|
||||
SigSha256 string `json:"sig_sha256"`
|
||||
Context string `json:"context,omitempty"`
|
||||
}
|
||||
|
||||
type variant struct {
|
||||
name string
|
||||
newPrivateKey func([]byte) (*mldsa.PrivateKey, error)
|
||||
newPublicKey func([]byte) (*mldsa.PublicKey, error)
|
||||
}
|
||||
|
||||
func main() {
|
||||
variants := []variant{
|
||||
{"ml_dsa_44", mldsa.NewPrivateKey44, mldsa.NewPublicKey44},
|
||||
{"ml_dsa_65", mldsa.NewPrivateKey65, mldsa.NewPublicKey65},
|
||||
{"ml_dsa_87", mldsa.NewPrivateKey87, mldsa.NewPublicKey87},
|
||||
}
|
||||
|
||||
s := sha3.NewShake128()
|
||||
seed := make([]byte, 32)
|
||||
var vectors []Vector
|
||||
|
||||
for _, v := range variants {
|
||||
for i := 0; i < 3; i++ {
|
||||
s.Read(seed)
|
||||
priv, err := v.newPrivateKey(seed)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pk := priv.PublicKey().Bytes()
|
||||
|
||||
msg := make([]byte, 32+i*17)
|
||||
s.Read(msg)
|
||||
|
||||
sig, err := mldsa.SignDeterministic(priv, msg, "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pub, err := v.newPublicKey(pk)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := mldsa.Verify(pub, msg, sig, ""); err != nil {
|
||||
panic(fmt.Sprintf("verify failed: %v", err))
|
||||
}
|
||||
|
||||
pkHash := sha256.Sum256(pk)
|
||||
sigHash := sha256.Sum256(sig)
|
||||
vectors = append(vectors, Vector{
|
||||
Kind: v.name,
|
||||
Seed: hex.EncodeToString(seed),
|
||||
Msg: hex.EncodeToString(msg),
|
||||
PkSha256: hex.EncodeToString(pkHash[:]),
|
||||
SigSha256: hex.EncodeToString(sigHash[:]),
|
||||
})
|
||||
}
|
||||
|
||||
s.Read(seed)
|
||||
priv, err := v.newPrivateKey(seed)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pk := priv.PublicKey().Bytes()
|
||||
msg := make([]byte, 40)
|
||||
s.Read(msg)
|
||||
|
||||
sig, err := mldsa.SignDeterministic(priv, msg, "test-context")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pub, err := v.newPublicKey(pk)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := mldsa.Verify(pub, msg, sig, "test-context"); err != nil {
|
||||
panic(fmt.Sprintf("verify with context failed: %v", err))
|
||||
}
|
||||
|
||||
pkHash := sha256.Sum256(pk)
|
||||
sigHash := sha256.Sum256(sig)
|
||||
vectors = append(vectors, Vector{
|
||||
Kind: v.name,
|
||||
Seed: hex.EncodeToString(seed),
|
||||
Msg: hex.EncodeToString(msg),
|
||||
PkSha256: hex.EncodeToString(pkHash[:]),
|
||||
SigSha256: hex.EncodeToString(sigHash[:]),
|
||||
Context: "test-context",
|
||||
})
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(vectors); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env v
|
||||
|
||||
import os
|
||||
|
||||
// generates ML-DSA test vectors by building Go's reference implementation
|
||||
// and running gen.go with crypto/internal/fips140/mldsa
|
||||
//
|
||||
// Usage:
|
||||
// v run testdata/gen.vsh
|
||||
// v run testdata/gen.vsh <go-source-path> # uses existing Go tree
|
||||
|
||||
const go_repo = 'https://github.com/golang/go.git'
|
||||
const go_ref = 'master'
|
||||
const go_mldsa_subpath = os.join_path('src', 'crypto', 'internal', 'fips140', 'mldsa')
|
||||
const go_gen_pkg = os.join_path('.', go_mldsa_subpath, 'gen')
|
||||
|
||||
fn main() {
|
||||
unbuffer_stdout()
|
||||
script_dir := os.dir(@FILE)
|
||||
|
||||
mut go_root := os.join_path(os.temp_dir(), 'go')
|
||||
if os.args.len > 1 {
|
||||
go_root = os.args[1]
|
||||
if !os.exists(os.join_path(go_root, go_mldsa_subpath)) {
|
||||
eprintln('error: ${go_root} does not look like a Go source tree')
|
||||
exit(1)
|
||||
}
|
||||
println('> Using existing Go tree at ${go_root}')
|
||||
} else {
|
||||
if os.exists(os.join_path(go_root, 'src')) {
|
||||
println('> Reusing cached Go tree at ${go_root}')
|
||||
run_or_exit('git -C ${go_root} pull --quiet')
|
||||
} else {
|
||||
println('> Cloning Go source tree to ${go_root}...')
|
||||
run_or_exit('git clone --depth 1 --branch ${go_ref} ${go_repo} ${go_root}')
|
||||
}
|
||||
}
|
||||
|
||||
go_bin := os.join_path(go_root, 'bin', 'go')
|
||||
if !os.exists(go_bin) {
|
||||
println('> Building Go toolchain...')
|
||||
run_or_exit('cd ${go_root}/src && bash make.bash')
|
||||
}
|
||||
println('> Go binary: ${go_bin}')
|
||||
|
||||
gen_dst_dir := os.join_path(go_root, go_mldsa_subpath, 'gen')
|
||||
os.mkdir_all(gen_dst_dir) or {
|
||||
eprintln('error: mkdir failed: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
os.cp(os.join_path(script_dir, 'gen.go'), os.join_path(gen_dst_dir, 'main.go')) or {
|
||||
eprintln('error: copy failed: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
println('> Generating test vectors...')
|
||||
res := run_or_exit('cd ${go_root} && GOROOT=${go_root} ${go_bin} run ${go_gen_pkg}')
|
||||
|
||||
os.rmdir_all(gen_dst_dir) or { eprintln('warning: rmdir failed: ${err}') }
|
||||
|
||||
vectors_path := os.join_path(script_dir, 'vectors.json')
|
||||
os.write_file(vectors_path, res) or {
|
||||
eprintln('error: write failed: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
println('> Wrote ${vectors_path}')
|
||||
}
|
||||
|
||||
fn run_or_exit(cmd string) string {
|
||||
res := os.execute_opt(cmd) or {
|
||||
eprintln('error: ${err}')
|
||||
exit(1)
|
||||
}
|
||||
if res.exit_code != 0 {
|
||||
eprintln('error: ${cmd}\n${res.output}')
|
||||
exit(1)
|
||||
}
|
||||
return res.output
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
[
|
||||
{
|
||||
"kind": "ml_dsa_44",
|
||||
"seed": "7f9c2ba4e88f827d616045507605853ed73b8093f6efbc88eb1a6eacfa66ef26",
|
||||
"msg": "3cb1eea988004b93103cfb0aeefd2a686e01fa4a58e8a3639ca8a1e3f9ae57e2",
|
||||
"pk_sha256": "678babd487c6319b5c23a54d179b513629ec1f728c3f5c50a81ef2b564ac25fe",
|
||||
"sig_sha256": "59fc79b87095bccf459639b831a588fa03ec7e341800459d13b527807e118cf2"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_44",
|
||||
"seed": "35b8cc873c23dc62b8d260169afa2f75ab916a58d974918835d25e6a435085b2",
|
||||
"msg": "badfd6dfaac359a5efbb7bcc4b59d538df9a04302e10c8bc1cbf1a0b3a5120ea17cda7cfad765f5623474d368ccca8af00",
|
||||
"pk_sha256": "a55cc6aa33a79d1b8349fedb9cad362555bf6440d2df48fbf80002234cb3366b",
|
||||
"sig_sha256": "29cbfbf3d8f618777f4411c7c41e58b253dd854531682a279eac3fc4fe4787ce"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_44",
|
||||
"seed": "07cd9f5e4c849f167a580b14aabdefaee7eef47cb0fca9767be1fda69419dfb9",
|
||||
"msg": "27e9df07348b196691abaeb580b32def58538b8d23f87732ea63b02b4fa0f4873360e2841928cd60dd4cee8cc0d4c922a96188d032675c8ac850933c7aff1533b94c",
|
||||
"pk_sha256": "71a532a9bb9e27b13a8429dab5c83fe6459ddb9af7f97176622efe990660bf6c",
|
||||
"sig_sha256": "3b14b5745c46d59df7296c46fc6dad0a7b4d82a78ebf6875571b78ea4b7ee720"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_44",
|
||||
"seed": "834adbb69c6115bad4692d8619f90b0cdf8a7b9c264029ac185b70b83f2801f2",
|
||||
"msg": "f4b3f70c593ea3aeeb613a7f1b1de33fd75081f592305f2e4526edc09631b10958f464d889f31ba0",
|
||||
"pk_sha256": "7142dd3dedabd42fe95cd8163c20b1fa0726873c5124c351e1a0184266bbb0c0",
|
||||
"sig_sha256": "dc746996fabd1798c059e50909c19116b8ad9a176d68559b215698fd8391ce87",
|
||||
"context": "test-context"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_65",
|
||||
"seed": "10250fda7f1368ec2967fc84ef2ae9aff268e0b1700affc6820b523a3d917135",
|
||||
"msg": "f2dff2ee06bfe72b3124721d4a26c04e53a75e30e73a7a9c4a95d91c55d495e9",
|
||||
"pk_sha256": "f5796043408f48fa583f50c8b0dc213db904ec2aa0ed260ade17e209483fbcfe",
|
||||
"sig_sha256": "8361bb70f9ff33f5a724deae3bda69b0e196b069559aba43699529e81f54213f"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_65",
|
||||
"seed": "f51dd0b5e9d83c6d5e8ce803aa62b8d654db53d09b8dcff273cdfeb573fad8bc",
|
||||
"msg": "d45578bec2e770d01efde86e721a3f7c6cce275dabe6e2143f1af18da7efddc4c7b70b5e345db93cc936bea323491ccb38",
|
||||
"pk_sha256": "fa7338dd88bf2e4a2cc2ea54329bc985d6513210f8e8d6642ee5b89b919def61",
|
||||
"sig_sha256": "0094df47dabf8e010c475e2ae15a8e16e6c2f4ae5806f3fb8abcad274fafe766"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_65",
|
||||
"seed": "a388f546a9ff00dd4e1300b9b2153d2041d205b443e41b45a653f2a5c4492c1a",
|
||||
"msg": "dd544512dda2529833462b71a41a45be97290b6f4cffda2cf990051634a4b1edf6114fb49083c1fa3b302ee097f051266be69dc716fdeef91b0d4ab2de525550bf80",
|
||||
"pk_sha256": "87c5c183b07579064590a5635d2270064ecc7a15f237bd8bc4c3d33fc26fbf83",
|
||||
"sig_sha256": "9c485e41e4e6f4d88f91bef234273be74eedfdb00cb295e392b787dcfdbf928a"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_65",
|
||||
"seed": "dc8a684bc3b5a4d46b7efae7afdc6292988dc9acae03f8634486c1abe2781aae",
|
||||
"msg": "4c02f3460d2cd4e6a463a2ba9562ee623cf0e9f82ab4d0b5c9d040a269366479dff0038abfaf2e0f",
|
||||
"pk_sha256": "ed9ad798178aca566a43a13f62a543d2813ce0a25cf64f0c9b434621c4196d1f",
|
||||
"sig_sha256": "4b13f48b7cc3d2285818cd0a245f615b63980661fcf2d03bc2fb82232eab8c2a",
|
||||
"context": "test-context"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_87",
|
||||
"seed": "f21f36968972e3f104ddcbe1eb831a87c213162e29b34adfa564d121e9f6e772",
|
||||
"msg": "9f4203fc5c6c22fa7a7350afddb620923a4a129b8acb19ea10f818c30e3b5b1c",
|
||||
"pk_sha256": "d775983143d00204131c7cee69560fab5fc57980c72f3fd44e2daeac5ebfc935",
|
||||
"sig_sha256": "5486471f23ff0426764108476ed6d05f55b778a0e268928af2814f59c614300e"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_87",
|
||||
"seed": "571fa79e57ee304388316a02fcd93a0d8ee02bb85701ee4ff097534b502c1b12",
|
||||
"msg": "fbb95c8ccb2f548921d99cc7c9fe17ac991b675e631144423eef7a5869168da63d1f4c21f650c02923bfd396ca6a5db541",
|
||||
"pk_sha256": "43e98d4a1f8add2e68d962728e65cd7d768cb40a24e9628eaa4f8ee8e71cabc2",
|
||||
"sig_sha256": "9e85f12b881853cd769c260ed3d9653b661f7556fb0930ea41bf7f52b968863f"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_87",
|
||||
"seed": "068624cbc5ffe208c0d1a74e1a29618d0bb60036f5249abfa88898e393718d6e",
|
||||
"msg": "fab05bb41279efcd4c5a0cc837ccfc22be4f725c081f6aa090749dba7077bae8d41af3fec5a6ee1b8adcd25e72de36434584ef567c643d344294e8b2086b87f69c3b",
|
||||
"pk_sha256": "7de08e43f59c2ac1e0512a9ad123e94170f66ca2eca488af9fb8edf594afbbca",
|
||||
"sig_sha256": "f18af676ecdda593f085d956723534948d7564ea52b65e8f5264566d5f1d6181"
|
||||
},
|
||||
{
|
||||
"kind": "ml_dsa_87",
|
||||
"seed": "dc0d5969857082987ca1c63b7182e86898fb9b8039e75eda219e289331610369",
|
||||
"msg": "271867b145b2908293963cd677c9a1ae6ceb28289b254cdeb76b12f33ce5cf3743131bfb550f0197",
|
||||
"pk_sha256": "7f38baac96bb9b7608ec4d3f60c9e8f0233f178bb46d2dca3c61a0b44ee36031",
|
||||
"sig_sha256": "3bec7820e2c95073ac6456de42f5b18c6aade840e0b18a2d79fe9027f70707fa",
|
||||
"context": "test-context"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
// vtest build: has_modern_openssl?
|
||||
module mldsa_test
|
||||
|
||||
import x.crypto.mldsa
|
||||
|
||||
fn test_mldsa_basic_sign_and_verify() ! {
|
||||
pv := mldsa.PrivateKey.generate(.ml_dsa_44)!
|
||||
pb := pv.public_key()
|
||||
msg := 'ml-dsa basic roundtrip'.bytes()
|
||||
sig := pv.sign(msg, deterministic: true)!
|
||||
assert sig.len > 0
|
||||
assert pb.verify(msg, sig)!
|
||||
mismatch := pb.verify('ml-dsa mismatch'.bytes(), sig)!
|
||||
assert !mismatch
|
||||
}
|
||||
|
||||
fn test_mldsa_seed_and_raw_key_roundtrip() ! {
|
||||
kind := mldsa.Kind.ml_dsa_65
|
||||
pv := mldsa.PrivateKey.generate(kind)!
|
||||
seed := pv.seed()
|
||||
priv := pv.bytes()
|
||||
pub_bytes := pv.public_key().bytes()
|
||||
assert seed.len == 32
|
||||
assert priv.len == kind.private_key_size()
|
||||
assert pub_bytes.len == kind.public_key_size()
|
||||
|
||||
pv_from_seed := mldsa.PrivateKey.from_seed(seed, kind)!
|
||||
assert pv_from_seed.bytes() == priv
|
||||
|
||||
pb := mldsa.PublicKey.from_bytes(pub_bytes, kind)!
|
||||
msg := 'ml-dsa imported public key'.bytes()
|
||||
sig := pv.sign(msg, deterministic: true)!
|
||||
assert pb.verify(msg, sig)!
|
||||
}
|
||||
Reference in new issue
Block a user