Skip to main content

wire/
key_binding.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Wire representation of the offline key-binding registry.
3
4use chrono::{DateTime, Utc};
5use objects::object::{ContentHash, KeyBinding, KeyBindingRegistry, KeyRole, StateSignature};
6use serde::{Deserialize, Serialize};
7
8use crate::{ObjectData, ObjectId, ObjectType, ProtocolError, Result};
9
10/// Liveness portion of a signed key binding, explicit at the wire boundary.
11///
12/// The conversion back to the object model restores `revoked_at` before any
13/// signature verification, so the fields covered by `added_by_sig` remain
14/// byte-for-byte equivalent to the source binding.
15#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
16pub struct WireKeyBindingLiveness {
17    pub revoked_at: Option<DateTime<Utc>>,
18}
19
20/// Transport form of a [`KeyBinding`].
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
22pub struct WireKeyBinding {
23    pub algorithm: String,
24    pub public_key: String,
25    pub identity_ref: String,
26    pub role: KeyRole,
27    pub added_by_sig: StateSignature,
28    pub valid_from: DateTime<Utc>,
29    pub delegated_from: Option<ContentHash>,
30    pub liveness: WireKeyBindingLiveness,
31}
32
33/// Versioned set of key bindings carried as one closure object.
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
35pub struct WireKeyBindingRegistry {
36    pub format_version: u8,
37    pub epoch: u64,
38    pub previous_registry: Option<ContentHash>,
39    pub authority_signature: StateSignature,
40    pub bindings: Vec<WireKeyBinding>,
41}
42
43impl From<&KeyBinding> for WireKeyBinding {
44    fn from(binding: &KeyBinding) -> Self {
45        Self {
46            algorithm: binding.algorithm.clone(),
47            public_key: binding.public_key.clone(),
48            identity_ref: binding.identity_ref.clone(),
49            role: binding.role,
50            added_by_sig: binding.added_by_sig.clone(),
51            valid_from: binding.valid_from,
52            delegated_from: binding.delegated_from,
53            liveness: WireKeyBindingLiveness {
54                revoked_at: binding.revoked_at,
55            },
56        }
57    }
58}
59
60impl From<WireKeyBinding> for KeyBinding {
61    fn from(binding: WireKeyBinding) -> Self {
62        Self {
63            algorithm: binding.algorithm,
64            public_key: binding.public_key,
65            identity_ref: binding.identity_ref,
66            role: binding.role,
67            added_by_sig: binding.added_by_sig,
68            valid_from: binding.valid_from,
69            revoked_at: binding.liveness.revoked_at,
70            delegated_from: binding.delegated_from,
71        }
72    }
73}
74
75impl From<&KeyBindingRegistry> for WireKeyBindingRegistry {
76    fn from(registry: &KeyBindingRegistry) -> Self {
77        Self {
78            format_version: registry.format_version,
79            epoch: registry.epoch,
80            previous_registry: registry.previous_registry,
81            authority_signature: registry.authority_signature.clone(),
82            bindings: registry.bindings.iter().map(WireKeyBinding::from).collect(),
83        }
84    }
85}
86
87impl From<WireKeyBindingRegistry> for KeyBindingRegistry {
88    fn from(registry: WireKeyBindingRegistry) -> Self {
89        Self {
90            format_version: registry.format_version,
91            epoch: registry.epoch,
92            previous_registry: registry.previous_registry,
93            authority_signature: registry.authority_signature,
94            bindings: registry
95                .bindings
96                .into_iter()
97                .map(KeyBinding::from)
98                .collect(),
99        }
100    }
101}
102
103/// Encode a validated registry as a typed, content-addressed wire object.
104pub fn encode_key_binding_registry(registry: &KeyBindingRegistry) -> Result<ObjectData> {
105    registry
106        .validate()
107        .map_err(|error| ProtocolError::InvalidState(error.to_string()))?;
108    let id = registry
109        .content_hash()
110        .map_err(|error| ProtocolError::InvalidState(error.to_string()))?;
111    let wire = WireKeyBindingRegistry::from(registry);
112    Ok(ObjectData {
113        id: ObjectId::Hash(id),
114        obj_type: ObjectType::KeyBinding,
115        data: rmp_serde::to_vec_named(&wire)?,
116        is_delta: false,
117    })
118}
119
120/// Decode a key-binding wire object and verify its advertised content address.
121pub fn decode_key_binding_registry(data: &ObjectData) -> Result<KeyBindingRegistry> {
122    let (ObjectId::Hash(expected), ObjectType::KeyBinding) = (&data.id, data.obj_type) else {
123        return Err(ProtocolError::InvalidState(
124            "object id/type mismatch for key-binding registry".to_string(),
125        ));
126    };
127    if data.is_delta {
128        return Err(ProtocolError::InvalidState(
129            "key-binding registry objects cannot be delta encoded".to_string(),
130        ));
131    }
132    let wire: WireKeyBindingRegistry = rmp_serde::from_slice(&data.data)?;
133    let registry = KeyBindingRegistry::from(wire);
134    registry
135        .validate()
136        .map_err(|error| ProtocolError::InvalidState(error.to_string()))?;
137    let actual = registry
138        .content_hash()
139        .map_err(|error| ProtocolError::InvalidState(error.to_string()))?;
140    if actual != *expected {
141        return Err(ProtocolError::InvalidState(format!(
142            "key-binding registry hash mismatch: expected {}, computed {}",
143            expected.to_hex(),
144            actual.to_hex()
145        )));
146    }
147    Ok(registry)
148}
149
150#[cfg(test)]
151mod tests {
152    use chrono::{TimeZone, Utc};
153    use objects::object::{ContentHash, KeyBinding, KeyBindingRegistry, KeyRole, StateSignature};
154
155    use super::{WireKeyBinding, decode_key_binding_registry, encode_key_binding_registry};
156    use crate::{ObjectData, ObjectType};
157
158    #[test]
159    fn key_binding_registry_roundtrips_with_liveness_overlay_losslessly() {
160        let binding = KeyBinding {
161            algorithm: "ed25519".to_string(),
162            public_key: "11".repeat(32),
163            identity_ref: "user:01HZZZZZZZZZZZZZZZZZZZZZZZ".to_string(),
164            role: KeyRole::Author,
165            added_by_sig: StateSignature {
166                algorithm: "ed25519".to_string(),
167                public_key: "22".repeat(32),
168                signature: "33".repeat(64),
169            },
170            valid_from: Utc.timestamp_opt(1_725_000_000, 123_456_789).unwrap(),
171            revoked_at: Some(Utc.timestamp_opt(1_735_000_000, 987_654_321).unwrap()),
172            delegated_from: Some(ContentHash::from_bytes([0x44; 32])),
173        };
174        let wire_binding = WireKeyBinding::from(&binding);
175        assert_eq!(wire_binding.liveness.revoked_at, binding.revoked_at);
176        assert_eq!(KeyBinding::from(wire_binding), binding);
177        let registry = KeyBindingRegistry::new(
178            1,
179            Some(ContentHash::from_bytes([0x55; 32])),
180            StateSignature {
181                algorithm: "ed25519".to_string(),
182                public_key: "66".repeat(32),
183                signature: "77".repeat(64),
184            },
185            vec![binding],
186        );
187
188        let wire = encode_key_binding_registry(&registry).expect("encode wire registry");
189        assert_eq!(wire.obj_type, ObjectType::KeyBinding);
190        assert_eq!(ObjectType::KeyBinding.wire_name(), "key_binding");
191        assert_eq!(
192            ObjectType::from_wire("key_binding").expect("parse key-binding type"),
193            ObjectType::KeyBinding
194        );
195        let encoded_frame = rmp_serde::to_vec_named(&wire).expect("encode object frame");
196        let transferred: ObjectData =
197            rmp_serde::from_slice(&encoded_frame).expect("decode object frame");
198        let decoded = decode_key_binding_registry(&transferred).expect("decode wire registry");
199
200        assert_eq!(decoded, registry);
201        assert_eq!(
202            decoded.content_hash().unwrap(),
203            registry.content_hash().unwrap()
204        );
205    }
206
207    #[test]
208    fn key_binding_registry_rejects_a_mismatched_content_address() {
209        let registry = KeyBindingRegistry::new(
210            0,
211            None,
212            StateSignature {
213                algorithm: "ed25519".to_string(),
214                public_key: "66".repeat(32),
215                signature: "77".repeat(64),
216            },
217            Vec::new(),
218        );
219        let mut wire = encode_key_binding_registry(&registry).expect("encode wire registry");
220        wire.id = crate::ObjectId::Hash(ContentHash::from_bytes([0x55; 32]));
221
222        let error = decode_key_binding_registry(&wire).expect_err("hash mismatch must fail closed");
223        assert!(error.to_string().contains("hash mismatch"));
224    }
225}