use std::io::{self, prelude::*};
use std::path::PathBuf;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::{Duration, Instant};
use anyhow::{bail, Context, Result};
use tracing::{debug, info, warn};
use crate::network::certs;
const ALPN_QUIC_HTTP: &[&[u8]] = &[b"hq-29"];
const PROMPT_TIMEOUT_SECS: u64 = 60;
pub fn rustls_client_config(
verifier: Arc<NikauCertVerification<'static>>,
) -> Result<Arc<dyn quinn::crypto::ClientConfig>> {
let mut rustls_config = quinn::rustls::ClientConfig::builder_with_provider(verifier.crypto_provider.clone())
.with_safe_default_protocol_versions().context("Failed to set client default protocol versions")?
.dangerous().with_custom_certificate_verifier(verifier.clone())
.with_client_auth_cert(
vec![verifier.our_cert.clone()],
verifier.our_privkey.clone_key(),
).context("Failed to assign client cert and privkey")?;
rustls_config.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect();
Ok(Arc::new(
quinn::crypto::rustls::QuicClientConfig::try_from(rustls_config)
.context("Failed to create QUIC client configuration")?
))
}
pub fn rustls_server_config(
verifier: Arc<NikauCertVerification<'static>>,
) -> Result<Arc<dyn quinn::crypto::ServerConfig>> {
let mut rustls_config = quinn::rustls::ServerConfig::builder_with_provider(verifier.crypto_provider.clone())
.with_safe_default_protocol_versions().context("Failed to set server default protocol versions")?
.with_client_cert_verifier(verifier.clone())
.with_single_cert(
vec![verifier.our_cert.clone()],
verifier.our_privkey.clone_key(),
).context("Failed to assign server cert and privkey")?;
rustls_config.alpn_protocols = ALPN_QUIC_HTTP.iter().map(|&x| x.into()).collect();
rustls_config.max_early_data_size = u32::MAX; Ok(Arc::new(
quinn::crypto::rustls::QuicServerConfig::try_from(rustls_config)
.context("Failed to create QUIC server configuration")?
))
}
#[derive(Debug)]
struct ApprovalState<'a> {
known_certs: Vec<rustls_pki_types::CertificateDer<'a>>,
prompt_active: bool,
}
#[derive(Debug)]
pub struct NikauCertVerification<'a> {
config_dir: PathBuf,
our_cert: rustls_pki_types::CertificateDer<'a>,
our_privkey: rustls_pki_types::PrivateKeyDer<'a>,
approved_cert_fingerprints: Vec<String>,
approval_state: RwLock<ApprovalState<'a>>,
fingerprint: Arc<Mutex<Option<String>>>,
crypto_provider: Arc<rustls::crypto::CryptoProvider>,
}
impl<'a> NikauCertVerification<'a> {
pub fn new(
splash_label: &str,
approved_cert_fingerprints: Vec<String>,
config_dir: &PathBuf,
fingerprint: Arc<Mutex<Option<String>>>,
) -> Result<Arc<Self>> {
let (our_cert, our_privkey) = certs::load_keypair(splash_label, config_dir)
.with_context(|| format!("Failed to load {} keypair", splash_label))?;
let approved_cert_fingerprints: Vec<String> = approved_cert_fingerprints
.into_iter()
.map(|fingerprint| fingerprint.to_lowercase().replace(':', ""))
.collect();
if !approved_cert_fingerprints.is_empty() {
info!(
"Configured {} preapproved fingerprints: {:?}",
approved_cert_fingerprints.len(),
approved_cert_fingerprints
)
}
Ok(Arc::new(NikauCertVerification {
config_dir: config_dir.clone(),
our_cert,
our_privkey,
approved_cert_fingerprints,
approval_state: RwLock::new(ApprovalState {
known_certs: certs::load_known_certs(config_dir)?,
prompt_active: false,
}),
fingerprint,
crypto_provider: Arc::new(rustls::crypto::ring::default_provider()),
}))
}
fn verify_cert(
&self,
their_cert: &rustls_pki_types::CertificateDer<'_>,
their_name: &str,
we_are_server: bool,
) -> Result<String> {
let their_cert_fingerprint = certs::fingerprint(their_cert);
if let Ok(mut approval_state) = self.approval_state.write() {
if approval_state.known_certs.contains(their_cert) {
info!(
"{} cert has been approved before: {}",
their_name, their_cert_fingerprint
);
return Ok(their_cert_fingerprint);
} else if self
.approved_cert_fingerprints
.contains(&their_cert_fingerprint)
{
info!(
"{} cert approved via --fingerprints: {}",
their_name, their_cert_fingerprint
);
approval_state.known_certs.push(their_cert.clone().into_owned());
return Ok(their_cert_fingerprint);
} else if approval_state.prompt_active {
bail!(
"{} cert rejected: Approval prompt is already pending",
their_name
);
} else {
approval_state.prompt_active = true;
}
} else {
bail!("Failed to lock known certs for check");
}
if prompt_unknown_cert(their_cert, we_are_server) {
info!("{} cert approved: {}", their_name, their_cert_fingerprint);
if let Err(e) =
certs::write_approved_cert(their_cert, &their_cert_fingerprint, &self.config_dir)
{
warn!(
"{} approved, but couldn't save cert to disk: {}",
their_name, e
);
}
if let Ok(mut approval_state) = self.approval_state.write() {
approval_state.known_certs.push(their_cert.clone().into_owned());
approval_state.prompt_active = false;
} else {
bail!("Failed to lock known certs for approval");
}
Ok(their_cert_fingerprint)
} else {
info!(
"{} cert not approved: {}",
their_name, their_cert_fingerprint
);
if let Ok(mut approval_state) = self.approval_state.write() {
approval_state.prompt_active = false;
} else {
bail!("Failed to lock known certs for disapproval");
}
bail!(
"{} cert wasn't approved by user: {}",
their_name,
their_cert_fingerprint
);
}
}
}
impl rustls::client::danger::ServerCertVerifier for NikauCertVerification<'_> {
fn verify_server_cert(
&self,
server_cert: &rustls_pki_types::CertificateDer<'_>,
_intermediates: &[rustls_pki_types::CertificateDer],
_server_name: &rustls_pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls_pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
if let Err(e) = self.verify_cert(server_cert, "Server", false) {
Err(rustls::Error::General(e.to_string()))
} else {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &rustls_pki_types::CertificateDer,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls12_signature(message, cert, dss, &self.crypto_provider.signature_verification_algorithms)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &rustls_pki_types::CertificateDer,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.crypto_provider.signature_verification_algorithms)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.crypto_provider.signature_verification_algorithms.supported_schemes()
}
}
impl<'a> rustls::server::danger::ClientCertVerifier for NikauCertVerification<'a> {
fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] {
&[]
}
fn verify_client_cert(
&self,
client_cert: &rustls_pki_types::CertificateDer<'_>,
_intermediates: &[rustls_pki_types::CertificateDer],
_now: rustls_pki_types::UnixTime,
) -> Result<rustls::server::danger::ClientCertVerified, rustls::Error> {
match self.verify_cert(client_cert, "Client", true) {
Err(e) => Err(rustls::Error::General(e.to_string())),
Ok(their_cert_fingerprint) => {
if let Ok(mut fingerprint) = self.fingerprint.lock() {
debug!(
"Saving fingerprint for connection: {}",
their_cert_fingerprint
);
if let Some(old_fingerprint) =
fingerprint.replace(their_cert_fingerprint.clone())
{
warn!("BUG: Obtained new client fingerprint {} but old fingerprint {} is still present, resetting state and rejecting new client (try again)", their_cert_fingerprint, old_fingerprint);
let _ = fingerprint.take();
Err(rustls::Error::General("Fingerprint is valid but an existing connection is still in progress, try again".to_string()))
} else {
Ok(rustls::server::danger::ClientCertVerified::assertion())
}
} else {
Err(rustls::Error::General(
"Failed to lock fingerprint".to_string(),
))
}
}
}
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &rustls_pki_types::CertificateDer,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls12_signature(message, cert, dss, &self.crypto_provider.signature_verification_algorithms)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &rustls_pki_types::CertificateDer,
dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.crypto_provider.signature_verification_algorithms)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.crypto_provider.signature_verification_algorithms.supported_schemes()
}
}
fn prompt_unknown_cert(their_cert: &rustls_pki_types::CertificateDer, we_are_server: bool) -> bool {
let their_cert_fingerprint = certs::fingerprint(their_cert);
if atty::isnt(atty::Stream::Stdin) {
warn!("Stdin is not a TTY, skipping user certificate approval prompt. Approve this cert by running the {} with '--fingerprints {}'", if we_are_server { "server" } else { "client" }, their_cert_fingerprint);
return false;
}
let message = if we_are_server {
format!(
"APPROVAL NEEDED: New unknown client connection
The server has received a connection from a new unknown client.
Only approve this if you are expecting a new client.
You will also likely need to confirm this connection on the client as well.
Comfirm that the client startup image has this fingerprint:
{}
Allow this new client and save its certificate for future connections? ({}s timeout) [y/N]
> ",
their_cert_fingerprint, PROMPT_TIMEOUT_SECS
)
} else {
format!(
"APPROVAL NEEDED: New unknown server connection
The client has connected to a new unknown server.
Only approve this if you are expecting to be connecting to a new server.
You will also likely need to confirm this connection on the server as well.
Confirm that the server startup image has this fingerprint:
{}
Allow this new server and save its certificate for future connections? ({}s timeout) [y/N]
> ",
their_cert_fingerprint, PROMPT_TIMEOUT_SECS
)
};
prompt_yn(&message, false)
}
fn prompt_yn(msg: &str, default: bool) -> bool {
match prompt_internal(msg) {
Ok(char_) => {
match char_ {
b'y' | b'Y' | b't' | b'T' => true,
_ => false,
}
}
Err(e) => {
warn!(
"Confirmation prompt failed, assuming '{}': {}",
if default { "yes" } else { "no" },
e
);
default
}
}
}
fn prompt_internal(msg: &str) -> Result<u8> {
let mut stdin = nonblock::NonBlockingReader::from_fd(io::stdin())
.context("Failed to set up nonblocking reader for stdin")?;
{
let mut discard = vec![];
stdin
.read_available(&mut discard)
.context("Failed to flush initial input")?;
}
let msg_formatted = msg.to_string();
let mut stdout = io::stdout();
stdout
.write_all(msg_formatted.as_bytes())
.context("Failed to write prompt to stdout")?;
stdout.flush().expect("Failed to flush stdout");
let end_at = Instant::now()
.checked_add(Duration::from_secs(PROMPT_TIMEOUT_SECS))
.expect("Failed to configure timeout");
let mut content = vec![];
loop {
thread::sleep(Duration::from_millis(50));
stdin
.read_available(&mut content)
.context("Failed to check for user input")?;
if let Some(c) = content.first() {
return Ok(*c);
}
if Instant::now() >= end_at {
println!();
bail!("Prompt timed out after {}s", PROMPT_TIMEOUT_SECS)
}
}
}