ssh_agent_lib/proto/extension/
constraint.rs

1//! SSH agent protocol key constraint messages
2//!
3//! Includes extension message definitions from:
4//! - [OpenSSH `PROTOCOL.agent`](https://github.com/openssh/openssh-portable/blob/cbbdf868bce431a59e2fa36ca244d5739429408d/PROTOCOL.agent)
5
6use ssh_encoding::{CheckedSum, Decode, Encode, Error as EncodingError, Reader, Writer};
7use ssh_key::public::KeyData;
8
9use super::KeyConstraintExtension;
10
11// Reserved fields are marked with an empty string
12const RESERVED_FIELD: &str = "";
13
14/// `restrict-destination-v00@openssh.com` key constraint extension.
15///
16/// The key constraint extension supports destination- and forwarding path-
17/// restricted keys. It may be attached as a constraint when keys or
18/// smartcard keys are added to an agent.
19///
20/// *Note*: This is an OpenSSH-specific extension to the agent protocol.
21///
22/// Described in [OpenSSH PROTOCOL.agent § 2](https://github.com/openssh/openssh-portable/blob/cbbdf868bce431a59e2fa36ca244d5739429408d/PROTOCOL.agent#L38)
23#[derive(Debug, Clone, PartialEq)]
24pub struct RestrictDestination {
25    /// Set of constraints for the destination.
26    pub constraints: Vec<DestinationConstraint>,
27}
28
29impl Decode for RestrictDestination {
30    type Error = crate::proto::error::ProtoError;
31
32    fn decode(reader: &mut impl Reader) -> Result<Self, Self::Error> {
33        let mut constraints = Vec::new();
34        while !reader.is_finished() {
35            constraints.push(reader.read_prefixed(DestinationConstraint::decode)?);
36        }
37        Ok(Self { constraints })
38    }
39}
40
41impl Encode for RestrictDestination {
42    fn encoded_len(&self) -> ssh_encoding::Result<usize> {
43        self.constraints.iter().try_fold(0, |acc, e| {
44            let constraint_len = e.encoded_len_prefixed()?;
45            usize::checked_add(acc, constraint_len).ok_or(EncodingError::Length)
46        })
47    }
48
49    fn encode(&self, writer: &mut impl Writer) -> ssh_encoding::Result<()> {
50        for constraint in &self.constraints {
51            constraint.encode_prefixed(writer)?;
52        }
53        Ok(())
54    }
55}
56
57impl KeyConstraintExtension for RestrictDestination {
58    const NAME: &'static str = "restrict-destination-v00@openssh.com";
59}
60
61/// Tuple containing username and hostname with keys.
62///
63/// *Note*: This is an OpenSSH-specific extension to the agent protocol.
64///
65/// Described in [OpenSSH PROTOCOL.agent § 2](https://github.com/openssh/openssh-portable/blob/cbbdf868bce431a59e2fa36ca244d5739429408d/PROTOCOL.agent#L38)
66#[derive(Debug, Clone, PartialEq)]
67pub struct HostTuple {
68    /// Username part of the tuple.
69    pub username: String,
70
71    /// Hostname part of the tuple.
72    pub hostname: String,
73
74    /// Set of keys for the tuple.
75    pub keys: Vec<KeySpec>,
76}
77
78impl Decode for HostTuple {
79    type Error = crate::proto::error::ProtoError;
80
81    fn decode(reader: &mut impl Reader) -> Result<Self, Self::Error> {
82        let username = String::decode(reader)?;
83        let hostname = String::decode(reader)?;
84        let _reserved = String::decode(reader)?;
85
86        let mut keys = Vec::new();
87        while !reader.is_finished() {
88            keys.push(KeySpec::decode(reader)?);
89        }
90
91        Ok(Self {
92            username,
93            hostname,
94            keys,
95        })
96    }
97}
98
99impl Encode for HostTuple {
100    fn encoded_len(&self) -> ssh_encoding::Result<usize> {
101        let prefix = [
102            self.username.encoded_len()?,
103            self.hostname.encoded_len()?,
104            RESERVED_FIELD.encoded_len()?,
105        ]
106        .checked_sum()?;
107        self.keys.iter().try_fold(prefix, |acc, e| {
108            let key_len = e.encoded_len()?;
109            usize::checked_add(acc, key_len).ok_or(EncodingError::Length)
110        })
111    }
112
113    fn encode(&self, writer: &mut impl Writer) -> ssh_encoding::Result<()> {
114        self.username.encode(writer)?;
115        self.hostname.encode(writer)?;
116        RESERVED_FIELD.encode(writer)?;
117        for key in &self.keys {
118            key.encode(writer)?;
119        }
120        Ok(())
121    }
122}
123
124/// Key destination constraint.
125///
126/// One or more [`DestinationConstraint`]s are included in
127/// the [`RestrictDestination`] key constraint extension.
128///
129/// *Note*: This is an OpenSSH-specific extension to the agent protocol.
130///
131/// Described in [OpenSSH PROTOCOL.agent § 2](https://github.com/openssh/openssh-portable/blob/cbbdf868bce431a59e2fa36ca244d5739429408d/PROTOCOL.agent#L38)
132#[derive(Debug, Clone, PartialEq)]
133pub struct DestinationConstraint {
134    /// Constraint's `from` endpoint.
135    pub from: HostTuple,
136
137    /// Constraint's `to` endpoint.
138    pub to: HostTuple,
139}
140
141impl Decode for DestinationConstraint {
142    type Error = crate::proto::error::ProtoError;
143
144    fn decode(reader: &mut impl Reader) -> Result<Self, Self::Error> {
145        let from = reader.read_prefixed(HostTuple::decode)?;
146        let to = reader.read_prefixed(HostTuple::decode)?;
147        let _reserved = String::decode(reader)?;
148
149        Ok(Self { from, to })
150    }
151}
152
153impl Encode for DestinationConstraint {
154    fn encoded_len(&self) -> ssh_encoding::Result<usize> {
155        [
156            self.from.encoded_len_prefixed()?,
157            self.to.encoded_len_prefixed()?,
158            RESERVED_FIELD.encoded_len()?,
159        ]
160        .checked_sum()
161    }
162
163    fn encode(&self, writer: &mut impl Writer) -> ssh_encoding::Result<()> {
164        self.from.encode_prefixed(writer)?;
165        self.to.encode_prefixed(writer)?;
166        RESERVED_FIELD.encode(writer)?;
167        Ok(())
168    }
169}
170
171/// Public key specification.
172///
173/// This structure is included in [`DestinationConstraint`],
174/// which in turn is used in the [`RestrictDestination`] key
175/// constraint extension.
176///
177/// *Note*: This is an OpenSSH-specific extension to the agent protocol.
178///
179/// Described in [OpenSSH PROTOCOL.agent § 2](https://github.com/openssh/openssh-portable/blob/cbbdf868bce431a59e2fa36ca244d5739429408d/PROTOCOL.agent#L38)
180#[derive(Debug, Clone, PartialEq)]
181pub struct KeySpec {
182    /// The public parts of the key.
183    pub keyblob: KeyData,
184
185    /// Flag indicating if this key is for a CA.
186    pub is_ca: bool,
187}
188
189impl Decode for KeySpec {
190    type Error = crate::proto::error::ProtoError;
191
192    fn decode(reader: &mut impl Reader) -> Result<Self, Self::Error> {
193        let keyblob = reader.read_prefixed(KeyData::decode)?;
194        Ok(Self {
195            keyblob,
196            is_ca: u8::decode(reader)? != 0,
197        })
198    }
199}
200
201impl Encode for KeySpec {
202    fn encoded_len(&self) -> ssh_encoding::Result<usize> {
203        [self.keyblob.encoded_len_prefixed()?, 1u8.encoded_len()?].checked_sum()
204    }
205
206    fn encode(&self, writer: &mut impl Writer) -> ssh_encoding::Result<()> {
207        self.keyblob.encode_prefixed(writer)?;
208        // TODO: contribute `impl Encode for bool` in ssh-encoding
209        // <https://www.rfc-editor.org/rfc/rfc4251#section-5>
210        if self.is_ca {
211            1u8.encode(writer)
212        } else {
213            0u8.encode(writer)
214        }
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use hex_literal::hex;
221    use testresult::TestResult;
222
223    use super::*;
224    use crate::proto::ProtoError;
225
226    fn round_trip<T>(msg: T) -> TestResult
227    where
228        T: Encode + Decode<Error = ProtoError> + std::fmt::Debug + std::cmp::PartialEq,
229    {
230        let mut buf: Vec<u8> = vec![];
231        msg.encode(&mut buf)?;
232        let mut re_encoded = &buf[..];
233
234        let msg2 = T::decode(&mut re_encoded)?;
235        assert_eq!(msg, msg2);
236
237        Ok(())
238    }
239
240    #[test]
241    fn parse_destination_constraint() -> TestResult {
242        let mut msg = &hex!(
243            "                                    00
244            0002 6f00 0000 0c00 0000 0000 0000 0000
245            0000 0000 0002 5700 0000 0000 0000 0a67
246            6974 6875 622e 636f 6d00 0000 0000 0000
247            3300 0000 0b73 7368 2d65 6432 3535 3139
248            0000 0020 e32a aa79 15ce b9b4 49d1 ba50
249            ea2a 28bb 1a6e 01f9 0bda 245a 2d1d 8769
250            7d18 a265 0000 0001 9700 0000 0773 7368
251            2d72 7361 0000 0003 0100 0100 0001 8100
252            a3ee 774d c50a 3081 c427 8ec8 5c2e ba8f
253            1228 a986 7b7e 5534 ef0c fea6 1c12 fd8f
254            568d 5246 3851 ed60 bf09 c62d 594e 8467
255            98ae 765a 3204 4aeb e3ca 0945 da0d b0bb
256            aad6 d6f2 0224 84be da18 2b0e aff0 b9e9
257            224c cbf0 4265 fc5d d675 b300 ec52 0cf8
258            15b2 67ab 3816 1f36 a96d 57df e158 2a81
259            cb02 0d21 1fb9 7488 3a25 327b da97 04a4
260            48dc 6205 e413 6604 1575 7524 79ec 2a06
261            cb58 d961 49ca 9bd9 49b2 4644 32ca d44b
262            b4bf b7f1 31b1 9310 9f96 63be e59f 0249
263            2358 ec68 9d8c c219 ed0e 3332 3036 9f59
264            c6ae 54c3 933c 030a cc3e c2a1 4f19 0035
265            efd7 277c 658e 5915 6bba 3d7a cfa5 f2bf
266            1be3 2706 f3d3 0419 ef95 cae6 d292 6fb1
267            4dc9 e204 b384 d3e2 393e 4b87 613d e014
268            0b9c be6c 3622 ad88 0ce0 60bb b849 f3b6
269            7672 6955 90ec 1dfc d402 b841 daf0 b79d
270            59a8 4c4a 6d0a 5350 d9fe 123a a84f 0bea
271            363e 24ab 1e50 5022 344e 14bf 6243 b124
272            25e6 3d45 996e 18e9 0a0e 7a8b ed9a 07a0
273            a62b 6246 867e 7b2b 99a3 d0c3 5d05 7038
274            fd69 f01f a5e8 3d62 732b 9372 bb6c c1de
275            7019 a7e4 b986 942c fa9d 6f37 5ff0 b239
276            0000 0000 6800 0000 1365 6364 7361 2d73
277            6861 322d 6e69 7374 7032 3536 0000 0008
278            6e69 7374 7032 3536 0000 0041 0449 8a48
279            4363 4047 b33a 6c64 64cc bba2 92a0 c050
280            7d9e 4b79 611a d832 336e 1b93 7cee e460
281            83a0 8bad ba39 c007 53ff 2eaf d262 95d1
282            4db0 d166 7660 1ffe f93a 6872 4800 0000
283            0000"
284        )[..];
285
286        let destination_constraint = RestrictDestination::decode(&mut msg)?;
287        eprintln!("Destination constraint: {destination_constraint:?}");
288
289        round_trip(destination_constraint)?;
290
291        #[rustfmt::skip]
292        let mut buffer: &[u8] = const_str::concat_bytes!(
293            [0, 0, 0, 110], //
294            [0, 0, 0, 12], //from:
295            [0, 0, 0, 0], //username
296            [0, 0, 0, 0], //hostname
297            [0, 0, 0, 0], //reserved
298            // no host keys here
299            [0, 0, 0, 86], //to:
300            [0, 0, 0, 6], b"wiktor",
301            [0, 0, 0, 12], b"metacode.biz",
302            [0, 0, 0, 0], // reserved, not in the spec authfd.c:469
303            [0, 0, 0, 51], //
304            [0, 0, 0, 11], //
305            b"ssh-ed25519",
306            [0, 0, 0, 32], // raw key
307            [177, 185, 198, 92, 165, 45, 127, 95, 202, 195, 226, 63, 6, 115, 10, 104, 18, 137, 172,
308            240, 153, 154, 174, 74, 83, 7, 1, 204, 14, 177, 153, 40], //
309            [0],  // is_ca
310            [0, 0, 0, 0], // reserved, not in the spec, authfd.c:495
311        );
312
313        let destination_constraint = RestrictDestination::decode(&mut buffer)?;
314        eprintln!("Destination constraint: {destination_constraint:?}");
315
316        round_trip(destination_constraint)?;
317
318        let mut buffer: &[u8] = &[
319            0, 0, 0, 102, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 0, 0, 0, 0,
320            0, 0, 0, 10, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109, 0, 0, 0, 0, 0, 0, 0, 51, 0,
321            0, 0, 11, 115, 115, 104, 45, 101, 100, 50, 53, 53, 49, 57, 0, 0, 0, 32, 227, 42, 170,
322            121, 21, 206, 185, 180, 73, 209, 186, 80, 234, 42, 40, 187, 26, 110, 1, 249, 11, 218,
323            36, 90, 45, 29, 135, 105, 125, 24, 162, 101, 0, 0, 0, 0, 0,
324        ];
325        let destination_constraint = RestrictDestination::decode(&mut buffer)?;
326        eprintln!("Destination constraint: {destination_constraint:?}");
327
328        round_trip(destination_constraint)?;
329
330        Ok(())
331    }
332}