Skip to main content

libfcp_core/
configuration.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 signed federation configuration shared by FCP clients and servers.
9
10use alloc::collections::BTreeSet;
11use alloc::vec::Vec;
12use core::convert::TryInto;
13
14use crate::{
15    EndpointIdentity, EndpointSigner, Error, FederationId, ML_DSA_65_PUBLIC_KEY_BYTES,
16    ML_DSA_65_SIGNATURE_BYTES,
17};
18
19/// Stable marker for a signed FCP federation configuration.
20pub const FEDERATION_CONFIG_MARKER: [u8; 4] = *b"FCFG";
21/// Current signed federation configuration format version.
22pub const FEDERATION_CONFIG_VERSION: u8 = 1;
23/// Maximum members in one configuration snapshot.
24pub const MAX_FEDERATION_MEMBERS: usize = 1_024;
25
26const IDENTITY_BYTES: usize = 32 + ML_DSA_65_PUBLIC_KEY_BYTES;
27const MEMBER_BYTES: usize = 32 + IDENTITY_BYTES;
28const FIXED_HEADER_BYTES: usize = 4 + 1 + 32 + IDENTITY_BYTES + 8 + 2;
29const SIGNATURE_BYTES: usize = 64 + ML_DSA_65_SIGNATURE_BYTES;
30
31/// Explicit association between a CFR participant identity and an FCP endpoint identity.
32///
33/// The CFR identity bytes are a `cfr_protocol::SigPublic` encoding. The core
34/// deliberately remains independent of CFR; `libfcp` validates and converts
35/// them through the pinned CFR API before routing messages.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct FederationMember {
38    /// Canonical 32-byte CFR participant identity key.
39    pub cfr_identity: [u8; 32],
40    /// Complete dual-algorithm FCP endpoint identity bound to that participant.
41    pub endpoint: EndpointIdentity,
42}
43
44/// Immutable federation policy snapshot published by its selected authority.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct FederationConfiguration {
47    /// Federation namespace to which every member binding applies.
48    pub federation: FederationId,
49    /// Pinned authority identity authorized to sign this snapshot.
50    pub authority: EndpointIdentity,
51    /// Monotonic application-defined configuration epoch.
52    pub epoch: u64,
53    /// Explicit participant-to-endpoint bindings for this epoch.
54    pub members: Vec<FederationMember>,
55}
56
57impl FederationConfiguration {
58    /// Creates a configuration after checking its bounded canonical member set.
59    pub fn new(
60        federation: FederationId,
61        authority: EndpointIdentity,
62        epoch: u64,
63        members: Vec<FederationMember>,
64    ) -> Result<Self, Error> {
65        let configuration = Self {
66            federation,
67            authority,
68            epoch,
69            members,
70        };
71        configuration.validate()?;
72        Ok(configuration)
73    }
74
75    fn validate(&self) -> Result<(), Error> {
76        if self.members.len() > MAX_FEDERATION_MEMBERS {
77            return Err(Error::TooManyFederationMembers);
78        }
79        let mut identities = BTreeSet::new();
80        let mut endpoints = BTreeSet::new();
81        for member in &self.members {
82            if !identities.insert(member.cfr_identity) || !endpoints.insert(member.endpoint) {
83                return Err(Error::DuplicateFederationMember);
84            }
85        }
86        Ok(())
87    }
88
89    fn signed_bytes(&self) -> Result<Vec<u8>, Error> {
90        self.validate()?;
91        let member_count =
92            u16::try_from(self.members.len()).map_err(|_| Error::TooManyFederationMembers)?;
93        let mut bytes = Vec::with_capacity(FIXED_HEADER_BYTES + self.members.len() * MEMBER_BYTES);
94        bytes.extend_from_slice(&FEDERATION_CONFIG_MARKER);
95        bytes.push(FEDERATION_CONFIG_VERSION);
96        bytes.extend_from_slice(self.federation.as_bytes());
97        write_identity(&mut bytes, &self.authority);
98        bytes.extend_from_slice(&self.epoch.to_be_bytes());
99        bytes.extend_from_slice(&member_count.to_be_bytes());
100        for member in &self.members {
101            bytes.extend_from_slice(&member.cfr_identity);
102            write_identity(&mut bytes, &member.endpoint);
103        }
104        Ok(bytes)
105    }
106}
107
108/// A federation configuration signed by its configured authority identity.
109///
110/// Both signatures are mandatory and verify the exact same canonical snapshot.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct SignedFederationConfiguration {
113    /// Public policy values authorized by the signatures.
114    pub configuration: FederationConfiguration,
115    classical_signature: [u8; 64],
116    post_quantum_signature: [u8; ML_DSA_65_SIGNATURE_BYTES],
117}
118
119impl SignedFederationConfiguration {
120    /// Signs a canonical configuration with exactly its pinned authority identity.
121    pub fn sign<S: EndpointSigner>(
122        configuration: FederationConfiguration,
123        signer: &S,
124    ) -> Result<Self, Error> {
125        if configuration.authority != signer.endpoint() {
126            return Err(Error::WrongConfigurationAuthority);
127        }
128        let transcript = configuration.signed_bytes()?;
129        Ok(Self {
130            configuration,
131            classical_signature: signer.sign_classical(&transcript),
132            post_quantum_signature: signer.sign_post_quantum(&transcript),
133        })
134    }
135
136    /// Verifies both snapshot signatures against the embedded pinned authority.
137    pub fn verify(&self) -> Result<(), Error> {
138        let transcript = self.configuration.signed_bytes()?;
139        self.configuration
140            .authority
141            .verify_classical(&transcript, &self.classical_signature)
142            .map_err(map_configuration_classical_error)?;
143        self.configuration
144            .authority
145            .verify_post_quantum(&transcript, &self.post_quantum_signature)
146            .map_err(map_configuration_pq_error)
147    }
148
149    /// Encodes the exact canonical signed configuration for application-selected delivery.
150    pub fn encode(&self) -> Result<Vec<u8>, Error> {
151        let mut wire = self.configuration.signed_bytes()?;
152        wire.extend_from_slice(&self.classical_signature);
153        wire.extend_from_slice(&self.post_quantum_signature);
154        Ok(wire)
155    }
156
157    /// Verifies a bounded canonical wire snapshot before materialising member records.
158    pub fn decode_verified(wire: &[u8]) -> Result<Self, Error> {
159        let parsed = parse_wire(wire)?;
160        let authority = parsed.authority;
161        let classical_signature = wire[parsed.signature_offset..parsed.signature_offset + 64]
162            .try_into()
163            .map_err(|_| Error::Truncated)?;
164        let post_quantum_signature = wire[parsed.signature_offset + 64..]
165            .try_into()
166            .map_err(|_| Error::Truncated)?;
167        authority
168            .verify_classical(&wire[..parsed.signature_offset], &classical_signature)
169            .map_err(map_configuration_classical_error)?;
170        authority
171            .verify_post_quantum(&wire[..parsed.signature_offset], &post_quantum_signature)
172            .map_err(map_configuration_pq_error)?;
173        let members = parsed
174            .member_bytes
175            .chunks_exact(MEMBER_BYTES)
176            .map(parse_member)
177            .collect::<Result<Vec<_>, _>>()?;
178        let configuration =
179            FederationConfiguration::new(parsed.federation, authority, parsed.epoch, members)?;
180        Ok(Self {
181            configuration,
182            classical_signature,
183            post_quantum_signature,
184        })
185    }
186}
187
188fn map_configuration_classical_error(error: Error) -> Error {
189    match error {
190        Error::BadSenderKey => Error::BadConfigurationAuthority,
191        Error::BadSignature => Error::BadConfigurationSignature,
192        other => other,
193    }
194}
195
196fn map_configuration_pq_error(error: Error) -> Error {
197    match error {
198        Error::BadPostQuantumKey => Error::BadConfigurationAuthority,
199        Error::BadPostQuantumSignature | Error::BadPostQuantumSignatureEncoding => {
200            Error::BadConfigurationSignature
201        }
202        other => other,
203    }
204}
205
206fn write_identity(bytes: &mut Vec<u8>, identity: &EndpointIdentity) {
207    bytes.extend_from_slice(identity.classical.as_bytes());
208    bytes.extend_from_slice(&identity.post_quantum);
209}
210
211fn read_identity(bytes: &[u8]) -> Result<EndpointIdentity, Error> {
212    if bytes.len() != IDENTITY_BYTES {
213        return Err(Error::Truncated);
214    }
215    Ok(EndpointIdentity::new(
216        crate::EndpointKey::from_bytes(bytes[..32].try_into().map_err(|_| Error::Truncated)?),
217        bytes[32..].try_into().map_err(|_| Error::Truncated)?,
218    ))
219}
220
221fn parse_member(bytes: &[u8]) -> Result<FederationMember, Error> {
222    if bytes.len() != MEMBER_BYTES {
223        return Err(Error::Truncated);
224    }
225    Ok(FederationMember {
226        cfr_identity: bytes[..32].try_into().map_err(|_| Error::Truncated)?,
227        endpoint: read_identity(&bytes[32..])?,
228    })
229}
230
231struct ParsedConfiguration<'a> {
232    federation: FederationId,
233    authority: EndpointIdentity,
234    epoch: u64,
235    member_bytes: &'a [u8],
236    signature_offset: usize,
237}
238
239fn parse_wire(wire: &[u8]) -> Result<ParsedConfiguration<'_>, Error> {
240    if wire.len() < FIXED_HEADER_BYTES + SIGNATURE_BYTES {
241        return Err(Error::Truncated);
242    }
243    if wire[..4] != FEDERATION_CONFIG_MARKER {
244        return Err(Error::BadConfigurationMarker);
245    }
246    if wire[4] != FEDERATION_CONFIG_VERSION {
247        return Err(Error::UnsupportedConfigurationVersion);
248    }
249    let federation =
250        FederationId::from_bytes(wire[5..37].try_into().map_err(|_| Error::Truncated)?);
251    let authority = read_identity(&wire[37..37 + IDENTITY_BYTES])?;
252    let epoch_start = 37 + IDENTITY_BYTES;
253    let epoch = u64::from_be_bytes(
254        wire[epoch_start..epoch_start + 8]
255            .try_into()
256            .map_err(|_| Error::Truncated)?,
257    );
258    let count_start = epoch_start + 8;
259    let count = usize::from(u16::from_be_bytes(
260        wire[count_start..count_start + 2]
261            .try_into()
262            .map_err(|_| Error::Truncated)?,
263    ));
264    if count > MAX_FEDERATION_MEMBERS {
265        return Err(Error::TooManyFederationMembers);
266    }
267    let member_bytes_len = count
268        .checked_mul(MEMBER_BYTES)
269        .ok_or(Error::TooManyFederationMembers)?;
270    let signature_offset = FIXED_HEADER_BYTES
271        .checked_add(member_bytes_len)
272        .ok_or(Error::TooManyFederationMembers)?;
273    let expected = signature_offset
274        .checked_add(SIGNATURE_BYTES)
275        .ok_or(Error::TooManyFederationMembers)?;
276    if wire.len() < expected {
277        return Err(Error::Truncated);
278    }
279    if wire.len() > expected {
280        return Err(Error::TrailingBytes);
281    }
282    Ok(ParsedConfiguration {
283        federation,
284        authority,
285        epoch,
286        member_bytes: &wire[FIXED_HEADER_BYTES..signature_offset],
287        signature_offset,
288    })
289}