Skip to main content

aethel_core/
identity_error.rs

1//! Rust-side mirror of the `identity-error` variant defined in the
2//! `aethel:core` WIT world ([`wit/aethel-core.wit`](../../wit/aethel-core.wit)).
3//!
4//! Kept as one flat, closed set — not a hierarchy — matching the WIT design.
5//!
6//! # Which variants a caller can actually observe
7//!
8//! Five of the eight are reachable through the component today, and are driven
9//! end-to-end by tests in `tests/component_execution.rs`:
10//! `InvalidInputLength`, `SerializationError`, `ThresholdNotMet`,
11//! `RejectionSamplingFailed`, and `InvalidShareSet`.
12//!
13//! Three are **reserved and currently unreachable**: [`IdentityError::NormBoundViolation`],
14//! [`IdentityError::ChallengeMismatch`] and [`IdentityError::InvalidAttributeCommitment`]. They
15//! are named here deliberately rather than removed — see their individual doc
16//! comments and `component_error_variant_reachability` in
17//! `tests/component_execution.rs`, which pins the split so it cannot drift
18//! silently.
19//!
20//! `RejectionSamplingFailed` is reachable but not forceable from outside: it
21//! needs all 16 rejection-sampling iterations to fail, which is negligible for
22//! honest parameters. It is exercised natively instead.
23
24use crate::saap::SaapValidationError;
25use crate::sampling::RejectionError;
26
27/// Closed set of failure reasons across all `aethel:core` WIT operations.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum IdentityError {
30    /// A byte-slice input was too short or malformed for the operation.
31    InvalidInputLength,
32    /// Serialization or deserialization of a proof/projection failed.
33    SerializationError,
34    /// Rejection sampling exhausted its fixed iteration ceiling.
35    RejectionSamplingFailed,
36    /// A response vector's infinity norm exceeded the rejection bound.
37    ///
38    /// **Reserved, no producer today.** Reachable only through the superseded
39    /// `saap` verifier, which is crate-private and not exported by the
40    /// component. `saap-verify-presentation` reports a well-formed proof that
41    /// does not verify as `ok(false)`, never as an error, because "this does
42    /// not verify" is a verdict and not a failure to reach one.
43    ///
44    /// Kept for the predicate relation (RFC §5.6 relation 3), which is
45    /// deliberately deferred and will need to distinguish a range proof whose
46    /// response is out of bounds from one that simply does not hold.
47    NormBoundViolation,
48    /// A recomputed Fiat-Shamir challenge did not match the proof's challenge.
49    ///
50    /// **Reserved, no producer today.** Same reasoning as
51    /// [`Self::NormBoundViolation`].
52    ChallengeMismatch,
53    /// A disclosed attribute did not match its vector commitment.
54    ///
55    /// **Reserved, no producer today.** Same reasoning as
56    /// [`Self::NormBoundViolation`].
57    InvalidAttributeCommitment,
58    /// Fewer than the threshold number of shares were supplied for reconstruction.
59    ThresholdNotMet,
60    /// The supplied shares are not a valid, authenticated share set. Three
61    /// causes, all folded into one variant because they share a consequence —
62    /// none of them may reconstruct:
63    ///
64    /// - an evaluation index appears more than once
65    /// - more shares were supplied than the scheme issues
66    /// - a share's Merkle proof does not check out against the caller-supplied
67    ///   root (0X3-105)
68    ///
69    /// Distinct from [`Self::SerializationError`] on purpose. "This share is
70    /// malformed" and "you sent the same share twice" (or "this share was
71    /// never part of the sharing it claims to be") are different failures
72    /// with different fixes, and collapsing them is the same sentinel-flattening
73    /// this crate treats as a defect class elsewhere. Lagrange interpolation
74    /// over a repeated evaluation point is undefined, and interpolation over a
75    /// share nobody ever split is defined but meaningless; a share set
76    /// carrying either does not reconstruct to the shared secret, so it must
77    /// not reconstruct at all.
78    InvalidShareSet,
79
80    // ── A-4: `aethel-plp-1` wire envelope (native-only, no WIT producer) ────
81    //
82    // These four are new in 0.6.0 and reachable only through `wire::{decode_projection,
83    // decode_proof, verify_projection}` and the raw struct codecs
84    // (`EphemeralProjection::from_bytes` / `ZkIdentityProof::from_bytes`) they build
85    // on. None of them has a WIT producer: `wit/aethel-core.wit`'s `identity-error`
86    // variant is unchanged (adding a case there would be a breaking ordinal shift
87    // for every existing importer), so `component.rs`'s `From<IdentityError> for
88    // WitError` collapses all four to `WitError::SerializationError` at the
89    // component boundary — the same fold the RESERVED variants above already use,
90    // just in the other direction (many native causes, one WIT effect, rather than
91    // one native cause with several WIT-side reservations).
92    //
93    // Kept distinct on the native side because "wrong magic", "wrong version",
94    // "declared length disagrees with what's actually there", and "a coefficient
95    // isn't a member of R_q" are different failures with different fixes for a
96    // native caller debugging a bad wire payload, even though a WASM component
97    // caller sees one undifferentiated `serialization-error`.
98    /// An `aethel-plp-1` envelope's magic bytes did not match
99    /// [`crate::EIAB_MAGIC`].
100    WireBadMagic,
101    /// An `aethel-plp-1` envelope declared a version byte this build does not
102    /// recognize. See `wire::WIRE_VERSION_PLP1`.
103    WireBadVersion,
104    /// An `aethel-plp-1` envelope was too short to contain its header, or its
105    /// declared body length did not match the number of bytes actually
106    /// present after the header.
107    WireLengthMismatch,
108    /// A decoded polynomial coefficient was `>= Q`, i.e. not a member of
109    /// `R_q = Z_q[X]/(X^N + 1)`. Raised by [`crate::plp::EphemeralProjection::from_bytes`]
110    /// and [`crate::plp::ZkIdentityProof::from_bytes`] before any arithmetic
111    /// ever touches the value — `add_mod`/`sub_mod` assume reduced inputs, so
112    /// admitting an out-of-range coefficient would be a robustness defect
113    /// even though the Fiat-Shamir challenge recomputation means it is not,
114    /// by itself, a soundness break.
115    CoefficientOutOfRange,
116}
117
118impl From<SaapValidationError> for IdentityError {
119    fn from(e: SaapValidationError) -> Self {
120        match e {
121            SaapValidationError::NormBoundViolation => IdentityError::NormBoundViolation,
122            SaapValidationError::ChallengeMismatch => IdentityError::ChallengeMismatch,
123            SaapValidationError::InvalidAttributeCommitment => {
124                IdentityError::InvalidAttributeCommitment
125            }
126        }
127    }
128}
129
130impl From<RejectionError> for IdentityError {
131    fn from(e: RejectionError) -> Self {
132        match e {
133            RejectionError::AllIterationsRejected => IdentityError::RejectionSamplingFailed,
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::htss::SecretSharer;
142    use crate::plp::{checked_project_at_context, EphemeralProjection};
143
144    #[test]
145    fn every_saap_validation_error_maps() {
146        assert_eq!(
147            IdentityError::from(SaapValidationError::NormBoundViolation),
148            IdentityError::NormBoundViolation
149        );
150        assert_eq!(
151            IdentityError::from(SaapValidationError::ChallengeMismatch),
152            IdentityError::ChallengeMismatch
153        );
154        assert_eq!(
155            IdentityError::from(SaapValidationError::InvalidAttributeCommitment),
156            IdentityError::InvalidAttributeCommitment
157        );
158    }
159
160    #[test]
161    fn rejection_sampling_failure_maps() {
162        assert_eq!(
163            IdentityError::from(RejectionError::AllIterationsRejected),
164            IdentityError::RejectionSamplingFailed
165        );
166    }
167
168    // ── InvalidInputLength: driven through checked_project_at_context ──────────
169
170    // Fresh per-projection randomness for the error term e_τ. In tests a fixed
171    // 32-byte value is fine; in production this MUST be freshly sampled.
172    const RHO: [u8; 32] = [0x5au8; 32];
173
174    #[test]
175    fn invalid_input_length_from_a_short_secret() {
176        // One byte short of the required 32-byte seed (randomness is valid, so
177        // the failure is unambiguously about the secret length).
178        let short_secret = [0u8; 31];
179        let result = checked_project_at_context(&short_secret, b"context", &RHO);
180        assert_eq!(result.err(), Some(IdentityError::InvalidInputLength));
181    }
182
183    #[test]
184    fn invalid_input_length_from_short_randomness() {
185        // A valid secret but under-length randomness must also be rejected:
186        // silently proceeding would seed e_τ from too little entropy.
187        let secret = [0x42u8; 32];
188        let short_rho = [0u8; 31];
189        let result = checked_project_at_context(&secret, b"context", &short_rho);
190        assert_eq!(result.err(), Some(IdentityError::InvalidInputLength));
191    }
192
193    #[test]
194    fn checked_project_at_context_succeeds_with_a_valid_secret() {
195        // The validation isn't just rejecting everything — a correctly-sized
196        // secret and randomness must actually produce a projection.
197        let secret = [0x42u8; 32];
198        assert!(checked_project_at_context(&secret, b"context", &RHO).is_ok());
199    }
200
201    // ── SerializationError: driven through EphemeralProjection::from_bytes ─────
202
203    #[test]
204    fn serialization_error_from_truncated_projection_bytes() {
205        let truncated = [0u8; 10];
206        let result = EphemeralProjection::from_bytes(&truncated);
207        assert_eq!(result.err(), Some(IdentityError::SerializationError));
208    }
209
210    #[test]
211    fn ephemeral_projection_round_trips_through_bytes() {
212        let secret = [0x99u8; 32];
213        let projection = checked_project_at_context(&secret, b"round-trip", &RHO).unwrap();
214        let bytes = projection.to_bytes();
215        let decoded =
216            EphemeralProjection::from_bytes(&bytes).expect("well-formed bytes must decode");
217        assert_eq!(decoded.tau, projection.tau);
218        // Compare the whole rank-k matrix and vector. Checking one cell would
219        // let a codec that dropped the other components round-trip cleanly.
220        let flat_a = |p: &EphemeralProjection| {
221            p.matrix_a
222                .iter()
223                .flat_map(|row| row.iter().flat_map(|q| q.coeffs().to_vec()))
224                .collect::<alloc::vec::Vec<u32>>()
225        };
226        let flat_b = |p: &EphemeralProjection| {
227            p.public_b
228                .iter()
229                .flat_map(|q| q.coeffs().to_vec())
230                .collect::<alloc::vec::Vec<u32>>()
231        };
232        assert_eq!(flat_a(&decoded), flat_a(&projection));
233        assert_eq!(flat_b(&decoded), flat_b(&projection));
234    }
235
236    // ── ThresholdNotMet: driven through SecretSharer::reconstruct_secret_checked ─
237
238    #[test]
239    #[allow(deprecated)] // shares from the deprecated split_secret, on purpose
240    fn threshold_not_met_with_fewer_than_three_shares() {
241        let shares = SecretSharer::split_secret(12_345u64, 3, 5, 0xdead_beef);
242        let result = SecretSharer::reconstruct_secret_checked(&shares[0..2]);
243        assert_eq!(result.err(), Some(IdentityError::ThresholdNotMet));
244    }
245
246    #[test]
247    #[allow(deprecated)] // shares from the deprecated split_secret, on purpose
248    fn reconstruct_secret_checked_succeeds_at_the_threshold() {
249        let secret = 12_345u64;
250        let shares = SecretSharer::split_secret(secret, 3, 5, 0xdead_beef);
251        let result = SecretSharer::reconstruct_secret_checked(&shares[0..3]);
252        assert_eq!(result, Ok(secret));
253    }
254}