cleaned up TLS code, added certificate and key checks to warpgate check

This commit is contained in:
Eugene Pankov 2022-07-26 21:04:24 +02:00
parent be98a00bb2
commit 03db7b55fa
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
24 changed files with 313 additions and 182 deletions

6
Cargo.lock generated
View file

@ -4391,7 +4391,6 @@ dependencies = [
"warpgate-common",
"warpgate-db-entities",
"warpgate-protocol-ssh",
"warpgate-web",
]
[[package]]
@ -4414,6 +4413,8 @@ dependencies = [
"rand",
"rand_chacha",
"rand_core",
"rustls",
"rustls-pemfile",
"sea-orm",
"serde",
"serde_json",
@ -4427,6 +4428,7 @@ dependencies = [
"uuid",
"warpgate-db-entities",
"warpgate-db-migrations",
"webpki",
]
[[package]]
@ -4501,7 +4503,6 @@ dependencies = [
"anyhow",
"async-trait",
"bytes",
"delegate",
"mysql_common",
"once_cell",
"password-hash 0.2.3",
@ -4515,7 +4516,6 @@ dependencies = [
"tokio-rustls",
"tracing",
"uuid",
"warpgate-admin",
"warpgate-common",
"warpgate-database-protocols",
"warpgate-db-entities",

View file

@ -31,3 +31,6 @@ openapi:
cd warpgate-web && yarn openapi:client:admin && yarn openapi:client:gateway
cleanup: (fix "--allow-dirty") (clippy "--fix" "--allow-dirty") fmt svelte-check lint
udeps:
cargo udeps --all-targets

View file

@ -40,5 +40,4 @@ uuid = { version = "1.0", features = ["v4", "serde"] }
warpgate-common = { version = "*", path = "../warpgate-common" }
warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" }
warpgate-protocol-ssh = { version = "*", path = "../warpgate-protocol-ssh" }
warpgate-web = { version = "*", path = "../warpgate-web" }
regex = "1.6"

View file

@ -9,7 +9,7 @@ anyhow = "1.0"
argon2 = "0.4"
async-trait = "0.1"
bytes = "1.2"
chrono = {version = "0.4", features = ["serde"]}
chrono = { version = "0.4", features = ["serde"] }
data-encoding = "2.3"
humantime-serde = "1.1"
lazy_static = "1.4"
@ -17,20 +17,32 @@ once_cell = "1.10"
packet = "0.1"
password-hash = "0.4"
poem = "^1.3.30"
poem-openapi = {version = "2.0.4", features = ["swagger-ui", "chrono", "uuid", "static-files"]}
poem-openapi = { version = "2.0.4", features = [
"swagger-ui",
"chrono",
"uuid",
"static-files",
] }
rand = "0.8"
rand_chacha = "0.3"
rand_core = {version = "0.6", features = ["std"]}
sea-orm = {version = "^0.9", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"], default-features = false}
rand_core = { version = "0.6", features = ["std"] }
sea-orm = { version = "^0.9", features = [
"sqlx-sqlite",
"runtime-tokio-native-tls",
"macros",
], default-features = false }
serde = "1.0"
serde_json = "1.0"
thiserror = "1.0"
tokio = {version = "1.20", features = ["tracing"]}
totp-rs = {version = "2.0", features = ["otpauth"]}
tokio = { version = "1.20", features = ["tracing"] }
totp-rs = { version = "2.0", features = ["otpauth"] }
tracing = "0.1"
tracing-core = "0.1"
tracing-subscriber = "0.3"
url = "2.2"
uuid = {version = "1.0", features = ["v4", "serde"]}
warpgate-db-entities = {version = "*", path = "../warpgate-db-entities"}
warpgate-db-migrations = {version = "*", path = "../warpgate-db-migrations"}
uuid = { version = "1.0", features = ["v4", "serde"] }
warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" }
warpgate-db-migrations = { version = "*", path = "../warpgate-db-migrations" }
rustls = { version = "0.20", features = ["dangerous_configuration"] }
rustls-pemfile = "1.0"
webpki = "0.22"

View file

@ -230,7 +230,6 @@ fn _default_ssh_keys_path() -> String {
"./data/keys".to_owned()
}
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq, Copy)]
pub enum SshHostKeyVerificationMode {
#[serde(rename = "prompt")]

View file

@ -13,6 +13,7 @@ mod protocols;
pub mod recordings;
mod services;
mod state;
mod tls;
mod try_macro;
mod types;
@ -23,5 +24,6 @@ pub use error::WarpgateError;
pub use protocols::*;
pub use services::*;
pub use state::{SessionState, SessionStateInit, State};
pub use tls::*;
pub use try_macro::*;
pub use types::*;

View file

@ -13,11 +13,11 @@ pub enum TargetTestError {
Unreachable,
#[error("authentication failed")]
AuthenticationError,
#[error("connection error")]
#[error("connection error: {0}")]
ConnectionError(String),
#[error("misconfigured")]
#[error("misconfigured: {0}")]
Misconfigured(String),
#[error("I/O")]
#[error("I/O: {0}")]
Io(#[from] std::io::Error),
}

View file

@ -19,13 +19,13 @@ use writer::RecordingWriter;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("I/O")]
#[error("I/O: {0}")]
Io(#[from] std::io::Error),
#[error("Database")]
#[error("Database: {0}")]
Database(#[from] sea_orm::DbErr),
#[error("Failed to serialize a recording item")]
#[error("Failed to serialize a recording item: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Writer is closed")]

View file

@ -4,8 +4,7 @@ use tokio::time::Instant;
use warpgate_db_entities::Recording::RecordingKind;
use super::writer::RecordingWriter;
use super::Recorder;
use super::{Result, Error};
use super::{Error, Recorder, Result};
#[derive(Serialize)]
#[serde(untagged)]

View file

@ -0,0 +1,111 @@
use std::path::Path;
use std::sync::Arc;
use poem::listener::RustlsCertificate;
use rustls::sign::{CertifiedKey, SigningKey};
use rustls::{Certificate, PrivateKey};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use crate::RustlsSetupError;
pub struct TlsCertificateBundle {
bytes: Vec<u8>,
certificates: Vec<Certificate>,
}
pub struct TlsPrivateKey {
bytes: Vec<u8>,
key: Arc<dyn SigningKey>,
}
pub struct TlsCertificateAndPrivateKey {
pub certificate: TlsCertificateBundle,
pub private_key: TlsPrivateKey,
}
impl TlsCertificateBundle {
pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, RustlsSetupError> {
let mut file = File::open(path).await?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes).await?;
Self::from_bytes(bytes)
}
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, RustlsSetupError> {
let certificates = rustls_pemfile::certs(&mut &bytes[..]).map(|mut certs| {
certs
.drain(..)
.map(Certificate)
.collect::<Vec<Certificate>>()
})?;
if certificates.is_empty() {
return Err(RustlsSetupError::NoCertificates)
}
Ok(Self {
bytes,
certificates,
})
}
}
impl TlsPrivateKey {
pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, RustlsSetupError> {
let mut file = File::open(path).await?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes).await?;
Self::from_bytes(bytes)
}
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, RustlsSetupError> {
let mut key = rustls_pemfile::pkcs8_private_keys(&mut bytes.as_slice())?
.drain(..)
.next()
.map(PrivateKey);
if key.is_none() {
key = rustls_pemfile::rsa_private_keys(&mut bytes.as_slice())?
.drain(..)
.next()
.map(PrivateKey);
}
let key = key.ok_or(RustlsSetupError::NoKeys)?;
let key = rustls::sign::any_supported_type(&key)?;
Ok(Self { bytes, key })
}
}
impl Into<Vec<u8>> for TlsCertificateBundle {
fn into(self) -> Vec<u8> {
self.bytes
}
}
impl Into<Vec<u8>> for TlsPrivateKey {
fn into(self) -> Vec<u8> {
self.bytes
}
}
impl Into<RustlsCertificate> for TlsCertificateAndPrivateKey {
fn into(self) -> RustlsCertificate {
RustlsCertificate::new()
.cert(self.certificate)
.key(self.private_key)
}
}
impl Into<CertifiedKey> for TlsCertificateAndPrivateKey {
fn into(self) -> CertifiedKey {
let cert = self.certificate;
let key = self.private_key;
CertifiedKey {
cert: cert.certificates,
key: key.key,
ocsp: None,
sct_list: None,
}
}
}

View file

@ -0,0 +1,15 @@
#[derive(thiserror::Error, Debug)]
pub enum RustlsSetupError {
#[error("rustls: {0}")]
Rustls(#[from] rustls::Error),
#[error("sign: {0}")]
Sign(#[from] rustls::sign::SignError),
#[error("no certificates found in certificate file")]
NoCertificates,
#[error("no private keys found in key file")]
NoKeys,
#[error("I/O: {0}")]
Io(#[from] std::io::Error),
#[error("PKI: {0}")]
Pki(#[from] webpki::Error),
}

View file

@ -0,0 +1,5 @@
mod cert;
mod error;
pub use cert::*;
pub use error::*;

View file

@ -0,0 +1,4 @@
use uuid::Uuid;
pub type SessionId = Uuid;
pub type ProtocolName = &'static str;

View file

@ -2,72 +2,7 @@ use std::fmt::Debug;
use std::net::{SocketAddr, ToSocketAddrs};
use std::ops::Deref;
use bytes::Bytes;
use data_encoding::HEXLOWER;
use rand::Rng;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::helpers::rng::get_crypto_rng;
pub type SessionId = Uuid;
pub type ProtocolName = &'static str;
#[derive(PartialEq, Eq, Clone)]
pub struct Secret<T>(T);
impl Secret<String> {
pub fn random() -> Self {
Secret::new(HEXLOWER.encode(&Bytes::from_iter(get_crypto_rng().gen::<[u8; 32]>())))
}
}
impl<T> Secret<T> {
pub const fn new(v: T) -> Self {
Self(v)
}
pub fn expose_secret(&self) -> &T {
&self.0
}
}
impl<T> From<T> for Secret<T> {
fn from(v: T) -> Self {
Self::new(v)
}
}
impl<'de, T> Deserialize<'de> for Secret<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = Deserialize::deserialize::<D>(deserializer)?;
Ok(Self::new(v))
}
}
impl<T> Serialize for Secret<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<T> Debug for Secret<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<secret>")
}
}
#[derive(Clone)]
pub struct ListenEndpoint(pub SocketAddr);

View file

@ -0,0 +1,7 @@
mod aliases;
mod listen_endpoint;
mod secret;
pub use aliases::*;
pub use listen_endpoint::*;
pub use secret::*;

View file

@ -0,0 +1,64 @@
use std::fmt::Debug;
use bytes::Bytes;
use data_encoding::HEXLOWER;
use rand::Rng;
use serde::{Deserialize, Serialize};
use crate::helpers::rng::get_crypto_rng;
#[derive(PartialEq, Eq, Clone)]
pub struct Secret<T>(T);
impl Secret<String> {
pub fn random() -> Self {
Secret::new(HEXLOWER.encode(&Bytes::from_iter(get_crypto_rng().gen::<[u8; 32]>())))
}
}
impl<T> Secret<T> {
pub const fn new(v: T) -> Self {
Self(v)
}
pub fn expose_secret(&self) -> &T {
&self.0
}
}
impl<T> From<T> for Secret<T> {
fn from(v: T) -> Self {
Self::new(v)
}
}
impl<'de, T> Deserialize<'de> for Secret<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = Deserialize::deserialize::<D>(deserializer)?;
Ok(Self::new(v))
}
}
impl<T> Serialize for Secret<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<T> Debug for Secret<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<secret>")
}
}

View file

@ -19,7 +19,7 @@ use common::page_admin_auth;
pub use common::PROTOCOL_NAME;
use logging::{log_request_result, span_for_request};
use poem::endpoint::{EmbeddedFileEndpoint, EmbeddedFilesEndpoint};
use poem::listener::{Listener, RustlsCertificate, RustlsConfig, TcpListener};
use poem::listener::{Listener, RustlsConfig, TcpListener};
use poem::middleware::SetHeader;
use poem::session::{CookieConfig, MemoryStorage, ServerSession};
use poem::web::Data;
@ -28,7 +28,7 @@ use poem_openapi::OpenApiService;
use tokio::sync::Mutex;
use tracing::*;
use warpgate_admin::admin_api_app;
use warpgate_common::{ProtocolServer, Services, Target, TargetTestError};
use warpgate_common::{ProtocolServer, Services, Target, TargetTestError, TlsCertificateAndPrivateKey, TlsCertificateBundle, TlsPrivateKey};
use warpgate_web::Assets;
use crate::common::{endpoint_admin_auth, endpoint_auth, page_auth, COOKIE_MAX_AGE};
@ -136,29 +136,29 @@ impl ProtocolServer for HTTPProtocolServer {
}
});
let (certificate, key) = {
let certificate_and_key = {
let config = self.services.config.lock().await;
let certificate_path = config
.paths_relative_to
.join(&config.store.http.certificate);
let key_path = config.paths_relative_to.join(&config.store.http.key);
(
std::fs::read(&certificate_path).with_context(|| {
TlsCertificateAndPrivateKey {
certificate: TlsCertificateBundle::from_file(&certificate_path).await.with_context(|| {
format!("reading TLS private key from '{}'", key_path.display())
})?,
private_key: TlsPrivateKey::from_file(&key_path).await.with_context(|| {
format!(
"reading SSL certificate from '{}'",
"reading TLS certificate from '{}'",
certificate_path.display()
)
})?,
std::fs::read(&key_path).with_context(|| {
format!("reading SSL private key from '{}'", key_path.display())
})?,
)
}
};
info!(?address, "Listening");
Server::new(TcpListener::bind(address).rustls(
RustlsConfig::new().fallback(RustlsCertificate::new().cert(certificate).key(key)),
RustlsConfig::new().fallback(certificate_and_key.into()),
))
.run(app)
.await?;

View file

@ -5,7 +5,6 @@ name = "warpgate-protocol-mysql"
version = "0.3.0"
[dependencies]
warpgate-admin = { version = "*", path = "../warpgate-admin" }
warpgate-common = { version = "*", path = "../warpgate-common" }
warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" }
warpgate-database-protocols = { version = "*", path = "../warpgate-database-protocols" }
@ -19,7 +18,6 @@ mysql_common = "0.29"
rand = "0.8"
sha1 = "0.10.1"
password-hash = { version = "0.2", features = ["std"] }
delegate = "0.6"
rustls = { version = "0.20", features = ["dangerous_configuration"] }
rustls-pemfile = "1.0"
tokio-rustls = "0.23"

View file

@ -1,10 +1,10 @@
use std::error::Error;
use warpgate_common::WarpgateError;
use warpgate_common::{RustlsSetupError, WarpgateError};
use warpgate_database_protocols::error::Error as SqlxError;
use crate::stream::MySqlStreamError;
use crate::tls::{MaybeTlsStreamError, RustlsSetupError};
use crate::tls::MaybeTlsStreamError;
#[derive(thiserror::Error, Debug)]
pub enum MySqlError {

View file

@ -8,17 +8,22 @@ mod stream;
mod tls;
use std::fmt::Debug;
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::{Context, Result};
use async_trait::async_trait;
use rustls::server::NoClientAuth;
use rustls::ServerConfig;
use tokio::net::TcpListener;
use tracing::*;
use warpgate_common::{ProtocolServer, Services, SessionStateInit, Target, TargetTestError};
use warpgate_common::{
ProtocolServer, Services, SessionStateInit, Target, TargetTestError,
TlsCertificateAndPrivateKey, TlsCertificateBundle, TlsPrivateKey,
};
use crate::session::MySqlSession;
use crate::session_handle::MySqlSessionHandle;
use crate::tls::FromCertificateAndKey;
use crate::tls::ResolveServerCert;
pub struct MySQLProtocolServer {
services: Services,
@ -35,27 +40,34 @@ impl MySQLProtocolServer {
#[async_trait]
impl ProtocolServer for MySQLProtocolServer {
async fn run(self, address: SocketAddr) -> Result<()> {
let (certificate, key) = {
let certificate_and_key = {
let config = self.services.config.lock().await;
let certificate_path = config
.paths_relative_to
.join(&config.store.mysql.certificate);
let key_path = config.paths_relative_to.join(&config.store.mysql.key);
(
std::fs::read(&certificate_path).with_context(|| {
TlsCertificateAndPrivateKey {
certificate: TlsCertificateBundle::from_file(&certificate_path)
.await
.with_context(|| {
format!("reading SSL private key from '{}'", key_path.display())
})?,
private_key: TlsPrivateKey::from_file(&key_path).await.with_context(|| {
format!(
"reading SSL certificate from '{}'",
certificate_path.display()
)
})?,
std::fs::read(&key_path).with_context(|| {
format!("reading SSL private key from '{}'", key_path.display())
})?,
)
}
};
let tls_config = ServerConfig::try_from_certificate_and_key(certificate, key)?;
let tls_config = ServerConfig::builder()
.with_safe_defaults()
.with_client_cert_verifier(NoClientAuth::new())
.with_cert_resolver(Arc::new(ResolveServerCert(Arc::new(
certificate_and_key.into(),
))));
info!(?address, "Listening");
let listener = TcpListener::bind(address).await?;

View file

@ -3,5 +3,5 @@ mod rustls_helpers;
mod rustls_root_certs;
pub use maybe_tls_stream::{MaybeTlsStream, MaybeTlsStreamError, UpgradableStream};
pub use rustls_helpers::{configure_tls_connector, FromCertificateAndKey, RustlsSetupError};
pub use rustls_helpers::{configure_tls_connector, ResolveServerCert};
pub use rustls_root_certs::ROOT_CERT_STORE;

View file

@ -3,75 +3,14 @@ use std::sync::Arc;
use std::time::SystemTime;
use rustls::client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier};
use rustls::server::{ClientHello, NoClientAuth, ResolvesServerCert};
use rustls::server::{ClientHello, ResolvesServerCert};
use rustls::sign::CertifiedKey;
use rustls::{Certificate, ClientConfig, Error as TlsError, PrivateKey, ServerConfig, ServerName};
use rustls::{ClientConfig, Error as TlsError, ServerName};
use warpgate_common::RustlsSetupError;
use super::ROOT_CERT_STORE;
#[derive(thiserror::Error, Debug)]
pub enum RustlsSetupError {
#[error("rustls")]
Rustls(#[from] rustls::Error),
#[error("sign")]
Sign(#[from] rustls::sign::SignError),
#[error("no private keys in key file")]
NoKeys,
#[error("I/O")]
Io(#[from] std::io::Error),
#[error("PKI")]
Pki(#[from] webpki::Error),
}
pub trait FromCertificateAndKey<E>
where
Self: Sized,
{
fn try_from_certificate_and_key(cert: Vec<u8>, key: Vec<u8>) -> Result<Self, E>;
}
impl FromCertificateAndKey<RustlsSetupError> for rustls::ServerConfig {
fn try_from_certificate_and_key(
cert: Vec<u8>,
key_bytes: Vec<u8>,
) -> Result<Self, RustlsSetupError> {
let certificates = rustls_pemfile::certs(&mut &cert[..]).map(|mut certs| {
certs
.drain(..)
.map(Certificate)
.collect::<Vec<Certificate>>()
})?;
let mut key = rustls_pemfile::pkcs8_private_keys(&mut key_bytes.as_slice())?
.drain(..)
.next()
.map(PrivateKey);
if key.is_none() {
key = rustls_pemfile::rsa_private_keys(&mut key_bytes.as_slice())?
.drain(..)
.next()
.map(PrivateKey);
}
let key = key.ok_or(RustlsSetupError::NoKeys)?;
let key = rustls::sign::any_supported_type(&key)?;
let cert_key = Arc::new(CertifiedKey {
cert: certificates,
key,
ocsp: None,
sct_list: None,
});
Ok(ServerConfig::builder()
.with_safe_defaults()
.with_client_cert_verifier(NoClientAuth::new())
.with_cert_resolver(Arc::new(ResolveServerCert(cert_key))))
}
}
struct ResolveServerCert(Arc<CertifiedKey>);
pub struct ResolveServerCert(pub Arc<CertifiedKey>);
impl ResolvesServerCert for ResolveServerCert {
fn resolve(&self, _: ClientHello) -> Option<Arc<CertifiedKey>> {
@ -117,7 +56,7 @@ pub async fn configure_tls_connector(
Ok(config)
}
struct DummyTlsVerifier;
pub struct DummyTlsVerifier;
impl ServerCertVerifier for DummyTlsVerifier {
fn verify_server_cert(

View file

@ -7,7 +7,9 @@ pub static ROOT_CERT_STORE: Lazy<RootCertStore> = Lazy::new(|| {
for cert in
rustls_native_certs::load_native_certs().expect("could not load root TLS certificates")
{
roots.add(&rustls::Certificate(cert.0)).expect("could not add root TLS certificate");
roots
.add(&rustls::Certificate(cert.0))
.expect("could not add root TLS certificate");
}
roots
});

View file

@ -1,10 +1,35 @@
use anyhow::Result;
use anyhow::{Context, Result};
use tracing::*;
use warpgate_common::{TlsCertificateBundle, TlsPrivateKey};
use crate::config::load_config;
pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
load_config(&cli.config, true)?;
let config = load_config(&cli.config, true)?;
if config.store.http.enable {
TlsCertificateBundle::from_file(
config
.paths_relative_to
.join(&config.store.http.certificate),
)
.await
.with_context(|| format!("Checking HTTPS certificate"))?;
TlsPrivateKey::from_file(config.paths_relative_to.join(&config.store.http.key))
.await
.with_context(|| format!("Checking HTTPS key"))?;
}
if config.store.mysql.enable {
TlsCertificateBundle::from_file(
config
.paths_relative_to
.join(&config.store.mysql.certificate),
)
.await
.with_context(|| format!("Checking MySQL certificate"))?;
TlsPrivateKey::from_file(config.paths_relative_to.join(&config.store.mysql.key))
.await
.with_context(|| format!("Checking MySQL key"))?;
}
info!("No problems found");
Ok(())
}