iroh_db_security/
checkpoint.rs

1use iroh_db_core::{AuthorId, CapabilityId, DomainId};
2
3use crate::{ControlTransition, IrohSigner, key::authority_checkpoint_signature_message};
4
5const CHECKPOINT_VERSION: u16 = 1;
6const MAX_CHECKPOINT_BYTES: usize = 1024 * 1024;
7const MAX_CHECKPOINT_CAPABILITIES: usize = 1024;
8const MAX_CHECKPOINT_REVOCATIONS: usize = 16_384;
9
10/// A signed, history-compacting authority baseline for a targeted invitation.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct AuthorityCheckpoint {
13    domain_id: DomainId,
14    epoch: u64,
15    head: Option<ControlTransition>,
16    capability_ids: Vec<CapabilityId>,
17    active_capability_ids: Vec<CapabilityId>,
18    subject_revocations: Vec<(AuthorId, u64)>,
19    issuer: AuthorId,
20    issuer_capability: CapabilityId,
21    subject: AuthorId,
22    signature: [u8; 64],
23}
24
25impl AuthorityCheckpoint {
26    #[allow(clippy::too_many_arguments)]
27    pub fn issue(
28        domain_id: DomainId,
29        epoch: u64,
30        head: Option<ControlTransition>,
31        mut capability_ids: Vec<CapabilityId>,
32        mut active_capability_ids: Vec<CapabilityId>,
33        mut subject_revocations: Vec<(AuthorId, u64)>,
34        issuer_capability: CapabilityId,
35        subject: AuthorId,
36        signer: &IrohSigner,
37    ) -> Result<Self, CheckpointError> {
38        capability_ids.sort_unstable();
39        capability_ids.dedup();
40        active_capability_ids.sort_unstable();
41        active_capability_ids.dedup();
42        subject_revocations.sort_unstable_by_key(|entry| entry.0);
43        if subject_revocations
44            .windows(2)
45            .any(|pair| pair[0].0 == pair[1].0)
46        {
47            return Err(CheckpointError::InvalidShape);
48        }
49        let mut checkpoint = Self {
50            domain_id,
51            epoch,
52            head,
53            capability_ids,
54            active_capability_ids,
55            subject_revocations,
56            issuer: signer.author(),
57            issuer_capability,
58            subject,
59            signature: [0; 64],
60        };
61        checkpoint.validate_shape()?;
62        checkpoint.signature = signer.sign_authority_checkpoint(&checkpoint.signing_bytes()?);
63        Ok(checkpoint)
64    }
65
66    pub const fn domain_id(&self) -> DomainId {
67        self.domain_id
68    }
69    pub const fn epoch(&self) -> u64 {
70        self.epoch
71    }
72    pub const fn head(&self) -> Option<&ControlTransition> {
73        self.head.as_ref()
74    }
75    pub fn capability_ids(&self) -> &[CapabilityId] {
76        &self.capability_ids
77    }
78    pub fn active_capability_ids(&self) -> &[CapabilityId] {
79        &self.active_capability_ids
80    }
81    pub fn subject_revocations(&self) -> &[(AuthorId, u64)] {
82        &self.subject_revocations
83    }
84    pub const fn issuer(&self) -> AuthorId {
85        self.issuer
86    }
87    pub const fn issuer_capability(&self) -> CapabilityId {
88        self.issuer_capability
89    }
90    pub const fn subject(&self) -> AuthorId {
91        self.subject
92    }
93
94    pub fn encode_canonical(&self) -> Result<Vec<u8>, CheckpointError> {
95        let bytes = minicbor::to_vec(CheckpointWire {
96            content: self.signing_bytes()?,
97            signature: self.signature,
98        })
99        .map_err(codec_error)?;
100        if bytes.len() > MAX_CHECKPOINT_BYTES {
101            return Err(CheckpointError::TooLarge(bytes.len()));
102        }
103        Ok(bytes)
104    }
105
106    pub fn decode_canonical(bytes: &[u8]) -> Result<Self, CheckpointError> {
107        if bytes.len() > MAX_CHECKPOINT_BYTES {
108            return Err(CheckpointError::TooLarge(bytes.len()));
109        }
110        let wire: CheckpointWire = decode_exact(bytes)?;
111        let content: CheckpointContent = decode_exact(&wire.content)?;
112        if content.version != CHECKPOINT_VERSION {
113            return Err(CheckpointError::UnsupportedVersion(content.version));
114        }
115        let head = content
116            .head
117            .map(|bytes| ControlTransition::decode_canonical(&bytes))
118            .transpose()
119            .map_err(authority_error)?;
120        let checkpoint = Self {
121            domain_id: content.domain_id,
122            epoch: content.epoch,
123            head,
124            capability_ids: content.capability_ids,
125            active_capability_ids: content.active_capability_ids,
126            subject_revocations: content.subject_revocations,
127            issuer: content.issuer,
128            issuer_capability: content.issuer_capability,
129            subject: content.subject,
130            signature: wire.signature,
131        };
132        checkpoint.validate_shape()?;
133        checkpoint.verify()?;
134        if checkpoint.signing_bytes()? != wire.content || checkpoint.encode_canonical()? != bytes {
135            return Err(CheckpointError::NonCanonical);
136        }
137        Ok(checkpoint)
138    }
139
140    pub fn verify(&self) -> Result<(), CheckpointError> {
141        let public = iroh_base::PublicKey::try_from(self.issuer.as_bytes())
142            .map_err(|_| CheckpointError::InvalidAuthor)?;
143        public
144            .verify(
145                &authority_checkpoint_signature_message(&self.signing_bytes()?),
146                &iroh_base::Signature::from_bytes(&self.signature),
147            )
148            .map_err(|_| CheckpointError::InvalidSignature)
149    }
150
151    fn validate_shape(&self) -> Result<(), CheckpointError> {
152        if self.capability_ids.is_empty()
153            || self.capability_ids.len() > MAX_CHECKPOINT_CAPABILITIES
154            || self.active_capability_ids.is_empty()
155            || self.active_capability_ids.len() > self.capability_ids.len()
156            || self.subject_revocations.len() > MAX_CHECKPOINT_REVOCATIONS
157            || !strictly_sorted(&self.capability_ids)
158            || !strictly_sorted(&self.active_capability_ids)
159            || self
160                .active_capability_ids
161                .iter()
162                .any(|id| !self.capability_ids.contains(id))
163            || !self
164                .subject_revocations
165                .windows(2)
166                .all(|pair| pair[0].0 < pair[1].0)
167            || !self.capability_ids.contains(&self.issuer_capability)
168        {
169            return Err(CheckpointError::InvalidShape);
170        }
171        match self.head.as_ref() {
172            None if self.epoch == 0 => {}
173            Some(head) if head.domain_id() == self.domain_id && head.new_epoch() == self.epoch => {}
174            _ => return Err(CheckpointError::InvalidShape),
175        }
176        if self.subject_revocations.iter().any(|(_, sequence)| {
177            *sequence == 0
178                || self
179                    .head
180                    .as_ref()
181                    .is_none_or(|head| *sequence > head.sequence())
182        }) {
183            return Err(CheckpointError::InvalidShape);
184        }
185        Ok(())
186    }
187
188    fn signing_bytes(&self) -> Result<Vec<u8>, CheckpointError> {
189        minicbor::to_vec(CheckpointContent {
190            version: CHECKPOINT_VERSION,
191            domain_id: self.domain_id,
192            epoch: self.epoch,
193            head: self
194                .head
195                .as_ref()
196                .map(ControlTransition::encode_canonical)
197                .transpose()
198                .map_err(authority_error)?,
199            capability_ids: self.capability_ids.clone(),
200            active_capability_ids: self.active_capability_ids.clone(),
201            subject_revocations: self.subject_revocations.clone(),
202            issuer: self.issuer,
203            issuer_capability: self.issuer_capability,
204            subject: self.subject,
205        })
206        .map_err(codec_error)
207    }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
211pub enum CheckpointError {
212    #[error("authority checkpoint codec failed: {0}")]
213    Codec(String),
214    #[error("authority checkpoint is not canonical")]
215    NonCanonical,
216    #[error("unsupported authority checkpoint version: {0}")]
217    UnsupportedVersion(u16),
218    #[error("authority checkpoint has invalid shape")]
219    InvalidShape,
220    #[error("authority checkpoint signature is invalid")]
221    InvalidSignature,
222    #[error("authority checkpoint endpoint is invalid")]
223    InvalidAuthor,
224    #[error("authority checkpoint exceeds its bound: {0}")]
225    TooLarge(usize),
226    #[error("authority checkpoint evidence is invalid: {0}")]
227    Authority(String),
228}
229
230#[derive(minicbor::Encode, minicbor::Decode)]
231#[cbor(array)]
232struct CheckpointContent {
233    #[n(0)]
234    version: u16,
235    #[n(1)]
236    domain_id: DomainId,
237    #[n(2)]
238    epoch: u64,
239    #[n(3)]
240    head: Option<Vec<u8>>,
241    #[n(4)]
242    capability_ids: Vec<CapabilityId>,
243    #[n(5)]
244    subject_revocations: Vec<(AuthorId, u64)>,
245    #[n(6)]
246    issuer: AuthorId,
247    #[n(7)]
248    issuer_capability: CapabilityId,
249    #[n(8)]
250    subject: AuthorId,
251    #[n(9)]
252    active_capability_ids: Vec<CapabilityId>,
253}
254
255#[derive(minicbor::Encode, minicbor::Decode)]
256#[cbor(array)]
257struct CheckpointWire {
258    #[n(0)]
259    content: Vec<u8>,
260    #[n(1)]
261    signature: [u8; 64],
262}
263
264fn strictly_sorted<T: Ord>(values: &[T]) -> bool {
265    values.windows(2).all(|pair| pair[0] < pair[1])
266}
267fn decode_exact<T: for<'a> minicbor::Decode<'a, ()>>(bytes: &[u8]) -> Result<T, CheckpointError> {
268    let mut decoder = minicbor::Decoder::new(bytes);
269    let value = decoder.decode().map_err(codec_error)?;
270    if decoder.position() != bytes.len() {
271        return Err(CheckpointError::NonCanonical);
272    }
273    Ok(value)
274}
275fn codec_error(error: impl std::fmt::Display) -> CheckpointError {
276    CheckpointError::Codec(error.to_string())
277}
278fn authority_error(error: impl std::fmt::Display) -> CheckpointError {
279    CheckpointError::Authority(error.to_string())
280}