Skip to main content

cryptography/public_key/
ntru_hrss701.rs

1//! NTRU-HRSS-701 — round-3 NTRU parameter set $(N = 701, q = 8192)$.
2//!
3//! Algorithmic core, OWCPA + FO-style KEM, and side-channel inventory
4//! are documented in [`crate::public_key::ntru_pqc_shared`]; this file
5//! is the parameter binding plus the five HRSS-specific
6//! [`NtruVariant`](crate::public_key::ntru_pqc_shared::NtruVariant)
7//! method overrides:
8//!
9//! - `sample_fg` uses `Sample_iid_plus` for both `f` and `g` (sample
10//!   IID then conditionally negate even-indexed coefficients so that
11//!   $\langle x \cdot r, r \rangle \ge 0$); HPS uses fixed-weight
12//!   sampling for `g`.
13//! - `sample_rm` uses iid-only for both `r` and `m`; HPS uses
14//!   fixed-weight for `m`.
15//! - `update_g_after_z3_to_zq` computes $g \gets 3 (x - 1) g$; HPS
16//!   uses $g \gets 3 g$.
17//! - `poly_lift` uses the $(x - 1)$-factor lift; HPS uses the
18//!   trivial $\mathbb{Z}_3 \to \mathbb{Z}_q$ embedding.
19//! - `check_m` is a no-op; HRSS accepts any $S_3$ message.
20//!
21//! HRSS also picks the LOGQ-13 Sq packer (13 bits per coefficient).
22//!
23//! Validated against all 100 entries of the round-3 KAT file
24//! `PQCkemKAT_1450.rsp` (sampled subset by default; full sweep under
25//! `--ignored`).
26
27
28
29
30// ---- parameter constants ---------------------------------------------------
31
32const N: usize = 701;
33const LOGQ: usize = 13;
34const Q: u32 = 1 << LOGQ;
35const Q_MASK: u16 = (Q as u16).wrapping_sub(1);
36
37const PRFKEYBYTES: usize = 32;
38const SHAREDKEYBYTES: usize = 32;
39
40const SAMPLE_IID_BYTES: usize = N - 1; // 700
41const SAMPLE_FG_BYTES: usize = 2 * SAMPLE_IID_BYTES; // 1400
42const SAMPLE_RM_BYTES: usize = 2 * SAMPLE_IID_BYTES; // 1400
43
44const PACK_DEG: usize = N - 1; // 700
45const PACK_TRINARY_BYTES: usize = (PACK_DEG + 4) / 5; // 140
46
47const OWCPA_MSGBYTES: usize = 2 * PACK_TRINARY_BYTES; // 280
48const OWCPA_PUBLICKEYBYTES: usize = (LOGQ * PACK_DEG + 7) / 8; // 1138
49const OWCPA_SECRETKEYBYTES: usize = 2 * PACK_TRINARY_BYTES + OWCPA_PUBLICKEYBYTES; // 1418
50const OWCPA_BYTES: usize = (LOGQ * PACK_DEG + 7) / 8; // 1138
51
52/// Public-key length in bytes.
53pub const PUBLIC_KEY_BYTES: usize = OWCPA_PUBLICKEYBYTES; // 1138
54/// Private-key length in bytes (includes implicit-rejection PRF key).
55pub const PRIVATE_KEY_BYTES: usize = OWCPA_SECRETKEYBYTES + PRFKEYBYTES; // 1450
56/// Ciphertext length in bytes.
57pub const CIPHERTEXT_BYTES: usize = OWCPA_BYTES; // 1138
58/// Shared-secret length in bytes.
59pub const SHARED_SECRET_BYTES: usize = SHAREDKEYBYTES; // 32
60
61// ---- lift(m) for HRSS: (x - 1) * (a / (x - 1) mod (3, Phi_n)) --------------
62
63fn poly_lift_hrss(r: &mut [u16; N], a: &[u16; N]) {
64    // HRSS lift: compute b = a / (x - 1) mod (3, Phi_n) and then r = (x-1)*b.
65    //
66    // Define z by <z * x^i, x - 1> = delta_{i,0} mod 3:
67    //   t      = -1/N mod 3 = -N mod 3 = 3 - (N mod 3)
68    //   z[0]   = 2 - t mod 3
69    //   z[1]   = 0   mod 3
70    //   z[j]   = z[j-1] + t mod 3
71    // Then b[k] = <z * x^k, a>.
72    let t: u16 = (3 - (N % 3)) as u16;
73
74    // z[1] = 0 (drops the a[1]·0 and a[2]·0 cross-terms in b[0]/b[1]).
75    let mut b = [0u16; N];
76    b[0] = a[0]
77        .wrapping_mul(2u16.wrapping_sub(t))
78        .wrapping_add(a[2].wrapping_mul(t));
79    b[1] = a[1].wrapping_mul(2u16.wrapping_sub(t));
80    b[2] = a[2].wrapping_mul(2u16.wrapping_sub(t));
81
82    let mut zj: u16 = 0; // z[1]
83    for i in 3..N {
84        b[0] = b[0].wrapping_add(a[i].wrapping_mul(zj.wrapping_add(2 * t)));
85        b[1] = b[1].wrapping_add(a[i].wrapping_mul(zj.wrapping_add(t)));
86        b[2] = b[2].wrapping_add(a[i].wrapping_mul(zj));
87        // `t` and `zj` are public constants of the loop iteration, so a
88        // hardware modulo is fine here; the rest of the file routes
89        // mod-3 reductions through `crate::public_key::ntru_pqc_shared::mod3`
90        // because they take secret-derived inputs.
91        zj = (zj.wrapping_add(t)) % 3;
92    }
93    b[1] = b[1].wrapping_add(a[0].wrapping_mul(zj.wrapping_add(t)));
94    b[2] = b[2].wrapping_add(a[0].wrapping_mul(zj));
95    b[2] = b[2].wrapping_add(a[1].wrapping_mul(zj.wrapping_add(t)));
96
97    for i in 3..N {
98        b[i] = b[i - 3].wrapping_add(
99            2u16.wrapping_mul(a[i].wrapping_add(a[i - 1]).wrapping_add(a[i - 2])),
100        );
101    }
102
103    crate::public_key::ntru_pqc_shared::poly_mod_3_phi_n::<N>(&mut b);
104    crate::public_key::ntru_pqc_shared::poly_z3_to_zq::<N>(&mut b, Q_MASK);
105
106    // r := (x - 1) * b
107    r[0] = 0u16.wrapping_sub(b[0]);
108    for i in 0..N - 1 {
109        r[i + 1] = b[i].wrapping_sub(b[i + 1]);
110    }
111}
112
113// ---- HRSS Sample_iid_plus distribution -------------------------------------
114//
115// Sample r via the IID-uniform-mod-3 distribution, then conditionally
116// negate the even-indexed coefficients so that <x · r, r> >= 0. This is the
117// only sampling distribution HRSS uses (replacing the HPS mix of IID for f
118// and fixed-weight for g/m).
119
120fn sample_iid_plus(r: &mut [u16; N], uniform_bytes: &[u8]) {
121    debug_assert_eq!(uniform_bytes.len(), SAMPLE_IID_BYTES);
122    crate::public_key::ntru_pqc_shared::sample_iid::<N>(r, uniform_bytes);
123
124    // Map {0, 1, 2} -> {0, 1, 2^16 - 1}
125    for i in 0..N - 1 {
126        let c = r[i];
127        r[i] = c | (0u16.wrapping_sub(c >> 1));
128    }
129
130    // s = <x * r, r>; r[N-1] is zero. All arithmetic is u16 wrapping;
131    // the C reference widens to u32 around the multiplies but the
132    // truncation back to u16 makes the widening a no-op.
133    let mut s: u16 = 0;
134    for i in 0..N - 1 {
135        s = s.wrapping_add(r[i + 1].wrapping_mul(r[i]));
136    }
137
138    // sign(s) — sign(0) = 1; the C uses `1 | (-(s>>15))`.
139    let s_sign: u16 = 1 | 0u16.wrapping_sub(s >> 15);
140
141    let mut i = 0;
142    while i < N {
143        r[i] = s_sign.wrapping_mul(r[i]);
144        i += 2;
145    }
146
147    // Map {0, 1, 2^16-1} -> {0, 1, 2}
148    for i in 0..N {
149        r[i] = 3 & (r[i] ^ (r[i] >> 15));
150    }
151}
152
153// ---- variant marker -------------------------------------------------------
154
155struct Hrss701Variant;
156
157impl crate::public_key::ntru_pqc_shared::NtruVariant<N, LOGQ> for Hrss701Variant {
158    const Q_MASK: u16 = Q_MASK;
159    /// HRSS-701 doesn't use fixed-weight sampling; the value here is
160    /// irrelevant because every variant-specific `sample_*` /
161    /// `check_m` method is overridden below.
162    const WEIGHT: usize = 0;
163    const SAMPLE_FG_BYTES: usize = SAMPLE_FG_BYTES;
164    const SAMPLE_RM_BYTES: usize = SAMPLE_RM_BYTES;
165    const PACK_TRINARY_BYTES: usize = PACK_TRINARY_BYTES;
166    const OWCPA_PUBLICKEYBYTES: usize = OWCPA_PUBLICKEYBYTES;
167    const OWCPA_SECRETKEYBYTES: usize = OWCPA_SECRETKEYBYTES;
168    const OWCPA_BYTES: usize = OWCPA_BYTES;
169    const OWCPA_MSGBYTES: usize = OWCPA_MSGBYTES;
170
171    fn sample_fg(f: &mut [u16; N], g: &mut [u16; N], seed: &[u8]) {
172        debug_assert_eq!(seed.len(), SAMPLE_FG_BYTES);
173        sample_iid_plus(f, &seed[..SAMPLE_IID_BYTES]);
174        sample_iid_plus(g, &seed[SAMPLE_IID_BYTES..]);
175    }
176
177    fn sample_rm(r: &mut [u16; N], m: &mut [u16; N], seed: &[u8]) {
178        debug_assert_eq!(seed.len(), SAMPLE_RM_BYTES);
179        crate::public_key::ntru_pqc_shared::sample_iid::<N>(r, &seed[..SAMPLE_IID_BYTES]);
180        crate::public_key::ntru_pqc_shared::sample_iid::<N>(m, &seed[SAMPLE_IID_BYTES..]);
181    }
182
183    fn update_g_after_z3_to_zq(g: &mut [u16; N]) {
184        // HRSS branch: g <- 3 * (x - 1) * g  (mod q)
185        for i in (1..N).rev() {
186            g[i] = (3u16).wrapping_mul(g[i - 1].wrapping_sub(g[i]));
187        }
188        g[0] = 0u16.wrapping_sub((3u16).wrapping_mul(g[0]));
189    }
190
191    fn poly_lift(r: &mut [u16; N], a: &[u16; N]) {
192        poly_lift_hrss(r, a);
193    }
194
195    fn check_m(_m: &[u16; N]) -> i32 {
196        // HRSS accepts any S_3 element.
197        0
198    }
199
200    fn poly_sq_tobytes(r: &mut [u8], a: &[u16; N]) {
201        crate::public_key::ntru_pqc_shared::poly_sq_tobytes_logq13::<N>(r, a);
202    }
203
204    fn poly_sq_frombytes(r: &mut [u16; N], a: &[u8]) {
205        crate::public_key::ntru_pqc_shared::poly_sq_frombytes_logq13::<N>(r, a);
206    }
207}
208
209// ---- public API + standard tests (macro-generated) -------------------------
210
211crate::public_key::ntru_pqc_shared::define_pqc_kem! {
212    namespace = NtruHrss701,
213    public_key = NtruHrss701PublicKey,
214    private_key = NtruHrss701PrivateKey,
215    ciphertext = NtruHrss701Ciphertext,
216    shared_secret = NtruHrss701SharedSecret,
217    variant = Hrss701Variant,
218    kat_path = "../../kat/ntruhrss701.rsp",
219}