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#[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}