~klve/poptea

229378cf25b07db01c5dc83a5691f73716c76a34 — klve bunc ntnn 5 months ago d1b2b04 f/client-tofu
Tofu poc with persistence to file
6 files changed, 159 insertions(+), 22 deletions(-)

M Cargo.lock
M Cargo.toml
M src/bin/poptea-cli.rs
M src/infra/client.rs
M src/infra/fs.rs
M src/lib.rs
M Cargo.lock => Cargo.lock +67 -0
@@ 15,6 15,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"

[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
 "block-padding",
 "generic-array",
]

[[package]]
name = "block-padding"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"

[[package]]
name = "bumpalo"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 73,6 89,15 @@ dependencies = [
]

[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
 "generic-array",
]

[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 83,6 108,16 @@ dependencies = [
]

[[package]]
name = "generic-array"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
dependencies = [
 "typenum",
 "version_check",
]

[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 103,6 138,12 @@ dependencies = [
]

[[package]]
name = "keccak"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7"

[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 198,6 239,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"

[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"

[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 207,7 254,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
name = "poptea"
version = "0.1.0"
dependencies = [
 "data-encoding",
 "rustls",
 "sha3",
 "url",
 "x509-parser",
]


@@ 277,6 326,18 @@ dependencies = [
]

[[package]]
name = "sha3"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809"
dependencies = [
 "block-buffer",
 "digest",
 "keccak",
 "opaque-debug",
]

[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"


@@ 329,6 390,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"

[[package]]
name = "typenum"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"

[[package]]
name = "unicode-bidi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"

M Cargo.toml => Cargo.toml +2 -0
@@ 9,3 9,5 @@ edition = "2021"
rustls = {version = "0.20.1", features = ["dangerous_configuration"]}
url = "2.2.2"
x509-parser = "0.12.0"
sha3 = "0.9.1"
data-encoding = "2.3.2"

M src/bin/poptea-cli.rs => src/bin/poptea-cli.rs +10 -3
@@ 1,15 1,22 @@
use io::Write;
use poptea::GeminiClient;
use std::{io, sync::Arc};
use std::{
    io,
    sync::{Arc, Mutex},
};

fn main() {
    let url = std::env::args().nth(1).expect("please provide gemini url");
    let fs = poptea::FileSystem::new("~/.poptea".into());
    let fs = Arc::new(
        Mutex::new(poptea::FileSystem::new(".poptea".into()))
    );

    let client = poptea::TlsClient::new(Arc::new(fs));
    let client = poptea::TlsClient::new(fs.clone());
    let res = client.get(&url).expect("failed to make a request");

    io::stdout()
        .write_all(&res.body.unwrap_or_else(|| b"response has no body".to_vec()))
        .expect("failed to write to stdout");

    fs.lock().unwrap().flush_trust_store();
}

M src/infra/client.rs => src/infra/client.rs +17 -11
@@ 1,23 1,30 @@
use std::fs::File;
use url::Url;

use data_encoding::BASE32HEX_NOPAD;
use io::{Read, Write};
use sha3::{Digest, Sha3_256};
use std::str::FromStr;
use std::sync::Arc;
use std::{io, io::BufRead, convert::Into};
use std::sync::{Arc, Mutex};
use std::{convert::Into, io, io::BufRead};
use x509_parser::prelude::*;

use crate::{GemResponse, GemStatus, GeminiClient, PopResult, TrustStore, VerifyStatus};

fn fingerprint(cert: &rustls::Certificate) -> PopResult<(String, String)> {
    let (_, pk) = X509Certificate::from_der(cert.as_ref()).unwrap();
    let res = pk.public_key().subject_public_key.as_ref();
    let sub = pk.subject().to_string();
    let sub_pk = pk.public_key().subject_public_key.as_ref();

    Ok((pk.subject().to_string(), format!("{:?}", res)))
    let mut hasher = Sha3_256::new();
    hasher.update(sub_pk);
    let result = hasher.finalize();

    Ok((sub[3..].to_string(), BASE32HEX_NOPAD.encode(&result[..])))
}

struct TofuVerification {
    store: Arc<dyn TrustStore>,
    store: Arc<Mutex<dyn TrustStore>>,
}

impl rustls::client::ServerCertVerifier for TofuVerification {


@@ 34,22 41,21 @@ impl rustls::client::ServerCertVerifier for TofuVerification {
        let mut file = File::create(path).unwrap();
        file.write_all(cert.as_ref()).unwrap();
        let (addr, fingerprint) = fingerprint(cert).unwrap();
        match self.store.verify(&addr, fingerprint) {
        let store = self.store.lock().unwrap().verify(&addr, fingerprint);
        match store {
            Ok(VerifyStatus::Trusted) => Ok(rustls::client::ServerCertVerified::assertion()),
            Ok(VerifyStatus::Untrusted) => {
                Err(rustls::Error::InvalidCertificateData("untrusted".into()))
            }
            Ok(VerifyStatus::Untrusted) => Err(rustls::Error::General("untrusted".into())),
            Err(_) => Err(rustls::Error::General("storage error".into())),
        }
    }
}

pub struct TlsClient {
    store: Arc<dyn TrustStore>,
    store: Arc<Mutex<dyn TrustStore>>,
}

impl TlsClient {
    pub fn new(store: Arc<dyn TrustStore>) -> Self {
    pub fn new(store: Arc<Mutex<dyn TrustStore>>) -> Self {
        Self { store }
    }


M src/infra/fs.rs => src/infra/fs.rs +61 -5
@@ 1,16 1,72 @@
use crate::{PopResult, TrustStore, VerifyStatus};
use std::fs::{create_dir, OpenOptions};
use std::io::{self, BufRead};
use std::io::prelude::*;
use std::io::LineWriter;
use std::collections::HashMap;

pub struct FileSystem {}
pub struct FileSystem {
    trust_store: HashMap<String, String>,
    pop_dir: String,
}

impl FileSystem {
    pub fn new(pop_dir: String) -> Self {
        Self {}
        let mut trust_store = HashMap::new();
        Self::load_trust_store(&pop_dir, &mut trust_store);

        Self { 
            trust_store,
            pop_dir,
        }
    }

    fn load_trust_store(pop_dir: &str, store: &mut HashMap<String, String>) {
        let trust_path = format!("{}/known_hosts", pop_dir);
        let file = OpenOptions::new()
            .write(true)
            .read(true)
            .open(trust_path).unwrap();

        for line in io::BufReader::new(file).lines() {
            if let Ok(kh) = line {
                let (host, fingerprint) = kh
                    .split_once(" ").unwrap();

                store.insert(host.to_string(), fingerprint.to_string());
            }
        }
    }

    pub fn flush_trust_store(&self) {
        let trust_path = format!("{}/known_hosts", self.pop_dir);
        let file = OpenOptions::new()
            .write(true)
            .read(true)
            .open(trust_path).unwrap();
        let mut file = LineWriter::new(file);

        for (h, f) in &self.trust_store {
            file.write_all(format!("{} {}\n", h, f).as_bytes()).unwrap();
        };

        file.flush().unwrap();
    }
}

impl TrustStore for FileSystem {
    fn verify(&self, addr: &str, fingerprint: String) -> PopResult<VerifyStatus> {
        println!("{} {}", addr, fingerprint);
        Ok(VerifyStatus::Untrusted)
    fn verify(&mut self, addr: &str, fingerprint: String) -> PopResult<VerifyStatus> {
        let remote_f = fingerprint.clone();
        let f = &self
            .trust_store
            .entry(addr.to_string())
            .or_insert(fingerprint)
            .to_string();

        if remote_f.eq(f) {
            Ok(VerifyStatus::Trusted)
        } else {
            Ok(VerifyStatus::Untrusted)
        }
    }
}

M src/lib.rs => src/lib.rs +2 -3
@@ 1,7 1,7 @@
use std::str::FromStr;

mod infra;
pub use infra::{TlsClient, FileSystem};
pub use infra::{FileSystem, TlsClient};

#[derive(Debug)]
pub enum GemStatus {


@@ 61,11 61,10 @@ pub enum PopError {
pub type PopResult<T> = Result<T, PopError>;

pub trait TrustStore: Send + Sync {
    fn verify(&self, addr: &str, fingerprint: String) -> PopResult<VerifyStatus>;
    fn verify(&mut self, addr: &str, fingerprint: String) -> PopResult<VerifyStatus>;
}

pub enum VerifyStatus {
    Trusted,
    Untrusted,
}