Skip to content

Commit 77b8591

Browse files
committed
ssh-key: decode Certificate when found in KeyData
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 77b8591

File tree

5 files changed

+129
-7
lines changed

5 files changed

+129
-7
lines changed

ssh-key/src/certificate.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ use alloc::{
1717
string::{String, ToString},
1818
vec::Vec,
1919
};
20-
use core::str::FromStr;
20+
use core::{
21+
hash::{Hash, Hasher},
22+
str::FromStr,
23+
};
2124
use encoding::{Base64Reader, CheckedSum, Decode, Encode, Reader, Writer};
2225
use signature::Verifier;
2326

@@ -160,7 +163,7 @@ pub struct Certificate {
160163
signature: Signature,
161164

162165
/// Comment on the certificate.
163-
comment: String,
166+
pub comment: String,
164167
}
165168

166169
impl Certificate {
@@ -511,6 +514,23 @@ impl Encode for Certificate {
511514
}
512515
}
513516

517+
impl Hash for Certificate {
518+
#[inline]
519+
fn hash<H: Hasher>(&self, state: &mut H) {
520+
self.public_key.hash(state);
521+
self.serial.hash(state);
522+
self.cert_type.hash(state);
523+
self.key_id.hash(state);
524+
self.valid_principals.hash(state);
525+
self.valid_after.hash(state);
526+
self.valid_before.hash(state);
527+
self.critical_options.hash(state);
528+
self.extensions.hash(state);
529+
self.signature_key.hash(state);
530+
self.signature.hash(state);
531+
}
532+
}
533+
514534
impl FromStr for Certificate {
515535
type Err = Error;
516536

ssh-key/src/certificate/cert_type.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
//! OpenSSH certificate types.
22
3+
use core::hash::Hash;
4+
35
use crate::{Error, Result};
46
use encoding::{Decode, Encode, Reader, Writer};
57

68
/// Types of OpenSSH certificates: user or host.
7-
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
9+
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
810
#[repr(u32)]
911
#[derive(Default)]
1012
pub enum CertType {

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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use crate::{Algorithm, EcdsaCurve, Error, Mpint, PrivateKey, PublicKey, Result, private, public};
44
use alloc::vec::Vec;
5-
use core::fmt;
5+
use core::{fmt, hash::Hash};
66
use encoding::{CheckedSum, Decode, Encode, Reader, Writer};
77
use signature::{SignatureEncoding, Signer, Verifier};
88

@@ -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.comment = cert.comment().into();
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)