Workspacify
This commit is contained in:
23
keys/Cargo.toml
Normal file
23
keys/Cargo.toml
Normal 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
195
keys/src/buffer.rs
Normal 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
10
keys/src/lib.rs
Normal 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
844
keys/src/private_key.rs
Normal 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")));
|
||||
}
|
||||
816
keys/src/private_key_file.rs
Normal file
816
keys/src/private_key_file.rs
Normal 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
448
keys/src/public_key.rs
Normal 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
204
keys/src/public_key_file.rs
Normal 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 { .. })));
|
||||
}
|
||||
Reference in New Issue
Block a user