From d44a6c116679d9787f7979ad214ec0203ce3b5f2 Mon Sep 17 00:00:00 2001 From: core Date: Fri, 15 Dec 2023 15:16:59 -0500 Subject: [PATCH] work --- .idea/dataSources.xml | 2 +- .idea/trifid.iml | 1 + Cargo.lock | 77 ++++++++++++++++++++- Cargo.toml | 5 +- trifid-api-derive/Cargo.toml | 13 ++++ trifid-api-derive/src/lib.rs | 0 trifid-api/Cargo.toml | 6 +- trifid-api/config.toml | 8 ++- trifid-api/src/ca.rs | 100 +++++++++++++++++++++++++++ trifid-api/src/config.rs | 1 + trifid-api/src/macros.rs | 0 trifid-api/src/main.rs | 4 ++ trifid-api/src/models.rs | 38 +++++++--- trifid-api/src/routes/v1/networks.rs | 54 +++++++++++++-- 14 files changed, 290 insertions(+), 19 deletions(-) create mode 100644 trifid-api-derive/Cargo.toml create mode 100644 trifid-api-derive/src/lib.rs create mode 100644 trifid-api/src/ca.rs create mode 100644 trifid-api/src/macros.rs diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 1fec8e5..7074b84 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -1,7 +1,7 @@ - + postgresql true org.postgresql.Driver diff --git a/.idea/trifid.iml b/.idea/trifid.iml index 7ec81cf..7ee0faa 100644 --- a/.idea/trifid.iml +++ b/.idea/trifid.iml @@ -12,6 +12,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index e27538c..37a0d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.3" @@ -545,6 +555,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "checked_int_cast" version = "1.0.0" @@ -574,6 +608,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -779,6 +814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -1867,6 +1903,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "option-ext" version = "0.2.0" @@ -1998,6 +2040,17 @@ version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +[[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", +] + [[package]] name = "postgres-protocol" version = "0.6.6" @@ -2721,7 +2774,7 @@ dependencies = [ [[package]] name = "tfcli" -version = "0.3.0" +version = "0.3.1" dependencies = [ "clap", "comfy-table", @@ -3027,17 +3080,29 @@ dependencies = [ "actix-cors", "actix-web", "bb8", + "chacha20poly1305", "diesel", "diesel-async", "diesel_migrations", "env_logger", + "hex", "log", "mail-send", "rand", "serde", "serde_json", + "thiserror", "toml 0.8.5", "totp-rs", + "trifid-pki", +] + +[[package]] +name = "trifid-api-derive" +version = "0.1.0" +dependencies = [ + "quote", + "syn 2.0.38", ] [[package]] @@ -3095,6 +3160,16 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.9" diff --git a/Cargo.toml b/Cargo.toml index a968496..078a5d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ members = [ "dnapi-rs", "tfcli", "nebula-ffi", - "trifid-api" + + "trifid-api", + "trifid-api-derive" ] + resolver = "2" \ No newline at end of file diff --git a/trifid-api-derive/Cargo.toml b/trifid-api-derive/Cargo.toml new file mode 100644 index 0000000..d54c24f --- /dev/null +++ b/trifid-api-derive/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "trifid-api-derive" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full", "fold"] } +quote = "1" \ No newline at end of file diff --git a/trifid-api-derive/src/lib.rs b/trifid-api-derive/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/trifid-api/Cargo.toml b/trifid-api/Cargo.toml index 5e8cdb8..0138a65 100644 --- a/trifid-api/Cargo.toml +++ b/trifid-api/Cargo.toml @@ -25,4 +25,8 @@ diesel_migrations = "2" bb8 = "0.8" rand = "0.8" mail-send = "0.4" -totp-rs = { version = "5.4", features = ["gen_secret", "otpauth"] } \ No newline at end of file +totp-rs = { version = "5.4", features = ["gen_secret", "otpauth"] } +trifid-pki = { version = "0.1", path = "../trifid-pki", features = ["serde_derive"] } +chacha20poly1305 = "0.10" +hex = "0.4" +thiserror = "1" \ No newline at end of file diff --git a/trifid-api/config.toml b/trifid-api/config.toml index aabe030..26b2a47 100644 --- a/trifid-api/config.toml +++ b/trifid-api/config.toml @@ -25,7 +25,7 @@ server = "mail.e3t.cc" port = 465 # (Required) The username to authenticate with. username = "core" -# (Required) The password to authenticate with. If set to %PASSWORD%, will be filled from the environment variable TRIFID_EMAIL_PASSWORD. +# (Required) The password to authenticate with. If set to $PASSWORD$, will be filled from the environment variable TRIFID_EMAIL_PASSWORD. password = "$PASSWORD$" # (Required) The "From Name" to send the email from from_name = "Trifid" @@ -45,4 +45,8 @@ magic_link_expiry_seconds = 3600 # 1 hour session_token_expiry_seconds = 31536000 # ~1 year # (Required) How long should auth tokens be valid for, in seconds? This controls how long users can remain logged in # before they must re-authenticate via 2FA. -auth_token_expiry_seconds = 86400 # 24 hours \ No newline at end of file +auth_token_expiry_seconds = 86400 # 24 hours +# (Required) (VERY IMPORTANT!) The per-instance encryption key used to encrypt sensitive data in the database. +# It is INCREDIBLY IMPORTANT that you change this value! It should be a 32-byte/256-bit hex-encoded randomly generated +# key. +data_encryption_key = "dd5aa62f0fd9b7fb4ff65567493f889557212f3a8e9587a79268161f9ae070a6" \ No newline at end of file diff --git a/trifid-api/src/ca.rs b/trifid-api/src/ca.rs new file mode 100644 index 0000000..79d7c18 --- /dev/null +++ b/trifid-api/src/ca.rs @@ -0,0 +1,100 @@ +use std::error::Error; +use std::time::SystemTime; +use actix_web::cookie::time::Duration; +use chacha20poly1305::{AeadCore, KeyInit, Nonce, XChaCha20Poly1305, XNonce}; +use chacha20poly1305::aead::{Aead, Payload}; +use log::error; +use rand::Rng; +use rand::rngs::OsRng; +use thiserror::Error; +use crate::models::SigningCA; +use trifid_pki::cert::{NebulaCertificate, NebulaCertificateDetails}; +use trifid_pki::ed25519_dalek::{SignatureError, SigningKey}; +use crate::config::Config; + +#[derive(Error, Debug)] +pub enum CryptographyError { + #[error("certificate signing error: {0}")] + CertificateSigningError(Box), + #[error("PEM serialization error: {0}")] + PemSerializeError(Box), + #[error("JSON serialization error: {0}")] + JsonSerializeError(serde_json::Error), + #[error("Invalid data_encryption_key content: {0}")] + InvalidKey(hex::FromHexError), + #[error("Invalid data_encryption_key length (must be 32 bytes)")] + InvalidKeyLength, + #[error("Error locking lockbox")] + LockingError, + #[error("Invalid salt length")] + InvalidSaltLength, + #[error("Key material decryption failed")] + DecryptFailed, + #[error("Invalid signing key length after lockbox unlock")] + InvalidSigningKeyLength, + #[error("Signature error {0}")] + SignatureError(SignatureError) +} + +pub fn create_signing_ca(expires: SystemTime, org_id: String, user_email: String, config: &Config) -> Result { + let key = SigningKey::generate(&mut OsRng); + + let mut cert = NebulaCertificate { + details: NebulaCertificateDetails { + name: format!("Certificate Authority for {user_email}'s Organization"), + ips: vec![], + subnets: vec![], + groups: vec![], + not_before: SystemTime::now() - Duration::hours(24), + not_after: expires, + public_key: key.verifying_key().as_bytes().clone(), + is_ca: true, + issuer: "".to_string(), + }, + signature: vec![], + }; + cert.sign(&key).map_err(|e| CryptographyError::CertificateSigningError(e))?; + + let pem = cert.serialize_to_pem().map_err(|e| CryptographyError::PemSerializeError(e))?; + + let cert_value = serde_json::to_value(cert).map_err(|e| CryptographyError::JsonSerializeError(e))?; + + let lockbox_key = XChaCha20Poly1305::new_from_slice(&hex::decode(&config.tokens.data_encryption_key).map_err(|e| CryptographyError::InvalidKey(e))?).map_err(|_| CryptographyError::InvalidKeyLength)?; + + let salt = XChaCha20Poly1305::generate_nonce(&mut OsRng); + + let aad: [u8; 16] = OsRng.gen(); + + let lockbox = lockbox_key.encrypt(&salt, Payload { + msg: &key.to_keypair_bytes(), + aad: &aad, + }).map_err(|e| CryptographyError::LockingError)?; + + Ok(SigningCA { + id: randid!(id "ca"), + pem: String::from_utf8(pem).unwrap(), + cert: cert_value, + expires_at: cert.details.not_after.clone(), + organization_id: org_id, + salt: salt.as_slice().to_vec(), + info: aad.to_vec(), + private_key: lockbox, + }) +} + +pub fn sign_cert_with_ca(ca: &SigningCA, cert: &mut NebulaCertificate, config: &Config) -> Result<(), CryptographyError> { + let lockbox_key = XChaCha20Poly1305::new_from_slice(&hex::decode(&config.tokens.data_encryption_key).map_err(|e| CryptographyError::InvalidKey(e))?).map_err(|_| CryptographyError::InvalidKeyLength)?; + + let salt_u24: [u8; 24] = ca.salt.try_into().map_err(|_| CryptographyError::InvalidSaltLength)?; + + let salt = XNonce::from(salt_u24); + + let plaintext = lockbox_key.decrypt(&salt, Payload { + msg: &ca.private_key, + aad: &ca.info, + }).map_err(|_| CryptographyError::DecryptFailed)?; + + let key = SigningKey::from_keypair_bytes(&plaintext.try_into().map_err(|_| CryptographyError::InvalidSigningKeyLength)?).map_err(|e| CryptographyError::SignatureError(e))?; + + cert.sign(&key).map_err(|e| CryptographyError::CertificateSigningError(e)) +} \ No newline at end of file diff --git a/trifid-api/src/config.rs b/trifid-api/src/config.rs index 8f0270c..667fd14 100644 --- a/trifid-api/src/config.rs +++ b/trifid-api/src/config.rs @@ -43,4 +43,5 @@ pub struct ConfigTokens { pub magic_link_expiry_seconds: u64, pub session_token_expiry_seconds: u64, pub auth_token_expiry_seconds: u64, + pub data_encryption_key: String } diff --git a/trifid-api/src/macros.rs b/trifid-api/src/macros.rs new file mode 100644 index 0000000..e69de29 diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 5cbdb97..12127dc 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -24,6 +24,10 @@ pub mod schema; pub mod id; pub mod auth; pub mod email; +#[macro_use] +pub mod macros; +pub mod ca; +pub mod crypt; #[derive(Clone)] pub struct AppState { diff --git a/trifid-api/src/models.rs b/trifid-api/src/models.rs index adad667..3fe7945 100644 --- a/trifid-api/src/models.rs +++ b/trifid-api/src/models.rs @@ -1,8 +1,9 @@ use diesel::{Associations, Identifiable, Insertable, Queryable, Selectable}; +use serde::{Deserialize, Serialize}; use std::time::SystemTime; use serde_json::Value; -#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, PartialEq, Clone)] +#[derive(Queryable, Selectable, Insertable, Identifiable, Debug, PartialEq, Clone, Serialize, Deserialize)] #[diesel(table_name = crate::schema::users)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct User { @@ -11,7 +12,7 @@ pub struct User { } #[derive( - Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, + Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, Serialize, Deserialize )] #[diesel(belongs_to(User))] #[diesel(table_name = crate::schema::magic_links)] @@ -23,7 +24,7 @@ pub struct MagicLink { } #[derive( - Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, + Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, Serialize, Deserialize )] #[diesel(belongs_to(User))] #[diesel(table_name = crate::schema::session_tokens)] @@ -35,7 +36,7 @@ pub struct SessionToken { } #[derive( - Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, + Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, Serialize, Deserialize )] #[diesel(belongs_to(User))] #[diesel(table_name = crate::schema::totp_authenticators)] @@ -51,7 +52,7 @@ pub struct TotpAuthenticator { } #[derive( - Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, + Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, Serialize, Deserialize )] #[diesel(belongs_to(User))] #[diesel(table_name = crate::schema::auth_tokens)] @@ -63,7 +64,7 @@ pub struct AuthToken { } #[derive( -Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, +Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, Serialize, Deserialize )] #[diesel(belongs_to(User, foreign_key = owner_id))] #[diesel(table_name = crate::schema::organizations)] @@ -86,7 +87,7 @@ id -> Varchar, */ #[derive( -Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, +Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, Serialize, Deserialize )] #[diesel(belongs_to(Organization))] #[diesel(table_name = crate::schema::signing_cas)] @@ -101,6 +102,17 @@ pub struct SigningCA { pub info: Vec, pub private_key: Vec } +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +pub struct SigningCANormalized { + pub id: String, + pub pem: String, + pub cert: Value, + pub expires_at: String, + pub organization_id: String, + pub salt: Vec, + pub info: Vec, + pub private_key: Vec +} /* id VARCHAR NOT NULL PRIMARY KEY, @@ -113,7 +125,7 @@ id VARCHAR NOT NULL PRIMARY KEY, */ #[derive( -Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone, +Queryable, Selectable, Insertable, Identifiable, Associations, Debug, PartialEq, Clone )] #[diesel(belongs_to(Organization))] #[diesel(belongs_to(SigningCA, foreign_key = signing_ca_id))] @@ -127,4 +139,14 @@ pub struct Network { pub created_at: SystemTime, pub name: String, pub lighthouses_as_relays: bool +} +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct NetworkNormalized { + pub id: String, + pub cidr: String, + pub organization_id: String, + pub signing_ca_id: String, + pub created_at: String, + pub name: String, + pub lighthouses_as_relays: bool } \ No newline at end of file diff --git a/trifid-api/src/routes/v1/networks.rs b/trifid-api/src/routes/v1/networks.rs index f550861..1444d70 100644 --- a/trifid-api/src/routes/v1/networks.rs +++ b/trifid-api/src/routes/v1/networks.rs @@ -1,7 +1,14 @@ +use std::time::{Duration, SystemTime}; use actix_web::HttpRequest; -use actix_web::web::Json; -use serde::Deserialize; -use crate::AppState; +use actix_web::web::{Data, Json}; +use serde::{Deserialize, Serialize}; +use crate::{AppState, auth, enforce, randid}; +use crate::models::{Network, NetworkNormalized, Organization, SigningCA, User}; +use crate::response::JsonAPIResponse; +use diesel::{SelectableHelper, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; +use crate::ca::create_signing_ca; +use crate::schema::users; #[derive(Deserialize, Debug)] pub struct CreateNetworkReq { @@ -9,8 +16,45 @@ pub struct CreateNetworkReq { pub name: String } +#[derive(Serialize, Debug)] pub struct CreateNetworkResp { - + pub data: NetworkNormalized } -pub async fn create_network_req(req: Json, state: Data, req_info: HttpRequest) \ No newline at end of file +pub async fn create_network_req(req: Json, state: Data, req_info: HttpRequest) -> JsonAPIResponse { + let mut conn = handle_error!(state.pool.get().await); + + let auth_info = auth!(req_info, conn); + let (session_token, auth_token) = enforce!(sess auth auth_info); + + let user = handle_error!( + users::table + .find(&session_token.user_id) + .first::(&mut conn) + .await + ); + + let new_org = Organization { + id: randid!(id "org"), + owner_id: user.id.clone(), + name: format!("{}'s Organization", user.email), + }; + + // create 3 signing CAs, to mimic upstream DN functionality + + let ca_oneyear = handle_error!(create_signing_ca(SystemTime::now() + Duration::from_secs(86400 * 365), new_org.id.clone(), user.email.clone(), &state.config)); + let ca_twoyears = handle_error!(create_signing_ca(SystemTime::now() + Duration::from_secs(86400 * 365 * 2), new_org.id.clone(), user.email.clone(), &state.config)); + let ca_threeyears = handle_error!(create_signing_ca(SystemTime::now() + Duration::from_secs(86400 * 365 * 3), new_org.id.clone(), user.email.clone(), &state.config)); + + let new_network = Network { + id: randid!(id "net"), + cidr: req.0.cidr.clone(), + organization_id: new_org.id.clone(), + signing_ca_id: "".to_string(), + created_at: SystemTime::now(), + name: "".to_string(), + lighthouses_as_relays: false, + }; + + todo!() +} \ No newline at end of file