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, 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: String,
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 bindings: Vec<WireKeyBinding>,
38}
39
40impl From<&KeyBinding> for WireKeyBinding {
41    fn from(binding: &KeyBinding) -> Self {
42        Self {
43            algorithm: binding.algorithm.clone(),
44            public_key: binding.public_key.clone(),
45            identity_ref: binding.identity_ref.clone(),
46            role: binding.role.clone(),
47            added_by_sig: binding.added_by_sig.clone(),
48            valid_from: binding.valid_from,
49            delegated_from: binding.delegated_from,
50            liveness: WireKeyBindingLiveness {
51                revoked_at: binding.revoked_at,
52            },
53        }
54    }
55}
56
57impl From<WireKeyBinding> for KeyBinding {
58    fn from(binding: WireKeyBinding) -> Self {
59        Self {
60            algorithm: binding.algorithm,
61            public_key: binding.public_key,
62            identity_ref: binding.identity_ref,
63            role: binding.role,
64            added_by_sig: binding.added_by_sig,
65            valid_from: binding.valid_from,
66            revoked_at: binding.liveness.revoked_at,
67            delegated_from: binding.delegated_from,
68        }
69    }
70}
71
72impl From<&KeyBindingRegistry> for WireKeyBindingRegistry {
73    fn from(registry: &KeyBindingRegistry) -> Self {
74        Self {
75            format_version: registry.format_version,
76            bindings: registry.bindings.iter().map(WireKeyBinding::from).collect(),
77        }
78    }
79}
80
81impl From<WireKeyBindingRegistry> for KeyBindingRegistry {
82    fn from(registry: WireKeyBindingRegistry) -> Self {
83        Self {
84            format_version: registry.format_version,
85            bindings: registry
86                .bindings
87                .into_iter()
88                .map(KeyBinding::from)
89                .collect(),
90        }
91    }
92}
93
94/// Encode a validated registry as a typed, content-addressed wire object.
95pub fn encode_key_binding_registry(registry: &KeyBindingRegistry) -> Result<ObjectData> {
96    registry
97        .validate()
98        .map_err(|error| ProtocolError::InvalidState(error.to_string()))?;
99    let id = registry
100        .content_hash()
101        .map_err(|error| ProtocolError::InvalidState(error.to_string()))?;
102    let wire = WireKeyBindingRegistry::from(registry);
103    Ok(ObjectData {
104        id: ObjectId::Hash(id),
105        obj_type: ObjectType::KeyBinding,
106        data: rmp_serde::to_vec_named(&wire)?,
107        is_delta: false,
108    })
109}
110
111/// Decode a key-binding wire object and verify its advertised content address.
112pub fn decode_key_binding_registry(data: &ObjectData) -> Result<KeyBindingRegistry> {
113    let (ObjectId::Hash(expected), ObjectType::KeyBinding) = (&data.id, data.obj_type) else {
114        return Err(ProtocolError::InvalidState(
115            "object id/type mismatch for key-binding registry".to_string(),
116        ));
117    };
118    if data.is_delta {
119        return Err(ProtocolError::InvalidState(
120            "key-binding registry objects cannot be delta encoded".to_string(),
121        ));
122    }
123    let wire: WireKeyBindingRegistry = rmp_serde::from_slice(&data.data)?;
124    let registry = KeyBindingRegistry::from(wire);
125    registry
126        .validate()
127        .map_err(|error| ProtocolError::InvalidState(error.to_string()))?;
128    let actual = registry
129        .content_hash()
130        .map_err(|error| ProtocolError::InvalidState(error.to_string()))?;
131    if actual != *expected {
132        return Err(ProtocolError::InvalidState(format!(
133            "key-binding registry hash mismatch: expected {}, computed {}",
134            expected.to_hex(),
135            actual.to_hex()
136        )));
137    }
138    Ok(registry)
139}
140
141#[cfg(test)]
142mod tests {
143    use chrono::{TimeZone, Utc};
144    use objects::object::{ContentHash, KeyBinding, KeyBindingRegistry, StateSignature};
145
146    use super::{WireKeyBinding, decode_key_binding_registry, encode_key_binding_registry};
147    use crate::{ObjectData, ObjectType};
148
149    #[test]
150    fn key_binding_registry_roundtrips_with_liveness_overlay_losslessly() {
151        let binding = KeyBinding {
152            algorithm: "ed25519".to_string(),
153            public_key: "11".repeat(32),
154            identity_ref: "user:01HZZZZZZZZZZZZZZZZZZZZZZZ".to_string(),
155            role: "author".to_string(),
156            added_by_sig: StateSignature {
157                algorithm: "ed25519".to_string(),
158                public_key: "22".repeat(32),
159                signature: "33".repeat(64),
160            },
161            valid_from: Utc.timestamp_opt(1_725_000_000, 123_456_789).unwrap(),
162            revoked_at: Some(Utc.timestamp_opt(1_735_000_000, 987_654_321).unwrap()),
163            delegated_from: Some(ContentHash::from_bytes([0x44; 32])),
164        };
165        let wire_binding = WireKeyBinding::from(&binding);
166        assert_eq!(wire_binding.liveness.revoked_at, binding.revoked_at);
167        assert_eq!(KeyBinding::from(wire_binding), binding);
168        let registry = KeyBindingRegistry::new(vec![binding]);
169
170        let wire = encode_key_binding_registry(&registry).expect("encode wire registry");
171        assert_eq!(wire.obj_type, ObjectType::KeyBinding);
172        assert_eq!(ObjectType::KeyBinding.wire_name(), "key_binding");
173        assert_eq!(
174            ObjectType::from_wire("key_binding").expect("parse key-binding type"),
175            ObjectType::KeyBinding
176        );
177        let encoded_frame = rmp_serde::to_vec_named(&wire).expect("encode object frame");
178        let transferred: ObjectData =
179            rmp_serde::from_slice(&encoded_frame).expect("decode object frame");
180        let decoded = decode_key_binding_registry(&transferred).expect("decode wire registry");
181
182        assert_eq!(decoded, registry);
183        assert_eq!(
184            decoded.content_hash().unwrap(),
185            registry.content_hash().unwrap()
186        );
187    }
188
189    #[test]
190    fn key_binding_registry_rejects_a_mismatched_content_address() {
191        let registry = KeyBindingRegistry::empty();
192        let mut wire = encode_key_binding_registry(&registry).expect("encode wire registry");
193        wire.id = crate::ObjectId::Hash(ContentHash::from_bytes([0x55; 32]));
194
195        let error = decode_key_binding_registry(&wire).expect_err("hash mismatch must fail closed");
196        assert!(error.to_string().contains("hash mismatch"));
197    }
198}