Skip to content

Commit a8470b3

Browse files
authored
ssh-key: decode Certificate when found in KeyData (#449)
When a certificate key type is found in SSH binary format being decoded by KeyData::decode, decode it into a Certificate, stored in a new variant of the KeyData enum. Previously, KeyData::decode attempted to decode certificates as an opaque key, which triggered overflow and/or encoding errors, because the OpaquePublicKeyBytes::decode function only handled the first length-prefixed field of the certificate (expecting only binary key data), and allowed the torn remainder of the certificate data to be handled by whatever function called `Reader::read` next. Added conversion to/from Certificate/KeyData, and integration tests for decoding a Certificate from wire-format binary public key. Signed-off-by: Ross Williams <ross@ross-williams.net>
1 parent 2e823a8 commit a8470b3

7 files changed

Lines changed: 119 additions & 15 deletions

File tree

ssh-key/src/certificate.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ mod unix_time;
99
pub use self::{builder::Builder, cert_type::CertType, field::Field, options_map::OptionsMap};
1010

1111
use crate::{
12-
Algorithm, Error, Fingerprint, HashAlg, Result, Signature,
12+
Algorithm, Comment, Error, Fingerprint, HashAlg, Result, Signature,
1313
public::{KeyData, SshFormat},
1414
};
1515
use alloc::{
16-
borrow::ToOwned,
1716
string::{String, ToString},
1817
vec::Vec,
1918
};
@@ -117,7 +116,7 @@ use {
117116
/// human-readable formats like JSON and TOML.
118117
///
119118
/// [PROTOCOL.certkeys]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
120-
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
119+
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
121120
pub struct Certificate {
122121
/// CA-provided random bitstring of arbitrary length
123122
/// (but typically 16 or 32 bytes).
@@ -160,7 +159,7 @@ pub struct Certificate {
160159
signature: Signature,
161160

162161
/// Comment on the certificate.
163-
comment: String,
162+
comment: Comment,
164163
}
165164

166165
impl Certificate {
@@ -182,7 +181,7 @@ impl Certificate {
182181
return Err(Error::AlgorithmUnknown);
183182
}
184183

185-
cert.comment = encapsulation.comment.to_owned();
184+
cert.comment = Comment::from(encapsulation.comment);
186185
Ok(reader.finish(cert)?)
187186
}
188187

@@ -229,7 +228,12 @@ impl Certificate {
229228

230229
/// Get the comment on this certificate.
231230
pub fn comment(&self) -> &str {
232-
self.comment.as_str()
231+
self.comment.as_str_lossy()
232+
}
233+
234+
/// Set the comment on this certificate.
235+
pub fn set_comment(&mut self, comment: impl Into<Comment>) {
236+
self.comment = comment.into();
233237
}
234238

235239
/// Nonces are a CA-provided random bitstring of arbitrary length
@@ -470,7 +474,7 @@ impl Certificate {
470474
reserved: Vec::decode(reader)?,
471475
signature_key: reader.read_prefixed(KeyData::decode)?,
472476
signature: reader.read_prefixed(Signature::decode)?,
473-
comment: String::new(),
477+
comment: Comment::default(),
474478
})
475479
}
476480
}

ssh-key/src/certificate/builder.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! OpenSSH certificate builder.
22
33
use super::{CertType, Certificate, Field, OptionsMap};
4-
use crate::{Result, Signature, SigningKey, public};
4+
use crate::{Comment, Result, Signature, SigningKey, public};
55
use alloc::{string::String, vec::Vec};
66

77
#[cfg(feature = "rand_core")]
@@ -88,7 +88,7 @@ pub struct Builder {
8888
valid_before: u64,
8989
critical_options: OptionsMap,
9090
extensions: OptionsMap,
91-
comment: Option<String>,
91+
comment: Option<Comment>,
9292
}
9393

9494
impl Builder {
@@ -255,7 +255,7 @@ impl Builder {
255255
/// Add a comment to this certificate.
256256
///
257257
/// Default `""`
258-
pub fn comment(&mut self, comment: impl Into<String>) -> Result<&mut Self> {
258+
pub fn comment(&mut self, comment: impl Into<Comment>) -> Result<&mut Self> {
259259
if self.comment.is_some() {
260260
return Err(Field::Comment.invalid_error());
261261
}

ssh-key/src/certificate/cert_type.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{Error, Result};
44
use encoding::{Decode, Encode, Reader, Writer};
55

66
/// Types of OpenSSH certificates: user or host.
7-
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
7+
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
88
#[repr(u32)]
99
#[derive(Default)]
1010
pub enum CertType {

ssh-key/src/certificate/options_map.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use core::{
99
use encoding::{CheckedSum, Decode, Encode, Reader, Writer};
1010

1111
/// Key/value map type used for certificate's critical options and extensions.
12-
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)]
12+
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
1313
pub struct OptionsMap(pub BTreeMap<String, String>);
1414

1515
impl OptionsMap {

ssh-key/src/public/key_data.rs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ use crate::{Algorithm, Error, Fingerprint, HashAlg, Result};
55
use encoding::{CheckedSum, Decode, Encode, Reader, Writer};
66

77
#[cfg(feature = "alloc")]
8-
use super::{DsaPublicKey, OpaquePublicKey, RsaPublicKey};
8+
use {
9+
super::{DsaPublicKey, OpaquePublicKey, RsaPublicKey},
10+
crate::Certificate,
11+
alloc::boxed::Box,
12+
};
913

1014
#[cfg(feature = "ecdsa")]
1115
use super::{EcdsaPublicKey, SkEcdsaSha2NistP256};
@@ -40,6 +44,14 @@ pub enum KeyData {
4044
/// [PROTOCOL.u2f]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.u2f?annotate=HEAD
4145
SkEd25519(SkEd25519),
4246

47+
/// Certificate key as specified in [draft-miller-ssh-cert]
48+
/// (and [PROTOCOL.certkeys], now deprecated in favor of the IETF draft).
49+
///
50+
/// [draft-miller-ssh-cert]: https://datatracker.ietf.org/doc/draft-miller-ssh-cert
51+
/// [PROTOCOL.certkeys]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
52+
#[cfg(feature = "alloc")]
53+
Certificate(Box<Certificate>),
54+
4355
/// Opaque public key data.
4456
#[cfg(feature = "alloc")]
4557
Other(OpaquePublicKey),
@@ -60,6 +72,8 @@ impl KeyData {
6072
Self::SkEcdsaSha2NistP256(_) => Algorithm::SkEcdsaSha2NistP256,
6173
Self::SkEd25519(_) => Algorithm::SkEd25519,
6274
#[cfg(feature = "alloc")]
75+
Self::Certificate(cert) => cert.algorithm(),
76+
#[cfg(feature = "alloc")]
6377
Self::Other(key) => key.algorithm(),
6478
}
6579
}
@@ -133,6 +147,24 @@ impl KeyData {
133147
}
134148
}
135149

150+
/// Get the certificate if this key is the correct type.
151+
#[cfg(feature = "alloc")]
152+
pub fn certificate(&self) -> Option<&Certificate> {
153+
match self {
154+
Self::Certificate(certificate) => Some(certificate.as_ref()),
155+
_ => None,
156+
}
157+
}
158+
159+
/// Get the certificate, consuming the [`KeyData`], if this key is the correct type.
160+
#[cfg(feature = "alloc")]
161+
pub fn into_certificate(self) -> Option<Certificate> {
162+
match self {
163+
Self::Certificate(certificate) => Some(*certificate),
164+
_ => None,
165+
}
166+
}
167+
136168
/// Is this key a DSA key?
137169
#[cfg(feature = "alloc")]
138170
pub fn is_dsa(&self) -> bool {
@@ -173,6 +205,12 @@ impl KeyData {
173205
matches!(self, Self::Other(_))
174206
}
175207

208+
/// Is this a certificate?
209+
#[cfg(feature = "alloc")]
210+
pub fn is_certificate(&self) -> bool {
211+
matches!(self, Self::Certificate(_))
212+
}
213+
176214
/// Decode [`KeyData`] for the specified algorithm.
177215
pub fn decode_as(reader: &mut impl Reader, algorithm: Algorithm) -> Result<Self> {
178216
match algorithm {
@@ -198,6 +236,12 @@ impl KeyData {
198236
}
199237
}
200238

239+
/// Decode [`KeyData`] as a certificate with the specified algorithm.
240+
#[cfg(feature = "alloc")]
241+
pub fn decode_as_certificate(reader: &mut impl Reader, algorithm: Algorithm) -> Result<Self> {
242+
Certificate::decode_as(reader, algorithm).map(|cert| Self::Certificate(Box::new(cert)))
243+
}
244+
201245
/// Get the encoded length of this key data without a leading algorithm
202246
/// identifier.
203247
pub(crate) fn encoded_key_data_len(&self) -> encoding::Result<usize> {
@@ -213,6 +257,8 @@ impl KeyData {
213257
Self::SkEcdsaSha2NistP256(sk) => sk.encoded_len(),
214258
Self::SkEd25519(sk) => sk.encoded_len(),
215259
#[cfg(feature = "alloc")]
260+
Self::Certificate(cert) => cert.encoded_len(),
261+
#[cfg(feature = "alloc")]
216262
Self::Other(other) => other.key.encoded_len(),
217263
}
218264
}
@@ -231,6 +277,8 @@ impl KeyData {
231277
Self::SkEcdsaSha2NistP256(sk) => sk.encode(writer),
232278
Self::SkEd25519(sk) => sk.encode(writer),
233279
#[cfg(feature = "alloc")]
280+
Self::Certificate(cert) => cert.encode(writer),
281+
#[cfg(feature = "alloc")]
234282
Self::Other(other) => other.key.encode(writer),
235283
}
236284
}
@@ -241,6 +289,12 @@ impl Decode for KeyData {
241289

242290
fn decode(reader: &mut impl Reader) -> Result<Self> {
243291
let algorithm = Algorithm::decode(reader)?;
292+
#[cfg(feature = "alloc")]
293+
if let Algorithm::Other(name) = &algorithm {
294+
if let Ok(certificate_algorithm) = Algorithm::new_certificate(name.as_str()) {
295+
return Self::decode_as_certificate(reader, certificate_algorithm);
296+
}
297+
}
244298
Self::decode_as(reader, algorithm)
245299
}
246300
}
@@ -299,3 +353,10 @@ impl From<SkEd25519> for KeyData {
299353
Self::SkEd25519(public_key)
300354
}
301355
}
356+
357+
#[cfg(feature = "alloc")]
358+
impl From<Certificate> for KeyData {
359+
fn from(certificate: Certificate) -> KeyData {
360+
Self::Certificate(Box::new(certificate))
361+
}
362+
}

ssh-key/src/signature.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ where
8383
/// [RFC5656]: https://datatracker.ietf.org/doc/html/rfc5656
8484
/// [RFC8032]: https://datatracker.ietf.org/doc/html/rfc8032
8585
/// [RFC8332]: https://datatracker.ietf.org/doc/html/rfc8332
86-
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord)]
86+
#[derive(Clone, Eq, Hash, PartialEq, PartialOrd, Ord)]
8787
pub struct Signature {
8888
/// Signature algorithm.
8989
algorithm: Algorithm,

ssh-key/tests/certificate.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
33
#![cfg(feature = "alloc")]
44

5+
use encoding::Decode;
56
use hex_literal::hex;
6-
use ssh_key::{Algorithm, Certificate};
7+
use ssh_key::{Algorithm, Certificate, public::KeyData};
78
use std::str::FromStr;
89

910
#[cfg(feature = "ecdsa")]
@@ -262,6 +263,44 @@ fn encode_rsa_4096_openssh() {
262263
);
263264
}
264265

266+
fn decode_keydata(certificate_str: &str) {
267+
// Decode certificate from OpenSSH format to bytes-on-wire
268+
let cert = Certificate::from_str(certificate_str).unwrap();
269+
let cert_encoded = cert.to_bytes().unwrap();
270+
271+
// Decode bytes-on-wire to KeyData
272+
let mut reader = &cert_encoded[..];
273+
let key_data = KeyData::decode(&mut reader).unwrap();
274+
275+
// Convert KeyData to Certificate and override comment from
276+
// OpenSSH encapsulation format so input and output match
277+
let mut cert_decoded = key_data.into_certificate().unwrap();
278+
cert_decoded.set_comment(cert.comment());
279+
280+
// Write Certificate to OpenSSH format and compare to input
281+
assert_eq!(
282+
certificate_str.trim_end(),
283+
&cert_decoded.to_openssh().unwrap()
284+
);
285+
}
286+
287+
#[cfg(feature = "ecdsa")]
288+
#[test]
289+
fn decode_ecdsa_keydata() {
290+
decode_keydata(ECDSA_P256_CERT_EXAMPLE)
291+
}
292+
293+
#[cfg(feature = "ed25519")]
294+
#[test]
295+
fn decode_ed25519_keydata() {
296+
decode_keydata(ED25519_CERT_EXAMPLE)
297+
}
298+
299+
#[test]
300+
fn decode_rsa_4096_keydata() {
301+
decode_keydata(RSA_4096_CERT_EXAMPLE)
302+
}
303+
265304
#[cfg(feature = "ed25519")]
266305
#[test]
267306
fn verify_ed25519_certificate_signature() {

0 commit comments

Comments
 (0)