Skip to content

Commit

Permalink
ssh-key: preliminary certificate encoder support (#578)
Browse files Browse the repository at this point in the history
Support for encoding OpenSSH certificates from the `Certificate` type.
  • Loading branch information
tarcieri authored Apr 5, 2022
1 parent 8f565e3 commit c8a157b
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 21 deletions.
71 changes: 71 additions & 0 deletions ssh-key/src/certificate.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! OpenSSH certificate support.
use crate::{
checked::CheckedSum,
decoder::{Base64Decoder, Decode, Decoder},
encoder::{base64_encoded_len, Encode, Encoder},
public::{Encapsulation, KeyData},
CertificateAlg, Error, Result,
};
Expand Down Expand Up @@ -79,6 +81,28 @@ impl Certificate {
pub fn public_key(&self) -> &KeyData {
&self.public_key
}

/// Encode OpenSSH certificate to a [`String`].
pub fn to_string(&self) -> Result<String> {
let encoded_len = [
2, // interstitial spaces
self.algorithm().as_str().len(),
base64_encoded_len(self.encoded_len()?),
self.comment.len(),
]
.checked_sum()?;

let mut out = vec![0u8; encoded_len];
let actual_len = Encapsulation::encode(
&mut out,
self.algorithm().as_str(),
self.comment(),
|encoder| self.encode(encoder),
)?
.len();
out.truncate(actual_len);
Ok(String::from_utf8(out)?)
}
}

impl Decode for Certificate {
Expand Down Expand Up @@ -118,6 +142,53 @@ impl Decode for Certificate {
}
}

impl Encode for Certificate {
fn encoded_len(&self) -> Result<usize> {
[
self.algorithm.encoded_len()?,
4, // nonce length prefix (uint32)
self.nonce.len(),
self.public_key.encoded_key_data_len()?,
8, // serial (uint64)
4, // cert type (uint32)
4, // key id length prefix (uint32)
self.key_id.len(),
4, // valid principals length prefix (uint32)
self.valid_principals.len(),
8, // valid after (uint64)
8, // valid before (uint64)
4, // critical options length prefix (uint32)
self.critical_options.len(),
4, // extensions length prefix (uint32)
self.extensions.len(),
4, // reserved length prefix (uint32)
self.reserved.len(),
4, // signature key length prefix (uint32)
self.signature_key.len(),
4, // signature length prefix (uint32)
self.signature.len(),
]
.checked_sum()
}

fn encode(&self, encoder: &mut impl Encoder) -> Result<()> {
self.algorithm.encode(encoder)?;
encoder.encode_byte_slice(&self.nonce)?;
self.public_key.encode_key_data(encoder)?;
encoder.encode_u64(self.serial)?;
encoder.encode_u32(self.cert_type)?;
encoder.encode_str(&self.key_id)?;
encoder.encode_str(&self.valid_principals)?;
encoder.encode_u64(self.valid_after)?;
encoder.encode_u64(self.valid_before)?;
encoder.encode_str(&self.critical_options)?;
encoder.encode_str(&self.extensions)?;
encoder.encode_str(&self.reserved)?;
encoder.encode_byte_slice(&self.signature_key)?;
encoder.encode_byte_slice(&self.signature)
}
}

impl FromStr for Certificate {
type Err = Error;

Expand Down
10 changes: 10 additions & 0 deletions ssh-key/src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ pub(crate) trait Encoder: Sized {
self.encode_raw(&num.to_be_bytes())
}

/// Encode a `uint64` as described in [RFC4251 § 5]:
///
/// > Represents a 64-bit unsigned integer. Stored as eight bytes in
/// > the order of decreasing significance (network byte order).
///
/// [RFC4251 § 5]: https://datatracker.ietf.org/doc/html/rfc4251#section-5
fn encode_u64(&mut self, num: u64) -> Result<()> {
self.encode_raw(&num.to_be_bytes())
}

/// Encode a `usize` as a `uint32` as described in [RFC4251 § 5].
///
/// Uses [`Encoder::encode_u32`] after converting from a `usize`, handling
Expand Down
54 changes: 33 additions & 21 deletions ssh-key/src/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,33 +342,23 @@ impl KeyData {
_ => Err(Error::Algorithm),
}
}
}

impl Decode for KeyData {
fn decode(decoder: &mut impl Decoder) -> Result<Self> {
let algorithm = Algorithm::decode(decoder)?;
Self::decode_algorithm(decoder, algorithm)
}
}

impl Encode for KeyData {
fn encoded_len(&self) -> Result<usize> {
let key_len = match self {
/// Get the encoded length of this key data without a leading algorithm
/// identifier.
pub(crate) fn encoded_key_data_len(&self) -> Result<usize> {
match self {
#[cfg(feature = "alloc")]
Self::Dsa(key) => key.encoded_len()?,
Self::Dsa(key) => key.encoded_len(),
#[cfg(feature = "ecdsa")]
Self::Ecdsa(key) => key.encoded_len()?,
Self::Ed25519(key) => key.encoded_len()?,
Self::Ecdsa(key) => key.encoded_len(),
Self::Ed25519(key) => key.encoded_len(),
#[cfg(feature = "alloc")]
Self::Rsa(key) => key.encoded_len()?,
};

[self.algorithm().encoded_len()?, key_len].checked_sum()
Self::Rsa(key) => key.encoded_len(),
}
}

fn encode(&self, encoder: &mut impl Encoder) -> Result<()> {
self.algorithm().encode(encoder)?;

/// Encode the key data without a leading algorithm identifier.
pub(crate) fn encode_key_data(&self, encoder: &mut impl Encoder) -> Result<()> {
match self {
#[cfg(feature = "alloc")]
Self::Dsa(key) => key.encode(encoder),
Expand All @@ -380,3 +370,25 @@ impl Encode for KeyData {
}
}
}

impl Decode for KeyData {
fn decode(decoder: &mut impl Decoder) -> Result<Self> {
let algorithm = Algorithm::decode(decoder)?;
Self::decode_algorithm(decoder, algorithm)
}
}

impl Encode for KeyData {
fn encoded_len(&self) -> Result<usize> {
[
self.algorithm().encoded_len()?,
self.encoded_key_data_len()?,
]
.checked_sum()
}

fn encode(&self, encoder: &mut impl Encoder) -> Result<()> {
self.algorithm().encode(encoder)?;
self.encode_key_data(encoder)
}
}
28 changes: 28 additions & 0 deletions ssh-key/tests/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,31 @@ fn decode_rsa_4096_openssh() {

assert_eq!("[email protected]", key.comment());
}

#[test]
fn encode_dsa_openssh() {
let key = Certificate::from_str(DSA_CERT_EXAMPLE).unwrap();
assert_eq!(DSA_CERT_EXAMPLE.trim_end(), &key.to_string().unwrap());
}

#[cfg(feature = "ecdsa")]
#[test]
fn encode_ecdsa_p256_openssh() {
let key = Certificate::from_str(ECDSA_P256_CERT_EXAMPLE).unwrap();
assert_eq!(
ECDSA_P256_CERT_EXAMPLE.trim_end(),
&key.to_string().unwrap()
);
}

#[test]
fn encode_ed25519_openssh() {
let key = Certificate::from_str(ED25519_CERT_EXAMPLE).unwrap();
assert_eq!(ED25519_CERT_EXAMPLE.trim_end(), &key.to_string().unwrap());
}

#[test]
fn encode_rsa_4096_openssh() {
let key = Certificate::from_str(RSA_4096_CERT_EXAMPLE).unwrap();
assert_eq!(RSA_4096_CERT_EXAMPLE.trim_end(), &key.to_string().unwrap());
}

0 comments on commit c8a157b

Please sign in to comment.