Skip to main content

libfcp_core/
envelope.rs

1// Copyright Nixort <https://github.com/Nixort> 2026.
2//
3// License: GNU General Public License v3.0 only.
4// You can find the license file in the project root.
5//
6// Federated CFR Connect Protocol (FCP).
7
8//! Canonical FCP envelopes, mandatory dual signatures and bounded wire parsing.
9
10use alloc::vec::Vec;
11use core::convert::TryInto;
12
13use crate::{
14    AttemptId, CloseCode, EndpointIdentity, EndpointSigner, EnvelopeId, Error, FederationId,
15    WebRtcBinding, FCP_WIRE_MARKER, FCP_WIRE_VERSION, MAX_CANDIDATE_BYTES, MAX_CFR_CONTROL_BYTES,
16    MAX_DESCRIPTION_BYTES, MAX_ENVELOPE_BYTES, ML_DSA_65_SIGNATURE_BYTES,
17};
18
19/// FCP envelope kind.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[repr(u8)]
22pub enum Kind {
23    /// Initial signed WebRTC offer.
24    Offer = 1,
25    /// Signed answer bound to one offer ID.
26    Answer = 2,
27    /// Signed opaque ICE candidate bound to one negotiation parent.
28    Candidate = 3,
29    /// Signed close signal.
30    Close = 4,
31    /// Opaque CFR control bytes over an established reliable data channel.
32    CfrControl = 5,
33}
34
35impl TryFrom<u8> for Kind {
36    type Error = Error;
37
38    fn try_from(value: u8) -> Result<Self, Self::Error> {
39        match value {
40            1 => Ok(Self::Offer),
41            2 => Ok(Self::Answer),
42            3 => Ok(Self::Candidate),
43            4 => Ok(Self::Close),
44            5 => Ok(Self::CfrControl),
45            _ => Err(Error::UnknownKind),
46        }
47    }
48}
49
50/// The kind-specific canonical FCP body.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum Body {
53    /// Opaque engine offer description and a binding digest.
54    Offer {
55        /// Digest of exact description and DTLS fingerprint bytes.
56        binding: WebRtcBinding,
57        /// Bounded opaque engine description.
58        description: Vec<u8>,
59    },
60    /// Opaque engine answer bound to one offer.
61    Answer {
62        /// Complete-envelope digest of the corresponding offer.
63        offer_id: EnvelopeId,
64        /// Digest of exact description and DTLS fingerprint bytes.
65        binding: WebRtcBinding,
66        /// Bounded opaque engine description.
67        description: Vec<u8>,
68    },
69    /// An opaque engine ICE candidate.
70    Candidate {
71        /// Complete-envelope digest of the offer or answer it belongs to.
72        parent_id: EnvelopeId,
73        /// Sender-local diagnostic sequence number.
74        sequence: u32,
75        /// Bounded opaque candidate bytes.
76        candidate: Vec<u8>,
77    },
78    /// A caller-defined close reason.
79    Close {
80        /// Stable application close code.
81        reason: CloseCode,
82    },
83    /// Opaque raw CFR payload bytes.
84    CfrControl {
85        /// Exact bytes passed to `cfr_protocol::Conference::handle`.
86        payload: Vec<u8>,
87    },
88}
89
90impl Body {
91    pub(crate) fn kind(&self) -> Kind {
92        match self {
93            Self::Offer { .. } => Kind::Offer,
94            Self::Answer { .. } => Kind::Answer,
95            Self::Candidate { .. } => Kind::Candidate,
96            Self::Close { .. } => Kind::Close,
97            Self::CfrControl { .. } => Kind::CfrControl,
98        }
99    }
100
101    fn validate(&self) -> Result<(), Error> {
102        match self {
103            Self::Offer { description, .. } | Self::Answer { description, .. } => {
104                validate_len(description.len(), MAX_DESCRIPTION_BYTES)
105            }
106            Self::Candidate { candidate, .. } => validate_len(candidate.len(), MAX_CANDIDATE_BYTES),
107            Self::Close { .. } => Ok(()),
108            Self::CfrControl { payload } => validate_len(payload.len(), MAX_CFR_CONTROL_BYTES),
109        }
110    }
111}
112
113/// A verified or to-be-signed FCP envelope.
114///
115/// Both signatures authenticate the exact same canonical bytes, including the
116/// complete sender and recipient identities. No single-signature record is a
117/// valid FCP envelope.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct Envelope {
120    /// Federation routing namespace.
121    pub federation: FederationId,
122    /// Connection attempt identifier.
123    pub attempt: AttemptId,
124    /// Complete identity of the envelope author.
125    pub sender: EndpointIdentity,
126    /// Complete identity of the intended endpoint.
127    pub recipient: EndpointIdentity,
128    /// Kind-specific body.
129    pub body: Body,
130    classical_signature: [u8; 64],
131    post_quantum_signature: [u8; ML_DSA_65_SIGNATURE_BYTES],
132}
133
134impl Envelope {
135    /// Creates and signs a canonical FCP envelope with both required algorithms.
136    pub fn sign<S: EndpointSigner>(
137        signer: &S,
138        federation: FederationId,
139        attempt: AttemptId,
140        recipient: EndpointIdentity,
141        body: Body,
142    ) -> Result<Self, Error> {
143        body.validate()?;
144        let mut envelope = Self {
145            federation,
146            attempt,
147            sender: signer.endpoint(),
148            recipient,
149            body,
150            classical_signature: [0; 64],
151            post_quantum_signature: [0; ML_DSA_65_SIGNATURE_BYTES],
152        };
153        let transcript = envelope.signed_bytes()?;
154        envelope.classical_signature = signer.sign_classical(&transcript);
155        envelope.post_quantum_signature = signer.sign_post_quantum(&transcript);
156        Ok(envelope)
157    }
158
159    /// Parses one complete canonical envelope without trusting its sender.
160    pub fn decode(wire: &[u8]) -> Result<Self, Error> {
161        if wire.len() > MAX_ENVELOPE_BYTES {
162            return Err(Error::TooLarge);
163        }
164        let mut reader = Reader::new(wire);
165        if reader.fixed::<3>()? != FCP_WIRE_MARKER {
166            return Err(Error::BadMarker);
167        }
168        if reader.u8()? != FCP_WIRE_VERSION {
169            return Err(Error::UnsupportedVersion);
170        }
171        let kind = Kind::try_from(reader.u8()?)?;
172        let federation = FederationId::from_bytes(reader.fixed()?);
173        let attempt = AttemptId::from_bytes(reader.fixed()?);
174        let sender = read_identity(&mut reader)?;
175        let recipient = read_identity(&mut reader)?;
176        let body = match kind {
177            Kind::Offer => Body::Offer {
178                binding: WebRtcBinding::from_bytes(reader.fixed()?),
179                description: reader.bytes(MAX_DESCRIPTION_BYTES)?,
180            },
181            Kind::Answer => Body::Answer {
182                offer_id: EnvelopeId::from_bytes(reader.fixed()?),
183                binding: WebRtcBinding::from_bytes(reader.fixed()?),
184                description: reader.bytes(MAX_DESCRIPTION_BYTES)?,
185            },
186            Kind::Candidate => Body::Candidate {
187                parent_id: EnvelopeId::from_bytes(reader.fixed()?),
188                sequence: reader.u32()?,
189                candidate: reader.bytes(MAX_CANDIDATE_BYTES)?,
190            },
191            Kind::Close => Body::Close {
192                reason: CloseCode::from_u16(reader.u16()?),
193            },
194            Kind::CfrControl => Body::CfrControl {
195                payload: reader.bytes(MAX_CFR_CONTROL_BYTES)?,
196            },
197        };
198        body.validate()?;
199        let classical_signature = reader.fixed()?;
200        let post_quantum_signature = reader.fixed()?;
201        reader.finish()?;
202        let result = Self {
203            federation,
204            attempt,
205            sender,
206            recipient,
207            body,
208            classical_signature,
209            post_quantum_signature,
210        };
211        if result.encode()? != wire {
212            return Err(Error::NonCanonical);
213        }
214        Ok(result)
215    }
216
217    /// Parses and verifies one canonical envelope before returning it.
218    pub fn decode_verified(wire: &[u8]) -> Result<Self, Error> {
219        verify_wire(wire)?;
220        Self::decode(wire)
221    }
222
223    /// Verifies both mandatory signatures under the embedded sender identity.
224    pub fn verify(&self) -> Result<(), Error> {
225        let transcript = self.signed_bytes()?;
226        self.sender
227            .verify_classical(&transcript, &self.classical_signature)?;
228        self.sender
229            .verify_post_quantum(&transcript, &self.post_quantum_signature)
230    }
231
232    /// Returns the kind carried by this envelope.
233    #[must_use]
234    pub fn kind(&self) -> Kind {
235        self.body.kind()
236    }
237
238    /// Returns the stable envelope id.
239    pub fn id(&self) -> Result<EnvelopeId, Error> {
240        Ok(EnvelopeId::from_bytes(
241            *blake3::hash(&self.encode()?).as_bytes(),
242        ))
243    }
244
245    /// Returns the full canonical signed envelope wire encoding.
246    pub fn encode(&self) -> Result<Vec<u8>, Error> {
247        let mut bytes = self.signed_bytes()?;
248        bytes.extend_from_slice(&self.classical_signature);
249        bytes.extend_from_slice(&self.post_quantum_signature);
250        if bytes.len() > MAX_ENVELOPE_BYTES {
251            return Err(Error::TooLarge);
252        }
253        Ok(bytes)
254    }
255
256    fn signed_bytes(&self) -> Result<Vec<u8>, Error> {
257        self.body.validate()?;
258        let mut writer = Writer::default();
259        writer.fixed(&FCP_WIRE_MARKER);
260        writer.u8(FCP_WIRE_VERSION);
261        writer.u8(self.kind() as u8);
262        writer.fixed(self.federation.as_bytes());
263        writer.fixed(self.attempt.as_bytes());
264        write_identity(&mut writer, &self.sender);
265        write_identity(&mut writer, &self.recipient);
266        match &self.body {
267            Body::Offer {
268                binding,
269                description,
270            } => {
271                writer.fixed(binding.as_bytes());
272                writer.bytes(description)?;
273            }
274            Body::Answer {
275                offer_id,
276                binding,
277                description,
278            } => {
279                writer.fixed(offer_id.as_bytes());
280                writer.fixed(binding.as_bytes());
281                writer.bytes(description)?;
282            }
283            Body::Candidate {
284                parent_id,
285                sequence,
286                candidate,
287            } => {
288                writer.fixed(parent_id.as_bytes());
289                writer.u32(*sequence);
290                writer.bytes(candidate)?;
291            }
292            Body::Close { reason } => writer.u16(reason.as_u16()),
293            Body::CfrControl { payload } => writer.bytes(payload)?,
294        }
295        Ok(writer.finish())
296    }
297}
298
299fn verify_wire(wire: &[u8]) -> Result<(), Error> {
300    if wire.len() > MAX_ENVELOPE_BYTES {
301        return Err(Error::TooLarge);
302    }
303    let mut reader = Reader::new(wire);
304    if reader.fixed::<3>()? != FCP_WIRE_MARKER {
305        return Err(Error::BadMarker);
306    }
307    if reader.u8()? != FCP_WIRE_VERSION {
308        return Err(Error::UnsupportedVersion);
309    }
310    let kind = Kind::try_from(reader.u8()?)?;
311    let _federation = reader.fixed::<32>()?;
312    let _attempt = reader.fixed::<16>()?;
313    let sender = read_identity(&mut reader)?;
314    let _recipient = read_identity(&mut reader)?;
315    match kind {
316        Kind::Offer => {
317            let _binding = reader.fixed::<32>()?;
318            let _description = reader.bytes_ref(MAX_DESCRIPTION_BYTES)?;
319        }
320        Kind::Answer => {
321            let _offer = reader.fixed::<32>()?;
322            let _binding = reader.fixed::<32>()?;
323            let _description = reader.bytes_ref(MAX_DESCRIPTION_BYTES)?;
324        }
325        Kind::Candidate => {
326            let _parent = reader.fixed::<32>()?;
327            let _sequence = reader.u32()?;
328            let _candidate = reader.bytes_ref(MAX_CANDIDATE_BYTES)?;
329        }
330        Kind::Close => {
331            let _reason = reader.u16()?;
332        }
333        Kind::CfrControl => {
334            let _payload = reader.bytes_ref(MAX_CFR_CONTROL_BYTES)?;
335        }
336    }
337    let signed_end = reader.offset;
338    let classical_signature = reader.fixed()?;
339    let post_quantum_signature = reader.fixed()?;
340    reader.finish()?;
341    sender.verify_classical(&wire[..signed_end], &classical_signature)?;
342    sender.verify_post_quantum(&wire[..signed_end], &post_quantum_signature)
343}
344
345fn write_identity(writer: &mut Writer, identity: &EndpointIdentity) {
346    writer.fixed(identity.classical.as_bytes());
347    writer.fixed(&identity.post_quantum);
348}
349
350fn read_identity(reader: &mut Reader<'_>) -> Result<EndpointIdentity, Error> {
351    Ok(EndpointIdentity::new(
352        crate::EndpointKey::from_bytes(reader.fixed()?),
353        reader.fixed()?,
354    ))
355}
356
357fn validate_len(length: usize, maximum: usize) -> Result<(), Error> {
358    if length > maximum || length > u32::MAX as usize {
359        return Err(Error::FieldTooLarge);
360    }
361    Ok(())
362}
363
364#[derive(Default)]
365struct Writer(Vec<u8>);
366
367impl Writer {
368    fn fixed(&mut self, value: &[u8]) {
369        self.0.extend_from_slice(value);
370    }
371
372    fn u8(&mut self, value: u8) {
373        self.0.push(value);
374    }
375
376    fn u16(&mut self, value: u16) {
377        self.fixed(&value.to_be_bytes());
378    }
379
380    fn u32(&mut self, value: u32) {
381        self.fixed(&value.to_be_bytes());
382    }
383
384    fn bytes(&mut self, value: &[u8]) -> Result<(), Error> {
385        validate_len(value.len(), MAX_CFR_CONTROL_BYTES)?;
386        let length = u32::try_from(value.len()).map_err(|_| Error::FieldTooLarge)?;
387        self.u32(length);
388        self.fixed(value);
389        Ok(())
390    }
391
392    fn finish(self) -> Vec<u8> {
393        self.0
394    }
395}
396
397struct Reader<'a> {
398    wire: &'a [u8],
399    offset: usize,
400}
401
402impl<'a> Reader<'a> {
403    const fn new(wire: &'a [u8]) -> Self {
404        Self { wire, offset: 0 }
405    }
406
407    fn fixed<const N: usize>(&mut self) -> Result<[u8; N], Error> {
408        let end = self.offset.checked_add(N).ok_or(Error::Truncated)?;
409        let bytes = self.wire.get(self.offset..end).ok_or(Error::Truncated)?;
410        self.offset = end;
411        bytes.try_into().map_err(|_| Error::Truncated)
412    }
413
414    fn u8(&mut self) -> Result<u8, Error> {
415        Ok(self.fixed::<1>()?[0])
416    }
417
418    fn u16(&mut self) -> Result<u16, Error> {
419        Ok(u16::from_be_bytes(self.fixed()?))
420    }
421
422    fn u32(&mut self) -> Result<u32, Error> {
423        Ok(u32::from_be_bytes(self.fixed()?))
424    }
425
426    fn bytes(&mut self, maximum: usize) -> Result<Vec<u8>, Error> {
427        Ok(self.bytes_ref(maximum)?.to_vec())
428    }
429
430    fn bytes_ref(&mut self, maximum: usize) -> Result<&'a [u8], Error> {
431        let length = usize::try_from(self.u32()?).map_err(|_| Error::FieldTooLarge)?;
432        validate_len(length, maximum)?;
433        let end = self.offset.checked_add(length).ok_or(Error::Truncated)?;
434        let bytes = self.wire.get(self.offset..end).ok_or(Error::Truncated)?;
435        self.offset = end;
436        Ok(bytes)
437    }
438
439    fn finish(&self) -> Result<(), Error> {
440        if self.offset == self.wire.len() {
441            Ok(())
442        } else {
443            Err(Error::TrailingBytes)
444        }
445    }
446}