Skip to main content

rune_ring/
core.rs

1//! Ring signing and verification.
2
3use rand_core::{CryptoRng, RngCore};
4use zeroize::Zeroize;
5
6use crate::challenge::{hash_chain, normalize_challenge, sample_challenge, validate_challenge};
7use crate::error::RuneError;
8use crate::keys::{PublicKey, SecretKey};
9use crate::math::{
10    poly_add, poly_infinity_norm, poly_is_well_formed, poly_mul_schoolbook, poly_sub,
11    sample_bounded_poly, zero_poly, Poly, ZeroizingPoly,
12};
13use crate::params::Params;
14
15/// A ring signature produced by [`ring_sign`].
16///
17/// The signature proves that one member of the ring signed the message without
18/// revealing which member. Verification via [`ring_verify`] requires only the
19/// ring's public keys.
20#[derive(Clone, Debug, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct RingSignature {
23    /// Secret response polynomials.
24    pub z_s: Vec<Poly>,
25    /// Error response polynomials.
26    pub z_e: Vec<Poly>,
27    /// Sequential challenge polynomials.
28    pub c: Vec<Poly>,
29}
30
31/// Signs `message` with the secret key at `signer_index` in `ring`.
32///
33/// # Errors
34///
35/// Returns an error if the ring is malformed, the signer index is out of range,
36/// the secret key does not match the signer public key, or rejection sampling
37/// exceeds the configured attempt limit.
38#[allow(clippy::many_single_char_names, clippy::similar_names)]
39pub fn ring_sign(
40    message: &[u8],
41    signer_index: usize,
42    sk: &SecretKey,
43    ring: &[PublicKey],
44    params: &Params,
45    rng: &mut (impl CryptoRng + RngCore),
46) -> Result<RingSignature, RuneError> {
47    let k = ring.len();
48    validate_ring(ring, params)?;
49
50    if signer_index >= k {
51        return Err(RuneError::SignerIndexOutOfRange);
52    }
53    if sk.t() != &ring[signer_index].t {
54        return Err(RuneError::MalformedPublicKey);
55    }
56
57    let a = &ring[0].a;
58    let response_bound = params.response_bound();
59
60    // Expected iterations to acceptance: ~1 for RUNE_256, ~4 for RUNE_128.
61    for _ in 0..params.max_attempts() {
62        let y_s = ZeroizingPoly(sample_bounded_poly(params.gamma(), params, rng));
63        let y_e = ZeroizingPoly(sample_bounded_poly(params.gamma(), params, rng));
64
65        let mut z_s = vec![zero_poly(params); k];
66        let mut z_e = vec![zero_poly(params); k];
67        let mut c = vec![zero_poly(params); k];
68        let mut w = vec![zero_poly(params); k];
69
70        w[signer_index] = poly_add(&poly_mul_schoolbook(a, &y_s.0, params), &y_e.0, params);
71
72        // The chain starts after the signer and closes immediately before it,
73        // so c_pi is derived from the last simulated commitment.
74        for j in 1..k {
75            let i = (signer_index + j) % k;
76            let prev = (i + k - 1) % k;
77            let seed = hash_chain(message, ring, i, &w[prev], params);
78            c[i] = sample_challenge(&seed, params);
79            z_s[i] = sample_bounded_poly(response_bound - 1, params, rng);
80            z_e[i] = sample_bounded_poly(response_bound - 1, params, rng);
81
82            let az = poly_mul_schoolbook(a, &z_s[i], params);
83            let az_plus_e = poly_add(&az, &z_e[i], params);
84            let ct = poly_mul_schoolbook(&c[i], &ring[i].t, params);
85            w[i] = poly_sub(&az_plus_e, &ct, params);
86        }
87
88        let prev = (signer_index + k - 1) % k;
89        let seed = hash_chain(message, ring, signer_index, &w[prev], params);
90        c[signer_index] = sample_challenge(&seed, params);
91
92        let cs = ZeroizingPoly(poly_mul_schoolbook(&c[signer_index], sk.s(), params));
93        let ce = ZeroizingPoly(poly_mul_schoolbook(&c[signer_index], sk.e(), params));
94        z_s[signer_index] = poly_add(&y_s.0, &cs.0, params);
95        z_e[signer_index] = poly_add(&y_e.0, &ce.0, params);
96
97        if poly_infinity_norm(&z_s[signer_index]) >= response_bound
98            || poly_infinity_norm(&z_e[signer_index]) >= response_bound
99        {
100            z_s[signer_index].zeroize();
101            z_e[signer_index].zeroize();
102            continue;
103        }
104
105        // y_s and y_e are zeroized here via ZeroizingPoly::drop.
106        return Ok(RingSignature { z_s, z_e, c });
107    }
108
109    Err(RuneError::RejectionSamplingFailed)
110}
111
112/// Verifies a Rune ring signature.
113///
114/// # Errors
115///
116/// Returns an error if the ring or signature structure is malformed. Failed
117/// cryptographic checks return `Ok(false)`.
118pub fn ring_verify(
119    message: &[u8],
120    sig: &RingSignature,
121    ring: &[PublicKey],
122    params: &Params,
123) -> Result<bool, RuneError> {
124    let k = ring.len();
125    validate_ring(ring, params)?;
126
127    if sig.z_s.len() != k || sig.z_e.len() != k || sig.c.len() != k {
128        return Err(RuneError::MalformedSignature);
129    }
130
131    let response_bound = params.response_bound();
132    for i in 0..k {
133        if !poly_is_well_formed(&sig.z_s[i], params)
134            || !poly_is_well_formed(&sig.z_e[i], params)
135            || !poly_is_well_formed(&sig.c[i], params)
136        {
137            return Err(RuneError::MalformedSignature);
138        }
139        if !validate_challenge(&sig.c[i], params) {
140            return Ok(false);
141        }
142        if normalize_challenge(&sig.c[i], params) != sig.c[i] {
143            return Ok(false);
144        }
145        if poly_infinity_norm(&sig.z_s[i]) >= response_bound
146            || poly_infinity_norm(&sig.z_e[i]) >= response_bound
147        {
148            return Ok(false);
149        }
150    }
151
152    let a = &ring[0].a;
153    let mut commitments = vec![zero_poly(params); k];
154    for (i, pk) in ring.iter().enumerate() {
155        let az = poly_mul_schoolbook(a, &sig.z_s[i], params);
156        let az_plus_e = poly_add(&az, &sig.z_e[i], params);
157        let ct = poly_mul_schoolbook(&sig.c[i], &pk.t, params);
158        commitments[i] = poly_sub(&az_plus_e, &ct, params);
159    }
160
161    for i in 0..k {
162        let prev = (i + k - 1) % k;
163        let seed = hash_chain(message, ring, i, &commitments[prev], params);
164        let expected = sample_challenge(&seed, params);
165        if !validate_challenge(&expected, params) {
166            return Err(RuneError::MalformedChallenge);
167        }
168        if expected != sig.c[i] {
169            return Ok(false);
170        }
171    }
172
173    Ok(true)
174}
175
176fn validate_ring(ring: &[PublicKey], params: &Params) -> Result<(), RuneError> {
177    if ring.len() < 2 {
178        return Err(RuneError::RingTooSmall);
179    }
180
181    let a = &ring[0].a;
182    if !poly_is_well_formed(a, params) {
183        return Err(RuneError::MalformedPublicKey);
184    }
185
186    for pk in ring {
187        if pk.a != *a {
188            return Err(RuneError::InconsistentRingParameter);
189        }
190        if !poly_is_well_formed(&pk.a, params) || !poly_is_well_formed(&pk.t, params) {
191            return Err(RuneError::MalformedPublicKey);
192        }
193    }
194
195    Ok(())
196}