├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── src └── lib.rs └── tests └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["jD91mZM2 "] 3 | description = "SSL without a certificate authority" 4 | license-file = "LICENSE" 5 | name = "sslhash" 6 | repository = "https://github.com/jD91mZM2/sslhash" 7 | version = "0.1.1" 8 | [dependencies] 9 | failure = "0.1.1" 10 | openssl = "0.10.2" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 jD91mZM2 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sslhash [![Crates.io](https://img.shields.io/crates/v/sslhash.svg)](https://crates.io/crates/sslhash) 2 | 3 | SSL without a certificate authority. 4 | 5 | ## Why? 6 | 7 | Have you ever wanted to use TLS over non-http TCP streams? 8 | If yes you'll realize it's quite complicated to set up certificates. 9 | Not all IPs even have domains! 10 | 11 | sslhash basically skips the hostname check and instead compares it to a user-supplied hash of the public key. 12 | This takes the good of SSL (Security, audited, etc) and removes the bad of SSL (certificate authorities). 13 | 14 | ## Usage 15 | 16 | Server: 17 | 18 | ```Rust 19 | // Create a builder. 20 | // Default values: 21 | // - RSA bits: 3072 22 | // - Cache directory: The same directory as the executable 23 | let (acceptor, hash) = AcceptorBuilder::default().build().unwrap(); 24 | 25 | // Replace "localhost:1234" with what you want to bind to. 26 | // On UNIX, use 0.0.0.0 as IP to make it public. 27 | let tcp = TcpListener::bind("localhost:1234").unwrap(); 28 | let (client, _) = tcp.accept().unwrap(); 29 | let mut client = acceptor.accept(client).unwrap(); 30 | 31 | // client is a SslStream now ready to be used. 32 | // Somehow transfer the hash to the client. 33 | // A simple way would be to tell the user to give this to all clients. 34 | ``` 35 | 36 | Client: 37 | 38 | ```Rust 39 | let connector = SslConnector::builder(SslMethod::tls()).unwrap().build(); 40 | 41 | // Replace "localhost:1234" with what you want to connect to. 42 | let client = TcpStream::connect("localhost:1234").unwrap(); 43 | 44 | // Assumes you have a String called "hash" that is the hash of the server's public key. 45 | // Somehow receive this from the server. 46 | // A simple way would be to ask the user for the hash. 47 | let mut client = sslhash::connect(&connector, client, hash).unwrap(); 48 | ``` 49 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] extern crate failure; 2 | extern crate openssl; 3 | 4 | use openssl::asn1::Asn1Time; 5 | use openssl::bn::BigNum; 6 | use openssl::error::ErrorStack; 7 | use openssl::hash::MessageDigest; 8 | use openssl::pkey::{PKey, Private}; 9 | use openssl::rsa::Rsa; 10 | use openssl::sha; 11 | use openssl::ssl::{HandshakeError, SslAcceptor, SslConnector, SslMethod, SslStream, SslVerifyMode}; 12 | use openssl::x509::{X509, X509Name}; 13 | use std::env; 14 | use std::fs::File; 15 | use std::io::{Error as IoError, ErrorKind as IoErrorKind, Read, Write}; 16 | use std::path::PathBuf; 17 | 18 | const RSA_BITS: u32 = 3072; 19 | 20 | /// Create a string SHA-256 hash from binary data. 21 | pub fn sha256(data: &[u8]) -> String { 22 | use std::fmt::Write; 23 | let hash = sha::sha256(data); 24 | 25 | let mut string = String::with_capacity(hash.len() * 2); 26 | for byte in &hash { 27 | write!(string, "{:02X}", byte).unwrap(); 28 | } 29 | string 30 | } 31 | 32 | #[derive(Debug, Fail)] 33 | pub enum AcceptorError { 34 | #[fail(display = "openssl error: {}", _0)] 35 | OpensslError(#[cause] ErrorStack), 36 | #[fail(display = "io error: {}", _0)] 37 | IoError(#[cause] IoError), 38 | #[fail(display = "failed to get current exe directory: no parent")] 39 | ExeNoParent 40 | } 41 | impl From for AcceptorError { 42 | fn from(err: ErrorStack) -> Self { AcceptorError::OpensslError(err) } 43 | } 44 | impl From for AcceptorError { 45 | fn from(err: IoError) -> Self { AcceptorError::IoError(err) } 46 | } 47 | 48 | enum CacheDir { 49 | ExeDir, 50 | Path(PathBuf) 51 | } 52 | /// Build an SslAcceptor with a generated (and by default cached) key 53 | pub struct AcceptorBuilder { 54 | bits: u32, 55 | cache_dir: Option 56 | } 57 | impl Default for AcceptorBuilder { 58 | fn default() -> Self { 59 | Self { 60 | bits: RSA_BITS, 61 | cache_dir: Some(CacheDir::ExeDir) 62 | } 63 | } 64 | } 65 | impl AcceptorBuilder { 66 | /// Set the number of RSA key bits. Default: 3072 67 | pub fn set_bits(mut self, bits: u32) -> Self { 68 | self.bits = bits; 69 | self 70 | } 71 | /// Set the cache directory or disable caching. Default is the same directory as the EXE. 72 | pub fn set_cache_dir(mut self, cache_dir: Option) -> Self { 73 | self.cache_dir = cache_dir.map(CacheDir::Path); 74 | self 75 | } 76 | /// Build a random PKey object as well as a hash of the public key 77 | pub fn build_pkey(self) -> Result<(PKey, String), AcceptorError> { 78 | // Resolve cache variable 79 | let cache = match self.cache_dir { 80 | Some(CacheDir::ExeDir) => 81 | Some(env::current_exe()? 82 | .canonicalize()? 83 | .parent() 84 | .ok_or(AcceptorError::ExeNoParent)? 85 | .join("key.pem")), 86 | Some(CacheDir::Path(mut path)) => { 87 | path.push("key.pem"); 88 | Some(path) 89 | }, 90 | None => None, 91 | }; 92 | 93 | // Attempt to read cache 94 | let rsa = if let Some(ref cache) = cache { 95 | let mut bytes = Vec::new(); 96 | match File::open(cache) { 97 | Ok(mut file) => { 98 | file.read_to_end(&mut bytes)?; 99 | Some(Rsa::private_key_from_pem(&bytes)?) 100 | }, 101 | Err(ref err) if err.kind() == IoErrorKind::NotFound => None, 102 | Err(err) => return Err(err.into()) 103 | } 104 | } else { None }; 105 | 106 | // In case cache does not exist 107 | let rsa = match rsa { 108 | Some(rsa) => rsa, 109 | None => { 110 | let rsa = Rsa::generate(self.bits)?; 111 | if let Some(ref cache) = cache { 112 | File::create(cache)?.write_all(&rsa.private_key_to_pem()?)?; 113 | } 114 | rsa 115 | } 116 | }; 117 | let hash = sha256(&rsa.public_key_to_pem()?); 118 | Ok((PKey::from_rsa(rsa)?, hash)) 119 | } 120 | /// Build a SslAcceptor with the output of `build_pkey` 121 | pub fn build(self) -> Result<(SslAcceptor, String), AcceptorError> { 122 | // Generate key 123 | let (pkey, hash) = self.build_pkey()?; 124 | 125 | // Generate certificate 126 | let mut builder = X509::builder()?; 127 | builder.set_serial_number(&*BigNum::from_u32(1)?.to_asn1_integer()?)?; 128 | builder.set_not_before(&*Asn1Time::days_from_now(0)?)?; 129 | builder.set_not_after(&*Asn1Time::days_from_now(365)?)?; 130 | builder.set_pubkey(&pkey)?; 131 | builder.set_issuer_name(&*{ 132 | let mut builder = X509Name::builder()?; 133 | builder.append_entry_by_text("C", "..")?; 134 | builder.append_entry_by_text("O", ".....")?; 135 | builder.append_entry_by_text("CN", "localhost")?; 136 | 137 | builder.build() 138 | })?; 139 | builder.sign(&pkey, MessageDigest::sha256())?; 140 | let cert = builder.build(); 141 | 142 | // Create acceptor 143 | let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; 144 | builder.set_private_key(&pkey)?; 145 | builder.set_certificate(&cert)?; 146 | Ok((builder.build(), hash)) 147 | } 148 | } 149 | 150 | /// As a client, connect to the server and compare the public key hash with `cmp_hash` 151 | pub fn connect(connector: &SslConnector, stream: S, cmp_hash: String) 152 | -> Result, HandshakeError> 153 | { 154 | let mut configure = connector.configure()? 155 | .use_server_name_indication(false) 156 | .verify_hostname(false); 157 | configure.set_verify_callback(SslVerifyMode::PEER, move |_, cert| { 158 | if let Some(cert) = cert.current_cert() { 159 | if let Ok(pkey) = cert.public_key() { 160 | if let Ok(pem) = pkey.public_key_to_pem() { 161 | let hash = sha256(&pem); 162 | return hash.trim().eq_ignore_ascii_case(&cmp_hash) 163 | } 164 | } 165 | } 166 | false 167 | }); 168 | configure.connect("", stream) 169 | } 170 | -------------------------------------------------------------------------------- /tests/main.rs: -------------------------------------------------------------------------------- 1 | extern crate openssl; 2 | extern crate sslhash; 3 | 4 | use openssl::ssl::{SslConnector, SslMethod}; 5 | use sslhash::AcceptorBuilder; 6 | use std::io::prelude::*; 7 | use std::net::{TcpStream, TcpListener}; 8 | use std::thread; 9 | 10 | const TEST_PAYLOAD: &[u8] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 11 | const IP_TEST1: &str = "localhost:1234"; 12 | const IP_TEST2: &str = "localhost:2345"; 13 | 14 | #[test] 15 | fn test() { 16 | let (acceptor, hash) = AcceptorBuilder::default().set_cache_dir(None).build().unwrap(); 17 | 18 | let thread = thread::spawn(move || { 19 | let tcp = TcpListener::bind(IP_TEST1).unwrap(); 20 | let (client, _) = tcp.accept().unwrap(); 21 | let mut client = acceptor.accept(client).unwrap(); 22 | 23 | let mut buf = [0; 10]; 24 | client.read_exact(&mut buf).unwrap(); 25 | 26 | assert_eq!(&buf, TEST_PAYLOAD); 27 | }); 28 | 29 | let connector = SslConnector::builder(SslMethod::tls()).unwrap().build(); 30 | let client = TcpStream::connect(IP_TEST1).unwrap(); 31 | let mut client = sslhash::connect(&connector, client, hash).unwrap(); 32 | 33 | client.write_all(TEST_PAYLOAD).unwrap(); 34 | client.flush().unwrap(); 35 | 36 | thread.join().unwrap(); 37 | } 38 | 39 | #[test] 40 | fn invalid_hash() { 41 | let (acceptor, _) = AcceptorBuilder::default().set_cache_dir(None).build().unwrap(); 42 | 43 | let thread = thread::spawn(move || { 44 | let tcp = TcpListener::bind(IP_TEST2).unwrap(); 45 | let (client, _) = tcp.accept().unwrap(); 46 | assert!(acceptor.accept(client).is_err()); 47 | }); 48 | 49 | let connector = SslConnector::builder(SslMethod::tls()).unwrap().build(); 50 | let client = TcpStream::connect(IP_TEST2).unwrap(); 51 | assert!(sslhash::connect(&connector, client, String::from("1234")).is_err()); 52 | 53 | thread.join().unwrap(); 54 | } 55 | --------------------------------------------------------------------------------