heddle_object_model/object/
key_binding.rs1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7use super::{ContentHash, StateSignature};
8
9pub const KEY_BINDING_SIGNING_PAYLOAD_VERSION_TAG: &[u8] = b"hd-key-binding-v1\x00";
11
12pub const KEY_BINDING_REGISTRY_SIGNING_PAYLOAD_VERSION_TAG: &[u8] =
14 b"hd-key-binding-registry-v3\x00";
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum KeyRole {
20 Author,
22 Reviewer,
24 CiRunner,
26}
27
28impl KeyRole {
29 pub fn as_str(self) -> &'static str {
30 match self {
31 Self::Author => "author",
32 Self::Reviewer => "reviewer",
33 Self::CiRunner => "ci_runner",
34 }
35 }
36}
37
38#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40pub struct KeyBinding {
41 pub algorithm: String,
43 pub public_key: String,
45 pub identity_ref: String,
47 pub role: KeyRole,
49 pub added_by_sig: StateSignature,
51 pub valid_from: DateTime<Utc>,
53 #[serde(default)]
55 pub revoked_at: Option<DateTime<Utc>>,
56 #[serde(default)]
59 pub delegated_from: Option<ContentHash>,
60}
61
62impl KeyBinding {
63 pub fn canonical_signing_payload(&self) -> Vec<u8> {
65 let mut payload = Vec::with_capacity(256);
66 payload.extend_from_slice(KEY_BINDING_SIGNING_PAYLOAD_VERSION_TAG);
67 push_field(&mut payload, self.algorithm.as_bytes());
68 push_field(&mut payload, self.public_key.as_bytes());
69 push_field(&mut payload, self.identity_ref.as_bytes());
70 push_field(&mut payload, self.role.as_str().as_bytes());
71 push_time(&mut payload, self.valid_from);
72 push_optional_time(&mut payload, self.revoked_at);
73 push_optional_hash(&mut payload, self.delegated_from);
74 payload
75 }
76
77 pub fn content_hash(&self) -> Result<ContentHash, KeyBindingError> {
79 self.validate()?;
80 let encoded =
81 rmp_serde::to_vec(self).map_err(|error| KeyBindingError::Codec(error.to_string()))?;
82 Ok(ContentHash::compute_typed("key-binding", &encoded))
83 }
84
85 pub fn validate(&self) -> Result<(), KeyBindingError> {
88 require_non_empty(&self.algorithm, KeyBindingError::EmptyAlgorithm)?;
89 require_hex(&self.public_key, KeyBindingError::InvalidPublicKey)?;
90 require_non_empty(&self.identity_ref, KeyBindingError::EmptyIdentityRef)?;
91 require_non_empty(
92 &self.added_by_sig.algorithm,
93 KeyBindingError::EmptyAddedByAlgorithm,
94 )?;
95 require_hex(
96 &self.added_by_sig.public_key,
97 KeyBindingError::InvalidAddedByPublicKey,
98 )?;
99 require_hex(
100 &self.added_by_sig.signature,
101 KeyBindingError::InvalidAddedBySignature,
102 )?;
103 if self
104 .revoked_at
105 .is_some_and(|revoked| revoked < self.valid_from)
106 {
107 return Err(KeyBindingError::RevokedBeforeValid);
108 }
109 Ok(())
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
115pub struct KeyBindingRegistry {
116 pub format_version: u8,
117 pub epoch: u64,
119 pub previous_registry: Option<ContentHash>,
121 pub authority_signature: StateSignature,
123 pub bindings: Vec<KeyBinding>,
124}
125
126impl KeyBindingRegistry {
127 pub const FORMAT_VERSION: u8 = 3;
128
129 pub fn new(
130 epoch: u64,
131 previous_registry: Option<ContentHash>,
132 authority_signature: StateSignature,
133 bindings: Vec<KeyBinding>,
134 ) -> Self {
135 Self {
136 format_version: Self::FORMAT_VERSION,
137 epoch,
138 previous_registry,
139 authority_signature,
140 bindings,
141 }
142 }
143
144 pub fn canonical_checkpoint_signing_payload(&self) -> Result<Vec<u8>, KeyBindingError> {
146 let mut payload = Vec::with_capacity(128 + self.bindings.len() * 32);
147 payload.extend_from_slice(KEY_BINDING_REGISTRY_SIGNING_PAYLOAD_VERSION_TAG);
148 payload.push(self.format_version);
149 payload.extend_from_slice(&self.epoch.to_le_bytes());
150 push_optional_hash(&mut payload, self.previous_registry);
151 payload.extend_from_slice(&(self.bindings.len() as u64).to_le_bytes());
152 for binding in &self.bindings {
153 payload.extend_from_slice(binding.content_hash()?.as_bytes());
154 }
155 Ok(payload)
156 }
157
158 pub fn encode(&self) -> Result<Vec<u8>, KeyBindingError> {
159 self.validate()?;
160 rmp_serde::to_vec(self).map_err(|error| KeyBindingError::Codec(error.to_string()))
161 }
162
163 pub fn decode(bytes: &[u8]) -> Result<Self, KeyBindingError> {
164 let registry: Self = rmp_serde::from_slice(bytes)
165 .map_err(|error| KeyBindingError::Codec(error.to_string()))?;
166 registry.validate()?;
167 Ok(registry)
168 }
169
170 pub fn content_hash(&self) -> Result<ContentHash, KeyBindingError> {
172 Ok(ContentHash::compute_typed(
173 "key-binding-registry",
174 &self.encode()?,
175 ))
176 }
177
178 pub fn validate(&self) -> Result<(), KeyBindingError> {
179 if self.format_version != Self::FORMAT_VERSION {
180 return Err(KeyBindingError::UnsupportedVersion(self.format_version));
181 }
182 match (self.epoch, self.previous_registry) {
183 (0, Some(_)) => return Err(KeyBindingError::GenesisHasPrevious),
184 (1.., None) => return Err(KeyBindingError::MissingPreviousRegistry(self.epoch)),
185 _ => {}
186 }
187 require_non_empty(
188 &self.authority_signature.algorithm,
189 KeyBindingError::EmptyAuthorityAlgorithm,
190 )?;
191 require_hex(
192 &self.authority_signature.public_key,
193 KeyBindingError::InvalidAuthorityPublicKey,
194 )?;
195 require_hex(
196 &self.authority_signature.signature,
197 KeyBindingError::InvalidAuthoritySignature,
198 )?;
199 for (index, binding) in self.bindings.iter().enumerate() {
200 binding.validate()?;
201 if self.bindings[..index].iter().any(|prior| {
202 prior.algorithm.eq_ignore_ascii_case(&binding.algorithm)
203 && prior.public_key.eq_ignore_ascii_case(&binding.public_key)
204 }) {
205 return Err(KeyBindingError::DuplicateKey);
206 }
207 }
208 Ok(())
209 }
210}
211
212fn require_non_empty(value: &str, error: KeyBindingError) -> Result<(), KeyBindingError> {
213 if value.trim().is_empty() {
214 Err(error)
215 } else {
216 Ok(())
217 }
218}
219
220fn require_hex(value: &str, error: KeyBindingError) -> Result<(), KeyBindingError> {
221 if value.is_empty() || hex::decode(value).is_err() {
222 Err(error)
223 } else {
224 Ok(())
225 }
226}
227
228fn push_field(payload: &mut Vec<u8>, value: &[u8]) {
229 payload.extend_from_slice(&(value.len() as u64).to_le_bytes());
230 payload.extend_from_slice(value);
231}
232
233fn push_optional_hash(payload: &mut Vec<u8>, value: Option<ContentHash>) {
234 match value {
235 Some(value) => {
236 payload.push(1);
237 payload.extend_from_slice(value.as_bytes());
238 }
239 None => payload.push(0),
240 }
241}
242
243fn push_time(payload: &mut Vec<u8>, value: DateTime<Utc>) {
244 payload.extend_from_slice(&value.timestamp().to_le_bytes());
245 payload.extend_from_slice(&value.timestamp_subsec_nanos().to_le_bytes());
246}
247
248fn push_optional_time(payload: &mut Vec<u8>, value: Option<DateTime<Utc>>) {
249 match value {
250 Some(value) => {
251 payload.push(1);
252 push_time(payload, value);
253 }
254 None => payload.push(0),
255 }
256}
257
258#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
259pub enum KeyBindingError {
260 #[error("unsupported key-binding registry version {0}")]
261 UnsupportedVersion(u8),
262 #[error("key-binding registry codec error: {0}")]
263 Codec(String),
264 #[error("genesis key-binding registry must not reference a previous checkpoint")]
265 GenesisHasPrevious,
266 #[error("key-binding registry epoch {0} must reference its previous checkpoint")]
267 MissingPreviousRegistry(u64),
268 #[error("key-binding registry authority signature algorithm must not be empty")]
269 EmptyAuthorityAlgorithm,
270 #[error("key-binding registry authority public key must be non-empty hexadecimal bytes")]
271 InvalidAuthorityPublicKey,
272 #[error("key-binding registry authority signature must be non-empty hexadecimal bytes")]
273 InvalidAuthoritySignature,
274 #[error("key binding algorithm must not be empty")]
275 EmptyAlgorithm,
276 #[error("key binding public key must be non-empty hexadecimal bytes")]
277 InvalidPublicKey,
278 #[error("key binding identity_ref must not be empty")]
279 EmptyIdentityRef,
280 #[error("key binding authorizing signature algorithm must not be empty")]
281 EmptyAddedByAlgorithm,
282 #[error("key binding authorizing public key must be non-empty hexadecimal bytes")]
283 InvalidAddedByPublicKey,
284 #[error("key binding authorizing signature must be non-empty hexadecimal bytes")]
285 InvalidAddedBySignature,
286 #[error("key binding revoked_at must not precede valid_from")]
287 RevokedBeforeValid,
288 #[error("key-binding registry contains a duplicate algorithm/public-key pair")]
289 DuplicateKey,
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn prior_registry_format_is_rejected_instead_of_dual_read() {
298 let legacy = KeyBindingRegistry {
299 format_version: 2,
300 epoch: 0,
301 previous_registry: None,
302 authority_signature: StateSignature {
303 algorithm: "ed25519".to_string(),
304 public_key: "11".repeat(32),
305 signature: "22".repeat(64),
306 },
307 bindings: Vec::new(),
308 };
309 let bytes = rmp_serde::to_vec(&legacy).expect("encode unsupported fixture");
310 assert!(matches!(
311 KeyBindingRegistry::decode(&bytes),
312 Err(KeyBindingError::UnsupportedVersion(2))
313 ));
314 }
315}