dig_message/error.rs
1//! The dig-message error taxonomy — stable, catalogued variants a scripted client can key off (§6.2).
2//! WU1 owns the encoding/framing/compression failures; the seal/signature/replay variants arrive with
3//! WU2/WU4.
4
5use thiserror::Error;
6
7/// Every fail-cleanly outcome of encoding, framing, or (de)compressing a dig-message (SPEC §1, §1.1,
8/// §2). A receiver MUST reject — never panic — on any of these.
9#[derive(Debug, Error, PartialEq, Eq)]
10pub enum MessageError {
11 /// The on-wire frame exceeds [`crate::MAX_ENVELOPE_BYTES`] (SPEC §1).
12 #[error("envelope is {size} bytes, exceeds the {max}-byte cap")]
13 EnvelopeTooLarge { size: usize, max: usize },
14
15 /// The frame ended before a complete envelope could be decoded (SPEC §1).
16 #[error("truncated envelope: {0}")]
17 Truncated(String),
18
19 /// The envelope declares a version this reader does not support (SPEC §2 field 1).
20 #[error("unsupported envelope version {0}")]
21 UnsupportedVersion(u8),
22
23 /// The compression algorithm id is not recognized (SPEC §1.1). Never mis-decode with a wrong codec.
24 #[error("unsupported compression id {0}")]
25 UnsupportedCompression(u8),
26
27 /// The declared uncompressed length exceeds the bomb-guard cap (SPEC §1.1).
28 #[error("declared uncompressed length {declared} exceeds the {max}-byte cap")]
29 DecompressionBomb { declared: usize, max: usize },
30
31 /// The decoded output length did not match the declared `uncompressed_len` (SPEC §1.1).
32 #[error("decompressed length mismatch: expected {expected}, got {actual}")]
33 DecompressedLengthMismatch { expected: usize, actual: usize },
34
35 /// A payload's uncompressed length does not fit the `u32` wire field (SPEC §5.2).
36 #[error("payload is {0} bytes, exceeds the u32 uncompressed_len field")]
37 PayloadTooLarge(usize),
38
39 /// A Streamable (de)serialization or codec-level failure with the underlying detail (SPEC §1).
40 #[error("codec error: {0}")]
41 Codec(String),
42
43 /// The `message_type` is not registered (SPEC §4). Returned when the dispatch shape is a request or
44 /// stream frame so the caller can reply UNSUPPORTED_TYPE; an unknown one-shot/response is instead
45 /// silently dropped (never surfaced as an error — the forward-compat property).
46 #[error("unsupported message type {0:#010x}")]
47 UnsupportedType(u32),
48
49 /// A [`crate::MessageType`] was registered twice (SPEC §4 additive-only: an id, once assigned, is
50 /// never renumbered or repurposed). A duplicate registration is a caller bug, reported — never
51 /// silently overwriting the existing handler.
52 #[error("message type {0:#010x} is already registered")]
53 DuplicateType(u32),
54
55 // --- WU2: the seal / signature / replay / expiry pipeline (SPEC §5) ---
56 /// A received G1 point (the `kem_enc` encapsulation or the resolved sender key) failed the
57 /// mandatory prime-order subgroup / non-identity check BEFORE any DH (SPEC §5.1). Fail-closed:
58 /// blocks small-subgroup / invalid-curve key-recovery attacks.
59 #[error("G1 subgroup / non-identity check failed on the seal key material")]
60 InvalidPoint,
61
62 /// The sender DID could not be resolved to a BLS G1 identity key at the claimed epoch (SPEC §5.2).
63 /// An unknown/unresolvable sender means the seal cannot be authenticated — fail-closed.
64 #[error("sender DID could not be resolved to an identity key")]
65 UnresolvableSender,
66
67 /// Sealing failed to produce ciphertext (an AEAD encrypt or ephemeral-key error, SPEC §5.1).
68 #[error("seal failed: {0}")]
69 SealFailed(String),
70
71 /// The AEAD open failed — a wrong recipient key, wrong sender key, tampered ciphertext, or
72 /// tampered AAD header all land here (SPEC §5.1/§5.2). Fail-closed, no plaintext is revealed.
73 #[error("seal open failed (wrong key or tampered ciphertext/header)")]
74 OpenFailed,
75
76 /// The mandatory BLS G2 sender signature was absent, malformed, or did not verify against the
77 /// resolved sender key over `SIG_DOMAIN || transcript` (SPEC §5.1). Fail-closed REJECT.
78 #[error("BLS sender signature verification failed")]
79 BadSignature,
80
81 /// The sealed inner `message_type`/`correlation_id` did not equal the cleartext header — an
82 /// anti type-confusion / anti-splice REJECT (SPEC §5.2 step b).
83 #[error("sealed inner header does not match the cleartext header (type-confusion / splice)")]
84 HeaderMismatch,
85
86 /// The message is a replay or is stale: a duplicate counter, a counter below the sliding window,
87 /// or a `timestamp_ms` outside the freshness window (SPEC §5.6). Fail-closed DROP.
88 #[error("anti-replay check failed (duplicate, stale, or outside the freshness window)")]
89 Replay,
90
91 /// The message is past its sender-controlled `expires_at` TTL and is DISCARDED with no side
92 /// effect (SPEC §5.6b).
93 #[error("message expired (now > expires_at)")]
94 Expired,
95
96 /// `expires_at` exceeds `timestamp_ms + MAX_MESSAGE_TTL_MS` — a near-infinite validity claim,
97 /// REJECTED (never clamped, since clamping would alter signed content) (SPEC §5.6b).
98 #[error("expires_at exceeds the maximum message TTL")]
99 TtlTooLong,
100
101 // --- WU4: the streaming state machine (SPEC §3) ---
102 /// A new stream OPEN would exceed [`crate::MAX_CONCURRENT_STREAMS`] concurrently-open streams for
103 /// the peer (SPEC §3 gate item). Fail-closed DoS guard: the peer RESETs rather than opening more.
104 #[error("too many concurrent streams (cap {cap})")]
105 StreamLimit { cap: usize },
106
107 /// A stream frame is invalid for the current state machine position: an out-of-order/gap/replayed
108 /// `seq`, a frame for an unknown stream, a DATA beyond the granted credit window, a frame after a
109 /// half-close, or an out-of-sequence handshake frame (SPEC §3). Fail-closed REJECT → RESET.
110 #[error("stream protocol violation: {0}")]
111 StreamProtocol(&'static str),
112}
113
114/// The crate result alias.
115pub type Result<T> = std::result::Result<T, MessageError>;