Skip to main content

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}