From 8c4921f797128520705fc493ee4c8f56d8514da6 Mon Sep 17 00:00:00 2001 From: Evie Viau Date: Wed, 14 Sep 2022 09:03:43 -0400 Subject: [PATCH] First version of basic self-CA --- .gitignore | 2 + Cargo.lock | 62 ++++++- Cargo.toml | 9 +- README.md | 3 +- .../m20220913_213320_create_certificates.rs | 6 + src/cert/generate.rs | 76 +++++++++ src/cert/mod.rs | 39 +++++ src/entities/certificate.rs | 37 ++++ src/entities/client.rs | 20 ++- src/entities/mod.rs | 1 + src/entities/prelude.rs | 1 + src/entities/proxy.rs | 15 ++ src/main.rs | 160 +++++++++++++++++- 13 files changed, 419 insertions(+), 12 deletions(-) create mode 100644 src/cert/generate.rs create mode 100644 src/cert/mod.rs create mode 100644 src/entities/certificate.rs diff --git a/.gitignore b/.gitignore index 330e3c8..d012e02 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ # Private files .env +private-key.pem +private-bytes # Misc regexes.yaml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c3e9dd4..de4bd32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aead" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.7.5" @@ -35,7 +45,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" dependencies = [ - "aead", + "aead 0.4.3", "aes", "cipher 0.3.0", "ctr", @@ -549,6 +559,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fc89c7c5b9e7a02dfe45cd2367bae382f9ed31c61ca8debe5f827c420a2f08" +dependencies = [ + "cfg-if", + "cipher 0.4.3", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead 0.5.1", + "chacha20", + "cipher 0.4.3", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.22" @@ -582,6 +616,7 @@ checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -727,6 +762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -896,6 +932,7 @@ dependencies = [ "axum", "base64ct", "blake3", + "chacha20poly1305", "chrono", "dotenv", "futures", @@ -1966,6 +2003,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash 0.5.0", +] + [[package]] name = "polyval" version = "0.5.3" @@ -1975,7 +2023,7 @@ dependencies = [ "cfg-if", "cpufeatures", "opaque-debug", - "universal-hash", + "universal-hash 0.4.1", ] [[package]] @@ -3150,6 +3198,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 92adb36..3a5cdc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,12 +22,12 @@ ulid = "1.0.0" axum = "0.6.0-rc.1" tower = "0.4.13" +chacha20poly1305 = "0.10.1" +argon2 = "0.4.1" blake3 = "1.3.1" base64ct = { version = "1.5.2", features = ["alloc"] } rand = "0.8.5" -argon2 = "0.4.1" - tokio = { version = "1.21.0", features = ["full"] } pretty_env_logger = "0.4.0" @@ -47,4 +47,7 @@ user-agent-parser = "0.3.3" lapin = "2.1.1" -picky = "7.0.0-rc.3" \ No newline at end of file +picky = "7.0.0-rc.3" + +[profile.dev.package.num-bigint-dig] +opt-level = 3 \ No newline at end of file diff --git a/README.md b/README.md index 9a94c38..634cd5a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ For more information, visit https://driptorch.net/ | DATABASE_URL | PostgreSQL database connection URL | Y | | AMQP_ADDR | Message queue (RabbitMQ) connection URL | Y | | UAP_REGEXES | Path to the [BrowserScope UA regex YAML](https://github.com/ua-parser/uap-core/blob/master/regexes.yaml) | N | -| RSA_KEY | Path to the RSA private key used to create certificates !!! KEEP THIS SAFE !!! SERIOUSLY !!! | Y | +| RSA_KEY | Path to the RSA private key used to create certificates !!! KEEP THIS SAFE | Y | +| XCC20_KEY | Path to the XChaCha20-Poly1305 key used to encrypt private keys !!! KEEP THIS SAFE | Y | --- diff --git a/migration/src/m20220913_213320_create_certificates.rs b/migration/src/m20220913_213320_create_certificates.rs index 4ea3ac4..3f1f835 100644 --- a/migration/src/m20220913_213320_create_certificates.rs +++ b/migration/src/m20220913_213320_create_certificates.rs @@ -29,6 +29,11 @@ impl MigrationTrait for Migration { .binary() .not_null() ) + .col(ColumnDef::new(Certificate::Nonce) + .binary() + .not_null() + .unique_key() + ) .col(ColumnDef::new(Certificate::CertType) .string() .not_null() @@ -52,5 +57,6 @@ pub enum Certificate { Id, Data, Key, + Nonce, CertType } diff --git a/src/cert/generate.rs b/src/cert/generate.rs new file mode 100644 index 0000000..5a95428 --- /dev/null +++ b/src/cert/generate.rs @@ -0,0 +1,76 @@ +use std::fmt; +use std::fmt::Formatter; +use picky::x509::{Cert, KeyIdGenMethod}; +use picky::x509::certificate::{CertError, CertificateBuilder}; +use picky::x509::date::UtcDate; + +use chrono::prelude::*; +use picky::hash::HashAlgorithm; +use picky::key::PrivateKey; +use picky::signature::SignatureAlgorithm; +use picky::x509::name::DirectoryName; + +pub enum InterTarget { + CLIENT, + PROXY +} + +impl fmt::Display for InterTarget { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + InterTarget::CLIENT => write!(f, "CLIENT"), + InterTarget::PROXY => write!(f, "PROXY") + } + } +} + +pub async fn generate_root_cert(key: &PrivateKey) -> Result { + let current_date: DateTime = Utc::now(); + + CertificateBuilder::new() + .validity( + UtcDate::ymd( + current_date.year() as u16, + current_date.month() as u8, + current_date.day() as u8 + ).unwrap(), + UtcDate::ymd( + (current_date.year() + 20) as u16, + current_date.month() as u8, + current_date.day() as u8 + ).unwrap() + ) + .self_signed(DirectoryName::new_common_name("Driptorch"), key) + .ca(true) + .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512)) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_512)) + .build() +} + +pub async fn generate_inter_cert(key: &PrivateKey, target: InterTarget, root: (&Cert, &PrivateKey)) -> Result { + let current_date: DateTime = Utc::now(); + + CertificateBuilder::new() + .validity( + UtcDate::ymd( + current_date.year() as u16, + current_date.month() as u8, + current_date.day() as u8 + ).unwrap(), + UtcDate::ymd( + (current_date.year() + 5) as u16, + current_date.month() as u8, + current_date.day() as u8 + ).unwrap() + ) + .issuer_cert(&root.0, &root.1) + .ca(true) + .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512)) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_512)) + .pathlen(0) + .subject( + DirectoryName::new_common_name(format!("Driptorch {}", target.to_string())), + key.to_public_key() + ) + .build() +} \ No newline at end of file diff --git a/src/cert/mod.rs b/src/cert/mod.rs new file mode 100644 index 0000000..b70231c --- /dev/null +++ b/src/cert/mod.rs @@ -0,0 +1,39 @@ +use std::{env, fmt, fs}; +use std::fmt::Formatter; +use std::path::Path; +use chacha20poly1305::{AeadCore, KeyInit, XChaCha20Poly1305}; +use chacha20poly1305::aead::{Aead, OsRng}; + +pub mod generate; + +pub enum Types { + ROOT, + CLIENTINTER, + PROXYINTER, + CLIENTLEAF, + PROXYLEAF +} + +impl fmt::Display for Types { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Types::ROOT => write!(f, "ROOT"), + Types::CLIENTINTER => write!(f, "CLIENTINTER"), + Types::PROXYINTER => write!(f, "PROXYINTER"), + Types::CLIENTLEAF => write!(f, "CLIENTLEAF"), + Types::PROXYLEAF => write!(f, "PROXYLEAF") + } + } +} + +pub async fn encrypt_priv_key(key: Vec) -> (Vec, Vec) { + let cipher = XChaCha20Poly1305::new_from_slice( + fs::read( + Path::new(&env::var("XCC20_KEY").expect("XCC20_KEY must be set!")) + ).expect("Failed to load the XCC20 key!").as_slice() + ).expect("Error creating chacha20 cipher!"); + + let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); + + (nonce.to_vec(), cipher.encrypt(&nonce, key.as_slice()).expect("Failed to encrypt private key!")) +} \ No newline at end of file diff --git a/src/entities/certificate.rs b/src/entities/certificate.rs new file mode 100644 index 0000000..ab738ab --- /dev/null +++ b/src/entities/certificate.rs @@ -0,0 +1,37 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "certificate")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: String, + pub data: Vec, + pub key: Vec, + #[sea_orm(unique)] + pub nonce: Vec, + pub cert_type: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::proxy::Entity")] + Proxy, + #[sea_orm(has_many = "super::client::Entity")] + Client, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Proxy.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Client.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/client.rs b/src/entities/client.rs index 029307c..eb95cdc 100644 --- a/src/entities/client.rs +++ b/src/entities/client.rs @@ -15,14 +15,24 @@ pub struct Model { pub dns: bool, pub proxy: bool, pub health: String, + pub certificate: String, } -#[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation {} +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::certificate::Entity", + from = "Column::Certificate", + to = "super::certificate::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Certificate, +} -impl RelationTrait for Relation { - fn def(&self) -> RelationDef { - panic!("No RelationDef") +impl Related for Entity { + fn to() -> RelationDef { + Relation::Certificate.def() } } diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 2380bcc..852560d 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -2,6 +2,7 @@ pub mod prelude; +pub mod certificate; pub mod client; pub mod proxy; pub mod record; diff --git a/src/entities/prelude.rs b/src/entities/prelude.rs index b2f4027..7bc976c 100644 --- a/src/entities/prelude.rs +++ b/src/entities/prelude.rs @@ -1,5 +1,6 @@ //! SeaORM Entity. Generated by sea-orm-codegen 0.9.2 +pub use super::certificate::Entity as Certificate; pub use super::client::Entity as Client; pub use super::proxy::Entity as Proxy; pub use super::record::Entity as Record; diff --git a/src/entities/proxy.rs b/src/entities/proxy.rs index e45ab6d..9c960d5 100644 --- a/src/entities/proxy.rs +++ b/src/entities/proxy.rs @@ -10,10 +10,19 @@ pub struct Model { pub record: String, pub port: i32, pub active: bool, + pub certificate: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { + #[sea_orm( + belongs_to = "super::certificate::Entity", + from = "Column::Certificate", + to = "super::certificate::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Certificate, #[sea_orm( belongs_to = "super::record::Entity", from = "Column::Record", @@ -24,6 +33,12 @@ pub enum Relation { Record, } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Certificate.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::Record.def() diff --git a/src/main.rs b/src/main.rs index ab1e253..aa6a5d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,15 +10,24 @@ use axum::routing::{delete, get, post}; use dotenv::dotenv; use lapin::ConnectionProperties; use picky::key::PrivateKey; -use sea_orm::{ConnectOptions, Database}; +use picky::x509::Cert; +use sea_orm::{ActiveValue, ColumnTrait, ConnectOptions, Database, EntityTrait, QueryFilter}; use sea_orm_migration::prelude::*; use tower::ServiceBuilder; +use ulid::Ulid; +use crate::cert::encrypt_priv_key; +use crate::cert::generate::{generate_inter_cert, generate_root_cert}; +use crate::cert::generate::InterTarget::{CLIENT, PROXY}; +use crate::cert::Types::{CLIENTINTER, PROXYINTER, ROOT}; +use crate::certificate::Model; +use crate::entities::certificate; mod entities; mod dns; mod util; mod routes; mod rpc; +mod cert; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -58,6 +67,9 @@ async fn main() { if !Path::new(&env::var("RSA_KEY").expect("RSA_KEY must be set! Halting start-up.")).exists() { error!("Please generate an RSA private key for creating certificates!") } + if !Path::new(&env::var("XCC20_KEY").expect("XCC20_KEY must be set! Halting start-up.")).exists() { + error!("Please generate a 32 bits of randomness to encrypt private keys!") + } let root_rsa_key = PrivateKey::from_pem_str( &*fs::read_to_string( @@ -78,6 +90,152 @@ async fn main() { .await .expect("Failed to run migrations! Halting start-up."); + info!("Checking for root cert..."); + let root_cert_model: Option = certificate::Entity::find() + .filter(certificate::Column::CertType.eq(cert::Types::ROOT.to_string())) + .one(&connection) + .await + .expect("Failed to retrieve the root cert from the database! Halting start-up."); + + let root_cert: Cert; + + match root_cert_model { + None => { + info!("Generating new root cert..."); + + let new_root_cert = generate_root_cert(&root_rsa_key) + .await + .expect("Failed to generate the root cert from private key! Halting start-up."); + + root_cert = new_root_cert.clone(); + + let root_insert = certificate::Entity::insert(certificate::ActiveModel { + id: ActiveValue::Set(Ulid::new().to_string()), + data: ActiveValue::Set( + new_root_cert.to_der().expect("Failed to convert cert into der!") + ), + key: ActiveValue::set(vec![145, 66, 62, 61, 56, 156, 145, 164]), + nonce: ActiveValue::set(vec![145, 71, 62, 66, 56, 156, 145, 164]), + cert_type: ActiveValue::Set(ROOT.to_string()) + }) + .exec(&connection) + .await + .expect("Failed to insert new root cert into database! Halting start-up."); + + info!("Generated new root cert: {}", root_insert.last_insert_id); + } + Some(root_cert_model) => { + info!("Found root cert: {}", root_cert_model.id); + + root_cert = Cert::from_der(&root_cert_model.data) + .expect("Failed to decode root cert!"); + } + } + + info!("Checking for proxy intermediate cert..."); + let proxy_inter_model: Option = certificate::Entity::find() + .filter(certificate::Column::CertType.eq(PROXYINTER.to_string())) + .one(&connection) + .await + .expect("Failed to retrieve the proxy intermediate cert from the database! Halting start-up."); + + let proxy_inter_cert: Cert; + + match proxy_inter_model { + None => { + info!("Generating proxy intermediate cert..."); + + // Generate 4096 bit RSA private key + let priv_key = PrivateKey::generate_rsa(4096) + .expect("Failed to generate a key"); + + let encrypted_priv_key = encrypt_priv_key(priv_key + .clone() + .to_pkcs8() + .expect("Failed to convert generated private key to pkcs8!") + ).await; + + let new_proxy_inter_cert = generate_inter_cert(&priv_key, PROXY, (&root_cert, &root_rsa_key)) + .await + .expect("Failed to generate new proxy intermediate cert"); + + proxy_inter_cert = new_proxy_inter_cert.clone(); + + let proxy_inter_insert = certificate::Entity::insert(certificate::ActiveModel { + id: ActiveValue::Set(Ulid::new().to_string()), + data: ActiveValue::Set( + new_proxy_inter_cert.to_der().expect("Failed to convert cert into der!") + ), + key: ActiveValue::set(encrypted_priv_key.1), + nonce: ActiveValue::set(encrypted_priv_key.0), + cert_type: ActiveValue::Set(PROXYINTER.to_string()) + }) + .exec(&connection) + .await + .expect("Failed to insert new proxy intermediate cert into database! Halting start-up."); + + info!("Generated new proxy intermediate cert: {}", proxy_inter_insert.last_insert_id) + } + Some(proxy_inter_model) => { + info!("Found proxy intermediate cert: {}", proxy_inter_model.id); + + proxy_inter_cert = Cert::from_der(&proxy_inter_model.data) + .expect("Failed to decode proxy intermediate cert!"); + } + } + + info!("Checking for client intermediate cert..."); + let client_inter_model: Option = certificate::Entity::find() + .filter(certificate::Column::CertType.eq(CLIENTINTER.to_string())) + .one(&connection) + .await + .expect("Failed to retrieve the client intermediate cert from the database! Halting start-up."); + + let client_inter_cert: Cert; + + match client_inter_model { + None => { + info!("Generating client intermediate cert..."); + + // Generate 4096 bit RSA private key + let priv_key = PrivateKey::generate_rsa(4096) + .expect("Failed to generate a key"); + + let encrypted_priv_key = encrypt_priv_key(priv_key + .clone() + .to_pkcs8() + .expect("Failed to convert generated private key to pkcs8!") + ).await; + + let new_client_inter_cert = generate_inter_cert(&priv_key, CLIENT, (&root_cert, &root_rsa_key)) + .await + .expect("Failed to generate new client intermediate cert"); + + client_inter_cert = new_client_inter_cert.clone(); + + let client_inter_insert = certificate::Entity::insert(certificate::ActiveModel { + id: ActiveValue::Set(Ulid::new().to_string()), + data: ActiveValue::Set( + new_client_inter_cert.to_der().expect("Failed to convert cert into der!") + ), + key: ActiveValue::set(encrypted_priv_key.1), + nonce: ActiveValue::set(encrypted_priv_key.0), + cert_type: ActiveValue::Set(CLIENTINTER.to_string()) + }) + .exec(&connection) + .await + .expect("Failed to insert new client intermediate cert into database! Halting start-up."); + + info!("Generated new client intermediate cert: {}", client_inter_insert.last_insert_id) + } + Some(client_inter_model) => { + info!("Found client intermediate cert: {}", client_inter_model.id); + + client_inter_cert = Cert::from_der(&client_inter_model.data) + .expect("Failed to decode client intermediate cert!"); + } + } + info!("Connecting to message broker..."); let amqp_addr = env::var("AMQP_ADDR") .expect("AMQP_ADDR mut be set! Halting start-up.");