volli-manager 0.1.12

Manager for volli
Documentation
use std::{fs, net::ToSocketAddrs, sync::Arc, time::Duration};

use ed25519_dalek::{SigningKey, VerifyingKey};
use eyre::{Report, eyre};
use hex::encode as hex_encode;
use quinn::crypto::rustls::QuicServerConfig;
use quinn::{Endpoint, ServerConfig};
use rand_core::OsRng;
use rcgen::{CertifiedKey, generate_simple_self_signed};
use rustls::ServerConfig as TlsServerConfig;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use sha2::{Digest, Sha256};
use tokio_rustls::TlsAcceptor;

use crate::{
    config::ServerConfigOpts,
    keys::{
        load_bind_host, load_csk, load_profile_host, load_quic_port, load_signing_key,
        load_tcp_port, save_profile_host, secret_dir,
    },
};
use volli_core::token;
use volli_core::{DEFAULT_QUIC_PORT, DEFAULT_TCP_PORT, env_config};

#[derive(Debug)]
pub struct SigningInit {
    pub key: Arc<SigningKey>,
    pub newly_generated: bool,
    pub sk_path: Option<std::path::PathBuf>,
    pub pk_path: Option<std::path::PathBuf>,
    pub id: String,
    pub fingerprint: [u8; 32],
}

#[derive(Debug)]
pub struct CertInit {
    pub chain: Vec<CertificateDer<'static>>,
    pub key: PrivateKeyDer<'static>,
    pub fingerprint: String,
    pub cert_der: Vec<u8>,
    pub key_der: Vec<u8>,
    pub cert_path: Option<std::path::PathBuf>,
    pub key_path: Option<std::path::PathBuf>,
    pub newly_generated: bool,
}

pub fn init_signing(
    secret_dir: &std::path::Path,
    show_bootstrap: &mut bool,
) -> Result<SigningInit, Report> {
    let sk_path = secret_dir.join("manager_sk");
    let pk_path = secret_dir.join("manager_pk");
    if sk_path.exists() && pk_path.exists() {
        let key = load_signing_key(Some(secret_dir))?;
        let verifying: VerifyingKey = key.verifying_key();
        let id = hex_encode(verifying.to_bytes());
        let fp: [u8; 32] = Sha256::digest(verifying.to_bytes()).into();
        Ok(SigningInit {
            key: Arc::new(key),
            newly_generated: false,
            sk_path: Some(sk_path),
            pk_path: Some(pk_path),
            id,
            fingerprint: fp,
        })
    } else {
        *show_bootstrap = true;
        let mut rng = OsRng;
        let key = SigningKey::generate(&mut rng);
        let verifying: VerifyingKey = key.verifying_key();
        let id = hex_encode(verifying.to_bytes());
        let fp: [u8; 32] = Sha256::digest(verifying.to_bytes()).into();
        Ok(SigningInit {
            key: Arc::new(key),
            newly_generated: true,
            sk_path: Some(sk_path),
            pk_path: Some(pk_path),
            id,
            fingerprint: fp,
        })
    }
}

pub fn init_csk(profile: &str) -> Result<([u8; 32], u32, bool), Report> {
    match load_csk(profile)? {
        Some(v) => Ok((v.0, v.1, false)),
        None => {
            let mut k = [0u8; 32];
            getrandom::fill(&mut k)?;
            Ok((k, 1, true))
        }
    }
}

pub fn init_cert(cfg: &ServerConfigOpts, secret_dir: &std::path::Path) -> Result<CertInit, Report> {
    let (cert_fs, key_fs) = if cfg.cert.is_none() && cfg.key.is_none() {
        (
            Some(secret_dir.join("tls_cert.der")),
            Some(secret_dir.join("tls_key.der")),
        )
    } else {
        (
            cfg.cert.as_ref().map(std::path::PathBuf::from),
            cfg.key.as_ref().map(std::path::PathBuf::from),
        )
    };
    let (c_der, k_der, persist) = if let (Some(c), Some(k)) = (cert_fs.as_ref(), key_fs.as_ref()) {
        if c.exists() && k.exists() {
            (std::fs::read(c)?, std::fs::read(k)?, false)
        } else {
            let CertifiedKey { cert, signing_key } =
                generate_simple_self_signed(vec!["volli".into()])?;
            (cert.der().to_vec(), signing_key.serialize_der(), true)
        }
    } else {
        let CertifiedKey { cert, signing_key } = generate_simple_self_signed(vec!["volli".into()])?;
        (cert.der().to_vec(), signing_key.serialize_der(), false)
    };
    let fp = hex_encode(Sha256::digest(&c_der));
    Ok(CertInit {
        chain: vec![CertificateDer::from(c_der.clone())],
        key: PrivateKeyDer::Pkcs8(k_der.clone().into()),
        fingerprint: fp,
        cert_der: c_der,
        key_der: k_der,
        cert_path: cert_fs,
        key_path: key_fs,
        newly_generated: persist,
    })
}

pub fn setup_tls_acceptor(
    certs: &[CertificateDer<'static>],
    key: &PrivateKeyDer<'static>,
) -> Result<TlsAcceptor, Report> {
    let mut config = TlsServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(certs.to_vec(), key.clone_key())?;
    config.alpn_protocols = vec![b"volli/worker".to_vec(), b"volli/manager".to_vec()];
    Ok(TlsAcceptor::from(Arc::new(config)))
}

pub fn setup_quic(
    host: &str,
    port: u16,
    certs: &[CertificateDer<'static>],
    key: &PrivateKeyDer<'static>,
) -> Result<Endpoint, Report> {
    let mut tls = TlsServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(certs.to_vec(), key.clone_key())?;
    tls.alpn_protocols = vec![b"volli/worker".to_vec(), b"volli/manager".to_vec()];
    let quic_crypto = QuicServerConfig::try_from(tls)?;
    let mut server_config = ServerConfig::with_crypto(Arc::new(quic_crypto));
    let ecfg = env_config();
    let hb = ecfg.heartbeat_secs();
    let mut transport = quinn::TransportConfig::default();
    transport.keep_alive_interval(Some(Duration::from_secs(hb)));
    transport.max_idle_timeout(Some(ecfg.quic_idle_duration()?.try_into()?));
    server_config.transport = Arc::new(transport);
    server_config.retry_token_lifetime(Duration::from_millis(1000));
    let addr = (host, port)
        .to_socket_addrs()?
        .next()
        .ok_or_else(|| eyre!("invalid bind addr"))?;
    let endpoint = Endpoint::server(server_config, addr)?;
    Ok(endpoint)
}

#[allow(dead_code)]
pub fn load_or_generate_cert(
    cert_path: Option<&str>,
    key_path: Option<&str>,
    secret_dir: Option<&std::path::Path>,
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>, String), Report> {
    let (cert_path_fs, key_path_fs) = if cert_path.is_none() && key_path.is_none() {
        let dir = secret_dir
            .map(std::path::PathBuf::from)
            .unwrap_or_else(crate::keys::default_secret_dir);
        (
            Some(dir.join("tls_cert.der")),
            Some(dir.join("tls_key.der")),
        )
    } else {
        (
            cert_path.map(std::path::PathBuf::from),
            key_path.map(std::path::PathBuf::from),
        )
    };

    let (cert_der, key_der) = if let (Some(c), Some(k)) =
        (cert_path_fs.as_ref(), key_path_fs.as_ref())
    {
        if c.exists() && k.exists() {
            (fs::read(c)?, fs::read(k)?)
        } else {
            if let Some(parent) = c.parent() {
                fs::create_dir_all(parent)?;
            }
            let CertifiedKey { cert, signing_key } =
                generate_simple_self_signed(vec!["volli".into()])?;
            let cert_der = cert.der().to_vec();
            let key_der = signing_key.serialize_der();
            fs::write(c, &cert_der)?;
            fs::write(k, &key_der)?;
            (cert_der, key_der)
        }
    } else {
        let CertifiedKey { cert, signing_key } = generate_simple_self_signed(vec!["volli".into()])?;
        (cert.der().to_vec(), signing_key.serialize_der())
    };

    let fingerprint = hex_encode(Sha256::digest(&cert_der));
    Ok((
        vec![CertificateDer::from(cert_der)],
        PrivateKeyDer::Pkcs8(key_der.into()),
        fingerprint,
    ))
}

pub fn build_secret(
    csk: &[u8; 32],
    host: &str,
    quic_port: u16,
    tcp_port: u16,
    cert_der: &[u8],
) -> Result<String, Report> {
    let token = token::issue_bootstrap_token(
        csk,
        "self",
        "default",
        "*",
        86_400,
        host,
        quic_port,
        tcp_port,
        cert_der.to_vec(),
    )?;
    token::encode_token(&token)
}

/// Generate a join secret for the given profile using persisted configuration.
pub fn join_secret(profile: &str) -> Result<String, Report> {
    let (csk, _) = load_csk(profile)?.ok_or_else(|| eyre!("missing cluster key"))?;
    let host = match load_profile_host(profile)? {
        Some(h) => h,
        None => {
            let fallback = load_bind_host(profile)?.unwrap_or_else(|| "127.0.0.1".into());
            let _ = save_profile_host(profile, &fallback);
            fallback
        }
    };
    let quic = load_quic_port(profile)?.unwrap_or(DEFAULT_QUIC_PORT);
    let tcp = load_tcp_port(profile)?.unwrap_or(DEFAULT_TCP_PORT);
    let dir = secret_dir(Some(profile));
    let cert_der = std::fs::read(dir.join("tls_cert.der"))?;
    build_secret(&csk, &host, quic, tcp, &cert_der)
}