Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+27
View File
@@ -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.
+33
View File
@@ -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
}
```
+594
View File
@@ -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
}
+159
View File
@@ -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))
}
+87
View File
@@ -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
+518
View File
@@ -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
}
+221
View File
@@ -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}'
}
}
+188
View File
@@ -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)
}
+129
View File
@@ -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
}
+66
View File
@@ -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))
}
+146
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+89
View File
@@ -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"
}
]
+34
View File
@@ -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)!
}