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
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub struct KeyBinding {
15 pub algorithm: String,
17 pub public_key: String,
19 pub identity_ref: String,
21 pub role: String,
23 pub added_by_sig: StateSignature,
25 pub valid_from: DateTime<Utc>,
27 #[serde(default)]
29 pub revoked_at: Option<DateTime<Utc>>,
30 #[serde(default)]
33 pub delegated_from: Option<ContentHash>,
34}
35
36impl KeyBinding {
37 pub fn canonical_signing_payload(&self) -> Vec<u8> {
39 let mut payload = Vec::with_capacity(256);
40 payload.extend_from_slice(KEY_BINDING_SIGNING_PAYLOAD_VERSION_TAG);
41 push_field(&mut payload, self.algorithm.as_bytes());
42 push_field(&mut payload, self.public_key.as_bytes());
43 push_field(&mut payload, self.identity_ref.as_bytes());
44 push_field(&mut payload, self.role.as_bytes());
45 push_time(&mut payload, self.valid_from);
46 push_optional_time(&mut payload, self.revoked_at);
47 push_optional_hash(&mut payload, self.delegated_from);
48 payload
49 }
50
51 pub fn content_hash(&self) -> Result<ContentHash, KeyBindingError> {
53 self.validate()?;
54 let encoded =
55 rmp_serde::to_vec(self).map_err(|error| KeyBindingError::Codec(error.to_string()))?;
56 Ok(ContentHash::compute_typed("key-binding", &encoded))
57 }
58
59 pub fn validate(&self) -> Result<(), KeyBindingError> {
62 require_non_empty(&self.algorithm, KeyBindingError::EmptyAlgorithm)?;
63 require_hex(&self.public_key, KeyBindingError::InvalidPublicKey)?;
64 require_non_empty(&self.identity_ref, KeyBindingError::EmptyIdentityRef)?;
65 require_non_empty(&self.role, KeyBindingError::EmptyRole)?;
66 require_non_empty(
67 &self.added_by_sig.algorithm,
68 KeyBindingError::EmptyAddedByAlgorithm,
69 )?;
70 require_hex(
71 &self.added_by_sig.public_key,
72 KeyBindingError::InvalidAddedByPublicKey,
73 )?;
74 require_hex(
75 &self.added_by_sig.signature,
76 KeyBindingError::InvalidAddedBySignature,
77 )?;
78 if self
79 .revoked_at
80 .is_some_and(|revoked| revoked < self.valid_from)
81 {
82 return Err(KeyBindingError::RevokedBeforeValid);
83 }
84 Ok(())
85 }
86}
87
88#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
90pub struct KeyBindingRegistry {
91 pub format_version: u8,
92 pub bindings: Vec<KeyBinding>,
93}
94
95impl KeyBindingRegistry {
96 pub const FORMAT_VERSION: u8 = 1;
97
98 pub fn new(bindings: Vec<KeyBinding>) -> Self {
99 Self {
100 format_version: Self::FORMAT_VERSION,
101 bindings,
102 }
103 }
104
105 pub fn empty() -> Self {
106 Self::new(Vec::new())
107 }
108
109 pub fn encode(&self) -> Result<Vec<u8>, KeyBindingError> {
110 self.validate()?;
111 rmp_serde::to_vec(self).map_err(|error| KeyBindingError::Codec(error.to_string()))
112 }
113
114 pub fn decode(bytes: &[u8]) -> Result<Self, KeyBindingError> {
115 let registry: Self = rmp_serde::from_slice(bytes)
116 .map_err(|error| KeyBindingError::Codec(error.to_string()))?;
117 registry.validate()?;
118 Ok(registry)
119 }
120
121 pub fn content_hash(&self) -> Result<ContentHash, KeyBindingError> {
123 Ok(ContentHash::compute_typed(
124 "key-binding-registry",
125 &self.encode()?,
126 ))
127 }
128
129 pub fn validate(&self) -> Result<(), KeyBindingError> {
130 if self.format_version != Self::FORMAT_VERSION {
131 return Err(KeyBindingError::UnsupportedVersion(self.format_version));
132 }
133 for (index, binding) in self.bindings.iter().enumerate() {
134 binding.validate()?;
135 if self.bindings[..index].iter().any(|prior| {
136 prior.algorithm.eq_ignore_ascii_case(&binding.algorithm)
137 && prior.public_key.eq_ignore_ascii_case(&binding.public_key)
138 }) {
139 return Err(KeyBindingError::DuplicateKey);
140 }
141 }
142 Ok(())
143 }
144}
145
146fn require_non_empty(value: &str, error: KeyBindingError) -> Result<(), KeyBindingError> {
147 if value.trim().is_empty() {
148 Err(error)
149 } else {
150 Ok(())
151 }
152}
153
154fn require_hex(value: &str, error: KeyBindingError) -> Result<(), KeyBindingError> {
155 if value.is_empty() || hex::decode(value).is_err() {
156 Err(error)
157 } else {
158 Ok(())
159 }
160}
161
162fn push_field(payload: &mut Vec<u8>, value: &[u8]) {
163 payload.extend_from_slice(&(value.len() as u64).to_le_bytes());
164 payload.extend_from_slice(value);
165}
166
167fn push_optional_hash(payload: &mut Vec<u8>, value: Option<ContentHash>) {
168 match value {
169 Some(value) => {
170 payload.push(1);
171 payload.extend_from_slice(value.as_bytes());
172 }
173 None => payload.push(0),
174 }
175}
176
177fn push_time(payload: &mut Vec<u8>, value: DateTime<Utc>) {
178 payload.extend_from_slice(&value.timestamp().to_le_bytes());
179 payload.extend_from_slice(&value.timestamp_subsec_nanos().to_le_bytes());
180}
181
182fn push_optional_time(payload: &mut Vec<u8>, value: Option<DateTime<Utc>>) {
183 match value {
184 Some(value) => {
185 payload.push(1);
186 push_time(payload, value);
187 }
188 None => payload.push(0),
189 }
190}
191
192#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
193pub enum KeyBindingError {
194 #[error("unsupported key-binding registry version {0}")]
195 UnsupportedVersion(u8),
196 #[error("key-binding registry codec error: {0}")]
197 Codec(String),
198 #[error("key binding algorithm must not be empty")]
199 EmptyAlgorithm,
200 #[error("key binding public key must be non-empty hexadecimal bytes")]
201 InvalidPublicKey,
202 #[error("key binding identity_ref must not be empty")]
203 EmptyIdentityRef,
204 #[error("key binding role must not be empty")]
205 EmptyRole,
206 #[error("key binding authorizing signature algorithm must not be empty")]
207 EmptyAddedByAlgorithm,
208 #[error("key binding authorizing public key must be non-empty hexadecimal bytes")]
209 InvalidAddedByPublicKey,
210 #[error("key binding authorizing signature must be non-empty hexadecimal bytes")]
211 InvalidAddedBySignature,
212 #[error("key binding revoked_at must not precede valid_from")]
213 RevokedBeforeValid,
214 #[error("key-binding registry contains a duplicate algorithm/public-key pair")]
215 DuplicateKey,
216}