dstu_core/crypto_sign257.rs
1//! `crypto_sign` equivalent for DSTU 4145's `m=257` curve - additive sibling of
2//! [`crate::crypto_sign`], mirroring its shape exactly (`SigningKey`/`VerifyingKey`/`Signature`,
3//! deterministic nonce derivation, `sign`/`sign_digest`/`verify`/`verify_digest`), built on
4//! `hazmat::dstu4145::{gf2m257, curve257, scalar257, signature257}` instead of the `m=163`
5//! modules. `docs/TASKS.md` T-199, `docs/DECISIONS.md` D-185/D-186.
6//!
7//! **Why a separate module, not a curve-selecting `crypto_sign`** (`docs/DECISIONS.md` D-186's own
8//! addendum, reversing that entry's original Decisions 1-3): converting `crypto_sign`'s
9//! `SigningKey`/`VerifyingKey`/`Signature` into curve-tagged enums would break
10//! `dstu-core-capi/src/sign.rs`'s C ABI (a separate root-workspace crate wrapping these exact
11//! types) for no benefit the additive-sibling shape doesn't also deliver - and matches the
12//! project's own established precedent for exactly this situation (`crypto_box512`, T-193:
13//! additive sibling module, capi/binding wiring explicitly deferred as a separate task). This
14//! shape is also *stronger* on the original downgrade concern D-186 Decision 2 raised: with
15//! distinct types, a caller wanting `m=257`-level assurance cannot accidentally accept an `m=163`
16//! signature at all - the compiler forbids it, rather than relying on a caller to inspect a
17//! returned `CurveId` and not ignore it.
18//!
19//! **The curve-tag byte (`docs/DECISIONS.md` D-186 Decision 1) lives at the `uacrypt`
20//! serialization layer, not here** - this module's own `to_bytes`/`from_bytes` are plain
21//! fixed-width encodings (33/66 bytes), same convention as `crypto_sign`'s untagged 21/42-byte
22//! ones. A shared `CurveId`/tagged-blob reader belongs where untrusted, curve-unknown-in-advance
23//! input is actually parsed (`uacrypt verify`, T-199's own remaining scope) - duplicating a tag
24//! byte into every language binding's own hand-rolled parser is exactly the D-118 lesson
25//! (`crypto_secretstream`'s wire-format validation) this project already learned once.
26//!
27//! `VerifyingKey::verify`/`verify_digest` return a plain `bool`, same ergonomics as `crypto_sign`,
28//! not `Result<CurveId, _>` (D-186 Decision 2's original text): once a caller holds a
29//! `crypto_sign257::VerifyingKey` rather than `crypto_sign::VerifyingKey`, the curve is already
30//! known statically, nothing to report back.
31//!
32//! Nonce derivation uses `hazmat::kupyna_kmac::Kupyna384Kmac` (48-byte key/output), **not**
33//! `crypto_sign`'s `Kupyna256Kmac`: `curve257::order()` is itself ~256 bits, so folding a
34//! same-width 256-bit KMAC output mod it (as `crypto_sign`'s 256-bit-output-mod-~163-bit-`n` does
35//! safely, that ratio being wide enough for the bias to be cryptographically negligible) would
36//! reintroduce real bias here - flagged as unresolved in `docs/DECISIONS.md` D-186 Decision 5,
37//! closed by widening to a 384-bit KMAC output instead (128 bits of margin over `n`'s ~256 bits).
38//!
39//! See [`crate::crypto_sign`]'s own module doc for the full design rationale this mirrors
40//! (deterministic-nonce misuse-resistance argument, the `Q = -d*G` convention, the `sign`/
41//! `sign_digest` split for large/streamed messages) - not restated here.
42//!
43//! # Example
44//!
45//! ```rust
46//! use dstu_core::crypto_sign257::SigningKey;
47//!
48//! # if cfg!(miri) { return; } // scalar_multiply calls - minutes each under Miri's interpreter
49//! let signing_key = SigningKey::generate().expect("OS CSPRNG should not fail");
50//! let verifying_key = signing_key.verifying_key();
51//!
52//! let message = b"a message whose origin and integrity matter";
53//! let signature = signing_key.sign(message);
54//! assert!(verifying_key.verify(message, &signature));
55//!
56//! assert!(!verifying_key.verify(b"a different message", &signature));
57//! let other_key = SigningKey::generate().expect("OS CSPRNG should not fail");
58//! assert!(!other_key.verifying_key().verify(message, &signature));
59//! ```
60
61use crate::hazmat::dstu4145::curve257::{self, Point};
62use crate::hazmat::dstu4145::gf2m257::FieldElement;
63use crate::hazmat::dstu4145::scalar257::Scalar;
64use crate::hazmat::dstu4145::signature257;
65use crate::hazmat::kupyna::Kupyna256;
66use crate::hazmat::kupyna_kmac::Kupyna384Kmac;
67use zeroize::Zeroize;
68
69/// A DSTU 4145 `m=257` signature, `r || s` (33 bytes each, 66 total).
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct Signature {
72 r: [u8; 33],
73 s: [u8; 33],
74}
75
76impl Signature {
77 #[must_use]
78 pub fn to_bytes(&self) -> [u8; 66] {
79 let mut out = [0u8; 66];
80 out[..33].copy_from_slice(&self.r);
81 out[33..].copy_from_slice(&self.s);
82 out
83 }
84
85 #[must_use]
86 pub fn from_bytes(bytes: &[u8; 66]) -> Self {
87 let mut r = [0u8; 33];
88 let mut s = [0u8; 33];
89 r.copy_from_slice(&bytes[..33]);
90 s.copy_from_slice(&bytes[33..]);
91 Signature { r, s }
92 }
93}
94
95/// A DSTU 4145 `m=257` private key - see [`crate::crypto_sign::SigningKey`]'s own doc comment for
96/// why signing itself needs no RNG.
97pub struct SigningKey(Scalar);
98
99impl Drop for SigningKey {
100 fn drop(&mut self) {
101 self.0.zeroize();
102 }
103}
104
105/// A DSTU 4145 `m=257` public key `Q = -d*G` (same convention as `crypto_sign::VerifyingKey`).
106#[derive(Clone, Copy)]
107pub struct VerifyingKey(Point);
108
109impl SigningKey {
110 /// Builds a signing key from a big-endian 33-byte scalar. Returns `None` if `d` is zero or
111 /// not less than the curve order `n` - same validation as `crypto_sign::SigningKey::from_bytes`.
112 #[must_use]
113 pub fn from_bytes(d: &[u8; 33]) -> Option<Self> {
114 let n = curve257::order();
115 if d.iter().all(|&b| b == 0) || d >= &n {
116 return None;
117 }
118 Some(SigningKey(Scalar::from_be_bytes(d)))
119 }
120
121 /// Generates a fresh signing key from the OS CSPRNG - same rejection-sampling approach as
122 /// `crypto_sign::SigningKey::generate` (`docs/TASKS.md` T-122), re-derived for this curve's
123 /// own order rather than assumed to carry over: `curve257::order()`'s top byte is `0x00`
124 /// (D-185 - unlike `m=163`'s `0x04`), so masking each 33-byte candidate's top byte down to
125 /// zero and its second byte to its low bit (`n`'s own bit-length is 256, one bit narrower than
126 /// the 33-byte/264-bit draw) keeps the rejection rate near 50%, the same target
127 /// `crypto_sign`'s own masking hits for `m=163`.
128 ///
129 /// # Errors
130 ///
131 /// Returns [`crate::randombytes::RandomError`] if the OS CSPRNG fails while drawing a
132 /// candidate.
133 #[cfg(any(feature = "std", feature = "getrandom"))]
134 pub fn generate() -> Result<Self, crate::randombytes::RandomError> {
135 loop {
136 let mut candidate = [0u8; 33];
137 crate::randombytes::randombytes_buf(&mut candidate)?;
138 candidate[0] = 0;
139 candidate[1] &= 0x01;
140 let scalar = Scalar::from_candidate_bytes(&candidate);
141 candidate.zeroize();
142 if let Some(scalar) = scalar {
143 return Ok(SigningKey(scalar));
144 }
145 }
146 }
147
148 /// Returns `d`'s big-endian 33-byte encoding - see `crypto_sign::SigningKey::to_bytes`'s own
149 /// doc comment for the caller-zeroizes-it convention this matches.
150 #[must_use]
151 pub fn to_bytes(&self) -> [u8; 33] {
152 self.0.to_be_bytes()
153 }
154
155 #[must_use]
156 pub fn verifying_key(&self) -> VerifyingKey {
157 let g = Point::generator();
158 let q = g.scalar_multiply(&self.0.to_be_bytes()).negate();
159 VerifyingKey(q)
160 }
161
162 /// Signs `message` - see `crypto_sign::SigningKey::sign`'s own doc comment.
163 #[must_use]
164 pub fn sign(&self, message: &[u8]) -> Signature {
165 self.sign_digest(&Kupyna256::digest(message))
166 }
167
168 /// Signs an already-computed 32-byte Kupyna-256 digest directly - see
169 /// `crypto_sign::SigningKey::sign_digest`'s own doc comment (T-113's streaming-message note).
170 #[must_use]
171 pub fn sign_digest(&self, digest: &[u8; 32]) -> Signature {
172 let g = Point::generator();
173 let mut counter: u8 = 0;
174 loop {
175 let e = derive_nonce(self.0, digest, counter);
176 if let Some((r, s)) = signature257::sign(digest, self.0, e, g) {
177 return Signature { r, s };
178 }
179 counter = counter.wrapping_add(1);
180 }
181 }
182}
183
184impl VerifyingKey {
185 #[must_use]
186 pub fn to_uncompressed_bytes(&self) -> [u8; 66] {
187 let mut out = [0u8; 66];
188 match self.0 {
189 Point::Affine(x, y) => {
190 out[..33].copy_from_slice(&x.to_be_bytes());
191 out[33..].copy_from_slice(&y.to_be_bytes());
192 }
193 Point::Infinity => {} // never produced by verifying_key() for a valid SigningKey
194 }
195 out
196 }
197
198 #[must_use]
199 pub fn from_uncompressed_bytes(bytes: &[u8; 66]) -> Self {
200 let x = FieldElement::from_be_bytes(&bytes[..33]);
201 let y = FieldElement::from_be_bytes(&bytes[33..]);
202 VerifyingKey(Point::Affine(x, y))
203 }
204
205 /// See `crypto_sign::VerifyingKey::verify`'s own doc comment.
206 #[must_use]
207 pub fn verify(&self, message: &[u8], sig: &Signature) -> bool {
208 self.verify_digest(&Kupyna256::digest(message), sig)
209 }
210
211 /// See `crypto_sign::VerifyingKey::verify_digest`'s own doc comment (T-113's streaming-message
212 /// note). Full public-key validation, including the general small-subgroup rejection
213 /// `hazmat::dstu4145::signature257::verify` itself performs (cofactor 4, not `m=163`'s 2 - see
214 /// that function's own module doc).
215 #[must_use]
216 pub fn verify_digest(&self, digest: &[u8; 32], sig: &Signature) -> bool {
217 let g = Point::generator();
218 signature257::verify(digest, &sig.r, &sig.s, self.0, g)
219 }
220}
221
222/// Deterministic ephemeral-nonce derivation, `m=257` - see the module doc for why
223/// `Kupyna384Kmac` (48-byte key/output) replaces `crypto_sign`'s `Kupyna256Kmac` here. `d`'s
224/// 33-byte big-endian value is left-padded with zeros to the 48-byte key length - an embedding,
225/// not a truncation.
226fn derive_nonce(d: Scalar, hash: &[u8; 32], counter: u8) -> Scalar {
227 let mut key = [0u8; 48];
228 key[15..].copy_from_slice(&d.to_be_bytes());
229 let mut message = [0u8; 33];
230 message[..32].copy_from_slice(hash);
231 message[32] = counter;
232
233 let Ok(mac) = Kupyna384Kmac::mac(&key, &message) else {
234 unreachable!("key is always exactly 48 bytes, Kupyna384Kmac's required length")
235 };
236 key.zeroize();
237 Scalar::reduce_wide_bytes(&mac)
238}