Compare commits
6 Commits
main
...
initial-ss
| Author | SHA1 | Date | |
|---|---|---|---|
| d036997de3 | |||
| 9fe5b78962 | |||
| 31cd34d280 | |||
| 268ca2d1a5 | |||
| b30823a502 | |||
| a813b65535 |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -13,9 +13,6 @@ Cargo.lock
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
tarpaulin-report.html
|
||||
proptest-regressions/
|
||||
config.toml
|
||||
|
||||
2914
Cargo.lock
generated
Normal file
2914
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
80
Cargo.toml
Normal file
80
Cargo.toml
Normal file
@@ -0,0 +1,80 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"client",
|
||||
"configuration",
|
||||
"crypto",
|
||||
"host",
|
||||
"keys",
|
||||
"resolver",
|
||||
"server",
|
||||
"ssh",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Adam Wick <awick@uhsure.com>"]
|
||||
categories = ["cryptography", "network-programming", "security"]
|
||||
description = "A collection of privacy- and security-focused network protocol implementations."
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/acw/hushd"
|
||||
keywords = ["hush", "security", "dns", "ssh", "ssl", "tor"]
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/acw/hushd"
|
||||
rust-version = "1.85.0"
|
||||
version = "0.1.0"
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
aes = { version = "0.8.4", features = ["zeroize"] }
|
||||
base64 = "0.22.1"
|
||||
bcrypt-pbkdf = "0.10.0"
|
||||
bytes = "1.10.1"
|
||||
clap = { version = "4.5.35", features = ["derive"] }
|
||||
console-subscriber = "0.4.1"
|
||||
ctr = "0.9.2"
|
||||
ed25519-dalek = "2.1.1"
|
||||
elliptic-curve = { version = "0.13.8", features = ["alloc", "digest", "ecdh", "pem", "pkcs8", "sec1", "serde", "std", "hash2curve", "voprf"] }
|
||||
error-stack = "0.5.0"
|
||||
futures = "0.3.31"
|
||||
generic-array = "0.14.7"
|
||||
getrandom = "0.3.2"
|
||||
internment = { version = "0.8.6", features = ["arc"] }
|
||||
itertools = "0.14.0"
|
||||
nix = { version = "0.29.0", features = ["user"] }
|
||||
num-bigint-dig = { version = "0.8.4", features = ["arbitrary", "i128", "zeroize", "prime", "rand"] }
|
||||
num-integer = { version = "0.1.46", features = ["i128"] }
|
||||
num-traits = { version = "0.2.19", features = ["i128"] }
|
||||
num_enum = "0.7.3"
|
||||
p256 = { version = "0.13.2", features = ["ecdh", "ecdsa-core", "hash2curve", "serde", "test-vectors"] }
|
||||
p384 = { version = "0.13.1", features = ["ecdh", "ecdsa-core", "hash2curve", "serde", "test-vectors"] }
|
||||
p521 = { version = "0.13.3", features = ["ecdh", "ecdsa-core", "hash2curve", "serde", "test-vectors"] }
|
||||
proptest = "1.6.0"
|
||||
proptest-derive = "0.5.1"
|
||||
rand = "0.9.0"
|
||||
rand_chacha = "0.9.0"
|
||||
rustix = "1.0.5"
|
||||
sec1 = "0.7.3"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
tempfile = "3.19.1"
|
||||
thiserror = "2.0.12"
|
||||
tokio = { version = "1.44.2", features = ["full", "tracing"] }
|
||||
toml = "0.8.20"
|
||||
tracing = "0.1.41"
|
||||
tracing-core = "0.1.33"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "tracing", "json"] }
|
||||
url = "2.5.4"
|
||||
whoami = { version = "1.6.0", default-features = false }
|
||||
xdg = "2.5.2"
|
||||
zeroize = "1.8.1"
|
||||
|
||||
client = { path = "./client" }
|
||||
configuration = { path = "./configuration" }
|
||||
crypto = { path = "./crypto" }
|
||||
host = { path = "./host" }
|
||||
keys = { path = "./keys" }
|
||||
resolver = { path = "./resolver" }
|
||||
server = { path = "./server" }
|
||||
ssh = { path = "./ssh" }
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2025 Adam Wick
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
14
client/Cargo.toml
Normal file
14
client/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "client"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
configuration = { workspace = true }
|
||||
crypto = { workspace = true }
|
||||
error-stack = { workspace = true }
|
||||
host = { workspace = true }
|
||||
resolver = { workspace = true }
|
||||
ssh = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
whoami = { workspace = true }
|
||||
212
client/src/lib.rs
Normal file
212
client/src/lib.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use configuration::client::{ClientCommand, ClientConfiguration};
|
||||
use crypto::{
|
||||
CompressionAlgorithm, EncryptionAlgorithm, HostKeyAlgorithm, KeyExchangeAlgorithm, MacAlgorithm,
|
||||
};
|
||||
use host::Host;
|
||||
use resolver::Resolver;
|
||||
use ssh::{self, SshKeyExchange};
|
||||
use ssh::OperationalError;
|
||||
use error_stack::{report, Report, ResultExt};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
use tracing::Instrument;
|
||||
|
||||
pub async fn hush(base_config: ClientConfiguration) -> error_stack::Result<(), OperationalError> {
|
||||
match base_config.command() {
|
||||
ClientCommand::ListMacAlgorithms => print_options(MacAlgorithm::allowed()),
|
||||
|
||||
ClientCommand::ListHostKeyAlgorithms => print_options(HostKeyAlgorithm::allowed()),
|
||||
|
||||
ClientCommand::ListEncryptionAlgorithms => print_options(EncryptionAlgorithm::allowed()),
|
||||
|
||||
ClientCommand::ListKeyExchangeAlgorithms => print_options(KeyExchangeAlgorithm::allowed()),
|
||||
|
||||
ClientCommand::ListCompressionAlgorithms => print_options(CompressionAlgorithm::allowed()),
|
||||
|
||||
ClientCommand::Connect { target } => connect(&base_config, target).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Target {
|
||||
username: String,
|
||||
host: Host,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl fmt::Display for Target {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}@{}:{}", self.username, self.host, self.port)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TargetParseError {
|
||||
#[error("Invalid port number '{port_string}': {error}")]
|
||||
InvalidPort {
|
||||
port_string: String,
|
||||
error: std::num::ParseIntError,
|
||||
},
|
||||
#[error("Invalid hostname '{hostname}': {error}")]
|
||||
InvalidHostname {
|
||||
hostname: String,
|
||||
error: host::HostParseError,
|
||||
},
|
||||
}
|
||||
|
||||
impl FromStr for Target {
|
||||
type Err = Report<TargetParseError>;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (username, host_and_port) = match s.split_once('@') {
|
||||
None => (whoami::username(), s),
|
||||
Some((username, rest)) => (username.to_string(), rest),
|
||||
};
|
||||
|
||||
let (port, host) = match host_and_port.rsplit_once(':') {
|
||||
None => (22, host_and_port),
|
||||
Some((host, portstr)) => match u16::from_str(portstr) {
|
||||
Ok(port) => (port, host),
|
||||
Err(error) => {
|
||||
return Err(report!(TargetParseError::InvalidPort {
|
||||
port_string: portstr.to_string(),
|
||||
error,
|
||||
})
|
||||
.attach_printable(format!("from target string {:?}", s)))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let host = Host::from_str(host)
|
||||
.map_err(|error| {
|
||||
report!(TargetParseError::InvalidHostname {
|
||||
hostname: host.to_string(),
|
||||
error,
|
||||
})
|
||||
})
|
||||
.attach_printable(format!("from target string {:?}", s))?;
|
||||
|
||||
Ok(Target {
|
||||
username,
|
||||
host,
|
||||
port,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code, unused_variables)]
|
||||
async fn connect(
|
||||
base_config: &ClientConfiguration,
|
||||
target: &str,
|
||||
) -> error_stack::Result<(), OperationalError> {
|
||||
let mut resolver: Resolver = Resolver::new(&base_config.dns_config)
|
||||
.await
|
||||
.change_context(OperationalError::Resolver)?;
|
||||
// let mut resolver = Resolver::new(&base_config.resolver)
|
||||
// .await
|
||||
// .change_context(OperationalError::DnsConfig)?;
|
||||
let target = Target::from_str(target)
|
||||
.change_context(OperationalError::UnableToParseHostAddress)
|
||||
.attach_printable_lazy(|| format!("target address '{}'", target))?;
|
||||
tracing::trace!(%target, "determined SSH target");
|
||||
|
||||
let mut stream = target
|
||||
.host
|
||||
.connect(&mut resolver, 22)
|
||||
.await
|
||||
.change_context(OperationalError::Connection)?;
|
||||
let server_addr_str = stream
|
||||
.peer_addr()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|_| "<unknown>".to_string());
|
||||
let client_addr_str = stream
|
||||
.local_addr()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|_| "<unknown>".to_string());
|
||||
let stream_span = tracing::debug_span!(
|
||||
"client connection",
|
||||
server = %server_addr_str,
|
||||
client = %client_addr_str,
|
||||
);
|
||||
tracing::trace!(%target, "connected to target server");
|
||||
|
||||
let stream_error_info = || server_addr_str.clone();
|
||||
|
||||
let their_preamble = ssh::Preamble::read(&mut stream)
|
||||
.instrument(stream_span.clone())
|
||||
.await
|
||||
.change_context(OperationalError::Connection)?;
|
||||
tracing::trace!("received their preamble");
|
||||
|
||||
ssh::Preamble::default()
|
||||
.write(&mut stream)
|
||||
.instrument(stream_span)
|
||||
.await
|
||||
.change_context(OperationalError::Connection)?;
|
||||
tracing::trace!("wrote our preamble");
|
||||
|
||||
if !their_preamble.preamble.is_empty() {
|
||||
for line in their_preamble.preamble.lines() {
|
||||
tracing::info!("server: {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
software = ?their_preamble.software_name,
|
||||
version = ?their_preamble.software_version,
|
||||
commentary = ?their_preamble.commentary,
|
||||
"received server preamble"
|
||||
);
|
||||
|
||||
let stream = ssh::SshChannel::new(stream).expect("can build new SSH channel");
|
||||
let their_initial = stream
|
||||
.read()
|
||||
.await
|
||||
.attach_printable_lazy(stream_error_info)
|
||||
.change_context(OperationalError::KeyExchange)?
|
||||
.ok_or_else(|| report!(OperationalError::KeyExchange))
|
||||
.attach_printable_lazy(stream_error_info)
|
||||
.attach_printable_lazy(|| "No initial key exchange message found")?;
|
||||
|
||||
let their_kex: SshKeyExchange = SshKeyExchange::try_from(their_initial)
|
||||
.attach_printable_lazy(stream_error_info)
|
||||
.change_context(OperationalError::KeyExchange)?;
|
||||
|
||||
println!("their_key: {:?}", their_kex);
|
||||
|
||||
// let mut rng = rand::thread_rng();
|
||||
//
|
||||
// let packet = stream
|
||||
// .read()
|
||||
// .await
|
||||
// .map_err(|error| OperationalError::Read {
|
||||
// context: "Initial key exchange read",
|
||||
// error,
|
||||
// })?
|
||||
// .expect("has initial packet");
|
||||
// let message_type = SshMessageID::from(packet.buffer[0]);
|
||||
// tracing::debug!(size=packet.buffer.len(), %message_type, "Initial buffer received.");
|
||||
// let keyx = SshKeyExchange::try_from(packet)?;
|
||||
// println!("{:?}", keyx);
|
||||
//
|
||||
// let client_config = config.settings_for("");
|
||||
// let my_kex = SshKeyExchange::new(&mut rng, client_config)?;
|
||||
// stream
|
||||
// .write(my_kex.into())
|
||||
// .await
|
||||
// .map_err(|error| OperationalError::Write {
|
||||
// context: "initial negotiation message",
|
||||
// error,
|
||||
// })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_options<T: fmt::Display>(items: &[T]) -> error_stack::Result<(), OperationalError> {
|
||||
for item in items.iter() {
|
||||
println!("{}", item);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
24
configuration/Cargo.toml
Normal file
24
configuration/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "configuration"
|
||||
edition = "2024"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
console-subscriber = { workspace = true }
|
||||
crypto = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
rustix = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-core = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
url = { workspace = true }
|
||||
xdg = { workspace = true }
|
||||
69
configuration/src/client.rs
Normal file
69
configuration/src/client.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use crate::command_line::ClientArguments;
|
||||
pub use crate::command_line::ClientCommand;
|
||||
use crate::config_file::ConfigFile;
|
||||
use crate::error::ConfigurationError;
|
||||
use crate::logging::LoggingConfiguration;
|
||||
use crate::resolver::DnsConfig;
|
||||
use crate::runtime::RuntimeConfiguration;
|
||||
use clap::Parser;
|
||||
use std::ffi::OsString;
|
||||
use tracing_core::LevelFilter;
|
||||
|
||||
pub struct ClientConfiguration {
|
||||
pub runtime: RuntimeConfiguration,
|
||||
pub logging: LoggingConfiguration,
|
||||
pub dns_config: DnsConfig,
|
||||
_config_file: Option<ConfigFile>,
|
||||
command: ClientCommand,
|
||||
}
|
||||
|
||||
impl ClientConfiguration {
|
||||
/// Load a basic client configuration for this run.
|
||||
///
|
||||
/// This will parse the process's command line arguments, and parse
|
||||
/// a config file if given, but will not interpret this information
|
||||
/// beyond that required to understand the user's goals for the
|
||||
/// runtime system and logging. For this reason, it is not async,
|
||||
/// as it is responsible for determining all the information we
|
||||
/// will use to generate the runtime.
|
||||
pub fn new<I, T>(args: I) -> Result<Self, ConfigurationError>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<OsString> + Clone,
|
||||
{
|
||||
let mut command_line_arguments = ClientArguments::try_parse_from(args)?;
|
||||
let mut _config_file =
|
||||
ConfigFile::new(command_line_arguments.arguments.config_file.take())?;
|
||||
let mut runtime = RuntimeConfiguration::default();
|
||||
let mut logging = LoggingConfiguration::default();
|
||||
|
||||
// we prefer the command line to the config file, so first merge
|
||||
// in the config file so that when we later merge the command line,
|
||||
// it overwrites any config file options.
|
||||
if let Some(config_file) = _config_file.as_mut() {
|
||||
config_file.merge_standard_options_into(&mut runtime, &mut logging);
|
||||
}
|
||||
command_line_arguments
|
||||
.arguments
|
||||
.merge_standard_options_into(&mut runtime, &mut logging);
|
||||
if command_line_arguments.command.is_list_command() {
|
||||
logging.filter = LevelFilter::ERROR;
|
||||
}
|
||||
|
||||
// FIXME!!
|
||||
let dns_config = DnsConfig::default();
|
||||
|
||||
Ok(ClientConfiguration {
|
||||
runtime,
|
||||
logging,
|
||||
dns_config,
|
||||
_config_file,
|
||||
command: command_line_arguments.command,
|
||||
})
|
||||
}
|
||||
|
||||
// Returns the command we received from the user
|
||||
pub fn command(&self) -> &ClientCommand {
|
||||
&self.command
|
||||
}
|
||||
}
|
||||
217
configuration/src/command_line.rs
Normal file
217
configuration/src/command_line.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use crate::console::ConsoleConfiguration;
|
||||
use crate::logging::{LogTarget, LoggingConfiguration};
|
||||
use crate::runtime::RuntimeConfiguration;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
#[cfg(test)]
|
||||
use std::str::FromStr;
|
||||
use tracing_core::LevelFilter;
|
||||
|
||||
#[derive(Parser, Debug, Default)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct ServerArguments {
|
||||
#[command(flatten)]
|
||||
pub arguments: CommandLineArguments,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Default)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct ClientArguments {
|
||||
#[command(flatten)]
|
||||
pub arguments: CommandLineArguments,
|
||||
|
||||
/// The command we're running as the client
|
||||
#[command(subcommand)]
|
||||
pub command: ClientCommand,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Default)]
|
||||
pub struct CommandLineArguments {
|
||||
/// The config file to use for this command.
|
||||
#[arg(short, long)]
|
||||
pub config_file: Option<PathBuf>,
|
||||
|
||||
/// The number of "normal" threads to use for this process.
|
||||
///
|
||||
/// These are the threads that do the predominant part of the work
|
||||
/// for the system.
|
||||
#[arg(short, long)]
|
||||
threads: Option<usize>,
|
||||
|
||||
/// The number of "blocking" threads to use for this process.
|
||||
///
|
||||
/// These threads are used for long-running operations, or operations
|
||||
/// that require extensive interaction with non-asynchronous-friendly
|
||||
/// IO. This should definitely be >= 1, but does not need to be super
|
||||
/// high.
|
||||
#[arg(short, long)]
|
||||
blocking_threads: Option<usize>,
|
||||
|
||||
/// The place to send log data to.
|
||||
#[arg(short = 'o', long)]
|
||||
log_file: Option<PathBuf>,
|
||||
|
||||
/// The log level to report.
|
||||
#[arg(short, long)]
|
||||
log_level: Option<LevelFilter>,
|
||||
|
||||
/// A network server IP address to use for tokio-console inspection.
|
||||
#[arg(short = 's', long, group = "console")]
|
||||
console_network_server: Option<SocketAddr>,
|
||||
|
||||
/// A unix domain socket address to use for tokio-console inspection.
|
||||
#[arg(short = 'u', long, group = "console")]
|
||||
console_unix_socket: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Subcommand)]
|
||||
pub enum ClientCommand {
|
||||
/// List the key exchange algorithms we currently allow
|
||||
#[default]
|
||||
ListKeyExchangeAlgorithms,
|
||||
/// List the host key algorithms we currently allow
|
||||
ListHostKeyAlgorithms,
|
||||
/// List the encryption algorithms we currently allow
|
||||
ListEncryptionAlgorithms,
|
||||
/// List the MAC algorithms we currently allow
|
||||
ListMacAlgorithms,
|
||||
/// List the compression algorithms we currently allow
|
||||
ListCompressionAlgorithms,
|
||||
/// Connect to the given host and port
|
||||
Connect { target: String },
|
||||
}
|
||||
|
||||
impl ClientCommand {
|
||||
/// Is this a command that's just going to list some information to the console?
|
||||
pub fn is_list_command(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ClientCommand::ListKeyExchangeAlgorithms
|
||||
| ClientCommand::ListHostKeyAlgorithms
|
||||
| ClientCommand::ListEncryptionAlgorithms
|
||||
| ClientCommand::ListMacAlgorithms
|
||||
| ClientCommand::ListCompressionAlgorithms
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandLineArguments {
|
||||
pub fn merge_standard_options_into(
|
||||
&mut self,
|
||||
runtime_config: &mut RuntimeConfiguration,
|
||||
logging_config: &mut LoggingConfiguration,
|
||||
) {
|
||||
if let Some(threads) = self.threads {
|
||||
runtime_config.tokio_worker_threads = threads;
|
||||
}
|
||||
|
||||
if let Some(threads) = self.blocking_threads {
|
||||
runtime_config.tokio_blocking_threads = threads;
|
||||
}
|
||||
|
||||
if let Some(log_file) = self.log_file.take() {
|
||||
logging_config.target = LogTarget::File(log_file);
|
||||
}
|
||||
|
||||
if let Some(log_level) = self.log_level.take() {
|
||||
logging_config.filter = log_level;
|
||||
}
|
||||
|
||||
if (self.console_network_server.is_some() || self.console_unix_socket.is_some())
|
||||
&& runtime_config.console.is_none()
|
||||
{
|
||||
runtime_config.console = Some(ConsoleConfiguration::default());
|
||||
}
|
||||
|
||||
if let Some(cns) = self.console_network_server.take() {
|
||||
if let Some(x) = runtime_config.console.as_mut() {
|
||||
x.server_addr = cns.into();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cus) = self.console_unix_socket.take() {
|
||||
if let Some(x) = runtime_config.console.as_mut() {
|
||||
x.server_addr = cus.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn apply_command_line(
|
||||
mut cmdargs: CommandLineArguments,
|
||||
) -> (RuntimeConfiguration, LoggingConfiguration) {
|
||||
let mut original_runtime = RuntimeConfiguration::default();
|
||||
let mut original_logging = LoggingConfiguration::default();
|
||||
|
||||
cmdargs.merge_standard_options_into(&mut original_runtime, &mut original_logging);
|
||||
|
||||
(original_runtime, original_logging)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_line_wins() {
|
||||
let original_runtime = RuntimeConfiguration::default();
|
||||
|
||||
let cmd = CommandLineArguments {
|
||||
threads: Some(original_runtime.tokio_worker_threads + 1),
|
||||
..CommandLineArguments::default()
|
||||
};
|
||||
let (test1_run, _) = apply_command_line(cmd);
|
||||
assert_ne!(
|
||||
original_runtime.tokio_worker_threads,
|
||||
test1_run.tokio_worker_threads
|
||||
);
|
||||
assert_eq!(
|
||||
original_runtime.tokio_blocking_threads,
|
||||
test1_run.tokio_blocking_threads
|
||||
);
|
||||
|
||||
let cmd = CommandLineArguments {
|
||||
blocking_threads: Some(original_runtime.tokio_blocking_threads + 1),
|
||||
..CommandLineArguments::default()
|
||||
};
|
||||
let (test2_run, _) = apply_command_line(cmd);
|
||||
assert_eq!(
|
||||
original_runtime.tokio_worker_threads,
|
||||
test2_run.tokio_worker_threads
|
||||
);
|
||||
assert_ne!(
|
||||
original_runtime.tokio_blocking_threads,
|
||||
test2_run.tokio_blocking_threads
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_set_console_settings() {
|
||||
let cmd = CommandLineArguments {
|
||||
console_network_server: Some(
|
||||
SocketAddr::from_str("127.0.0.1:8080").expect("reasonable address"),
|
||||
),
|
||||
..CommandLineArguments::default()
|
||||
};
|
||||
let (test1_run, _) = apply_command_line(cmd);
|
||||
assert!(test1_run.console.is_some());
|
||||
assert!(matches!(
|
||||
test1_run.console.unwrap().server_addr,
|
||||
console_subscriber::ServerAddr::Tcp(_)
|
||||
));
|
||||
|
||||
let temp_path = tempfile::NamedTempFile::new()
|
||||
.expect("can build temp file")
|
||||
.into_temp_path();
|
||||
std::fs::remove_file(&temp_path).unwrap();
|
||||
let filename = temp_path.to_path_buf();
|
||||
|
||||
let cmd = CommandLineArguments {
|
||||
console_unix_socket: Some(filename),
|
||||
..CommandLineArguments::default()
|
||||
};
|
||||
let (test2_run, _) = apply_command_line(cmd);
|
||||
assert!(test2_run.console.is_some());
|
||||
assert!(matches!(
|
||||
test2_run.console.unwrap().server_addr,
|
||||
console_subscriber::ServerAddr::Unix(_)
|
||||
));
|
||||
}
|
||||
586
configuration/src/config_file.rs
Normal file
586
configuration/src/config_file.rs
Normal file
@@ -0,0 +1,586 @@
|
||||
use crate::error::ConfigurationError;
|
||||
use crate::logging::{LogMode, LogTarget, LoggingConfiguration};
|
||||
use crate::resolver::DnsConfig;
|
||||
use crate::runtime::RuntimeConfiguration;
|
||||
use crypto::known_algorithms::{
|
||||
ALLOWED_COMPRESSION_ALGORITHMS, ALLOWED_ENCRYPTION_ALGORITHMS, ALLOWED_HOST_KEY_ALGORITHMS,
|
||||
ALLOWED_KEY_EXCHANGE_ALGORITHMS, ALLOWED_MAC_ALGORITHMS,
|
||||
};
|
||||
use proptest::arbitrary::{any, Arbitrary};
|
||||
use proptest::strategy::{BoxedStrategy, Just, Strategy};
|
||||
use serde::de::{self, Unexpected};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
use tracing_core::Level;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct ConfigFile {
|
||||
runtime: Option<RuntimeConfig>,
|
||||
logging: Option<LoggingConfig>,
|
||||
pub resolver: Option<DnsConfig>,
|
||||
pub sockets: Option<HashMap<String, SocketConfig>>,
|
||||
pub keys: HashMap<String, KeyConfig>,
|
||||
defaults: Option<ServerConfig>,
|
||||
servers: HashMap<String, ServerConfig>,
|
||||
}
|
||||
|
||||
impl Arbitrary for ConfigFile {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
||||
(
|
||||
any::<Option<RuntimeConfig>>(),
|
||||
any::<Option<LoggingConfig>>(),
|
||||
any::<Option<DnsConfig>>(),
|
||||
proptest::option::of(keyed_section(SocketConfig::arbitrary())),
|
||||
keyed_section(KeyConfig::arbitrary()),
|
||||
any::<Option<ServerConfig>>(),
|
||||
keyed_section(ServerConfig::arbitrary()),
|
||||
)
|
||||
.prop_map(
|
||||
|(runtime, logging, resolver, sockets, keys, defaults, servers)| ConfigFile {
|
||||
runtime,
|
||||
logging,
|
||||
resolver,
|
||||
sockets,
|
||||
keys,
|
||||
defaults,
|
||||
servers,
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn keyed_section<S>(strat: S) -> BoxedStrategy<HashMap<String, S::Value>>
|
||||
where
|
||||
S: Strategy + 'static,
|
||||
{
|
||||
proptest::collection::hash_map(
|
||||
proptest::string::string_regex("[a-zA-Z0-9-]{1,30}").unwrap(),
|
||||
strat,
|
||||
0..40,
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub struct SocketConfig {
|
||||
path: PathBuf,
|
||||
user: Option<String>,
|
||||
group: Option<String>,
|
||||
permissions: Option<Permissions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
pub enum Permissions {
|
||||
User,
|
||||
Group,
|
||||
UserGroup,
|
||||
Everyone,
|
||||
}
|
||||
|
||||
impl From<Permissions> for rustix::fs::Mode {
|
||||
fn from(value: Permissions) -> Self {
|
||||
match value {
|
||||
Permissions::User => rustix::fs::Mode::RUSR | rustix::fs::Mode::WUSR,
|
||||
|
||||
Permissions::Group => rustix::fs::Mode::RUSR | rustix::fs::Mode::WUSR,
|
||||
|
||||
Permissions::UserGroup => {
|
||||
rustix::fs::Mode::RUSR
|
||||
| rustix::fs::Mode::WUSR
|
||||
| rustix::fs::Mode::RGRP
|
||||
| rustix::fs::Mode::WGRP
|
||||
}
|
||||
|
||||
Permissions::Everyone => {
|
||||
rustix::fs::Mode::RUSR
|
||||
| rustix::fs::Mode::WUSR
|
||||
| rustix::fs::Mode::RGRP
|
||||
| rustix::fs::Mode::WGRP
|
||||
| rustix::fs::Mode::ROTH
|
||||
| rustix::fs::Mode::WOTH
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SocketConfig {
|
||||
pub async fn into_listener(self) -> Result<tokio::net::UnixListener, ConfigurationError> {
|
||||
let base = tokio::net::UnixListener::bind(&self.path).map_err(|error| {
|
||||
ConfigurationError::CouldNotMakeSocket {
|
||||
path: self.path.clone(),
|
||||
error,
|
||||
}
|
||||
})?;
|
||||
|
||||
let user = self
|
||||
.user
|
||||
.map(|x| {
|
||||
nix::unistd::User::from_name(&x)
|
||||
.map_err(|error| ConfigurationError::CouldNotSetPerms {
|
||||
thing: "find user".to_string(),
|
||||
path: self.path.clone(),
|
||||
error: error.into(),
|
||||
})
|
||||
.transpose()
|
||||
.unwrap_or_else(|| {
|
||||
Err(ConfigurationError::CouldNotSetPerms {
|
||||
thing: "find user".to_string(),
|
||||
path: self.path.clone(),
|
||||
error: std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"could not find user",
|
||||
),
|
||||
})
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let group = self
|
||||
.group
|
||||
.map(|x| {
|
||||
nix::unistd::Group::from_name(&x)
|
||||
.map_err(|error| ConfigurationError::CouldNotSetPerms {
|
||||
thing: "find user".to_string(),
|
||||
path: self.path.clone(),
|
||||
error: error.into(),
|
||||
})
|
||||
.transpose()
|
||||
.unwrap_or_else(|| {
|
||||
Err(ConfigurationError::CouldNotSetPerms {
|
||||
thing: "find user".to_string(),
|
||||
path: self.path.clone(),
|
||||
error: std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"could not find user",
|
||||
),
|
||||
})
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
if user.is_some() || group.is_some() {
|
||||
std::os::unix::fs::chown(
|
||||
&self.path,
|
||||
user.map(|x| x.uid.as_raw()),
|
||||
group.map(|x| x.gid.as_raw()),
|
||||
)
|
||||
.map_err(|error| ConfigurationError::CouldNotSetPerms {
|
||||
thing: "set user/group ownership".to_string(),
|
||||
path: self.path.clone(),
|
||||
error,
|
||||
})?;
|
||||
}
|
||||
|
||||
let perms = self.permissions.unwrap_or(Permissions::UserGroup);
|
||||
rustix::fs::chmod(&self.path, perms.into()).map_err(|error| {
|
||||
ConfigurationError::CouldNotSetPerms {
|
||||
thing: "set permissions".to_string(),
|
||||
path: self.path.clone(),
|
||||
error: error.into(),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(base)
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for SocketConfig {
|
||||
type Strategy = BoxedStrategy<SocketConfig>;
|
||||
type Parameters = ();
|
||||
|
||||
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
||||
(
|
||||
PathBuf::arbitrary(),
|
||||
proptest::option::of(proptest::string::string_regex("[a-zA-Z0-9]{1,30}").unwrap()),
|
||||
proptest::option::of(proptest::string::string_regex("[a-zA-Z0-9]{1,30}").unwrap()),
|
||||
proptest::option::of(Permissions::arbitrary()),
|
||||
)
|
||||
.prop_map(|(path, user, group, permissions)| SocketConfig {
|
||||
path,
|
||||
user,
|
||||
group,
|
||||
permissions,
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for Permissions {
|
||||
type Strategy = BoxedStrategy<Permissions>;
|
||||
type Parameters = ();
|
||||
|
||||
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
||||
proptest::prop_oneof![
|
||||
proptest::strategy::Just(Permissions::User),
|
||||
proptest::strategy::Just(Permissions::Group),
|
||||
proptest::strategy::Just(Permissions::UserGroup),
|
||||
proptest::strategy::Just(Permissions::Everyone),
|
||||
]
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct RuntimeConfig {
|
||||
worker_threads: Option<usize>,
|
||||
blocking_threads: Option<usize>,
|
||||
}
|
||||
|
||||
impl Arbitrary for RuntimeConfig {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
||||
(any::<Option<u16>>(), any::<Option<u16>>())
|
||||
.prop_map(|(worker_threads, blocking_threads)| RuntimeConfig {
|
||||
worker_threads: worker_threads.map(Into::into),
|
||||
blocking_threads: blocking_threads.map(Into::into),
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct LoggingConfig {
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "parse_level",
|
||||
serialize_with = "write_level"
|
||||
)]
|
||||
level: Option<Level>,
|
||||
include_filename: Option<bool>,
|
||||
include_lineno: Option<bool>,
|
||||
include_thread_ids: Option<bool>,
|
||||
include_thread_names: Option<bool>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "parse_mode",
|
||||
serialize_with = "write_mode"
|
||||
)]
|
||||
mode: Option<LogMode>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "parse_target",
|
||||
serialize_with = "write_target"
|
||||
)]
|
||||
target: Option<LogTarget>,
|
||||
}
|
||||
|
||||
impl Arbitrary for LoggingConfig {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
|
||||
let level_strat = proptest::prop_oneof![
|
||||
Just(None),
|
||||
Just(Some(Level::TRACE)),
|
||||
Just(Some(Level::DEBUG)),
|
||||
Just(Some(Level::INFO)),
|
||||
Just(Some(Level::WARN)),
|
||||
Just(Some(Level::ERROR)),
|
||||
];
|
||||
|
||||
let mode_strat = proptest::prop_oneof![
|
||||
Just(None),
|
||||
Just(Some(LogMode::Compact)),
|
||||
Just(Some(LogMode::Pretty)),
|
||||
Just(Some(LogMode::Json)),
|
||||
];
|
||||
|
||||
let target_strat = proptest::prop_oneof![
|
||||
Just(None),
|
||||
Just(Some(LogTarget::StdErr)),
|
||||
Just(Some(LogTarget::StdOut)),
|
||||
Just(Some({
|
||||
let tempfile = tempfile::NamedTempFile::new().unwrap();
|
||||
let name = tempfile.into_temp_path();
|
||||
LogTarget::File(name.to_path_buf())
|
||||
})),
|
||||
];
|
||||
|
||||
(
|
||||
level_strat,
|
||||
any::<Option<bool>>(),
|
||||
any::<Option<bool>>(),
|
||||
any::<Option<bool>>(),
|
||||
any::<Option<bool>>(),
|
||||
mode_strat,
|
||||
target_strat,
|
||||
)
|
||||
.prop_map(
|
||||
|(
|
||||
level,
|
||||
include_filename,
|
||||
include_lineno,
|
||||
include_thread_ids,
|
||||
include_thread_names,
|
||||
mode,
|
||||
target,
|
||||
)| LoggingConfig {
|
||||
level,
|
||||
include_filename,
|
||||
include_lineno,
|
||||
include_thread_ids,
|
||||
include_thread_names,
|
||||
mode,
|
||||
target,
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_level<'de, D>(deserializer: D) -> Result<Option<Level>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: Option<String> = Option::deserialize(deserializer)?;
|
||||
|
||||
s.map(|x| match x.to_lowercase().as_str() {
|
||||
"trace" => Ok(Some(Level::TRACE)),
|
||||
"debug" => Ok(Some(Level::DEBUG)),
|
||||
"info" => Ok(Some(Level::INFO)),
|
||||
"warn" => Ok(Some(Level::WARN)),
|
||||
"error" => Ok(Some(Level::ERROR)),
|
||||
_ => Err(de::Error::invalid_value(
|
||||
Unexpected::Str(&x),
|
||||
&"valid logging level (trace, debug, info, warn, or error",
|
||||
)),
|
||||
})
|
||||
.unwrap_or_else(|| Ok(None))
|
||||
}
|
||||
|
||||
fn write_level<S: Serializer>(item: &Option<Level>, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match item {
|
||||
None => serializer.serialize_none(),
|
||||
Some(Level::TRACE) => serializer.serialize_some("trace"),
|
||||
Some(Level::DEBUG) => serializer.serialize_some("debug"),
|
||||
Some(Level::INFO) => serializer.serialize_some("info"),
|
||||
Some(Level::WARN) => serializer.serialize_some("warn"),
|
||||
Some(Level::ERROR) => serializer.serialize_some("error"),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mode<'de, D>(deserializer: D) -> Result<Option<LogMode>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: Option<String> = Option::deserialize(deserializer)?;
|
||||
|
||||
s.map(|x| match x.to_lowercase().as_str() {
|
||||
"compact" => Ok(Some(LogMode::Compact)),
|
||||
"pretty" => Ok(Some(LogMode::Pretty)),
|
||||
"json" => Ok(Some(LogMode::Json)),
|
||||
_ => Err(de::Error::invalid_value(
|
||||
Unexpected::Str(&x),
|
||||
&"valid logging level (trace, debug, info, warn, or error",
|
||||
)),
|
||||
})
|
||||
.unwrap_or_else(|| Ok(None))
|
||||
}
|
||||
|
||||
fn write_mode<S: Serializer>(item: &Option<LogMode>, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match item {
|
||||
None => serializer.serialize_none(),
|
||||
Some(LogMode::Compact) => serializer.serialize_some("compact"),
|
||||
Some(LogMode::Pretty) => serializer.serialize_some("pretty"),
|
||||
Some(LogMode::Json) => serializer.serialize_some("json"),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_target<'de, D>(deserializer: D) -> Result<Option<LogTarget>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: Option<String> = Option::deserialize(deserializer)?;
|
||||
|
||||
Ok(s.map(|x| match x.to_lowercase().as_str() {
|
||||
"stdout" => LogTarget::StdOut,
|
||||
"stderr" => LogTarget::StdErr,
|
||||
_ => LogTarget::File(x.into()),
|
||||
}))
|
||||
}
|
||||
|
||||
fn write_target<S: Serializer>(item: &Option<LogTarget>, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match item {
|
||||
None => serializer.serialize_none(),
|
||||
Some(LogTarget::StdOut) => serializer.serialize_some("stdout"),
|
||||
Some(LogTarget::StdErr) => serializer.serialize_some("stderr"),
|
||||
Some(LogTarget::File(file)) => serializer.serialize_some(file),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct KeyConfig {
|
||||
pub public: PathBuf,
|
||||
pub private: PathBuf,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl Arbitrary for KeyConfig {
|
||||
type Parameters = bool;
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_generate_real_keys: Self::Parameters) -> Self::Strategy {
|
||||
let password = proptest::string::string_regex("[a-zA-Z0-9_!@#$%^&*]{8,40}").unwrap();
|
||||
|
||||
proptest::option::of(password)
|
||||
.prop_map(|password| {
|
||||
let public_file = tempfile::NamedTempFile::new().unwrap();
|
||||
let private_file = tempfile::NamedTempFile::new().unwrap();
|
||||
|
||||
let public = public_file.into_temp_path().to_path_buf();
|
||||
let private = private_file.into_temp_path().to_path_buf();
|
||||
|
||||
KeyConfig {
|
||||
public,
|
||||
private,
|
||||
password,
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
pub struct ServerConfig {
|
||||
key_exchange_algorithms: Option<Vec<String>>,
|
||||
server_host_algorithms: Option<Vec<String>>,
|
||||
encryption_algorithms: Option<Vec<String>>,
|
||||
mac_algorithms: Option<Vec<String>>,
|
||||
compression_algorithms: Option<Vec<String>>,
|
||||
predict: Option<String>,
|
||||
}
|
||||
|
||||
impl Arbitrary for ServerConfig {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
||||
let keyx = proptest::sample::select(ALLOWED_KEY_EXCHANGE_ALGORITHMS);
|
||||
let hostkey = proptest::sample::select(ALLOWED_HOST_KEY_ALGORITHMS);
|
||||
let enc = proptest::sample::select(ALLOWED_ENCRYPTION_ALGORITHMS);
|
||||
let mac = proptest::sample::select(ALLOWED_MAC_ALGORITHMS);
|
||||
let comp = proptest::sample::select(ALLOWED_COMPRESSION_ALGORITHMS);
|
||||
|
||||
(
|
||||
proptest::collection::hash_set(keyx.clone(), 0..ALLOWED_KEY_EXCHANGE_ALGORITHMS.len()),
|
||||
proptest::collection::hash_set(hostkey, 0..ALLOWED_HOST_KEY_ALGORITHMS.len()),
|
||||
proptest::collection::hash_set(enc, 0..ALLOWED_ENCRYPTION_ALGORITHMS.len()),
|
||||
proptest::collection::hash_set(mac, 0..ALLOWED_MAC_ALGORITHMS.len()),
|
||||
proptest::collection::hash_set(comp, 0..ALLOWED_COMPRESSION_ALGORITHMS.len()),
|
||||
proptest::option::of(keyx),
|
||||
)
|
||||
.prop_map(|(kex, host, enc, mac, comp, pred)| ServerConfig {
|
||||
key_exchange_algorithms: finalize_options(kex),
|
||||
server_host_algorithms: finalize_options(host),
|
||||
encryption_algorithms: finalize_options(enc),
|
||||
mac_algorithms: finalize_options(mac),
|
||||
compression_algorithms: finalize_options(comp),
|
||||
predict: pred.map(str::to_string),
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_options(values: HashSet<&str>) -> Option<Vec<String>> {
|
||||
if values.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(values.into_iter().map(str::to_string).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigFileError {
|
||||
#[error("Could not open provided config file: {0}")]
|
||||
CouldNotOpen(std::io::Error),
|
||||
#[error("Could not read config file: {0}")]
|
||||
CouldNotRead(std::io::Error),
|
||||
#[error("Error in config file: {0}")]
|
||||
ParseError(#[from] toml::de::Error),
|
||||
}
|
||||
|
||||
impl ConfigFile {
|
||||
/// Try to read in a config file, using the given path if provided, or the XDG-standard
|
||||
/// path if not.
|
||||
///
|
||||
/// If the user didn't provide a config file, and there isn't a config file in the standard
|
||||
/// XDG place, returns `Ok(None)`.
|
||||
///
|
||||
/// This will return errors if there is a parse error in understanding the file, if
|
||||
/// there's some basic disk error in reading the file, or if the user provided a config
|
||||
/// file that we couldn't find on disk.
|
||||
pub fn new(provided_path: Option<PathBuf>) -> Result<Option<Self>, ConfigFileError> {
|
||||
let config_file = if let Some(path) = provided_path {
|
||||
let file = std::fs::File::open(path).map_err(ConfigFileError::CouldNotOpen)?;
|
||||
Some(file)
|
||||
} else {
|
||||
let Ok(xdg_base_dirs) = xdg::BaseDirectories::with_prefix("hushd") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let path = xdg_base_dirs.find_config_file("config.toml");
|
||||
path.and_then(|x| std::fs::File::open(x).ok())
|
||||
};
|
||||
|
||||
let Some(mut config_file) = config_file else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut contents = String::new();
|
||||
let _ = config_file
|
||||
.read_to_string(&mut contents)
|
||||
.map_err(ConfigFileError::CouldNotRead)?;
|
||||
let config = toml::from_str(&contents)?;
|
||||
|
||||
Ok(Some(config))
|
||||
}
|
||||
|
||||
/// Merge any settings found in the config file into our current configuration.
|
||||
///
|
||||
pub fn merge_standard_options_into(
|
||||
&mut self,
|
||||
runtime: &mut RuntimeConfiguration,
|
||||
_logging: &mut LoggingConfiguration,
|
||||
) {
|
||||
if let Some(runtime_config) = self.runtime.take() {
|
||||
runtime.tokio_worker_threads = runtime_config
|
||||
.worker_threads
|
||||
.unwrap_or(runtime.tokio_worker_threads);
|
||||
runtime.tokio_blocking_threads = runtime_config
|
||||
.blocking_threads
|
||||
.unwrap_or(runtime.tokio_blocking_threads);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_keys_example_parses() {
|
||||
let path = format!("{}/tests/all_keys.toml", env!("CARGO_MANIFEST_DIR"));
|
||||
let result = ConfigFile::new(Some(path.into()));
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn valid_configs_parse(config in ConfigFile::arbitrary()) {
|
||||
use std::io::Write;
|
||||
|
||||
let mut tempfile = tempfile::NamedTempFile::new().unwrap();
|
||||
let contents = toml::to_string(&config).unwrap();
|
||||
tempfile.write_all(contents.as_bytes()).unwrap();
|
||||
let path = tempfile.into_temp_path();
|
||||
|
||||
let parsed = ConfigFile::new(Some(path.to_path_buf())).unwrap().unwrap();
|
||||
assert_eq!(config, parsed);
|
||||
}
|
||||
}
|
||||
78
configuration/src/connection.rs
Normal file
78
configuration/src/connection.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use crypto::known_algorithms::{
|
||||
ALLOWED_COMPRESSION_ALGORITHMS, ALLOWED_ENCRYPTION_ALGORITHMS, ALLOWED_HOST_KEY_ALGORITHMS,
|
||||
ALLOWED_KEY_EXCHANGE_ALGORITHMS, ALLOWED_MAC_ALGORITHMS,
|
||||
};
|
||||
use crypto::{
|
||||
CompressionAlgorithm, EncryptionAlgorithm, HostKeyAlgorithm, KeyExchangeAlgorithm, MacAlgorithm,
|
||||
};
|
||||
use proptest::arbitrary::Arbitrary;
|
||||
use proptest::strategy::{BoxedStrategy, Strategy};
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ClientConnectionOpts {
|
||||
pub key_exchange_algorithms: Vec<KeyExchangeAlgorithm>,
|
||||
pub server_host_key_algorithms: Vec<HostKeyAlgorithm>,
|
||||
pub encryption_algorithms: Vec<EncryptionAlgorithm>,
|
||||
pub mac_algorithms: Vec<MacAlgorithm>,
|
||||
pub compression_algorithms: Vec<CompressionAlgorithm>,
|
||||
pub languages: Vec<String>,
|
||||
pub predict: Option<KeyExchangeAlgorithm>,
|
||||
}
|
||||
|
||||
impl Default for ClientConnectionOpts {
|
||||
fn default() -> Self {
|
||||
ClientConnectionOpts {
|
||||
key_exchange_algorithms: vec![KeyExchangeAlgorithm::Curve25519Sha256],
|
||||
server_host_key_algorithms: vec![HostKeyAlgorithm::Ed25519],
|
||||
encryption_algorithms: vec![EncryptionAlgorithm::Aes256Ctr],
|
||||
mac_algorithms: vec![MacAlgorithm::HmacSha256],
|
||||
compression_algorithms: vec![],
|
||||
languages: vec![],
|
||||
predict: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for ClientConnectionOpts {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
||||
let keyx = proptest::sample::select(ALLOWED_KEY_EXCHANGE_ALGORITHMS);
|
||||
let hostkey = proptest::sample::select(ALLOWED_HOST_KEY_ALGORITHMS);
|
||||
let enc = proptest::sample::select(ALLOWED_ENCRYPTION_ALGORITHMS);
|
||||
let mac = proptest::sample::select(ALLOWED_MAC_ALGORITHMS);
|
||||
let comp = proptest::sample::select(ALLOWED_COMPRESSION_ALGORITHMS);
|
||||
|
||||
(
|
||||
proptest::collection::hash_set(keyx.clone(), 1..ALLOWED_KEY_EXCHANGE_ALGORITHMS.len()),
|
||||
proptest::collection::hash_set(hostkey, 1..ALLOWED_HOST_KEY_ALGORITHMS.len()),
|
||||
proptest::collection::hash_set(enc, 1..ALLOWED_ENCRYPTION_ALGORITHMS.len()),
|
||||
proptest::collection::hash_set(mac, 1..ALLOWED_MAC_ALGORITHMS.len()),
|
||||
proptest::collection::hash_set(comp, 1..ALLOWED_COMPRESSION_ALGORITHMS.len()),
|
||||
proptest::option::of(keyx),
|
||||
)
|
||||
.prop_map(|(kex, host, enc, mac, comp, pred)| ClientConnectionOpts {
|
||||
key_exchange_algorithms: finalize_options(kex).unwrap(),
|
||||
server_host_key_algorithms: finalize_options(host).unwrap(),
|
||||
encryption_algorithms: finalize_options(enc).unwrap(),
|
||||
mac_algorithms: finalize_options(mac).unwrap(),
|
||||
compression_algorithms: finalize_options(comp).unwrap(),
|
||||
languages: vec![],
|
||||
predict: pred.and_then(|x| KeyExchangeAlgorithm::from_str(x).ok()),
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_options<T, E>(values: HashSet<&str>) -> Result<Vec<T>, E>
|
||||
where
|
||||
T: FromStr<Err = E>,
|
||||
{
|
||||
values
|
||||
.into_iter()
|
||||
.map(T::from_str)
|
||||
.collect::<Result<Vec<T>, E>>()
|
||||
}
|
||||
48
configuration/src/console.rs
Normal file
48
configuration/src/console.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use console_subscriber::{ConsoleLayer, ServerAddr};
|
||||
use core::time::Duration;
|
||||
use std::path::PathBuf;
|
||||
use tracing_subscriber::Layer;
|
||||
|
||||
pub struct ConsoleConfiguration {
|
||||
client_buffer_capacity: usize,
|
||||
publish_interval: Duration,
|
||||
retention: Duration,
|
||||
pub server_addr: ServerAddr,
|
||||
poll_duration_histogram_max: Duration,
|
||||
}
|
||||
|
||||
impl Default for ConsoleConfiguration {
|
||||
fn default() -> Self {
|
||||
ConsoleConfiguration {
|
||||
client_buffer_capacity: ConsoleLayer::DEFAULT_CLIENT_BUFFER_CAPACITY,
|
||||
publish_interval: ConsoleLayer::DEFAULT_PUBLISH_INTERVAL,
|
||||
retention: ConsoleLayer::DEFAULT_RETENTION,
|
||||
server_addr: xdg::BaseDirectories::with_prefix("hushd")
|
||||
.and_then(|x| x.get_runtime_directory().cloned())
|
||||
.map(|mut v| {
|
||||
v.push("console.sock");
|
||||
v
|
||||
})
|
||||
.map(|p| ServerAddr::Unix(p.clone()))
|
||||
.unwrap_or_else(|_| PathBuf::from("console.sock").into()),
|
||||
poll_duration_histogram_max: ConsoleLayer::DEFAULT_SCHEDULED_DURATION_MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConsoleConfiguration {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn layer<S>(&self) -> Box<dyn Layer<S> + Send + Sync + 'static>
|
||||
where
|
||||
S: tracing_core::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
|
||||
{
|
||||
ConsoleLayer::builder()
|
||||
.client_buffer_capacity(self.client_buffer_capacity)
|
||||
.publish_interval(self.publish_interval)
|
||||
.retention(self.retention)
|
||||
.server_addr(self.server_addr.clone())
|
||||
.poll_duration_histogram_max(self.poll_duration_histogram_max)
|
||||
.spawn()
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
33
configuration/src/error.rs
Normal file
33
configuration/src/error.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use crate::config_file::ConfigFileError;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigurationError {
|
||||
#[error(transparent)]
|
||||
ConfigFile(#[from] ConfigFileError),
|
||||
#[error(transparent)]
|
||||
CommandLineError(#[from] clap::error::Error),
|
||||
#[error("Could not read file {file}: {error}")]
|
||||
CouldNotRead {
|
||||
file: PathBuf,
|
||||
error: std::io::Error,
|
||||
},
|
||||
#[error("Error loading public key information")]
|
||||
PublicKey,
|
||||
#[error("Error loading private key")]
|
||||
PrivateKey,
|
||||
#[error("Error configuring DNS resolver")]
|
||||
Resolver,
|
||||
#[error("Could not create UNIX listener socket at {path}: {error}")]
|
||||
CouldNotMakeSocket {
|
||||
path: PathBuf,
|
||||
error: std::io::Error,
|
||||
},
|
||||
#[error("Could not {thing} for {path}: {error}")]
|
||||
CouldNotSetPerms {
|
||||
thing: String,
|
||||
path: PathBuf,
|
||||
error: std::io::Error,
|
||||
},
|
||||
}
|
||||
86
configuration/src/lib.rs
Normal file
86
configuration/src/lib.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
pub mod client;
|
||||
mod command_line;
|
||||
mod config_file;
|
||||
pub mod connection;
|
||||
mod console;
|
||||
pub mod error;
|
||||
mod logging;
|
||||
pub mod resolver;
|
||||
mod runtime;
|
||||
pub mod server;
|
||||
|
||||
use tokio::runtime::Runtime;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
//impl ClientConfiguration {
|
||||
// pub async fn try_from(
|
||||
// mut basic: BasicClientConfiguration,
|
||||
// ) -> error_stack::Result<Self, ConfigurationError> {
|
||||
// let mut _ssh_keys = HashMap::new();
|
||||
// let mut _defaults = ClientConnectionOpts::default();
|
||||
// let resolver = basic
|
||||
// .config_file
|
||||
// .as_mut()
|
||||
// .and_then(|x| x.resolver.take())
|
||||
// .unwrap_or_default();
|
||||
// let user_ssh_keys = basic.config_file.map(|cf| cf.keys).unwrap_or_default();
|
||||
//
|
||||
// tracing::info!(
|
||||
// provided_ssh_keys = user_ssh_keys.len(),
|
||||
// "loading user-provided SSH keys"
|
||||
// );
|
||||
// for (key_name, key_info) in user_ssh_keys.into_iter() {
|
||||
// let _public_keys = PublicKey::load(key_info.public).await.unwrap();
|
||||
// tracing::info!(?key_name, "public keys loaded");
|
||||
// let _private_key = load_openssh_file_keys(key_info.private, &key_info.password)
|
||||
// .await
|
||||
// .change_context(ConfigurationError::PrivateKey)?;
|
||||
// tracing::info!(?key_name, "private keys loaded");
|
||||
// }
|
||||
//
|
||||
// Ok(ClientConfiguration {
|
||||
// _runtime: basic.runtime,
|
||||
// _logging: basic.logging,
|
||||
// resolver,
|
||||
// _ssh_keys,
|
||||
// _defaults,
|
||||
// command: basic.command,
|
||||
// })
|
||||
// }
|
||||
//}
|
||||
//
|
||||
|
||||
/// Set up the tracing subscribers based on the config file and command line options.
|
||||
///
|
||||
/// This will definitely set up our logging substrate, but may also create a subscriber
|
||||
/// for the console.
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn establish_subscribers(
|
||||
logging: &logging::LoggingConfiguration,
|
||||
runtime: &mut runtime::RuntimeConfiguration,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let tracing_layer = logging.layer()?;
|
||||
let mut layers = vec![tracing_layer];
|
||||
|
||||
if let Some(console_config) = runtime.console.take() {
|
||||
layers.push(console_config.layer());
|
||||
}
|
||||
|
||||
tracing_subscriber::registry().with(layers).init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a new tokio runtime based on the configuration / command line options
|
||||
/// provided.
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn configured_runtime(
|
||||
runtime: &runtime::RuntimeConfiguration,
|
||||
) -> Result<Runtime, std::io::Error> {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.max_blocking_threads(runtime.tokio_blocking_threads)
|
||||
.worker_threads(runtime.tokio_worker_threads)
|
||||
.build()
|
||||
}
|
||||
95
configuration/src/logging.rs
Normal file
95
configuration/src/logging.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use std::path::PathBuf;
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
use tracing_subscriber::{EnvFilter, Layer};
|
||||
|
||||
pub struct LoggingConfiguration {
|
||||
pub(crate) filter: LevelFilter,
|
||||
pub(crate) include_filename: bool,
|
||||
pub(crate) include_lineno: bool,
|
||||
pub(crate) include_thread_ids: bool,
|
||||
pub(crate) include_thread_names: bool,
|
||||
pub(crate) mode: LogMode,
|
||||
pub(crate) target: LogTarget,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum LogMode {
|
||||
Compact,
|
||||
Pretty,
|
||||
Json,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum LogTarget {
|
||||
StdOut,
|
||||
StdErr,
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
impl LogTarget {
|
||||
fn supports_ansi(&self) -> bool {
|
||||
matches!(self, LogTarget::StdOut | LogTarget::StdErr)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LoggingConfiguration {
|
||||
fn default() -> Self {
|
||||
LoggingConfiguration {
|
||||
filter: LevelFilter::INFO,
|
||||
include_filename: false,
|
||||
include_lineno: false,
|
||||
include_thread_ids: true,
|
||||
include_thread_names: true,
|
||||
mode: LogMode::Compact,
|
||||
target: LogTarget::StdErr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LoggingConfiguration {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn layer<S>(&self) -> Result<Box<dyn Layer<S> + Send + Sync + 'static>, std::io::Error>
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
{
|
||||
let filter = EnvFilter::builder()
|
||||
.with_default_directive(self.filter.into())
|
||||
.with_env_var("HUSHD_LOG")
|
||||
.from_env_lossy();
|
||||
|
||||
let base = tracing_subscriber::fmt::layer()
|
||||
.with_file(self.include_filename)
|
||||
.with_line_number(self.include_lineno)
|
||||
.with_thread_ids(self.include_thread_ids)
|
||||
.with_thread_names(self.include_thread_names)
|
||||
.with_ansi(self.target.supports_ansi());
|
||||
|
||||
macro_rules! finalize {
|
||||
($layer: expr) => {
|
||||
match self.mode {
|
||||
LogMode::Compact => Ok($layer.compact().with_filter(filter).boxed()),
|
||||
LogMode::Json => Ok($layer.json().with_filter(filter).boxed()),
|
||||
LogMode::Pretty => Ok($layer.pretty().with_filter(filter).boxed()),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
match self.target {
|
||||
LogTarget::StdOut => finalize!(base.with_writer(std::io::stdout)),
|
||||
LogTarget::StdErr => finalize!(base.with_writer(std::io::stderr)),
|
||||
LogTarget::File(ref path) => {
|
||||
let log_file = std::fs::File::create(path)?;
|
||||
finalize!(base.with_writer(std::sync::Mutex::new(log_file)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_ansi() {
|
||||
assert!(LogTarget::StdOut.supports_ansi());
|
||||
assert!(LogTarget::StdErr.supports_ansi());
|
||||
assert!(!LogTarget::File("/dev/null".into()).supports_ansi());
|
||||
}
|
||||
359
configuration/src/resolver.rs
Normal file
359
configuration/src/resolver.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
use proptest::arbitrary::Arbitrary;
|
||||
use proptest::strategy::{BoxedStrategy, Just, Strategy};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::SocketAddr;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub struct DnsConfig {
|
||||
built_in: Option<BuiltinDnsOption>,
|
||||
pub local_domain: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub search_domains: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub name_servers: Vec<NameServerConfig>,
|
||||
#[serde(default)]
|
||||
pub retry_attempts: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub cache_size: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub max_concurrent_requests_for_query: Option<u16>,
|
||||
#[serde(default)]
|
||||
pub preserve_intermediates: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub shuffle_dns_servers: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub allow_mdns: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for DnsConfig {
|
||||
fn default() -> Self {
|
||||
DnsConfig {
|
||||
built_in: Some(BuiltinDnsOption::Cloudflare),
|
||||
local_domain: None,
|
||||
search_domains: vec![],
|
||||
name_servers: vec![],
|
||||
retry_attempts: None,
|
||||
cache_size: None,
|
||||
max_concurrent_requests_for_query: None,
|
||||
preserve_intermediates: None,
|
||||
shuffle_dns_servers: None,
|
||||
allow_mdns: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl DnsConfig {
|
||||
pub(crate) fn empty() -> Self {
|
||||
DnsConfig {
|
||||
built_in: None,
|
||||
local_domain: None,
|
||||
search_domains: vec![],
|
||||
name_servers: vec![],
|
||||
retry_attempts: None,
|
||||
cache_size: None,
|
||||
max_concurrent_requests_for_query: None,
|
||||
preserve_intermediates: None,
|
||||
shuffle_dns_servers: None,
|
||||
allow_mdns: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for DnsConfig {
|
||||
type Parameters = bool;
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(always_use_builtin: Self::Parameters) -> Self::Strategy {
|
||||
if always_use_builtin {
|
||||
BuiltinDnsOption::arbitrary()
|
||||
.prop_map(|x| DnsConfig {
|
||||
built_in: Some(x),
|
||||
local_domain: None,
|
||||
search_domains: vec![],
|
||||
name_servers: vec![],
|
||||
retry_attempts: None,
|
||||
cache_size: None,
|
||||
max_concurrent_requests_for_query: None,
|
||||
preserve_intermediates: None,
|
||||
shuffle_dns_servers: None,
|
||||
allow_mdns: None,
|
||||
})
|
||||
.boxed()
|
||||
} else {
|
||||
let built_in = proptest::option::of(BuiltinDnsOption::arbitrary());
|
||||
built_in
|
||||
.prop_flat_map(|built_in| {
|
||||
let local_domain = proptest::option::of(domain_name_strat());
|
||||
let search_domains = proptest::collection::vec(domain_name_strat(), 0..10);
|
||||
let min_servers = if built_in.is_some() { 0 } else { 1 };
|
||||
let name_servers =
|
||||
proptest::collection::vec(NameServerConfig::arbitrary(), min_servers..6);
|
||||
let retry_attempts = proptest::option::of(u16::arbitrary());
|
||||
let cache_size = proptest::option::of(u32::arbitrary());
|
||||
let max_concurrent_requests_for_query = proptest::option::of(u16::arbitrary());
|
||||
let preserve_intermediates = proptest::option::of(bool::arbitrary());
|
||||
let shuffle_dns_servers = proptest::option::of(bool::arbitrary());
|
||||
let allow_mdns = proptest::option::of(bool::arbitrary());
|
||||
|
||||
(
|
||||
local_domain,
|
||||
search_domains,
|
||||
name_servers,
|
||||
retry_attempts,
|
||||
cache_size,
|
||||
max_concurrent_requests_for_query,
|
||||
preserve_intermediates,
|
||||
shuffle_dns_servers,
|
||||
allow_mdns,
|
||||
)
|
||||
.prop_map(
|
||||
move |(
|
||||
local_domain,
|
||||
search_domains,
|
||||
name_servers,
|
||||
retry_attempts,
|
||||
cache_size,
|
||||
max_concurrent_requests_for_query,
|
||||
preserve_intermediates,
|
||||
shuffle_dns_servers,
|
||||
allow_mdns,
|
||||
)| DnsConfig {
|
||||
built_in,
|
||||
local_domain,
|
||||
search_domains,
|
||||
name_servers,
|
||||
retry_attempts,
|
||||
cache_size,
|
||||
max_concurrent_requests_for_query,
|
||||
preserve_intermediates,
|
||||
shuffle_dns_servers,
|
||||
allow_mdns,
|
||||
},
|
||||
)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn domain_name_strat() -> BoxedStrategy<String> {
|
||||
let chunk = proptest::string::string_regex("[a-zA-Z0-9]{2,32}").unwrap();
|
||||
let sets = proptest::collection::vec(chunk, 2..6);
|
||||
sets.prop_map(|set| {
|
||||
let mut output = String::new();
|
||||
|
||||
for x in set.into_iter() {
|
||||
if !output.is_empty() {
|
||||
output.push('.');
|
||||
}
|
||||
|
||||
output.push_str(&x);
|
||||
}
|
||||
|
||||
output
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
|
||||
enum BuiltinDnsOption {
|
||||
Google,
|
||||
Cloudflare,
|
||||
Quad9,
|
||||
}
|
||||
|
||||
impl Arbitrary for BuiltinDnsOption {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
|
||||
proptest::prop_oneof![
|
||||
Just(BuiltinDnsOption::Google),
|
||||
Just(BuiltinDnsOption::Cloudflare),
|
||||
Just(BuiltinDnsOption::Quad9),
|
||||
]
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub struct NameServerConfig {
|
||||
#[serde(serialize_with = "serialize_url", deserialize_with = "deserialize_url")]
|
||||
pub address: Url,
|
||||
#[serde(default)]
|
||||
pub timeout_in_seconds: Option<u64>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bind_address: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
fn serialize_url<S: serde::Serializer>(url: &Url, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.collect_str(url)
|
||||
}
|
||||
|
||||
fn deserialize_url<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Url, D::Error> {
|
||||
struct Visitor {}
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = Url;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(formatter, "a legal URL")
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||
Url::parse(v).map_err(|e| E::custom(e))
|
||||
}
|
||||
|
||||
fn visit_borrowed_str<E: serde::de::Error>(self, v: &'de str) -> Result<Self::Value, E> {
|
||||
Url::parse(v).map_err(|e| E::custom(e))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(Visitor {})
|
||||
}
|
||||
|
||||
impl Arbitrary for NameServerConfig {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
|
||||
let scheme = proptest::prop_oneof![
|
||||
Just("http".to_string()),
|
||||
Just("https".to_string()),
|
||||
Just("tcp".to_string()),
|
||||
Just("udp".to_string()),
|
||||
];
|
||||
let domain = proptest::string::string_regex("[A-Za-z0-9]{1,40}").unwrap();
|
||||
let port = proptest::option::of(u16::arbitrary());
|
||||
let user = proptest::option::of(proptest::string::string_regex("[A-Za-z]{1,30}").unwrap());
|
||||
let password =
|
||||
proptest::option::of(proptest::string::string_regex(":[A-Za-z]{1,30}").unwrap());
|
||||
let path =
|
||||
proptest::option::of(proptest::string::string_regex("/[A-Za-z/]{1,30}").unwrap());
|
||||
|
||||
let uri_strategy = (scheme, domain, user, password, port, path).prop_map(
|
||||
|(scheme, domain, user, password, port, path)| {
|
||||
let userpass_prefix = match (user, password) {
|
||||
(None, None) => String::new(),
|
||||
(Some(u), None) => format!("{u}@"),
|
||||
(None, Some(p)) => format!(":{p}@"),
|
||||
(Some(u), Some(p)) => format!("{u}:{p}@"),
|
||||
};
|
||||
let path = path.unwrap_or_default();
|
||||
let port = port.map(|x| format!(":{x}")).unwrap_or_default();
|
||||
let uri_str = format!("{scheme}://{userpass_prefix}{domain}{port}{path}");
|
||||
Url::parse(&uri_str).unwrap()
|
||||
},
|
||||
);
|
||||
|
||||
(
|
||||
uri_strategy,
|
||||
proptest::option::of(u64::arbitrary()),
|
||||
proptest::option::of(SocketAddr::arbitrary()),
|
||||
)
|
||||
.prop_map(|(address, mut timeout_in_seconds, mut bind_address)| {
|
||||
if let Some(bind_address) = bind_address.as_mut() {
|
||||
clear_flow_and_scope_info(bind_address);
|
||||
}
|
||||
|
||||
if let Some(timeout_in_seconds) = timeout_in_seconds.as_mut() {
|
||||
*timeout_in_seconds &= 0x7FFF_FFFF_FFFF_FFFF;
|
||||
}
|
||||
|
||||
NameServerConfig {
|
||||
address,
|
||||
timeout_in_seconds,
|
||||
bind_address,
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_flow_and_scope_info(address: &mut SocketAddr) {
|
||||
if let SocketAddr::V6(addr) = address {
|
||||
addr.set_flowinfo(0);
|
||||
addr.set_scope_id(0);
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn valid_configs_parse(config in DnsConfig::arbitrary_with(false)) {
|
||||
let toml = toml::to_string(&config).unwrap();
|
||||
let reversed: DnsConfig = toml::from_str(&toml).unwrap();
|
||||
assert_eq!(config, reversed);
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsConfig {
|
||||
/// Return the configurations for all of the name servers that the user
|
||||
/// has selected.
|
||||
pub fn name_servers(&self) -> Vec<NameServerConfig> {
|
||||
let mut results = self.name_servers.clone();
|
||||
|
||||
match self.built_in {
|
||||
None => {}
|
||||
Some(BuiltinDnsOption::Cloudflare) => {
|
||||
results.push(NameServerConfig {
|
||||
address: Url::parse("udp://1.1.1.1:53").unwrap(),
|
||||
timeout_in_seconds: None,
|
||||
bind_address: None,
|
||||
});
|
||||
results.push(NameServerConfig {
|
||||
address: Url::parse("udp://1.0.0.1").unwrap(),
|
||||
timeout_in_seconds: None,
|
||||
bind_address: None,
|
||||
});
|
||||
results.push(NameServerConfig {
|
||||
address: Url::parse("udp://2606:4700:4700::1111").unwrap(),
|
||||
timeout_in_seconds: None,
|
||||
bind_address: None,
|
||||
});
|
||||
results.push(NameServerConfig {
|
||||
address: Url::parse("udp://2606:4700:4700::1001:53").unwrap(),
|
||||
timeout_in_seconds: None,
|
||||
bind_address: None,
|
||||
});
|
||||
}
|
||||
Some(BuiltinDnsOption::Google) => {
|
||||
results.push(NameServerConfig {
|
||||
address: Url::parse("udp://8.8.8.8:53").unwrap(),
|
||||
timeout_in_seconds: None,
|
||||
bind_address: None,
|
||||
});
|
||||
results.push(NameServerConfig {
|
||||
address: Url::parse("udp://8.8.4.4").unwrap(),
|
||||
timeout_in_seconds: None,
|
||||
bind_address: None,
|
||||
});
|
||||
results.push(NameServerConfig {
|
||||
address: Url::parse("udp://2001:4860:4860::8888:53").unwrap(),
|
||||
timeout_in_seconds: None,
|
||||
bind_address: None,
|
||||
});
|
||||
results.push(NameServerConfig {
|
||||
address: Url::parse("udp://2001:4860:4860::8844").unwrap(),
|
||||
timeout_in_seconds: None,
|
||||
bind_address: None,
|
||||
});
|
||||
}
|
||||
Some(BuiltinDnsOption::Quad9) => {
|
||||
results.push(NameServerConfig {
|
||||
address: Url::parse("udp://9.9.9.9:53").unwrap(),
|
||||
timeout_in_seconds: None,
|
||||
bind_address: None,
|
||||
});
|
||||
results.push(NameServerConfig {
|
||||
address: Url::parse("udp://2620::00fe:00f3").unwrap(),
|
||||
timeout_in_seconds: None,
|
||||
bind_address: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
}
|
||||
17
configuration/src/runtime.rs
Normal file
17
configuration/src/runtime.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use crate::console::ConsoleConfiguration;
|
||||
|
||||
pub struct RuntimeConfiguration {
|
||||
pub tokio_worker_threads: usize,
|
||||
pub tokio_blocking_threads: usize,
|
||||
pub console: Option<ConsoleConfiguration>,
|
||||
}
|
||||
|
||||
impl Default for RuntimeConfiguration {
|
||||
fn default() -> Self {
|
||||
RuntimeConfiguration {
|
||||
tokio_worker_threads: 4,
|
||||
tokio_blocking_threads: 16,
|
||||
console: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
69
configuration/src/server.rs
Normal file
69
configuration/src/server.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use crate::command_line::ServerArguments;
|
||||
use crate::config_file::{ConfigFile, SocketConfig};
|
||||
use crate::error::ConfigurationError;
|
||||
use crate::logging::LoggingConfiguration;
|
||||
use crate::runtime::RuntimeConfiguration;
|
||||
use clap::Parser;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use tokio::net::UnixListener;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ServerConfiguration {
|
||||
pub runtime: RuntimeConfiguration,
|
||||
pub logging: LoggingConfiguration,
|
||||
pub sockets: HashMap<String, SocketConfig>,
|
||||
}
|
||||
|
||||
impl ServerConfiguration {
|
||||
/// Load a server configuration for this run.
|
||||
///
|
||||
/// This will parse the process's command line arguments, and parse
|
||||
/// a config file if given, so it can take awhile. Even though this
|
||||
/// function does a bunch of IO, it is not async, because it is expected
|
||||
/// ro run before we have a tokio runtime fully established. (This
|
||||
/// function will determine a bunch of things related to the runtime,
|
||||
/// like how many threads to run, what tracing subscribers to include,
|
||||
/// etc.)
|
||||
pub fn new<I, T>(args: I) -> Result<Self, ConfigurationError>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<OsString> + Clone,
|
||||
{
|
||||
let mut new_configuration = Self::default();
|
||||
let mut command_line = ServerArguments::try_parse_from(args)?;
|
||||
let mut config_file = ConfigFile::new(command_line.arguments.config_file.take())?;
|
||||
|
||||
// we prefer the command line to the config file, so first merge
|
||||
// in the config file so that when we later merge the command line,
|
||||
// it overwrites any config file options.
|
||||
if let Some(config_file) = config_file.as_mut() {
|
||||
config_file.merge_standard_options_into(
|
||||
&mut new_configuration.runtime,
|
||||
&mut new_configuration.logging,
|
||||
);
|
||||
|
||||
new_configuration.sockets = config_file.sockets.take().unwrap_or_default();
|
||||
}
|
||||
|
||||
command_line.arguments.merge_standard_options_into(
|
||||
&mut new_configuration.runtime,
|
||||
&mut new_configuration.logging,
|
||||
);
|
||||
|
||||
Ok(new_configuration)
|
||||
}
|
||||
|
||||
/// Generate a series of UNIX sockets that clients will attempt to connect to.
|
||||
pub async fn generate_listener_sockets(
|
||||
&mut self,
|
||||
) -> Result<HashMap<String, UnixListener>, ConfigurationError> {
|
||||
let mut results = HashMap::new();
|
||||
|
||||
for (name, config) in std::mem::take(&mut self.sockets).into_iter() {
|
||||
results.insert(name, config.into_listener().await?);
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
10
crypto/Cargo.toml
Normal file
10
crypto/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "crypto"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
num-bigint-dig = { workspace = true }
|
||||
num-integer = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
24
crypto/src/known_algorithms.rs
Normal file
24
crypto/src/known_algorithms.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
pub static ALLOWED_KEY_EXCHANGE_ALGORITHMS: &[&str] = &[
|
||||
"ecdh-sha2-nistp256",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp521",
|
||||
"curve25519-sha256",
|
||||
];
|
||||
|
||||
pub static ALLOWED_HOST_KEY_ALGORITHMS: &[&str] = &[
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ssh-rsa",
|
||||
];
|
||||
|
||||
pub static ALLOWED_ENCRYPTION_ALGORITHMS: &[&str] = &[
|
||||
"aes256-ctr",
|
||||
"aes256-gcm@openssh.com",
|
||||
"chacha20-poly1305@openssh.com",
|
||||
];
|
||||
|
||||
pub static ALLOWED_MAC_ALGORITHMS: &[&str] = &["hmac-sha2-256", "hmac-sha2-512"];
|
||||
|
||||
pub static ALLOWED_COMPRESSION_ALGORITHMS: &[&str] = &["none", "zlib"];
|
||||
272
crypto/src/lib.rs
Normal file
272
crypto/src/lib.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
pub mod known_algorithms;
|
||||
pub mod rsa;
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum KeyExchangeAlgorithm {
|
||||
EcdhSha2Nistp256,
|
||||
EcdhSha2Nistp384,
|
||||
EcdhSha2Nistp521,
|
||||
Curve25519Sha256,
|
||||
}
|
||||
|
||||
impl KeyExchangeAlgorithm {
|
||||
pub fn allowed() -> &'static [KeyExchangeAlgorithm] {
|
||||
&[
|
||||
KeyExchangeAlgorithm::EcdhSha2Nistp256,
|
||||
KeyExchangeAlgorithm::EcdhSha2Nistp384,
|
||||
KeyExchangeAlgorithm::EcdhSha2Nistp521,
|
||||
KeyExchangeAlgorithm::Curve25519Sha256,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AlgoFromStrError {
|
||||
#[error("Did not recognize key exchange algorithm '{0}'")]
|
||||
UnknownKeyExchangeAlgorithm(String),
|
||||
#[error("Did not recognize host key algorithm '{0}'")]
|
||||
UnknownHostKeyAlgorithm(String),
|
||||
#[error("Did not recognize encryption algorithm '{0}'")]
|
||||
UnknownEncryptionAlgorithm(String),
|
||||
#[error("Did not recognize MAC algorithm '{0}'")]
|
||||
UnknownMacAlgorithm(String),
|
||||
#[error("Did not recognize compression algorithm '{0}'")]
|
||||
UnknownCompressionAlgorithm(String),
|
||||
}
|
||||
|
||||
impl FromStr for KeyExchangeAlgorithm {
|
||||
type Err = AlgoFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"ecdh-sha2-nistp256" => Ok(KeyExchangeAlgorithm::EcdhSha2Nistp256),
|
||||
"ecdh-sha2-nistp384" => Ok(KeyExchangeAlgorithm::EcdhSha2Nistp384),
|
||||
"ecdh-sha2-nistp521" => Ok(KeyExchangeAlgorithm::EcdhSha2Nistp521),
|
||||
"curve25519-sha256" => Ok(KeyExchangeAlgorithm::Curve25519Sha256),
|
||||
other => Err(AlgoFromStrError::UnknownKeyExchangeAlgorithm(
|
||||
other.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for KeyExchangeAlgorithm {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
KeyExchangeAlgorithm::EcdhSha2Nistp256 => write!(f, "ecdh-sha2-nistp256"),
|
||||
KeyExchangeAlgorithm::EcdhSha2Nistp384 => write!(f, "ecdh-sha2-nistp384"),
|
||||
KeyExchangeAlgorithm::EcdhSha2Nistp521 => write!(f, "ecdh-sha2-nistp521"),
|
||||
KeyExchangeAlgorithm::Curve25519Sha256 => write!(f, "curve25519-sha256"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_invert_kex_algos() {
|
||||
for variant in KeyExchangeAlgorithm::allowed().iter() {
|
||||
let s = variant.to_string();
|
||||
let reversed = KeyExchangeAlgorithm::from_str(&s);
|
||||
assert!(reversed.is_ok());
|
||||
assert_eq!(variant, &reversed.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum HostKeyAlgorithm {
|
||||
Ed25519,
|
||||
EcdsaSha2Nistp256,
|
||||
EcdsaSha2Nistp384,
|
||||
EcdsaSha2Nistp521,
|
||||
Rsa,
|
||||
}
|
||||
|
||||
impl HostKeyAlgorithm {
|
||||
pub fn allowed() -> &'static [Self] {
|
||||
&[
|
||||
HostKeyAlgorithm::Ed25519,
|
||||
HostKeyAlgorithm::EcdsaSha2Nistp256,
|
||||
HostKeyAlgorithm::EcdsaSha2Nistp384,
|
||||
HostKeyAlgorithm::EcdsaSha2Nistp521,
|
||||
HostKeyAlgorithm::Rsa,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for HostKeyAlgorithm {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
HostKeyAlgorithm::Ed25519 => write!(f, "ssh-ed25519"),
|
||||
HostKeyAlgorithm::EcdsaSha2Nistp256 => write!(f, "ecdsa-sha2-nistp256"),
|
||||
HostKeyAlgorithm::EcdsaSha2Nistp384 => write!(f, "ecdsa-sha2-nistp384"),
|
||||
HostKeyAlgorithm::EcdsaSha2Nistp521 => write!(f, "ecdsa-sha2-nistp521"),
|
||||
HostKeyAlgorithm::Rsa => write!(f, "ssh-rsa"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for HostKeyAlgorithm {
|
||||
type Err = AlgoFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"ssh-ed25519" => Ok(HostKeyAlgorithm::Ed25519),
|
||||
"ecdsa-sha2-nistp256" => Ok(HostKeyAlgorithm::EcdsaSha2Nistp256),
|
||||
"ecdsa-sha2-nistp384" => Ok(HostKeyAlgorithm::EcdsaSha2Nistp384),
|
||||
"ecdsa-sha2-nistp521" => Ok(HostKeyAlgorithm::EcdsaSha2Nistp521),
|
||||
"ssh-rsa" => Ok(HostKeyAlgorithm::Rsa),
|
||||
unknown => Err(AlgoFromStrError::UnknownHostKeyAlgorithm(
|
||||
unknown.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_invert_host_key_algos() {
|
||||
for variant in HostKeyAlgorithm::allowed().iter() {
|
||||
let s = variant.to_string();
|
||||
let reversed = HostKeyAlgorithm::from_str(&s);
|
||||
assert!(reversed.is_ok());
|
||||
assert_eq!(variant, &reversed.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum EncryptionAlgorithm {
|
||||
Aes256Ctr,
|
||||
Aes256Gcm,
|
||||
ChaCha20Poly1305,
|
||||
}
|
||||
|
||||
impl EncryptionAlgorithm {
|
||||
pub fn allowed() -> &'static [Self] {
|
||||
&[
|
||||
EncryptionAlgorithm::Aes256Ctr,
|
||||
EncryptionAlgorithm::Aes256Gcm,
|
||||
EncryptionAlgorithm::ChaCha20Poly1305,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EncryptionAlgorithm {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
EncryptionAlgorithm::Aes256Ctr => write!(f, "aes256-ctr"),
|
||||
EncryptionAlgorithm::Aes256Gcm => write!(f, "aes256-gcm@openssh.com"),
|
||||
EncryptionAlgorithm::ChaCha20Poly1305 => write!(f, "chacha20-poly1305@openssh.com"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EncryptionAlgorithm {
|
||||
type Err = AlgoFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"aes256-ctr" => Ok(EncryptionAlgorithm::Aes256Ctr),
|
||||
"aes256-gcm@openssh.com" => Ok(EncryptionAlgorithm::Aes256Gcm),
|
||||
"chacha20-poly1305@openssh.com" => Ok(EncryptionAlgorithm::ChaCha20Poly1305),
|
||||
_ => Err(AlgoFromStrError::UnknownEncryptionAlgorithm(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_invert_encryption_algos() {
|
||||
for variant in EncryptionAlgorithm::allowed().iter() {
|
||||
let s = variant.to_string();
|
||||
let reversed = EncryptionAlgorithm::from_str(&s);
|
||||
assert!(reversed.is_ok());
|
||||
assert_eq!(variant, &reversed.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum MacAlgorithm {
|
||||
HmacSha256,
|
||||
HmacSha512,
|
||||
}
|
||||
|
||||
impl MacAlgorithm {
|
||||
pub fn allowed() -> &'static [Self] {
|
||||
&[MacAlgorithm::HmacSha256, MacAlgorithm::HmacSha512]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MacAlgorithm {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
MacAlgorithm::HmacSha256 => write!(f, "hmac-sha2-256"),
|
||||
MacAlgorithm::HmacSha512 => write!(f, "hmac-sha2-512"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MacAlgorithm {
|
||||
type Err = AlgoFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"hmac-sha2-256" => Ok(MacAlgorithm::HmacSha256),
|
||||
"hmac-sha2-512" => Ok(MacAlgorithm::HmacSha512),
|
||||
_ => Err(AlgoFromStrError::UnknownMacAlgorithm(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_invert_mac_algos() {
|
||||
for variant in MacAlgorithm::allowed().iter() {
|
||||
let s = variant.to_string();
|
||||
let reversed = MacAlgorithm::from_str(&s);
|
||||
assert!(reversed.is_ok());
|
||||
assert_eq!(variant, &reversed.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum CompressionAlgorithm {
|
||||
None,
|
||||
Zlib,
|
||||
}
|
||||
|
||||
impl CompressionAlgorithm {
|
||||
pub fn allowed() -> &'static [Self] {
|
||||
&[CompressionAlgorithm::None, CompressionAlgorithm::Zlib]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CompressionAlgorithm {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
CompressionAlgorithm::None => write!(f, "none"),
|
||||
CompressionAlgorithm::Zlib => write!(f, "zlib"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CompressionAlgorithm {
|
||||
type Err = AlgoFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"none" => Ok(CompressionAlgorithm::None),
|
||||
"zlib" => Ok(CompressionAlgorithm::Zlib),
|
||||
_ => Err(AlgoFromStrError::UnknownCompressionAlgorithm(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_invert_compression_algos() {
|
||||
for variant in CompressionAlgorithm::allowed().iter() {
|
||||
let s = variant.to_string();
|
||||
let reversed = CompressionAlgorithm::from_str(&s);
|
||||
assert!(reversed.is_ok());
|
||||
assert_eq!(variant, &reversed.unwrap());
|
||||
}
|
||||
}
|
||||
251
crypto/src/rsa.rs
Normal file
251
crypto/src/rsa.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use num_bigint_dig::{BigInt, BigUint, ModInverse};
|
||||
use num_integer::{sqrt, Integer};
|
||||
use num_traits::Pow;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
/// An RSA public key
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct PublicKey {
|
||||
/// The public modulus, the product of the primes 'p' and 'q'
|
||||
n: BigUint,
|
||||
/// The public exponent.
|
||||
e: BigUint,
|
||||
}
|
||||
|
||||
/// An RSA private key
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct PrivateKey {
|
||||
/// The public modulus, the product of the primes 'p' and 'q'
|
||||
n: BigUint,
|
||||
/// The public exponent
|
||||
e: BigUint,
|
||||
/// The private exponent
|
||||
d: BigUint,
|
||||
/// The prime 'p'
|
||||
p: BigUint,
|
||||
/// The prime 'q'
|
||||
q: BigUint,
|
||||
/// d mod (p-1)
|
||||
dmodp1: BigUint,
|
||||
/// d mod (q-1)
|
||||
dmodq1: BigUint,
|
||||
/// q^-1 mod P
|
||||
qinv: BigInt,
|
||||
}
|
||||
|
||||
impl Drop for PrivateKey {
|
||||
fn drop(&mut self) {
|
||||
self.d.zeroize();
|
||||
self.p.zeroize();
|
||||
self.q.zeroize();
|
||||
self.dmodp1.zeroize();
|
||||
self.dmodq1.zeroize();
|
||||
self.qinv.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
/// Generate a public key from the given input values.
|
||||
///
|
||||
/// No checking is performed at this point to ensure that these
|
||||
/// values are sane in any way.
|
||||
pub fn new(n: BigUint, e: BigUint) -> Self {
|
||||
PublicKey { n, e }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PrivateKeyLoadError {
|
||||
#[error("Invalid value for public 'e' value; must be between 2^16 and 2^256, got {0}")]
|
||||
InvalidEValue(BigUint),
|
||||
#[error("Could not recover primes 'p' and 'q' from provided private key data")]
|
||||
CouldNotRecoverPrimes,
|
||||
#[error("Could not generate modular inverse of 'q' in provided private key data")]
|
||||
CouldNotGenerateModInv,
|
||||
#[error("Could not cross-confirm value '{value}' in provided private key data")]
|
||||
IncoherentValue { value: &'static str },
|
||||
}
|
||||
|
||||
impl PrivateKey {
|
||||
/// Generate a private key from the associated public key and the private
|
||||
/// value 'd'.
|
||||
///
|
||||
/// This will do some further computations, and should not be called when
|
||||
/// you absolutely must get an answer back immediately. Or, to put it
|
||||
/// another way, you really should call this with `block_in_place` or
|
||||
/// similar in an async context.
|
||||
pub fn from_d(public: &PublicKey, d: BigUint) -> Result<Self, PrivateKeyLoadError> {
|
||||
let (p, q) = recover_primes(&public.n, &public.e, &d)?;
|
||||
let dmodp1 = &d % (&p - 1u64);
|
||||
let dmodq1 = &d % (&q - 1u64);
|
||||
let qinv = (&q)
|
||||
.mod_inverse(&p)
|
||||
.ok_or(PrivateKeyLoadError::CouldNotGenerateModInv)?;
|
||||
|
||||
Ok(PrivateKey {
|
||||
n: public.n.clone(),
|
||||
e: public.e.clone(),
|
||||
d,
|
||||
p,
|
||||
q,
|
||||
dmodp1,
|
||||
dmodq1,
|
||||
qinv,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a private key from the associated public key, the private
|
||||
/// value 'd', and several other useful values.
|
||||
///
|
||||
/// This will do some additional computations, and should not be called
|
||||
/// when you absolutely must get an answer back immediately. Or, to put
|
||||
/// it another way, you really should call this with `block_in_place` or
|
||||
/// similar.
|
||||
///
|
||||
/// This version of this function performs some safety checking to ensure
|
||||
/// the provided values are reasonable. To avoid this cost, but at the
|
||||
/// risk of accepting bad key data, use the `_unchecked` variant.
|
||||
pub fn from_parts(
|
||||
public: &PublicKey,
|
||||
d: BigUint,
|
||||
qinv: BigInt,
|
||||
p: BigUint,
|
||||
q: BigUint,
|
||||
) -> Result<Self, PrivateKeyLoadError> {
|
||||
let computed_private = Self::from_d(public, d)?;
|
||||
|
||||
if qinv != computed_private.qinv {
|
||||
return Err(PrivateKeyLoadError::IncoherentValue { value: "qinv" });
|
||||
}
|
||||
|
||||
if p != computed_private.p {
|
||||
return Err(PrivateKeyLoadError::IncoherentValue { value: "p" });
|
||||
}
|
||||
|
||||
if q != computed_private.q {
|
||||
return Err(PrivateKeyLoadError::IncoherentValue { value: "q" });
|
||||
}
|
||||
|
||||
Ok(computed_private)
|
||||
}
|
||||
|
||||
/// Generate a private key from the associated public key, the private
|
||||
/// value 'd', and several other useful values.
|
||||
///
|
||||
/// This will do some additional computations, and should not be called
|
||||
/// when you absolutely must get an answer back immediately. Or, to put
|
||||
/// it another way, you really should call this with `block_in_place` or
|
||||
/// similar.
|
||||
///
|
||||
/// This version of this function performs no safety checking to ensure
|
||||
/// the provided values are reasonable.
|
||||
pub fn from_parts_unchecked(
|
||||
public: &PublicKey,
|
||||
d: BigUint,
|
||||
qinv: BigInt,
|
||||
p: BigUint,
|
||||
q: BigUint,
|
||||
) -> Result<Self, PrivateKeyLoadError> {
|
||||
let dmodp1 = &d % (&p - 1u64);
|
||||
let dmodq1 = &d % (&q - 1u64);
|
||||
|
||||
Ok(PrivateKey {
|
||||
n: public.n.clone(),
|
||||
e: public.e.clone(),
|
||||
d,
|
||||
qinv,
|
||||
p,
|
||||
q,
|
||||
dmodp1,
|
||||
dmodq1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Recover the two primes, `p` and `q`, used to generate the given private
|
||||
/// key.
|
||||
///
|
||||
/// This algorithm is as straightforward an implementation of Appendix C.1
|
||||
/// of NIST 800-56b, revision 2, as I could make it.
|
||||
fn recover_primes(
|
||||
n: &BigUint,
|
||||
e: &BigUint,
|
||||
d: &BigUint,
|
||||
) -> Result<(BigUint, BigUint), PrivateKeyLoadError> {
|
||||
// Assumptions:
|
||||
// 1. The modulus n is the product of two prime factors p and q, with p > q.
|
||||
// 2. Both p and q are less than 2^(nBits/2), where nBits ≥ 2048 is the bit length of n.
|
||||
let n_bits = n.bits() * 8;
|
||||
let max_p_or_q = BigUint::from(2u8).pow(n_bits / 2);
|
||||
// 3. The public exponent e is an odd integer between 2^16 and 2^256.
|
||||
let two = BigUint::from(2u64);
|
||||
if e < &two.pow(16u64) || e > &two.pow(256u64) {
|
||||
return Err(PrivateKeyLoadError::InvalidEValue(e.clone()));
|
||||
}
|
||||
// 4. The private exponent d is a positive integer that is less than λ(n) = LCM(p – 1, q – 1).
|
||||
// 5. The exponents e and d satisfy de ≡ 1 (mod λ(n)).
|
||||
|
||||
// Implementation:
|
||||
// 1. Let a = (de – 1) × GCD(n – 1, de – 1).
|
||||
let mut de_minus_1 = Zeroizing::new(d * e);
|
||||
*de_minus_1 -= 1u64;
|
||||
let n_minus_one = Zeroizing::new(n - 1u64);
|
||||
let gcd_of_n1_and_de1 = Zeroizing::new(n_minus_one.gcd(&de_minus_1));
|
||||
let a = Zeroizing::new(&*de_minus_1 * &*gcd_of_n1_and_de1);
|
||||
// 2. Let m = a/n and r = a – mn, so that a = mn + r and 0 ≤ r < n.
|
||||
let m = Zeroizing::new(&*a / n);
|
||||
let mn = Zeroizing::new(&*m * n);
|
||||
if *mn > *a {
|
||||
// if mn is greater than 'a', then 'r' is going to be negative, which
|
||||
// violates our assumptions.
|
||||
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
|
||||
}
|
||||
let r = Zeroizing::new(&*a - (&*m * n));
|
||||
if &*r >= n {
|
||||
// this violates the other side condition of 2
|
||||
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
|
||||
}
|
||||
// 3. Let b = ( (n – r)/(m + 1) ) + 1; if b is not an integer or b^2 ≤ 4n, then output an
|
||||
// error indicator, and exit without further processing.
|
||||
let b = Zeroizing::new(((n - &*r) / (&*m + 1u64)) + 1u64);
|
||||
let b_squared = Zeroizing::new(&*b * &*b);
|
||||
// 4n contains no secret information, actually, so no need to add the zeorize trait
|
||||
let four_n = 4usize * n;
|
||||
if *b_squared <= four_n {
|
||||
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
|
||||
}
|
||||
// 4. Let ϒ be the positive square root of b2 – 4n; if ϒ is not an integer, then output
|
||||
// an error indicator, and exit without further processing.
|
||||
let b_squared_minus_four_n = Zeroizing::new(&*b_squared - four_n);
|
||||
let y = Zeroizing::new(sqrt((*b_squared_minus_four_n).clone()));
|
||||
let cross_check = Zeroizing::new(&*y * &*y);
|
||||
if cross_check != b_squared_minus_four_n {
|
||||
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
|
||||
}
|
||||
// 5. Let p = (b + ϒ)/2 and let q = (b – ϒ)/2.
|
||||
let mut p = &*b + &*y;
|
||||
p >>= 1;
|
||||
let mut q = &*b - &*y;
|
||||
q >>= 1;
|
||||
// go back and check some of our assumptions from above:
|
||||
// 1. The modulus n is the product of two prime factors p and q, with p > q.
|
||||
if n != &(&p * &q) {
|
||||
p.zeroize();
|
||||
q.zeroize();
|
||||
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
|
||||
}
|
||||
if p <= q {
|
||||
p.zeroize();
|
||||
q.zeroize();
|
||||
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
|
||||
}
|
||||
// 2. Both p and q are less than 2^(nBits/2), where nBits ≥ 2048 is the bit length of n.
|
||||
if p >= max_p_or_q || q >= max_p_or_q {
|
||||
p.zeroize();
|
||||
q.zeroize();
|
||||
return Err(PrivateKeyLoadError::CouldNotRecoverPrimes);
|
||||
}
|
||||
|
||||
// 6. Output (p, q) as the prime factors.
|
||||
Ok((p, q))
|
||||
}
|
||||
11
host/Cargo.toml
Normal file
11
host/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "host"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
error-stack = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
resolver = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
208
host/src/lib.rs
Normal file
208
host/src/lib.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use error_stack::{report, ResultExt};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use resolver::name::Name;
|
||||
use resolver::{ResolveError, Resolver};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Host {
|
||||
IPv4(Ipv4Addr),
|
||||
IPv6(Ipv6Addr),
|
||||
Hostname(Name),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HostParseError {
|
||||
#[error("Could not parse IPv6 address {address:?}: {error}")]
|
||||
CouldNotParseIPv6 {
|
||||
address: String,
|
||||
error: AddrParseError,
|
||||
},
|
||||
#[error("Invalid hostname {hostname:?}")]
|
||||
InvalidHostname { hostname: String },
|
||||
}
|
||||
|
||||
impl fmt::Display for Host {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Host::IPv4(x) => x.fmt(f),
|
||||
Host::IPv6(x) => x.fmt(f),
|
||||
Host::Hostname(x) => x.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Host {
|
||||
type Err = HostParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Ok(addr) = Ipv4Addr::from_str(s) {
|
||||
return Ok(Host::IPv4(addr));
|
||||
}
|
||||
|
||||
if let Ok(addr) = Ipv6Addr::from_str(s) {
|
||||
return Ok(Host::IPv6(addr));
|
||||
}
|
||||
|
||||
if let Some(prefix_removed) = s.strip_prefix('[') {
|
||||
if let Some(cleaned) = prefix_removed.strip_suffix(']') {
|
||||
match Ipv6Addr::from_str(cleaned) {
|
||||
Ok(addr) => return Ok(Host::IPv6(addr)),
|
||||
Err(error) => {
|
||||
return Err(HostParseError::CouldNotParseIPv6 {
|
||||
address: s.to_string(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(name) = Name::from_str(s) {
|
||||
return Ok(Host::Hostname(name));
|
||||
}
|
||||
|
||||
println!(" ... not a hostname");
|
||||
Err(HostParseError::InvalidHostname {
|
||||
hostname: s.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConnectionError {
|
||||
#[error("Connection error: failed to resolve host")]
|
||||
ResolveError,
|
||||
#[error("No valid IP addresses found")]
|
||||
NoAddresses,
|
||||
#[error("Error connecting to host: {error}")]
|
||||
ConnectionError {
|
||||
#[from]
|
||||
error: std::io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl Host {
|
||||
/// Resolve this host address to a set of underlying IP addresses.
|
||||
///
|
||||
/// It is possible that the set of addresses provided may be empty, if the
|
||||
/// address properly resolves (as in, we get a good DNS response) but there
|
||||
/// are no relevant records for us to use for IPv4 or IPv6 connections. There
|
||||
/// is also no guarantee that the host will have both IPv4 and IPv6 addresses,
|
||||
/// so you may only see one or the other.
|
||||
pub async fn resolve(
|
||||
&self,
|
||||
resolver: &mut Resolver,
|
||||
) -> error_stack::Result<HashSet<IpAddr>, ResolveError> {
|
||||
match self {
|
||||
Host::IPv4(addr) => Ok(HashSet::from([IpAddr::V4(*addr)])),
|
||||
Host::IPv6(addr) => Ok(HashSet::from([IpAddr::V6(*addr)])),
|
||||
Host::Hostname(name) => resolver.lookup(name).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to this host and port.
|
||||
///
|
||||
/// This routine will attempt to connect to every address provided by the
|
||||
/// resolver, and return the first successful connection. If all of the
|
||||
/// connections fail, it will return the first error it receives. This routine
|
||||
/// will also return an error if there are no addresses to connect to (which
|
||||
/// can happen in cases in which [`Host::resolve`] would return an empty set.
|
||||
pub async fn connect(
|
||||
&self,
|
||||
resolver: &mut Resolver,
|
||||
port: u16,
|
||||
) -> error_stack::Result<TcpStream, ConnectionError> {
|
||||
let addresses = self
|
||||
.resolve(resolver)
|
||||
.await
|
||||
.change_context(ConnectionError::ResolveError)
|
||||
.attach_printable_lazy(|| format!("target address {}", self))?;
|
||||
|
||||
let mut connectors = FuturesUnordered::new();
|
||||
|
||||
for address in addresses.into_iter() {
|
||||
tracing::trace!(?address, "adding possible target address");
|
||||
let connect_future = TcpStream::connect(SocketAddr::new(address, port));
|
||||
connectors.push(connect_future);
|
||||
}
|
||||
|
||||
let mut error = None;
|
||||
|
||||
while let Some(result) = connectors.next().await {
|
||||
match result {
|
||||
Err(e) if error.is_none() => error = Some(e),
|
||||
Err(_) => {}
|
||||
Ok(v) => return Ok(v),
|
||||
}
|
||||
}
|
||||
|
||||
let final_error = if let Some(e) = error {
|
||||
ConnectionError::ConnectionError { error: e }
|
||||
} else {
|
||||
ConnectionError::NoAddresses
|
||||
};
|
||||
|
||||
Err(report!(final_error)).attach_printable_lazy(|| format!("target address {}", self))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip4_hosts_work() {
|
||||
assert!(
|
||||
matches!(Host::from_str("127.0.0.1"), Ok(Host::IPv4(addr)) if addr == Ipv4Addr::new(127, 0, 0, 1))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_ip6_hosts_work() {
|
||||
assert!(matches!(
|
||||
Host::from_str("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
|
||||
Ok(Host::IPv6(_))
|
||||
));
|
||||
assert!(matches!(Host::from_str("2001:db8::1"), Ok(Host::IPv6(_))));
|
||||
assert!(matches!(Host::from_str("2001:DB8::1"), Ok(Host::IPv6(_))));
|
||||
assert!(matches!(Host::from_str("::1"), Ok(Host::IPv6(_))));
|
||||
assert!(matches!(Host::from_str("::"), Ok(Host::IPv6(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapped_ip6_hosts_work() {
|
||||
assert!(matches!(
|
||||
Host::from_str("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"),
|
||||
Ok(Host::IPv6(_))
|
||||
));
|
||||
assert!(matches!(Host::from_str("[2001:db8::1]"), Ok(Host::IPv6(_))));
|
||||
assert!(matches!(Host::from_str("[2001:DB8::1]"), Ok(Host::IPv6(_))));
|
||||
assert!(matches!(Host::from_str("[::1]"), Ok(Host::IPv6(_))));
|
||||
assert!(matches!(Host::from_str("[::]"), Ok(Host::IPv6(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_domains_work() {
|
||||
assert!(matches!(
|
||||
Host::from_str("uhsure.com"),
|
||||
Ok(Host::Hostname(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
Host::from_str("www.cs.indiana.edu"),
|
||||
Ok(Host::Hostname(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_inputs_fail() {
|
||||
assert!(matches!(
|
||||
Host::from_str("[uhsure.com]"),
|
||||
Err(HostParseError::CouldNotParseIPv6 { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
Host::from_str("-uhsure.com"),
|
||||
Err(HostParseError::InvalidHostname { .. })
|
||||
));
|
||||
}
|
||||
36
hush/src/main.rs
Normal file
36
hush/src/main.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use hush::config::client::ClientConfiguration;
|
||||
use std::process;
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn main() {
|
||||
let mut config = match ClientConfiguration::new(std::env::args()) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("ERROR: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = hush::config::establish_subscribers(&config.logging, &mut config.runtime) {
|
||||
eprintln!("ERROR: could not set up logging infrastructure: {}", e);
|
||||
process::exit(2);
|
||||
}
|
||||
|
||||
let runtime = match hush::config::configured_runtime(&config.runtime) {
|
||||
Ok(runtime) => runtime,
|
||||
Err(e) => {
|
||||
tracing::error!(%e, "could not start system runtime");
|
||||
process::exit(3);
|
||||
}
|
||||
};
|
||||
|
||||
let result = runtime.block_on(async move {
|
||||
tracing::info!("Starting Hush");
|
||||
hush::client::hush(config).await
|
||||
});
|
||||
|
||||
if let Err(e) = result {
|
||||
tracing::error!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
68
hushd/src/main.rs
Normal file
68
hushd/src/main.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use hush::config::server::ServerConfiguration;
|
||||
use std::process;
|
||||
|
||||
//async fn run_dns() -> tokio::io::Result<()> {
|
||||
// let socket = UdpSocket::bind("127.0.0.1:3553").await?;
|
||||
// let mut buffer = [0; 65536];
|
||||
//
|
||||
// tracing::info!(
|
||||
// "Bound socket at {}, starting main DNS handler loop.",
|
||||
// socket.local_addr()?
|
||||
// );
|
||||
// let (first_len, first_addr) = socket.recv_from(&mut buffer).await?;
|
||||
// println!("Received {} bytes from {}", first_len, first_addr);
|
||||
//
|
||||
// let remote = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 3553));
|
||||
// let (mut stream, _handle) = UdpStream::with_bound(socket, remote);
|
||||
//
|
||||
// while let Some(item) = stream.next().await {
|
||||
// match item {
|
||||
// Err(e) => eprintln!("Got an error: {:?}", e),
|
||||
// Ok(msg) => {
|
||||
// println!("Got a message from {}", msg.addr());
|
||||
// match msg.to_message() {
|
||||
// Err(e) => println!(" ... but it was malformed ({e})."),
|
||||
// Ok(v) => println!(" ... and it was {v}"),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Ok(())
|
||||
//}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn main() {
|
||||
let mut config = match ServerConfiguration::new(std::env::args()) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("ERROR: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = hush::config::establish_subscribers(&config.logging, &mut config.runtime) {
|
||||
eprintln!("ERROR: could not set up logging infrastruxture: {}", e);
|
||||
process::exit(2);
|
||||
};
|
||||
|
||||
let runtime = match hush::config::configured_runtime(&config.runtime) {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
tracing::error!(%error, "could not start system runtime");
|
||||
process::exit(3);
|
||||
}
|
||||
};
|
||||
|
||||
let result = runtime.block_on(async move {
|
||||
let version =
|
||||
std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "<unknown>".to_string());
|
||||
tracing::info!(%version, "Starting Hush server (hushd)");
|
||||
hush::server::run(config).await
|
||||
});
|
||||
|
||||
if let Err(e) = result {
|
||||
tracing::error!("{}", e);
|
||||
process::exit(5);
|
||||
}
|
||||
}
|
||||
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 { .. })));
|
||||
}
|
||||
20
resolver/Cargo.toml
Normal file
20
resolver/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "resolver"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bytes = { workspace = true }
|
||||
configuration = { workspace = true }
|
||||
crypto = { workspace = true }
|
||||
error-stack = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
internment = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
proptest-derive = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
247
resolver/src/lib.rs
Normal file
247
resolver/src/lib.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
pub mod name;
|
||||
mod protocol;
|
||||
mod resolution_table;
|
||||
|
||||
use configuration::resolver::{DnsConfig, NameServerConfig};
|
||||
use crate::name::Name;
|
||||
use crate::resolution_table::ResolutionTable;
|
||||
use error_stack::{report, ResultExt};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tokio::net::{TcpSocket, UdpSocket};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ResolverConfigError {
|
||||
#[error("Bad local domain name provided")]
|
||||
BadDomainName,
|
||||
#[error("Couldn't create a DNS client for the given address, port, and protocol.")]
|
||||
FailedToCreateDnsClient,
|
||||
#[error("No DNS servers found to search, and mDNS not enabled")]
|
||||
NoHosts,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ResolveError {
|
||||
#[error("No servers available for query")]
|
||||
NoServersAvailable,
|
||||
#[error("No responses found for query")]
|
||||
NoResponses,
|
||||
#[error("Error reading response from server")]
|
||||
ResponseError,
|
||||
}
|
||||
|
||||
pub struct Resolver {
|
||||
search_domains: Vec<Name>,
|
||||
max_time_to_wait_for_initial: Duration,
|
||||
time_to_wait_after_first: Duration,
|
||||
time_to_wait_for_lingering: Duration,
|
||||
connections: Arc<Mutex<Vec<(NameServerConfig, protocol::Client)>>>,
|
||||
table: Arc<Mutex<ResolutionTable>>,
|
||||
tasks: JoinSet<()>,
|
||||
}
|
||||
|
||||
pub struct ResolverState {
|
||||
client_connections: Vec<(NameServerConfig, protocol::Client)>,
|
||||
cache: HashMap<Name, Vec<DnsResolution>>,
|
||||
}
|
||||
|
||||
pub struct DnsResolution {
|
||||
address: IpAddr,
|
||||
expires_at: Instant,
|
||||
}
|
||||
|
||||
impl Resolver {
|
||||
/// Create a new DNS resolution engine for use by some part of the system.
|
||||
pub async fn new(config: &DnsConfig) -> error_stack::Result<Self, ResolverConfigError> {
|
||||
let mut search_domains = Vec::new();
|
||||
let mut tasks = JoinSet::new();
|
||||
|
||||
if let Some(local) = config.local_domain.as_ref() {
|
||||
let name = Name::from_str(local)
|
||||
.change_context(ResolverConfigError::BadDomainName)
|
||||
.attach_printable("Trying to add local domain.")
|
||||
.attach_printable_lazy(|| "Offending non-name: '{local}'")?;
|
||||
|
||||
search_domains.push(name);
|
||||
}
|
||||
|
||||
for search_domain in config.search_domains.iter() {
|
||||
let name = Name::from_str(search_domain)
|
||||
.change_context(ResolverConfigError::BadDomainName)
|
||||
.attach_printable("Trying to add search domain.")
|
||||
.attach_printable_lazy(|| "Offending non-name: '{search_domain}'")?;
|
||||
|
||||
search_domains.push(name);
|
||||
}
|
||||
|
||||
let mut client_connections = Vec::new();
|
||||
|
||||
for target in config.name_servers.iter() {
|
||||
let port = target.address.port().unwrap_or(53);
|
||||
let Some(address) = target.address.host() else {
|
||||
return Err(report!(ResolverConfigError::FailedToCreateDnsClient))
|
||||
.attach_printable("No address to connect to?")
|
||||
.attach_printable_lazy(|| format!("Target address was {}", target.address));
|
||||
};
|
||||
|
||||
let address = match address {
|
||||
url::Host::Ipv4(addr) => IpAddr::V4(addr),
|
||||
url::Host::Ipv6(addr) => IpAddr::V6(addr),
|
||||
url::Host::Domain(name) => {
|
||||
return Err(report!(ResolverConfigError::FailedToCreateDnsClient))
|
||||
.attach_printable("Cannot use domain names to identify domain servers")
|
||||
.attach_printable_lazy(|| format!("Target address was {name}"));
|
||||
}
|
||||
};
|
||||
let target_addr = SocketAddr::new(address, port);
|
||||
|
||||
match target.address.scheme() {
|
||||
"tcp" => {
|
||||
let socket = if target_addr.is_ipv4() {
|
||||
TcpSocket::new_v4()
|
||||
} else {
|
||||
TcpSocket::new_v6()
|
||||
};
|
||||
|
||||
let socket = socket
|
||||
.change_context(ResolverConfigError::FailedToCreateDnsClient)
|
||||
.attach_printable("Could not create a socket")
|
||||
.attach_printable_lazy(|| {
|
||||
format!("For target DNS server {}", target.address)
|
||||
})?;
|
||||
|
||||
if let Some(bind_address) = target.bind_address {
|
||||
socket
|
||||
.bind(bind_address)
|
||||
.change_context(ResolverConfigError::FailedToCreateDnsClient)
|
||||
.attach_printable("Could not bind local address for socket.")
|
||||
.attach_printable_lazy(|| {
|
||||
format!("Binding to TCP address {}", bind_address)
|
||||
})
|
||||
.attach_printable_lazy(|| {
|
||||
format!("For target DNS server {}", target.address)
|
||||
})?;
|
||||
}
|
||||
|
||||
let stream = socket
|
||||
.connect(target_addr)
|
||||
.await
|
||||
.change_context(ResolverConfigError::FailedToCreateDnsClient)
|
||||
.attach_printable_lazy(|| {
|
||||
format!("Connecting to target {}", target_addr)
|
||||
})?;
|
||||
let client = protocol::Client::from_tcp(stream, &mut tasks)
|
||||
.await
|
||||
.change_context(ResolverConfigError::FailedToCreateDnsClient)
|
||||
.attach_printable_lazy(|| {
|
||||
format!("Connecting to target {}", target_addr)
|
||||
})?;
|
||||
|
||||
client_connections.push((target.clone(), client));
|
||||
}
|
||||
|
||||
"udp" => {
|
||||
let port = target.address.port().unwrap_or(53);
|
||||
let Some(address) = target.address.host() else {
|
||||
tracing::warn!(address = %target.address, "proposed domain server has no host");
|
||||
continue;
|
||||
};
|
||||
let address = match address {
|
||||
url::Host::Ipv4(addr) => IpAddr::V4(addr),
|
||||
url::Host::Ipv6(addr) => IpAddr::V6(addr),
|
||||
url::Host::Domain(name) => {
|
||||
tracing::warn!(
|
||||
address = %target.address,
|
||||
hostname = name,
|
||||
"currently, we can't use hostnames for domain servers"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let sock_addr = SocketAddr::new(address, port);
|
||||
let bind_address = target.bind_address.unwrap_or_else(|| {
|
||||
if sock_addr.is_ipv4() {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0)
|
||||
} else {
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), 0)
|
||||
}
|
||||
});
|
||||
|
||||
let udp_socket = UdpSocket::bind(bind_address)
|
||||
.await
|
||||
.change_context(ResolverConfigError::FailedToCreateDnsClient)
|
||||
.attach_printable("Generating UDP socket")
|
||||
.attach_printable_lazy(|| format!("binding to address {bind_address}"))
|
||||
.attach_printable_lazy(|| format!("targeting {}", target.address))?;
|
||||
|
||||
udp_socket
|
||||
.connect(target_addr)
|
||||
.await
|
||||
.change_context(ResolverConfigError::FailedToCreateDnsClient)
|
||||
.attach_printable("Connecting UDP socket")
|
||||
.attach_printable_lazy(|| format!("binding to address {bind_address}"))
|
||||
.attach_printable_lazy(|| format!("targeting {}", target.address))?;
|
||||
|
||||
let client = protocol::Client::from_udp(udp_socket, &mut tasks).await;
|
||||
|
||||
client_connections.push((target.clone(), client));
|
||||
}
|
||||
|
||||
"unix" => unimplemented!(),
|
||||
"unixd" => unimplemented!(),
|
||||
|
||||
"http" => unimplemented!(),
|
||||
"https" => unimplemented!(),
|
||||
|
||||
_ => {
|
||||
tracing::warn!(address = %target.address, "Unknown scheme for building DNS connections");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Resolver {
|
||||
search_domains,
|
||||
// FIXME: All of these should be configurable
|
||||
max_time_to_wait_for_initial: Duration::from_millis(150),
|
||||
time_to_wait_after_first: Duration::from_millis(50),
|
||||
time_to_wait_for_lingering: Duration::from_secs(2),
|
||||
connections: Arc::new(Mutex::new(client_connections)),
|
||||
table: Arc::new(Mutex::new(ResolutionTable::new())),
|
||||
tasks,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn lookup(&self, _name: &Name) -> error_stack::Result<HashSet<IpAddr>, ResolveError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
//#[tokio::test]
|
||||
//async fn fetch_cached() {
|
||||
// let resolver = Resolver::new(&DnsConfig::empty()).await.unwrap();
|
||||
// let name = Name::from_utf8("name.foo").unwrap();
|
||||
// let addr = IpAddr::from_str("1.2.4.5").unwrap();
|
||||
//
|
||||
// resolver
|
||||
// .inject_resolution(name.clone(), addr.clone(), Duration::from_secs(100000))
|
||||
// .await;
|
||||
// let read = resolver.lookup(&name).await.unwrap();
|
||||
// assert!(read.contains(&addr));
|
||||
//}
|
||||
|
||||
//#[tokio::test]
|
||||
//async fn uhsure() {
|
||||
// let resolver = Resolver::new(&DnsConfig::default()).await.unwrap();
|
||||
// let name = Name::from_ascii("uhsure.com").unwrap();
|
||||
// let result = resolver.lookup(&name).await.unwrap();
|
||||
// println!("result = {:?}", result);
|
||||
// assert!(!result.is_empty());
|
||||
//}
|
||||
505
resolver/src/name.rs
Normal file
505
resolver/src/name.rs
Normal file
@@ -0,0 +1,505 @@
|
||||
use bytes::{Buf, BufMut};
|
||||
use error_stack::{report, ResultExt};
|
||||
use internment::ArcIntern;
|
||||
use proptest::arbitrary::Arbitrary;
|
||||
use proptest::char::CharStrategy;
|
||||
use proptest::strategy::{BoxedStrategy, Strategy};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::hash::Hash;
|
||||
use std::ops::{Range, RangeInclusive};
|
||||
use std::str::FromStr;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
pub struct Name {
|
||||
labels: Vec<Label>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut first_section = true;
|
||||
|
||||
for &Label(ref label) in self.labels.iter() {
|
||||
if first_section {
|
||||
first_section = false;
|
||||
write!(f, "{}", label.as_str())?;
|
||||
} else {
|
||||
write!(f, ".{}", label.as_str())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
<Self as fmt::Debug>::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, Eq)]
|
||||
pub struct Label(ArcIntern<String>);
|
||||
|
||||
impl fmt::Debug for Label {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.as_str().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Label {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.as_str().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Label {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.eq_ignore_ascii_case(other.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NameParseError {
|
||||
#[error("Provided name '{name}' is too long ({observed_length} bytes); maximum length of a DNS name is 255 octets.")]
|
||||
NameTooLong {
|
||||
name: String,
|
||||
observed_length: usize,
|
||||
},
|
||||
|
||||
#[error("Provided name '{name}' contains illegal character '{illegal_character}'.")]
|
||||
NonAsciiCharacter {
|
||||
name: String,
|
||||
illegal_character: char,
|
||||
},
|
||||
|
||||
#[error("Provided name '{name}' contains an empty section/label, which isn't allowed.")]
|
||||
EmptyLabel { name: String },
|
||||
|
||||
#[error("Provided name '{name}' contains a label ('{label}') that is too long ({observed_length} letters, which is more than 63).")]
|
||||
LabelTooLong {
|
||||
observed_length: usize,
|
||||
name: String,
|
||||
label: String,
|
||||
},
|
||||
|
||||
#[error("Provided name '{name}' contains a label ('{label}') that begins with an illegal character; it must be a letter.")]
|
||||
LabelStartsWrong { name: String, label: String },
|
||||
|
||||
#[error("Provided name '{name}' contains a label ('{label}') that ends with an illegal character; it must be a letter or number.")]
|
||||
LabelEndsWrong { name: String, label: String },
|
||||
|
||||
#[error("Provided name '{name}' contains a label ('{label}') that contains a non-letter, non-number, and non-dash.")]
|
||||
IllegalInternalCharacter { name: String, label: String },
|
||||
}
|
||||
|
||||
impl FromStr for Name {
|
||||
type Err = NameParseError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let observed_length = value.as_bytes().len();
|
||||
|
||||
if observed_length > 255 {
|
||||
return Err(NameParseError::NameTooLong {
|
||||
name: value.to_string(),
|
||||
observed_length,
|
||||
});
|
||||
}
|
||||
|
||||
for char in value.chars() {
|
||||
if !(char.is_ascii_alphanumeric() || char == '.' || char == '-') {
|
||||
return Err(NameParseError::NonAsciiCharacter {
|
||||
name: value.to_string(),
|
||||
illegal_character: char,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut labels = vec![];
|
||||
|
||||
for label_str in value.split('.') {
|
||||
if label_str.is_empty() {
|
||||
return Err(NameParseError::EmptyLabel {
|
||||
name: value.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if label_str.len() > 63 {
|
||||
return Err(NameParseError::LabelTooLong {
|
||||
name: value.to_string(),
|
||||
label: label_str.to_string(),
|
||||
observed_length: label_str.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let letter = |x| ('a'..='z').contains(&x) || ('A'..='Z').contains(&x);
|
||||
let letter_or_num = |x| letter(x) || ('0'..='9').contains(&x);
|
||||
let letter_num_dash = |x| letter_or_num(x) || (x == '-');
|
||||
|
||||
if !label_str.starts_with(letter) {
|
||||
return Err(NameParseError::LabelStartsWrong {
|
||||
name: value.to_string(),
|
||||
label: label_str.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if !label_str.ends_with(letter_or_num) {
|
||||
return Err(NameParseError::LabelEndsWrong {
|
||||
name: value.to_string(),
|
||||
label: label_str.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if label_str.contains(|x| !letter_num_dash(x)) {
|
||||
return Err(NameParseError::IllegalInternalCharacter {
|
||||
name: value.to_string(),
|
||||
label: label_str.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// RFC 1035 says that all domain names are case-insensitive. We
|
||||
// arbitrarily normalize to lowercase here, because it shouldn't
|
||||
// matter to anyone.
|
||||
labels.push(Label(ArcIntern::new(label_str.to_string())));
|
||||
}
|
||||
|
||||
Ok(Name { labels })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NameReadError {
|
||||
#[error("Could not read name field out of an empty buffer.")]
|
||||
EmptyBuffer,
|
||||
|
||||
#[error("Buffer truncated before we could read the last label")]
|
||||
TruncatedBuffer,
|
||||
|
||||
#[error("Provided label value too long; must be 63 octets or less.")]
|
||||
LabelTooLong,
|
||||
|
||||
#[error("Label truncated while reading. Broken stream?")]
|
||||
LabelTruncated,
|
||||
|
||||
#[error("Label starts with an illegal character (must be [A-Za-z])")]
|
||||
WrongFirstByte,
|
||||
|
||||
#[error("Label ends with an illegal character (must be [A-Za-z0-9]")]
|
||||
WrongLastByte,
|
||||
|
||||
#[error("Label contains an illegal character (must be [A-Za-z0-9] or a dash)")]
|
||||
WrongInnerByte,
|
||||
}
|
||||
|
||||
impl Name {
|
||||
/// Read a name frm a record, or return an error describing what went wrong.
|
||||
///
|
||||
/// This function will advance the read pointer on the record. If the result is
|
||||
/// an error, the read pointer is not guaranteed to be in a good place, so you
|
||||
/// may need to manage that externally if you want to implement some sort of
|
||||
/// "try" functionality.
|
||||
pub fn read<B: Buf>(buffer: &mut B) -> error_stack::Result<Name, NameReadError> {
|
||||
let mut labels = Vec::new();
|
||||
let name_so_far = |labels: Vec<Label>| {
|
||||
let mut result = String::new();
|
||||
|
||||
for Label(label) in labels.into_iter() {
|
||||
if result.is_empty() {
|
||||
result.push_str(label.as_str());
|
||||
} else {
|
||||
result.push('.');
|
||||
result.push_str(label.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
};
|
||||
|
||||
loop {
|
||||
if !buffer.has_remaining() && labels.is_empty() {
|
||||
return Err(report!(NameReadError::EmptyBuffer));
|
||||
}
|
||||
|
||||
if !buffer.has_remaining() {
|
||||
return Err(report!(NameReadError::TruncatedBuffer))
|
||||
.attach_printable_lazy(|| format!("name thus far: '{}'", name_so_far(labels)));
|
||||
}
|
||||
|
||||
let label_octet_length = buffer.get_u8() as usize;
|
||||
if label_octet_length == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if label_octet_length > 63 {
|
||||
return Err(report!(NameReadError::LabelTooLong)).attach_printable_lazy(|| {
|
||||
format!(
|
||||
"label length too big; max is supposed to be 63, saw {label_octet_length}"
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
if !buffer.remaining() < label_octet_length {
|
||||
let remaining = buffer.copy_to_bytes(buffer.remaining());
|
||||
let partial = String::from_utf8_lossy(&remaining).to_string();
|
||||
|
||||
return Err(report!(NameReadError::LabelTruncated))
|
||||
.attach_printable_lazy(|| format!("name thus far: '{}'", name_so_far(labels)))
|
||||
.attach_printable_lazy(|| {
|
||||
format!(
|
||||
"expected {} octets, but only found {}",
|
||||
label_octet_length,
|
||||
buffer.remaining()
|
||||
)
|
||||
})
|
||||
.attach_printable_lazy(|| format!("partial read: '{partial}'"));
|
||||
}
|
||||
|
||||
let label_bytes = buffer.copy_to_bytes(label_octet_length);
|
||||
let Some(first_byte) = label_bytes.first() else {
|
||||
panic!(
|
||||
"INTERNAL ERROR: Should have at least one byte, we checked this previously."
|
||||
);
|
||||
};
|
||||
let Some(last_byte) = label_bytes.last() else {
|
||||
panic!(
|
||||
"INTERNAL ERROR: Should have at least one byte, we checked this previously."
|
||||
);
|
||||
};
|
||||
|
||||
let letter = |x| (b'a'..=b'z').contains(&x) || (b'A'..=b'Z').contains(&x);
|
||||
let letter_or_num = |x| letter(x) || (b'0'..=b'9').contains(&x);
|
||||
let letter_num_dash = |x| letter_or_num(x) || (x == b'-');
|
||||
|
||||
if !letter(*first_byte) {
|
||||
return Err(report!(NameReadError::WrongFirstByte))
|
||||
.attach_printable_lazy(|| format!("name thus far: '{}'", name_so_far(labels)))
|
||||
.attach_printable_lazy(|| {
|
||||
format!("bad label: '{}'", String::from_utf8_lossy(&label_bytes))
|
||||
});
|
||||
}
|
||||
|
||||
if !letter_or_num(*last_byte) {
|
||||
return Err(report!(NameReadError::WrongLastByte))
|
||||
.attach_printable_lazy(|| format!("name thus far: '{}'", name_so_far(labels)))
|
||||
.attach_printable_lazy(|| {
|
||||
format!("bad label: '{}'", String::from_utf8_lossy(&label_bytes))
|
||||
});
|
||||
}
|
||||
|
||||
if label_bytes.iter().any(|x| !letter_num_dash(*x)) {
|
||||
return Err(report!(NameReadError::WrongInnerByte))
|
||||
.attach_printable_lazy(|| format!("name thus far: '{}'", name_so_far(labels)))
|
||||
.attach_printable_lazy(|| {
|
||||
format!("bad label: '{}'", String::from_utf8_lossy(&label_bytes))
|
||||
});
|
||||
}
|
||||
|
||||
let label = label_bytes.into_iter().map(|x| x as char).collect();
|
||||
labels.push(Label(ArcIntern::new(label)));
|
||||
}
|
||||
|
||||
Ok(Name { labels })
|
||||
}
|
||||
|
||||
/// Write a name out to the given buffer.
|
||||
///
|
||||
/// This will try as hard as it can to write the value to the given buffer. If an
|
||||
/// error occurs, you may end up with partially-written data, so if you're worried
|
||||
/// about that you should be careful to mark where you started in the output buffer.
|
||||
pub fn write<B: BufMut>(&self, buffer: &mut B) -> error_stack::Result<(), NameWriteError> {
|
||||
for &Label(ref label) in self.labels.iter() {
|
||||
let bytes = label.as_bytes();
|
||||
|
||||
if buffer.remaining_mut() < (bytes.len() + 1) {
|
||||
return Err(report!(NameWriteError::NoRoomForLabel))
|
||||
.attach_printable_lazy(|| format!("Writing name {self}"))
|
||||
.attach_printable_lazy(|| format!("For label {label}"));
|
||||
}
|
||||
|
||||
if bytes.is_empty() || bytes.len() > 63 {
|
||||
return Err(report!(NameWriteError::IllegalLabel))
|
||||
.attach_printable_lazy(|| format!("Writing name {self}"))
|
||||
.attach_printable_lazy(|| format!("For label {label}"));
|
||||
}
|
||||
|
||||
buffer.put_u8(bytes.len() as u8);
|
||||
buffer.put_slice(bytes);
|
||||
}
|
||||
|
||||
if !buffer.has_remaining_mut() {
|
||||
return Err(report!(NameWriteError::NoRoomForNull))
|
||||
.attach_printable_lazy(|| format!("Writing name {self}"));
|
||||
}
|
||||
buffer.put_u8(0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NameWriteError {
|
||||
#[error("Not enough room to write label in name")]
|
||||
NoRoomForLabel,
|
||||
|
||||
#[error("Ran out of room writing the terminating NULL for the name")]
|
||||
NoRoomForNull,
|
||||
|
||||
#[error("Internal error: Illegal label (this shouldn't happen)")]
|
||||
IllegalLabel,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ArbitraryDomainNameSpecifications {
|
||||
total_length_range: Range<usize>,
|
||||
label_length_range: Range<usize>,
|
||||
number_labels: Range<usize>,
|
||||
}
|
||||
|
||||
impl Default for ArbitraryDomainNameSpecifications {
|
||||
fn default() -> Self {
|
||||
ArbitraryDomainNameSpecifications {
|
||||
total_length_range: 5..256,
|
||||
label_length_range: 1..64,
|
||||
number_labels: 2..5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for Name {
|
||||
type Parameters = ArbitraryDomainNameSpecifications;
|
||||
type Strategy = BoxedStrategy<Name>;
|
||||
|
||||
fn arbitrary_with(spec: Self::Parameters) -> Self::Strategy {
|
||||
(spec.number_labels.clone(), spec.total_length_range.clone())
|
||||
.prop_flat_map(move |(mut labels, mut total_length)| {
|
||||
// we need to make sure that our total length and our label count are,
|
||||
// at the very minimum, compatible. If they're not, we'll need to adjust
|
||||
// them.
|
||||
//
|
||||
// in general, we prefer to update our number of labels, if we can, but
|
||||
// only to the extent that the labels count is at least the minimum of
|
||||
// our input specification. if we try to go below that, we increase total
|
||||
// length as required. If we're forced into a place where we need to take
|
||||
// the label length below it's minimum and/or the total_length over its
|
||||
// maximum, we just give up and panic.
|
||||
//
|
||||
// Note that the minimum length of n labels is (n * label_minimum) + n - 1.
|
||||
// Consider, for example, a label_minimum of 1 and a label length of 3.
|
||||
// A minimum string is "a.b.c", which is (3 * 1) + 3 - 1 = 5 characters
|
||||
// long.
|
||||
//
|
||||
// This loop does the first part, lowering the number of labels until we
|
||||
// either get below the total length or reach the minimum number of labels.
|
||||
while (labels * spec.label_length_range.start) + (labels - 1) > total_length {
|
||||
if labels == spec.number_labels.start {
|
||||
break;
|
||||
} else {
|
||||
labels -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, if it's not right, we just set it to be right.
|
||||
if (labels * spec.total_length_range.start) + (labels - 1) > total_length {
|
||||
total_length = (labels * spec.total_length_range.start) + (labels - 1);
|
||||
}
|
||||
|
||||
// And if this takes us over our limit, just panic.
|
||||
if total_length >= spec.total_length_range.end {
|
||||
panic!("Unresolvable generation condition; couldn't resolve label count {} with total_length {}, with specification {:?}", labels, total_length, spec);
|
||||
}
|
||||
|
||||
proptest::collection::vec(Label::arbitrary_with(spec.label_length_range.clone()), labels)
|
||||
.prop_map(|labels| Name{ labels })
|
||||
}).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for Label {
|
||||
type Parameters = Range<usize>;
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary() -> Self::Strategy {
|
||||
Self::arbitrary_with(1..64)
|
||||
}
|
||||
|
||||
fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
|
||||
args.prop_flat_map(|length| {
|
||||
let first = char_selector(&['a'..='z', 'A'..='Z']);
|
||||
let middle = char_selector(&['a'..='z', 'A'..='Z', '-'..='-', '0'..='9']);
|
||||
let last = char_selector(&['a'..='z', 'A'..='Z', '0'..='9']);
|
||||
|
||||
match length {
|
||||
0 => panic!("Should not be able to generate a label of length 0"),
|
||||
1 => first.prop_map(|x| x.into()).boxed(),
|
||||
2 => (first, last).prop_map(|(a, b)| format!("{a}{b}")).boxed(),
|
||||
_ => (first, proptest::collection::vec(middle, length - 2), last)
|
||||
.prop_map(move |(first, middle, last)| {
|
||||
let mut result = String::with_capacity(length);
|
||||
result.push(first);
|
||||
for c in middle.iter() {
|
||||
result.push(*c);
|
||||
}
|
||||
result.push(last);
|
||||
result
|
||||
})
|
||||
.boxed(),
|
||||
}
|
||||
})
|
||||
.prop_map(|x| Label(ArcIntern::new(x)))
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn char_selector<'a>(ranges: &'a [RangeInclusive<char>]) -> CharStrategy<'a> {
|
||||
CharStrategy::new(
|
||||
Cow::Borrowed(&[]),
|
||||
Cow::Borrowed(&[]),
|
||||
Cow::Borrowed(ranges),
|
||||
)
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn any_random_names_parses(name: Name) {
|
||||
let name_str = name.to_string();
|
||||
let new_name = Name::from_str(&name_str).expect("can re-parse name");
|
||||
assert_eq!(name, new_name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_random_name_roundtrips(name: Name) {
|
||||
let mut write_buffer = bytes::BytesMut::with_capacity(512);
|
||||
name.write(&mut write_buffer).expect("can write name");
|
||||
let mut read_buffer = write_buffer.freeze();
|
||||
let new_name = Name::read(&mut read_buffer).expect("can read name");
|
||||
assert_eq!(name, new_name);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn illegal_names_generate_errors() {
|
||||
assert!(Name::from_str("").is_err());
|
||||
assert!(Name::from_str(".").is_err());
|
||||
assert!(Name::from_str(".com").is_err());
|
||||
assert!(Name::from_str("com.").is_err());
|
||||
assert!(Name::from_str("9.com").is_err());
|
||||
assert!(Name::from_str("foo-.com").is_err());
|
||||
assert!(Name::from_str("-foo.com").is_err());
|
||||
assert!(Name::from_str(".foo.com").is_err());
|
||||
assert!(Name::from_str("fo*o.com").is_err());
|
||||
assert!(Name::from_str(
|
||||
"foo.abcdefghiabcdefghiabcdefghiabcdefghiabcdefghiabcdefghijjjjjjabcdefghij.com"
|
||||
)
|
||||
.is_err());
|
||||
assert!(Name::from_str("abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_ignore_case() {
|
||||
assert_eq!(
|
||||
Name::from_str("UHSURE.COM").unwrap(),
|
||||
Name::from_str("uhsure.com").unwrap()
|
||||
);
|
||||
}
|
||||
9
resolver/src/protocol.rs
Normal file
9
resolver/src/protocol.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod client;
|
||||
mod header;
|
||||
pub mod question;
|
||||
mod request;
|
||||
mod resource_record;
|
||||
mod response;
|
||||
mod server;
|
||||
|
||||
pub use client::Client;
|
||||
273
resolver/src/protocol/client.rs
Normal file
273
resolver/src/protocol/client.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
use crate::protocol::header::Header;
|
||||
use crate::protocol::question::Question;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use error_stack::ResultExt;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncReadExt, WriteHalf};
|
||||
use tokio::net::{TcpStream, UdpSocket, UnixDatagram, UnixStream};
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
type Callback = fn() -> ();
|
||||
|
||||
pub struct Client {
|
||||
callback: Arc<RwLock<Callback>>,
|
||||
channel: DnsChannel,
|
||||
}
|
||||
|
||||
pub enum DnsChannel {
|
||||
Tcp(WriteHalf<TcpStream>),
|
||||
Udp(Arc<UdpSocket>),
|
||||
UnixData(Arc<UnixDatagram>),
|
||||
Unix(WriteHalf<UnixStream>),
|
||||
}
|
||||
|
||||
fn empty_callback() {}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SendError {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum GeneralAddr {
|
||||
Network(SocketAddr),
|
||||
Unix(std::os::unix::net::SocketAddr),
|
||||
}
|
||||
|
||||
impl From<SocketAddr> for GeneralAddr {
|
||||
fn from(value: SocketAddr) -> Self {
|
||||
GeneralAddr::Network(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tokio::net::unix::SocketAddr> for GeneralAddr {
|
||||
fn from(value: tokio::net::unix::SocketAddr) -> Self {
|
||||
GeneralAddr::Unix(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum ServerProcessorError {
|
||||
#[error("Could not read message header from server.")]
|
||||
CouldNotReadHeader,
|
||||
}
|
||||
|
||||
async fn process_server_response(
|
||||
callback: &Arc<RwLock<Callback>>,
|
||||
mut bytes: Bytes,
|
||||
source: GeneralAddr,
|
||||
) -> error_stack::Result<(), ServerProcessorError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn run_response_processing_loop(
|
||||
server: String,
|
||||
maximum_consecurive_errors: u64,
|
||||
callback: Arc<RwLock<Callback>>,
|
||||
mut fetcher: impl AsyncFnMut() -> Result<(Bytes, GeneralAddr), std::io::Error>,
|
||||
) {
|
||||
let mut consecutive_errors = 0;
|
||||
|
||||
loop {
|
||||
match fetcher().await {
|
||||
Ok((bytes, source)) => {
|
||||
if let Err(e) = process_server_response(&callback, bytes, source).await {
|
||||
consecutive_errors += 1;
|
||||
tracing::warn!(
|
||||
server,
|
||||
error = %e,
|
||||
maximum_consecurive_errors,
|
||||
consecutive_errors,
|
||||
"error processing DNS server response"
|
||||
);
|
||||
} else {
|
||||
consecutive_errors = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
consecutive_errors += 1;
|
||||
tracing::warn!(
|
||||
server,
|
||||
error = %e,
|
||||
maximum_consecurive_errors,
|
||||
consecutive_errors,
|
||||
"failed to read response from DNS server"
|
||||
);
|
||||
|
||||
if consecutive_errors >= maximum_consecurive_errors {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
tracing::error!(
|
||||
server,
|
||||
"quitting DNS response processing loop due to too many consecutive errors"
|
||||
);
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new DNS client from the given, targeted UnixDatagram socket.
|
||||
///
|
||||
/// By "targeted", we mean that this socket should've had `connect` called on
|
||||
/// it, so that the client can just send datagrams to the server without having
|
||||
/// to know where to send them. It is unspecified what will happen to DNS clients
|
||||
/// if you do not call `connect` beforehand.
|
||||
pub async fn from_unix_datagram(socket: UnixDatagram, group: &mut JoinSet<()>) -> Self {
|
||||
let callback = Arc::new(RwLock::new(empty_callback as Callback));
|
||||
let socket = Arc::new(socket);
|
||||
|
||||
let reader_socket = socket.clone();
|
||||
let reader_callback = callback.clone();
|
||||
let server_addr = socket.local_addr()
|
||||
.ok()
|
||||
.map(|x| x.as_pathname().map(|x| x.display().to_string()))
|
||||
.flatten()
|
||||
.unwrap_or_else(|| "<unknown>".into());
|
||||
let server = format!("unixd://{}", server_addr);
|
||||
|
||||
group.spawn(async move {
|
||||
let fetcher = async move || {
|
||||
let mut buffer = BytesMut::with_capacity(16384);
|
||||
|
||||
match reader_socket.recv_buf_from(&mut buffer).await {
|
||||
Err(x) => Err(x),
|
||||
Ok((size, from)) => {
|
||||
unsafe {
|
||||
buffer.set_len(size);
|
||||
};
|
||||
Ok((buffer.freeze(), from.into()))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
run_response_processing_loop(server, 5, reader_callback, fetcher).await;
|
||||
});
|
||||
|
||||
Client {
|
||||
callback,
|
||||
channel: DnsChannel::UnixData(socket),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_unix_stream(
|
||||
socket: UnixStream,
|
||||
group: &mut JoinSet<()>,
|
||||
) -> std::io::Result<Self> {
|
||||
let callback = Arc::new(RwLock::new(empty_callback as Callback));
|
||||
let server_addr = socket.local_addr()
|
||||
.ok()
|
||||
.map(|x| x.as_pathname().map(|x| x.display().to_string()))
|
||||
.flatten()
|
||||
.unwrap_or_else(|| "<unknown>".into());
|
||||
let server = format!("unix://{}", server_addr);
|
||||
let other_side = GeneralAddr::Unix(socket.peer_addr()?.into());
|
||||
let (mut reader, writer) = tokio::io::split(socket);
|
||||
|
||||
let reader_callback = callback.clone();
|
||||
group.spawn(async move {
|
||||
let fetcher = async move || {
|
||||
let size = reader.read_u16().await?;
|
||||
|
||||
let mut buffer = vec![0u8; size as usize];
|
||||
reader.read_exact(&mut buffer).await?;
|
||||
|
||||
Ok((Bytes::from(buffer), other_side.clone()))
|
||||
};
|
||||
|
||||
run_response_processing_loop(server, 5, reader_callback, fetcher).await;
|
||||
});
|
||||
|
||||
Ok(Client {
|
||||
callback,
|
||||
channel: DnsChannel::Unix(writer),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn from_udp(socket: UdpSocket, group: &mut JoinSet<()>) -> Self {
|
||||
let callback = Arc::new(RwLock::new(empty_callback as Callback));
|
||||
let socket = Arc::new(socket);
|
||||
|
||||
let reader_callback = callback.clone();
|
||||
let reader_socket = socket.clone();
|
||||
let server_addr = socket.local_addr()
|
||||
.ok()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "<unknown>".into());
|
||||
let server = format!("udp://{}", server_addr);
|
||||
|
||||
group.spawn(async move {
|
||||
let fetcher = async move || {
|
||||
let mut buffer = BytesMut::with_capacity(16384);
|
||||
|
||||
match reader_socket.recv_buf_from(&mut buffer).await {
|
||||
Err(x) => Err(x),
|
||||
Ok((size, from)) => {
|
||||
unsafe {
|
||||
buffer.set_len(size);
|
||||
};
|
||||
Ok((buffer.freeze(), from.into()))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
run_response_processing_loop(server, 5, reader_callback, fetcher).await;
|
||||
});
|
||||
|
||||
Client {
|
||||
callback,
|
||||
channel: DnsChannel::Udp(socket),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_tcp(socket: TcpStream, group: &mut JoinSet<()>) -> std::io::Result<Self> {
|
||||
let callback = Arc::new(RwLock::new(empty_callback as Callback));
|
||||
let other_side = GeneralAddr::Network(socket.peer_addr()?);
|
||||
let server_addr = socket.local_addr()
|
||||
.ok()
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| "<unknown>".into());
|
||||
let server = format!("tcp://{}", server_addr);
|
||||
let (mut reader, writer) = tokio::io::split(socket);
|
||||
|
||||
let reader_callback = callback.clone();
|
||||
group.spawn(async move {
|
||||
let fetcher = async move || {
|
||||
let size = reader.read_u16().await?;
|
||||
|
||||
let mut buffer = vec![0u8; size as usize];
|
||||
reader.read_exact(&mut buffer).await?;
|
||||
|
||||
Ok((Bytes::from(buffer), other_side.clone()))
|
||||
};
|
||||
|
||||
run_response_processing_loop(server, 5, reader_callback, fetcher).await;
|
||||
});
|
||||
|
||||
Ok(Client {
|
||||
callback,
|
||||
channel: DnsChannel::Tcp(writer),
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a set of questions to the upstream server.
|
||||
///
|
||||
/// Any response(s) that is/are sent will be handled as part of the callback
|
||||
/// scheme. This function will thus only return an error if there's a problem
|
||||
/// sending the question to the server.
|
||||
pub async fn send_questions(_questions: Vec<Question>) -> error_stack::Result<(), SendError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the callback handler for when we receive responses from the server.
|
||||
///
|
||||
/// This function may take awhile to execute, depending on how busy we are
|
||||
/// taking responses, as it takes write ownership of a read-write lock that's
|
||||
/// almost always written.
|
||||
pub async fn set_callback(&self, callback: Callback) {
|
||||
*self.callback.write().await = callback;
|
||||
}
|
||||
}
|
||||
225
resolver/src/protocol/header.rs
Normal file
225
resolver/src/protocol/header.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use bytes::{Buf, BufMut};
|
||||
use error_stack::{report, ResultExt};
|
||||
use num_enum::{FromPrimitive, IntoPrimitive};
|
||||
use proptest::arbitrary::Arbitrary;
|
||||
use proptest::strategy::{BoxedStrategy, Just, Strategy};
|
||||
use std::fmt;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, proptest_derive::Arbitrary)]
|
||||
pub struct Header {
|
||||
pub message_id: u16,
|
||||
pub is_response: bool,
|
||||
pub opcode: OpCode,
|
||||
pub authoritative_answer: bool,
|
||||
pub message_truncated: bool,
|
||||
pub recursion_desired: bool,
|
||||
pub recursion_available: bool,
|
||||
pub response_code: ResponseCode,
|
||||
pub question_count: u16,
|
||||
pub answer_count: u16,
|
||||
pub name_server_count: u16,
|
||||
pub additional_record_count: u16,
|
||||
}
|
||||
|
||||
#[derive(FromPrimitive, IntoPrimitive, PartialEq, PartialOrd, Eq, Ord, Debug, Copy, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum OpCode {
|
||||
StandardQuery = 0,
|
||||
InverseQuery = 1,
|
||||
ServiceStatusRequest = 2,
|
||||
#[num_enum(catch_all)]
|
||||
Other(u8),
|
||||
}
|
||||
|
||||
impl Arbitrary for OpCode {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
||||
// while the type is 8 bits in rust, it's 4 bits in the protocol,
|
||||
// and dealing with the run-off is messy. so this only generates
|
||||
// valid-sized values here. it also biases toward the legit values.
|
||||
// both things might want to be reconsidered in the future.
|
||||
proptest::prop_oneof![
|
||||
Just(OpCode::StandardQuery),
|
||||
Just(OpCode::InverseQuery),
|
||||
Just(OpCode::ServiceStatusRequest),
|
||||
(3u8..=15).prop_map(OpCode::Other),
|
||||
]
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromPrimitive, IntoPrimitive, PartialEq, PartialOrd, Eq, Ord, Debug, Copy, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum ResponseCode {
|
||||
NoErrorConditions = 0,
|
||||
FormatError = 1,
|
||||
ServerFailure = 2,
|
||||
NameError = 3,
|
||||
NotImplemented = 4,
|
||||
Refused = 5,
|
||||
#[num_enum(catch_all)]
|
||||
Other(u8),
|
||||
}
|
||||
|
||||
impl fmt::Display for ResponseCode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ResponseCode::NoErrorConditions => write!(f, "No errors"),
|
||||
ResponseCode::FormatError => write!(f, "Illegal format"),
|
||||
ResponseCode::ServerFailure => write!(f, "Server failure"),
|
||||
ResponseCode::NameError => write!(f, "Name error"),
|
||||
ResponseCode::NotImplemented => write!(f, "Not implemented"),
|
||||
ResponseCode::Refused => write!(f, "Refused"),
|
||||
ResponseCode::Other(x) => write!(f, "unknown error {x}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for ResponseCode {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
||||
// while the type is 8 bits in rust, it's 4 bits in the protocol,
|
||||
// and dealing with the run-off is messy. so this only generates
|
||||
// valid-sized values here. it also biases toward the legit values.
|
||||
// both things might want to be reconsidered in the future.
|
||||
proptest::prop_oneof![
|
||||
Just(ResponseCode::NoErrorConditions),
|
||||
Just(ResponseCode::FormatError),
|
||||
Just(ResponseCode::ServerFailure),
|
||||
Just(ResponseCode::NameError),
|
||||
Just(ResponseCode::NotImplemented),
|
||||
Just(ResponseCode::Refused),
|
||||
(6u8..=15).prop_map(ResponseCode::Other),
|
||||
]
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HeaderReadError {
|
||||
#[error("Buffer not large enough to have a DNS message header in it.")]
|
||||
BufferTooSmall,
|
||||
|
||||
#[error("Invalid data in zero-filled space.")]
|
||||
NonZeroInZeroes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HeaderWriteError {
|
||||
#[error("Buffer not large enough to write header.")]
|
||||
BufferTooSmall,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub fn read<B: Buf>(buffer: &mut B) -> error_stack::Result<Self, HeaderReadError> {
|
||||
if buffer.remaining() < 12
|
||||
/* 6 16-bit fields = 6 * 2 = 1 */
|
||||
{
|
||||
return Err(report!(HeaderReadError::BufferTooSmall)).attach_printable_lazy(|| {
|
||||
format!(
|
||||
"Need at least {} bytes, but only had {}",
|
||||
6 * 12,
|
||||
buffer.remaining()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
let message_id = buffer.get_u16();
|
||||
let flags = buffer.get_u16();
|
||||
let question_count = buffer.get_u16();
|
||||
let answer_count = buffer.get_u16();
|
||||
let name_server_count = buffer.get_u16();
|
||||
let additional_record_count = buffer.get_u16();
|
||||
|
||||
let is_response = (0x8000 & flags) != 0;
|
||||
let opcode = OpCode::from(((flags >> 11) & 0xF) as u8);
|
||||
let authoritative_answer = (0x0400 & flags) != 0;
|
||||
let message_truncated = (0x0200 & flags) != 0;
|
||||
let recursion_desired = (0x0100 & flags) != 0;
|
||||
let recursion_available = (0x0080 & flags) != 0;
|
||||
let zeroes = 0x0070 & flags;
|
||||
let response_code = ResponseCode::from((flags & 0x000F) as u8);
|
||||
|
||||
if zeroes != 0 {
|
||||
return Err(report!(HeaderReadError::NonZeroInZeroes))
|
||||
.attach_printable_lazy(|| format!("Saw {:#x} instead.", zeroes >> 4));
|
||||
}
|
||||
|
||||
Ok(Header {
|
||||
message_id,
|
||||
is_response,
|
||||
opcode,
|
||||
authoritative_answer,
|
||||
message_truncated,
|
||||
recursion_desired,
|
||||
recursion_available,
|
||||
response_code,
|
||||
question_count,
|
||||
answer_count,
|
||||
name_server_count,
|
||||
additional_record_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<B: BufMut>(self, buffer: &mut B) -> error_stack::Result<(), HeaderWriteError> {
|
||||
if buffer.remaining_mut() < 12
|
||||
/* 6 16-bit fields = 6 * 2 = 1 */
|
||||
{
|
||||
return Err(report!(HeaderWriteError::BufferTooSmall)).attach_printable_lazy(|| {
|
||||
format!(
|
||||
"Need at least {} to write DNS header, only have {}",
|
||||
6 * 12,
|
||||
buffer.remaining_mut()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
let mut flags: u16 = 0;
|
||||
|
||||
if self.is_response {
|
||||
flags |= 0x8000;
|
||||
}
|
||||
let opcode: u8 = self.opcode.into();
|
||||
flags |= (opcode as u16) << 11;
|
||||
if self.authoritative_answer {
|
||||
flags |= 0x0400;
|
||||
}
|
||||
if self.message_truncated {
|
||||
flags |= 0x0200;
|
||||
}
|
||||
if self.recursion_desired {
|
||||
flags |= 0x0100;
|
||||
}
|
||||
if self.recursion_available {
|
||||
flags |= 0x0080;
|
||||
}
|
||||
let response_code: u8 = self.response_code.into();
|
||||
flags |= response_code as u16;
|
||||
|
||||
buffer.put_u16(self.message_id);
|
||||
buffer.put_u16(flags);
|
||||
buffer.put_u16(self.question_count);
|
||||
buffer.put_u16(self.answer_count);
|
||||
buffer.put_u16(self.name_server_count);
|
||||
buffer.put_u16(self.additional_record_count);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn headers_roundtrip(header: Header) {
|
||||
let mut write_buffer = bytes::BytesMut::with_capacity(128 * 1024);
|
||||
let safe_header = header.clone();
|
||||
header.write(&mut write_buffer).expect("can write name");
|
||||
let mut read_buffer = write_buffer.freeze();
|
||||
let new_header = Header::read(&mut read_buffer).expect("can read name");
|
||||
assert_eq!(safe_header, new_header);
|
||||
}
|
||||
}
|
||||
91
resolver/src/protocol/question.rs
Normal file
91
resolver/src/protocol/question.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::name::Name;
|
||||
use crate::protocol::resource_record::raw::{RecordClass, RecordType};
|
||||
use bytes::{Buf, BufMut, TryGetError};
|
||||
use error_stack::{report, ResultExt};
|
||||
use std::fmt;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, proptest_derive::Arbitrary)]
|
||||
pub struct Question {
|
||||
name: Name,
|
||||
record_type: RecordType,
|
||||
record_class: RecordClass,
|
||||
}
|
||||
|
||||
impl fmt::Display for Question {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "<Q:{}@{}.{}>", self.name, self.record_type, self.record_class)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum QuestionReadError {
|
||||
#[error("Could not read name for question.")]
|
||||
CouldNotReadName,
|
||||
|
||||
#[error("Could not read the record type for the question: {0}")]
|
||||
CouldNotReadType(TryGetError),
|
||||
|
||||
#[error("Could not read the record class for the question.")]
|
||||
CouldNotReadClass(TryGetError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum QuestionWriteError {
|
||||
#[error("Could not write name for the question.")]
|
||||
CouldNotWriteName,
|
||||
|
||||
#[error("Buffer not large enough to write question type and class.")]
|
||||
BufferTooSmall,
|
||||
}
|
||||
|
||||
impl Question {
|
||||
pub fn read<B: Buf>(buffer: &mut B) -> error_stack::Result<Self, QuestionReadError> {
|
||||
let name = Name::read(buffer).change_context(QuestionReadError::CouldNotReadName)?;
|
||||
let record_type_u16 = buffer
|
||||
.try_get_u16()
|
||||
.map_err(|e| report!(QuestionReadError::CouldNotReadType(e)))
|
||||
.attach_printable_lazy(|| format!("question was about '{name}'"))?;
|
||||
let record_class_u16 = buffer
|
||||
.try_get_u16()
|
||||
.map_err(|e| report!(QuestionReadError::CouldNotReadClass(e)))
|
||||
.attach_printable_lazy(|| format!("question was about '{name}'"))?;
|
||||
|
||||
let record_type = RecordType::from(record_type_u16);
|
||||
let record_class = RecordClass::from(record_class_u16);
|
||||
|
||||
Ok(Question {
|
||||
name,
|
||||
record_type,
|
||||
record_class,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<B: BufMut>(&self, buffer: &mut B) -> error_stack::Result<(), QuestionWriteError> {
|
||||
self.name
|
||||
.write(buffer)
|
||||
.change_context(QuestionWriteError::CouldNotWriteName)
|
||||
.attach_printable_lazy(|| format!("Question was about '{}'", self.name))?;
|
||||
|
||||
if buffer.remaining_mut() < 4 {
|
||||
return Err(report!(QuestionWriteError::BufferTooSmall))
|
||||
.attach_printable_lazy(|| format!("Question was about '{}'", self.name));
|
||||
}
|
||||
|
||||
buffer.put_u16(self.record_type.into());
|
||||
buffer.put_u16(self.record_class.into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn questions_roundtrip(question: Question) {
|
||||
let mut write_buffer = bytes::BytesMut::with_capacity(128 * 1024);
|
||||
question.write(&mut write_buffer).expect("can write name");
|
||||
let mut read_buffer = write_buffer.freeze();
|
||||
let new_question = Question::read(&mut read_buffer).expect("can read name");
|
||||
assert_eq!(question, new_question);
|
||||
}
|
||||
}
|
||||
156
resolver/src/protocol/request.rs
Normal file
156
resolver/src/protocol/request.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use bytes::{Buf, BufMut};
|
||||
use error_stack::{report, ResultExt};
|
||||
use crate::protocol::header::{Header, OpCode, ResponseCode};
|
||||
use crate::protocol::question::Question;
|
||||
use crate::protocol::resource_record::ResourceRecord;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, proptest_derive::Arbitrary)]
|
||||
pub struct Request {
|
||||
source_message_id: u16,
|
||||
opcode: OpCode,
|
||||
recursion_desired: bool,
|
||||
questions: Vec<Question>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RequestReadError {
|
||||
#[error("Error reading request header.")]
|
||||
Header,
|
||||
#[error("Message isn't a request.")]
|
||||
NotRequest,
|
||||
#[error("Illegal message.")]
|
||||
IllegalMessage,
|
||||
#[error("Error reading request question.")]
|
||||
Question,
|
||||
#[error("Error reading request answers (?!)")]
|
||||
Answer,
|
||||
#[error("Error reading requst name servers.")]
|
||||
NameServers,
|
||||
#[error("Reading request additional records.")]
|
||||
AdditionalRecords,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RequestWriteError {
|
||||
#[error("Error writing request header.")]
|
||||
Header,
|
||||
#[error("Error writing request question.")]
|
||||
Question,
|
||||
#[error("Request had too many questions.")]
|
||||
TooManyQuestions,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn read<B: Buf>(buffer: &mut B) -> error_stack::Result<Self, RequestReadError> {
|
||||
let header = Header::read(buffer)
|
||||
.change_context(RequestReadError::Header)?;
|
||||
|
||||
if header.is_response {
|
||||
return Err(report!(RequestReadError::NotRequest))
|
||||
.attach_printable_lazy(|| format!("message id is {}", header.message_id));
|
||||
}
|
||||
|
||||
if header.authoritative_answer {
|
||||
return Err(report!(RequestReadError::IllegalMessage))
|
||||
.attach_printable("Request messages are not allowed to set the 'authoritative answer' bit.")
|
||||
.attach_printable_lazy(|| format!("message id is {}", header.message_id));
|
||||
}
|
||||
|
||||
if header.response_code != ResponseCode::NoErrorConditions {
|
||||
return Err(report!(RequestReadError::IllegalMessage))
|
||||
.attach_printable("Request messages are not allowed to set the response code.")
|
||||
.attach_printable_lazy(|| format!("message id is {}", header.message_id));
|
||||
}
|
||||
|
||||
if header.answer_count != 0 {
|
||||
return Err(report!(RequestReadError::IllegalMessage))
|
||||
.attach_printable("Request messages are not allowed to include answers.")
|
||||
.attach_printable_lazy(|| format!("{} answers declared", header.answer_count))
|
||||
.attach_printable_lazy(|| format!("message id is {}", header.message_id));
|
||||
}
|
||||
|
||||
if header.name_server_count != 0 {
|
||||
return Err(report!(RequestReadError::IllegalMessage))
|
||||
.attach_printable("Request messages are not allowed to include name servers.")
|
||||
.attach_printable_lazy(|| format!("{} name servers declared", header.name_server_count))
|
||||
.attach_printable_lazy(|| format!("message id is {}", header.message_id));
|
||||
}
|
||||
|
||||
if header.additional_record_count != 0 {
|
||||
return Err(report!(RequestReadError::IllegalMessage))
|
||||
.attach_printable("Request messages are not allowed to include additional records.")
|
||||
.attach_printable_lazy(|| format!("{} aditional records declared", header.additional_record_count))
|
||||
.attach_printable_lazy(|| format!("message id is {}", header.message_id));
|
||||
}
|
||||
|
||||
|
||||
let mut questions = vec![];
|
||||
|
||||
for i in 0..header.question_count {
|
||||
let question = Question::read(buffer)
|
||||
.change_context(RequestReadError::Question)
|
||||
.attach_printable_lazy(|| format!("for question #{} of {}", i+1, header.question_count))
|
||||
.attach_printable_lazy(|| format!("message id is {}", header.message_id))?;
|
||||
|
||||
questions.push(question);
|
||||
}
|
||||
|
||||
|
||||
Ok(Request {
|
||||
source_message_id: header.message_id,
|
||||
opcode: header.opcode,
|
||||
recursion_desired: header.recursion_desired,
|
||||
questions,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<B: BufMut>(self, buffer: &mut B) -> error_stack::Result<(), RequestWriteError> {
|
||||
let question_count = self.questions.len();
|
||||
|
||||
if question_count > (u16::MAX as usize) {
|
||||
return Err(report!(RequestWriteError::TooManyQuestions))
|
||||
.attach_printable(format!("message_id is {}", self.source_message_id));
|
||||
}
|
||||
|
||||
let header = Header {
|
||||
message_id: self.source_message_id,
|
||||
is_response: false,
|
||||
opcode: self.opcode,
|
||||
authoritative_answer: false,
|
||||
message_truncated: false,
|
||||
recursion_desired: self.recursion_desired,
|
||||
recursion_available: false,
|
||||
response_code: ResponseCode::NoErrorConditions,
|
||||
question_count: question_count as u16,
|
||||
answer_count: 0,
|
||||
name_server_count: 0,
|
||||
additional_record_count: 0,
|
||||
};
|
||||
|
||||
header.write(buffer)
|
||||
.change_context(RequestWriteError::Header)
|
||||
.attach_printable_lazy(|| format!("message ID is {}", self.source_message_id))?;
|
||||
|
||||
for (index, question) in self.questions.into_iter().enumerate() {
|
||||
question.write(buffer)
|
||||
.change_context(RequestWriteError::Question)
|
||||
.attach_printable_lazy(|| format!("message ID is {}", self.source_message_id))
|
||||
.attach_printable_lazy(|| format!("question #{} of {}", index+1, question_count))
|
||||
.attach_printable_lazy(|| format!("{}", question))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn request_roundtrip(request: Request) {
|
||||
let mut write_buffer = bytes::BytesMut::with_capacity(128 * 1024);
|
||||
let safe_request = request.clone();
|
||||
request.write(&mut write_buffer).expect("can write name");
|
||||
let mut read_buffer = write_buffer.freeze();
|
||||
let new_request = Request::read(&mut read_buffer).expect("can read name");
|
||||
assert_eq!(safe_request, new_request);
|
||||
}
|
||||
}
|
||||
1273
resolver/src/protocol/resource_record.rs
Normal file
1273
resolver/src/protocol/resource_record.rs
Normal file
File diff suppressed because it is too large
Load Diff
304
resolver/src/protocol/resource_record/raw.rs
Normal file
304
resolver/src/protocol/resource_record/raw.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
use crate::name::Name;
|
||||
use bytes::{Buf, BufMut, Bytes};
|
||||
use error_stack::{report, ResultExt};
|
||||
use num_enum::{FromPrimitive, IntoPrimitive};
|
||||
use proptest::arbitrary::Arbitrary;
|
||||
use proptest::strategy::{BoxedStrategy, Just, Strategy};
|
||||
use std::fmt;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct RawResourceRecord {
|
||||
pub name: Name,
|
||||
pub record_type: RecordType,
|
||||
pub record_class: RecordClass,
|
||||
pub ttl: u32,
|
||||
pub data: Bytes,
|
||||
}
|
||||
|
||||
impl Arbitrary for RawResourceRecord {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
|
||||
(
|
||||
Name::arbitrary(),
|
||||
RecordType::arbitrary(),
|
||||
RecordClass::arbitrary(),
|
||||
u32::arbitrary(),
|
||||
proptest::collection::vec(u8::arbitrary(), 0..65535),
|
||||
)
|
||||
.prop_map(
|
||||
|(name, record_type, record_class, ttl, data)| RawResourceRecord {
|
||||
name,
|
||||
record_type,
|
||||
record_class,
|
||||
ttl,
|
||||
data: data.into(),
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromPrimitive, IntoPrimitive, PartialEq, PartialOrd, Eq, Ord, Debug, Copy, Clone)]
|
||||
#[repr(u16)]
|
||||
pub enum RecordType {
|
||||
A = 1,
|
||||
AAAA = 28,
|
||||
NS = 2,
|
||||
MD = 3,
|
||||
MF = 4,
|
||||
CNAME = 5,
|
||||
SOA = 6,
|
||||
MB = 7,
|
||||
MG = 8,
|
||||
MR = 9,
|
||||
NULL = 10,
|
||||
WKS = 11,
|
||||
PTR = 12,
|
||||
HINFO = 13,
|
||||
MINFO = 14,
|
||||
MX = 15,
|
||||
TXT = 16,
|
||||
URI = 256,
|
||||
#[num_enum(catch_all)]
|
||||
Other(u16),
|
||||
}
|
||||
|
||||
impl fmt::Display for RecordType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RecordType::A => write!(f, "A"),
|
||||
RecordType::AAAA => write!(f, "AAAA"),
|
||||
RecordType::NS => write!(f, "NS"),
|
||||
RecordType::MD => write!(f, "MD"),
|
||||
RecordType::MF => write!(f, "MF"),
|
||||
RecordType::CNAME => write!(f, "CNAME"),
|
||||
RecordType::SOA => write!(f, "SOA"),
|
||||
RecordType::MB => write!(f, "MB"),
|
||||
RecordType::MG => write!(f, "MG"),
|
||||
RecordType::MR => write!(f, "MR"),
|
||||
RecordType::NULL => write!(f, "NULL"),
|
||||
RecordType::WKS => write!(f, "WKS"),
|
||||
RecordType::PTR => write!(f, "PTR"),
|
||||
RecordType::HINFO => write!(f, "HINFO"),
|
||||
RecordType::MINFO => write!(f, "MINFO"),
|
||||
RecordType::MX => write!(f, "MX"),
|
||||
RecordType::TXT => write!(f, "TXT"),
|
||||
RecordType::URI => write!(f, "URI"),
|
||||
RecordType::Other(x) => write!(f, "UNKNOWN<{x}>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for RecordType {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
|
||||
// this is intentionally biased towards the legit values
|
||||
proptest::prop_oneof![
|
||||
Just(RecordType::A),
|
||||
Just(RecordType::AAAA),
|
||||
Just(RecordType::NS),
|
||||
Just(RecordType::MD),
|
||||
Just(RecordType::MF),
|
||||
Just(RecordType::CNAME),
|
||||
Just(RecordType::SOA),
|
||||
Just(RecordType::MB),
|
||||
Just(RecordType::MG),
|
||||
Just(RecordType::MR),
|
||||
Just(RecordType::NULL),
|
||||
Just(RecordType::WKS),
|
||||
Just(RecordType::PTR),
|
||||
Just(RecordType::HINFO),
|
||||
Just(RecordType::MINFO),
|
||||
Just(RecordType::MX),
|
||||
Just(RecordType::TXT),
|
||||
proptest::prop_oneof![
|
||||
(17u16..28).prop_map(|x| RecordType::Other(x)),
|
||||
(29u16..256).prop_map(|x| RecordType::Other(x)),
|
||||
(257u16..=65535).prop_map(|x| RecordType::Other(x)),
|
||||
Just(RecordType::Other(0)),
|
||||
],
|
||||
]
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromPrimitive, IntoPrimitive, PartialEq, PartialOrd, Eq, Ord, Debug, Copy, Clone)]
|
||||
#[repr(u16)]
|
||||
pub enum RecordClass {
|
||||
IN = 1,
|
||||
CS = 2,
|
||||
CH = 3,
|
||||
HS = 4,
|
||||
#[num_enum(catch_all)]
|
||||
Other(u16),
|
||||
}
|
||||
|
||||
impl fmt::Display for RecordClass {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RecordClass::IN => write!(f, "IN"),
|
||||
RecordClass::CS => write!(f, "CS"),
|
||||
RecordClass::CH => write!(f, "CH"),
|
||||
RecordClass::HS => write!(f, "HS"),
|
||||
RecordClass::Other(x) => write!(f, "UNKNOWN<{x}>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for RecordClass {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
|
||||
// this is intentionally biased towards the legit values
|
||||
proptest::prop_oneof![
|
||||
Just(RecordClass::IN),
|
||||
Just(RecordClass::CS),
|
||||
Just(RecordClass::CH),
|
||||
Just(RecordClass::HS),
|
||||
(5u16..=65535).prop_map(|x| RecordClass::Other(x)),
|
||||
Just(RecordClass::Other(0)),
|
||||
]
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ResourceRecordReadError {
|
||||
#[error("Failed to read initial record name.")]
|
||||
InitialRecord,
|
||||
|
||||
#[error("Resource record truncated; couldn't find its type field.")]
|
||||
NoTypeField,
|
||||
|
||||
#[error("Resource record truncated; couldn't find its class field.")]
|
||||
NoClassField,
|
||||
|
||||
#[error("Resource record truncated; couldn't find its TTL field.")]
|
||||
NoTtl,
|
||||
|
||||
#[error("Resource record truncated; couldn't find its data length.")]
|
||||
NoDataLength,
|
||||
|
||||
#[error("Resource record truncated; couldn't read its entire data field.")]
|
||||
DataTruncated,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ResourceRecordWriteError {
|
||||
#[error("Could not write name to the output record.")]
|
||||
CouldNotWriteName,
|
||||
|
||||
#[error("Could not write resource record type and class to output record.")]
|
||||
CouldNotWriteTypeClass,
|
||||
|
||||
#[error("Could not write TTL to output record.")]
|
||||
CountNotWriteTtl,
|
||||
|
||||
#[error("Could not write resource record data length to output record.")]
|
||||
CountNotWriteDataLength,
|
||||
|
||||
#[error("Could not write resource record data to output record.")]
|
||||
CountNotWriteData,
|
||||
|
||||
#[error("Input data was too large to write to output stream.")]
|
||||
InputDataTooLarge,
|
||||
}
|
||||
|
||||
impl RawResourceRecord {
|
||||
pub fn read<B: Buf>(buffer: &mut B) -> error_stack::Result<Self, ResourceRecordReadError> {
|
||||
let name = Name::read(buffer).change_context(ResourceRecordReadError::InitialRecord)?;
|
||||
|
||||
let record_type = buffer
|
||||
.try_get_u16()
|
||||
.map_err(|_| report!(ResourceRecordReadError::NoTypeField))?
|
||||
.into();
|
||||
let record_class = buffer
|
||||
.try_get_u16()
|
||||
.map_err(|_| report!(ResourceRecordReadError::NoClassField))?
|
||||
.into();
|
||||
let ttl = buffer
|
||||
.try_get_u32()
|
||||
.map_err(|_| report!(ResourceRecordReadError::NoTtl))?;
|
||||
let rdata_length = buffer
|
||||
.try_get_u16()
|
||||
.map_err(|_| report!(ResourceRecordReadError::NoDataLength))?;
|
||||
|
||||
if buffer.remaining() < (rdata_length as usize) {
|
||||
return Err(report!(ResourceRecordReadError::DataTruncated)).attach_printable_lazy(
|
||||
|| {
|
||||
format!(
|
||||
"Expected {rdata_length} bytes, but only saw {}",
|
||||
buffer.remaining()
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
let data = buffer.copy_to_bytes(rdata_length as usize);
|
||||
|
||||
Ok(RawResourceRecord {
|
||||
name,
|
||||
record_type,
|
||||
record_class,
|
||||
ttl,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<B: BufMut>(
|
||||
&self,
|
||||
buffer: &mut B,
|
||||
) -> error_stack::Result<(), ResourceRecordWriteError> {
|
||||
self.name
|
||||
.write(buffer)
|
||||
.change_context(ResourceRecordWriteError::CouldNotWriteName)?;
|
||||
|
||||
if buffer.remaining_mut() < 4 {
|
||||
return Err(report!(ResourceRecordWriteError::CouldNotWriteTypeClass));
|
||||
}
|
||||
buffer.put_u16(self.record_type.into());
|
||||
buffer.put_u16(self.record_class.into());
|
||||
|
||||
if buffer.remaining_mut() < 4 {
|
||||
return Err(report!(ResourceRecordWriteError::CountNotWriteTtl));
|
||||
}
|
||||
buffer.put_u32(self.ttl);
|
||||
|
||||
if buffer.remaining_mut() < 2 {
|
||||
return Err(report!(ResourceRecordWriteError::CountNotWriteDataLength));
|
||||
}
|
||||
if self.data.len() > (u16::MAX as usize) {
|
||||
return Err(report!(ResourceRecordWriteError::InputDataTooLarge))
|
||||
.attach_printable_lazy(|| {
|
||||
format!(
|
||||
"Incoming data was {} bytes, needs to be < 2^16",
|
||||
self.data.len()
|
||||
)
|
||||
});
|
||||
}
|
||||
buffer.put_u16(self.data.len() as u16);
|
||||
|
||||
if buffer.remaining_mut() < self.data.len() {
|
||||
return Err(report!(ResourceRecordWriteError::CountNotWriteData));
|
||||
}
|
||||
buffer.put_slice(&self.data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn any_random_record_roundtrips(record: RawResourceRecord) {
|
||||
let mut write_buffer = bytes::BytesMut::with_capacity(128 * 1024);
|
||||
record.write(&mut write_buffer).expect("can write name");
|
||||
let mut read_buffer = write_buffer.freeze();
|
||||
let new_record = RawResourceRecord::read(&mut read_buffer).expect("can read name");
|
||||
assert_eq!(record, new_record);
|
||||
}
|
||||
}
|
||||
258
resolver/src/protocol/response.rs
Normal file
258
resolver/src/protocol/response.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use bytes::{Buf, BufMut};
|
||||
use error_stack::ResultExt;
|
||||
use crate::protocol::header::{Header, ResponseCode};
|
||||
use crate::protocol::question::Question;
|
||||
use crate::protocol::resource_record::ResourceRecord;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, proptest_derive::Arbitrary)]
|
||||
pub enum Response {
|
||||
Valid {
|
||||
source_message_id: u16,
|
||||
authoritative: bool,
|
||||
truncated: bool,
|
||||
answers: Vec<ResourceRecord>,
|
||||
name_servers: Vec<ResourceRecord>,
|
||||
additional_records: Vec<ResourceRecord>,
|
||||
},
|
||||
FormatError {
|
||||
source_message_id: u16,
|
||||
},
|
||||
ServerFailure {
|
||||
source_message_id: u16,
|
||||
},
|
||||
NameError {
|
||||
source_message_id: u16,
|
||||
},
|
||||
NotImplemented {
|
||||
source_message_id: u16,
|
||||
},
|
||||
Refused {
|
||||
source_message_id: u16,
|
||||
},
|
||||
UnknownError {
|
||||
#[proptest(strategy="6u8..=15")]
|
||||
error_code: u8,
|
||||
source_message_id: u16,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ResponseReadError {
|
||||
#[error("Could not read response header.")]
|
||||
HeaderReadError,
|
||||
#[error("Could not read question included in response.")]
|
||||
QuestionReadError,
|
||||
#[error("Could not read resource record included as an answer.")]
|
||||
AnswerReadError,
|
||||
#[error("Could not read name server record included in response.")]
|
||||
NameServerReadError,
|
||||
#[error("Could not read supplemental record included in response.")]
|
||||
AdditionalInfoReadError,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ResponseWriteError {
|
||||
#[error("Could not write response header.")]
|
||||
Header,
|
||||
#[error("Could not write answer.")]
|
||||
Answer,
|
||||
#[error("Could not write name server.")]
|
||||
NameServer,
|
||||
#[error("Could not write additional record.")]
|
||||
AdditionalRecord,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn message_id(&self) -> u16 {
|
||||
match self {
|
||||
Response::Valid { source_message_id, .. } => *source_message_id,
|
||||
Response::FormatError { source_message_id } => *source_message_id,
|
||||
Response::ServerFailure { source_message_id } => *source_message_id,
|
||||
Response::NameError { source_message_id } => *source_message_id,
|
||||
Response::NotImplemented { source_message_id } => *source_message_id,
|
||||
Response::Refused { source_message_id } => *source_message_id,
|
||||
Response::UnknownError { source_message_id, .. } => *source_message_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read<B: Buf>(buffer: &mut B) -> error_stack::Result<Response, ResponseReadError> {
|
||||
let header = Header::read(buffer)
|
||||
.change_context(ResponseReadError::HeaderReadError)?;
|
||||
|
||||
// check for errors, and short-cut out if we find any
|
||||
match header.response_code {
|
||||
ResponseCode::NoErrorConditions => {}
|
||||
|
||||
ResponseCode::FormatError => return Ok(Response::FormatError {
|
||||
source_message_id: header.message_id,
|
||||
}),
|
||||
|
||||
ResponseCode::ServerFailure => return Ok(Response::ServerFailure {
|
||||
source_message_id: header.message_id,
|
||||
}),
|
||||
|
||||
ResponseCode::NameError => return Ok(Response::NameError {
|
||||
source_message_id: header.message_id,
|
||||
}),
|
||||
|
||||
ResponseCode::NotImplemented => return Ok(Response::NotImplemented {
|
||||
source_message_id: header.message_id,
|
||||
}),
|
||||
|
||||
ResponseCode::Refused => return Ok(Response::Refused {
|
||||
source_message_id: header.message_id,
|
||||
}),
|
||||
|
||||
ResponseCode::Other(error_code) => return Ok(Response::UnknownError {
|
||||
error_code,
|
||||
source_message_id: header.message_id,
|
||||
}),
|
||||
}
|
||||
|
||||
// it seems weird to get questions in a response, but we need to parse
|
||||
// them out if they exist.
|
||||
for _ in 0..header.question_count {
|
||||
let question = Question::read(buffer)
|
||||
.change_context(ResponseReadError::QuestionReadError)?;
|
||||
|
||||
tracing::warn!(
|
||||
%question,
|
||||
"got question during server response."
|
||||
);
|
||||
}
|
||||
|
||||
let mut answers = vec![];
|
||||
for idx in 0..header.answer_count {
|
||||
let answer = ResourceRecord::read(buffer)
|
||||
.change_context(ResponseReadError::AnswerReadError)
|
||||
.attach_printable_lazy(|| format!("In answer {} of {}", idx + 1, header.answer_count))?;
|
||||
|
||||
answers.push(answer);
|
||||
}
|
||||
|
||||
let mut name_servers = vec![];
|
||||
for idx in 0..header.name_server_count {
|
||||
let name_server = ResourceRecord::read(buffer)
|
||||
.change_context(ResponseReadError::NameServerReadError)
|
||||
.attach_printable_lazy(|| format!("In answer {} of {}", idx + 1, header.name_server_count))?;
|
||||
|
||||
name_servers.push(name_server);
|
||||
}
|
||||
|
||||
let mut additional_records = vec![];
|
||||
for idx in 0..header.additional_record_count {
|
||||
let extra = ResourceRecord::read(buffer)
|
||||
.change_context(ResponseReadError::AnswerReadError)
|
||||
.attach_printable_lazy(|| format!("In answer {} of {}", idx + 1, header.additional_record_count))?;
|
||||
|
||||
additional_records.push(extra);
|
||||
}
|
||||
|
||||
Ok(Response::Valid {
|
||||
source_message_id: header.message_id,
|
||||
authoritative: header.authoritative_answer,
|
||||
truncated: header.message_truncated,
|
||||
answers,
|
||||
name_servers,
|
||||
additional_records,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_error<B: BufMut>(source_message_id: u16, error_code: ResponseCode, buffer: &mut B) -> error_stack::Result<(), ResponseWriteError> {
|
||||
let header = Header {
|
||||
message_id: source_message_id,
|
||||
is_response: true,
|
||||
opcode: super::header::OpCode::StandardQuery,
|
||||
authoritative_answer: false,
|
||||
message_truncated: false,
|
||||
recursion_desired: false,
|
||||
recursion_available: false,
|
||||
response_code: error_code,
|
||||
question_count: 0,
|
||||
answer_count: 0,
|
||||
name_server_count: 0,
|
||||
additional_record_count: 0,
|
||||
};
|
||||
|
||||
header
|
||||
.write(buffer)
|
||||
.change_context(ResponseWriteError::Header)
|
||||
.attach_printable_lazy(|| format!("responding to message {source_message_id} with error code {error_code}"))
|
||||
}
|
||||
|
||||
pub fn write<B: BufMut>(self, buffer: &mut B) -> error_stack::Result<(), ResponseWriteError> {
|
||||
let (source_message_id, authoritative, truncated, answers, name_servers, additional_records) = match self {
|
||||
Response::FormatError { source_message_id } => return Self::write_error(source_message_id, ResponseCode::FormatError, buffer),
|
||||
|
||||
Response::ServerFailure { source_message_id } => return Self::write_error(source_message_id, ResponseCode::ServerFailure, buffer),
|
||||
|
||||
Response::NameError { source_message_id } => return Self::write_error(source_message_id, ResponseCode::NameError, buffer),
|
||||
|
||||
Response::NotImplemented { source_message_id } => return Self::write_error(source_message_id, ResponseCode::NotImplemented, buffer),
|
||||
|
||||
Response::Refused { source_message_id } => return Self::write_error(source_message_id, ResponseCode::Refused, buffer),
|
||||
|
||||
Response::UnknownError { error_code, source_message_id } => return Self::write_error(source_message_id, ResponseCode::Other(error_code), buffer),
|
||||
|
||||
Response::Valid { source_message_id, authoritative, truncated, answers, name_servers, additional_records } => {
|
||||
(source_message_id, authoritative, truncated, answers, name_servers, additional_records)
|
||||
}
|
||||
};
|
||||
|
||||
let header = Header {
|
||||
message_id: source_message_id,
|
||||
is_response: true,
|
||||
opcode: super::header::OpCode::StandardQuery,
|
||||
authoritative_answer: authoritative,
|
||||
message_truncated: truncated,
|
||||
recursion_desired: false,
|
||||
recursion_available: false,
|
||||
response_code: ResponseCode::NoErrorConditions,
|
||||
question_count: 0,
|
||||
answer_count: answers.len() as u16,
|
||||
name_server_count: name_servers.len() as u16,
|
||||
additional_record_count: additional_records.len() as u16,
|
||||
};
|
||||
|
||||
header.write(buffer)
|
||||
.change_context(ResponseWriteError::Header)
|
||||
.attach_printable_lazy(|| format!("Writing clean response to {source_message_id}"))?;
|
||||
|
||||
let answer_count = answers.len();
|
||||
for (item, answer) in answers.into_iter().enumerate() {
|
||||
answer.write(buffer)
|
||||
.change_context(ResponseWriteError::Answer)
|
||||
.attach_printable_lazy(|| format!("Writing clean response to {source_message_id}"))
|
||||
.attach_printable_lazy(|| format!("Writing answer {} of {}", item+1, answer_count))?;
|
||||
}
|
||||
|
||||
let ns_count = name_servers.len();
|
||||
for (item, answer) in name_servers.into_iter().enumerate() {
|
||||
answer.write(buffer)
|
||||
.change_context(ResponseWriteError::NameServer)
|
||||
.attach_printable_lazy(|| format!("Writing clean response to {source_message_id}"))
|
||||
.attach_printable_lazy(|| format!("Writing name server {} of {}", item+1, ns_count))?;
|
||||
}
|
||||
|
||||
let ar_count = additional_records.len();
|
||||
for (item, answer) in additional_records.into_iter().enumerate() {
|
||||
answer.write(buffer)
|
||||
.change_context(ResponseWriteError::AdditionalRecord)
|
||||
.attach_printable_lazy(|| format!("Writing clean response to {source_message_id}"))
|
||||
.attach_printable_lazy(|| format!("Writing additional record {} of {}", item+1, ar_count))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn any_random_response_roundtrips(record: Response) {
|
||||
let mut write_buffer = bytes::BytesMut::with_capacity(128 * 1024);
|
||||
record.clone().write(&mut write_buffer).expect("can write name");
|
||||
let mut read_buffer = write_buffer.freeze();
|
||||
let new_record = Response::read(&mut read_buffer).expect("can read name");
|
||||
assert_eq!(record, new_record);
|
||||
}
|
||||
}
|
||||
1
resolver/src/protocol/server.rs
Normal file
1
resolver/src/protocol/server.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
176
resolver/src/resolution_table.rs
Normal file
176
resolver/src/resolution_table.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use crate::name::Name;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::net::IpAddr;
|
||||
use tokio::time::{Duration, Instant};
|
||||
|
||||
pub struct ResolutionTable {
|
||||
inner: HashMap<Name, Vec<Resolution>>,
|
||||
}
|
||||
|
||||
struct Resolution {
|
||||
result: IpAddr,
|
||||
expiration: Instant,
|
||||
}
|
||||
|
||||
impl Default for ResolutionTable {
|
||||
fn default() -> Self {
|
||||
ResolutionTable::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolutionTable {
|
||||
/// Generate a new, empty resolution table to use in a new DNS implementation,
|
||||
/// or shared by a bunch of them.
|
||||
pub fn new() -> Self {
|
||||
ResolutionTable {
|
||||
inner: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean the table of expired entries.
|
||||
pub fn garbage_collect(&mut self) {
|
||||
let now = Instant::now();
|
||||
|
||||
self.inner.retain(|_, items| {
|
||||
items.retain(|x| x.expiration > now);
|
||||
!items.is_empty()
|
||||
});
|
||||
}
|
||||
|
||||
/// Add a new entry to the resolution table, with a TTL on it.
|
||||
pub fn add_entry(&mut self, name: Name, maps_to: IpAddr, ttl: Duration) {
|
||||
let now = Instant::now();
|
||||
|
||||
let new_entry = Resolution {
|
||||
result: maps_to,
|
||||
expiration: now + ttl,
|
||||
};
|
||||
|
||||
match self.inner.entry(name) {
|
||||
Entry::Vacant(vac) => {
|
||||
vac.insert(vec![new_entry]);
|
||||
}
|
||||
|
||||
Entry::Occupied(mut occ) => {
|
||||
occ.get_mut().push(new_entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up an entry in the resolution map. This will only return
|
||||
/// unexpired items.
|
||||
pub fn lookup(&mut self, name: &Name) -> HashSet<IpAddr> {
|
||||
let mut result = HashSet::new();
|
||||
let now = Instant::now();
|
||||
|
||||
if let Some(entry) = self.inner.get_mut(name) {
|
||||
entry.retain(|x| {
|
||||
let retain = x.expiration > now;
|
||||
|
||||
if retain {
|
||||
result.insert(x.result);
|
||||
}
|
||||
|
||||
retain
|
||||
});
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
#[cfg(test)]
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn empty_set_gets_fail() {
|
||||
let mut empty = ResolutionTable::default();
|
||||
assert!(empty.lookup(&Name::from_str("foo").unwrap()).is_empty());
|
||||
assert!(empty.lookup(&Name::from_str("bar").unwrap()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_lookups() {
|
||||
let mut table = ResolutionTable::new();
|
||||
let foo = Name::from_str("foo").unwrap();
|
||||
let bar = Name::from_str("bar").unwrap();
|
||||
let baz = Name::from_str("baz").unwrap();
|
||||
let localhost = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
let other = IpAddr::V6(Ipv6Addr::from_str("2001:18e8:2:e::11e").unwrap());
|
||||
let long_time = Duration::from_secs(10000000000);
|
||||
|
||||
table.add_entry(foo.clone(), localhost, long_time);
|
||||
table.add_entry(bar.clone(), localhost, long_time);
|
||||
table.add_entry(bar.clone(), other, long_time);
|
||||
|
||||
assert_eq!(1, table.lookup(&foo).len());
|
||||
assert_eq!(2, table.lookup(&bar).len());
|
||||
assert!(table.lookup(&baz).is_empty());
|
||||
assert!(table.lookup(&foo).contains(&localhost));
|
||||
assert!(!table.lookup(&foo).contains(&other));
|
||||
assert!(table.lookup(&bar).contains(&localhost));
|
||||
assert!(table.lookup(&bar).contains(&other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_cleans_up() {
|
||||
let mut table = ResolutionTable::new();
|
||||
let foo = Name::from_str("foo").unwrap();
|
||||
let localhost = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
let other = IpAddr::V6(Ipv6Addr::from_str("2001:18e8:2:e::11e").unwrap());
|
||||
let short_time = Duration::from_millis(100);
|
||||
let long_time = Duration::from_secs(10000000000);
|
||||
|
||||
table.add_entry(foo.clone(), localhost, long_time);
|
||||
table.add_entry(foo.clone(), other, short_time);
|
||||
let wait_until = Instant::now() + (2 * short_time);
|
||||
while Instant::now() < wait_until {
|
||||
std::thread::sleep(short_time);
|
||||
}
|
||||
|
||||
assert_eq!(1, table.lookup(&foo).len());
|
||||
assert!(table.lookup(&foo).contains(&localhost));
|
||||
assert!(!table.lookup(&foo).contains(&other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_collection_works() {
|
||||
let mut table = ResolutionTable::new();
|
||||
let foo = Name::from_str("foo").unwrap();
|
||||
let localhost = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
let other = IpAddr::V6(Ipv6Addr::from_str("2001:18e8:2:e::11e").unwrap());
|
||||
let short_time = Duration::from_millis(100);
|
||||
let long_time = Duration::from_secs(10000000000);
|
||||
|
||||
table.add_entry(foo.clone(), localhost, long_time);
|
||||
table.add_entry(foo.clone(), other, short_time);
|
||||
let wait_until = Instant::now() + (2 * short_time);
|
||||
while Instant::now() < wait_until {
|
||||
std::thread::sleep(short_time);
|
||||
}
|
||||
table.garbage_collect();
|
||||
|
||||
assert_eq!(1, table.inner.get(&foo).unwrap().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_collection_clears_empties() {
|
||||
let mut table = ResolutionTable::new();
|
||||
let foo = Name::from_str("foo").unwrap();
|
||||
table.inner.insert(foo.clone(), vec![]);
|
||||
table.garbage_collect();
|
||||
assert!(table.inner.is_empty());
|
||||
|
||||
let localhost = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
let short_time = Duration::from_millis(100);
|
||||
table.add_entry(foo.clone(), localhost, short_time);
|
||||
let wait_until = Instant::now() + (2 * short_time);
|
||||
while Instant::now() < wait_until {
|
||||
std::thread::sleep(short_time);
|
||||
}
|
||||
table.garbage_collect();
|
||||
assert!(table.inner.is_empty());
|
||||
}
|
||||
10
server/Cargo.toml
Normal file
10
server/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "server"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
configuration = { workspace = true }
|
||||
error-stack = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
27
server/src/lib.rs
Normal file
27
server/src/lib.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
mod socket;
|
||||
mod state;
|
||||
|
||||
use configuration::server::ServerConfiguration;
|
||||
use error_stack::ResultExt;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TopLevelError {
|
||||
#[error("Configuration error")]
|
||||
ConfigurationError,
|
||||
#[error("Failure running UNIX socket handling task")]
|
||||
SocketHandlerFailure,
|
||||
}
|
||||
|
||||
pub async fn run(mut config: ServerConfiguration) -> error_stack::Result<(), TopLevelError> {
|
||||
let _server_state = state::ServerState::default();
|
||||
|
||||
let listeners = config
|
||||
.generate_listener_sockets()
|
||||
.await
|
||||
.change_context(TopLevelError::ConfigurationError)?;
|
||||
|
||||
for (_name, _listener) in listeners.into_iter() {}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
78
server/src/socket.rs
Normal file
78
server/src/socket.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use crate::TopLevelError;
|
||||
use error_stack::ResultExt;
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tracing::Instrument;
|
||||
|
||||
pub struct SocketServer {
|
||||
name: String,
|
||||
path: String,
|
||||
num_sessions_run: u64,
|
||||
listener: UnixListener,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl SocketServer {
|
||||
/// Create a new server that will handle inputs from the client program.
|
||||
///
|
||||
/// This function will just generate the function required, without starting the
|
||||
/// underlying task. To start the task, use [`SocketServer::start`], although that
|
||||
/// method will take ownership of the object.
|
||||
pub fn new(name: String, listener: UnixListener) -> Self {
|
||||
let path = listener
|
||||
.local_addr()
|
||||
.map(|x| x.as_pathname().map(|p| format!("{}", p.display())))
|
||||
.unwrap_or_else(|_| None)
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
|
||||
tracing::trace!(%name, %path, "Creating new socket listener");
|
||||
|
||||
SocketServer {
|
||||
name,
|
||||
path,
|
||||
num_sessions_run: 0,
|
||||
listener,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start running the service, returning a handle that will pass on an error if
|
||||
/// one occurs in the core of this task.
|
||||
///
|
||||
/// Typically, errors shouldn't happen in the core task, as all it does is listen
|
||||
/// for new connections and then spawn other tasks based on them. If errors occur
|
||||
/// there, the core task should be unaffected.
|
||||
pub async fn start(mut self) -> error_stack::Result<(), TopLevelError> {
|
||||
loop {
|
||||
let (stream, addr) = self
|
||||
.listener
|
||||
.accept()
|
||||
.await
|
||||
.change_context(TopLevelError::SocketHandlerFailure)?;
|
||||
let remote_addr = addr
|
||||
.as_pathname()
|
||||
.map(|x| x.display())
|
||||
.map(|x| format!("{}", x))
|
||||
.unwrap_or("<unknown>".to_string());
|
||||
|
||||
let span = tracing::debug_span!(
|
||||
"unix socket handler",
|
||||
socket_name = %self.name,
|
||||
socket_path = %self.path,
|
||||
session_no = %self.num_sessions_run,
|
||||
%remote_addr,
|
||||
);
|
||||
|
||||
self.num_sessions_run += 1;
|
||||
|
||||
tokio::task::spawn(Self::run_session(stream).instrument(span))
|
||||
.await
|
||||
.change_context(TopLevelError::SocketHandlerFailure)?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a session.
|
||||
///
|
||||
/// This is here because it's convenient, not because it shares state (obviously,
|
||||
/// given the type signature). But it's somewhat logically associated with this type,
|
||||
/// so it seems reasonable to make it an associated function.
|
||||
async fn run_session(handle: UnixStream) {}
|
||||
}
|
||||
31
server/src/state.rs
Normal file
31
server/src/state.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use crate::TopLevelError;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ServerState {
|
||||
/// The set of top level tasks that are currently running.
|
||||
top_level_tasks: JoinSet<Result<(), TopLevelError>>,
|
||||
}
|
||||
|
||||
impl ServerState {
|
||||
/// Block until all the current top level tasks have closed.
|
||||
///
|
||||
/// This will log any errors found in these tasks to the current log,
|
||||
/// if there is one, but will otherwise drop them.
|
||||
#[allow(unused)]
|
||||
pub async fn shutdown(&mut self) {
|
||||
while let Some(next) = self.top_level_tasks.join_next_with_id().await {
|
||||
match next {
|
||||
Err(e) => tracing::error!(id = %e.id(), "Failed to attach to top-level task"),
|
||||
|
||||
Ok((id, Err(e))) => {
|
||||
tracing::error!(%id, "Top-level server error: {}", e);
|
||||
}
|
||||
|
||||
Ok((id, Ok(()))) => {
|
||||
tracing::debug!(%id, "Cleanly closed server task.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1795
specifications/rfc4253.txt
Normal file
1795
specifications/rfc4253.txt
Normal file
File diff suppressed because it is too large
Load Diff
283
specifications/rfc6668.txt
Normal file
283
specifications/rfc6668.txt
Normal file
@@ -0,0 +1,283 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Internet Engineering Task Force (IETF) D. Bider
|
||||
Request for Comments: 6668 Bitvise Limited
|
||||
Updates: 4253 M. Baushke
|
||||
Category: Standards Track Juniper Networks, Inc.
|
||||
ISSN: 2070-1721 July 2012
|
||||
|
||||
|
||||
SHA-2 Data Integrity Verification for
|
||||
the Secure Shell (SSH) Transport Layer Protocol
|
||||
|
||||
Abstract
|
||||
|
||||
This memo defines algorithm names and parameters for use in some of
|
||||
the SHA-2 family of secure hash algorithms for data integrity
|
||||
verification in the Secure Shell (SSH) protocol. It also updates RFC
|
||||
4253 by specifying a new RECOMMENDED data integrity algorithm.
|
||||
|
||||
Status of This Memo
|
||||
|
||||
This is an Internet Standards Track document.
|
||||
|
||||
This document is a product of the Internet Engineering Task Force
|
||||
(IETF). It represents the consensus of the IETF community. It has
|
||||
received public review and has been approved for publication by the
|
||||
Internet Engineering Steering Group (IESG). Further information on
|
||||
Internet Standards is available in Section 2 of RFC 5741.
|
||||
|
||||
Information about the current status of this document, any errata,
|
||||
and how to provide feedback on it may be obtained at
|
||||
http://www.rfc-editor.org/info/rfc6668.
|
||||
|
||||
Copyright Notice
|
||||
|
||||
Copyright (c) 2012 IETF Trust and the persons identified as the
|
||||
document authors. All rights reserved.
|
||||
|
||||
This document is subject to BCP 78 and the IETF Trust's Legal
|
||||
Provisions Relating to IETF Documents
|
||||
(http://trustee.ietf.org/license-info) in effect on the date of
|
||||
publication of this document. Please review these documents
|
||||
carefully, as they describe your rights and restrictions with respect
|
||||
to this document. Code Components extracted from this document must
|
||||
include Simplified BSD License text as described in Section 4.e of
|
||||
the Trust Legal Provisions and are provided without warranty as
|
||||
described in the Simplified BSD License.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider & Baushke Standards Track [Page 1]
|
||||
|
||||
RFC 6668 Sha2-Transport Layer Protocol July 2012
|
||||
|
||||
|
||||
1. Overview and Rationale
|
||||
|
||||
The Secure Shell (SSH) [RFC4251] is a very common protocol for secure
|
||||
remote login on the Internet. Currently, SSH defines data integrity
|
||||
verification using SHA-1 and MD5 algorithms [RFC4253]. Due to recent
|
||||
security concerns with these two algorithms ([RFC6194] and [RFC6151],
|
||||
respectively), implementors and users request support for data
|
||||
integrity verification using some of the SHA-2 family of secure hash
|
||||
algorithms.
|
||||
|
||||
1.1. Requirements Terminology
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
|
||||
document are to be interpreted as described in [RFC2119].
|
||||
|
||||
2. Data Integrity Algorithms
|
||||
|
||||
This memo adopts the style and conventions of [RFC4253] in specifying
|
||||
how the use of new data integrity algorithms are indicated in SSH.
|
||||
|
||||
The following new data integrity algorithms are defined:
|
||||
|
||||
hmac-sha2-256 RECOMMENDED HMAC-SHA2-256
|
||||
(digest length = 32 bytes,
|
||||
key length = 32 bytes)
|
||||
|
||||
hmac-sha2-512 OPTIONAL HMAC-SHA2-512
|
||||
(digest length = 64 bytes,
|
||||
key length = 64 bytes)
|
||||
|
||||
Figure 1
|
||||
|
||||
The Hashed Message Authentication Code (HMAC) mechanism was
|
||||
originally defined in [RFC2104] and has been updated in [RFC6151].
|
||||
|
||||
The SHA-2 family of secure hash algorithms is defined in
|
||||
[FIPS-180-3].
|
||||
|
||||
Sample code for the SHA-based HMAC algorithms are available in
|
||||
[RFC6234]. The variants, HMAC-SHA2-224 and HMAC-SHA2-384 algorithms,
|
||||
were considered but not added to this list as they have the same
|
||||
computational requirements of HMAC-SHA2-256 and HMAC-SHA2-512,
|
||||
respectively, and do not seem to be much used in practice.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider & Baushke Standards Track [Page 2]
|
||||
|
||||
RFC 6668 Sha2-Transport Layer Protocol July 2012
|
||||
|
||||
|
||||
Test vectors for use of HMAC with SHA-2 are provided in [RFC4231].
|
||||
Users, implementors, and administrators may choose to put these new
|
||||
MACs into the proposal ahead of the REQUIRED hmac-sha1 algorithm
|
||||
defined in [RFC4253] so that they are negotiated first.
|
||||
|
||||
3. IANA Considerations
|
||||
|
||||
This document augments the MAC Algorithm Names in [RFC4253] and
|
||||
[RFC4250].
|
||||
|
||||
IANA has updated the "Secure Shell (SSH) Protocol Parameters"
|
||||
registry with the following entries:
|
||||
|
||||
MAC Algorithm Name Reference Note
|
||||
hmac-sha2-256 RFC 6668 Section 2
|
||||
hmac-sha2-512 RFC 6668 Section 2
|
||||
|
||||
Figure 2
|
||||
|
||||
4. Security Considerations
|
||||
|
||||
The security considerations of RFC 4253 [RFC4253] apply to this
|
||||
document.
|
||||
|
||||
The National Institute of Standards and Technology (NIST)
|
||||
publications: NIST Special Publication (SP) 800-107 [800-107] and
|
||||
NIST SP 800-131A [800-131A] suggest that HMAC-SHA1 and HMAC-SHA2-256
|
||||
have a security strength of 128 bits and 256 bits, respectively,
|
||||
which are considered acceptable key lengths.
|
||||
|
||||
Many users seem to be interested in the perceived safety of using the
|
||||
SHA2-based algorithms for hashing.
|
||||
|
||||
5. References
|
||||
|
||||
5.1. Normative References
|
||||
|
||||
[FIPS-180-3]
|
||||
National Institute of Standards and Technology (NIST),
|
||||
United States of America, "Secure Hash Standard (SHS)",
|
||||
FIPS PUB 180-3, October 2008, <http://csrc.nist.gov/
|
||||
publications/fips/fips180-3/fips180-3_final.pdf>.
|
||||
|
||||
[RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed-
|
||||
Hashing for Message Authentication", RFC 2104, February
|
||||
1997.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider & Baushke Standards Track [Page 3]
|
||||
|
||||
RFC 6668 Sha2-Transport Layer Protocol July 2012
|
||||
|
||||
|
||||
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
|
||||
Requirement Levels", BCP 14, RFC 2119, March 1997.
|
||||
|
||||
[RFC4231] Nystrom, M., "Identifiers and Test Vectors for HMAC-
|
||||
SHA-224, HMAC-SHA-256, HMAC-SHA-384, and HMAC-SHA-512",
|
||||
RFC 4231, December 2005.
|
||||
|
||||
[RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Transport Layer Protocol", RFC 4253, January 2006.
|
||||
|
||||
5.2. Informative References
|
||||
|
||||
[800-107] National Institute of Standards and Technology (NIST),
|
||||
"Recommendation for Applications Using Approved Hash
|
||||
Algorithms", NIST Special Publication 800-107, February
|
||||
2009, <http://csrc.nist.gov/publications/
|
||||
nistpubs/800-107/NIST-SP-800-107.pdf>.
|
||||
|
||||
[800-131A] National Institute of Standards and Technology (NIST),
|
||||
"Transitions: Recommendation for the Transitioning of the
|
||||
Use of Cryptographic Algorithms and Key Lengths", DRAFT
|
||||
NIST Special Publication 800-131A, January 2011,
|
||||
<http://csrc.nist.gov/publications/nistpubs/800-131A/
|
||||
sp800-131A.pdf>.
|
||||
|
||||
[RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Protocol Assigned Numbers", RFC 4250, January 2006.
|
||||
|
||||
[RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Protocol Architecture", RFC 4251, January 2006.
|
||||
|
||||
[RFC6151] Turner, S. and L. Chen, "Updated Security Considerations
|
||||
for the MD5 Message-Digest and the HMAC-MD5 Algorithms",
|
||||
RFC 6151, March 2011.
|
||||
|
||||
[RFC6194] Polk, T., Chen, L., Turner, S., and P. Hoffman, "Security
|
||||
Considerations for the SHA-0 and SHA-1 Message-Digest
|
||||
Algorithms", RFC 6194, March 2011.
|
||||
|
||||
[RFC6234] Eastlake 3rd, D. and T. Hansen, "US Secure Hash Algorithms
|
||||
(SHA and SHA-based HMAC and HKDF)", RFC 6234, May 2011.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider & Baushke Standards Track [Page 4]
|
||||
|
||||
RFC 6668 Sha2-Transport Layer Protocol July 2012
|
||||
|
||||
|
||||
Authors' Addresses
|
||||
|
||||
Denis Bider
|
||||
Bitvise Limited
|
||||
Suites 41/42, Victoria House
|
||||
26 Main Street
|
||||
GI
|
||||
|
||||
Phone: +1 869 762 1410
|
||||
EMail: ietf-ssh2@denisbider.com
|
||||
URI: http://www.bitvise.com/
|
||||
|
||||
|
||||
Mark D. Baushke
|
||||
Juniper Networks, Inc.
|
||||
1194 N Mathilda Av
|
||||
Sunnyvale, CA 94089-1206
|
||||
US
|
||||
|
||||
Phone: +1 408 745 2952
|
||||
EMail: mdb@juniper.net
|
||||
URI: http://www.juniper.net/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider & Baushke Standards Track [Page 5]
|
||||
|
||||
787
specifications/rfc8308.txt
Normal file
787
specifications/rfc8308.txt
Normal file
@@ -0,0 +1,787 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Internet Engineering Task Force (IETF) D. Bider
|
||||
Request for Comments: 8308 Bitvise Limited
|
||||
Updates: 4251, 4252, 4253, 4254 March 2018
|
||||
Category: Standards Track
|
||||
ISSN: 2070-1721
|
||||
|
||||
|
||||
Extension Negotiation in the Secure Shell (SSH) Protocol
|
||||
|
||||
Abstract
|
||||
|
||||
This memo updates RFCs 4251, 4252, 4253, and 4254 by defining a
|
||||
mechanism for Secure Shell (SSH) clients and servers to exchange
|
||||
information about supported protocol extensions confidentially after
|
||||
SSH key exchange.
|
||||
|
||||
Status of This Memo
|
||||
|
||||
This is an Internet Standards Track document.
|
||||
|
||||
This document is a product of the Internet Engineering Task Force
|
||||
(IETF). It represents the consensus of the IETF community. It has
|
||||
received public review and has been approved for publication by the
|
||||
Internet Engineering Steering Group (IESG). Further information on
|
||||
Internet Standards is available in Section 2 of RFC 7841.
|
||||
|
||||
Information about the current status of this document, any errata,
|
||||
and how to provide feedback on it may be obtained at
|
||||
https://www.rfc-editor.org/info/rfc8308.
|
||||
|
||||
Copyright Notice
|
||||
|
||||
Copyright (c) 2018 IETF Trust and the persons identified as the
|
||||
document authors. All rights reserved.
|
||||
|
||||
This document is subject to BCP 78 and the IETF Trust's Legal
|
||||
Provisions Relating to IETF Documents
|
||||
(https://trustee.ietf.org/license-info) in effect on the date of
|
||||
publication of this document. Please review these documents
|
||||
carefully, as they describe your rights and restrictions with respect
|
||||
to this document. Code Components extracted from this document must
|
||||
include Simplified BSD License text as described in Section 4.e of
|
||||
the Trust Legal Provisions and are provided without warranty as
|
||||
described in the Simplified BSD License.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 1]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
Table of Contents
|
||||
|
||||
1. Overview and Rationale ..........................................3
|
||||
1.1. Requirements Terminology ...................................3
|
||||
1.2. Wire Encoding Terminology ..................................3
|
||||
2. Extension Negotiation Mechanism .................................3
|
||||
2.1. Signaling of Extension Negotiation in SSH_MSG_KEXINIT ......3
|
||||
2.2. Enabling Criteria ..........................................4
|
||||
2.3. SSH_MSG_EXT_INFO Message ...................................4
|
||||
2.4. Message Order ..............................................5
|
||||
2.5. Interpretation of Extension Names and Values ...............6
|
||||
3. Initially Defined Extensions ....................................6
|
||||
3.1. "server-sig-algs" ..........................................6
|
||||
3.2. "delay-compression" ........................................7
|
||||
3.2.1. Awkwardly Timed Key Re-Exchange .....................9
|
||||
3.2.2. Subsequent Re-Exchange ..............................9
|
||||
3.2.3. Compatibility Note: OpenSSH up to Version 7.5 .......9
|
||||
3.3. "no-flow-control" .........................................10
|
||||
3.3.1. Prior "No Flow Control" Practice ...................10
|
||||
3.4. "elevation" ...............................................11
|
||||
4. IANA Considerations ............................................12
|
||||
4.1. Additions to Existing Registries ..........................12
|
||||
4.2. New Registry: Extension Names .............................12
|
||||
4.2.1. Future Assignments to Extension Names Registry .....12
|
||||
5. Security Considerations ........................................12
|
||||
6. References .....................................................13
|
||||
6.1. Normative References ......................................13
|
||||
6.2. Informative References ....................................13
|
||||
Acknowledgments ...................................................14
|
||||
Author's Address ..................................................14
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 2]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
1. Overview and Rationale
|
||||
|
||||
Secure Shell (SSH) is a common protocol for secure communication on
|
||||
the Internet. The original design of the SSH transport layer
|
||||
[RFC4253] lacks proper extension negotiation. Meanwhile, diverse
|
||||
implementations take steps to ensure that known message types contain
|
||||
no unrecognized information. This makes it difficult for
|
||||
implementations to signal capabilities and negotiate extensions
|
||||
without risking disconnection. This obstacle has been recognized in
|
||||
the process of updating SSH to support RSA signatures using SHA-256
|
||||
and SHA-512 [RFC8332]. To avoid trial and error as well as
|
||||
authentication penalties, a client must be able to discover public
|
||||
key algorithms a server accepts. This extension mechanism permits
|
||||
this discovery.
|
||||
|
||||
This memo updates RFCs 4251, 4252, 4253, and 4254.
|
||||
|
||||
1.1. Requirements Terminology
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
|
||||
"OPTIONAL" in this document are to be interpreted as described in
|
||||
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
|
||||
capitals, as shown here.
|
||||
|
||||
1.2. Wire Encoding Terminology
|
||||
|
||||
The wire encoding types in this document -- "byte", "uint32",
|
||||
"string", "boolean", "name-list" -- have meanings as described in
|
||||
[RFC4251].
|
||||
|
||||
2. Extension Negotiation Mechanism
|
||||
|
||||
2.1. Signaling of Extension Negotiation in SSH_MSG_KEXINIT
|
||||
|
||||
Applications implementing this mechanism MUST add one of the
|
||||
following indicator names to the field kex_algorithms in the
|
||||
SSH_MSG_KEXINIT message sent by the application in the first key
|
||||
exchange:
|
||||
|
||||
o When acting as server: "ext-info-s"
|
||||
|
||||
o When acting as client: "ext-info-c"
|
||||
|
||||
The indicator name is added without quotes and MAY be added at any
|
||||
position in the name-list, subject to proper separation from other
|
||||
names as per name-list conventions.
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 3]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
The names are added to the kex_algorithms field because this is one
|
||||
of two name-list fields in SSH_MSG_KEXINIT that do not have a
|
||||
separate copy for each data direction.
|
||||
|
||||
The indicator names inserted by the client and server are different
|
||||
to ensure these names will not produce a match and therefore not
|
||||
affect the algorithm chosen in key exchange algorithm negotiation.
|
||||
|
||||
The inclusion of textual indicator names is intended to provide a
|
||||
clue for implementers to discover this mechanism.
|
||||
|
||||
2.2. Enabling Criteria
|
||||
|
||||
If a client or server offers "ext-info-c" or "ext-info-s"
|
||||
respectively, it MUST be prepared to accept an SSH_MSG_EXT_INFO
|
||||
message from the peer.
|
||||
|
||||
A server only needs to send "ext-info-s" if it intends to process
|
||||
SSH_MSG_EXT_INFO from the client. A client only needs to send
|
||||
"ext-info-c" if it plans to process SSH_MSG_EXT_INFO from the server.
|
||||
|
||||
If a server receives an "ext-info-c", or a client receives an
|
||||
"ext-info-s", it MAY send an SSH_MSG_EXT_INFO message but is not
|
||||
required to do so.
|
||||
|
||||
Neither party needs to wait for the other's SSH_MSG_KEXINIT in order
|
||||
to decide whether to send the appropriate indicator in its own
|
||||
SSH_MSG_KEXINIT.
|
||||
|
||||
Implementations MUST NOT send an incorrect indicator name for their
|
||||
role. Implementations MAY disconnect if the counterparty sends an
|
||||
incorrect indicator. If "ext-info-c" or "ext-info-s" ends up being
|
||||
negotiated as a key exchange method, the parties MUST disconnect.
|
||||
|
||||
2.3. SSH_MSG_EXT_INFO Message
|
||||
|
||||
A party that received the "ext-info-c" or "ext-info-s" indicator MAY
|
||||
send the following message:
|
||||
|
||||
byte SSH_MSG_EXT_INFO (value 7)
|
||||
uint32 nr-extensions
|
||||
repeat the following 2 fields "nr-extensions" times:
|
||||
string extension-name
|
||||
string extension-value (binary)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 4]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
Implementers should pay careful attention to Section 2.5, in
|
||||
particular to the requirement to tolerate any sequence of bytes
|
||||
(including null bytes at any position) in an unknown extension's
|
||||
extension-value.
|
||||
|
||||
2.4. Message Order
|
||||
|
||||
If a client sends SSH_MSG_EXT_INFO, it MUST send it as the next
|
||||
packet following the client's first SSH_MSG_NEWKEYS message to the
|
||||
server.
|
||||
|
||||
If a server sends SSH_MSG_EXT_INFO, it MAY send it at zero, one, or
|
||||
both of the following opportunities:
|
||||
|
||||
o As the next packet following the server's first SSH_MSG_NEWKEYS.
|
||||
|
||||
Where clients need information in the server's SSH_MSG_EXT_INFO to
|
||||
authenticate, it is helpful if the server sends its
|
||||
SSH_MSG_EXT_INFO not only as the next packet after
|
||||
SSH_MSG_NEWKEYS, but without delay.
|
||||
|
||||
Clients cannot rely on this because the server is not required to
|
||||
send the message at this time; if sent, it may be delayed by the
|
||||
network. However, if a timely SSH_MSG_EXT_INFO is received, a
|
||||
client can pipeline an authentication request after its
|
||||
SSH_MSG_SERVICE_REQUEST, even when it needs extension information.
|
||||
|
||||
o Immediately preceding the server's SSH_MSG_USERAUTH_SUCCESS, as
|
||||
defined in [RFC4252].
|
||||
|
||||
The server MAY send SSH_MSG_EXT_INFO at this second opportunity,
|
||||
whether or not it sent it at the first. A client that sent
|
||||
"ext-info-c" MUST accept a server's SSH_MSG_EXT_INFO at both
|
||||
opportunities but MUST NOT require it.
|
||||
|
||||
This allows a server to reveal support for additional extensions
|
||||
that it was unwilling to reveal to an unauthenticated client. If
|
||||
a server sends a second SSH_MSG_EXT_INFO, this replaces any
|
||||
initial one, and both the client and the server re-evaluate
|
||||
extensions in effect. The server's second SSH_MSG_EXT_INFO is
|
||||
matched against the client's original.
|
||||
|
||||
The timing of the second opportunity is chosen for the following
|
||||
reasons. If the message was sent earlier, it would not allow the
|
||||
server to withhold information until the client has authenticated.
|
||||
If it was sent later, a client that needs information from the
|
||||
second SSH_MSG_EXT_INFO immediately after it authenticates would
|
||||
have no way to reliably know whether to expect the message.
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 5]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
2.5. Interpretation of Extension Names and Values
|
||||
|
||||
Each extension is identified by its extension-name and defines the
|
||||
conditions under which the extension is considered to be in effect.
|
||||
Applications MUST ignore unrecognized extension-names.
|
||||
|
||||
When it is specified, an extension MAY dictate that, in order to take
|
||||
effect, both parties must include it in their SSH_MSG_EXT_INFO or
|
||||
that it is sufficient for only one party to include it. However,
|
||||
other rules MAY be specified. The relative order in which extensions
|
||||
appear in an SSH_MSG_EXT_INFO message MUST be ignored.
|
||||
|
||||
Extension-value fields are interpreted as defined by their respective
|
||||
extension. This field MAY be empty if permitted by the extension.
|
||||
Applications that do not implement or recognize an extension MUST
|
||||
ignore its extension-value, regardless of its size or content.
|
||||
Applications MUST tolerate any sequence of bytes -- including null
|
||||
bytes at any position -- in an unknown extension's extension-value.
|
||||
|
||||
The cumulative size of an SSH_MSG_EXT_INFO message is limited only by
|
||||
the maximum packet length that an implementation may apply in
|
||||
accordance with [RFC4253]. Implementations MUST accept well-formed
|
||||
SSH_MSG_EXT_INFO messages up to the maximum packet length they
|
||||
accept.
|
||||
|
||||
3. Initially Defined Extensions
|
||||
|
||||
3.1. "server-sig-algs"
|
||||
|
||||
This extension is sent with the following extension name and value:
|
||||
|
||||
string "server-sig-algs"
|
||||
name-list public-key-algorithms-accepted
|
||||
|
||||
The name-list type is a strict subset of the string type and is thus
|
||||
permissible as an extension-value. See [RFC4251] for more
|
||||
information.
|
||||
|
||||
This extension is sent by the server and contains a list of public
|
||||
key algorithms that the server is able to process as part of a
|
||||
"publickey" authentication request. If a client sends this
|
||||
extension, the server MAY ignore it and MAY disconnect.
|
||||
|
||||
In this extension, a server MUST enumerate all public key algorithms
|
||||
it might accept during user authentication. However, early server
|
||||
implementations that do not enumerate all accepted algorithms do
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 6]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
exist. For this reason, a client MAY send a user authentication
|
||||
request using a public key algorithm not included in "server-sig-
|
||||
algs".
|
||||
|
||||
A client that wishes to proceed with public key authentication MAY
|
||||
wait for the server's SSH_MSG_EXT_INFO so it can send a "publickey"
|
||||
authentication request with an appropriate public key algorithm,
|
||||
rather than resorting to trial and error.
|
||||
|
||||
Servers that implement public key authentication SHOULD implement
|
||||
this extension.
|
||||
|
||||
If a server does not send this extension, a client MUST NOT make any
|
||||
assumptions about the server's public key algorithm support, and MAY
|
||||
proceed with authentication requests using trial and error. Note
|
||||
that implementations are known to exist that apply authentication
|
||||
penalties if the client attempts to use an unexpected public key
|
||||
algorithm.
|
||||
|
||||
Authentication penalties are applied by servers to deter brute-force
|
||||
password guessing, username enumeration, and other types of behavior
|
||||
deemed suspicious by server administrators or implementers.
|
||||
Penalties may include automatic IP address throttling or blocking,
|
||||
and they may trigger email alerts or auditing.
|
||||
|
||||
3.2. "delay-compression"
|
||||
|
||||
This extension MAY be sent by both parties as follows:
|
||||
|
||||
string "delay-compression"
|
||||
string:
|
||||
name-list compression_algorithms_client_to_server
|
||||
name-list compression_algorithms_server_to_client
|
||||
|
||||
The extension-value is a string that encodes two name-lists. The
|
||||
name-lists themselves have the encoding of strings. For example, to
|
||||
indicate a preference for algorithms "foo,bar" in the client-to-
|
||||
server direction and "bar,baz" in the server-to-client direction, a
|
||||
sender encodes the extension-value as follows (including its length):
|
||||
|
||||
00000016 00000007 666f6f2c626172 00000007 6261722c62617a
|
||||
|
||||
This same encoding could be sent by either party -- client or server.
|
||||
|
||||
This extension allows the server and client to renegotiate
|
||||
compression algorithm support without having to conduct a key
|
||||
re-exchange, which puts new algorithms into effect immediately upon
|
||||
successful authentication.
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 7]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
This extension takes effect only if both parties send it. Name-lists
|
||||
MAY include any compression algorithm that could have been negotiated
|
||||
in SSH_MSG_KEXINIT, except algorithms that define their own delayed
|
||||
compression semantics. This means "zlib,none" is a valid algorithm
|
||||
list in this context, but "zlib@openssh.com" is not.
|
||||
|
||||
If both parties send this extension, but the name-lists do not
|
||||
contain a common algorithm in either direction, the parties MUST
|
||||
disconnect in the same way as if negotiation failed as part of
|
||||
SSH_MSG_KEXINIT.
|
||||
|
||||
If this extension takes effect, the renegotiated compression
|
||||
algorithm is activated for the very next SSH message after the
|
||||
trigger message:
|
||||
|
||||
o Sent by the server, the trigger message is
|
||||
SSH_MSG_USERAUTH_SUCCESS.
|
||||
|
||||
o Sent by the client, the trigger message is SSH_MSG_NEWCOMPRESS.
|
||||
|
||||
If this extension takes effect, the client MUST send the following
|
||||
message within a reasonable number of outgoing SSH messages after
|
||||
receiving SSH_MSG_USERAUTH_SUCCESS, but not necessarily as the first
|
||||
such outgoing message:
|
||||
|
||||
byte SSH_MSG_NEWCOMPRESS (value 8)
|
||||
|
||||
The purpose of SSH_MSG_NEWCOMPRESS is to avoid a race condition where
|
||||
the server cannot reliably know whether a message sent by the client
|
||||
was sent before or after receiving the server's
|
||||
SSH_MSG_USERAUTH_SUCCESS. For example, clients may send keep-alive
|
||||
messages during logon processing.
|
||||
|
||||
As is the case for all extensions unless otherwise noted, the server
|
||||
MAY delay including this extension until its secondary
|
||||
SSH_MSG_EXT_INFO, sent before SSH_MSG_USERAUTH_SUCCESS. This allows
|
||||
the server to avoid advertising compression until the client has
|
||||
authenticated.
|
||||
|
||||
If the parties renegotiate compression using this extension in a
|
||||
session where compression is already enabled and the renegotiated
|
||||
algorithm is the same in one or both directions, then the internal
|
||||
compression state MUST be reset for each direction at the time the
|
||||
renegotiated algorithm takes effect.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 8]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
3.2.1. Awkwardly Timed Key Re-Exchange
|
||||
|
||||
A party that has signaled, or intends to signal, support for this
|
||||
extension in an SSH session MUST NOT initiate key re-exchange in that
|
||||
session until either of the following occurs:
|
||||
|
||||
o This extension was negotiated, and the party that's about to start
|
||||
key re-exchange already sent its trigger message for compression.
|
||||
|
||||
o The party has sent (if server) or received (if client) the message
|
||||
SSH_MSG_USERAUTH_SUCCESS, and this extension was not negotiated.
|
||||
|
||||
If a party violates this rule, the other party MAY disconnect.
|
||||
|
||||
In general, parties SHOULD NOT start key re-exchange before
|
||||
successful user authentication but MAY tolerate it if not using this
|
||||
extension.
|
||||
|
||||
3.2.2. Subsequent Re-Exchange
|
||||
|
||||
In subsequent key re-exchanges that unambiguously begin after the
|
||||
compression trigger messages, the compression algorithms negotiated
|
||||
in re-exchange override the algorithms negotiated with this
|
||||
extension.
|
||||
|
||||
3.2.3. Compatibility Note: OpenSSH up to Version 7.5
|
||||
|
||||
This extension uses a binary extension-value encoding. OpenSSH
|
||||
clients up to and including version 7.5 advertise support to receive
|
||||
SSH_MSG_EXT_INFO but disconnect on receipt of an extension-value
|
||||
containing null bytes. This is an error fixed in OpenSSH
|
||||
version 7.6.
|
||||
|
||||
Implementations that wish to interoperate with OpenSSH 7.5 and
|
||||
earlier are advised to check the remote party's SSH version string
|
||||
and omit this extension if an affected version is detected. Affected
|
||||
versions do not implement this extension, so there is no harm in
|
||||
omitting it. The extension SHOULD NOT be omitted if the detected
|
||||
OpenSSH version is 7.6 or higher. This would make it harder for the
|
||||
OpenSSH project to implement this extension in a higher version.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 9]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
3.3. "no-flow-control"
|
||||
|
||||
This extension is sent with the following extension name and value:
|
||||
|
||||
string "no-flow-control"
|
||||
string choice of: "p" for preferred | "s" for supported
|
||||
|
||||
A party SHOULD send "s" if it supports "no-flow-control" but does not
|
||||
prefer to enable it. A party SHOULD send "p" if it prefers to enable
|
||||
the extension if the other party supports it. Parties MAY disconnect
|
||||
if they receive a different extension value.
|
||||
|
||||
For this extension to take effect, the following must occur:
|
||||
|
||||
o This extension MUST be sent by both parties.
|
||||
|
||||
o At least one party MUST have sent the value "p" (preferred).
|
||||
|
||||
If this extension takes effect, the "initial window size" fields in
|
||||
SSH_MSG_CHANNEL_OPEN and SSH_MSG_CHANNEL_OPEN_CONFIRMATION, as
|
||||
defined in [RFC4254], become meaningless. The values of these fields
|
||||
MUST be ignored, and a channel behaves as if all window sizes are
|
||||
infinite. Neither side is required to send any
|
||||
SSH_MSG_CHANNEL_WINDOW_ADJUST messages, and if received, such
|
||||
messages MUST be ignored.
|
||||
|
||||
This extension is intended for, but not limited to, use by file
|
||||
transfer applications that are only going to use one channel and for
|
||||
which the flow control provided by SSH is an impediment, rather than
|
||||
a feature.
|
||||
|
||||
Implementations MUST refuse to open more than one simultaneous
|
||||
channel when this extension is in effect. Nevertheless, server
|
||||
implementations SHOULD support clients opening more than one
|
||||
non-simultaneous channel.
|
||||
|
||||
3.3.1. Prior "No Flow Control" Practice
|
||||
|
||||
Before this extension, some applications would simply not implement
|
||||
SSH flow control, sending an initial channel window size of 2^32 - 1.
|
||||
Applications SHOULD NOT do this for the following reasons:
|
||||
|
||||
o It is plausible to transfer more than 2^32 bytes over a channel.
|
||||
Such a channel will hang if the other party implements SSH flow
|
||||
control according to [RFC4254].
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 10]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
o Implementations that cannot handle large channel window sizes
|
||||
exist, and they can exhibit non-graceful behaviors, including
|
||||
disconnect.
|
||||
|
||||
3.4. "elevation"
|
||||
|
||||
The terms "elevation" and "elevated" refer to an operating system
|
||||
mechanism where an administrator's logon session is associated with
|
||||
two security contexts: one limited and one with administrative
|
||||
rights. To "elevate" such a session is to activate the security
|
||||
context with full administrative rights. For more information about
|
||||
this mechanism on Windows, see [WINADMIN] and [WINTOKEN].
|
||||
|
||||
This extension MAY be sent by the client as follows:
|
||||
|
||||
string "elevation"
|
||||
string choice of: "y" | "n" | "d"
|
||||
|
||||
A client sends "y" to indicate its preference that the session should
|
||||
be elevated; "n" to not be elevated; and "d" for the server to use
|
||||
its default behavior. The server MAY disconnect if it receives a
|
||||
different extension value. If a client does not send the "elevation"
|
||||
extension, the server SHOULD act as if "d" was sent.
|
||||
|
||||
If a client has included this extension, then after authentication, a
|
||||
server that supports this extension SHOULD indicate to the client
|
||||
whether elevation was done by sending the following global request:
|
||||
|
||||
byte SSH_MSG_GLOBAL_REQUEST
|
||||
string "elevation"
|
||||
boolean want reply = false
|
||||
boolean elevation performed
|
||||
|
||||
Clients that implement this extension help reduce attack surface for
|
||||
Windows servers that handle administrative logins. Where clients do
|
||||
not support this extension, servers must elevate sessions to allow
|
||||
full access by administrative users always. Where clients support
|
||||
this extension, sessions can be created without elevation unless
|
||||
requested.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 11]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
4. IANA Considerations
|
||||
|
||||
4.1. Additions to Existing Registries
|
||||
|
||||
IANA has added the following entries to the "Message Numbers"
|
||||
registry [IANA-M] under the "Secure Shell (SSH) Protocol Parameters"
|
||||
registry [RFC4250]:
|
||||
|
||||
Value Message ID Reference
|
||||
-----------------------------------------
|
||||
7 SSH_MSG_EXT_INFO RFC 8308
|
||||
8 SSH_MSG_NEWCOMPRESS RFC 8308
|
||||
|
||||
IANA has also added the following entries to the "Key Exchange Method
|
||||
Names" registry [IANA-KE]:
|
||||
|
||||
Method Name Reference Note
|
||||
------------------------------------------
|
||||
ext-info-s RFC 8308 Section 2
|
||||
ext-info-c RFC 8308 Section 2
|
||||
|
||||
4.2. New Registry: Extension Names
|
||||
|
||||
Also under the "Secure Shell (SSH) Protocol Parameters" registry,
|
||||
IANA has created a new "Extension Names" registry, with the following
|
||||
initial content:
|
||||
|
||||
Extension Name Reference Note
|
||||
------------------------------------------------
|
||||
server-sig-algs RFC 8308 Section 3.1
|
||||
delay-compression RFC 8308 Section 3.2
|
||||
no-flow-control RFC 8308 Section 3.3
|
||||
elevation RFC 8308 Section 3.4
|
||||
|
||||
4.2.1. Future Assignments to Extension Names Registry
|
||||
|
||||
Names in the "Extension Names" registry MUST follow the conventions
|
||||
for names defined in [RFC4250], Section 4.6.1.
|
||||
|
||||
Requests for assignments of new non-local names in the "Extension
|
||||
Names" registry (i.e., names not including the '@' character) MUST be
|
||||
done using the IETF Review policy, as described in [RFC8126].
|
||||
|
||||
5. Security Considerations
|
||||
|
||||
Security considerations are discussed throughout this document. This
|
||||
document updates the SSH protocol as defined in [RFC4251] and related
|
||||
documents. The security considerations of [RFC4251] apply.
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 12]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
6. References
|
||||
|
||||
6.1. Normative References
|
||||
|
||||
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
|
||||
Requirement Levels", BCP 14, RFC 2119,
|
||||
DOI 10.17487/RFC2119, March 1997,
|
||||
<https://www.rfc-editor.org/info/rfc2119>.
|
||||
|
||||
[RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Protocol Assigned Numbers", RFC 4250,
|
||||
DOI 10.17487/RFC4250, January 2006,
|
||||
<https://www.rfc-editor.org/info/rfc4250>.
|
||||
|
||||
[RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Protocol Architecture", RFC 4251, DOI 10.17487/RFC4251,
|
||||
January 2006, <https://www.rfc-editor.org/info/rfc4251>.
|
||||
|
||||
[RFC4252] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Authentication Protocol", RFC 4252, DOI 10.17487/RFC4252,
|
||||
January 2006, <https://www.rfc-editor.org/info/rfc4252>.
|
||||
|
||||
[RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253,
|
||||
January 2006, <https://www.rfc-editor.org/info/rfc4253>.
|
||||
|
||||
[RFC4254] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Connection Protocol", RFC 4254, DOI 10.17487/RFC4254,
|
||||
January 2006, <https://www.rfc-editor.org/info/rfc4254>.
|
||||
|
||||
[RFC8126] Cotton, M., Leiba, B., and T. Narten, "Guidelines for
|
||||
Writing an IANA Considerations Section in RFCs", BCP 26,
|
||||
RFC 8126, DOI 10.17487/RFC8126, June 2017,
|
||||
<https://www.rfc-editor.org/info/rfc8126>.
|
||||
|
||||
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
|
||||
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
|
||||
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
|
||||
|
||||
6.2. Informative References
|
||||
|
||||
[IANA-KE] IANA, "Key Exchange Method Names",
|
||||
<https://www.iana.org/assignments/ssh-parameters/>.
|
||||
|
||||
[IANA-M] IANA, "Message Numbers",
|
||||
<https://www.iana.org/assignments/ssh-parameters/>.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 13]
|
||||
|
||||
RFC 8308 Extension Negotiation in SSH March 2018
|
||||
|
||||
|
||||
[RFC8332] Bider, D., "Use of RSA Keys with SHA-256 and SHA-512 in
|
||||
the Secure Shell (SSH) Protocol", RFC 8332,
|
||||
DOI 10.17487/RFC8332, March 2018,
|
||||
<https://www.rfc-editor.org/info/rfc8332>.
|
||||
|
||||
[WINADMIN] Microsoft, "How to launch a process as a Full
|
||||
Administrator when UAC is enabled?", March 2013,
|
||||
<https://blogs.msdn.microsoft.com/winsdk/2013/03/22/
|
||||
how-to-launch-a-process-as-a-full-administrator-when-
|
||||
uac-is-enabled/>.
|
||||
|
||||
[WINTOKEN] Microsoft, "TOKEN_ELEVATION_TYPE enumeration",
|
||||
<https://msdn.microsoft.com/en-us/library/windows/desktop/
|
||||
bb530718.aspx>.
|
||||
|
||||
Acknowledgments
|
||||
|
||||
Thanks to Markus Friedl and Damien Miller for comments and initial
|
||||
implementation. Thanks to Peter Gutmann, Roumen Petrov, Mark D.
|
||||
Baushke, Daniel Migault, Eric Rescorla, Matthew A. Miller, Mirja
|
||||
Kuehlewind, Adam Roach, Spencer Dawkins, Alexey Melnikov, and Ben
|
||||
Campbell for reviews and feedback.
|
||||
|
||||
Author's Address
|
||||
|
||||
Denis Bider
|
||||
Bitvise Limited
|
||||
4105 Lombardy Court
|
||||
Colleyville, TX 76034
|
||||
United States of America
|
||||
|
||||
Email: ietf-ssh3@denisbider.com
|
||||
URI: https://www.bitvise.com/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 14]
|
||||
|
||||
507
specifications/rfc8332.txt
Normal file
507
specifications/rfc8332.txt
Normal file
@@ -0,0 +1,507 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Internet Engineering Task Force (IETF) D. Bider
|
||||
Request for Comments: 8332 Bitvise Limited
|
||||
Updates: 4252, 4253 March 2018
|
||||
Category: Standards Track
|
||||
ISSN: 2070-1721
|
||||
|
||||
|
||||
Use of RSA Keys with SHA-256 and SHA-512
|
||||
in the Secure Shell (SSH) Protocol
|
||||
|
||||
Abstract
|
||||
|
||||
This memo updates RFCs 4252 and 4253 to define new public key
|
||||
algorithms for use of RSA keys with SHA-256 and SHA-512 for server
|
||||
and client authentication in SSH connections.
|
||||
|
||||
Status of This Memo
|
||||
|
||||
This is an Internet Standards Track document.
|
||||
|
||||
This document is a product of the Internet Engineering Task Force
|
||||
(IETF). It represents the consensus of the IETF community. It has
|
||||
received public review and has been approved for publication by the
|
||||
Internet Engineering Steering Group (IESG). Further information on
|
||||
Internet Standards is available in Section 2 of RFC 7841.
|
||||
|
||||
Information about the current status of this document, any errata,
|
||||
and how to provide feedback on it may be obtained at
|
||||
https://www.rfc-editor.org/info/rfc8332.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 1]
|
||||
|
||||
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
|
||||
|
||||
|
||||
Copyright Notice
|
||||
|
||||
Copyright (c) 2018 IETF Trust and the persons identified as the
|
||||
document authors. All rights reserved.
|
||||
|
||||
This document is subject to BCP 78 and the IETF Trust's Legal
|
||||
Provisions Relating to IETF Documents
|
||||
(https://trustee.ietf.org/license-info) in effect on the date of
|
||||
publication of this document. Please review these documents
|
||||
carefully, as they describe your rights and restrictions with respect
|
||||
to this document. Code Components extracted from this document must
|
||||
include Simplified BSD License text as described in Section 4.e of
|
||||
the Trust Legal Provisions and are provided without warranty as
|
||||
described in the Simplified BSD License.
|
||||
|
||||
This document may contain material from IETF Documents or IETF
|
||||
Contributions published or made publicly available before November
|
||||
10, 2008. The person(s) controlling the copyright in some of this
|
||||
material may not have granted the IETF Trust the right to allow
|
||||
modifications of such material outside the IETF Standards Process.
|
||||
Without obtaining an adequate license from the person(s) controlling
|
||||
the copyright in such materials, this document may not be modified
|
||||
outside the IETF Standards Process, and derivative works of it may
|
||||
not be created outside the IETF Standards Process, except to format
|
||||
it for publication as an RFC or to translate it into languages other
|
||||
than English.
|
||||
|
||||
Table of Contents
|
||||
|
||||
1. Overview and Rationale . . . . . . . . . . . . . . . . . . . 3
|
||||
1.1. Requirements Terminology . . . . . . . . . . . . . . . . 3
|
||||
1.2. Wire Encoding Terminology . . . . . . . . . . . . . . . . 3
|
||||
2. Public Key Format vs. Public Key Algorithm . . . . . . . . . 3
|
||||
3. New RSA Public Key Algorithms . . . . . . . . . . . . . . . . 4
|
||||
3.1. Use for Server Authentication . . . . . . . . . . . . . . 5
|
||||
3.2. Use for Client Authentication . . . . . . . . . . . . . . 5
|
||||
3.3. Discovery of Public Key Algorithms Supported by Servers . 6
|
||||
4. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 6
|
||||
5. Security Considerations . . . . . . . . . . . . . . . . . . . 7
|
||||
5.1. Key Size and Signature Hash . . . . . . . . . . . . . . . 7
|
||||
5.2. Transition . . . . . . . . . . . . . . . . . . . . . . . 7
|
||||
5.3. PKCS #1 v1.5 Padding and Signature Verification . . . . . 7
|
||||
6. References . . . . . . . . . . . . . . . . . . . . . . . . . 8
|
||||
6.1. Normative References . . . . . . . . . . . . . . . . . . 8
|
||||
6.2. Informative References . . . . . . . . . . . . . . . . . 8
|
||||
Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . 9
|
||||
Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 9
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 2]
|
||||
|
||||
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
|
||||
|
||||
|
||||
1. Overview and Rationale
|
||||
|
||||
Secure Shell (SSH) is a common protocol for secure communication on
|
||||
the Internet. In [RFC4253], SSH originally defined the public key
|
||||
algorithms "ssh-rsa" for server and client authentication using RSA
|
||||
with SHA-1, and "ssh-dss" using 1024-bit DSA and SHA-1. These
|
||||
algorithms are now considered deficient. For US government use, NIST
|
||||
has disallowed 1024-bit RSA and DSA, and use of SHA-1 for signing
|
||||
[NIST.800-131A].
|
||||
|
||||
This memo updates RFCs 4252 and 4253 to define new public key
|
||||
algorithms allowing for interoperable use of existing and new RSA
|
||||
keys with SHA-256 and SHA-512.
|
||||
|
||||
1.1. Requirements Terminology
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
|
||||
"OPTIONAL" in this document are to be interpreted as described in
|
||||
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
|
||||
capitals, as shown here.
|
||||
|
||||
1.2. Wire Encoding Terminology
|
||||
|
||||
The wire encoding types in this document -- "boolean", "byte",
|
||||
"string", "mpint" -- have meanings as described in [RFC4251].
|
||||
|
||||
2. Public Key Format vs. Public Key Algorithm
|
||||
|
||||
In [RFC4252], the concept "public key algorithm" is used to establish
|
||||
a relationship between one algorithm name, and:
|
||||
|
||||
A. procedures used to generate and validate a private/public
|
||||
keypair;
|
||||
B. a format used to encode a public key; and
|
||||
C. procedures used to calculate, encode, and verify a signature.
|
||||
|
||||
This document uses the term "public key format" to identify only A
|
||||
and B in isolation. The term "public key algorithm" continues to
|
||||
identify all three aspects -- A, B, and C.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 3]
|
||||
|
||||
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
|
||||
|
||||
|
||||
3. New RSA Public Key Algorithms
|
||||
|
||||
This memo adopts the style and conventions of [RFC4253] in specifying
|
||||
how use of a public key algorithm is indicated in SSH.
|
||||
|
||||
The following new public key algorithms are defined:
|
||||
|
||||
rsa-sha2-256 RECOMMENDED sign Raw RSA key
|
||||
rsa-sha2-512 OPTIONAL sign Raw RSA key
|
||||
|
||||
These algorithms are suitable for use both in the SSH transport layer
|
||||
[RFC4253] for server authentication and in the authentication layer
|
||||
[RFC4252] for client authentication.
|
||||
|
||||
Since RSA keys are not dependent on the choice of hash function, the
|
||||
new public key algorithms reuse the "ssh-rsa" public key format as
|
||||
defined in [RFC4253]:
|
||||
|
||||
string "ssh-rsa"
|
||||
mpint e
|
||||
mpint n
|
||||
|
||||
All aspects of the "ssh-rsa" format are kept, including the encoded
|
||||
string "ssh-rsa". This allows existing RSA keys to be used with the
|
||||
new public key algorithms, without requiring re-encoding or affecting
|
||||
already trusted key fingerprints.
|
||||
|
||||
Signing and verifying using these algorithms is performed according
|
||||
to the RSASSA-PKCS1-v1_5 scheme in [RFC8017] using SHA-2 [SHS] as
|
||||
hash.
|
||||
|
||||
For the algorithm "rsa-sha2-256", the hash used is SHA-256.
|
||||
For the algorithm "rsa-sha2-512", the hash used is SHA-512.
|
||||
|
||||
The resulting signature is encoded as follows:
|
||||
|
||||
string "rsa-sha2-256" / "rsa-sha2-512"
|
||||
string rsa_signature_blob
|
||||
|
||||
The value for 'rsa_signature_blob' is encoded as a string that
|
||||
contains an octet string S (which is the output of RSASSA-PKCS1-v1_5)
|
||||
and that has the same length (in octets) as the RSA modulus. When S
|
||||
contains leading zeros, there exist signers that will send a shorter
|
||||
encoding of S that omits them. A verifier MAY accept shorter
|
||||
encodings of S with one or more leading zeros omitted.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 4]
|
||||
|
||||
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
|
||||
|
||||
|
||||
3.1. Use for Server Authentication
|
||||
|
||||
To express support and preference for one or both of these algorithms
|
||||
for server authentication, the SSH client or server includes one or
|
||||
both algorithm names, "rsa-sha2-256" and/or "rsa-sha2-512", in the
|
||||
name-list field "server_host_key_algorithms" in the SSH_MSG_KEXINIT
|
||||
packet [RFC4253]. If one of the two host key algorithms is
|
||||
negotiated, the server sends an "ssh-rsa" public key as part of the
|
||||
negotiated key exchange method (e.g., in SSH_MSG_KEXDH_REPLY) and
|
||||
encodes a signature with the appropriate signature algorithm name --
|
||||
either "rsa-sha2-256" or "rsa-sha2-512".
|
||||
|
||||
3.2. Use for Client Authentication
|
||||
|
||||
To use this algorithm for client authentication, the SSH client sends
|
||||
an SSH_MSG_USERAUTH_REQUEST message [RFC4252] encoding the
|
||||
"publickey" method and encoding the string field "public key
|
||||
algorithm name" with the value "rsa-sha2-256" or "rsa-sha2-512". The
|
||||
"public key blob" field encodes the RSA public key using the
|
||||
"ssh-rsa" public key format.
|
||||
|
||||
For example, as defined in [RFC4252] and [RFC4253], an SSH
|
||||
"publickey" authentication request using an "rsa-sha2-512" signature
|
||||
would be properly encoded as follows:
|
||||
|
||||
byte SSH_MSG_USERAUTH_REQUEST
|
||||
string user name
|
||||
string service name
|
||||
string "publickey"
|
||||
boolean TRUE
|
||||
string "rsa-sha2-512"
|
||||
string public key blob:
|
||||
string "ssh-rsa"
|
||||
mpint e
|
||||
mpint n
|
||||
string signature:
|
||||
string "rsa-sha2-512"
|
||||
string rsa_signature_blob
|
||||
|
||||
If the client includes the signature field, the client MUST encode
|
||||
the same algorithm name in the signature as in
|
||||
SSH_MSG_USERAUTH_REQUEST -- either "rsa-sha2-256" or "rsa-sha2-512".
|
||||
If a server receives a mismatching request, it MAY apply arbitrary
|
||||
authentication penalties, including but not limited to authentication
|
||||
failure or disconnect.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 5]
|
||||
|
||||
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
|
||||
|
||||
|
||||
OpenSSH 7.2 (but not 7.2p2) incorrectly encodes the algorithm in the
|
||||
signature as "ssh-rsa" when the algorithm in SSH_MSG_USERAUTH_REQUEST
|
||||
is "rsa-sha2-256" or "rsa-sha2-512". In this case, the signature
|
||||
does actually use either SHA-256 or SHA-512. A server MAY, but is
|
||||
not required to, accept this variant or another variant that
|
||||
corresponds to a good-faith implementation and is considered safe to
|
||||
accept.
|
||||
|
||||
3.3. Discovery of Public Key Algorithms Supported by Servers
|
||||
|
||||
Implementation experience has shown that there are servers that apply
|
||||
authentication penalties to clients attempting public key algorithms
|
||||
that the SSH server does not support.
|
||||
|
||||
Servers that accept rsa-sha2-* signatures for client authentication
|
||||
SHOULD implement the extension negotiation mechanism defined in
|
||||
[RFC8308], including especially the "server-sig-algs" extension.
|
||||
|
||||
When authenticating with an RSA key against a server that does not
|
||||
implement the "server-sig-algs" extension, clients MAY default to an
|
||||
"ssh-rsa" signature to avoid authentication penalties. When the new
|
||||
rsa-sha2-* algorithms have been sufficiently widely adopted to
|
||||
warrant disabling "ssh-rsa", clients MAY default to one of the new
|
||||
algorithms.
|
||||
|
||||
4. IANA Considerations
|
||||
|
||||
IANA has updated the "Secure Shell (SSH) Protocol Parameters"
|
||||
registry, established with [RFC4250], to extend the table "Public Key
|
||||
Algorithm Names" [IANA-PKA] as follows.
|
||||
|
||||
- To the immediate right of the column "Public Key Algorithm Name",
|
||||
a new column has been added, titled "Public Key Format". For
|
||||
existing entries, the column "Public Key Format" has been assigned
|
||||
the same value as under "Public Key Algorithm Name".
|
||||
|
||||
- Immediately following the existing entry for "ssh-rsa", two
|
||||
sibling entries have been added:
|
||||
|
||||
P. K. Alg. Name P. K. Format Reference Note
|
||||
rsa-sha2-256 ssh-rsa RFC 8332 Section 3
|
||||
rsa-sha2-512 ssh-rsa RFC 8332 Section 3
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 6]
|
||||
|
||||
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
|
||||
|
||||
|
||||
5. Security Considerations
|
||||
|
||||
The security considerations of [RFC4251] apply to this document.
|
||||
|
||||
5.1. Key Size and Signature Hash
|
||||
|
||||
The National Institute of Standards and Technology (NIST) Special
|
||||
Publication 800-131A, Revision 1 [NIST.800-131A] disallows RSA and
|
||||
DSA keys shorter than 2048 bits for US government use. The same
|
||||
document disallows the SHA-1 hash function for digital signature
|
||||
generation, except under NIST's protocol-specific guidance.
|
||||
|
||||
It is prudent to follow this advice also outside of US government
|
||||
use.
|
||||
|
||||
5.2. Transition
|
||||
|
||||
This document is based on the premise that RSA is used in
|
||||
environments where a gradual, compatible transition to improved
|
||||
algorithms will be better received than one that is abrupt and
|
||||
incompatible. It advises that SSH implementations add support for
|
||||
new RSA public key algorithms along with SSH_MSG_EXT_INFO and the
|
||||
"server-sig-algs" extension to allow coexistence of new deployments
|
||||
with older versions that support only "ssh-rsa". Nevertheless,
|
||||
implementations SHOULD start to disable "ssh-rsa" in their default
|
||||
configurations as soon as the implementers believe that new RSA
|
||||
signature algorithms have been widely adopted.
|
||||
|
||||
5.3. PKCS #1 v1.5 Padding and Signature Verification
|
||||
|
||||
This document prescribes RSASSA-PKCS1-v1_5 signature padding because:
|
||||
|
||||
(1) RSASSA-PSS is not universally available to all implementations;
|
||||
(2) PKCS #1 v1.5 is widely supported in existing SSH
|
||||
implementations;
|
||||
(3) PKCS #1 v1.5 is not known to be insecure for use in this scheme.
|
||||
|
||||
Implementers are advised that a signature with RSASSA-PKCS1-v1_5
|
||||
padding MUST NOT be verified by applying the RSA key to the
|
||||
signature, and then parsing the output to extract the hash. This may
|
||||
give an attacker opportunities to exploit flaws in the parsing and
|
||||
vary the encoding. Verifiers MUST instead apply RSASSA-PKCS1-v1_5
|
||||
padding to the expected hash, then compare the encoded bytes with the
|
||||
output of the RSA operation.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 7]
|
||||
|
||||
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
|
||||
|
||||
|
||||
6. References
|
||||
|
||||
6.1. Normative References
|
||||
|
||||
[SHS] NIST, "Secure Hash Standard (SHS)", FIPS Publication
|
||||
180-4, August 2015,
|
||||
<http://dx.doi.org/10.6028/NIST.FIPS.180-4>.
|
||||
|
||||
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
|
||||
Requirement Levels", BCP 14, RFC 2119,
|
||||
DOI 10.17487/RFC2119, March 1997,
|
||||
<https://www.rfc-editor.org/info/rfc2119>.
|
||||
|
||||
[RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Protocol Architecture", RFC 4251, DOI 10.17487/RFC4251,
|
||||
January 2006, <https://www.rfc-editor.org/info/rfc4251>.
|
||||
|
||||
[RFC4252] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Authentication Protocol", RFC 4252, DOI 10.17487/RFC4252,
|
||||
January 2006, <https://www.rfc-editor.org/info/rfc4252>.
|
||||
|
||||
[RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253,
|
||||
January 2006, <https://www.rfc-editor.org/info/rfc4253>.
|
||||
|
||||
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
|
||||
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
|
||||
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
|
||||
|
||||
[RFC8308] Bider, D., "Extension Negotiation in the Secure Shell
|
||||
(SSH) Protocol", RFC 8308, DOI 10.17487/RFC8308, March
|
||||
2018, <https://www.rfc-editor.org/info/rfc8308>.
|
||||
|
||||
6.2. Informative References
|
||||
|
||||
[NIST.800-131A]
|
||||
NIST, "Transitions: Recommendation for Transitioning the
|
||||
Use of Cryptographic Algorithms and Key Lengths", NIST
|
||||
Special Publication 800-131A, Revision 1,
|
||||
DOI 10.6028/NIST.SP.800-131Ar1, November 2015,
|
||||
<http://nvlpubs.nist.gov/nistpubs/SpecialPublications/
|
||||
NIST.SP.800-131Ar1.pdf>.
|
||||
|
||||
[RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Protocol Assigned Numbers", RFC 4250,
|
||||
DOI 10.17487/RFC4250, January 2006,
|
||||
<https://www.rfc-editor.org/info/rfc4250>.
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 8]
|
||||
|
||||
RFC 8332 Use of RSA Keys with SHA-256 and SHA-512 March 2018
|
||||
|
||||
|
||||
[RFC8017] Moriarty, K., Ed., Kaliski, B., Jonsson, J., and A. Rusch,
|
||||
"PKCS #1: RSA Cryptography Specifications Version 2.2",
|
||||
RFC 8017, DOI 10.17487/RFC8017, November 2016,
|
||||
<https://www.rfc-editor.org/info/rfc8017>.
|
||||
|
||||
[IANA-PKA]
|
||||
IANA, "Secure Shell (SSH) Protocol Parameters",
|
||||
<https://www.iana.org/assignments/ssh-parameters/>.
|
||||
|
||||
Acknowledgments
|
||||
|
||||
Thanks to Jon Bright, Niels Moeller, Stephen Farrell, Mark D.
|
||||
Baushke, Jeffrey Hutzelman, Hanno Boeck, Peter Gutmann, Damien
|
||||
Miller, Mat Berchtold, Roumen Petrov, Daniel Migault, Eric Rescorla,
|
||||
Russ Housley, Alissa Cooper, Adam Roach, and Ben Campbell for
|
||||
reviews, comments, and suggestions.
|
||||
|
||||
Author's Address
|
||||
|
||||
Denis Bider
|
||||
Bitvise Limited
|
||||
4105 Lombardy Court
|
||||
Colleyville, Texas 76034
|
||||
United States of America
|
||||
|
||||
Email: ietf-ssh3@denisbider.com
|
||||
URI: https://www.bitvise.com/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bider Standards Track [Page 9]
|
||||
|
||||
317
specifications/rfc8709.txt
Normal file
317
specifications/rfc8709.txt
Normal file
@@ -0,0 +1,317 @@
|
||||
|
||||
|
||||
|
||||
|
||||
Internet Engineering Task Force (IETF) B. Harris
|
||||
Request for Comments: 8709
|
||||
Updates: 4253 L. Velvindron
|
||||
Category: Standards Track cyberstorm.mu
|
||||
ISSN: 2070-1721 February 2020
|
||||
|
||||
|
||||
Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH)
|
||||
Protocol
|
||||
|
||||
Abstract
|
||||
|
||||
This document describes the use of the Ed25519 and Ed448 digital
|
||||
signature algorithms in the Secure Shell (SSH) protocol.
|
||||
Accordingly, this RFC updates RFC 4253.
|
||||
|
||||
Status of This Memo
|
||||
|
||||
This is an Internet Standards Track document.
|
||||
|
||||
This document is a product of the Internet Engineering Task Force
|
||||
(IETF). It represents the consensus of the IETF community. It has
|
||||
received public review and has been approved for publication by the
|
||||
Internet Engineering Steering Group (IESG). Further information on
|
||||
Internet Standards is available in Section 2 of RFC 7841.
|
||||
|
||||
Information about the current status of this document, any errata,
|
||||
and how to provide feedback on it may be obtained at
|
||||
https://www.rfc-editor.org/info/rfc8709.
|
||||
|
||||
Copyright Notice
|
||||
|
||||
Copyright (c) 2020 IETF Trust and the persons identified as the
|
||||
document authors. All rights reserved.
|
||||
|
||||
This document is subject to BCP 78 and the IETF Trust's Legal
|
||||
Provisions Relating to IETF Documents
|
||||
(https://trustee.ietf.org/license-info) in effect on the date of
|
||||
publication of this document. Please review these documents
|
||||
carefully, as they describe your rights and restrictions with respect
|
||||
to this document. Code Components extracted from this document must
|
||||
include Simplified BSD License text as described in Section 4.e of
|
||||
the Trust Legal Provisions and are provided without warranty as
|
||||
described in the Simplified BSD License.
|
||||
|
||||
Table of Contents
|
||||
|
||||
1. Introduction
|
||||
2. Conventions Used in This Document
|
||||
2.1. Requirements Language
|
||||
3. Public Key Algorithm
|
||||
4. Public Key Format
|
||||
5. Signature Algorithm
|
||||
6. Signature Format
|
||||
7. Verification Algorithm
|
||||
8. SSHFP DNS Resource Records
|
||||
9. IANA Considerations
|
||||
10. Security Considerations
|
||||
11. References
|
||||
11.1. Normative References
|
||||
11.2. Informative References
|
||||
Acknowledgements
|
||||
Authors' Addresses
|
||||
|
||||
1. Introduction
|
||||
|
||||
Secure Shell (SSH) [RFC4251] is a secure remote-login protocol. It
|
||||
provides for an extensible variety of public key algorithms for
|
||||
identifying servers and users to one another. Ed25519 [RFC8032] is a
|
||||
digital signature system. OpenSSH 6.5 [OpenSSH-6.5] introduced
|
||||
support for using Ed25519 for server and user authentication and was
|
||||
then followed by other SSH implementations.
|
||||
|
||||
This document describes the method implemented by OpenSSH and others
|
||||
and formalizes the use of the name "ssh-ed25519". Additionally, this
|
||||
document describes the use of Ed448 and formalizes the use of the
|
||||
name "ssh-ed448".
|
||||
|
||||
2. Conventions Used in This Document
|
||||
|
||||
The descriptions of key and signature formats use the notation
|
||||
introduced in [RFC4251], Section 3 and the string data type from
|
||||
[RFC4251], Section 5.
|
||||
|
||||
2.1. Requirements Language
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
|
||||
"OPTIONAL" in this document are to be interpreted as described in
|
||||
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
|
||||
capitals, as shown here.
|
||||
|
||||
3. Public Key Algorithm
|
||||
|
||||
This document describes a public key algorithm for use with SSH, as
|
||||
per [RFC4253], Section 6.6. The name of the algorithm is "ssh-
|
||||
ed25519". This algorithm only supports signing and not encryption.
|
||||
|
||||
Additionally, this document describes another public key algorithm.
|
||||
The name of the algorithm is "ssh-ed448". This algorithm only
|
||||
supports signing and not encryption.
|
||||
|
||||
Standard implementations of SSH SHOULD implement these signature
|
||||
algorithms.
|
||||
|
||||
4. Public Key Format
|
||||
|
||||
The "ssh-ed25519" key format has the following encoding:
|
||||
|
||||
string "ssh-ed25519"
|
||||
|
||||
string key
|
||||
|
||||
Here, 'key' is the 32-octet public key described in [RFC8032],
|
||||
Section 5.1.5.
|
||||
|
||||
The "ssh-ed448" key format has the following encoding:
|
||||
|
||||
string "ssh-ed448"
|
||||
|
||||
string key
|
||||
|
||||
Here, 'key' is the 57-octet public key described in [RFC8032],
|
||||
Section 5.2.5.
|
||||
|
||||
5. Signature Algorithm
|
||||
|
||||
Signatures are generated according to the procedure in Sections 5.1.6
|
||||
and 5.2.6 of [RFC8032].
|
||||
|
||||
6. Signature Format
|
||||
|
||||
The "ssh-ed25519" key format has the following encoding:
|
||||
|
||||
string "ssh-ed25519"
|
||||
|
||||
string signature
|
||||
|
||||
Here, 'signature' is the 64-octet signature produced in accordance
|
||||
with [RFC8032], Section 5.1.6.
|
||||
|
||||
The "ssh-ed448" key format has the following encoding:
|
||||
|
||||
string "ssh-ed448"
|
||||
|
||||
string signature
|
||||
|
||||
Here, 'signature' is the 114-octet signature produced in accordance
|
||||
with [RFC8032], Section 5.2.6.
|
||||
|
||||
7. Verification Algorithm
|
||||
|
||||
Ed25519 signatures are verified according to the procedure in
|
||||
[RFC8032], Section 5.1.7.
|
||||
|
||||
Ed448 signatures are verified according to the procedure in
|
||||
[RFC8032], Section 5.2.7.
|
||||
|
||||
8. SSHFP DNS Resource Records
|
||||
|
||||
Usage and generation of the SSHFP DNS resource record is described in
|
||||
[RFC4255]. The generation of SSHFP resource records for "ssh-
|
||||
ed25519" keys is described in [RFC7479]. This section illustrates
|
||||
the generation of SSHFP resource records for "ssh-ed448" keys, and
|
||||
this document also specifies the corresponding Ed448 code point to
|
||||
"SSHFP RR Types for public key algorithms" in the "DNS SSHFP Resource
|
||||
Record Parameters" IANA registry [IANA-SSHFP].
|
||||
|
||||
The generation of SSHFP resource records for "ssh-ed448" keys is
|
||||
described as follows.
|
||||
|
||||
The encoding of Ed448 public keys is described in [ED448]. In brief,
|
||||
an Ed448 public key is a 57-octet value representing a 455-bit
|
||||
y-coordinate of an elliptic curve point, and a sign bit indicating
|
||||
the corresponding x-coordinate.
|
||||
|
||||
The SSHFP Resource Record for the Ed448 public key with SHA-256
|
||||
fingerprint would, for example, be:
|
||||
|
||||
example.com. IN SSHFP 6 2 ( a87f1b687ac0e57d2a081a2f2826723
|
||||
34d90ed316d2b818ca9580ea384d924
|
||||
01 )
|
||||
|
||||
The '2' here indicates SHA-256 [RFC6594].
|
||||
|
||||
9. IANA Considerations
|
||||
|
||||
This document augments the Public Key Algorithm Names in [RFC4250],
|
||||
Section 4.11.3.
|
||||
|
||||
IANA has added the following entry to "Public Key Algorithm Names" in
|
||||
the "Secure Shell (SSH) Protocol Parameters" registry [IANA-SSH]:
|
||||
|
||||
+---------------------------+-----------+
|
||||
| Public Key Algorithm Name | Reference |
|
||||
+===========================+===========+
|
||||
| ssh-ed25519 | RFC 8709 |
|
||||
+---------------------------+-----------+
|
||||
| ssh-ed448 | RFC 8709 |
|
||||
+---------------------------+-----------+
|
||||
|
||||
Table 1
|
||||
|
||||
IANA has added the following entry to "SSHFP RR Types for public key
|
||||
algorithms" in the "DNS SSHFP Resource Record Parameters" registry
|
||||
[IANA-SSHFP]:
|
||||
|
||||
+-------+-------------+-----------+
|
||||
| Value | Description | Reference |
|
||||
+=======+=============+===========+
|
||||
| 6 | Ed448 | RFC 8709 |
|
||||
+-------+-------------+-----------+
|
||||
|
||||
Table 2
|
||||
|
||||
10. Security Considerations
|
||||
|
||||
The security considerations in [RFC4251], Section 9 apply to all SSH
|
||||
implementations, including those using Ed25519 and Ed448.
|
||||
|
||||
The security considerations in [RFC8032], Section 8 and [RFC7479],
|
||||
Section 3 apply to all uses of Ed25519 and Ed448, including those in
|
||||
SSH.
|
||||
|
||||
11. References
|
||||
|
||||
11.1. Normative References
|
||||
|
||||
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
|
||||
Requirement Levels", BCP 14, RFC 2119,
|
||||
DOI 10.17487/RFC2119, March 1997,
|
||||
<https://www.rfc-editor.org/info/rfc2119>.
|
||||
|
||||
[RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Protocol Assigned Numbers", RFC 4250,
|
||||
DOI 10.17487/RFC4250, January 2006,
|
||||
<https://www.rfc-editor.org/info/rfc4250>.
|
||||
|
||||
[RFC4251] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Protocol Architecture", RFC 4251, DOI 10.17487/RFC4251,
|
||||
January 2006, <https://www.rfc-editor.org/info/rfc4251>.
|
||||
|
||||
[RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253,
|
||||
January 2006, <https://www.rfc-editor.org/info/rfc4253>.
|
||||
|
||||
[RFC4255] Schlyter, J. and W. Griffin, "Using DNS to Securely
|
||||
Publish Secure Shell (SSH) Key Fingerprints", RFC 4255,
|
||||
DOI 10.17487/RFC4255, January 2006,
|
||||
<https://www.rfc-editor.org/info/rfc4255>.
|
||||
|
||||
[RFC6594] Sury, O., "Use of the SHA-256 Algorithm with RSA, Digital
|
||||
Signature Algorithm (DSA), and Elliptic Curve DSA (ECDSA)
|
||||
in SSHFP Resource Records", RFC 6594,
|
||||
DOI 10.17487/RFC6594, April 2012,
|
||||
<https://www.rfc-editor.org/info/rfc6594>.
|
||||
|
||||
[RFC8032] Josefsson, S. and I. Liusvaara, "Edwards-Curve Digital
|
||||
Signature Algorithm (EdDSA)", RFC 8032,
|
||||
DOI 10.17487/RFC8032, January 2017,
|
||||
<https://www.rfc-editor.org/info/rfc8032>.
|
||||
|
||||
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
|
||||
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
|
||||
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
|
||||
|
||||
11.2. Informative References
|
||||
|
||||
[ED448] Hamburg, M., "Ed448-Goldilocks, a new elliptic curve",
|
||||
January 2015, <https://eprint.iacr.org/2015/625.pdf>.
|
||||
|
||||
[IANA-SSH] IANA, "Secure Shell (SSH) Protocol Parameters",
|
||||
<https://www.iana.org/assignments/ssh-parameters>.
|
||||
|
||||
[IANA-SSHFP]
|
||||
IANA, "DNS SSHFP Resource Record Parameters",
|
||||
<https://www.iana.org/assignments/dns-sshfp-rr-
|
||||
parameters>.
|
||||
|
||||
[OpenSSH-6.5]
|
||||
Friedl, M., Provos, N., de Raadt, T., Steves, K., Miller,
|
||||
D., Tucker, D., McIntyre, J., Rice, T., and B. Lindstrom,
|
||||
"OpenSSH 6.5 release notes", January 2014,
|
||||
<http://www.openssh.com/txt/release-6.5>.
|
||||
|
||||
[RFC7479] Moonesamy, S., "Using Ed25519 in SSHFP Resource Records",
|
||||
RFC 7479, DOI 10.17487/RFC7479, March 2015,
|
||||
<https://www.rfc-editor.org/info/rfc7479>.
|
||||
|
||||
Acknowledgements
|
||||
|
||||
The OpenSSH implementation of Ed25519 in SSH was written by Markus
|
||||
Friedl. We are also grateful to Mark Baushke, Benjamin Kaduk, and
|
||||
Daniel Migault for their comments.
|
||||
|
||||
Authors' Addresses
|
||||
|
||||
Ben Harris
|
||||
2A Eachard Road
|
||||
Cambridge
|
||||
CB3 0HY
|
||||
United Kingdom
|
||||
|
||||
Email: bjh21@bjh21.me.uk
|
||||
|
||||
|
||||
Loganaden Velvindron
|
||||
cyberstorm.mu
|
||||
88, Avenue De Plevitz
|
||||
Roches Brunes
|
||||
Mauritius
|
||||
|
||||
Email: logan@cyberstorm.mu
|
||||
196
specifications/rfc8758.txt
Normal file
196
specifications/rfc8758.txt
Normal file
@@ -0,0 +1,196 @@
|
||||
|
||||
|
||||
|
||||
|
||||
Internet Engineering Task Force (IETF) L. Velvindron
|
||||
Request for Comments: 8758 cyberstorm.mu
|
||||
BCP: 227 April 2020
|
||||
Updates: 4253
|
||||
Category: Best Current Practice
|
||||
ISSN: 2070-1721
|
||||
|
||||
|
||||
Deprecating RC4 in Secure Shell (SSH)
|
||||
|
||||
Abstract
|
||||
|
||||
This document deprecates RC4 in Secure Shell (SSH). Therefore, this
|
||||
document formally moves RFC 4345 to Historic status.
|
||||
|
||||
Status of This Memo
|
||||
|
||||
This memo documents an Internet Best Current Practice.
|
||||
|
||||
This document is a product of the Internet Engineering Task Force
|
||||
(IETF). It represents the consensus of the IETF community. It has
|
||||
received public review and has been approved for publication by the
|
||||
Internet Engineering Steering Group (IESG). Further information on
|
||||
BCPs is available in Section 2 of RFC 7841.
|
||||
|
||||
Information about the current status of this document, any errata,
|
||||
and how to provide feedback on it may be obtained at
|
||||
https://www.rfc-editor.org/info/rfc8758.
|
||||
|
||||
Copyright Notice
|
||||
|
||||
Copyright (c) 2020 IETF Trust and the persons identified as the
|
||||
document authors. All rights reserved.
|
||||
|
||||
This document is subject to BCP 78 and the IETF Trust's Legal
|
||||
Provisions Relating to IETF Documents
|
||||
(https://trustee.ietf.org/license-info) in effect on the date of
|
||||
publication of this document. Please review these documents
|
||||
carefully, as they describe your rights and restrictions with respect
|
||||
to this document. Code Components extracted from this document must
|
||||
include Simplified BSD License text as described in Section 4.e of
|
||||
the Trust Legal Provisions and are provided without warranty as
|
||||
described in the Simplified BSD License.
|
||||
|
||||
Table of Contents
|
||||
|
||||
1. Introduction
|
||||
1.1. Requirements Language
|
||||
2. Updates to RFC 4253
|
||||
3. IANA Considerations
|
||||
4. Security Considerations
|
||||
5. References
|
||||
5.1. Normative References
|
||||
5.2. Informative References
|
||||
Acknowledgements
|
||||
Author's Address
|
||||
|
||||
1. Introduction
|
||||
|
||||
The usage of RC4 suites (also designated as "arcfour") for SSH is
|
||||
specified in [RFC4253] and [RFC4345]. [RFC4253] specifies the
|
||||
allocation of the "arcfour" cipher for SSH. [RFC4345] specifies and
|
||||
allocates the "arcfour128" and "arcfour256" ciphers for SSH. RC4
|
||||
encryption has known weaknesses [RFC7465] [RFC8429]; therefore, this
|
||||
document starts the deprecation process for their use in Secure Shell
|
||||
(SSH) [RFC4253]. Accordingly, [RFC4253] is updated to note the
|
||||
deprecation of the RC4 ciphers, and [RFC4345] is moved to Historic
|
||||
status, as all ciphers it specifies MUST NOT be used.
|
||||
|
||||
1.1. Requirements Language
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
|
||||
"OPTIONAL" in this document are to be interpreted as described in
|
||||
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
|
||||
capitals, as shown here.
|
||||
|
||||
2. Updates to RFC 4253
|
||||
|
||||
[RFC4253] is updated to prohibit arcfour's use in SSH. [RFC4253],
|
||||
Section 6.3 allocates the "arcfour" cipher by defining a list of
|
||||
defined ciphers in which the "arcfour" cipher appears as optional, as
|
||||
shown in Table 1.
|
||||
|
||||
+---------+----------+----------------------------------------------+
|
||||
| arcfour | OPTIONAL | the ARCFOUR stream cipher |
|
||||
| | | with a 128-bit key |
|
||||
+---------+----------+----------------------------------------------+
|
||||
|
||||
Table 1
|
||||
|
||||
This document updates the status of the "arcfour" ciphers in the list
|
||||
found in [RFC4253], Section 6.3 by moving it from OPTIONAL to MUST
|
||||
NOT.
|
||||
|
||||
+---------+----------+----------------------------------------------+
|
||||
| arcfour | MUST NOT | the ARCFOUR stream cipher |
|
||||
| | | with a 128-bit key |
|
||||
+---------+----------+----------------------------------------------+
|
||||
|
||||
Table 2
|
||||
|
||||
[RFC4253] defines the "arcfour" ciphers with the following text:
|
||||
|
||||
| The "arcfour" cipher is the Arcfour stream cipher with 128-bit
|
||||
| keys. The Arcfour cipher is believed to be compatible with the
|
||||
| RC4 cipher [SCHNEIER]. Arcfour (and RC4) has problems with weak
|
||||
| keys, and should be used with caution.
|
||||
|
||||
This document updates [RFC4253], Section 6.3 by replacing the text
|
||||
above with the following text:
|
||||
|
||||
| The "arcfour" cipher is the Arcfour stream cipher with 128-bit
|
||||
| keys. The Arcfour cipher is compatible with the RC4 cipher
|
||||
| [SCHNEIER]. Arcfour (and RC4) has known weaknesses [RFC7465]
|
||||
| [RFC8429] and MUST NOT be used.
|
||||
|
||||
3. IANA Considerations
|
||||
|
||||
The IANA has updated the "Encryption Algorithm Names" subregistry in
|
||||
the "Secure Shell (SSH) Protocol Parameters" registry [IANA]. The
|
||||
registration procedure is IETF review, which is achieved by this
|
||||
document. The registry has been updated as follows:
|
||||
|
||||
+---------------------------+-----------+----------+
|
||||
| Encryption Algorithm Name | Reference | Note |
|
||||
+===========================+===========+==========+
|
||||
| arcfour | RFC 8758 | HISTORIC |
|
||||
+---------------------------+-----------+----------+
|
||||
| arcfour128 | RFC 8758 | HISTORIC |
|
||||
+---------------------------+-----------+----------+
|
||||
| arcfour256 | RFC 8758 | HISTORIC |
|
||||
+---------------------------+-----------+----------+
|
||||
|
||||
Table 3
|
||||
|
||||
4. Security Considerations
|
||||
|
||||
This document only prohibits the use of RC4 in SSH; it introduces no
|
||||
new security considerations.
|
||||
|
||||
5. References
|
||||
|
||||
5.1. Normative References
|
||||
|
||||
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
|
||||
Requirement Levels", BCP 14, RFC 2119,
|
||||
DOI 10.17487/RFC2119, March 1997,
|
||||
<https://www.rfc-editor.org/info/rfc2119>.
|
||||
|
||||
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
|
||||
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
|
||||
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
|
||||
|
||||
5.2. Informative References
|
||||
|
||||
[IANA] "Secure Shell (SSH) Protocol Parameters",
|
||||
<https://www.iana.org/assignments/ssh-parameters>.
|
||||
|
||||
[RFC4253] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Transport Layer Protocol", RFC 4253, DOI 10.17487/RFC4253,
|
||||
January 2006, <https://www.rfc-editor.org/info/rfc4253>.
|
||||
|
||||
[RFC4345] Harris, B., "Improved Arcfour Modes for the Secure Shell
|
||||
(SSH) Transport Layer Protocol", RFC 4345,
|
||||
DOI 10.17487/RFC4345, January 2006,
|
||||
<https://www.rfc-editor.org/info/rfc4345>.
|
||||
|
||||
[RFC7465] Popov, A., "Prohibiting RC4 Cipher Suites", RFC 7465,
|
||||
DOI 10.17487/RFC7465, February 2015,
|
||||
<https://www.rfc-editor.org/info/rfc7465>.
|
||||
|
||||
[RFC8429] Kaduk, B. and M. Short, "Deprecate Triple-DES (3DES) and
|
||||
RC4 in Kerberos", BCP 218, RFC 8429, DOI 10.17487/RFC8429,
|
||||
October 2018, <https://www.rfc-editor.org/info/rfc8429>.
|
||||
|
||||
[SCHNEIER] Schneier, B., "Applied Cryptography Second Edition:
|
||||
Protocols, Algorithms, and Source in Code in C", John
|
||||
Wiley and Sons New York, NY, 1996.
|
||||
|
||||
Acknowledgements
|
||||
|
||||
The author would like to thank Eric Rescorla, Daniel Migault, and
|
||||
Rich Salz.
|
||||
|
||||
Author's Address
|
||||
|
||||
Loganaden Velvindron
|
||||
cyberstorm.mu
|
||||
Mauritius
|
||||
|
||||
Email: logan@cyberstorm.mu
|
||||
1028
specifications/rfc9142.txt
Normal file
1028
specifications/rfc9142.txt
Normal file
File diff suppressed because it is too large
Load Diff
272
specifications/rfc9519.txt
Normal file
272
specifications/rfc9519.txt
Normal file
@@ -0,0 +1,272 @@
|
||||
|
||||
|
||||
|
||||
|
||||
Internet Engineering Task Force (IETF) P. Yee
|
||||
Request for Comments: 9519 AKAYLA
|
||||
Updates: 4250, 4716, 4819, 8308 January 2024
|
||||
Category: Standards Track
|
||||
ISSN: 2070-1721
|
||||
|
||||
|
||||
Update to the IANA SSH Protocol Parameters Registry Requirements
|
||||
|
||||
Abstract
|
||||
|
||||
This specification updates the registration policies for adding new
|
||||
entries to registries within the IANA "Secure Shell (SSH) Protocol
|
||||
Parameters" group of registries. Previously, the registration policy
|
||||
was generally IETF Review, as defined in RFC 8126, although a few
|
||||
registries require Standards Action. This specification changes it
|
||||
from IETF Review to Expert Review. This document updates RFCs 4250,
|
||||
4716, 4819, and 8308.
|
||||
|
||||
Status of This Memo
|
||||
|
||||
This is an Internet Standards Track document.
|
||||
|
||||
This document is a product of the Internet Engineering Task Force
|
||||
(IETF). It represents the consensus of the IETF community. It has
|
||||
received public review and has been approved for publication by the
|
||||
Internet Engineering Steering Group (IESG). Further information on
|
||||
Internet Standards is available in Section 2 of RFC 7841.
|
||||
|
||||
Information about the current status of this document, any errata,
|
||||
and how to provide feedback on it may be obtained at
|
||||
https://www.rfc-editor.org/info/rfc9519.
|
||||
|
||||
Copyright Notice
|
||||
|
||||
Copyright (c) 2024 IETF Trust and the persons identified as the
|
||||
document authors. All rights reserved.
|
||||
|
||||
This document is subject to BCP 78 and the IETF Trust's Legal
|
||||
Provisions Relating to IETF Documents
|
||||
(https://trustee.ietf.org/license-info) in effect on the date of
|
||||
publication of this document. Please review these documents
|
||||
carefully, as they describe your rights and restrictions with respect
|
||||
to this document. Code Components extracted from this document must
|
||||
include Revised BSD License text as described in Section 4.e of the
|
||||
Trust Legal Provisions and are provided without warranty as described
|
||||
in the Revised BSD License.
|
||||
|
||||
Table of Contents
|
||||
|
||||
1. Introduction
|
||||
1.1. Requirements Language
|
||||
2. SSH Protocol Parameters Affected
|
||||
3. Designated Expert Pool
|
||||
4. IANA Considerations
|
||||
5. Security Considerations
|
||||
6. References
|
||||
6.1. Normative References
|
||||
6.2. Informative References
|
||||
Acknowledgements
|
||||
Author's Address
|
||||
|
||||
1. Introduction
|
||||
|
||||
The IANA "Secure Shell (SSH) Protocol Parameters" registry was
|
||||
populated by several RFCs including [RFC4250], [RFC4716], [RFC4819],
|
||||
and [RFC8308]. Outside of some narrow value ranges that require
|
||||
Standards Action in order to add new values or that are marked for
|
||||
Private Use, the registration policy for other portions of the
|
||||
registry was IETF Review [RFC8126]. This specification changes the
|
||||
policy from IETF Review to Expert Review. This change is in line
|
||||
with similar changes undertaken for certain IPsec and TLS registries.
|
||||
|
||||
1.1. Requirements Language
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
|
||||
"OPTIONAL" in this document are to be interpreted as described in
|
||||
BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all
|
||||
capitals, as shown here.
|
||||
|
||||
2. SSH Protocol Parameters Affected
|
||||
|
||||
The following table lists the "Secure Shell (SSH) Protocol
|
||||
Parameters" registries whose registration policy has changed from
|
||||
IETF Review to Expert Review. Where this change applied to a
|
||||
specific range of values within the particular parameter, that range
|
||||
is given in the notes column. Affected registries now list this
|
||||
document as a reference.
|
||||
|
||||
+===============================+===========+=======================+
|
||||
| Parameter Name | RFC | Notes |
|
||||
+===============================+===========+=======================+
|
||||
| Authentication Method | [RFC4250] | |
|
||||
| Names | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Channel Connection | [RFC4250] | 0x00000001-0xFDFFFFFF |
|
||||
| Failure Reason Codes | | (inclusive) |
|
||||
| and Descriptions | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Compression Algorithm | [RFC4250] | |
|
||||
| Names | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Connection Protocol | [RFC4250] | |
|
||||
| Channel Request Names | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Connection Protocol | [RFC4250] | |
|
||||
| Channel Types | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Connection Protocol | [RFC4250] | |
|
||||
| Global Request Names | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Connection Protocol | [RFC4250] | |
|
||||
| Subsystem Names | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Disconnection Messages | [RFC4250] | 0x00000001-0xFDFFFFFF |
|
||||
| Reason Codes and | | (inclusive) |
|
||||
| Descriptions | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Encryption Algorithm | [RFC4250] | |
|
||||
| Names | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Extended Channel Data | [RFC4250] | 0x00000001-0xFDFFFFFF |
|
||||
| Transfer data_type_code | | (inclusive) |
|
||||
| and Data | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Extension Names | [RFC8308] | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Key Exchange Method | [RFC4250] | |
|
||||
| Names | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| MAC Algorithm Names | [RFC4250] | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Pseudo-Terminal Encoded | [RFC4250] | |
|
||||
| Terminal Modes | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Public Key Algorithm | [RFC4250] | |
|
||||
| Names | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Publickey Subsystem | [RFC4819] | |
|
||||
| Attributes | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Publickey Subsystem | [RFC4819] | |
|
||||
| Request Names | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Publickey Subsystem | [RFC4819] | |
|
||||
| Response Names | | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Service Names | [RFC4250] | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| Signal Names | [RFC4250] | |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
| SSH Public-Key File | [RFC4716] | Excluding header-tags |
|
||||
| Header Tags | | beginning with x- |
|
||||
+-------------------------------+-----------+-----------------------+
|
||||
|
||||
Table 1: Secure Shell (SSH) Protocol Parameters Affected
|
||||
|
||||
The only IANA SSH protocol parameter registries not affected are
|
||||
"Message Numbers" and "Publickey Subsystem Status Codes", as these
|
||||
remain Standards Action due to their limited resources as one-byte
|
||||
registry values.
|
||||
|
||||
3. Designated Expert Pool
|
||||
|
||||
Expert Review [RFC8126] registry requests are registered after a
|
||||
three-week review period on the <ssh-reg-review@ietf.org> mailing
|
||||
list, and on the advice of one or more designated experts. However,
|
||||
to allow for the allocation of values prior to publication, the
|
||||
designated experts may approve registration once they are satisfied
|
||||
that such a specification will be published.
|
||||
|
||||
Registration requests sent to the mailing list for review SHOULD use
|
||||
an appropriate subject (e.g., "Request to register value in SSH
|
||||
protocol parameters <specific parameter> registry").
|
||||
|
||||
Within the review period, the designated experts will either approve
|
||||
or deny the registration request, communicating this decision to the
|
||||
review list and IANA. Denials MUST include an explanation and, if
|
||||
applicable, suggestions as to how to make the request successful.
|
||||
Registration requests that are undetermined for a period longer than
|
||||
21 days can be brought to the IESG's attention (using the
|
||||
<iesg@ietf.org> mailing list) for resolution.
|
||||
|
||||
Criteria that SHOULD be applied by the designated experts includes
|
||||
determining whether the proposed registration duplicates existing
|
||||
functionality (which is not permitted), whether it is likely to be of
|
||||
general applicability or useful only for a single application, and
|
||||
whether the registration description is clear.
|
||||
|
||||
IANA MUST only accept registry updates from the designated experts
|
||||
and the IESG. It SHOULD direct all requests for registration from
|
||||
other sources to the review mailing list.
|
||||
|
||||
It is suggested that multiple designated experts be appointed who are
|
||||
able to represent the perspectives of different applications using
|
||||
this specification, in order to enable broadly informed review of
|
||||
registration decisions. In cases where a registration decision could
|
||||
be perceived as creating a conflict of interest for a particular
|
||||
expert, that expert SHOULD defer to the judgment of the other
|
||||
experts.
|
||||
|
||||
4. IANA Considerations
|
||||
|
||||
This memo is entirely about updating the IANA "Secure Shell (SSH)
|
||||
Protocol Parameters" registry.
|
||||
|
||||
5. Security Considerations
|
||||
|
||||
This memo does not change the Security Considerations for any of the
|
||||
updated RFCs.
|
||||
|
||||
6. References
|
||||
|
||||
6.1. Normative References
|
||||
|
||||
[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate
|
||||
Requirement Levels", BCP 14, RFC 2119,
|
||||
DOI 10.17487/RFC2119, March 1997,
|
||||
<https://www.rfc-editor.org/info/rfc2119>.
|
||||
|
||||
[RFC4250] Lehtinen, S. and C. Lonvick, Ed., "The Secure Shell (SSH)
|
||||
Protocol Assigned Numbers", RFC 4250,
|
||||
DOI 10.17487/RFC4250, January 2006,
|
||||
<https://www.rfc-editor.org/info/rfc4250>.
|
||||
|
||||
[RFC4819] Galbraith, J., Van Dyke, J., and J. Bright, "Secure Shell
|
||||
Public Key Subsystem", RFC 4819, DOI 10.17487/RFC4819,
|
||||
March 2007, <https://www.rfc-editor.org/info/rfc4819>.
|
||||
|
||||
[RFC8126] Cotton, M., Leiba, B., and T. Narten, "Guidelines for
|
||||
Writing an IANA Considerations Section in RFCs", BCP 26,
|
||||
RFC 8126, DOI 10.17487/RFC8126, June 2017,
|
||||
<https://www.rfc-editor.org/info/rfc8126>.
|
||||
|
||||
[RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
|
||||
2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
|
||||
May 2017, <https://www.rfc-editor.org/info/rfc8174>.
|
||||
|
||||
[RFC8308] Bider, D., "Extension Negotiation in the Secure Shell
|
||||
(SSH) Protocol", RFC 8308, DOI 10.17487/RFC8308, March
|
||||
2018, <https://www.rfc-editor.org/info/rfc8308>.
|
||||
|
||||
6.2. Informative References
|
||||
|
||||
[CURDLE-MA]
|
||||
Turner, S., "Subject: [Curdle] Time to Review IANA SSH
|
||||
Registries Policies?", message to the Curdle mailing list,
|
||||
February 2021,
|
||||
<https://mailarchive.ietf.org/arch/msg/curdle/
|
||||
gdiOlZr9bnrZv8umVyguGG3woIM/>.
|
||||
|
||||
[RFC4716] Galbraith, J. and R. Thayer, "The Secure Shell (SSH)
|
||||
Public Key File Format", RFC 4716, DOI 10.17487/RFC4716,
|
||||
November 2006, <https://www.rfc-editor.org/info/rfc4716>.
|
||||
|
||||
Acknowledgements
|
||||
|
||||
The impetus for this specification was a February 2021 discussion on
|
||||
the CURDLE mailing list [CURDLE-MA].
|
||||
|
||||
Author's Address
|
||||
|
||||
Peter E. Yee
|
||||
AKAYLA
|
||||
Mountain View, CA 94043
|
||||
United States of America
|
||||
Email: peter@akayla.com
|
||||
18
ssh/Cargo.toml
Normal file
18
ssh/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "ssh"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bytes = { workspace = true }
|
||||
configuration = { workspace = true }
|
||||
error-stack = { workspace = true }
|
||||
getrandom = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
num_enum = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
proptest-derive = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
428
ssh/src/channel.rs
Normal file
428
ssh/src/channel.rs
Normal file
@@ -0,0 +1,428 @@
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use proptest::arbitrary::Arbitrary;
|
||||
use proptest::strategy::{BoxedStrategy, Strategy};
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
const MAX_BUFFER_SIZE: usize = 64 * 1024;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SshPacket {
|
||||
pub buffer: Bytes,
|
||||
}
|
||||
|
||||
pub struct SshChannel<Stream> {
|
||||
read_side: Mutex<ReadSide<Stream>>,
|
||||
write_side: Mutex<WriteSide<Stream>>,
|
||||
cipher_block_size: usize,
|
||||
mac_length: usize,
|
||||
channel_is_closed: bool,
|
||||
}
|
||||
|
||||
struct ReadSide<Stream> {
|
||||
stream: ReadHalf<Stream>,
|
||||
buffer: BytesMut,
|
||||
}
|
||||
|
||||
struct WriteSide<Stream> {
|
||||
stream: WriteHalf<Stream>,
|
||||
buffer: BytesMut,
|
||||
rng: ChaCha20Rng,
|
||||
}
|
||||
|
||||
impl<Stream> SshChannel<Stream>
|
||||
where
|
||||
Stream: AsyncReadExt + AsyncWriteExt,
|
||||
Stream: Send + Sync,
|
||||
Stream: Unpin,
|
||||
{
|
||||
/// Create a new SSH channel.
|
||||
///
|
||||
/// SshChannels are designed to make sure the various SSH channel read /
|
||||
/// write operations are cancel- and concurrency-safe. They take ownership
|
||||
/// of the underlying stream once established, where "established" means
|
||||
/// that we have read and written the initial SSH banners.
|
||||
pub fn new(stream: Stream) -> Result<SshChannel<Stream>, getrandom::Error> {
|
||||
let (read_half, write_half) = tokio::io::split(stream);
|
||||
|
||||
Ok(SshChannel {
|
||||
read_side: Mutex::new(ReadSide {
|
||||
stream: read_half,
|
||||
buffer: BytesMut::with_capacity(MAX_BUFFER_SIZE),
|
||||
}),
|
||||
write_side: Mutex::new(WriteSide {
|
||||
stream: write_half,
|
||||
buffer: BytesMut::with_capacity(MAX_BUFFER_SIZE),
|
||||
rng: ChaCha20Rng::try_from_os_rng()?,
|
||||
}),
|
||||
mac_length: 0,
|
||||
cipher_block_size: 8,
|
||||
channel_is_closed: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read an SshPacket from the wire.
|
||||
///
|
||||
/// This function is cancel safe, and can be used in `select` (or similar)
|
||||
/// without problems. It is also safe to be used in a multitasking setting.
|
||||
/// Returns Ok(Some(...)) if a packet is found, or Ok(None) if we have
|
||||
/// successfully reached the end of stream. It will also return Ok(None)
|
||||
/// repeatedly after the stream is closed.
|
||||
pub async fn read(&self) -> Result<Option<SshPacket>, std::io::Error> {
|
||||
if self.channel_is_closed {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut reader = self.read_side.lock().await;
|
||||
let mut local_buffer = vec![0; 4096];
|
||||
|
||||
// First, let's try to at least get a size in there.
|
||||
while reader.buffer.len() < 5 {
|
||||
let amt_read = reader.stream.read(&mut local_buffer).await?;
|
||||
reader.buffer.extend_from_slice(&local_buffer[0..amt_read]);
|
||||
}
|
||||
|
||||
let packet_size = ((reader.buffer[0] as usize) << 24)
|
||||
| ((reader.buffer[1] as usize) << 16)
|
||||
| ((reader.buffer[2] as usize) << 8)
|
||||
| reader.buffer[3] as usize;
|
||||
let padding_size = reader.buffer[4] as usize;
|
||||
let total_size = 4 + 1 + packet_size + padding_size + self.mac_length;
|
||||
tracing::trace!(
|
||||
packet_size,
|
||||
padding_size,
|
||||
total_size,
|
||||
"Initial packet information determined"
|
||||
);
|
||||
|
||||
// Now we need to make sure that the buffer contains at least that
|
||||
// many bytes. We do this transfer -- from the wire to an internal
|
||||
// buffer -- to ensure cancel safety. If, at any point, this computation
|
||||
// is cancelled, the lock will be released and the buffer will be in
|
||||
// a reasonable place. A subsequent call should be able to pick up
|
||||
// wherever we left off.
|
||||
while reader.buffer.len() < total_size {
|
||||
let amt_read = reader.stream.read(&mut local_buffer).await?;
|
||||
reader.buffer.extend_from_slice(&local_buffer[0..amt_read]);
|
||||
}
|
||||
|
||||
let mut new_packet = reader.buffer.split_to(total_size);
|
||||
|
||||
let _header = new_packet.split_to(5);
|
||||
let payload = new_packet.split_to(total_size - padding_size - 5);
|
||||
let _mac = new_packet.split_off(padding_size);
|
||||
|
||||
Ok(Some(SshPacket {
|
||||
buffer: payload.freeze(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn encode(&self, rng: &mut ChaCha20Rng, packet: SshPacket) -> Option<Bytes> {
|
||||
let mut encoded_packet = BytesMut::new();
|
||||
|
||||
// Arbitrary-length padding, such that the total length of
|
||||
// (packet_length || padding_length || payload || random padding)
|
||||
// is a multiple of the cipher block size or 8, whichever is
|
||||
// larger. There MUST be at least four bytes of padding. The
|
||||
// padding SHOULD consist of random bytes. The maximum amount of
|
||||
// padding is 255 bytes.
|
||||
let paddingless_length = 4 + 1 + packet.buffer.len();
|
||||
// the padding we need to get to an even multiple of the cipher
|
||||
// block size, naturally jumping to the cipher block size if we're
|
||||
// already aligned. (this is just easier, and since we can't have
|
||||
// 0 as the final padding, seems reasonable to do.)
|
||||
let mut rounded_padding =
|
||||
self.cipher_block_size - (paddingless_length % self.cipher_block_size);
|
||||
// now we enforce the must be greater than or equal to 4 rule
|
||||
if rounded_padding < 4 {
|
||||
rounded_padding += self.cipher_block_size;
|
||||
}
|
||||
// if this ends up being > 256, then we've run into something terrible
|
||||
if rounded_padding > (u8::MAX as usize) {
|
||||
tracing::error!(
|
||||
payload_length = packet.buffer.len(),
|
||||
cipher_block_size = ?self.cipher_block_size,
|
||||
computed_padding = ?rounded_padding,
|
||||
"generated incoherent padding value in write"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
encoded_packet.put_u32(packet.buffer.len() as u32);
|
||||
encoded_packet.put_u8(rounded_padding as u8);
|
||||
encoded_packet.put(packet.buffer);
|
||||
|
||||
for _ in 0..rounded_padding {
|
||||
encoded_packet.put_u8(rng.random());
|
||||
}
|
||||
|
||||
Some(encoded_packet.freeze())
|
||||
}
|
||||
|
||||
/// Write an SshPacket to the wire.
|
||||
///
|
||||
/// This function is cancel safe, and can be used in `select` (or similar).
|
||||
/// By cancel safe, we mean that one of the following outcomes is guaranteed
|
||||
/// to occur if the operation is cancelled:
|
||||
///
|
||||
/// 1. The whole packet is written to the channel.
|
||||
/// 2. No part of the packet is written to the channel.
|
||||
/// 3. The channel is dead, and no further data can be written to it.
|
||||
///
|
||||
/// Note that this means that you cannot assume that the packet is not
|
||||
/// written if the operation is cancelled, it just ensures that you will
|
||||
/// not be in a place in which only part of the packet has been written.
|
||||
pub async fn write(&self, packet: SshPacket) -> Result<(), std::io::Error> {
|
||||
let mut final_data = { self.encode(&mut self.write_side.lock().await.rng, packet) };
|
||||
|
||||
loop {
|
||||
if self.channel_is_closed {
|
||||
return Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
|
||||
}
|
||||
|
||||
let mut writer = self.write_side.lock().await;
|
||||
|
||||
if let Some(bytes) = final_data.take() {
|
||||
if bytes.len() + writer.buffer.len() < MAX_BUFFER_SIZE {
|
||||
writer.buffer.put(bytes);
|
||||
} else if !bytes.is_empty() {
|
||||
final_data = Some(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_buffer = std::mem::take(&mut writer.buffer);
|
||||
let _written = writer.stream.write_buf(&mut current_buffer).await?;
|
||||
writer.buffer = current_buffer;
|
||||
|
||||
if writer.buffer.is_empty() && final_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for SshPacket {
|
||||
type Parameters = bool;
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(start_with_real_message: Self::Parameters) -> Self::Strategy {
|
||||
if start_with_real_message {
|
||||
unimplemented!()
|
||||
} else {
|
||||
let data = proptest::collection::vec(u8::arbitrary(), 0..35000);
|
||||
data.prop_map(|x| SshPacket { buffer: x.into() }).boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum SendData {
|
||||
Left(SshPacket),
|
||||
Right(SshPacket),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Arbitrary for SendData {
|
||||
type Parameters = <SshPacket as Arbitrary>::Parameters;
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
|
||||
(bool::arbitrary(), SshPacket::arbitrary_with(args))
|
||||
.prop_map(|(is_left, packet)| {
|
||||
if is_left {
|
||||
SendData::Left(packet)
|
||||
} else {
|
||||
SendData::Right(packet)
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn can_read_back_anything(packet in SshPacket::arbitrary()) {
|
||||
let result = tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let (left, right) = tokio::io::duplex(8192);
|
||||
let leftssh = SshChannel::new(left).unwrap();
|
||||
let rightssh = SshChannel::new(right).unwrap();
|
||||
let packet_copy = packet.clone();
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
leftssh.write(packet_copy).await.unwrap();
|
||||
});
|
||||
rightssh.read().await.unwrap()
|
||||
});
|
||||
|
||||
assert_eq!(packet, result.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequences_send_correctly_serial(sequence in proptest::collection::vec(SendData::arbitrary(), 0..100)) {
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let (left, right) = tokio::io::duplex(8192);
|
||||
let leftssh = SshChannel::new(left).unwrap();
|
||||
let rightssh = SshChannel::new(right).unwrap();
|
||||
|
||||
let sequence_left = sequence.clone();
|
||||
let sequence_right = sequence;
|
||||
|
||||
let left_task = tokio::task::spawn(async move {
|
||||
let mut errored = false;
|
||||
|
||||
for item in sequence_left.into_iter() {
|
||||
match item {
|
||||
SendData::Left(packet) => {
|
||||
let result = leftssh.write(packet).await;
|
||||
errored = result.is_err();
|
||||
}
|
||||
|
||||
SendData::Right(packet) => {
|
||||
if let Ok(Some(item)) = leftssh.read().await {
|
||||
errored = item != packet;
|
||||
} else {
|
||||
errored = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errored {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
!errored
|
||||
});
|
||||
|
||||
let right_task = tokio::task::spawn(async move {
|
||||
let mut errored = false;
|
||||
|
||||
for item in sequence_right.into_iter() {
|
||||
match item {
|
||||
SendData::Right(packet) => {
|
||||
let result = rightssh.write(packet).await;
|
||||
errored = result.is_err();
|
||||
}
|
||||
|
||||
SendData::Left(packet) => {
|
||||
if let Ok(Some(item)) = rightssh.read().await {
|
||||
errored = item != packet;
|
||||
} else {
|
||||
errored = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errored {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
!errored
|
||||
});
|
||||
|
||||
assert!(left_task.await.unwrap());
|
||||
assert!(right_task.await.unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequences_send_correctly_parallel(sequence in proptest::collection::vec(SendData::arbitrary(), 0..100)) {
|
||||
use std::sync::Arc;
|
||||
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let (left, right) = tokio::io::duplex(8192);
|
||||
let leftsshw = Arc::new(SshChannel::new(left).unwrap());
|
||||
let leftsshr = leftsshw.clone();
|
||||
let rightsshw = Arc::new(SshChannel::new(right).unwrap());
|
||||
let rightsshr = rightsshw.clone();
|
||||
|
||||
let sequence_left_write = sequence.clone();
|
||||
let sequence_left_read = sequence.clone();
|
||||
let sequence_right_write = sequence.clone();
|
||||
let sequence_right_read = sequence.clone();
|
||||
|
||||
let left_task_write = tokio::task::spawn(async move {
|
||||
let mut errored = false;
|
||||
|
||||
for item in sequence_left_write.into_iter() {
|
||||
if let SendData::Left(packet) = item {
|
||||
let result = leftsshw.write(packet).await;
|
||||
errored = result.is_err();
|
||||
}
|
||||
|
||||
if errored {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
!errored
|
||||
});
|
||||
|
||||
let right_task_write = tokio::task::spawn(async move {
|
||||
let mut errored = false;
|
||||
|
||||
for item in sequence_right_write.into_iter() {
|
||||
if let SendData::Right(packet) = item {
|
||||
let result = rightsshw.write(packet).await;
|
||||
errored = result.is_err();
|
||||
}
|
||||
|
||||
if errored {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
!errored
|
||||
});
|
||||
|
||||
let left_task_read = tokio::task::spawn(async move {
|
||||
let mut errored = false;
|
||||
|
||||
for item in sequence_left_read.into_iter() {
|
||||
if let SendData::Right(packet) = item {
|
||||
if let Ok(Some(item)) = leftsshr.read().await {
|
||||
errored = item != packet;
|
||||
} else {
|
||||
errored = true;
|
||||
}
|
||||
}
|
||||
|
||||
if errored {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
!errored
|
||||
});
|
||||
|
||||
let right_task_read = tokio::task::spawn(async move {
|
||||
let mut errored = false;
|
||||
|
||||
for item in sequence_right_read.into_iter() {
|
||||
if let SendData::Left(packet) = item {
|
||||
if let Ok(Some(item)) = rightsshr.read().await {
|
||||
errored = item != packet;
|
||||
} else {
|
||||
errored = true;
|
||||
}
|
||||
}
|
||||
|
||||
if errored {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
!errored
|
||||
});
|
||||
|
||||
assert!(left_task_write.await.unwrap());
|
||||
assert!(right_task_write.await.unwrap());
|
||||
assert!(left_task_read.await.unwrap());
|
||||
assert!(right_task_read.await.unwrap());
|
||||
});
|
||||
}
|
||||
}
|
||||
11
ssh/src/lib.rs
Normal file
11
ssh/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
mod channel;
|
||||
mod message_ids;
|
||||
mod operational_error;
|
||||
mod packets;
|
||||
mod preamble;
|
||||
|
||||
pub use channel::SshChannel;
|
||||
pub use message_ids::SshMessageID;
|
||||
pub use operational_error::OperationalError;
|
||||
pub use packets::{SshKeyExchange, SshKeyExchangeProcessingError};
|
||||
pub use preamble::Preamble;
|
||||
173
ssh/src/message_ids.rs
Normal file
173
ssh/src/message_ids.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use crate::operational_error::OperationalError;
|
||||
use num_enum::{FromPrimitive, IntoPrimitive};
|
||||
use proptest::arbitrary::Arbitrary;
|
||||
use proptest::strategy::{BoxedStrategy, Just, Strategy};
|
||||
use std::fmt;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, FromPrimitive, IntoPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum SshMessageID {
|
||||
SSH_MSG_DISCONNECT = 1,
|
||||
SSH_MSG_IGNORE = 2,
|
||||
SSH_MSG_UNIMPLEMENTED = 3,
|
||||
SSH_MSG_DEBUG = 4,
|
||||
SSH_MSG_SERVICE_REQUEST = 5,
|
||||
SSH_MSG_SERVICE_ACCEPT = 6,
|
||||
SSH_MSG_KEXINIT = 20,
|
||||
SSH_MSG_NEWKEYS = 21,
|
||||
SSH_MSG_USERAUTH_REQUEST = 50,
|
||||
SSH_MSG_USERAUTH_FAILURE = 51,
|
||||
SSH_MSG_USERAUTH_SUCCESS = 52,
|
||||
SSH_MSG_USERAUTH_BANNER = 53,
|
||||
SSH_MSG_GLOBAL_REQUEST = 80,
|
||||
SSH_MSG_REQUEST_SUCCESS = 81,
|
||||
SSH_MSG_REQUEST_FAILURE = 82,
|
||||
SSH_MSG_CHANNEL_OPEN = 90,
|
||||
SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91,
|
||||
SSH_MSG_CHANNEL_OPEN_FAILURE = 92,
|
||||
SSH_MSG_CHANNEL_WINDOW_ADJUST = 93,
|
||||
SSH_MSG_CHANNEL_DATA = 94,
|
||||
SSH_MSG_CHANNEL_EXTENDED_DATA = 95,
|
||||
SSH_MSG_CHANNEL_EOF = 96,
|
||||
SSH_MSG_CHANNEL_CLOSE = 97,
|
||||
SSH_MSG_CHANNEL_REQUEST = 98,
|
||||
SSH_MSG_CHANNEL_SUCCESS = 99,
|
||||
SSH_MSG_CHANNEL_FAILURE = 100,
|
||||
#[num_enum(catch_all)]
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
impl fmt::Display for SshMessageID {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SshMessageID::SSH_MSG_DISCONNECT => write!(f, "SSH_MSG_DISCONNECT"),
|
||||
SshMessageID::SSH_MSG_IGNORE => write!(f, "SSH_MSG_IGNORE"),
|
||||
SshMessageID::SSH_MSG_UNIMPLEMENTED => write!(f, "SSH_MSG_UNIMPLEMENTED"),
|
||||
SshMessageID::SSH_MSG_DEBUG => write!(f, "SSH_MSG_DEBUG"),
|
||||
SshMessageID::SSH_MSG_SERVICE_REQUEST => write!(f, "SSH_MSG_SERVICE_REQUEST"),
|
||||
SshMessageID::SSH_MSG_SERVICE_ACCEPT => write!(f, "SSH_MSG_SERVICE_ACCEPT"),
|
||||
SshMessageID::SSH_MSG_KEXINIT => write!(f, "SSH_MSG_KEXINIT"),
|
||||
SshMessageID::SSH_MSG_NEWKEYS => write!(f, "SSH_MSG_NEWKEYS"),
|
||||
SshMessageID::SSH_MSG_USERAUTH_REQUEST => write!(f, "SSH_MSG_USERAUTH_REQUEST"),
|
||||
SshMessageID::SSH_MSG_USERAUTH_FAILURE => write!(f, "SSH_MSG_USERAUTH_FAILURE"),
|
||||
SshMessageID::SSH_MSG_USERAUTH_SUCCESS => write!(f, "SSH_MSG_USERAUTH_SUCCESS"),
|
||||
SshMessageID::SSH_MSG_USERAUTH_BANNER => write!(f, "SSH_MSG_USERAUTH_BANNER"),
|
||||
SshMessageID::SSH_MSG_GLOBAL_REQUEST => write!(f, "SSH_MSG_GLOBAL_REQUEST"),
|
||||
SshMessageID::SSH_MSG_REQUEST_SUCCESS => write!(f, "SSH_MSG_REQUEST_SUCCESS"),
|
||||
SshMessageID::SSH_MSG_REQUEST_FAILURE => write!(f, "SSH_MSG_REQUEST_FAILURE"),
|
||||
SshMessageID::SSH_MSG_CHANNEL_OPEN => write!(f, "SSH_MSG_CHANNEL_OPEN"),
|
||||
SshMessageID::SSH_MSG_CHANNEL_OPEN_CONFIRMATION => {
|
||||
write!(f, "SSH_MSG_CHANNEL_OPEN_CONFIRMATION")
|
||||
}
|
||||
SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE => write!(f, "SSH_MSG_CHANNEL_OPEN_FAILURE"),
|
||||
SshMessageID::SSH_MSG_CHANNEL_WINDOW_ADJUST => {
|
||||
write!(f, "SSH_MSG_CHANNEL_WINDOW_ADJUST")
|
||||
}
|
||||
SshMessageID::SSH_MSG_CHANNEL_DATA => write!(f, "SSH_MSG_CHANNEL_DATA"),
|
||||
SshMessageID::SSH_MSG_CHANNEL_EXTENDED_DATA => {
|
||||
write!(f, "SSH_MSG_CHANNEL_EXTENDED_DATA")
|
||||
}
|
||||
SshMessageID::SSH_MSG_CHANNEL_EOF => write!(f, "SSH_MSG_CHANNEL_EOF"),
|
||||
SshMessageID::SSH_MSG_CHANNEL_CLOSE => write!(f, "SSH_MSG_CHANNEL_CLOSE"),
|
||||
SshMessageID::SSH_MSG_CHANNEL_REQUEST => write!(f, "SSH_MSG_CHANNEL_REQUEST"),
|
||||
SshMessageID::SSH_MSG_CHANNEL_SUCCESS => write!(f, "SSH_MSG_CHANNEL_SUCCESS"),
|
||||
SshMessageID::SSH_MSG_CHANNEL_FAILURE => write!(f, "SSH_MSG_CHANNEL_FAILURE"),
|
||||
SshMessageID::Unknown(x) => write!(f, "SSH_MSG_UNKNOWN{}", x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_messages() {
|
||||
let mut found = std::collections::HashSet::new();
|
||||
|
||||
for i in u8::MIN..=u8::MAX {
|
||||
let id = SshMessageID::from_primitive(i);
|
||||
let display = id.to_string();
|
||||
|
||||
assert!(!found.contains(&display));
|
||||
found.insert(display);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SshMessageID> for OperationalError {
|
||||
fn from(message: SshMessageID) -> Self {
|
||||
match message {
|
||||
SshMessageID::SSH_MSG_DISCONNECT => OperationalError::Disconnect,
|
||||
SshMessageID::SSH_MSG_USERAUTH_FAILURE => OperationalError::UserAuthFailed,
|
||||
SshMessageID::SSH_MSG_REQUEST_FAILURE => OperationalError::RequestFailed,
|
||||
SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE => OperationalError::OpenChannelFailure,
|
||||
SshMessageID::SSH_MSG_CHANNEL_EOF => OperationalError::OtherEof,
|
||||
SshMessageID::SSH_MSG_CHANNEL_CLOSE => OperationalError::OtherClosed,
|
||||
SshMessageID::SSH_MSG_CHANNEL_FAILURE => OperationalError::ChannelFailure,
|
||||
|
||||
_ => OperationalError::UnexpectedMessage { message },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<OperationalError> for SshMessageID {
|
||||
type Error = OperationalError;
|
||||
|
||||
fn try_from(value: OperationalError) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
OperationalError::Disconnect => Ok(SshMessageID::SSH_MSG_DISCONNECT),
|
||||
OperationalError::UserAuthFailed => Ok(SshMessageID::SSH_MSG_USERAUTH_FAILURE),
|
||||
OperationalError::RequestFailed => Ok(SshMessageID::SSH_MSG_REQUEST_FAILURE),
|
||||
OperationalError::OpenChannelFailure => Ok(SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE),
|
||||
OperationalError::OtherEof => Ok(SshMessageID::SSH_MSG_CHANNEL_EOF),
|
||||
OperationalError::OtherClosed => Ok(SshMessageID::SSH_MSG_CHANNEL_CLOSE),
|
||||
OperationalError::ChannelFailure => Ok(SshMessageID::SSH_MSG_CHANNEL_FAILURE),
|
||||
OperationalError::UnexpectedMessage { message } => Ok(message),
|
||||
|
||||
_ => Err(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for SshMessageID {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
|
||||
proptest::prop_oneof![
|
||||
Just(SshMessageID::SSH_MSG_DISCONNECT),
|
||||
Just(SshMessageID::SSH_MSG_IGNORE),
|
||||
Just(SshMessageID::SSH_MSG_UNIMPLEMENTED),
|
||||
Just(SshMessageID::SSH_MSG_DEBUG),
|
||||
Just(SshMessageID::SSH_MSG_SERVICE_REQUEST),
|
||||
Just(SshMessageID::SSH_MSG_SERVICE_ACCEPT),
|
||||
Just(SshMessageID::SSH_MSG_KEXINIT),
|
||||
Just(SshMessageID::SSH_MSG_NEWKEYS),
|
||||
Just(SshMessageID::SSH_MSG_USERAUTH_REQUEST),
|
||||
Just(SshMessageID::SSH_MSG_USERAUTH_FAILURE),
|
||||
Just(SshMessageID::SSH_MSG_USERAUTH_SUCCESS),
|
||||
Just(SshMessageID::SSH_MSG_USERAUTH_BANNER),
|
||||
Just(SshMessageID::SSH_MSG_GLOBAL_REQUEST),
|
||||
Just(SshMessageID::SSH_MSG_REQUEST_SUCCESS),
|
||||
Just(SshMessageID::SSH_MSG_REQUEST_FAILURE),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_OPEN),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_OPEN_CONFIRMATION),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_OPEN_FAILURE),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_WINDOW_ADJUST),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_DATA),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_EXTENDED_DATA),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_EOF),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_CLOSE),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_REQUEST),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_SUCCESS),
|
||||
Just(SshMessageID::SSH_MSG_CHANNEL_FAILURE),
|
||||
]
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn error_encodings_invert(message in SshMessageID::arbitrary()) {
|
||||
let error_version = OperationalError::from(message);
|
||||
let back_to_message = SshMessageID::try_from(error_version).unwrap();
|
||||
assert_eq!(message, back_to_message);
|
||||
}
|
||||
}
|
||||
51
ssh/src/operational_error.rs
Normal file
51
ssh/src/operational_error.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use crate::{SshKeyExchangeProcessingError, SshMessageID};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum OperationalError {
|
||||
#[error("Configuration error")]
|
||||
ConfigurationError,
|
||||
#[error("DNS client configuration error")]
|
||||
DnsConfig,
|
||||
#[error("Failed to connect to target address")]
|
||||
Connection,
|
||||
#[error("Failure during key exchange / agreement protocol")]
|
||||
KeyExchange,
|
||||
#[error("Failed to complete initial read: {0}")]
|
||||
InitialRead(std::io::Error),
|
||||
#[error("SSH banner was not formatted in UTF-8: {0}")]
|
||||
BannerError(std::str::Utf8Error),
|
||||
#[error("Invalid initial SSH versionling line: {line}")]
|
||||
InvalidHeaderLine { line: String },
|
||||
#[error("Error writing initial banner: {0}")]
|
||||
WriteBanner(std::io::Error),
|
||||
#[error("Unexpected disconnect from other side.")]
|
||||
Disconnect,
|
||||
#[error("{message} in unexpected place.")]
|
||||
UnexpectedMessage { message: SshMessageID },
|
||||
#[error("User authorization failed.")]
|
||||
UserAuthFailed,
|
||||
#[error("Request failed.")]
|
||||
RequestFailed,
|
||||
#[error("Failed to open channel.")]
|
||||
OpenChannelFailure,
|
||||
#[error("Other side closed connection.")]
|
||||
OtherClosed,
|
||||
#[error("Other side sent EOF.")]
|
||||
OtherEof,
|
||||
#[error("Channel failed.")]
|
||||
ChannelFailure,
|
||||
#[error("Error in initial handshake: {0}")]
|
||||
KeyxProcessingError(#[from] SshKeyExchangeProcessingError),
|
||||
#[error("Invalid port number '{port_string}': {error}")]
|
||||
InvalidPort {
|
||||
port_string: String,
|
||||
error: std::num::ParseIntError,
|
||||
},
|
||||
#[error("Invalid hostname '{0}'")]
|
||||
InvalidHostname(String),
|
||||
#[error("Unable to parse host address")]
|
||||
UnableToParseHostAddress,
|
||||
#[error("Unable to configure resolver")]
|
||||
Resolver,
|
||||
}
|
||||
3
ssh/src/packets.rs
Normal file
3
ssh/src/packets.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod key_exchange;
|
||||
|
||||
pub use key_exchange::{SshKeyExchange, SshKeyExchangeProcessingError};
|
||||
332
ssh/src/packets/key_exchange.rs
Normal file
332
ssh/src/packets/key_exchange.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
use configuration::connection::ClientConnectionOpts;
|
||||
use crate::channel::SshPacket;
|
||||
use crate::message_ids::SshMessageID;
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use itertools::Itertools;
|
||||
use proptest::arbitrary::Arbitrary;
|
||||
use proptest::strategy::{BoxedStrategy, Strategy};
|
||||
use rand::{CryptoRng, Rng, SeedableRng};
|
||||
use std::string::FromUtf8Error;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SshKeyExchange {
|
||||
cookie: [u8; 16],
|
||||
keyx_algorithms: Vec<String>,
|
||||
server_host_key_algorithms: Vec<String>,
|
||||
encryption_algorithms_client_to_server: Vec<String>,
|
||||
encryption_algorithms_server_to_client: Vec<String>,
|
||||
mac_algorithms_client_to_server: Vec<String>,
|
||||
mac_algorithms_server_to_client: Vec<String>,
|
||||
compression_algorithms_client_to_server: Vec<String>,
|
||||
compression_algorithms_server_to_client: Vec<String>,
|
||||
languages_client_to_server: Vec<String>,
|
||||
languages_server_to_client: Vec<String>,
|
||||
first_kex_packet_follows: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SshKeyExchangeProcessingError {
|
||||
#[error("Message not appropriately tagged as SSH_MSG_KEXINIT")]
|
||||
TaggedWrong,
|
||||
#[error("Initial key exchange message was too short.")]
|
||||
TooShort,
|
||||
#[error("Invalid string encoding for name-list (not ASCII)")]
|
||||
NotAscii,
|
||||
#[error("Invalid conversion (from ASCII to UTF-8??): {0}")]
|
||||
NotUtf8(FromUtf8Error),
|
||||
#[error("Extraneous data at the end of key exchange message")]
|
||||
ExtraneousData,
|
||||
#[error("Received invalid reserved word ({0} != 0)")]
|
||||
InvalidReservedWord(u32),
|
||||
}
|
||||
|
||||
impl TryFrom<SshPacket> for SshKeyExchange {
|
||||
type Error = SshKeyExchangeProcessingError;
|
||||
|
||||
fn try_from(mut value: SshPacket) -> Result<Self, Self::Error> {
|
||||
if SshMessageID::from(value.buffer.get_u8()) != SshMessageID::SSH_MSG_KEXINIT {
|
||||
return Err(SshKeyExchangeProcessingError::TaggedWrong);
|
||||
}
|
||||
|
||||
let mut cookie = [0; 16];
|
||||
check_length(&mut value.buffer, 16)?;
|
||||
value.buffer.copy_to_slice(&mut cookie);
|
||||
|
||||
let keyx_algorithms = name_list(&mut value.buffer)?;
|
||||
let server_host_key_algorithms = name_list(&mut value.buffer)?;
|
||||
let encryption_algorithms_client_to_server = name_list(&mut value.buffer)?;
|
||||
let encryption_algorithms_server_to_client = name_list(&mut value.buffer)?;
|
||||
let mac_algorithms_client_to_server = name_list(&mut value.buffer)?;
|
||||
let mac_algorithms_server_to_client = name_list(&mut value.buffer)?;
|
||||
let compression_algorithms_client_to_server = name_list(&mut value.buffer)?;
|
||||
let compression_algorithms_server_to_client = name_list(&mut value.buffer)?;
|
||||
let languages_client_to_server = name_list(&mut value.buffer)?;
|
||||
let languages_server_to_client = name_list(&mut value.buffer)?;
|
||||
check_length(&mut value.buffer, 5)?;
|
||||
let first_kex_packet_follows = value.buffer.get_u8() != 0;
|
||||
let reserved = value.buffer.get_u32();
|
||||
if reserved != 0 {
|
||||
return Err(SshKeyExchangeProcessingError::InvalidReservedWord(reserved));
|
||||
}
|
||||
|
||||
if value.buffer.remaining() > 0 {
|
||||
return Err(SshKeyExchangeProcessingError::ExtraneousData);
|
||||
}
|
||||
|
||||
Ok(SshKeyExchange {
|
||||
cookie,
|
||||
keyx_algorithms,
|
||||
server_host_key_algorithms,
|
||||
encryption_algorithms_client_to_server,
|
||||
encryption_algorithms_server_to_client,
|
||||
mac_algorithms_client_to_server,
|
||||
mac_algorithms_server_to_client,
|
||||
compression_algorithms_client_to_server,
|
||||
compression_algorithms_server_to_client,
|
||||
languages_client_to_server,
|
||||
languages_server_to_client,
|
||||
first_kex_packet_follows,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SshKeyExchange> for SshPacket {
|
||||
fn from(value: SshKeyExchange) -> Self {
|
||||
let mut buffer = BytesMut::new();
|
||||
|
||||
let put_options = |buffer: &mut BytesMut, vals: Vec<String>| {
|
||||
let mut merged = String::new();
|
||||
#[allow(unstable_name_collisions)]
|
||||
let comma_sepped = vals.into_iter().intersperse(String::from(","));
|
||||
merged.extend(comma_sepped);
|
||||
let bytes = merged.as_bytes();
|
||||
buffer.put_u32(bytes.len() as u32);
|
||||
buffer.put_slice(bytes);
|
||||
};
|
||||
|
||||
buffer.put_u8(SshMessageID::SSH_MSG_KEXINIT.into());
|
||||
buffer.put_slice(&value.cookie);
|
||||
put_options(&mut buffer, value.keyx_algorithms);
|
||||
put_options(&mut buffer, value.server_host_key_algorithms);
|
||||
put_options(&mut buffer, value.encryption_algorithms_client_to_server);
|
||||
put_options(&mut buffer, value.encryption_algorithms_server_to_client);
|
||||
put_options(&mut buffer, value.mac_algorithms_client_to_server);
|
||||
put_options(&mut buffer, value.mac_algorithms_server_to_client);
|
||||
put_options(&mut buffer, value.compression_algorithms_client_to_server);
|
||||
put_options(&mut buffer, value.compression_algorithms_server_to_client);
|
||||
put_options(&mut buffer, value.languages_client_to_server);
|
||||
put_options(&mut buffer, value.languages_server_to_client);
|
||||
buffer.put_u8(value.first_kex_packet_follows as u8);
|
||||
buffer.put_u32(0);
|
||||
|
||||
SshPacket {
|
||||
buffer: buffer.freeze(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SshKeyExchange {
|
||||
/// Create a new SshKeyExchange message for this client or server based
|
||||
/// on the given connection options.
|
||||
///
|
||||
/// This function takes a random number generator because it needs to
|
||||
/// seed the message with a random cookie, but is otherwise deterministic.
|
||||
/// It will fail only in the case that the underlying random number
|
||||
/// generator fails, and return exactly that error.
|
||||
pub fn new<R>(rng: &mut R, value: ClientConnectionOpts) -> Result<Self, ()>
|
||||
where
|
||||
R: CryptoRng + Rng,
|
||||
{
|
||||
let mut result = SshKeyExchange {
|
||||
cookie: [0; 16],
|
||||
keyx_algorithms: value
|
||||
.key_exchange_algorithms
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
server_host_key_algorithms: value
|
||||
.server_host_key_algorithms
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
encryption_algorithms_client_to_server: value
|
||||
.encryption_algorithms
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
encryption_algorithms_server_to_client: value
|
||||
.encryption_algorithms
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
mac_algorithms_client_to_server: value
|
||||
.mac_algorithms
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
mac_algorithms_server_to_client: value
|
||||
.mac_algorithms
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
compression_algorithms_client_to_server: value
|
||||
.compression_algorithms
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
compression_algorithms_server_to_client: value
|
||||
.compression_algorithms
|
||||
.iter()
|
||||
.map(|x| x.to_string())
|
||||
.collect(),
|
||||
languages_client_to_server: value.languages.to_vec(),
|
||||
languages_server_to_client: value.languages.to_vec(),
|
||||
first_kex_packet_follows: value.predict.is_some(),
|
||||
};
|
||||
|
||||
rng.fill(&mut result.cookie);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for SshKeyExchange {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with(_: Self::Parameters) -> BoxedStrategy<Self> {
|
||||
let client_config = ClientConnectionOpts::arbitrary();
|
||||
let seed = <[u8; 32]>::arbitrary();
|
||||
|
||||
(client_config, seed)
|
||||
.prop_map(|(config, seed)| {
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(seed);
|
||||
SshKeyExchange::new(&mut rng, config).unwrap()
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn valid_kex_messages_parse(kex in SshKeyExchange::arbitrary()) {
|
||||
let as_packet: SshPacket = kex.clone().try_into().expect("can generate packet");
|
||||
let as_message = as_packet.try_into().expect("can regenerate message");
|
||||
assert_eq!(kex, as_message);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_length(buffer: &mut Bytes, length: usize) -> Result<(), SshKeyExchangeProcessingError> {
|
||||
if buffer.remaining() < length {
|
||||
Err(SshKeyExchangeProcessingError::TooShort)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn name_list(buffer: &mut Bytes) -> Result<Vec<String>, SshKeyExchangeProcessingError> {
|
||||
check_length(buffer, 4)?;
|
||||
let list_length = buffer.get_u32() as usize;
|
||||
|
||||
if list_length == 0 {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
check_length(buffer, list_length)?;
|
||||
let mut raw_bytes = vec![0u8; list_length];
|
||||
buffer.copy_to_slice(&mut raw_bytes);
|
||||
if !raw_bytes.iter().all(|c| c.is_ascii()) {
|
||||
return Err(SshKeyExchangeProcessingError::NotAscii);
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
for split in raw_bytes.split(|b| char::from(*b) == ',') {
|
||||
let str =
|
||||
String::from_utf8(split.to_vec()).map_err(SshKeyExchangeProcessingError::NotUtf8)?;
|
||||
result.push(str);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn standard_kex_message() -> SshPacket {
|
||||
let seed = [0u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(seed);
|
||||
let config = ClientConnectionOpts::default();
|
||||
let message = SshKeyExchange::new(&mut rng, config).expect("default settings work");
|
||||
message.try_into().expect("default settings serialize")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_see_bad_tag() {
|
||||
let standard = standard_kex_message();
|
||||
let mut buffer = standard.buffer.to_vec();
|
||||
buffer[0] += 1;
|
||||
let bad = SshPacket {
|
||||
buffer: buffer.into(),
|
||||
};
|
||||
assert!(matches!(
|
||||
SshKeyExchange::try_from(bad),
|
||||
Err(SshKeyExchangeProcessingError::TaggedWrong)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_for_extraneous_data() {
|
||||
let standard = standard_kex_message();
|
||||
let mut buffer = standard.buffer.to_vec();
|
||||
buffer.push(3);
|
||||
let bad = SshPacket {
|
||||
buffer: buffer.into(),
|
||||
};
|
||||
assert!(matches!(
|
||||
SshKeyExchange::try_from(bad),
|
||||
Err(SshKeyExchangeProcessingError::ExtraneousData)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_for_short_packets() {
|
||||
let standard = standard_kex_message();
|
||||
let mut buffer = standard.buffer.to_vec();
|
||||
let _ = buffer.pop();
|
||||
let bad = SshPacket {
|
||||
buffer: buffer.into(),
|
||||
};
|
||||
assert!(matches!(
|
||||
SshKeyExchange::try_from(bad),
|
||||
Err(SshKeyExchangeProcessingError::TooShort)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_for_invalid_data() {
|
||||
let standard = standard_kex_message();
|
||||
let mut buffer = standard.buffer.to_vec();
|
||||
buffer[22] = 0xc3;
|
||||
buffer[23] = 0x28;
|
||||
let bad = SshPacket {
|
||||
buffer: buffer.into(),
|
||||
};
|
||||
assert!(matches!(
|
||||
SshKeyExchange::try_from(bad),
|
||||
Err(SshKeyExchangeProcessingError::NotAscii)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_for_bad_reserved_word() {
|
||||
let standard = standard_kex_message();
|
||||
let mut buffer = standard.buffer.to_vec();
|
||||
let _ = buffer.pop();
|
||||
buffer.push(1);
|
||||
let bad = SshPacket {
|
||||
buffer: buffer.into(),
|
||||
};
|
||||
assert!(matches!(
|
||||
SshKeyExchange::try_from(bad),
|
||||
Err(SshKeyExchangeProcessingError::InvalidReservedWord(1))
|
||||
));
|
||||
}
|
||||
392
ssh/src/preamble.rs
Normal file
392
ssh/src/preamble.rs
Normal file
@@ -0,0 +1,392 @@
|
||||
use error_stack::{report, ResultExt};
|
||||
use proptest::arbitrary::Arbitrary;
|
||||
use proptest::strategy::{BoxedStrategy, Strategy};
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Preamble {
|
||||
pub preamble: String,
|
||||
pub software_name: String,
|
||||
pub software_version: String,
|
||||
pub commentary: String,
|
||||
}
|
||||
|
||||
impl Arbitrary for Preamble {
|
||||
type Parameters = ();
|
||||
type Strategy = BoxedStrategy<Self>;
|
||||
|
||||
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
|
||||
let name = proptest::string::string_regex("[[:alpha:]][[:alnum:]]{0,32}").unwrap();
|
||||
let soft_major = u8::arbitrary();
|
||||
let soft_minor = u8::arbitrary();
|
||||
let soft_patch = proptest::option::of(u8::arbitrary());
|
||||
let commentary = proptest::option::of(
|
||||
proptest::string::string_regex("[[:alnum:]][[[:alnum:]][[:blank:]][[:punct:]]]{0,64}")
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
(name, soft_major, soft_minor, soft_patch, commentary)
|
||||
.prop_map(|(name, major, minor, patch, commentary)| Preamble {
|
||||
preamble: String::new(),
|
||||
software_name: name,
|
||||
software_version: if let Some(patch) = patch {
|
||||
format!("{}.{}.{}", major, minor, patch)
|
||||
} else {
|
||||
format!("{}.{}", major, minor)
|
||||
},
|
||||
commentary: commentary.unwrap_or_default(),
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Preamble {
|
||||
fn default() -> Self {
|
||||
Preamble {
|
||||
preamble: String::new(),
|
||||
software_name: env!("CARGO_PKG_NAME").to_string(),
|
||||
software_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
commentary: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PreambleReadError {
|
||||
#[error("Reading from the input stream failed")]
|
||||
Read,
|
||||
#[error("Illegal version number, expected '2.0' (saw characters {0})")]
|
||||
IllegalVersion(String),
|
||||
#[error("No dash found after seeing SSH version number")]
|
||||
NoDashAfterVersion,
|
||||
#[error("Illegal character in SSH software name")]
|
||||
IllegalSoftwareNameChar,
|
||||
#[error("Protocol error in preamble: No line feed for carriage return")]
|
||||
NoLineFeedForCarriage,
|
||||
#[error("Missing the final newline in the preamble")]
|
||||
MissingFinalNewline,
|
||||
#[error("Illegal UTF-8 in software name")]
|
||||
InvalidSoftwareName,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PreambleWriteError {
|
||||
#[error("Could not write preamble to socket")]
|
||||
Write,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PreambleState {
|
||||
StartOfLine,
|
||||
Preamble,
|
||||
CarriageReturn,
|
||||
InitialS,
|
||||
SecondS,
|
||||
InitialH,
|
||||
InitialDash,
|
||||
Version2,
|
||||
VersionDot,
|
||||
Version0,
|
||||
VersionDash,
|
||||
SoftwareName,
|
||||
SoftwareVersion,
|
||||
Commentary,
|
||||
FinalCarriageReturn,
|
||||
}
|
||||
|
||||
impl Preamble {
|
||||
/// Read an SSH preamble from the given read channel.
|
||||
///
|
||||
/// Will fail if the underlying read channel fails, or if the preamble does not
|
||||
/// meet the formatting requirements of the RFC.
|
||||
pub async fn read<R: AsyncReadExt + Unpin>(
|
||||
connection: &mut R,
|
||||
) -> error_stack::Result<Preamble, PreambleReadError> {
|
||||
let mut preamble = String::new();
|
||||
let mut software_name_bytes = Vec::new();
|
||||
let mut software_version = String::new();
|
||||
let mut commentary = String::new();
|
||||
let mut state = PreambleState::StartOfLine;
|
||||
|
||||
loop {
|
||||
let next_byte = connection
|
||||
.read_u8()
|
||||
.await
|
||||
.change_context(PreambleReadError::Read)?;
|
||||
let next_char = char::from(next_byte);
|
||||
|
||||
tracing::trace!(?next_char, ?state, "processing next preamble character");
|
||||
match state {
|
||||
PreambleState::StartOfLine => match next_char {
|
||||
'S' => state = PreambleState::InitialS,
|
||||
_ => {
|
||||
preamble.push(next_char);
|
||||
state = PreambleState::Preamble;
|
||||
}
|
||||
},
|
||||
|
||||
PreambleState::Preamble => match next_char {
|
||||
'\r' => state = PreambleState::CarriageReturn,
|
||||
_ => preamble.push(next_char),
|
||||
},
|
||||
|
||||
PreambleState::CarriageReturn => match next_char {
|
||||
'\n' => state = PreambleState::StartOfLine,
|
||||
_ => return Err(report!(PreambleReadError::NoLineFeedForCarriage)),
|
||||
},
|
||||
|
||||
PreambleState::InitialS => match next_char {
|
||||
'S' => state = PreambleState::SecondS,
|
||||
_ => {
|
||||
preamble.push('S');
|
||||
preamble.push(next_char);
|
||||
state = PreambleState::Preamble;
|
||||
}
|
||||
},
|
||||
|
||||
PreambleState::SecondS => match next_char {
|
||||
'H' => state = PreambleState::InitialH,
|
||||
_ => {
|
||||
preamble.push_str("SS");
|
||||
preamble.push(next_char);
|
||||
state = PreambleState::Preamble;
|
||||
}
|
||||
},
|
||||
|
||||
PreambleState::InitialH => match next_char {
|
||||
'-' => state = PreambleState::InitialDash,
|
||||
_ => {
|
||||
preamble.push_str("SSH");
|
||||
preamble.push(next_char);
|
||||
state = PreambleState::InitialDash;
|
||||
}
|
||||
},
|
||||
|
||||
PreambleState::InitialDash => match next_char {
|
||||
'2' => state = PreambleState::Version2,
|
||||
_ => {
|
||||
return Err(report!(PreambleReadError::IllegalVersion(String::from(
|
||||
next_char
|
||||
))))
|
||||
}
|
||||
},
|
||||
|
||||
PreambleState::Version2 => match next_char {
|
||||
'.' => state = PreambleState::VersionDot,
|
||||
_ => {
|
||||
return Err(report!(PreambleReadError::IllegalVersion(format!(
|
||||
"2{}",
|
||||
next_char
|
||||
))))
|
||||
}
|
||||
},
|
||||
|
||||
PreambleState::VersionDot => match next_char {
|
||||
'0' => state = PreambleState::Version0,
|
||||
_ => {
|
||||
return Err(report!(PreambleReadError::IllegalVersion(format!(
|
||||
"2.{}",
|
||||
next_char
|
||||
))))
|
||||
}
|
||||
},
|
||||
|
||||
PreambleState::Version0 => match next_char {
|
||||
'-' => state = PreambleState::VersionDash,
|
||||
_ => return Err(report!(PreambleReadError::NoDashAfterVersion)),
|
||||
},
|
||||
|
||||
PreambleState::VersionDash => {
|
||||
software_name_bytes.push(next_byte);
|
||||
state = PreambleState::SoftwareName;
|
||||
}
|
||||
|
||||
PreambleState::SoftwareName => match next_char {
|
||||
'_' => state = PreambleState::SoftwareVersion,
|
||||
x if x == '-' || x.is_ascii_whitespace() => {
|
||||
return Err(report!(PreambleReadError::IllegalSoftwareNameChar))
|
||||
}
|
||||
_ => software_name_bytes.push(next_byte),
|
||||
},
|
||||
|
||||
PreambleState::SoftwareVersion => match next_char {
|
||||
' ' => state = PreambleState::Commentary,
|
||||
'\r' => state = PreambleState::FinalCarriageReturn,
|
||||
'_' => state = PreambleState::SoftwareVersion,
|
||||
x if x == '-' || x.is_ascii_whitespace() => {
|
||||
return Err(report!(PreambleReadError::IllegalSoftwareNameChar))
|
||||
.attach_printable_lazy(|| {
|
||||
format!("saw {:?} / {}", next_char, next_byte)
|
||||
})?
|
||||
}
|
||||
_ => software_version.push(next_char),
|
||||
},
|
||||
|
||||
PreambleState::FinalCarriageReturn => match next_char {
|
||||
'\n' => break,
|
||||
_ => return Err(report!(PreambleReadError::MissingFinalNewline)),
|
||||
},
|
||||
|
||||
PreambleState::Commentary => match next_char {
|
||||
'\r' => state = PreambleState::FinalCarriageReturn,
|
||||
_ => commentary.push(next_char),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let software_name = String::from_utf8(software_name_bytes)
|
||||
.change_context(PreambleReadError::InvalidSoftwareName)?;
|
||||
|
||||
Ok(Preamble {
|
||||
preamble,
|
||||
software_name,
|
||||
software_version,
|
||||
commentary,
|
||||
})
|
||||
}
|
||||
|
||||
// let mut read_buffer = vec![0; 4096];
|
||||
// let mut pre_message = String::new();
|
||||
// let protocol_version;
|
||||
// let software_name;
|
||||
// let software_version;
|
||||
// let commentary;
|
||||
// let mut prefix = String::new();
|
||||
//
|
||||
//
|
||||
// 'outer: loop {
|
||||
// let read_length = connection
|
||||
// .read(&mut read_buffer)
|
||||
// .await
|
||||
// .change_context(PreambleReadError::InitialRead)?;
|
||||
// let string_version = std::str::from_utf8(&read_buffer[0..read_length])
|
||||
// .change_context(PreambleReadError::BannerError)?;
|
||||
//
|
||||
// prefix.push_str(string_version);
|
||||
// let ends_with_newline = prefix.ends_with("\r\n");
|
||||
//
|
||||
// let new_prefix = if ends_with_newline {
|
||||
// // we are cleanly reading up to a \r\n, so our new prefix after
|
||||
// // this loop is empty
|
||||
// String::new()
|
||||
// } else if let Some((correct_bits, leftover)) = prefix.rsplit_once("\r\n") {
|
||||
// // there's some dangling bits in this read, so we'll cut this string
|
||||
// // at the final "\r\n" and then remember to use the leftover as the
|
||||
// // new prefix at the end of this loop
|
||||
// let result = leftover.to_string();
|
||||
// prefix = correct_bits.to_string();
|
||||
// result
|
||||
// } else {
|
||||
// // there's no "\r\n", so we don't have a full line yet, so keep reading
|
||||
// continue;
|
||||
// };
|
||||
//
|
||||
// for line in prefix.lines() {
|
||||
// if line.starts_with("SSH") {
|
||||
// let (_, interesting_bits) = line
|
||||
// .split_once('-')
|
||||
// .ok_or_else(||
|
||||
// report!(PreambleReadError::InvalidHeaderLine {
|
||||
// reason: "could not find dash after SSH",
|
||||
// line: line.to_string(),
|
||||
// }))?;
|
||||
//
|
||||
// let (protoversion, other_bits) = interesting_bits
|
||||
// .split_once('-')
|
||||
// .ok_or_else(|| report!(PreambleReadError::InvalidHeaderLine {
|
||||
// reason: "could not find dash after protocol version",
|
||||
// line: line.to_string(),
|
||||
// }))?;
|
||||
//
|
||||
// let (softwarever, comment) = match other_bits.split_once(' ') {
|
||||
// Some((s, c)) => (s, c),
|
||||
// None => (other_bits, ""),
|
||||
// };
|
||||
//
|
||||
// let (software_name_str, software_version_str) = softwarever
|
||||
// .split_once('_')
|
||||
// .ok_or_else(|| report!(PreambleReadError::InvalidHeaderLine {
|
||||
// reason: "could not find underscore between software name and version",
|
||||
// line: line.to_string(),
|
||||
// }))?;
|
||||
//
|
||||
// software_name = software_name_str.to_string();
|
||||
// software_version = software_version_str.to_string();
|
||||
// protocol_version = protoversion.to_string();
|
||||
// commentary = comment.to_string();
|
||||
// break 'outer;
|
||||
// } else {
|
||||
// pre_message.push_str(line);
|
||||
// pre_message.push('\n');
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// prefix = new_prefix;
|
||||
// }
|
||||
//
|
||||
// tracing::info!(
|
||||
// ?protocol_version,
|
||||
// ?software_version,
|
||||
// ?commentary,
|
||||
// "Got server information"
|
||||
// );
|
||||
//
|
||||
// Ok(Preamble {
|
||||
// protocol_version,
|
||||
// software_name,
|
||||
// software_version,
|
||||
// commentary,
|
||||
// })
|
||||
// }
|
||||
|
||||
/// Write a preamble to the given network socket.
|
||||
pub async fn write<W: AsyncWriteExt + Unpin>(
|
||||
&self,
|
||||
connection: &mut W,
|
||||
) -> error_stack::Result<(), PreambleWriteError> {
|
||||
let comment = if self.commentary.is_empty() {
|
||||
self.commentary.clone()
|
||||
} else {
|
||||
format!(" {}", self.commentary)
|
||||
};
|
||||
let output = format!(
|
||||
"SSH-2.0-{}_{}{}\r\n",
|
||||
self.software_name, self.software_version, comment
|
||||
);
|
||||
connection
|
||||
.write_all(output.as_bytes())
|
||||
.await
|
||||
.change_context(PreambleWriteError::Write)
|
||||
}
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn preamble_roundtrips(preamble in Preamble::arbitrary()) {
|
||||
let read_version = tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let (mut writer, mut reader) = tokio::io::duplex(4096);
|
||||
preamble.write(&mut writer).await.unwrap();
|
||||
Preamble::read(&mut reader).await.unwrap()
|
||||
});
|
||||
|
||||
assert_eq!(preamble, read_version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preamble_read_is_thrifty(preamble in Preamble::arbitrary(), b in u8::arbitrary()) {
|
||||
let (read_version, next) = tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let (mut writer, mut reader) = tokio::io::duplex(4096);
|
||||
preamble.write(&mut writer).await.unwrap();
|
||||
writer.write_u8(b).await.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
drop(writer);
|
||||
let preamble = Preamble::read(&mut reader).await.unwrap();
|
||||
let next = reader.read_u8().await.unwrap();
|
||||
(preamble, next)
|
||||
});
|
||||
|
||||
assert_eq!(preamble, read_version);
|
||||
assert_eq!(b, next);
|
||||
}
|
||||
}
|
||||
113
tests/all_keys.toml
Normal file
113
tests/all_keys.toml
Normal file
@@ -0,0 +1,113 @@
|
||||
[runtime]
|
||||
worker_threads = 4
|
||||
blocking_threads = 8
|
||||
|
||||
[logging]
|
||||
level = "DEBUG"
|
||||
include_filename = true
|
||||
include_lineno = true
|
||||
include_thread_ids = true
|
||||
include_thread_names = true
|
||||
mode = "Compact"
|
||||
target = "stdout"
|
||||
|
||||
[keys]
|
||||
|
||||
[keys.ecdsa_clear1]
|
||||
public = "tests/ssh_keys/ecdsa1.pub"
|
||||
private = "tests/ssh_keys/ecdsa1"
|
||||
|
||||
[keys.ecdsa_clear2]
|
||||
public = "tests/ssh_keys/ecdsa2.pub"
|
||||
private = "tests/ssh_keys/ecdsa2"
|
||||
|
||||
[keys.ecdsa_big1]
|
||||
public = "tests/ssh_keys/ecdsa384a.pub"
|
||||
private = "tests/ssh_keys/ecdsa384a"
|
||||
|
||||
[keys.ecdsa_big2]
|
||||
public = "tests/ssh_keys/ecdsa384b.pub"
|
||||
private = "tests/ssh_keys/ecdsa384b"
|
||||
|
||||
[keys.ecdsa_biggest1]
|
||||
public = "tests/ssh_keys/ecdsa384a.pub"
|
||||
private = "tests/ssh_keys/ecdsa384a"
|
||||
|
||||
[keys.ecdsa_biggest2]
|
||||
public = "tests/ssh_keys/ecdsa521b.pub"
|
||||
private = "tests/ssh_keys/ecdsa521b"
|
||||
|
||||
[keys.ed25519_clear1]
|
||||
public = "tests/ssh_keys/ed25519a.pub"
|
||||
private = "tests/ssh_keys/ed25519a"
|
||||
|
||||
[keys.ed25519_clear2]
|
||||
public = "tests/ssh_keys/ed25519b.pub"
|
||||
private = "tests/ssh_keys/ed25519b"
|
||||
|
||||
[keys.ed25519_pass_here]
|
||||
public = "tests/ssh_keys/ed25519a.pub"
|
||||
private = "tests/ssh_keys/ed25519a"
|
||||
password = "hush"
|
||||
|
||||
[keys.ed25519_pass_on_load]
|
||||
public = "tests/ssh_keys/ed25519b.pub"
|
||||
private = "tests/ssh_keys/ed25519b"
|
||||
|
||||
[keys.rsa_reasonable1]
|
||||
public = "tests/ssh_keys/rsa4096a.pub"
|
||||
private = "tests/ssh_keys/rsa4096a"
|
||||
|
||||
[keys.rsa_reasonable2]
|
||||
public = "tests/ssh_keys/rsa4096b.pub"
|
||||
private = "tests/ssh_keys/rsa4096b"
|
||||
|
||||
[keys.rsa_big1]
|
||||
public = "tests/ssh_keys/rsa7680a.pub"
|
||||
private = "tests/ssh_keys/rsa7680a"
|
||||
|
||||
[keys.rsa_big2]
|
||||
public = "tests/ssh_keys/rsa7680b.pub"
|
||||
private = "tests/ssh_keys/rsa7680b"
|
||||
|
||||
[keys.rsa_extra1]
|
||||
public = "tests/ssh_keys/rsa8192a.pub"
|
||||
private = "tests/ssh_keys/rsa8192a"
|
||||
|
||||
[keys.rsa_extra2]
|
||||
public = "tests/ssh_keys/rsa8192b.pub"
|
||||
private = "tests/ssh_keys/rsa8192b"
|
||||
|
||||
[keys.rsa_crazy1]
|
||||
public = "tests/ssh_keys/rsa15360a.pub"
|
||||
private = "tests/ssh_keys/rsa15360a"
|
||||
|
||||
[keys.rsa_crazy2]
|
||||
public = "tests/ssh_keys/rsa15360b.pub"
|
||||
private = "tests/ssh_keys/rsa15360b"
|
||||
|
||||
[defaults]
|
||||
key_exchange_algorithms = [ "curve25519-sha256" ]
|
||||
server_host_algorithms = [ "ed25519" ]
|
||||
encryption_algorithms = [ "aes256-ctr", "aes256-gcm" ]
|
||||
mac_algorithms = [ "hmac-sha256" ]
|
||||
compression_algorithms = [ "zlib" ]
|
||||
predict = "curve25519-sha256"
|
||||
|
||||
[servers]
|
||||
|
||||
[servers.sea]
|
||||
host = "sea.uhsure.com"
|
||||
encryption_algorithms = ["aes256-gcm"]
|
||||
compression_algorithms = []
|
||||
|
||||
[servers.origin]
|
||||
host = "104.238.156.29"
|
||||
encryption_algorithms = ["aes256-ctr"]
|
||||
compression_algorithms = []
|
||||
|
||||
|
||||
[servers.origin6]
|
||||
host = "2001:19f0:8001:1e9b:5400:04ff:fe7e:055d"
|
||||
encryption_algorithms = ["aes256-ctr"]
|
||||
compression_algorithms = []
|
||||
53
tests/broken_keys/bad_info
Normal file
53
tests/broken_keys/bad_info
Normal file
@@ -0,0 +1,53 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
9
tests/broken_keys/mismatched
Normal file
9
tests/broken_keys/mismatched
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNl
|
||||
Y2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTXRwCTNcWIwWe1HV/x
|
||||
m4Id6Dch54nlfBRq1/5DROdn2AVm4ZYSj8kjHI8lDqfxzXZ66reHJAcecmNrgd4g
|
||||
geeCAAAAsMnOb8HJzm/BAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy
|
||||
NTYAAABBBE/n2mCAHZDC0V1sX473/Yq5hksUOM8woNApzcq4fA9PfEvbJtxR3ri9
|
||||
3RQaw1U+yQImCKvhv4uEDn0tf2BuOHIAAAAhAIJyO57UJPPaZ4EWPefrZ9/zVe6j
|
||||
oI4ioVoVubbq38D9AAAAEGFkYW13aWNrQGVyZ2F0ZXMBAgMEBQYH
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
9
tests/ssh_keys/ecdsa1
Normal file
9
tests/ssh_keys/ecdsa1
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTXRwCTNcWIwWe1HV/xm4Id6Dch54nl
|
||||
fBRq1/5DROdn2AVm4ZYSj8kjHI8lDqfxzXZ66reHJAcecmNrgd4ggeeCAAAAsMnOb8HJzm
|
||||
/BAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdHAJM1xYjBZ7Ud
|
||||
X/Gbgh3oNyHnieV8FGrX/kNE52fYBWbhlhKPySMcjyUOp/HNdnrqt4ckBx5yY2uB3iCB54
|
||||
IAAAAhANE9lmc43afpjYdta5hvNo6Hgms6QC2ZiPMmdvfhP/8mAAAAEGFkYW13aWNrQGVy
|
||||
Z2F0ZXMBAgMEBQYH
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/ecdsa1.pub
Normal file
1
tests/ssh_keys/ecdsa1.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNdHAJM1xYjBZ7UdX/Gbgh3oNyHnieV8FGrX/kNE52fYBWbhlhKPySMcjyUOp/HNdnrqt4ckBx5yY2uB3iCB54I= adamwick@ergates
|
||||
9
tests/ssh_keys/ecdsa2
Normal file
9
tests/ssh_keys/ecdsa2
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQRP59pggB2QwtFdbF+O9/2KuYZLFDjP
|
||||
MKDQKc3KuHwPT3xL2ybcUd64vd0UGsNVPskCJgir4b+LhA59LX9gbjhyAAAAsAptbc0KbW
|
||||
3NAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBE/n2mCAHZDC0V1s
|
||||
X473/Yq5hksUOM8woNApzcq4fA9PfEvbJtxR3ri93RQaw1U+yQImCKvhv4uEDn0tf2BuOH
|
||||
IAAAAhAIJyO57UJPPaZ4EWPefrZ9/zVe6joI4ioVoVubbq38D9AAAAEGFkYW13aWNrQGVy
|
||||
Z2F0ZXMBAgMEBQYH
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/ecdsa2.pub
Normal file
1
tests/ssh_keys/ecdsa2.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBE/n2mCAHZDC0V1sX473/Yq5hksUOM8woNApzcq4fA9PfEvbJtxR3ri93RQaw1U+yQImCKvhv4uEDn0tf2BuOHI= adamwick@ergates
|
||||
10
tests/ssh_keys/ecdsa384a
Normal file
10
tests/ssh_keys/ecdsa384a
Normal file
@@ -0,0 +1,10 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQQNo4tCmBW6DJv2Xshowx5Esm8SL8iX
|
||||
gSNgoMOv8T9f1upJnNZ0NNBq0DTk2EIAlGFjFRGwY1LMvuXCEjPcNgNgbLkRpuSBZEpUdC
|
||||
v7vP+Q4CwAwa6ZLg/bUOxM6L3xD9wAAADY90b9x/dG/ccAAAATZWNkc2Etc2hhMi1uaXN0
|
||||
cDM4NAAAAAhuaXN0cDM4NAAAAGEEDaOLQpgVugyb9l7IaMMeRLJvEi/Il4EjYKDDr/E/X9
|
||||
bqSZzWdDTQatA05NhCAJRhYxURsGNSzL7lwhIz3DYDYGy5EabkgWRKVHQr+7z/kOAsAMGu
|
||||
mS4P21DsTOi98Q/cAAAAMFXAE7Bhbcr4CW9xJhaXm4ToN6lVq3OKw4C41ZxxIetLxfYhyZ
|
||||
IMpkNIlxhsT5JCugAAABBhZGFtd2lja0BlcmdhdGVz
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/ecdsa384a.pub
Normal file
1
tests/ssh_keys/ecdsa384a.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBA2ji0KYFboMm/ZeyGjDHkSybxIvyJeBI2Cgw6/xP1/W6kmc1nQ00GrQNOTYQgCUYWMVEbBjUsy+5cISM9w2A2BsuRGm5IFkSlR0K/u8/5DgLADBrpkuD9tQ7EzovfEP3A== adamwick@ergates
|
||||
10
tests/ssh_keys/ecdsa384b
Normal file
10
tests/ssh_keys/ecdsa384b
Normal file
@@ -0,0 +1,10 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSFuBPbGxyIn0K7nT7gLt1mO+RP7TuS
|
||||
zFVatSmKS7UDl8EaBvEHSCqZWWvpaz1hw2l7fENakdh2aAmG4ZQiQU2k9LLzOJUmQvkJzi
|
||||
x0d35GiyHvrgCbk+3jux0nRFqjf1EAAADg33k9/t95Pf4AAAATZWNkc2Etc2hhMi1uaXN0
|
||||
cDM4NAAAAAhuaXN0cDM4NAAAAGEEhbgT2xsciJ9Cu50+4C7dZjvkT+07ksxVWrUpiku1A5
|
||||
fBGgbxB0gqmVlr6Ws9YcNpe3xDWpHYdmgJhuGUIkFNpPSy8ziVJkL5Cc4sdHd+Rosh764A
|
||||
m5Pt47sdJ0Rao39RAAAAMQC7FlZiz5mGgDtp1i9/Wy/gpWySe5xiER/COiIPFMm3wUchO2
|
||||
tbJ06GM6maApazHuMAAAAQYWRhbXdpY2tAZXJnYXRlcwECAwQFBgc=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/ecdsa384b.pub
Normal file
1
tests/ssh_keys/ecdsa384b.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBIW4E9sbHIifQrudPuAu3WY75E/tO5LMVVq1KYpLtQOXwRoG8QdIKplZa+lrPWHDaXt8Q1qR2HZoCYbhlCJBTaT0svM4lSZC+QnOLHR3fkaLIe+uAJuT7eO7HSdEWqN/UQ== adamwick@ergates
|
||||
12
tests/ssh_keys/ecdsa521a
Normal file
12
tests/ssh_keys/ecdsa521a
Normal file
@@ -0,0 +1,12 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBaJo3rKMWJqSqcuYkQExpvxGezbZj
|
||||
khCmt26YtKCBxTyI+ptgV0wPujZOxuA+pYY909WGlulKHAzicP8feQZdUpoBK4eOwunitw
|
||||
EAphr3I0dqcBAJWd4KIMov11qYvcNb8nsUbmulk10+IVxJQL+bwHhLsenHArFRlBrPiHv+
|
||||
6thVibMAAAEQwd7yr8He8q8AAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
|
||||
AAAIUEAWiaN6yjFiakqnLmJEBMab8Rns22Y5IQprdumLSggcU8iPqbYFdMD7o2TsbgPqWG
|
||||
PdPVhpbpShwM4nD/H3kGXVKaASuHjsLp4rcBAKYa9yNHanAQCVneCiDKL9damL3DW/J7FG
|
||||
5rpZNdPiFcSUC/m8B4S7HpxwKxUZQaz4h7/urYVYmzAAAAQgEt3nVbegR+JPwhB+zxq7Ah
|
||||
1iKnuXdyNJytMm9PzOtC/0ufeCFtvWHB4J20ysAisdHfreftxJBh53yrSqmFYNmtkgAAAB
|
||||
BhZGFtd2lja0BlcmdhdGVzAQI=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/ecdsa521a.pub
Normal file
1
tests/ssh_keys/ecdsa521a.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFomjesoxYmpKpy5iRATGm/EZ7NtmOSEKa3bpi0oIHFPIj6m2BXTA+6Nk7G4D6lhj3T1YaW6UocDOJw/x95Bl1SmgErh47C6eK3AQCmGvcjR2pwEAlZ3gogyi/XWpi9w1vyexRua6WTXT4hXElAv5vAeEux6ccCsVGUGs+Ie/7q2FWJsw== adamwick@ergates
|
||||
12
tests/ssh_keys/ecdsa521b
Normal file
12
tests/ssh_keys/ecdsa521b
Normal file
@@ -0,0 +1,12 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
|
||||
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBYa7n+gYpTaP+AqX+rhHoYLbzSFjB
|
||||
namxsbIoqAvhcqFXpXqDTyfP5bbe+/xr6iiJZOuAxrxt30SD6r5JR4ls48gAKHuqEOsphS
|
||||
vKo4BucAQoJLfdD8Xl22RCRN9lpTzhvbl9iYUn0YV1scAU8xMmovBTdn9Z6QrIYqG5CMNn
|
||||
2fIr4TkAAAEQo4W2lqOFtpYAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
|
||||
AAAIUEAWGu5/oGKU2j/gKl/q4R6GC280hYwZ2psbGyKKgL4XKhV6V6g08nz+W23vv8a+oo
|
||||
iWTrgMa8bd9Eg+q+SUeJbOPIACh7qhDrKYUryqOAbnAEKCS33Q/F5dtkQkTfZaU84b25fY
|
||||
mFJ9GFdbHAFPMTJqLwU3Z/WekKyGKhuQjDZ9nyK+E5AAAAQgCPHZ4xrsWwGgVy8H54DRD0
|
||||
g8ihcJ7Fsa7I84mTd0N4x2EbHvrmDChyreZ9MelKG8Jvwea0D4BtnCDPXEfkHAw4NwAAAB
|
||||
BhZGFtd2lja0BlcmdhdGVzAQI=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/ecdsa521b.pub
Normal file
1
tests/ssh_keys/ecdsa521b.pub
Normal file
@@ -0,0 +1 @@
|
||||
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFhruf6BilNo/4Cpf6uEehgtvNIWMGdqbGxsiioC+FyoVeleoNPJ8/ltt77/GvqKIlk64DGvG3fRIPqvklHiWzjyAAoe6oQ6ymFK8qjgG5wBCgkt90PxeXbZEJE32WlPOG9uX2JhSfRhXWxwBTzEyai8FN2f1npCshiobkIw2fZ8ivhOQ== adamwick@ergates
|
||||
8
tests/ssh_keys/ed25519_pw_hush1
Normal file
8
tests/ssh_keys/ed25519_pw_hush1
Normal file
@@ -0,0 +1,8 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBUz8HBJf
|
||||
hkFUWB/uRKkshkAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIN8pW2btKRHc65DS
|
||||
yKjL7XBtvvPTHGd5KoI6L4KatxRZAAAAoDCMQNOT746RIs2IR7DixViQ4QjzodwVo//Y2Q
|
||||
J/aE/PdKJ34kmngtnyyredzNseTG838n5PpW2Jo5R0JpGcRcyX/KC0lUHfDlnyHo29sNqx
|
||||
fI39BEZnV87bA+pJhHKhLGxplWY9Hns+cpsh3mgNZGOC3M9zT1geO3AHdXMhI4HnYxw2/G
|
||||
OUW1muIrFzKaRV2bl7kKwH1DRyYqp3VxfS+oo=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/ed25519_pw_hush1.pub
Normal file
1
tests/ssh_keys/ed25519_pw_hush1.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN8pW2btKRHc65DSyKjL7XBtvvPTHGd5KoI6L4KatxRZ adamwick@ergates
|
||||
8
tests/ssh_keys/ed25519_pw_hush2
Normal file
8
tests/ssh_keys/ed25519_pw_hush2
Normal file
@@ -0,0 +1,8 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCuOPKhvD
|
||||
nx/dHJW4bC7y4uAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIET2SpCeHNG/3IIL
|
||||
j80xTImmn2JEcTRVM4khSQ/rCQ3FAAAAoIoSFrEKoEIGSAsr07HI/tSt2W/DKgeTYsUmNi
|
||||
r8suuyfNqETjlfZ9FWxw0UWW1mxA/XzLvRYx+kGPkqKM8DsCpVCMl2S/3hMCCoxhE5mKP9
|
||||
PE+VV0DKa6jywEcgMEiMejoRNnfHAAvI9KHAPPdIR11geqEMAx3nZFRy0IyZwxNYPFbijM
|
||||
mjv8RcoFGVoHT2R4abpO6oEADjJ0KqZRNdg9k=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/ed25519_pw_hush2.pub
Normal file
1
tests/ssh_keys/ed25519_pw_hush2.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIET2SpCeHNG/3IILj80xTImmn2JEcTRVM4khSQ/rCQ3F adamwick@ergates
|
||||
7
tests/ssh_keys/ed25519a
Normal file
7
tests/ssh_keys/ed25519a
Normal file
@@ -0,0 +1,7 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACCA4kdqb8sTeg7amwY8Tdck2zEbqcXDRFva/4VRFWNY0wAAAJj4Nb/Z+DW/
|
||||
2QAAAAtzc2gtZWQyNTUxOQAAACCA4kdqb8sTeg7amwY8Tdck2zEbqcXDRFva/4VRFWNY0w
|
||||
AAAEB+W/KcnOrffyr18T1GttW8Z6yutReqViIkm6cgOUAA7YDiR2pvyxN6DtqbBjxN1yTb
|
||||
MRupxcNEW9r/hVEVY1jTAAAAEGFkYW13aWNrQGVyZ2F0ZXMBAgMEBQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/ed25519a.pub
Normal file
1
tests/ssh_keys/ed25519a.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIDiR2pvyxN6DtqbBjxN1yTbMRupxcNEW9r/hVEVY1jT adamwick@ergates
|
||||
7
tests/ssh_keys/ed25519b
Normal file
7
tests/ssh_keys/ed25519b
Normal file
@@ -0,0 +1,7 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACCfk3xXxqLhgBYvgUyAC+vBu+3XBD/CY8SY7wp2hrXhEQAAAJj50iU4+dIl
|
||||
OAAAAAtzc2gtZWQyNTUxOQAAACCfk3xXxqLhgBYvgUyAC+vBu+3XBD/CY8SY7wp2hrXhEQ
|
||||
AAAED8AA2vVnMt9sKP6psihSU9ldHjV9yv1pOA2XmdyDcCsZ+TfFfGouGAFi+BTIAL68G7
|
||||
7dcEP8JjxJjvCnaGteERAAAAEGFkYW13aWNrQGVyZ2F0ZXMBAgMEBQ==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/ed25519b.pub
Normal file
1
tests/ssh_keys/ed25519b.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ+TfFfGouGAFi+BTIAL68G77dcEP8JjxJjvCnaGteER adamwick@ergates
|
||||
170
tests/ssh_keys/rsa15360a
Normal file
170
tests/ssh_keys/rsa15360a
Normal file
@@ -0,0 +1,170 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
13aWNrQGVyZ2F0ZXMBAg==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa15360a.pub
Normal file
1
tests/ssh_keys/rsa15360a.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa 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 adamwick@ergates
|
||||
170
tests/ssh_keys/rsa15360b
Normal file
170
tests/ssh_keys/rsa15360b
Normal file
@@ -0,0 +1,170 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
dpY2tAZXJnYXRlcwECAw==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa15360b.pub
Normal file
1
tests/ssh_keys/rsa15360b.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa 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 adamwick@ergates
|
||||
49
tests/ssh_keys/rsa4096a
Normal file
49
tests/ssh_keys/rsa4096a
Normal file
@@ -0,0 +1,49 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa4096a.pub
Normal file
1
tests/ssh_keys/rsa4096a.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC3ftJT8JKsBlMHj+mJ2JLUCH7da6Rn2KxKO4+9LzoZRnylf8EB7uO/nq/tyLyMMHBv8d25m0xne498z4VvKF/r4wt/gvWkmcauHL3WqTTJBfzc4oSGacVrCvUXX1LaSt/ZSuIUDLqWBE4lONFmdfInaB8ozqWjIgX3njhw9yNl/k53ZnAWiaOnG71tlIWha+jxubZ/T7RTp/4tiWpubWOF4QCDAbAAijDe3C8wvIlmKihZMdl0nPLx11Ope+uX/VMTZlmdYUpy9KkiyKwO2A5PFVmbqpb51WHVBEwJDVpOOda+Fow24R1ZWqVcUGtvau1jBLxC7MvqNPx1zNHKRWbQyRSug9B8DgYdTxVkU1bb8kmDA67apyl8Qr+CB7xECRUyTcDfigSJ2djbBaVgIe3LVHQeJAZNaZNy6FnhkxpuSBYxOBAOCzTrIIacYGivMF5/JjfO2cFH3y26UJbP+PXoZSYYSojg2Ksk3j+pZJTjr3tDqnJkfO/bMId9cNe+Csp55rg+IzcXfQxBPdmS1oyVi2MLY0mYDx/BlZRvIg5Hj+4SuY4owpSi1Ap5aZOKb/Su0YURu2zVQQBxmyTkbQoFB0wopoiM6nQZZCZayCjM36jqp9rsA83L89drtkrYUESR3rJ2boUhSy9lV3bT2frSmMtHqjNpToN1/o6sCva2tw== adamwick@ergates
|
||||
49
tests/ssh_keys/rsa4096b
Normal file
49
tests/ssh_keys/rsa4096b
Normal file
@@ -0,0 +1,49 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa4096b.pub
Normal file
1
tests/ssh_keys/rsa4096b.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCzT0NGauteX9iS/LQu3gunlDM4G8sCB5mG0diVelmCoj6QpeGXSc6LIH9AQSd5XFQ6p/nzsU5NeXjpjsOkov7ADl6MLspx0QcvT49KwpAk5lHU7JVAEq9BwRC74MHbKXng3Rc18bEy1RVJHCurxLEbOYlNbGOTGsFac9M90SPZAg/s+Rg63HJvrg6HTI0IjyjbVDuCErELGdpj2xc2prWzudGmuMkSqlX3hbv6KOrUuUFobwRnyRqF4sWSQ2Mf5stji6DIzaSLWBB5GaLMYDe4xWBC4P26/fzRF3zzf7t9WlR4G+2YFR9uzHwZmdRpsVKbWV4epFNVOvJ5uFEQyew++Advs4D41qFDjOUHkhH7XvWR+cdqMxkxw3Vg+aF1+lYFYiMyeaUxXeYokS7wmHL01F/gGWHrlTNwzC8qRBBnisNihuDYn0aZoxL4gaJKY+9bZhilvbk2+Xp5AgkE36L+kjPFZYKsEChGy6qSK/Mgoa+HQgiCDQCivXVVCGnAX4hWW5wzkgdCMiOV3cR2DXIlAXgKzsrD4OY+Z2waKEw1ZsMiuA23vAdjPddKmP0TXbDjLTEAgO7iXeV+RIq22T34mS+MGW7HsXH4ifzOxFTi4M5eZyY853HDFf0SUijVNzuZfQkDDjrRNUnwGhamiKgfu3jRO9mWrLUewtJbw/RW7Q== adamwick@ergates
|
||||
88
tests/ssh_keys/rsa7680a
Normal file
88
tests/ssh_keys/rsa7680a
Normal file
@@ -0,0 +1,88 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
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
|
||||
==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
1
tests/ssh_keys/rsa7680a.pub
Normal file
1
tests/ssh_keys/rsa7680a.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAADwQDawzhmtS+TPz4sxdIpD+UhMDXam7dTg3zbRO8WCzVYSIly5T7tJfV3EBtzfRqzjsV/KhyY6c2tA0sF8xSbcX/az6gWcYSO4BDeKKFjXq7VCKiK6eSZT0KE2VY3sdVdP/uRRvuXsZlRNz98Wpv6nR2oX+8N+rf+X8MAouNPeh2MAcPsAPsZs6aaOkVSRWpZCGaB7OJDY6r2WnS68K8KkJA3W5F5BPr4yHSpzbqWaxveYyrx3esNFoDrUZdFhFVmsuLO4B+S7l/cCXVfEoNKLRbUMySmydXaMSzE85LHqzGlvICLhf4RylMy1WmYWuaZSoIjgu3Aey4KAnU9vtFv7GMB+Imv9nTRDUN5U5wm0Jm8w0j8b9apUo1lQ241CS9DfjLKBgBwwNv7/C+m3e9q5erwcmVNNiutZwFa07FEZBfzL9HQdnLk8AJNpn1Srt75+jzsUOFtDSENaBbjozZookIe1yOm5erM44Y0GaEYMmDuhvqvvB5xOyKBqAsFhN9KmOP2q1qttMbGBe12CuIglB8Rqjeja/T/n02VUSniKLZ1XaclT5g2dwNZtSZluNdip5J3ja/kYCt+pUenYSiD+FEljgofAEi0vznnScjjPXa2Q98N4jw9q3W7+btmOAEdoyJxz8syiE8FNvFHgOCMBm75oaihzgzZkzgpcDMdBjIjCHT4PM3Umengu1dg3YFoOLcU7j+oxuSajVbLibodPfgJcQl5XYVWVwOYM5/YddywxrS6hjS0tc+JGz4oSZGN8SPSw1inkgJ5mQSyOpuYSeJnZDhLgGt8B38fOs7s9hI44MSefwfycrVWt3KTwy+UJdhGiRKqnpzdpvSNXdkQpwFDbUwZJi6hJv8zPj2aNZenm1IjQ8EJN/NS0p2xYUIuBi99aecO9Xf2sIYB98K6KZCMbB1XogUE0gdKeAhkU19ZJ4/ZaWsLiVh97u2mhcpyDCZCY0HXkpvVeLkJ1h2wgomd6pWX1fhzBBGknTCWT8B5QZ0d8vwo1jqrzMaZgpabU08aSgO/OVHd0Yytz45AX8QYrg9EpFPgBxlnF1+HE1WrT70nbkyx06OFNw6VqpW7tLBYVq1F9ay7JTObakdwfDJwu82Dy347XLBVVU4VKC2pc50fJdmEO5X4dS2n3FT99sJqYD1tbRjAXorQIjJYcLOHmHIIakZQxEtOf5SpSaStFD/pH4BGb/wrKgmtzKMD4Ya7BkiIS5f6popvAaPmKlCYKJHrFQrYFxMC6p7z5WXfTIPr8sCTsLLWAy1J33Zk1r8= adamwick@ergates
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user