Workspacify

This commit is contained in:
2025-05-03 17:30:01 -07:00
parent 9fe5b78962
commit d036997de3
60 changed files with 450 additions and 212 deletions

23
keys/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "keys"
edition = "2024"
[dependencies]
aes = { workspace = true }
base64 = { workspace = true }
bcrypt-pbkdf = { workspace = true }
bytes = { workspace = true }
ctr = { workspace = true }
crypto = { workspace = true }
ed25519-dalek = { workspace = true }
elliptic-curve = { workspace = true }
error-stack = { workspace = true }
generic-array = { workspace = true }
num-bigint-dig = { workspace = true }
p256 = { workspace = true }
p384 = { workspace = true }
p521 = { workspace = true }
sec1 = { workspace = true }
ssh = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }

195
keys/src/buffer.rs Normal file
View File

@@ -0,0 +1,195 @@
use bytes::{Buf, Bytes};
use error_stack::report;
use thiserror::Error;
/// A read-only buffer formatted according to the SSH standard.
///
/// Reads in this buffer are destructive, in that they will advance
/// an internal pointer, and thus generally require a mutable
/// reference.
pub struct SshReadBuffer<B> {
buffer: B,
}
impl<B: Buf> From<B> for SshReadBuffer<B> {
fn from(buffer: B) -> Self {
SshReadBuffer { buffer }
}
}
#[derive(Debug, Error, PartialEq)]
pub enum SshReadError {
#[error("Attempt to read byte off the end of the buffer")]
CouldNotReadU8,
#[error("Not enough data left in SSH buffer to read a u32 ({remaining} bytes left)")]
CouldNotReadU32 { remaining: usize },
#[error("Not enough data left to read length from SSH buffer ({remaining} bytes left)")]
CouldNotReadLength { remaining: usize },
#[error("Encountered truncated SSH buffer; needed {target} bytes, but only had {remaining}")]
TruncatedBuffer { target: usize, remaining: usize },
#[error("Invalid string in SSH buffer: {error}")]
StringFormatting { error: std::string::FromUtf8Error },
}
impl<B: Buf> SshReadBuffer<B> {
/// Try to read a single byte from the buffer, advancing the pointer.
pub fn get_u8(&mut self) -> error_stack::Result<u8, SshReadError> {
if self.buffer.has_remaining() {
Ok(self.buffer.get_u8())
} else {
Err(report!(SshReadError::CouldNotReadU8))
}
}
/// Read a u32 from the buffer, advancing the pointer
pub fn get_u32(&mut self) -> error_stack::Result<u32, SshReadError> {
let remaining = self.buffer.remaining();
if remaining < 4 {
return Err(report!(SshReadError::CouldNotReadU32 { remaining }));
}
Ok(self.buffer.get_u32())
}
/// Read the next chunk of bytes out of the read buffer.
pub fn get_bytes(&mut self) -> error_stack::Result<Bytes, SshReadError> {
let remaining = self.buffer.remaining();
if remaining < 4 {
return Err(report!(SshReadError::CouldNotReadLength { remaining }));
}
let length = self.buffer.get_u32() as usize;
if length > (remaining - 4) {
return Err(report!(SshReadError::TruncatedBuffer {
target: length,
remaining: self.buffer.remaining(),
}));
}
Ok(self.buffer.copy_to_bytes(length))
}
/// Read the next string from the read buffer.
pub fn get_string(&mut self) -> error_stack::Result<String, SshReadError> {
let bytes = self.get_bytes()?.to_vec();
let string =
String::from_utf8(bytes).map_err(|error| SshReadError::StringFormatting { error })?;
Ok(string)
}
/// Returns true iff there is still data available for reading in the underlying
/// buffer.
pub fn has_remaining(&self) -> bool {
self.buffer.has_remaining()
}
/// Returns the number of bytes remaining in the buffer
pub fn remaining(&self) -> usize {
self.buffer.remaining()
}
}
#[test]
fn empty_gets_error_properly() {
let mut empty: SshReadBuffer<Bytes> = Bytes::new().into();
assert_eq!(
&SshReadError::CouldNotReadU32 { remaining: 0 },
empty.get_u32().unwrap_err().current_context()
);
assert_eq!(
&SshReadError::CouldNotReadLength { remaining: 0 },
empty.get_bytes().unwrap_err().current_context()
);
}
#[test]
fn short_read_errors_properly() {
let mut short: SshReadBuffer<Bytes> = Bytes::from(vec![0]).into();
assert_eq!(
&SshReadError::CouldNotReadU32 { remaining: 1 },
short.get_u32().unwrap_err().current_context()
);
assert_eq!(
&SshReadError::CouldNotReadLength { remaining: 1 },
short.get_bytes().unwrap_err().current_context()
);
}
#[test]
fn truncated_read_errors_properly() {
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![0, 0, 0, 5, 2, 3]).into();
assert_eq!(
&SshReadError::TruncatedBuffer {
target: 5,
remaining: 2
},
buffer.get_bytes().unwrap_err().current_context()
);
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![0, 0, 0, 5, 2, 3]).into();
assert_eq!(
&SshReadError::TruncatedBuffer {
target: 5,
remaining: 2
},
buffer.get_string().unwrap_err().current_context()
);
}
#[test]
fn bad_string_errors_properly() {
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![0, 0, 0, 2, 0xC3, 0x28]).into();
assert!(matches!(
buffer.get_string().unwrap_err().current_context(),
SshReadError::StringFormatting { .. },
));
}
#[test]
fn normal_reads_work() {
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![0, 0, 0, 2, 2, 3]).into();
assert_eq!(2, buffer.get_u32().unwrap());
assert!(buffer.has_remaining());
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![0, 0, 0, 5, 1, 2, 3, 4, 5]).into();
assert_eq!(
Bytes::from(vec![1, 2, 3, 4, 5]),
buffer.get_bytes().unwrap()
);
assert!(!buffer.has_remaining());
let mut buffer: SshReadBuffer<Bytes> =
Bytes::from(vec![0, 0, 0, 5, 0x48, 0x65, 0x6c, 0x6c, 0x6f]).into();
assert_eq!("Hello".to_string(), buffer.get_bytes().unwrap());
assert!(!buffer.has_remaining());
}
#[test]
fn sequential_reads_work() {
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![
0, 1, 0, 5, 0, 0, 0, 2, 2, 3, 0, 0, 0, 3, 0x66, 0x6f, 0x6f,
])
.into();
assert_eq!(65541, buffer.get_u32().unwrap());
assert_eq!(Bytes::from(vec![2, 3]), buffer.get_bytes().unwrap());
assert_eq!("foo".to_string(), buffer.get_string().unwrap());
assert!(!buffer.has_remaining());
}
#[test]
fn remaining_works() {
let mut buffer: SshReadBuffer<Bytes> = Bytes::from(vec![
0, 0, 0, 3, 0x66, 0x6f, 0x6f, 0, 0, 0, 3, 0x62, 0x61, 0x72,
])
.into();
assert_eq!("foo".to_string(), buffer.get_string().unwrap());
assert_eq!(7, buffer.remaining());
assert_eq!("bar".to_string(), buffer.get_string().unwrap());
}

10
keys/src/lib.rs Normal file
View File

@@ -0,0 +1,10 @@
pub mod buffer;
mod private_key;
mod private_key_file;
mod public_key;
mod public_key_file;
pub use self::private_key::PrivateKey;
pub use self::private_key_file::{load_openssh_file_keys, PrivateKeyLoadError};
pub use self::public_key::PublicKey;
pub use self::public_key_file::PublicKeyLoadError;

844
keys/src/private_key.rs Normal file
View File

@@ -0,0 +1,844 @@
use crypto::rsa;
use crate::buffer::SshReadBuffer;
use crate::public_key::PublicKey;
use bytes::Buf;
use elliptic_curve::scalar::ScalarPrimitive;
use elliptic_curve::sec1::FromEncodedPoint;
use error_stack::{report, ResultExt};
use num_bigint_dig::{BigInt, BigUint};
pub enum PrivateKey {
Rsa(rsa::PublicKey, rsa::PrivateKey),
P256(p256::PublicKey, p256::SecretKey),
P384(p384::PublicKey, p384::SecretKey),
P521(p521::PublicKey, p521::SecretKey),
Ed25519(ed25519_dalek::VerifyingKey, ed25519_dalek::SigningKey),
}
impl PrivateKey {
// Get a copy of the public key associated with this private key.
//
// This function does do a clone, so will have a memory impact, if that's
// important to you.
pub fn public(&self) -> PublicKey {
match self {
PrivateKey::Rsa(public, _) => PublicKey::Rsa(public.clone()),
PrivateKey::P256(public, _) => PublicKey::P256(*public),
PrivateKey::P384(public, _) => PublicKey::P384(*public),
PrivateKey::P521(public, _) => PublicKey::P521(*public),
PrivateKey::Ed25519(public, _) => PublicKey::Ed25519(*public),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum PrivateKeyReadError {
#[error("No private key type indicator found in alleged private key bytes.")]
NoKeyType,
#[error("Unknown key type for private key: '{key_type}'")]
UnrecognizedKeyType { key_type: String },
#[error("Invalid RSA key in private key bytes")]
BadRsaKey,
#[error("Could not find RSA private key constant {constant}")]
CouldNotFindRsaConstant { constant: &'static str },
#[error("Could not find curve name for private elliptic curve key")]
CouldNotFindCurveName,
#[error("Encoded curve '{curve}' does not match key type '{key}'")]
MismatchedKeyAndCurve { key: String, curve: String },
#[error("Could not read public point for {curve} curve")]
CouldNotReadPublicPoint { curve: String },
#[error("Could not read private scalar for {curve} curve")]
CouldNotReadPrivateScalar { curve: String },
#[error("Invalid scalar for {curve} curve")]
InvalidScalar { curve: String },
#[error("Bad private scalar for {curve} curve: {error}")]
BadPrivateScalar {
curve: String,
error: elliptic_curve::Error,
},
#[error("INTERNAL ERROR: Got way too far with unknown curve {curve}")]
InternalError { curve: String },
#[error("Could not read ed25519 key's {part} data")]
CouldNotReadEd25519 { part: &'static str },
#[error("Invalid ed25519 {kind} key: {error}")]
InvalidEd25519Key { kind: &'static str, error: String },
#[error("Could not decode public point data in private key for {curve} curve: {error}")]
PointDecodeError { curve: String, error: sec1::Error },
#[error("Bad point for public key in curve {curve}")]
BadPointForPublicKey { curve: String },
}
pub fn read_private_key<B: Buf>(
ssh_buffer: &mut SshReadBuffer<B>,
) -> error_stack::Result<PrivateKey, PrivateKeyReadError> {
let encoded_key_type = ssh_buffer
.get_string()
.change_context(PrivateKeyReadError::NoKeyType)?;
match encoded_key_type.as_str() {
"ssh-rsa" => {
let n = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "n" })?;
let e = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "e" })?;
let d = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "d" })?;
let qinv = ssh_buffer.get_bytes().change_context(
PrivateKeyReadError::CouldNotFindRsaConstant { constant: "q⁻¹" },
)?;
let p = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "p" })?;
let q = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotFindRsaConstant { constant: "q" })?;
let n = BigUint::from_bytes_be(&n);
let e = BigUint::from_bytes_be(&e);
let d = BigUint::from_bytes_be(&d);
let qinv = BigInt::from_bytes_be(num_bigint_dig::Sign::Plus, &qinv);
let p = BigUint::from_bytes_be(&p);
let q = BigUint::from_bytes_be(&q);
let public_key = rsa::PublicKey::new(n, e);
let private_key = rsa::PrivateKey::from_parts(&public_key, d, qinv, p, q)
.change_context(PrivateKeyReadError::BadRsaKey)?;
Ok(PrivateKey::Rsa(public_key, private_key))
}
"ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521" => {
let curve = ssh_buffer
.get_string()
.change_context(PrivateKeyReadError::CouldNotFindCurveName)?;
let max_scalar_byte_length = match (curve.as_str(), encoded_key_type.as_str()) {
("nistp256", "ecdsa-sha2-nistp256") => 32,
("nistp384", "ecdsa-sha2-nistp384") => 48,
("nistp521", "ecdsa-sha2-nistp521") => 66,
_ => {
return Err(report!(PrivateKeyReadError::MismatchedKeyAndCurve {
key: encoded_key_type,
curve,
}))
}
};
let public_key_bytes = ssh_buffer.get_bytes().change_context_lazy(|| {
PrivateKeyReadError::CouldNotReadPublicPoint {
curve: curve.clone(),
}
})?;
let mut scalar_bytes = ssh_buffer.get_bytes().change_context_lazy(|| {
PrivateKeyReadError::CouldNotReadPrivateScalar {
curve: curve.clone(),
}
})?;
while scalar_bytes.remaining() > max_scalar_byte_length {
let zero = scalar_bytes.get_u8();
if zero != 0 {
return Err(report!(PrivateKeyReadError::InvalidScalar { curve }));
}
}
match curve.as_str() {
"nistp256" => {
let public_point =
p256::EncodedPoint::from_bytes(&public_key_bytes).map_err(|error| {
report!(PrivateKeyReadError::PointDecodeError {
curve: curve.clone(),
error
})
})?;
let public = p256::PublicKey::from_encoded_point(&public_point)
.into_option()
.ok_or_else(|| {
report!(PrivateKeyReadError::BadPointForPublicKey {
curve: curve.clone(),
})
})?;
let scalar = ScalarPrimitive::from_slice(&scalar_bytes).map_err(|error| {
report!(PrivateKeyReadError::BadPrivateScalar { curve, error })
})?;
let private = p256::SecretKey::new(scalar);
Ok(PrivateKey::P256(public, private))
}
"nistp384" => {
let public_point =
p384::EncodedPoint::from_bytes(&public_key_bytes).map_err(|error| {
report!(PrivateKeyReadError::PointDecodeError {
curve: curve.clone(),
error
})
})?;
let public = p384::PublicKey::from_encoded_point(&public_point)
.into_option()
.ok_or_else(|| {
report!(PrivateKeyReadError::BadPointForPublicKey {
curve: curve.clone(),
})
})?;
let scalar = ScalarPrimitive::from_slice(&scalar_bytes).map_err(|error| {
report!(PrivateKeyReadError::BadPrivateScalar { curve, error })
})?;
let private = p384::SecretKey::new(scalar);
Ok(PrivateKey::P384(public, private))
}
"nistp521" => {
let public_point =
p521::EncodedPoint::from_bytes(&public_key_bytes).map_err(|error| {
report!(PrivateKeyReadError::PointDecodeError {
curve: curve.clone(),
error
})
})?;
let public = p521::PublicKey::from_encoded_point(&public_point)
.into_option()
.ok_or_else(|| {
report!(PrivateKeyReadError::BadPointForPublicKey {
curve: curve.clone(),
})
})?;
let scalar = ScalarPrimitive::from_slice(&scalar_bytes).map_err(|error| {
report!(PrivateKeyReadError::BadPrivateScalar { curve, error })
})?;
let private = p521::SecretKey::new(scalar);
Ok(PrivateKey::P521(public, private))
}
_ => Err(report!(PrivateKeyReadError::InternalError { curve })),
}
}
"ssh-ed25519" => {
let public_bytes = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotReadEd25519 { part: "public" })?;
let mut private_bytes = ssh_buffer
.get_bytes()
.change_context(PrivateKeyReadError::CouldNotReadEd25519 { part: "private" })?;
let public_key =
ed25519_dalek::VerifyingKey::try_from(public_bytes.as_ref()).map_err(|error| {
report!(PrivateKeyReadError::InvalidEd25519Key {
kind: "public",
error: format!("{}", error),
})
})?;
if private_bytes.remaining() != 64 {
return Err(report!(PrivateKeyReadError::InvalidEd25519Key {
kind: "private",
error: format!(
"key should be 64 bytes long, saw {}",
private_bytes.remaining()
),
}));
}
let mut private_key = [0; 64];
private_bytes.copy_to_slice(&mut private_key);
let private_key =
ed25519_dalek::SigningKey::from_keypair_bytes(&private_key).map_err(|error| {
report!(PrivateKeyReadError::InvalidEd25519Key {
kind: "private",
error: format!("final load error: {}", error),
})
})?;
Ok(PrivateKey::Ed25519(public_key, private_key))
}
_ => Err(report!(PrivateKeyReadError::UnrecognizedKeyType {
key_type: encoded_key_type,
})),
}
}
#[cfg(test)]
const RSA_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61, 0x00, 0x00, 0x02, 0x01, 0x00,
0xb7, 0x7e, 0xd2, 0x53, 0xf0, 0x92, 0xac, 0x06, 0x53, 0x07, 0x8f, 0xe9, 0x89, 0xd8, 0x92, 0xd4,
0x08, 0x7e, 0xdd, 0x6b, 0xa4, 0x67, 0xd8, 0xac, 0x4a, 0x3b, 0x8f, 0xbd, 0x2f, 0x3a, 0x19, 0x46,
0x7c, 0xa5, 0x7f, 0xc1, 0x01, 0xee, 0xe3, 0xbf, 0x9e, 0xaf, 0xed, 0xc8, 0xbc, 0x8c, 0x30, 0x70,
0x6f, 0xf1, 0xdd, 0xb9, 0x9b, 0x4c, 0x67, 0x7b, 0x8f, 0x7c, 0xcf, 0x85, 0x6f, 0x28, 0x5f, 0xeb,
0xe3, 0x0b, 0x7f, 0x82, 0xf5, 0xa4, 0x99, 0xc6, 0xae, 0x1c, 0xbd, 0xd6, 0xa9, 0x34, 0xc9, 0x05,
0xfc, 0xdc, 0xe2, 0x84, 0x86, 0x69, 0xc5, 0x6b, 0x0a, 0xf5, 0x17, 0x5f, 0x52, 0xda, 0x4a, 0xdf,
0xd9, 0x4a, 0xe2, 0x14, 0x0c, 0xba, 0x96, 0x04, 0x4e, 0x25, 0x38, 0xd1, 0x66, 0x75, 0xf2, 0x27,
0x68, 0x1f, 0x28, 0xce, 0xa5, 0xa3, 0x22, 0x05, 0xf7, 0x9e, 0x38, 0x70, 0xf7, 0x23, 0x65, 0xfe,
0x4e, 0x77, 0x66, 0x70, 0x16, 0x89, 0xa3, 0xa7, 0x1b, 0xbd, 0x6d, 0x94, 0x85, 0xa1, 0x6b, 0xe8,
0xf1, 0xb9, 0xb6, 0x7f, 0x4f, 0xb4, 0x53, 0xa7, 0xfe, 0x2d, 0x89, 0x6a, 0x6e, 0x6d, 0x63, 0x85,
0xe1, 0x00, 0x83, 0x01, 0xb0, 0x00, 0x8a, 0x30, 0xde, 0xdc, 0x2f, 0x30, 0xbc, 0x89, 0x66, 0x2a,
0x28, 0x59, 0x31, 0xd9, 0x74, 0x9c, 0xf2, 0xf1, 0xd7, 0x53, 0xa9, 0x7b, 0xeb, 0x97, 0xfd, 0x53,
0x13, 0x66, 0x59, 0x9d, 0x61, 0x4a, 0x72, 0xf4, 0xa9, 0x22, 0xc8, 0xac, 0x0e, 0xd8, 0x0e, 0x4f,
0x15, 0x59, 0x9b, 0xaa, 0x96, 0xf9, 0xd5, 0x61, 0xd5, 0x04, 0x4c, 0x09, 0x0d, 0x5a, 0x4e, 0x39,
0xd6, 0xbe, 0x16, 0x8c, 0x36, 0xe1, 0x1d, 0x59, 0x5a, 0xa5, 0x5c, 0x50, 0x6b, 0x6f, 0x6a, 0xed,
0x63, 0x04, 0xbc, 0x42, 0xec, 0xcb, 0xea, 0x34, 0xfc, 0x75, 0xcc, 0xd1, 0xca, 0x45, 0x66, 0xd0,
0xc9, 0x14, 0xae, 0x83, 0xd0, 0x7c, 0x0e, 0x06, 0x1d, 0x4f, 0x15, 0x64, 0x53, 0x56, 0xdb, 0xf2,
0x49, 0x83, 0x03, 0xae, 0xda, 0xa7, 0x29, 0x7c, 0x42, 0xbf, 0x82, 0x07, 0xbc, 0x44, 0x09, 0x15,
0x32, 0x4d, 0xc0, 0xdf, 0x8a, 0x04, 0x89, 0xd9, 0xd8, 0xdb, 0x05, 0xa5, 0x60, 0x21, 0xed, 0xcb,
0x54, 0x74, 0x1e, 0x24, 0x06, 0x4d, 0x69, 0x93, 0x72, 0xe8, 0x59, 0xe1, 0x93, 0x1a, 0x6e, 0x48,
0x16, 0x31, 0x38, 0x10, 0x0e, 0x0b, 0x34, 0xeb, 0x20, 0x86, 0x9c, 0x60, 0x68, 0xaf, 0x30, 0x5e,
0x7f, 0x26, 0x37, 0xce, 0xd9, 0xc1, 0x47, 0xdf, 0x2d, 0xba, 0x50, 0x96, 0xcf, 0xf8, 0xf5, 0xe8,
0x65, 0x26, 0x18, 0x4a, 0x88, 0xe0, 0xd8, 0xab, 0x24, 0xde, 0x3f, 0xa9, 0x64, 0x94, 0xe3, 0xaf,
0x7b, 0x43, 0xaa, 0x72, 0x64, 0x7c, 0xef, 0xdb, 0x30, 0x87, 0x7d, 0x70, 0xd7, 0xbe, 0x0a, 0xca,
0x79, 0xe6, 0xb8, 0x3e, 0x23, 0x37, 0x17, 0x7d, 0x0c, 0x41, 0x3d, 0xd9, 0x92, 0xd6, 0x8c, 0x95,
0x8b, 0x63, 0x0b, 0x63, 0x49, 0x98, 0x0f, 0x1f, 0xc1, 0x95, 0x94, 0x6f, 0x22, 0x0e, 0x47, 0x8f,
0xee, 0x12, 0xb9, 0x8e, 0x28, 0xc2, 0x94, 0xa2, 0xd4, 0x0a, 0x79, 0x69, 0x93, 0x8a, 0x6f, 0xf4,
0xae, 0xd1, 0x85, 0x11, 0xbb, 0x6c, 0xd5, 0x41, 0x00, 0x71, 0x9b, 0x24, 0xe4, 0x6d, 0x0a, 0x05,
0x07, 0x4c, 0x28, 0xa6, 0x88, 0x8c, 0xea, 0x74, 0x19, 0x64, 0x26, 0x5a, 0xc8, 0x28, 0xcc, 0xdf,
0xa8, 0xea, 0xa7, 0xda, 0xec, 0x03, 0xcd, 0xcb, 0xf3, 0xd7, 0x6b, 0xb6, 0x4a, 0xd8, 0x50, 0x44,
0x91, 0xde, 0xb2, 0x76, 0x6e, 0x85, 0x21, 0x4b, 0x2f, 0x65, 0x57, 0x76, 0xd3, 0xd9, 0xfa, 0xd2,
0x98, 0xcb, 0x47, 0xaa, 0x33, 0x69, 0x4e, 0x83, 0x75, 0xfe, 0x8e, 0xac, 0x0a, 0xf6, 0xb6, 0xb7,
0x00, 0x00, 0x00, 0x03, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x54, 0x94, 0xd7, 0xdc, 0xab,
0x5a, 0xe0, 0x82, 0xb5, 0xc9, 0x19, 0x94, 0x1b, 0xdf, 0x41, 0xa7, 0x0d, 0x17, 0x75, 0x77, 0x05,
0xbc, 0x7c, 0x8a, 0xc6, 0x58, 0xf8, 0x23, 0xcb, 0x5e, 0x2b, 0x82, 0x6b, 0x38, 0x5a, 0x50, 0x1c,
0x55, 0x02, 0x94, 0x34, 0x50, 0x81, 0xf9, 0xf2, 0xb7, 0x68, 0x28, 0x9b, 0xe1, 0x50, 0x44, 0x1b,
0x0a, 0xb7, 0xf4, 0xa3, 0xaa, 0x73, 0x79, 0xdd, 0x48, 0x2e, 0x16, 0xec, 0x7c, 0x43, 0x55, 0x99,
0x67, 0x3b, 0x1e, 0xf2, 0xe8, 0xfa, 0xb4, 0xb5, 0x20, 0x48, 0xbd, 0x42, 0xd6, 0x8a, 0x6f, 0x6e,
0x09, 0xd9, 0x5f, 0x43, 0x18, 0xc0, 0xa2, 0x46, 0xed, 0xaa, 0x6f, 0xce, 0x98, 0x8e, 0xe7, 0x91,
0x0a, 0x7c, 0xd6, 0x15, 0x33, 0x61, 0x22, 0x5c, 0xe9, 0x67, 0x2a, 0xb4, 0xfb, 0x0f, 0xf3, 0x59,
0x34, 0x7e, 0x1d, 0x64, 0x0b, 0x81, 0x96, 0xc8, 0xc4, 0x7f, 0x62, 0x1e, 0xc7, 0x38, 0xe7, 0xd7,
0xeb, 0xb0, 0x0c, 0xfa, 0x63, 0x71, 0xdc, 0x71, 0x50, 0x7c, 0x0e, 0x4f, 0x46, 0x3c, 0x92, 0x28,
0xaa, 0x45, 0x99, 0x7d, 0x37, 0x7e, 0x4d, 0x1a, 0x03, 0xc0, 0x49, 0x58, 0xf2, 0xc4, 0x70, 0x85,
0xb1, 0x6a, 0x01, 0xa6, 0xe8, 0xb5, 0xb3, 0xf0, 0x64, 0x21, 0x3c, 0xb3, 0x86, 0x91, 0xcc, 0xdb,
0xcc, 0xf0, 0xcb, 0x7b, 0x66, 0xec, 0x0b, 0xdc, 0x08, 0x1e, 0x54, 0x29, 0xf0, 0x16, 0xc4, 0xcd,
0xb0, 0xe4, 0x96, 0x54, 0x54, 0x5d, 0x4d, 0xba, 0x35, 0xeb, 0x3a, 0x96, 0xeb, 0xcc, 0x2e, 0x71,
0x13, 0x4e, 0x41, 0x9f, 0x50, 0x30, 0xc0, 0x47, 0x70, 0x65, 0xf8, 0x91, 0x3c, 0xe3, 0xe5, 0xd3,
0xf2, 0x26, 0x76, 0x26, 0xab, 0x6c, 0x87, 0x01, 0x4e, 0xc5, 0x6a, 0x11, 0x27, 0x80, 0xa4, 0x14,
0xc4, 0xd5, 0xfb, 0x80, 0x97, 0xc8, 0x46, 0xb7, 0xc7, 0x0f, 0xe1, 0xca, 0x95, 0x2b, 0x9d, 0x0c,
0x3b, 0x56, 0x61, 0xe4, 0x39, 0x37, 0xef, 0xeb, 0x3e, 0xcc, 0x72, 0x0b, 0x52, 0x1d, 0xea, 0x39,
0x8a, 0x59, 0x46, 0x78, 0xb0, 0x98, 0xe2, 0xfe, 0x7f, 0xe3, 0x40, 0x81, 0x66, 0x35, 0x1f, 0x8e,
0x75, 0x2a, 0x2f, 0xb7, 0x0d, 0x37, 0x6a, 0x71, 0x8d, 0xb3, 0xef, 0xe1, 0x5c, 0x5d, 0x20, 0xf4,
0xf6, 0x59, 0x1c, 0x75, 0x83, 0x1a, 0xfa, 0x4f, 0x80, 0x72, 0xb6, 0x50, 0x6f, 0xfc, 0xb3, 0x70,
0xcb, 0x68, 0x8a, 0xb4, 0x06, 0x02, 0x3f, 0x33, 0xa4, 0x0e, 0x05, 0xd9, 0x25, 0xeb, 0x7e, 0x35,
0x24, 0xd2, 0x47, 0x64, 0x07, 0x4e, 0xf5, 0x65, 0x4e, 0x16, 0xcf, 0xaa, 0xfe, 0x4a, 0xbe, 0xc3,
0xb7, 0x7a, 0xd4, 0xaa, 0xf1, 0x24, 0x56, 0x60, 0xc8, 0x24, 0x07, 0x03, 0x01, 0xfd, 0x3d, 0x18,
0xec, 0x09, 0x1c, 0xec, 0x63, 0x74, 0x5b, 0xe7, 0x1b, 0x5c, 0x52, 0x30, 0x09, 0x00, 0xa6, 0xbf,
0x6b, 0x46, 0x26, 0xdf, 0x8c, 0x87, 0xde, 0x48, 0x42, 0x29, 0x48, 0x78, 0x55, 0xfb, 0x51, 0xda,
0xe3, 0x82, 0xfc, 0xfd, 0x21, 0x58, 0xb9, 0x7b, 0x17, 0xd0, 0x0a, 0x6a, 0xeb, 0x5a, 0xce, 0xdc,
0x71, 0x10, 0x03, 0xe4, 0x6b, 0x14, 0x4e, 0xda, 0x4e, 0xad, 0x9d, 0xa7, 0x63, 0x6f, 0x71, 0x23,
0xf6, 0x43, 0x0b, 0x43, 0x31, 0x71, 0xfb, 0x7e, 0x8d, 0x49, 0x0c, 0x1e, 0x37, 0x3d, 0x52, 0xad,
0xdb, 0xb7, 0x3a, 0x53, 0x13, 0xf7, 0x64, 0x4d, 0x3a, 0xf5, 0x6b, 0x45, 0x2d, 0xd3, 0xe0, 0x80,
0x16, 0xd5, 0xf4, 0x88, 0x2e, 0xbd, 0xc2, 0x23, 0x35, 0xe9, 0x73, 0xfa, 0x4c, 0x49, 0x63, 0x69,
0x8c, 0x60, 0x6d, 0x21, 0xdf, 0x9b, 0xff, 0xbf, 0xcc, 0xbc, 0x0f, 0xfa, 0x07, 0xa7, 0x6a, 0xcd,
0x43, 0x5b, 0xd5, 0xa3, 0x75, 0x16, 0xa2, 0x9a, 0x10, 0x70, 0x79, 0x00, 0x00, 0x01, 0x01, 0x00,
0xc8, 0xcd, 0xa4, 0x89, 0xf0, 0x84, 0x21, 0x20, 0x16, 0x54, 0x63, 0xa4, 0x1b, 0xcc, 0x68, 0xb9,
0x4e, 0x46, 0x1a, 0xdc, 0xb1, 0x8a, 0x32, 0x24, 0xae, 0x1c, 0xa7, 0x1c, 0x77, 0xfb, 0xd8, 0x37,
0xa4, 0x5b, 0x3c, 0x98, 0x96, 0xd5, 0x11, 0xe0, 0x45, 0xc7, 0xa1, 0xfb, 0xc3, 0x6d, 0x08, 0xf4,
0x0d, 0xf8, 0x13, 0x63, 0x50, 0xf3, 0x93, 0x71, 0x25, 0x47, 0x99, 0xe5, 0x80, 0x3e, 0x62, 0x43,
0x77, 0x3d, 0x58, 0x49, 0xc8, 0x4d, 0xae, 0xb0, 0x2f, 0x3c, 0x5e, 0x08, 0x97, 0x3a, 0xc7, 0x5f,
0x89, 0x3c, 0x44, 0xf0, 0xaa, 0xe9, 0xeb, 0xf4, 0x9a, 0x2d, 0x5c, 0xd4, 0xa7, 0x26, 0xaa, 0xd5,
0x18, 0xec, 0xd9, 0xc9, 0x0f, 0xde, 0xcd, 0xcc, 0xbd, 0xe4, 0xa3, 0x62, 0xed, 0xc0, 0x89, 0xa9,
0x19, 0xb4, 0x4e, 0xc7, 0x89, 0xf9, 0x2f, 0x2a, 0x39, 0x71, 0xfb, 0x00, 0xf8, 0x54, 0x45, 0x73,
0xfe, 0x77, 0x96, 0x32, 0x5a, 0xee, 0xf5, 0x53, 0xc2, 0x62, 0x13, 0x6d, 0x2d, 0x9d, 0x7e, 0xf6,
0x09, 0xf2, 0xd6, 0xf5, 0xb5, 0x32, 0x67, 0x3c, 0x4d, 0xf7, 0x02, 0x45, 0xf7, 0x61, 0x9b, 0x5a,
0x4e, 0x67, 0x2c, 0x7c, 0xeb, 0x2d, 0xde, 0x34, 0xa8, 0xc7, 0xfe, 0x1c, 0x4d, 0x0f, 0x99, 0x13,
0xe2, 0xef, 0x3d, 0x0b, 0xf3, 0x05, 0x79, 0x9d, 0x79, 0x7c, 0x70, 0xda, 0xfe, 0xb8, 0xea, 0x5d,
0xa0, 0x9d, 0x3c, 0xea, 0xc6, 0xe2, 0xc3, 0x9c, 0x42, 0x67, 0xba, 0x0b, 0x78, 0x68, 0xae, 0x5d,
0x49, 0xd1, 0x61, 0x6f, 0xe9, 0x7f, 0x84, 0x51, 0x38, 0x7d, 0x29, 0xfb, 0x9a, 0x3e, 0x06, 0x9d,
0xc1, 0x48, 0xe8, 0xb3, 0xff, 0xf3, 0x1e, 0x10, 0xec, 0x85, 0x99, 0xb5, 0x8b, 0xdd, 0xa6, 0xd6,
0xce, 0xe3, 0x92, 0x3f, 0x74, 0x50, 0x45, 0xc1, 0x80, 0xc3, 0x3b, 0x3e, 0x87, 0xd8, 0x34, 0xae,
0x00, 0x00, 0x01, 0x01, 0x00, 0xf2, 0x1c, 0x2d, 0x0f, 0xc1, 0x24, 0xd0, 0xd6, 0x88, 0xcb, 0x89,
0xb4, 0x73, 0xb6, 0x31, 0xfc, 0x19, 0x0d, 0x5c, 0x46, 0x7f, 0x9c, 0xbd, 0xd6, 0x51, 0x64, 0xd5,
0xaf, 0xbd, 0x0e, 0x40, 0xdb, 0x25, 0x5a, 0x8d, 0x65, 0xc6, 0xcd, 0x2a, 0x8f, 0x76, 0x8a, 0x24,
0x66, 0xe5, 0x7f, 0xf8, 0x3a, 0xb3, 0xf4, 0x7f, 0xf2, 0x8d, 0x55, 0xdc, 0x12, 0x29, 0xb5, 0x08,
0xa3, 0xae, 0xf2, 0xba, 0x69, 0xf8, 0x70, 0xb3, 0x5f, 0xab, 0x5b, 0x2f, 0x07, 0xf5, 0x88, 0xf4,
0x10, 0x4e, 0xbf, 0x40, 0x88, 0xe3, 0xc3, 0x6a, 0x5d, 0x76, 0xc7, 0xf2, 0xb7, 0xdb, 0xf4, 0xfc,
0x6c, 0xcf, 0x85, 0x88, 0xa8, 0x3b, 0x2b, 0x31, 0xfe, 0xc3, 0xc8, 0x33, 0x46, 0xaf, 0x5c, 0x74,
0x15, 0xf7, 0xdf, 0x30, 0x84, 0xb4, 0x4b, 0x42, 0xad, 0x4a, 0xb2, 0xb6, 0x1d, 0x8c, 0x94, 0x18,
0x10, 0x65, 0x27, 0x90, 0xea, 0x4e, 0x51, 0x6e, 0xe4, 0x7e, 0xaa, 0xb2, 0x04, 0x8a, 0x7b, 0xa0,
0x62, 0xef, 0x96, 0x1a, 0x13, 0x6e, 0x04, 0x0a, 0x76, 0x8d, 0xc7, 0x36, 0xf6, 0xb1, 0xc4, 0x70,
0x05, 0x3a, 0x7e, 0x55, 0xbe, 0xba, 0x6c, 0x7a, 0xa0, 0x53, 0x8f, 0xb2, 0x86, 0x96, 0xa5, 0x38,
0x56, 0x16, 0xd1, 0x9b, 0xf7, 0x3e, 0x51, 0x23, 0x4e, 0x01, 0x31, 0x55, 0x0f, 0x4c, 0x5e, 0x45,
0x3b, 0x41, 0x56, 0xfa, 0x3b, 0x4a, 0x09, 0x38, 0x28, 0xe9, 0x16, 0x68, 0xdb, 0x58, 0x49, 0xc3,
0x57, 0x7f, 0x42, 0x47, 0x76, 0xb9, 0x8d, 0x92, 0xf9, 0x3f, 0xb0, 0xf3, 0x1c, 0xbe, 0x0d, 0xea,
0xcf, 0xf9, 0x97, 0xf6, 0x94, 0xbd, 0x86, 0xed, 0xd2, 0x04, 0x02, 0xbb, 0x8a, 0xa9, 0xdf, 0x37,
0x11, 0x0f, 0x3d, 0x95, 0xa2, 0xe2, 0xa2, 0x17, 0x1f, 0x6e, 0x4a, 0x2f, 0x1e, 0x94, 0xbf, 0xef,
0x0c, 0x56, 0x5d, 0x42, 0x03, 0x00, 0x00, 0x01, 0x01, 0x00, 0xc2, 0x05, 0xc8, 0x82, 0x2b, 0xc2,
0xa3, 0x14, 0x2f, 0xa2, 0x88, 0xe8, 0x01, 0x77, 0xc5, 0x03, 0x51, 0x65, 0xd6, 0xc2, 0x54, 0xf3,
0x88, 0x72, 0x05, 0x65, 0x33, 0xae, 0x84, 0x25, 0xb9, 0xb7, 0x26, 0xae, 0x2e, 0x96, 0x84, 0xf7,
0x6b, 0x73, 0xc3, 0x13, 0x76, 0x72, 0x05, 0x1c, 0x21, 0x06, 0x50, 0xc0, 0xd9, 0x52, 0xfc, 0xd3,
0x0f, 0xd3, 0x0a, 0x68, 0xdc, 0xbd, 0xf9, 0xe4, 0xc9, 0xaa, 0x61, 0x1e, 0xc0, 0x56, 0x00, 0xc0,
0x5d, 0xf7, 0xdf, 0xd3, 0x87, 0x8a, 0x7f, 0xa6, 0xec, 0xd8, 0x03, 0x21, 0x57, 0x74, 0x47, 0x88,
0xb0, 0x4f, 0x4e, 0x98, 0x72, 0xf1, 0xf9, 0xd8, 0x65, 0xa2, 0x61, 0xfa, 0x83, 0x11, 0xe9, 0x77,
0x43, 0xf1, 0xfb, 0x4f, 0x2d, 0x06, 0x9f, 0x8a, 0xec, 0x59, 0xb0, 0xcd, 0x33, 0x88, 0x9c, 0x1f,
0x9c, 0xbc, 0xe3, 0xf4, 0x34, 0x04, 0xf8, 0xdc, 0x5c, 0x26, 0xd3, 0x6e, 0x91, 0xf3, 0x9a, 0x69,
0xb9, 0x22, 0xde, 0x43, 0xf4, 0x6f, 0xcc, 0x41, 0x4e, 0x9d, 0x40, 0xad, 0xfe, 0xd5, 0x3d, 0xbb,
0x6c, 0x22, 0x62, 0x0e, 0xa2, 0x09, 0xc1, 0xb8, 0xd9, 0x50, 0xe8, 0xe4, 0x1e, 0x74, 0x25, 0xf5,
0x9e, 0x62, 0x55, 0xe5, 0x1b, 0xb4, 0x7e, 0x5c, 0x8b, 0x2d, 0x10, 0xa1, 0x8b, 0x12, 0x66, 0x3f,
0xad, 0xb4, 0x84, 0xe4, 0xa4, 0x07, 0x7a, 0x9f, 0x8f, 0x7e, 0x04, 0xec, 0xf2, 0x38, 0x5c, 0x67,
0x08, 0x0c, 0xae, 0x19, 0xea, 0xac, 0xf1, 0x80, 0xbb, 0x19, 0xe1, 0xb8, 0x1a, 0x7f, 0x49, 0x6f,
0x2a, 0x9e, 0x8c, 0x68, 0xb8, 0x15, 0xd7, 0x6c, 0xaa, 0x4f, 0xed, 0x8f, 0x63, 0x39, 0x6b, 0x8d,
0xb1, 0xa4, 0xbd, 0x84, 0x0a, 0xff, 0x34, 0x8f, 0x8c, 0xa5, 0x27, 0xf2, 0xca, 0x11, 0x28, 0x79,
0x0d, 0x10, 0x6e, 0x58, 0x0d, 0xda, 0x05, 0x07, 0x54, 0x3d,
];
#[test]
fn legit_rsa_key_works() {
let bytes = bytes::Bytes::from(RSA_TEST_KEY);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
if let Err(ref e) = result {
println!("error = {:?}", e);
}
assert!(matches!(result, Ok(PrivateKey::Rsa(_, _))));
}
#[test]
fn successful_read_leaves_excess() {
let mut test_key = RSA_TEST_KEY.to_vec();
test_key.push(0xa1);
test_key.push(0x23);
test_key.push(0x05);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
if let Err(ref e) = result {
println!("error = {:?}", e);
}
assert!(matches!(result, Ok(PrivateKey::Rsa(_, _))));
assert_eq!(buffer.get_u8().unwrap(), 0xa1);
assert_eq!(buffer.get_u8().unwrap(), 0x23);
assert_eq!(buffer.get_u8().unwrap(), 0x05);
assert!(!buffer.has_remaining());
}
#[test]
fn short_rsa_reads_fail_properly() {
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..23]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"n"
)));
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..529]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"e"
)));
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..535]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"d"
)));
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..1247]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"q⁻¹"
)));
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..1550]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"p"
)));
let bytes = bytes::Bytes::from(&RSA_TEST_KEY[0..1750]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotFindRsaConstant { constant } if constant == &"q"
)));
}
#[test]
fn bad_rsa_key_is_bad() {
let mut test_key = RSA_TEST_KEY.to_vec();
test_key[20] += 1;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::BadRsaKey)));
}
#[cfg(test)]
const ED25519_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00,
0x00, 0x00, 0x20, 0x80, 0xe2, 0x47, 0x6a, 0x6f, 0xcb, 0x13, 0x7a, 0x0e, 0xda, 0x9b, 0x06, 0x3c,
0x4d, 0xd7, 0x24, 0xdb, 0x31, 0x1b, 0xa9, 0xc5, 0xc3, 0x44, 0x5b, 0xda, 0xff, 0x85, 0x51, 0x15,
0x63, 0x58, 0xd3, 0x00, 0x00, 0x00, 0x40, 0x7e, 0x5b, 0xf2, 0x9c, 0x9c, 0xea, 0xdf, 0x7f, 0x2a,
0xf5, 0xf1, 0x3d, 0x46, 0xb6, 0xd5, 0xbc, 0x67, 0xac, 0xae, 0xb5, 0x17, 0xaa, 0x56, 0x22, 0x24,
0x9b, 0xa7, 0x20, 0x39, 0x40, 0x00, 0xed, 0x80, 0xe2, 0x47, 0x6a, 0x6f, 0xcb, 0x13, 0x7a, 0x0e,
0xda, 0x9b, 0x06, 0x3c, 0x4d, 0xd7, 0x24, 0xdb, 0x31, 0x1b, 0xa9, 0xc5, 0xc3, 0x44, 0x5b, 0xda,
0xff, 0x85, 0x51, 0x15, 0x63, 0x58, 0xd3,
];
#[test]
fn good_ed25519_key_works() {
let bytes = bytes::Bytes::from(ED25519_TEST_KEY);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Ok(PrivateKey::Ed25519(_, _))));
assert!(!buffer.has_remaining());
}
#[test]
fn short_ed25519_reads_fail_properly() {
let bytes = bytes::Bytes::from(&ED25519_TEST_KEY[0..20]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyReadError::CouldNotReadEd25519 { part } if
part == &"public")));
let bytes = bytes::Bytes::from(&ED25519_TEST_KEY[0..60]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyReadError::CouldNotReadEd25519 { part } if
part == &"private")));
}
#[test]
fn catch_invalid_ed25519_public() {
let mut test_key = ED25519_TEST_KEY.to_vec();
test_key[19] = 0;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyReadError::InvalidEd25519Key { kind, .. } if
kind == &"public")));
}
#[test]
fn catch_short_ed25519_length() {
let mut test_key = ED25519_TEST_KEY.to_vec();
test_key[54] -= 1;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyReadError::InvalidEd25519Key { kind, error } if
kind == &"private" && error.contains("key should be"))));
}
#[test]
fn catch_invalid_private_key() {
let mut test_key = ED25519_TEST_KEY.to_vec();
test_key[110] -= 1;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyReadError::InvalidEd25519Key { kind, error } if
kind == &"private" && error.contains("final load"))));
}
#[cfg(test)]
const P256_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x41, 0x04, 0xd7, 0x47, 0x00, 0x93, 0x35, 0xc5, 0x88, 0xc1,
0x67, 0xb5, 0x1d, 0x5f, 0xf1, 0x9b, 0x82, 0x1d, 0xe8, 0x37, 0x21, 0xe7, 0x89, 0xe5, 0x7c, 0x14,
0x6a, 0xd7, 0xfe, 0x43, 0x44, 0xe7, 0x67, 0xd8, 0x05, 0x66, 0xe1, 0x96, 0x12, 0x8f, 0xc9, 0x23,
0x1c, 0x8f, 0x25, 0x0e, 0xa7, 0xf1, 0xcd, 0x76, 0x7a, 0xea, 0xb7, 0x87, 0x24, 0x07, 0x1e, 0x72,
0x63, 0x6b, 0x81, 0xde, 0x20, 0x81, 0xe7, 0x82, 0x00, 0x00, 0x00, 0x21, 0x00, 0xd1, 0x3d, 0x96,
0x67, 0x38, 0xdd, 0xa7, 0xe9, 0x8d, 0x87, 0x6d, 0x6b, 0x98, 0x6f, 0x36, 0x8e, 0x87, 0x82, 0x6b,
0x3a, 0x40, 0x2d, 0x99, 0x88, 0xf3, 0x26, 0x76, 0xf7, 0xe1, 0x3f, 0xff, 0x26,
];
#[test]
fn legit_p256_key_works() {
let bytes = bytes::Bytes::from(P256_TEST_KEY);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Ok(PrivateKey::P256(_, _))));
}
#[test]
fn check_for_mismatched_curves() {
let mut test_key = P256_TEST_KEY.to_vec();
test_key[32] = '3' as u8;
test_key[33] = '8' as u8;
test_key[34] = '4' as u8;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::MismatchedKeyAndCurve { key, curve } if
key == &"ecdsa-sha2-nistp256" && curve == &"nistp384")));
}
#[test]
fn ecc_short_reads_fail_correctly() {
let bytes = bytes::Bytes::from(&P256_TEST_KEY[0..48]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotReadPublicPoint { .. })));
let bytes = bytes::Bytes::from(&P256_TEST_KEY[0..112]);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::CouldNotReadPrivateScalar { .. })));
}
#[test]
fn p256_long_scalar_fails() {
let mut test_key = P256_TEST_KEY.to_vec();
assert_eq!(0x21, test_key[107]);
test_key[107] += 2;
test_key.push(0);
test_key.push(0);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[test]
fn invalid_p256_fails_appropriately() {
let mut test_key = P256_TEST_KEY.to_vec();
assert_eq!(4, test_key[39]);
test_key[39] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
if let Err(ref e) = result {
println!("error: {:?}", e);
}
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::PointDecodeError { .. })));
let mut test_key = P256_TEST_KEY.to_vec();
test_key[64] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::BadPointForPublicKey { .. })));
let mut test_key = P256_TEST_KEY.to_vec();
assert_eq!(0x21, test_key[107]);
test_key[107] = 0x22;
test_key.push(4);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[cfg(test)]
const P384_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x61, 0x04, 0x0d, 0xa3, 0x8b, 0x42, 0x98, 0x15, 0xba, 0x0c,
0x9b, 0xf6, 0x5e, 0xc8, 0x68, 0xc3, 0x1e, 0x44, 0xb2, 0x6f, 0x12, 0x2f, 0xc8, 0x97, 0x81, 0x23,
0x60, 0xa0, 0xc3, 0xaf, 0xf1, 0x3f, 0x5f, 0xd6, 0xea, 0x49, 0x9c, 0xd6, 0x74, 0x34, 0xd0, 0x6a,
0xd0, 0x34, 0xe4, 0xd8, 0x42, 0x00, 0x94, 0x61, 0x63, 0x15, 0x11, 0xb0, 0x63, 0x52, 0xcc, 0xbe,
0xe5, 0xc2, 0x12, 0x33, 0xdc, 0x36, 0x03, 0x60, 0x6c, 0xb9, 0x11, 0xa6, 0xe4, 0x81, 0x64, 0x4a,
0x54, 0x74, 0x2b, 0xfb, 0xbc, 0xff, 0x90, 0xe0, 0x2c, 0x00, 0xc1, 0xae, 0x99, 0x2e, 0x0f, 0xdb,
0x50, 0xec, 0x4c, 0xe8, 0xbd, 0xf1, 0x0f, 0xdc, 0x00, 0x00, 0x00, 0x30, 0x55, 0xc0, 0x13, 0xb0,
0x61, 0x6d, 0xca, 0xf8, 0x09, 0x6f, 0x71, 0x26, 0x16, 0x97, 0x9b, 0x84, 0xe8, 0x37, 0xa9, 0x55,
0xab, 0x73, 0x8a, 0xc3, 0x80, 0xb8, 0xd5, 0x9c, 0x71, 0x21, 0xeb, 0x4b, 0xc5, 0xf6, 0x21, 0xc9,
0x92, 0x0c, 0xa6, 0x43, 0x48, 0x97, 0x18, 0x6c, 0x4f, 0x92, 0x42, 0xba,
];
#[test]
fn legit_p384_key_works() {
let bytes = bytes::Bytes::from(P384_TEST_KEY);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Ok(PrivateKey::P384(_, _))));
}
#[test]
fn p384_long_scalar_fails() {
let mut test_key = P384_TEST_KEY.to_vec();
assert_eq!(0x30, test_key[139]);
test_key[139] += 2;
test_key.push(0);
test_key.push(0);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[test]
fn invalid_p384_fails_appropriately() {
let mut test_key = P384_TEST_KEY.to_vec();
assert_eq!(4, test_key[39]);
test_key[39] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::PointDecodeError { .. })));
let mut test_key = P384_TEST_KEY.to_vec();
test_key[64] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::BadPointForPublicKey { .. })));
let mut test_key = P384_TEST_KEY.to_vec();
assert_eq!(0x30, test_key[139]);
test_key[139] = 0x31;
test_key.push(4);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[cfg(test)]
const P521_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x85, 0x04, 0x01, 0x68, 0x9a, 0x37, 0xac, 0xa3, 0x16, 0x26,
0xa4, 0xaa, 0x72, 0xe6, 0x24, 0x40, 0x4c, 0x69, 0xbf, 0x11, 0x9e, 0xcd, 0xb6, 0x63, 0x92, 0x10,
0xa6, 0xb7, 0x6e, 0x98, 0xb4, 0xa0, 0x81, 0xc5, 0x3c, 0x88, 0xfa, 0x9b, 0x60, 0x57, 0x4c, 0x0f,
0xba, 0x36, 0x4e, 0xc6, 0xe0, 0x3e, 0xa5, 0x86, 0x3d, 0xd3, 0xd5, 0x86, 0x96, 0xe9, 0x4a, 0x1c,
0x0c, 0xe2, 0x70, 0xff, 0x1f, 0x79, 0x06, 0x5d, 0x52, 0x9a, 0x01, 0x2b, 0x87, 0x8e, 0xc2, 0xe9,
0xe2, 0xb7, 0x01, 0x00, 0xa6, 0x1a, 0xf7, 0x23, 0x47, 0x6a, 0x70, 0x10, 0x09, 0x59, 0xde, 0x0a,
0x20, 0xca, 0x2f, 0xd7, 0x5a, 0x98, 0xbd, 0xc3, 0x5b, 0xf2, 0x7b, 0x14, 0x6e, 0x6b, 0xa5, 0x93,
0x5d, 0x3e, 0x21, 0x5c, 0x49, 0x40, 0xbf, 0x9b, 0xc0, 0x78, 0x4b, 0xb1, 0xe9, 0xc7, 0x02, 0xb1,
0x51, 0x94, 0x1a, 0xcf, 0x88, 0x7b, 0xfe, 0xea, 0xd8, 0x55, 0x89, 0xb3, 0x00, 0x00, 0x00, 0x42,
0x01, 0x2d, 0xde, 0x75, 0x5b, 0x7a, 0x04, 0x7e, 0x24, 0xfc, 0x21, 0x07, 0xec, 0xf1, 0xab, 0xb0,
0x21, 0xd6, 0x22, 0xa7, 0xb9, 0x77, 0x72, 0x34, 0x9c, 0xad, 0x32, 0x6f, 0x4f, 0xcc, 0xeb, 0x42,
0xff, 0x4b, 0x9f, 0x78, 0x21, 0x6d, 0xbd, 0x61, 0xc1, 0xe0, 0x9d, 0xb4, 0xca, 0xc0, 0x22, 0xb1,
0xd1, 0xdf, 0xad, 0xe7, 0xed, 0xc4, 0x90, 0x61, 0xe7, 0x7c, 0xab, 0x4a, 0xa9, 0x85, 0x60, 0xd9,
0xad, 0x92,
];
#[test]
fn legit_p521_key_works() {
let bytes = bytes::Bytes::from(P521_TEST_KEY);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Ok(PrivateKey::P521(_, _))));
}
#[test]
fn p521_long_scalar_fails() {
let mut test_key = P521_TEST_KEY.to_vec();
assert_eq!(0x42, test_key[175]);
test_key[175] += 2;
test_key.push(0);
test_key.push(0);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[test]
fn invalid_p521_fails_appropriately() {
let mut test_key = P521_TEST_KEY.to_vec();
assert_eq!(4, test_key[39]);
test_key[39] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::PointDecodeError { .. })));
let mut test_key = P521_TEST_KEY.to_vec();
test_key[64] = 0x33;
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::BadPointForPublicKey { .. })));
let mut test_key = P521_TEST_KEY.to_vec();
assert_eq!(0x42, test_key[175]);
test_key[175] = 0x43;
test_key.push(4);
let bytes = bytes::Bytes::from(test_key);
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::InvalidScalar { .. })));
}
#[test]
fn dont_parse_unknown_key_types() {
let bytes = bytes::Bytes::from(b"\0\0\0\x07ssh-dsa\0\0\0\0".to_vec());
let mut buffer = SshReadBuffer::from(bytes);
let result = read_private_key(&mut buffer);
assert!(matches!(result, Err(e) if
matches!(
e.current_context(),
PrivateKeyReadError::UnrecognizedKeyType { key_type } if
key_type.as_str() == "ssh-dsa")));
}

View File

@@ -0,0 +1,816 @@
use super::{PublicKey, PublicKeyLoadError};
use crate::buffer::SshReadBuffer;
use crate::private_key::{read_private_key, PrivateKey};
use aes::cipher::{KeyIvInit, StreamCipher};
use base64::engine::{self, Engine};
use bytes::{Buf, Bytes};
use error_stack::{report, ResultExt};
use generic_array::GenericArray;
use std::io;
use std::path::Path;
type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
#[derive(Debug, thiserror::Error)]
pub enum PrivateKeyLoadError {
#[error("Could not read private key from file: {error}")]
CouldNotRead { error: io::Error },
#[error("Private key file is lacking necessary newlines.")]
FileLackingNewlines,
#[error("Could not find OpenSSL private key header")]
NoOpenSSLHeader,
#[error("Could not find OpenSSL private key trailer")]
NoOpenSSLTrailer,
#[error("Base64 decoding error: {error}")]
Base64 { error: base64::DecodeError },
#[error("Could not find OpenSSL magic header")]
NoMagicHeader,
#[error("Could not decode OpenSSL magic header: {error}")]
CouldNotDecodeMagicHeader { error: std::str::Utf8Error },
#[error("Unexpected magic value; expected 'openssh-key-v1', got {value}")]
UnexpectedMagicHeaderValue { value: String },
#[error("Could not determine cipher for private key")]
CouldNotDetermineCipher,
#[error("Could not determine KDF for private key")]
CouldNotDetermineKdf,
#[error("Could not determine KDF options for private key")]
CouldNotDetermineKdfOptions,
#[error("Could not determine encoded public key count")]
CouldNotDeterminePublicKeyCount,
#[error("Could not decode encoded public key")]
CouldNotLoadEncodedPublic,
#[error(transparent)]
PublicKeyError(#[from] PublicKeyLoadError),
#[error("Failed to properly decrypt contents ({checkint1} != {checkint2}")]
DecryptionCheckError { checkint1: u32, checkint2: u32 },
#[error("File may have been truncated; should have at least {reported_length} bytes, saw {remaining_bytes}")]
TruncationError {
reported_length: usize,
remaining_bytes: usize,
},
#[error("Very short file; could not find length of private key space")]
CouldNotFindPrivateBufferLength,
#[error("Padding does not match OpenSSH's requirements")]
PaddingError,
#[error("{amount} bytes of extraneous data found at end of private key buffer")]
ExtraneousData { amount: usize },
#[error("Private key does not match associated public key")]
MismatchedPublic,
#[error("Unknown private key encryption scheme '{scheme}'")]
UnknownEncryptionScheme { scheme: String },
#[error("Could not find salt bytes for key derivation")]
CouldNotFindSaltBytes,
#[error("Could not find number of key derivation rounds")]
CouldNotFindKdfRounds,
#[error("Extraneous info in key derivation block")]
ExtraneousKdfInfo,
#[error("Error running key derivation: {error}")]
KeyDerivationError { error: bcrypt_pbkdf::Error },
#[error("Internal error: hit empty encryption method way too late in decryption path")]
EmptyEncryptionWayTooLate,
#[error("Failed to decrypt encrypted data: {error}")]
StreamCipherError {
error: aes::cipher::StreamCipherError,
},
#[error("Could not get {which} post-decryption check bytes")]
CouldNotGetCheckBytes { which: &'static str },
#[error("Failed to load private key")]
CouldNotLoadEncodedPrivate,
#[error("Failed to load info string for private key")]
CouldNotLoadPrivateInfo,
}
#[derive(Debug, PartialEq)]
enum KeyEncryptionMode {
None,
Aes256Ctr,
}
impl KeyEncryptionMode {
fn key_and_iv_size(&self) -> usize {
self.key_size() + self.iv_size()
}
fn key_size(&self) -> usize {
match self {
KeyEncryptionMode::None => 0,
KeyEncryptionMode::Aes256Ctr => 32,
}
}
fn iv_size(&self) -> usize {
match self {
KeyEncryptionMode::None => 0,
KeyEncryptionMode::Aes256Ctr => 16,
}
}
}
#[derive(Debug)]
enum KeyDerivationMethod {
None,
Bcrypt { salt: Bytes, rounds: u32 },
}
#[derive(Debug)]
struct FileEncryptionData {
encryption_mode: KeyEncryptionMode,
key_derivation_method: KeyDerivationMethod,
}
pub async fn load_openssh_file_keys<P: AsRef<Path>>(
path: P,
provided_password: &Option<String>,
) -> error_stack::Result<Vec<(PrivateKey, String)>, PrivateKeyLoadError> {
let path = path.as_ref();
let binary_data = load_openssh_binary_data(path)
.await
.attach_printable_lazy(|| format!("in {}", path.display()))?;
let mut data_buffer = SshReadBuffer::from(binary_data);
let encryption_info = load_encryption_info(&mut data_buffer)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
let key_count = data_buffer
.get_u32()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotDeterminePublicKeyCount)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
let mut public_keys = vec![];
for _ in 0..key_count {
let encoded_public = data_buffer
.get_bytes()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadEncodedPublic)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
let found_public = PublicKey::try_from(encoded_public)
.change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadEncodedPublic)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
public_keys.push(found_public);
}
let private_key_data = decrypt_private_blob(data_buffer, encryption_info, provided_password)?;
let mut private_key_buffer = SshReadBuffer::from(private_key_data);
let checkint1 = private_key_buffer
.get_u32()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotGetCheckBytes { which: "first" })
.attach_printable_lazy(|| format!("in {}", path.display()))?;
let checkint2 = private_key_buffer
.get_u32()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotGetCheckBytes { which: "second" })
.attach_printable_lazy(|| format!("in {}", path.display()))?;
if checkint1 != checkint2 {
return Err(report!(PrivateKeyLoadError::DecryptionCheckError {
checkint1,
checkint2,
}));
}
let mut results = vec![];
for public_key in public_keys.into_iter() {
let private = read_private_key(&mut private_key_buffer)
.change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadEncodedPrivate)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
if private.public() != public_key {
return Err(report!(PrivateKeyLoadError::MismatchedPublic));
}
let private_info = private_key_buffer
.get_string()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotLoadPrivateInfo)
.attach_printable_lazy(|| format!("in {}", path.display()))?;
results.push((private, private_info));
}
let mut should_be = 1;
while let Ok(next) = private_key_buffer.get_u8() {
if next != should_be {
return Err(report!(PrivateKeyLoadError::PaddingError))
.attach_printable_lazy(|| format!("in {}", path.display()));
}
should_be += 1;
}
Ok(results)
}
async fn load_openssh_binary_data(path: &Path) -> error_stack::Result<Bytes, PrivateKeyLoadError> {
let file_data = tokio::fs::read_to_string(path)
.await
.map_err(|error| report!(PrivateKeyLoadError::CouldNotRead { error }))?;
let (openssh_header, everything_else) = file_data
.split_once('\n')
.ok_or_else(|| report!(PrivateKeyLoadError::FileLackingNewlines))?;
let (actual_key_data, openssh_trailer) = everything_else
.trim_end()
.rsplit_once('\n')
.ok_or_else(|| report!(PrivateKeyLoadError::FileLackingNewlines))?;
if openssh_header != "-----BEGIN OPENSSH PRIVATE KEY-----" {
return Err(report!(PrivateKeyLoadError::NoOpenSSLHeader));
}
if openssh_trailer != "-----END OPENSSH PRIVATE KEY-----" {
return Err(report!(PrivateKeyLoadError::NoOpenSSLTrailer));
}
let single_line_data: String = actual_key_data
.chars()
.filter(|x| !x.is_whitespace())
.collect();
let mut key_material = engine::general_purpose::STANDARD
.decode(single_line_data)
.map(Bytes::from)
.map_err(|de| report!(PrivateKeyLoadError::Base64 { error: de }))?;
if key_material.remaining() < 15 {
return Err(report!(PrivateKeyLoadError::NoMagicHeader));
}
let auth_magic = std::str::from_utf8(&key_material[0..15])
.map_err(|e| report!(PrivateKeyLoadError::CouldNotDecodeMagicHeader { error: e }))?;
if auth_magic != "openssh-key-v1\0" {
return Err(report!(PrivateKeyLoadError::UnexpectedMagicHeaderValue {
value: auth_magic.to_string(),
}));
}
key_material.advance(15);
Ok(key_material)
}
fn load_encryption_info<B: Buf>(
buffer: &mut SshReadBuffer<B>,
) -> error_stack::Result<FileEncryptionData, PrivateKeyLoadError> {
let cipher_name = buffer
.get_string()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineCipher)?;
let encryption_mode = match cipher_name.as_str() {
"none" => KeyEncryptionMode::None,
"aes256-ctr" => KeyEncryptionMode::Aes256Ctr,
_ => {
return Err(report!(PrivateKeyLoadError::UnknownEncryptionScheme {
scheme: cipher_name,
}))
}
};
let kdf_name = buffer
.get_string()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineKdf)?;
let key_derivation_method = match kdf_name.as_str() {
"none" => {
let _ = buffer
.get_bytes()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineKdfOptions)?;
KeyDerivationMethod::None
}
"bcrypt" => {
let mut blob = buffer
.get_bytes()
.map(SshReadBuffer::from)
.change_context_lazy(|| PrivateKeyLoadError::CouldNotDetermineKdfOptions)?;
let salt = blob
.get_bytes()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotFindSaltBytes)?;
let rounds = blob
.get_u32()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotFindKdfRounds)?;
if blob.has_remaining() {
return Err(report!(PrivateKeyLoadError::ExtraneousKdfInfo));
}
KeyDerivationMethod::Bcrypt { salt, rounds }
}
_ => {
return Err(report!(PrivateKeyLoadError::UnknownEncryptionScheme {
scheme: kdf_name,
}))
}
};
Ok(FileEncryptionData {
encryption_mode,
key_derivation_method,
})
}
fn decrypt_private_blob<B: Buf>(
mut buffer: SshReadBuffer<B>,
encryption_info: FileEncryptionData,
provided_password: &Option<String>,
) -> error_stack::Result<Bytes, PrivateKeyLoadError> {
let ciphertext = buffer
.get_bytes()
.change_context_lazy(|| PrivateKeyLoadError::CouldNotFindPrivateBufferLength)?;
if buffer.has_remaining() {
return Err(report!(PrivateKeyLoadError::ExtraneousData {
amount: buffer.remaining(),
}));
}
if encryption_info.encryption_mode == KeyEncryptionMode::None {
return Ok(ciphertext);
}
let password = match provided_password {
Some(x) => x.as_str(),
None => unimplemented!(),
};
let mut key_and_iv = vec![0; encryption_info.encryption_mode.key_and_iv_size()];
match encryption_info.key_derivation_method {
KeyDerivationMethod::None => {
for (idx, value) in password.as_bytes().iter().enumerate() {
if idx < key_and_iv.len() {
key_and_iv[idx] = *value;
}
}
}
KeyDerivationMethod::Bcrypt { salt, rounds } => {
bcrypt_pbkdf::bcrypt_pbkdf(password, &salt, rounds, &mut key_and_iv)
.map_err(|error| report!(PrivateKeyLoadError::KeyDerivationError { error }))?;
}
}
let key_size = encryption_info.encryption_mode.key_size();
let key = GenericArray::from_slice(&key_and_iv[0..key_size]);
let iv = GenericArray::from_slice(&key_and_iv[key_size..]);
match encryption_info.encryption_mode {
KeyEncryptionMode::None => Err(report!(PrivateKeyLoadError::EmptyEncryptionWayTooLate)),
KeyEncryptionMode::Aes256Ctr => {
let mut out_buffer = vec![0u8; ciphertext.len()];
let mut cipher = Aes256Ctr::new(key, iv);
cipher
.apply_keystream_b2b(&ciphertext, &mut out_buffer)
.map_err(|error| PrivateKeyLoadError::StreamCipherError { error })?;
Ok(out_buffer.into())
}
}
}
#[test]
fn password_generation_matches_saved() {
let salt = [
0x1f, 0x77, 0xd3, 0x33, 0xcc, 0x9e, 0xd1, 0x45, 0xe3, 0xc1, 0xc5, 0x26, 0xa8, 0x7d, 0xf4,
0x1a,
];
let key_and_iv = [
0xcd, 0xd5, 0x5f, 0x6c, 0x73, 0xa0, 0x5c, 0x46, 0x9d, 0xdd, 0x84, 0xbf, 0xab, 0x3a, 0xa6,
0x6e, 0xd6, 0x18, 0xeb, 0x4e, 0x34, 0x1d, 0x89, 0x38, 0x92, 0x4a, 0x0b, 0x5c, 0xca, 0xba,
0x3e, 0xed, 0x42, 0x2e, 0xd3, 0x1f, 0x0b, 0xb7, 0x22, 0x41, 0xeb, 0x3d, 0x37, 0x91, 0xf7,
0x12, 0x15, 0x1b,
];
let password = "foo";
let rounds = 24;
let mut output = [0; 48];
bcrypt_pbkdf::bcrypt_pbkdf(password, &salt, rounds, &mut output).unwrap();
assert_eq!(key_and_iv, output);
}
#[test]
fn can_regenerate_and_decode_saved_session() {
let salt = [
0x86, 0x4a, 0xa5, 0x81, 0x73, 0xb1, 0x12, 0x37, 0x54, 0x6e, 0x34, 0x22, 0x84, 0x89, 0xba,
0x8c,
];
let key_and_iv = [
0x6f, 0xda, 0x3b, 0x95, 0x1d, 0x85, 0xd7, 0xb0, 0x56, 0x8c, 0xc2, 0x4c, 0xa9, 0xf5, 0x95,
0x4c, 0x9b, 0x39, 0x75, 0x14, 0x29, 0x32, 0xac, 0x2b, 0xd3, 0xf8, 0x63, 0x50, 0xc8, 0xfa,
0xcb, 0xb4, 0xca, 0x9a, 0x53, 0xd1, 0xf1, 0x26, 0x26, 0xd8, 0x1a, 0x44, 0x76, 0x2b, 0x27,
0xd0, 0x43, 0x91,
];
let key_length = 32;
let iv_length = 16;
let rounds = 24;
let mut bcrypt_output = [0; 48];
bcrypt_pbkdf::bcrypt_pbkdf("foo", &salt, rounds, &mut bcrypt_output).unwrap();
assert_eq!(key_and_iv, bcrypt_output);
let key_bytes = &key_and_iv[0..key_length];
let iv_bytes = &key_and_iv[key_length..];
assert_eq!(iv_length, iv_bytes.len());
let plaintext = [
0xfc, 0xc7, 0x14, 0xe5, 0xfc, 0xc7, 0x14, 0xe5, 0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68,
0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00, 0x00, 0x00, 0x20, 0x85, 0x50, 0x4f,
0x2e, 0xd7, 0xab, 0x62, 0xf0, 0xc5, 0xe1, 0xaf, 0x7f, 0x20, 0x6b, 0xb7, 0x3d, 0x92, 0x7d,
0xa4, 0x00, 0xf9, 0xdd, 0x08, 0x38, 0x7b, 0xbf, 0x91, 0x3a, 0xd0, 0xfc, 0x00, 0x6d, 0x00,
0x00, 0x00, 0x40, 0x23, 0xfe, 0xe2, 0xb9, 0xae, 0x83, 0x97, 0xa1, 0x7d, 0x4f, 0x45, 0xb2,
0x61, 0x28, 0xeb, 0x6d, 0xd6, 0x5c, 0x38, 0x04, 0x2c, 0xbc, 0x9d, 0xf5, 0x1b, 0x47, 0x3b,
0x89, 0x20, 0x77, 0x6c, 0x8c, 0x85, 0x50, 0x4f, 0x2e, 0xd7, 0xab, 0x62, 0xf0, 0xc5, 0xe1,
0xaf, 0x7f, 0x20, 0x6b, 0xb7, 0x3d, 0x92, 0x7d, 0xa4, 0x00, 0xf9, 0xdd, 0x08, 0x38, 0x7b,
0xbf, 0x91, 0x3a, 0xd0, 0xfc, 0x00, 0x6d, 0x00, 0x00, 0x00, 0x10, 0x61, 0x64, 0x61, 0x6d,
0x77, 0x69, 0x63, 0x6b, 0x40, 0x65, 0x72, 0x67, 0x61, 0x74, 0x65, 0x73, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
];
let ciphertext = [
0x19, 0x96, 0xad, 0x71, 0x8d, 0x07, 0x9d, 0xf3, 0x8d, 0xe9, 0x63, 0x1c, 0xfe, 0xe4, 0xc2,
0x6a, 0x04, 0x12, 0xc1, 0x81, 0xc2, 0xe0, 0xd9, 0x63, 0xf1, 0xb8, 0xf1, 0x00, 0x6a, 0xb4,
0x35, 0xc3, 0x7e, 0x71, 0xa5, 0x65, 0xab, 0x82, 0x66, 0xd9, 0x3e, 0x68, 0x69, 0xa3, 0x01,
0xe1, 0x67, 0x42, 0x0a, 0x7c, 0xe2, 0x92, 0xab, 0x4f, 0x00, 0xfa, 0xaa, 0x20, 0x88, 0x6b,
0xa7, 0x39, 0x75, 0x0f, 0xab, 0xf5, 0x53, 0x47, 0x07, 0x10, 0xb5, 0xfb, 0xf1, 0x86, 0x9e,
0xbb, 0xe9, 0x22, 0x59, 0xa8, 0xdf, 0xf0, 0xa5, 0x28, 0xa5, 0x27, 0x26, 0x1b, 0x05, 0xb1,
0xae, 0xb4, 0xbf, 0x15, 0xa5, 0xbf, 0x64, 0x8a, 0xb3, 0x9c, 0x11, 0x16, 0xa2, 0x01, 0xa7,
0xfd, 0x2d, 0xfa, 0xc6, 0x01, 0xb2, 0xfd, 0xaa, 0x14, 0x38, 0x12, 0x79, 0xb1, 0x8a, 0x86,
0xa8, 0xdb, 0x84, 0xe9, 0xc8, 0xbb, 0x37, 0x36, 0xe4, 0x7d, 0x89, 0xd2, 0x1b, 0xab, 0x79,
0x68, 0x69, 0xb8, 0xe5, 0x04, 0x7a, 0x00, 0x14, 0x5a, 0xa5, 0x96, 0x0a, 0x1d, 0xe9, 0x5a,
0xfc, 0x80, 0x77, 0x06, 0x4d, 0xb9, 0x02, 0x95, 0x2c, 0x34,
];
let key = GenericArray::from_slice(&key_bytes);
let iv = GenericArray::from_slice(&iv_bytes);
let mut conversion_buffer = [0; 160];
let mut cipher = Aes256Ctr::new(key, iv);
cipher
.apply_keystream_b2b(&plaintext, &mut conversion_buffer)
.unwrap();
assert_eq!(ciphertext, conversion_buffer);
let mut cipher = Aes256Ctr::new(key, iv);
cipher
.apply_keystream_b2b(&ciphertext, &mut conversion_buffer)
.unwrap();
assert_eq!(plaintext, conversion_buffer);
}
#[tokio::test]
async fn test_keys_parse() {
let mut parsed_keys = vec![];
let test_key_directory = format!("{}/tests/ssh_keys", env!("CARGO_MANIFEST_DIR"));
let mut directory_reader = tokio::fs::read_dir(test_key_directory)
.await
.expect("can read test key directory");
while let Ok(Some(entry)) = directory_reader.next_entry().await {
if entry.path().extension().is_none()
&& entry
.file_type()
.await
.map(|x| x.is_file())
.unwrap_or_default()
{
let mut private_keys = load_openssh_file_keys(entry.path(), &Some("hush".to_string()))
.await
.expect("can parse saved private key");
parsed_keys.append(&mut private_keys);
}
}
assert_eq!(18, parsed_keys.len());
}
#[tokio::test]
async fn improper_newline_errors_work() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
named_temp
.write_all("-----BEGIN OPENSSH PRIVATE KEY-----".as_bytes())
.unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::FileLackingNewlines
));
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
named_temp
.write_all(
"-----BEGIN OPENSSH PRIVATE KEY-----\n-----END OPENSSH PRIVATE KEY-----".as_bytes(),
)
.unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::FileLackingNewlines
));
}
#[tokio::test]
async fn improper_header_trailer_errors_work() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
named_temp
.write_all("-----BEGIN OPENSSH PRIVTE KEY-----\n".as_bytes())
.unwrap();
named_temp.write_all("stuff\n".as_bytes()).unwrap();
named_temp
.write_all("-----END OPENSSH PRIVATE KEY-----\n".as_bytes())
.unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::NoOpenSSLHeader
));
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
named_temp
.write_all("-----BEGIN OPENSSH PRIVATE KEY-----\n".as_bytes())
.unwrap();
named_temp.write_all("stuff\n".as_bytes()).unwrap();
named_temp
.write_all("-----END OPENSSH PRIVATEKEY-----\n".as_bytes())
.unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::NoOpenSSLTrailer
));
}
#[tokio::test]
async fn invalid_initial_data_errors_work() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap();
writeln!(named_temp, "stuff").unwrap();
writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::Base64 { .. }
));
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap();
writeln!(
named_temp,
"{}",
base64::engine::general_purpose::STANDARD.encode(b"openssl\x00")
)
.unwrap();
writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::NoMagicHeader
));
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap();
writeln!(
named_temp,
"{}",
base64::engine::general_purpose::STANDARD.encode(b"openssl\xc3\x28key-v1\x00")
)
.unwrap();
writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(
result.unwrap_err().current_context(),
PrivateKeyLoadError::CouldNotDecodeMagicHeader { .. }
));
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap();
writeln!(named_temp, "b3BlbnNzbC1rZXktdjFh").unwrap();
writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap();
let path = named_temp.into_temp_path();
let result = load_openssh_binary_data(&path).await;
assert!(matches!(result.unwrap_err().current_context(),
PrivateKeyLoadError::UnexpectedMagicHeaderValue { value }
if value == "openssl-key-v1a"));
}
#[test]
fn errors_on_weird_encryption_info() {
let mut example_bytes = SshReadBuffer::from(Bytes::from(b"\0\0\0\x09aes256ctr".to_vec()));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(
matches!(result.current_context(), PrivateKeyLoadError::UnknownEncryptionScheme { scheme }
if scheme == "aes256ctr")
);
let mut example_bytes = SshReadBuffer::from(Bytes::from(b"\0\0\0\x0aaes256-ctr".to_vec()));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(matches!(
result.current_context(),
PrivateKeyLoadError::CouldNotDetermineKdf
));
let mut example_bytes =
SshReadBuffer::from(Bytes::from(b"\0\0\0\x0aaes256-ctr\0\0\0\x03foo".to_vec()));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(
matches!(result.current_context(), PrivateKeyLoadError::UnknownEncryptionScheme { scheme }
if scheme == "foo")
);
let mut example_bytes = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\0".to_vec(),
));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(matches!(
result.current_context(),
PrivateKeyLoadError::CouldNotFindSaltBytes
));
let mut example_bytes = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x06\0\0\0\x04ab".to_vec(),
));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(matches!(
result.current_context(),
PrivateKeyLoadError::CouldNotFindSaltBytes
));
let mut example_bytes = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x06\0\0\0\x02ab".to_vec(),
));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(matches!(
result.current_context(),
PrivateKeyLoadError::CouldNotFindKdfRounds
));
let mut example_bytes = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x0b\0\0\0\x02ab\0\0\0\x02c".to_vec(),
));
let result = load_encryption_info(&mut example_bytes).unwrap_err();
assert!(matches!(
result.current_context(),
PrivateKeyLoadError::ExtraneousKdfInfo
));
}
#[test]
fn basic_kdf_examples_work() {
let mut no_encryption = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x04none\0\0\0\x04none\0\0\0\0".to_vec(),
));
let result = load_encryption_info(&mut no_encryption).unwrap();
assert_eq!(KeyEncryptionMode::None, result.encryption_mode);
assert!(matches!(
result.key_derivation_method,
KeyDerivationMethod::None
));
let mut encryption = SshReadBuffer::from(Bytes::from(
b"\0\0\0\x0aaes256-ctr\0\0\0\x06bcrypt\0\0\0\x0a\0\0\0\x02ab\0\0\0\x18".to_vec(),
));
let result = load_encryption_info(&mut encryption).unwrap();
assert_eq!(KeyEncryptionMode::Aes256Ctr, result.encryption_mode);
assert!(
matches!(result.key_derivation_method, KeyDerivationMethod::Bcrypt { salt, rounds }
if salt.as_ref() == b"ab" && rounds == 24)
);
}
#[cfg(test)]
pub fn generate_test_file(bytes: &[u8]) -> tempfile::TempPath {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "-----BEGIN OPENSSH PRIVATE KEY-----").unwrap();
writeln!(
named_temp,
"{}",
base64::engine::general_purpose::STANDARD.encode(bytes)
)
.unwrap();
writeln!(named_temp, "-----END OPENSSH PRIVATE KEY-----").unwrap();
named_temp.into_temp_path()
}
#[tokio::test]
async fn invalid_encryption_info_stops_parsing() {
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x09aes256ctr");
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::UnknownEncryptionScheme { .. }))
);
}
#[tokio::test]
async fn invalid_public_keys_stops_parsing() {
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0");
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotDeterminePublicKeyCount))
);
let path = generate_test_file(
b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\x01\0\0\0\x03foo",
);
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotLoadEncodedPublic))
);
}
#[tokio::test]
async fn checkint_validation_works() {
let path = generate_test_file(
b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x02ab",
);
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotGetCheckBytes { which } if which == &"first"))
);
let path = generate_test_file(
b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x06abcdef",
);
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::CouldNotGetCheckBytes { which } if which == &"second"))
);
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x08\0\0\0\x01\0\0\0\x02");
let result = load_openssh_file_keys(&path, &None).await;
assert!(
matches!(result, Err(e) if matches!(e.current_context(), PrivateKeyLoadError::DecryptionCheckError { checkint1, checkint2 } if *checkint1 == 1 && *checkint2 == 2))
);
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x08\0\0\0\x01\0\0\0\x01");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn padding_checks_work() {
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x08\0\0\0\x01\0\0\0\x01");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_ok());
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x09\0\0\0\x01\0\0\0\x01\x01");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_ok());
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x0a\0\0\0\x01\0\0\0\x01\x01\x02");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_ok());
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x09\0\0\0\x01\0\0\0\x01\x00");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_err());
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x0a\0\0\0\x01\0\0\0\x01\x01\x03");
let result = load_openssh_file_keys(&path, &None).await;
assert!(result.is_err());
let path = generate_test_file(b"openssh-key-v1\0\0\0\0\x04none\0\0\0\x04none\0\0\0\0\0\0\0\0\0\0\0\x0a\0\0\0\x01\0\0\0\x01\x01\x03\x04");
let result = load_openssh_file_keys(&path, &None).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyLoadError::ExtraneousData { amount } if
amount == &1)));
}
#[tokio::test]
async fn file_errors_are_caught() {
let result = load_openssh_file_keys("--capitan--", &None).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyLoadError::CouldNotRead { .. })));
}
#[tokio::test]
async fn mismatched_keys_are_handled() {
let test_path = format!(
"{}/tests/broken_keys/mismatched",
env!("CARGO_MANIFEST_DIR")
);
let result = load_openssh_file_keys(test_path.as_str(), &None).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyLoadError::MismatchedPublic)));
}
#[tokio::test]
async fn broken_private_info_strings_are_handled() {
let test_path = format!("{}/tests/broken_keys/bad_info", env!("CARGO_MANIFEST_DIR"));
let result = load_openssh_file_keys(test_path.as_str(), &None).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PrivateKeyLoadError::CouldNotLoadPrivateInfo)));
}

448
keys/src/public_key.rs Normal file
View File

@@ -0,0 +1,448 @@
use crypto::rsa;
use crate::buffer::SshReadBuffer;
use bytes::Bytes;
use elliptic_curve::sec1::FromEncodedPoint;
use error_stack::{report, ResultExt};
use num_bigint_dig::BigUint;
use thiserror::Error;
/// An SSH public key type.
///
/// Technically, SSH supports additional key types not listed in this
/// enumeration, but we have chosen to be a little opinionated about
/// what constitutes a good key type. Note that SSH keys can also be
/// appended with additonal information in their various file types;
/// this code only processes the key material.
#[derive(Debug, PartialEq)]
pub enum PublicKey {
Rsa(rsa::PublicKey),
Ed25519(ed25519_dalek::VerifyingKey),
P256(p256::PublicKey),
P384(p384::PublicKey),
P521(p521::PublicKey),
}
impl PublicKey {
/// Returns the string that SSH would use to describe this key type.
///
/// It's not clear how standard these names are, but they are
/// associated with the output of OpenSSH, and appear to match
/// some of the strings listed in the SSH RFCs.
pub fn ssh_key_type_name(&self) -> &'static str {
match self {
PublicKey::Rsa(_) => "ssh-rsa",
PublicKey::P256(_) => "ecdsa-sha2-nistp256",
PublicKey::P384(_) => "ecdsa-sha2-nistp384",
PublicKey::P521(_) => "ecdsa-sha2-nistp521",
PublicKey::Ed25519(_) => "ssh-ed25519",
}
}
}
/// Errors that can occur trying to read an SSH public key from a
/// binary blob.
#[derive(Debug, Error)]
pub enum PublicKeyReadError {
#[error("Could not read encoded public key type")]
NoPublicKeyType,
#[error("Unrecognized encoded public key type: {key_type}")]
UnrecognizedKeyType { key_type: String },
#[error("Could not determine RSA public '{constant_name}' constant")]
CouldNotFindRsaConstant { constant_name: &'static str },
#[error("Extraneous information at the end of public '{key_type}' key")]
ExtraneousInfo { key_type: &'static str },
#[error("Could not find ed25519 public point")]
NoEd25519Data,
#[error("Invalid ed25519 public key value: {error}")]
InvalidEd25519Data {
error: ed25519_dalek::SignatureError,
},
#[error("Could not read ECDSA curve information")]
CouldNotReadCurve,
#[error(
"Mismatched ECDSA curve info in public key, saw key type {key_type} but curve {curve}"
)]
MismatchedCurveInfo { key_type: String, curve: String },
#[error("Could not read public {curve} point data")]
CouldNotReadEcdsaPoint { curve: String },
#[error("Invalid ECDSA point for curve {curve}: {error}")]
InvalidEcdsaPoint {
curve: String,
error: elliptic_curve::Error,
},
#[error("Invalid ECDSA public value for curve {curve}")]
InvalidEcdsaPublicValue { curve: String },
}
impl TryFrom<Bytes> for PublicKey {
type Error = error_stack::Report<PublicKeyReadError>;
fn try_from(value: Bytes) -> Result<Self, Self::Error> {
let mut ssh_buffer = SshReadBuffer::from(value);
let encoded_key_type = ssh_buffer
.get_string()
.change_context(PublicKeyReadError::NoPublicKeyType)?;
match encoded_key_type.as_str() {
"ssh-rsa" => {
let ebytes = ssh_buffer.get_bytes().change_context_lazy(|| {
PublicKeyReadError::CouldNotFindRsaConstant { constant_name: "e" }
})?;
let nbytes = ssh_buffer.get_bytes().change_context_lazy(|| {
PublicKeyReadError::CouldNotFindRsaConstant { constant_name: "n" }
})?;
if ssh_buffer.has_remaining() {
return Err(report!(PublicKeyReadError::ExtraneousInfo {
key_type: "RSA"
}));
}
let e = BigUint::from_bytes_be(&ebytes);
let n = BigUint::from_bytes_be(&nbytes);
Ok(PublicKey::Rsa(rsa::PublicKey::new(n, e)))
}
"ssh-ed25519" => {
let point_bytes = ssh_buffer
.get_bytes()
.change_context(PublicKeyReadError::NoEd25519Data)?;
if ssh_buffer.has_remaining() {
return Err(report!(PublicKeyReadError::ExtraneousInfo {
key_type: "ed25519"
}));
}
let point = ed25519_dalek::VerifyingKey::try_from(point_bytes.as_ref())
.map_err(|error| report!(PublicKeyReadError::InvalidEd25519Data { error }))?;
Ok(PublicKey::Ed25519(point))
}
"ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521" => {
let curve = ssh_buffer
.get_string()
.change_context(PublicKeyReadError::CouldNotReadCurve)?;
match (encoded_key_type.as_str(), curve.as_str()) {
("ecdsa-sha2-nistp256", "nistp256") => {}
("ecdsa-sha2-nistp384", "nistp384") => {}
("ecdsa-sha2-nistp521", "nistp521") => {}
_ => {
return Err(report!(PublicKeyReadError::MismatchedCurveInfo {
key_type: encoded_key_type,
curve,
}))
}
}
let encoded_point_bytes = ssh_buffer.get_bytes().change_context_lazy(|| {
PublicKeyReadError::CouldNotReadEcdsaPoint {
curve: curve.clone(),
}
})?;
match curve.as_str() {
"nistp256" => {
let point = p256::EncodedPoint::from_bytes(&encoded_point_bytes).map_err(
|error| {
report!(PublicKeyReadError::InvalidEcdsaPoint {
curve: curve.clone(),
error: error.into()
})
},
)?;
let public = p256::PublicKey::from_encoded_point(&point)
.into_option()
.ok_or_else(|| PublicKeyReadError::InvalidEcdsaPublicValue {
curve: curve.clone(),
})?;
Ok(PublicKey::P256(public))
}
"nistp384" => {
let point = p384::EncodedPoint::from_bytes(&encoded_point_bytes).map_err(
|error| {
report!(PublicKeyReadError::InvalidEcdsaPoint {
curve: curve.clone(),
error: error.into()
})
},
)?;
let public = p384::PublicKey::from_encoded_point(&point)
.into_option()
.ok_or_else(|| PublicKeyReadError::InvalidEcdsaPublicValue {
curve: curve.clone(),
})?;
Ok(PublicKey::P384(public))
}
"nistp521" => {
let point = p521::EncodedPoint::from_bytes(&encoded_point_bytes).map_err(
|error| {
report!(PublicKeyReadError::InvalidEcdsaPoint {
curve: curve.clone(),
error: error.into()
})
},
)?;
let public = p521::PublicKey::from_encoded_point(&point)
.into_option()
.ok_or_else(|| PublicKeyReadError::InvalidEcdsaPublicValue {
curve: curve.clone(),
})?;
Ok(PublicKey::P521(public))
}
_ => panic!(
"Should not be able to have a mismatched curve, but have {}",
curve
),
}
}
_ => Err(report!(PublicKeyReadError::UnrecognizedKeyType {
key_type: encoded_key_type
})),
}
}
}
#[test]
fn short_invalid_buffer_fails_correctly() {
let buffer = Bytes::from(vec![0, 1]);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(matches!(
error.current_context(),
&PublicKeyReadError::NoPublicKeyType
));
let buffer = Bytes::from(b"\x00\x00\x00\x05hippo".to_vec());
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), &PublicKeyReadError::UnrecognizedKeyType { ref key_type } if key_type.as_str() == "hippo")
);
}
#[cfg(test)]
const ECDSA256_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x41, 0x04, 0xd7, 0x47, 0x00, 0x93, 0x35, 0xc5, 0x88, 0xc1,
0x67, 0xb5, 0x1d, 0x5f, 0xf1, 0x9b, 0x82, 0x1d, 0xe8, 0x37, 0x21, 0xe7, 0x89, 0xe5, 0x7c, 0x14,
0x6a, 0xd7, 0xfe, 0x43, 0x44, 0xe7, 0x67, 0xd8, 0x05, 0x66, 0xe1, 0x96, 0x12, 0x8f, 0xc9, 0x23,
0x1c, 0x8f, 0x25, 0x0e, 0xa7, 0xf1, 0xcd, 0x76, 0x7a, 0xea, 0xb7, 0x87, 0x24, 0x07, 0x1e, 0x72,
0x63, 0x6b, 0x81, 0xde, 0x20, 0x81, 0xe7, 0x82,
];
#[cfg(test)]
const ECDSA384_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x33, 0x38, 0x34, 0x00, 0x00, 0x00, 0x61, 0x04, 0x0d, 0xa3, 0x8b, 0x42, 0x98, 0x15, 0xba, 0x0c,
0x9b, 0xf6, 0x5e, 0xc8, 0x68, 0xc3, 0x1e, 0x44, 0xb2, 0x6f, 0x12, 0x2f, 0xc8, 0x97, 0x81, 0x23,
0x60, 0xa0, 0xc3, 0xaf, 0xf1, 0x3f, 0x5f, 0xd6, 0xea, 0x49, 0x9c, 0xd6, 0x74, 0x34, 0xd0, 0x6a,
0xd0, 0x34, 0xe4, 0xd8, 0x42, 0x00, 0x94, 0x61, 0x63, 0x15, 0x11, 0xb0, 0x63, 0x52, 0xcc, 0xbe,
0xe5, 0xc2, 0x12, 0x33, 0xdc, 0x36, 0x03, 0x60, 0x6c, 0xb9, 0x11, 0xa6, 0xe4, 0x81, 0x64, 0x4a,
0x54, 0x74, 0x2b, 0xfb, 0xbc, 0xff, 0x90, 0xe0, 0x2c, 0x00, 0xc1, 0xae, 0x99, 0x2e, 0x0f, 0xdb,
0x50, 0xec, 0x4c, 0xe8, 0xbd, 0xf1, 0x0f, 0xdc,
];
#[cfg(test)]
const ECDSA521_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x6e,
0x69, 0x73, 0x74, 0x70, 0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, 0x74, 0x70,
0x35, 0x32, 0x31, 0x00, 0x00, 0x00, 0x85, 0x04, 0x01, 0x68, 0x9a, 0x37, 0xac, 0xa3, 0x16, 0x26,
0xa4, 0xaa, 0x72, 0xe6, 0x24, 0x40, 0x4c, 0x69, 0xbf, 0x11, 0x9e, 0xcd, 0xb6, 0x63, 0x92, 0x10,
0xa6, 0xb7, 0x6e, 0x98, 0xb4, 0xa0, 0x81, 0xc5, 0x3c, 0x88, 0xfa, 0x9b, 0x60, 0x57, 0x4c, 0x0f,
0xba, 0x36, 0x4e, 0xc6, 0xe0, 0x3e, 0xa5, 0x86, 0x3d, 0xd3, 0xd5, 0x86, 0x96, 0xe9, 0x4a, 0x1c,
0x0c, 0xe2, 0x70, 0xff, 0x1f, 0x79, 0x06, 0x5d, 0x52, 0x9a, 0x01, 0x2b, 0x87, 0x8e, 0xc2, 0xe9,
0xe2, 0xb7, 0x01, 0x00, 0xa6, 0x1a, 0xf7, 0x23, 0x47, 0x6a, 0x70, 0x10, 0x09, 0x59, 0xde, 0x0a,
0x20, 0xca, 0x2f, 0xd7, 0x5a, 0x98, 0xbd, 0xc3, 0x5b, 0xf2, 0x7b, 0x14, 0x6e, 0x6b, 0xa5, 0x93,
0x5d, 0x3e, 0x21, 0x5c, 0x49, 0x40, 0xbf, 0x9b, 0xc0, 0x78, 0x4b, 0xb1, 0xe9, 0xc7, 0x02, 0xb1,
0x51, 0x94, 0x1a, 0xcf, 0x88, 0x7b, 0xfe, 0xea, 0xd8, 0x55, 0x89, 0xb3,
];
#[test]
fn ecdsa_public_works() {
let buffer = Bytes::from(ECDSA256_TEST_KEY);
let public_key = PublicKey::try_from(buffer).unwrap();
assert!(matches!(public_key, PublicKey::P256(_)));
let buffer = Bytes::from(ECDSA384_TEST_KEY);
let public_key = PublicKey::try_from(buffer).unwrap();
assert!(matches!(public_key, PublicKey::P384(_)));
let buffer = Bytes::from(ECDSA521_TEST_KEY);
let public_key = PublicKey::try_from(buffer).unwrap();
assert!(matches!(public_key, PublicKey::P521(_)));
}
#[test]
fn checks_for_mismatched_curves() {
let mut raw_data = ECDSA256_TEST_KEY.to_vec();
raw_data[32] = 0x33;
raw_data[33] = 0x38;
raw_data[34] = 0x34;
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), PublicKeyReadError::MismatchedCurveInfo { ref key_type, ref curve }
if key_type.as_str() == "ecdsa-sha2-nistp256" && curve.as_str() == "nistp384")
);
}
#[test]
fn invalid_point_errors() {
let mut raw_data = ECDSA256_TEST_KEY.to_vec();
let _ = raw_data.pop().unwrap();
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), PublicKeyReadError::CouldNotReadEcdsaPoint { ref curve }
if curve.as_str() == "nistp256")
);
let mut raw_data = ECDSA256_TEST_KEY.to_vec();
raw_data[64] = 0x33;
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), PublicKeyReadError::InvalidEcdsaPublicValue { ref curve }
if curve.as_str() == "nistp256")
);
let mut raw_data = ECDSA256_TEST_KEY.to_vec();
raw_data[39] = 0x33;
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), PublicKeyReadError::InvalidEcdsaPoint { ref curve, .. }
if curve.as_str() == "nistp256")
);
}
#[cfg(test)]
const ED25519_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00,
0x00, 0x00, 0x20, 0x80, 0xe2, 0x47, 0x6a, 0x6f, 0xcb, 0x13, 0x7a, 0x0e, 0xda, 0x9b, 0x06, 0x3c,
0x4d, 0xd7, 0x24, 0xdb, 0x31, 0x1b, 0xa9, 0xc5, 0xc3, 0x44, 0x5b, 0xda, 0xff, 0x85, 0x51, 0x15,
0x63, 0x58, 0xd3,
];
#[test]
fn ed25519_public_works() {
let buffer = Bytes::from(ED25519_TEST_KEY);
let public_key = PublicKey::try_from(buffer).unwrap();
assert!(matches!(public_key, PublicKey::Ed25519(_)));
}
#[test]
fn shortened_ed25519_fails() {
let buffer = Bytes::from(&ED25519_TEST_KEY[0..ED25519_TEST_KEY.len() - 2]);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(matches!(
error.current_context(),
&PublicKeyReadError::NoEd25519Data
));
}
#[test]
fn invalid_data_kills_ed25519_read() {
let mut raw_data = ED25519_TEST_KEY.to_vec();
raw_data[19] = 0x00;
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(matches!(
error.current_context(),
&PublicKeyReadError::InvalidEd25519Data { .. }
));
}
#[test]
fn extraneous_data_kills_ed25519_read() {
let mut raw_data = ED25519_TEST_KEY.to_vec();
raw_data.push(0);
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(matches!(
error.current_context(),
&PublicKeyReadError::ExtraneousInfo { .. }
));
}
#[cfg(test)]
const RSA_TEST_KEY: &[u8] = &[
0x00, 0x00, 0x00, 0x07, 0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61, 0x00, 0x00, 0x00, 0x03, 0x01,
0x00, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0xb7, 0x7e, 0xd2, 0x53, 0xf0, 0x92, 0xac, 0x06, 0x53,
0x07, 0x8f, 0xe9, 0x89, 0xd8, 0x92, 0xd4, 0x08, 0x7e, 0xdd, 0x6b, 0xa4, 0x67, 0xd8, 0xac, 0x4a,
0x3b, 0x8f, 0xbd, 0x2f, 0x3a, 0x19, 0x46, 0x7c, 0xa5, 0x7f, 0xc1, 0x01, 0xee, 0xe3, 0xbf, 0x9e,
0xaf, 0xed, 0xc8, 0xbc, 0x8c, 0x30, 0x70, 0x6f, 0xf1, 0xdd, 0xb9, 0x9b, 0x4c, 0x67, 0x7b, 0x8f,
0x7c, 0xcf, 0x85, 0x6f, 0x28, 0x5f, 0xeb, 0xe3, 0x0b, 0x7f, 0x82, 0xf5, 0xa4, 0x99, 0xc6, 0xae,
0x1c, 0xbd, 0xd6, 0xa9, 0x34, 0xc9, 0x05, 0xfc, 0xdc, 0xe2, 0x84, 0x86, 0x69, 0xc5, 0x6b, 0x0a,
0xf5, 0x17, 0x5f, 0x52, 0xda, 0x4a, 0xdf, 0xd9, 0x4a, 0xe2, 0x14, 0x0c, 0xba, 0x96, 0x04, 0x4e,
0x25, 0x38, 0xd1, 0x66, 0x75, 0xf2, 0x27, 0x68, 0x1f, 0x28, 0xce, 0xa5, 0xa3, 0x22, 0x05, 0xf7,
0x9e, 0x38, 0x70, 0xf7, 0x23, 0x65, 0xfe, 0x4e, 0x77, 0x66, 0x70, 0x16, 0x89, 0xa3, 0xa7, 0x1b,
0xbd, 0x6d, 0x94, 0x85, 0xa1, 0x6b, 0xe8, 0xf1, 0xb9, 0xb6, 0x7f, 0x4f, 0xb4, 0x53, 0xa7, 0xfe,
0x2d, 0x89, 0x6a, 0x6e, 0x6d, 0x63, 0x85, 0xe1, 0x00, 0x83, 0x01, 0xb0, 0x00, 0x8a, 0x30, 0xde,
0xdc, 0x2f, 0x30, 0xbc, 0x89, 0x66, 0x2a, 0x28, 0x59, 0x31, 0xd9, 0x74, 0x9c, 0xf2, 0xf1, 0xd7,
0x53, 0xa9, 0x7b, 0xeb, 0x97, 0xfd, 0x53, 0x13, 0x66, 0x59, 0x9d, 0x61, 0x4a, 0x72, 0xf4, 0xa9,
0x22, 0xc8, 0xac, 0x0e, 0xd8, 0x0e, 0x4f, 0x15, 0x59, 0x9b, 0xaa, 0x96, 0xf9, 0xd5, 0x61, 0xd5,
0x04, 0x4c, 0x09, 0x0d, 0x5a, 0x4e, 0x39, 0xd6, 0xbe, 0x16, 0x8c, 0x36, 0xe1, 0x1d, 0x59, 0x5a,
0xa5, 0x5c, 0x50, 0x6b, 0x6f, 0x6a, 0xed, 0x63, 0x04, 0xbc, 0x42, 0xec, 0xcb, 0xea, 0x34, 0xfc,
0x75, 0xcc, 0xd1, 0xca, 0x45, 0x66, 0xd0, 0xc9, 0x14, 0xae, 0x83, 0xd0, 0x7c, 0x0e, 0x06, 0x1d,
0x4f, 0x15, 0x64, 0x53, 0x56, 0xdb, 0xf2, 0x49, 0x83, 0x03, 0xae, 0xda, 0xa7, 0x29, 0x7c, 0x42,
0xbf, 0x82, 0x07, 0xbc, 0x44, 0x09, 0x15, 0x32, 0x4d, 0xc0, 0xdf, 0x8a, 0x04, 0x89, 0xd9, 0xd8,
0xdb, 0x05, 0xa5, 0x60, 0x21, 0xed, 0xcb, 0x54, 0x74, 0x1e, 0x24, 0x06, 0x4d, 0x69, 0x93, 0x72,
0xe8, 0x59, 0xe1, 0x93, 0x1a, 0x6e, 0x48, 0x16, 0x31, 0x38, 0x10, 0x0e, 0x0b, 0x34, 0xeb, 0x20,
0x86, 0x9c, 0x60, 0x68, 0xaf, 0x30, 0x5e, 0x7f, 0x26, 0x37, 0xce, 0xd9, 0xc1, 0x47, 0xdf, 0x2d,
0xba, 0x50, 0x96, 0xcf, 0xf8, 0xf5, 0xe8, 0x65, 0x26, 0x18, 0x4a, 0x88, 0xe0, 0xd8, 0xab, 0x24,
0xde, 0x3f, 0xa9, 0x64, 0x94, 0xe3, 0xaf, 0x7b, 0x43, 0xaa, 0x72, 0x64, 0x7c, 0xef, 0xdb, 0x30,
0x87, 0x7d, 0x70, 0xd7, 0xbe, 0x0a, 0xca, 0x79, 0xe6, 0xb8, 0x3e, 0x23, 0x37, 0x17, 0x7d, 0x0c,
0x41, 0x3d, 0xd9, 0x92, 0xd6, 0x8c, 0x95, 0x8b, 0x63, 0x0b, 0x63, 0x49, 0x98, 0x0f, 0x1f, 0xc1,
0x95, 0x94, 0x6f, 0x22, 0x0e, 0x47, 0x8f, 0xee, 0x12, 0xb9, 0x8e, 0x28, 0xc2, 0x94, 0xa2, 0xd4,
0x0a, 0x79, 0x69, 0x93, 0x8a, 0x6f, 0xf4, 0xae, 0xd1, 0x85, 0x11, 0xbb, 0x6c, 0xd5, 0x41, 0x00,
0x71, 0x9b, 0x24, 0xe4, 0x6d, 0x0a, 0x05, 0x07, 0x4c, 0x28, 0xa6, 0x88, 0x8c, 0xea, 0x74, 0x19,
0x64, 0x26, 0x5a, 0xc8, 0x28, 0xcc, 0xdf, 0xa8, 0xea, 0xa7, 0xda, 0xec, 0x03, 0xcd, 0xcb, 0xf3,
0xd7, 0x6b, 0xb6, 0x4a, 0xd8, 0x50, 0x44, 0x91, 0xde, 0xb2, 0x76, 0x6e, 0x85, 0x21, 0x4b, 0x2f,
0x65, 0x57, 0x76, 0xd3, 0xd9, 0xfa, 0xd2, 0x98, 0xcb, 0x47, 0xaa, 0x33, 0x69, 0x4e, 0x83, 0x75,
0xfe, 0x8e, 0xac, 0x0a, 0xf6, 0xb6, 0xb7,
];
#[test]
fn rsa_public_works() {
let buffer = Bytes::from(RSA_TEST_KEY);
let public_key = PublicKey::try_from(buffer).unwrap();
assert!(matches!(public_key, PublicKey::Rsa(_)));
}
#[test]
fn rsa_requires_both_constants() {
let buffer = Bytes::from(&RSA_TEST_KEY[0..RSA_TEST_KEY.len() - 2]);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), &PublicKeyReadError::CouldNotFindRsaConstant { constant_name } if constant_name == "n")
);
let buffer = Bytes::from(&RSA_TEST_KEY[0..RSA_TEST_KEY.len() - 520]);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(
matches!(error.current_context(), &PublicKeyReadError::CouldNotFindRsaConstant { constant_name } if constant_name == "e")
);
}
#[test]
fn extraneous_data_kills_rsa_read() {
let mut raw_data = RSA_TEST_KEY.to_vec();
raw_data.push(0);
let buffer = Bytes::from(raw_data);
let error = PublicKey::try_from(buffer).unwrap_err();
assert!(matches!(
error.current_context(),
&PublicKeyReadError::ExtraneousInfo { .. }
));
}

204
keys/src/public_key_file.rs Normal file
View File

@@ -0,0 +1,204 @@
use crate::public_key::PublicKey;
use base64::engine::{self, Engine};
use bytes::Bytes;
use error_stack::{report, ResultExt};
use std::path::Path;
#[derive(Debug, thiserror::Error)]
pub enum PublicKeyLoadError {
#[error("Could not read file {file}: {error}")]
CouldNotRead {
file: String,
error: tokio::io::Error,
},
#[error("Base64 decoding error in {file}: {error}")]
Base64 {
file: String,
error: base64::DecodeError,
},
#[error("Could not find key type information in {file}")]
NoSshType { file: String },
#[error("Invalid public key material found in {file}")]
InvalidKeyMaterial { file: String },
#[error(
"Inconsistent key type found between stated type, {alleged}, and the encoded {found} key"
)]
InconsistentKeyType {
alleged: String,
found: &'static str,
},
}
impl PublicKey {
/// Loads one or more public keys from an OpenSSH-formatted file.
///
/// The given file should be created by `ssh-keygen` or similar from the
/// OpenSSL suite, or from Hush tooling. Other SSH implementations may use
/// other formats that are not understood by this function. This function
/// will work on public key files where the contents have been concatenated,
/// one per line.
///
/// Returns the key(s) loaded from the file, or an error if the file is in an
/// invalid format or contains a nonsensical crypto value. Each public key is
/// paired with the extra info that is appended at the end of the key, for
/// whatever use it might have.
pub async fn load<P: AsRef<Path>>(
path: P,
) -> error_stack::Result<Vec<(Self, String)>, PublicKeyLoadError> {
let mut results = vec![];
let path = path.as_ref();
let all_public_key_data = tokio::fs::read_to_string(path).await.map_err(|error| {
report!(PublicKeyLoadError::CouldNotRead {
file: path.to_path_buf().display().to_string(),
error,
})
})?;
for public_key_data in all_public_key_data.lines() {
let (alleged_key_type, rest) = public_key_data.split_once(' ').ok_or_else(|| {
report!(PublicKeyLoadError::NoSshType {
file: path.to_path_buf().display().to_string(),
})
})?;
let (key_material, info) = rest.split_once(' ').unwrap_or((rest, ""));
let key_material = engine::general_purpose::STANDARD
.decode(key_material)
.map_err(|de| {
report!(PublicKeyLoadError::Base64 {
file: path.to_path_buf().display().to_string(),
error: de,
})
})?;
let key_material = Bytes::from(key_material);
let public_key = PublicKey::try_from(key_material).change_context_lazy(|| {
PublicKeyLoadError::InvalidKeyMaterial {
file: path.to_path_buf().display().to_string(),
}
})?;
if alleged_key_type != public_key.ssh_key_type_name() {
return Err(report!(PublicKeyLoadError::InconsistentKeyType {
alleged: alleged_key_type.to_string(),
found: public_key.ssh_key_type_name(),
}));
}
results.push((public_key, info.to_string()));
}
Ok(results)
}
}
#[tokio::test]
async fn test_keys_parse() {
let mut parsed_keys = vec![];
let test_key_directory = format!("{}/tests/ssh_keys", env!("CARGO_MANIFEST_DIR"));
let mut directory_reader = tokio::fs::read_dir(test_key_directory)
.await
.expect("can read test key directory");
while let Ok(Some(entry)) = directory_reader.next_entry().await {
if matches!(entry.path().extension(), Some(ext) if ext == "pub") {
let mut public_keys = PublicKey::load(entry.path())
.await
.expect("can parse saved public key");
parsed_keys.append(&mut public_keys);
}
}
assert_eq!(18, parsed_keys.len());
}
#[tokio::test]
async fn concatenated_keys_parse() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
let ecdsa1 = tokio::fs::read(format!(
"{}/tests/ssh_keys/ecdsa1.pub",
env!("CARGO_MANIFEST_DIR")
))
.await
.unwrap();
named_temp.write_all(&ecdsa1).unwrap();
let ecdsa2 = tokio::fs::read(format!(
"{}/tests/ssh_keys/ecdsa2.pub",
env!("CARGO_MANIFEST_DIR")
))
.await
.unwrap();
named_temp.write_all(&ecdsa2).unwrap();
let path = named_temp.into_temp_path();
let parsed_keys = PublicKey::load(&path).await.unwrap();
assert_eq!(2, parsed_keys.len());
}
#[tokio::test]
async fn file_errors_are_caught() {
let result = PublicKey::load("--capitan--").await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PublicKeyLoadError::CouldNotRead { .. })));
}
#[tokio::test]
async fn key_file_must_have_space() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "foobar").unwrap();
let path = named_temp.into_temp_path();
let result = PublicKey::load(path).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PublicKeyLoadError::NoSshType { .. })));
}
#[tokio::test]
async fn key_file_must_have_valid_base64() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(named_temp, "ssh-ed25519 foobar").unwrap();
let path = named_temp.into_temp_path();
let result = PublicKey::load(path).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PublicKeyLoadError::Base64 { .. })));
}
#[tokio::test]
async fn checks_for_valid_key() {
use std::io::Write;
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
writeln!(
named_temp,
"ssh-ed25519 {}",
base64::engine::general_purpose::STANDARD.encode(b"foobar")
)
.unwrap();
let path = named_temp.into_temp_path();
let result = PublicKey::load(path).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PublicKeyLoadError::InvalidKeyMaterial { .. })));
}
#[tokio::test]
async fn mismatched_key_types_are_caught() {
use std::io::Write;
let test_rsa_key = format!("{}/tests/ssh_keys/rsa4096a.pub", env!("CARGO_MANIFEST_DIR"));
let rsa_key_contents = tokio::fs::read_to_string(test_rsa_key).await.unwrap();
let (_, rest) = rsa_key_contents.split_once(' ').unwrap();
println!("rest: {:?}", rest);
let mut named_temp = tempfile::NamedTempFile::new().unwrap();
write!(named_temp, "ssh-ed25519 ").unwrap();
writeln!(named_temp, "{}", rest).unwrap();
let path = named_temp.into_temp_path();
let result = PublicKey::load(path).await;
assert!(matches!(result, Err(e) if
matches!(e.current_context(), PublicKeyLoadError::InconsistentKeyType { .. })));
}