derec_library/primitives/verification/request.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
4use crate::transport::TransportProtocolExt as _;
5use crate::{
6 derec_message::{DeRecMessageBuilder, current_timestamp, extract_inner_message},
7 types::{ChannelId, SharedKey},
8 utils::verify_timestamps,
9};
10use derec_proto::{DeRecMessage, MessageBody, VerifyShareRequestMessage};
11use prost::Message;
12use rand::{Rng, rng};
13
14pub struct ProduceResult {
15 /// Serialized outer [`derec_proto::DeRecMessage`] envelope with the encrypted request payload.
16 pub envelope: Vec<u8>,
17 /// Fresh `u64` nonce that was embedded into the encrypted
18 /// [`derec_proto::VerifyShareRequestMessage`].
19 ///
20 /// This nonce is the **owner-controlled binding token** for the
21 /// challenge: [`super::response::process`] uses it (together with
22 /// `secret_id` and `version`) to confirm the helper's response
23 /// answers *this specific* outstanding request, preventing replay
24 /// of a captured-and-stale response and rejecting responses whose
25 /// helper-supplied `nonce` does not match anything the owner has
26 /// outstanding.
27 ///
28 /// # Retention
29 ///
30 /// Who keeps this value depends on which layer is driving the flow:
31 ///
32 /// - **Orchestrator users** (`DeRecProtocol::start(VerifyShares)`):
33 /// the orchestrator stores the full request — nonce included —
34 /// in
35 /// [`crate::protocol::PendingVerification`](crate::protocol)
36 /// automatically. Application code never touches it.
37 /// - **Primitive-direct callers** (driving `request::produce` and
38 /// `response::process` without the orchestrator, e.g. SDK parity
39 /// tests): MUST retain this `nonce` (typically in a per-
40 /// `channel_id` map) and pass the original
41 /// [`derec_proto::VerifyShareRequestMessage`] back to
42 /// [`super::response::process`] when the matching response arrives.
43 ///
44 /// In both layers the cryptographic contract is identical; the
45 /// retention machinery is what differs.
46 pub nonce: u64,
47}
48
49pub struct ExtractResult {
50 pub request: VerifyShareRequestMessage,
51}
52
53/// Produces a verification request envelope to initiate the DeRec *verification* flow.
54///
55/// In DeRec, verification allows an Owner to challenge a Helper to prove it still holds
56/// the expected share bytes. The Owner sends an encrypted
57/// [`derec_proto::VerifyShareRequestMessage`] containing:
58///
59/// - a `version` identifying the share-distribution version being verified
60/// - a `secret_id` binding the request to a specific secret
61/// - a fresh `nonce` used to bind the later proof to this specific request
62///
63/// The request is serialized, encrypted with the already established channel shared key,
64/// and wrapped in an outer plain [`derec_proto::DeRecMessage`] envelope.
65///
66/// The Helper is expected to compute:
67///
68/// `SHA384(share_content || nonce_be)`
69///
70/// and return that digest in a [`derec_proto::VerifyShareResponseMessage`].
71///
72/// # Arguments
73///
74/// * `channel_id` - Channel identifier for the previously paired Helper.
75/// * `secret_id` - Identifier of the secret whose share is being verified. Embedded into
76/// the request.
77/// * `version` - Distribution version to embed in the request. The responder is expected to
78/// echo this value in the response.
79/// * `shared_key` - Previously established 32-byte symmetric channel key used to encrypt the
80/// inner verification request.
81///
82/// # Returns
83///
84/// On success returns [`ProduceResult`] containing:
85///
86/// - `envelope`: serialized outer [`derec_proto::DeRecMessage`] bytes carrying an encrypted
87/// inner [`derec_proto::VerifyShareRequestMessage`]
88///
89/// # Errors
90///
91/// Returns [`crate::Error`] if outer envelope construction or symmetric encryption fails.
92///
93/// # Security Notes
94///
95/// - The nonce is generated using a cryptographically secure RNG and must be unpredictable
96/// to prevent replay of previously captured responses.
97/// - Only the inner protobuf message is encrypted; the outer envelope is plain.
98/// - The outer envelope timestamp equals the inner request timestamp, preserving the
99/// invariant `envelope.timestamp == request.timestamp`.
100///
101/// # Example
102///
103/// ```
104/// use derec_library::primitives::verification::request;
105/// use derec_library::types::ChannelId;
106///
107/// let channel_id = ChannelId(42);
108/// let shared_key = [7u8; 32];
109///
110/// let result = request::produce(
111/// channel_id,
112/// 1,
113/// 7,
114/// &shared_key,
115/// None,
116/// )
117/// .expect("failed to build verification request");
118///
119/// assert!(!result.envelope.is_empty());
120/// ```
121#[cfg_attr(
122 feature = "logging",
123 tracing::instrument(skip_all, fields(channel_id = channel_id.0, version = version))
124)]
125pub fn produce(
126 channel_id: ChannelId,
127 secret_id: u64,
128 version: u32,
129 shared_key: &SharedKey,
130 reply_to: Option<derec_proto::TransportProtocol>,
131) -> Result<ProduceResult, crate::Error> {
132 let mut rng = rng();
133
134 let timestamp = current_timestamp();
135 let nonce = rng.next_u64();
136
137 let message = VerifyShareRequestMessage {
138 secret_id,
139 version,
140 nonce,
141 timestamp: Some(timestamp),
142 reply_to,
143 };
144
145 let envelope = DeRecMessageBuilder::channel()
146 .channel_id(channel_id)
147 .timestamp(timestamp)
148 .message_body(MessageBody::VerifyShareRequest(message))
149 .encrypt(shared_key)?
150 .build()?
151 .encode_to_vec();
152
153 #[cfg(feature = "logging")]
154 tracing::info!("verification request envelope produced");
155
156 Ok(ProduceResult { envelope, nonce })
157}
158
159/// Decrypts and decodes a [`derec_proto::VerifyShareRequestMessage`] from an outer
160/// [`derec_proto::DeRecMessage`] envelope.
161///
162/// This function:
163///
164/// 1. Decodes the outer [`derec_proto::DeRecMessage`] envelope from `envelope_bytes`
165/// 2. Decrypts and decodes the inner [`derec_proto::VerifyShareRequestMessage`] using
166/// `shared_key`
167/// 3. Validates the invariant `envelope.timestamp == request.timestamp`
168///
169/// # Arguments
170///
171/// * `envelope_bytes` - Serialized outer [`derec_proto::DeRecMessage`] bytes carrying an
172/// encrypted inner [`derec_proto::VerifyShareRequestMessage`], as produced by [`produce`].
173/// * `shared_key` - Previously established 32-byte symmetric channel key used to decrypt
174/// the inner message.
175///
176/// # Returns
177///
178/// On success returns [`ExtractResult`] containing:
179///
180/// - `request`: the decrypted inner [`derec_proto::VerifyShareRequestMessage`]
181///
182/// # Errors
183///
184/// Returns [`crate::Error`] if:
185///
186/// - `envelope_bytes` cannot be decoded as a valid [`derec_proto::DeRecMessage`]
187/// - decryption or inner-message decoding fails
188/// - `envelope.timestamp != request.timestamp`
189/// - the inner message is not a [`derec_proto::VerifyShareRequestMessage`]
190///
191/// # Security: no freshness or replay protection
192///
193/// The timestamp check enforced here only binds the envelope to the
194/// inner body (`envelope.timestamp == body.timestamp`). It does NOT
195/// enforce a freshness window against the receiver's clock and does
196/// NOT detect replays of a previously-captured ciphertext. Because
197/// the channel key is long-lived, a recorded envelope stays
198/// decryptable indefinitely. Callers MUST add a freshness window
199/// and per-channel anti-replay (monotonic counter or nonce log) on
200/// top before driving any side-effecting state off the parsed body.
201///
202/// # Example
203///
204/// ```
205/// use derec_library::primitives::verification::request;
206/// use derec_library::types::ChannelId;
207///
208/// let channel_id = ChannelId(42);
209/// let shared_key = [7u8; 32];
210///
211/// let request::ProduceResult { envelope, .. } = request::produce(channel_id, 1, 7, &shared_key, None)
212/// .expect("failed to build verification request");
213///
214/// let request::ExtractResult { request } = request::extract(&envelope, &shared_key)
215/// .expect("failed to extract verification request");
216///
217/// assert_eq!(request.secret_id, 1);
218/// assert_eq!(request.version, 7);
219/// ```
220#[cfg_attr(
221 feature = "logging",
222 tracing::instrument(skip_all, fields(envelope_len = envelope_bytes.len()))
223)]
224pub fn extract(
225 envelope_bytes: &[u8],
226 shared_key: &SharedKey,
227) -> Result<ExtractResult, crate::Error> {
228 let envelope = DeRecMessage::decode(envelope_bytes).map_err(crate::Error::ProtobufDecode)?;
229
230 let request = match extract_inner_message(&envelope.message, shared_key)? {
231 MessageBody::VerifyShareRequest(message) => message,
232 _ => {
233 #[cfg(feature = "logging")]
234 tracing::warn!("unexpected message type; expected VerifyShareRequestMessage");
235
236 return Err(crate::Error::Invariant(
237 "Invalid message. Expected: VerifyShareRequestMessage",
238 ));
239 }
240 };
241
242 verify_timestamps(envelope.timestamp, request.timestamp)?;
243
244 if let Some(reply_to) = request.reply_to.as_ref() {
245 reply_to.validate()?;
246 }
247
248 #[cfg(feature = "logging")]
249 tracing::info!("verification request extracted and validated");
250
251 Ok(ExtractResult { request })
252}