Skip to main content

heddle_object_model/object/
key_binding.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Offline-verifiable bindings from signing keys to durable identities.
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7use super::{ContentHash, StateSignature};
8
9/// Domain separator for signatures that authorize a [`KeyBinding`].
10pub const KEY_BINDING_SIGNING_PAYLOAD_VERSION_TAG: &[u8] = b"hd-key-binding-v1\x00";
11
12/// A signing key's role within an identity's provenance chain.
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub struct KeyBinding {
15    /// Signature algorithm used by the bound key.
16    pub algorithm: String,
17    /// Hex-encoded raw public key bytes.
18    pub public_key: String,
19    /// Durable identity subject resolved by this key.
20    pub identity_ref: String,
21    /// Repository role granted to this key, such as `author` or `ci-runner`.
22    pub role: String,
23    /// Identity-key signature authorizing this binding.
24    pub added_by_sig: StateSignature,
25    /// First instant at which this binding may authenticate authored objects.
26    pub valid_from: DateTime<Utc>,
27    /// First instant at which this binding no longer authenticates new objects.
28    #[serde(default)]
29    pub revoked_at: Option<DateTime<Utc>>,
30    /// Content hash of the identity-owned root binding that authorized this
31    /// key. Only one delegation hop is permitted by repository verification.
32    #[serde(default)]
33    pub delegated_from: Option<ContentHash>,
34}
35
36impl KeyBinding {
37    /// Deterministic bytes covered by [`Self::added_by_sig`].
38    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    /// Stable address of this signed binding.
52    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    /// Validate the durable shape. Cryptographic authorization is checked by
60    /// the repository resolver, which has access to the signing backends.
61    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/// Versioned, content-addressed flat set of key bindings.
89#[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    /// Stable address of the registry's validated canonical encoding.
122    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}