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,
}
-