trifid-api 0.3.0-alpha1 and some work on a tfweb rewrite #6
|
@ -3087,6 +3087,7 @@ version = "0.3.0-alpha1"
|
|||
dependencies = [
|
||||
"actix-cors",
|
||||
"actix-web",
|
||||
"base64 0.21.5",
|
||||
"bb8",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
|
|
|
@ -34,4 +34,5 @@ chrono = "0.4"
|
|||
dnapi-rs = { version = "0.2", path = "../dnapi-rs" }
|
||||
nebula-config = { version = "0.1", path = "../nebula-config" }
|
||||
ipnet = "2.9"
|
||||
serde_yaml = "0.9"
|
||||
serde_yaml = "0.9"
|
||||
base64 = "0.21"
|
|
@ -17,5 +17,7 @@ CREATE TABLE hosts (
|
|||
platform VARCHAR NULL,
|
||||
update_available BOOLEAN NULL,
|
||||
|
||||
tags text[] NOT NULL
|
||||
tags text[] NOT NULL,
|
||||
|
||||
counter INT NULL
|
||||
);
|
|
@ -44,5 +44,5 @@ pub struct ConfigTokens {
|
|||
pub session_token_expiry_seconds: u64,
|
||||
pub auth_token_expiry_seconds: u64,
|
||||
pub data_encryption_key: String,
|
||||
pub cert_expiry_time_seconds: u64
|
||||
pub cert_expiry_time_seconds: u64,
|
||||
}
|
||||
|
|
|
@ -3,25 +3,33 @@
|
|||
// and the entirety of trifid-pki, as it deals with CA private keys.
|
||||
// Review carefully what you write here!
|
||||
|
||||
use std::collections::{Bound, HashMap};
|
||||
use std::error::Error;
|
||||
use std::net::{Ipv4Addr, SocketAddrV4};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use crate::crypt::sign_cert_with_ca;
|
||||
use crate::models::{Host, HostKey, HostOverride, Network, Role, RoleFirewallRule, SigningCA};
|
||||
use crate::schema::{
|
||||
host_keys, host_overrides, hosts, networks, role_firewall_rules, roles, signing_cas,
|
||||
};
|
||||
use crate::AppState;
|
||||
use actix_web::web::Data;
|
||||
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
|
||||
use diesel_async::pooled_connection::bb8::RunError;
|
||||
use diesel_async::RunQueryDsl;
|
||||
use ipnet::Ipv4Net;
|
||||
use nebula_config::{
|
||||
NebulaConfig, NebulaConfigCipher, NebulaConfigFirewall, NebulaConfigFirewallRule,
|
||||
NebulaConfigLighthouse, NebulaConfigListen, NebulaConfigPki, NebulaConfigPunchy,
|
||||
NebulaConfigRelay, NebulaConfigTun,
|
||||
};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::collections::{Bound, HashMap};
|
||||
use std::error::Error;
|
||||
use std::net::{Ipv4Addr, SocketAddrV4};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use thiserror::Error;
|
||||
use nebula_config::{NebulaConfig, NebulaConfigCipher, NebulaConfigFirewall, NebulaConfigFirewallRule, NebulaConfigLighthouse, NebulaConfigListen, NebulaConfigPki, NebulaConfigPunchy, NebulaConfigRelay, NebulaConfigTun};
|
||||
use trifid_pki::cert::{deserialize_nebula_certificate_from_pem, NebulaCertificate, NebulaCertificateDetails};
|
||||
use trifid_pki::cert::{
|
||||
deserialize_nebula_certificate_from_pem, NebulaCertificate, NebulaCertificateDetails,
|
||||
};
|
||||
use trifid_pki::x25519_dalek::PublicKey;
|
||||
use crate::AppState;
|
||||
use crate::crypt::sign_cert_with_ca;
|
||||
use crate::models::{Host, Network, SigningCA, HostKey, Role, RoleFirewallRule, HostOverride};
|
||||
use crate::schema::{host_keys, hosts, networks, role_firewall_rules, roles, signing_cas, host_overrides};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigGenError {
|
||||
|
@ -32,44 +40,68 @@ pub enum ConfigGenError {
|
|||
#[error("error parsing a signing CA: {0}")]
|
||||
InvalidCACert(serde_json::Error),
|
||||
#[error("an error occured: {0}")]
|
||||
GenericError(Box<dyn Error>)
|
||||
GenericError(Box<dyn Error>),
|
||||
}
|
||||
|
||||
pub async fn generate_config(host: &Host, dh_pubkey: PublicKey, state: Data<AppState>) -> Result<NebulaConfig, ConfigGenError> {
|
||||
let mut conn = state.pool.get().await.map_err(ConfigGenError::AcquireError)?;
|
||||
pub async fn generate_config(
|
||||
host: &Host,
|
||||
dh_pubkey: PublicKey,
|
||||
state: Data<AppState>,
|
||||
) -> Result<NebulaConfig, ConfigGenError> {
|
||||
let mut conn = state
|
||||
.pool
|
||||
.get()
|
||||
.await
|
||||
.map_err(ConfigGenError::AcquireError)?;
|
||||
|
||||
let cas = signing_cas::dsl::signing_cas.filter(signing_cas::organization_id.eq(&host.organization_id)).select(SigningCA::as_select()).load(&mut conn).await.map_err(ConfigGenError::DbError)?;
|
||||
let cas = signing_cas::dsl::signing_cas
|
||||
.filter(signing_cas::organization_id.eq(&host.organization_id))
|
||||
.select(SigningCA::as_select())
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.map_err(ConfigGenError::DbError)?;
|
||||
|
||||
let mut good_cas = vec![];
|
||||
let mut ca_string = String::new();
|
||||
|
||||
for ca in cas {
|
||||
if ca.expires_at < SystemTime::now() {
|
||||
let ca_cert: NebulaCertificate = serde_json::from_value(ca.cert.clone()).map_err(ConfigGenError::InvalidCACert)?;
|
||||
ca_string += &String::from_utf8_lossy(&ca_cert.serialize_to_pem().map_err(ConfigGenError::GenericError)?);
|
||||
let ca_cert: NebulaCertificate =
|
||||
serde_json::from_value(ca.cert.clone()).map_err(ConfigGenError::InvalidCACert)?;
|
||||
ca_string += &String::from_utf8_lossy(
|
||||
&ca_cert
|
||||
.serialize_to_pem()
|
||||
.map_err(ConfigGenError::GenericError)?,
|
||||
);
|
||||
good_cas.push((ca, ca_cert));
|
||||
}
|
||||
}
|
||||
|
||||
let (signing_ca, ca_cert) = &good_cas[0];
|
||||
|
||||
let network = networks::dsl::networks.find(&host.network_id).first::<Network>(&mut conn).await.map_err(ConfigGenError::DbError)?;
|
||||
let network = networks::dsl::networks
|
||||
.find(&host.network_id)
|
||||
.first::<Network>(&mut conn)
|
||||
.await
|
||||
.map_err(ConfigGenError::DbError)?;
|
||||
|
||||
let mut cert = NebulaCertificate {
|
||||
details: NebulaCertificateDetails {
|
||||
name: host.name.clone(),
|
||||
ips: vec![
|
||||
Ipv4Net::new(
|
||||
Ipv4Addr::from_str(&host.ip_address).unwrap(),
|
||||
Ipv4Net::from_str(&network.cidr).unwrap().prefix_len()
|
||||
).unwrap()
|
||||
],
|
||||
ips: vec![Ipv4Net::new(
|
||||
Ipv4Addr::from_str(&host.ip_address).unwrap(),
|
||||
Ipv4Net::from_str(&network.cidr).unwrap().prefix_len(),
|
||||
)
|
||||
.unwrap()],
|
||||
subnets: vec![],
|
||||
groups: if let Some(role_id) = &host.role_id {
|
||||
vec![format!("role:{}", role_id)]
|
||||
} else { vec![] },
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
not_before: SystemTime::now() - Duration::from_secs(3600),
|
||||
not_after: SystemTime::now() + Duration::from_secs(state.config.tokens.cert_expiry_time_seconds),
|
||||
not_after: SystemTime::now()
|
||||
+ Duration::from_secs(state.config.tokens.cert_expiry_time_seconds),
|
||||
public_key: *dh_pubkey.as_bytes(),
|
||||
is_ca: false,
|
||||
issuer: ca_cert.sha256sum().unwrap(),
|
||||
|
@ -79,12 +111,23 @@ pub async fn generate_config(host: &Host, dh_pubkey: PublicKey, state: Data<AppS
|
|||
|
||||
sign_cert_with_ca(signing_ca, &mut cert, &state.config).unwrap();
|
||||
|
||||
let all_blocked_hosts = hosts::dsl::hosts.filter(hosts::network_id.eq(&host.network_id)).filter(hosts::is_blocked.eq(true)).select(Host::as_select()).load(&mut conn).await.map_err(ConfigGenError::DbError)?;
|
||||
let all_blocked_hosts = hosts::dsl::hosts
|
||||
.filter(hosts::network_id.eq(&host.network_id))
|
||||
.filter(hosts::is_blocked.eq(true))
|
||||
.select(Host::as_select())
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.map_err(ConfigGenError::DbError)?;
|
||||
|
||||
let mut all_blocked_fingerprints = vec![];
|
||||
|
||||
for blocked_host in all_blocked_hosts {
|
||||
let hosts_blocked_key_entries = host_keys::dsl::host_keys.filter(host_keys::host_id.eq(&blocked_host.id)).select(HostKey::as_select()).load(&mut conn).await.map_err(ConfigGenError::DbError)?;
|
||||
let hosts_blocked_key_entries = host_keys::dsl::host_keys
|
||||
.filter(host_keys::host_id.eq(&blocked_host.id))
|
||||
.select(HostKey::as_select())
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.map_err(ConfigGenError::DbError)?;
|
||||
for blocked_key in hosts_blocked_key_entries {
|
||||
let cert = deserialize_nebula_certificate_from_pem(&blocked_key.client_cert).unwrap();
|
||||
let fingerprint = cert.sha256sum().unwrap();
|
||||
|
@ -102,15 +145,41 @@ pub async fn generate_config(host: &Host, dh_pubkey: PublicKey, state: Data<AppS
|
|||
|
||||
let mut static_host_map = HashMap::new();
|
||||
|
||||
let all_lighthouses = hosts::dsl::hosts.filter(hosts::network_id.eq(&host.network_id)).filter(hosts::is_lighthouse.eq(true)).select(Host::as_select()).load(&mut conn).await.map_err(ConfigGenError::DbError)?;
|
||||
let all_relays = hosts::dsl::hosts.filter(hosts::network_id.eq(&host.network_id)).filter(hosts::is_relay.eq(true)).select(Host::as_select()).load(&mut conn).await.map_err(ConfigGenError::DbError)?;
|
||||
let all_lighthouses = hosts::dsl::hosts
|
||||
.filter(hosts::network_id.eq(&host.network_id))
|
||||
.filter(hosts::is_lighthouse.eq(true))
|
||||
.select(Host::as_select())
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.map_err(ConfigGenError::DbError)?;
|
||||
let all_relays = hosts::dsl::hosts
|
||||
.filter(hosts::network_id.eq(&host.network_id))
|
||||
.filter(hosts::is_relay.eq(true))
|
||||
.select(Host::as_select())
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.map_err(ConfigGenError::DbError)?;
|
||||
|
||||
for lighthouse in &all_lighthouses {
|
||||
static_host_map.insert(Ipv4Addr::from_str(&lighthouse.ip_address).unwrap(), lighthouse.static_addresses.iter().map(|u| SocketAddrV4::from_str(&u.clone().unwrap()).unwrap()).collect::<Vec<_>>());
|
||||
static_host_map.insert(
|
||||
Ipv4Addr::from_str(&lighthouse.ip_address).unwrap(),
|
||||
lighthouse
|
||||
.static_addresses
|
||||
.iter()
|
||||
.map(|u| SocketAddrV4::from_str(&u.clone().unwrap()).unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
for relay in &all_relays {
|
||||
static_host_map.insert(Ipv4Addr::from_str(&relay.ip_address).unwrap(), relay.static_addresses.iter().map(|u| SocketAddrV4::from_str(&u.clone().unwrap()).unwrap()).collect::<Vec<_>>());
|
||||
static_host_map.insert(
|
||||
Ipv4Addr::from_str(&relay.ip_address).unwrap(),
|
||||
relay
|
||||
.static_addresses
|
||||
.iter()
|
||||
.map(|u| SocketAddrV4::from_str(&u.clone().unwrap()).unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
let lighthouse = Some(NebulaConfigLighthouse {
|
||||
|
@ -121,7 +190,10 @@ pub async fn generate_config(host: &Host, dh_pubkey: PublicKey, state: Data<AppS
|
|||
hosts: if host.is_lighthouse {
|
||||
vec![]
|
||||
} else {
|
||||
all_lighthouses.iter().map(|u| Ipv4Addr::from_str(&u.ip_address).unwrap()).collect::<Vec<_>>()
|
||||
all_lighthouses
|
||||
.iter()
|
||||
.map(|u| Ipv4Addr::from_str(&u.ip_address).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
remote_allow_list: Default::default(),
|
||||
local_allow_list: Default::default(),
|
||||
|
@ -132,52 +204,66 @@ pub async fn generate_config(host: &Host, dh_pubkey: PublicKey, state: Data<AppS
|
|||
relays: if host.is_relay || (host.is_lighthouse && network.lighthouses_as_relays) {
|
||||
vec![]
|
||||
} else if network.lighthouses_as_relays {
|
||||
all_lighthouses.iter().map(|u| Ipv4Addr::from_str(&u.ip_address).unwrap()).collect::<Vec<_>>()
|
||||
all_lighthouses
|
||||
.iter()
|
||||
.map(|u| Ipv4Addr::from_str(&u.ip_address).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
all_relays.iter().map(|u| Ipv4Addr::from_str(&u.ip_address).unwrap()).collect::<Vec<_>>()
|
||||
all_relays
|
||||
.iter()
|
||||
.map(|u| Ipv4Addr::from_str(&u.ip_address).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
use_relays: true
|
||||
use_relays: true,
|
||||
});
|
||||
|
||||
let mut inbound_firewall = None;
|
||||
|
||||
if let Some(role_id) = &host.role_id {
|
||||
let role = roles::dsl::roles.find(role_id).first::<Role>(&mut conn).await.map_err(ConfigGenError::DbError)?;
|
||||
let firewall_rules = role_firewall_rules::dsl::role_firewall_rules
|
||||
.filter(role_firewall_rules::role_id.eq(role_id))
|
||||
.select(RoleFirewallRule::as_select())
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.map_err(ConfigGenError::DbError)?;
|
||||
|
||||
let firewall_rules = role_firewall_rules::dsl::role_firewall_rules.filter(role_firewall_rules::role_id.eq(role_id)).select(RoleFirewallRule::as_select()).load(&mut conn).await.map_err(ConfigGenError::DbError)?;
|
||||
|
||||
inbound_firewall = Some(firewall_rules.iter().map(|u| NebulaConfigFirewallRule {
|
||||
port: if let Some((from, to)) = u.port_range {
|
||||
let start_port = match from {
|
||||
Bound::Included(u) => u,
|
||||
Bound::Excluded(u) => u+1,
|
||||
Bound::Unbounded => 0
|
||||
};
|
||||
let end_port = match from {
|
||||
Bound::Included(u) => u,
|
||||
Bound::Excluded(u) => u-1,
|
||||
Bound::Unbounded => 65535
|
||||
};
|
||||
Some(format!("{}-{}", start_port, end_port))
|
||||
} else {
|
||||
Some("any".to_string())
|
||||
},
|
||||
proto: Some(u.protocol.clone()),
|
||||
ca_name: None,
|
||||
ca_sha: None,
|
||||
host: if u.allowed_role_id.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some("any".to_string())
|
||||
},
|
||||
group: None,
|
||||
groups: if u.allowed_role_id.is_some() {
|
||||
Some(vec![format!("role:{}", u.allowed_role_id.clone().unwrap())])
|
||||
} else {
|
||||
None
|
||||
},
|
||||
cidr: None,
|
||||
}).collect::<Vec<_>>())
|
||||
inbound_firewall = Some(
|
||||
firewall_rules
|
||||
.iter()
|
||||
.map(|u| NebulaConfigFirewallRule {
|
||||
port: if let Some((from, to)) = u.port_range {
|
||||
let start_port = match from {
|
||||
Bound::Included(u) => u,
|
||||
Bound::Excluded(u) => u + 1,
|
||||
Bound::Unbounded => 0,
|
||||
};
|
||||
let end_port = match to {
|
||||
Bound::Included(u) => u,
|
||||
Bound::Excluded(u) => u - 1,
|
||||
Bound::Unbounded => 65535,
|
||||
};
|
||||
Some(format!("{}-{}", start_port, end_port))
|
||||
} else {
|
||||
Some("any".to_string())
|
||||
},
|
||||
proto: Some(u.protocol.clone()),
|
||||
ca_name: None,
|
||||
ca_sha: None,
|
||||
host: if u.allowed_role_id.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some("any".to_string())
|
||||
},
|
||||
group: None,
|
||||
groups: if u.allowed_role_id.is_some() {
|
||||
Some(vec![format!("role:{}", u.allowed_role_id.clone().unwrap())])
|
||||
} else {
|
||||
None
|
||||
},
|
||||
cidr: None,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
let config = NebulaConfig {
|
||||
|
@ -214,18 +300,16 @@ pub async fn generate_config(host: &Host, dh_pubkey: PublicKey, state: Data<AppS
|
|||
firewall: Some(NebulaConfigFirewall {
|
||||
conntrack: None,
|
||||
inbound: inbound_firewall,
|
||||
outbound: Some(vec![
|
||||
NebulaConfigFirewallRule {
|
||||
port: Some("any".to_string()),
|
||||
proto: Some("any".to_string()),
|
||||
ca_name: None,
|
||||
ca_sha: None,
|
||||
host: Some("any".to_string()),
|
||||
group: None,
|
||||
groups: None,
|
||||
cidr: None,
|
||||
}
|
||||
]),
|
||||
outbound: Some(vec![NebulaConfigFirewallRule {
|
||||
port: Some("any".to_string()),
|
||||
proto: Some("any".to_string()),
|
||||
ca_name: None,
|
||||
ca_sha: None,
|
||||
host: Some("any".to_string()),
|
||||
group: None,
|
||||
groups: None,
|
||||
cidr: None,
|
||||
}]),
|
||||
}),
|
||||
routines: 1,
|
||||
stats: None,
|
||||
|
@ -234,7 +318,12 @@ pub async fn generate_config(host: &Host, dh_pubkey: PublicKey, state: Data<AppS
|
|||
|
||||
let mut yaml_value = serde_yaml::to_value(&config).unwrap();
|
||||
|
||||
let all_overrides = host_overrides::dsl::host_overrides.filter(host_overrides::host_id.eq(&host.id)).select(HostOverride::as_select()).load(&mut conn).await.map_err(ConfigGenError::DbError)?;
|
||||
let all_overrides = host_overrides::dsl::host_overrides
|
||||
.filter(host_overrides::host_id.eq(&host.id))
|
||||
.select(HostOverride::as_select())
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.map_err(ConfigGenError::DbError)?;
|
||||
|
||||
// Cursed value overrides
|
||||
|
||||
|
@ -255,11 +344,11 @@ pub async fn generate_config(host: &Host, dh_pubkey: PublicKey, state: Data<AppS
|
|||
|
||||
current_val.as_mapping_mut().unwrap().insert(
|
||||
Value::String(key_split[key_split.len() - 1].to_string()),
|
||||
serde_yaml::from_str(h_override.value)?,
|
||||
serde_yaml::from_str(&h_override.value).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let merged_config: NebulaConfig = serde_yaml::from_value(yaml_value).unwrap();
|
||||
|
||||
Ok(merged_config)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,7 +143,7 @@ pub fn sign_cert_with_ca(
|
|||
pub struct DnclientKeyLockbox {
|
||||
pub info: Vec<u8>,
|
||||
pub nonce: Vec<u8>,
|
||||
pub key: Vec<u8>
|
||||
pub key: Vec<u8>,
|
||||
}
|
||||
|
||||
pub fn create_dnclient_ed_key(
|
||||
|
@ -155,7 +155,7 @@ pub fn create_dnclient_ed_key(
|
|||
&hex::decode(&config.tokens.data_encryption_key)
|
||||
.map_err(|e| CryptographyError::InvalidKey(e))?,
|
||||
)
|
||||
.map_err(|_| CryptographyError::InvalidKeyLength)?;
|
||||
.map_err(|_| CryptographyError::InvalidKeyLength)?;
|
||||
|
||||
let salt = XChaCha20Poly1305::generate_nonce(&mut OsRng);
|
||||
|
||||
|
@ -169,13 +169,16 @@ pub fn create_dnclient_ed_key(
|
|||
aad: &aad,
|
||||
},
|
||||
)
|
||||
.map_err(|e| CryptographyError::LockingError)?;
|
||||
.map_err(|_| CryptographyError::LockingError)?;
|
||||
|
||||
Ok((DnclientKeyLockbox {
|
||||
info: aad.to_vec(),
|
||||
nonce: salt.as_slice().to_vec(),
|
||||
key: lockbox,
|
||||
}, key.verifying_key()))
|
||||
Ok((
|
||||
DnclientKeyLockbox {
|
||||
info: aad.to_vec(),
|
||||
nonce: salt.as_slice().to_vec(),
|
||||
key: lockbox,
|
||||
},
|
||||
key.verifying_key(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn sign_dnclient_with_lockbox(
|
||||
|
@ -187,7 +190,7 @@ pub fn sign_dnclient_with_lockbox(
|
|||
&hex::decode(&config.tokens.data_encryption_key)
|
||||
.map_err(|e| CryptographyError::InvalidKey(e))?,
|
||||
)
|
||||
.map_err(|_| CryptographyError::InvalidKeyLength)?;
|
||||
.map_err(|_| CryptographyError::InvalidKeyLength)?;
|
||||
|
||||
let salt_u24: [u8; 24] = lockbox
|
||||
.nonce
|
||||
|
@ -212,7 +215,7 @@ pub fn sign_dnclient_with_lockbox(
|
|||
.try_into()
|
||||
.map_err(|_| CryptographyError::InvalidSigningKeyLength)?,
|
||||
)
|
||||
.map_err(|e| CryptographyError::SignatureError(e))?;
|
||||
.map_err(|e| CryptographyError::SignatureError(e))?;
|
||||
|
||||
Ok(key.sign(bytes))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@ pub mod auth;
|
|||
pub mod email;
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
pub mod crypt;
|
||||
mod config_generator;
|
||||
pub mod crypt;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
|
|
|
@ -290,6 +290,7 @@ pub struct Host {
|
|||
pub platform: Option<String>,
|
||||
pub update_available: Option<bool>,
|
||||
pub tags: Vec<Option<String>>,
|
||||
pub counter: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
@ -311,7 +312,7 @@ pub struct HostOverride {
|
|||
pub id: String,
|
||||
pub host_id: String,
|
||||
pub key: String,
|
||||
pub value: Value,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
@ -340,7 +341,7 @@ pub struct HostKey {
|
|||
|
||||
pub salt: Vec<u8>,
|
||||
pub info: Vec<u8>,
|
||||
pub server_ed_priv: Vec<u8>
|
||||
pub server_ed_priv: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
use crate::config_generator::generate_config;
|
||||
use crate::crypt::{create_dnclient_ed_key, sign_dnclient_with_lockbox};
|
||||
use crate::models::{Host, HostKey};
|
||||
use crate::response::JsonAPIResponse;
|
||||
use crate::schema::{host_keys, hosts};
|
||||
use crate::{randid, AppState};
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Data, Json};
|
||||
use base64::Engine;
|
||||
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use dnapi_rs::message::{
|
||||
CheckForUpdateResponse, CheckForUpdateResponseWrapper, DoUpdateRequest, DoUpdateResponse,
|
||||
RequestV1, RequestWrapper, SignedResponseWrapper, SignedResponse
|
||||
};
|
||||
use log::warn;
|
||||
use serde::Serialize;
|
||||
use std::time::SystemTime;
|
||||
use trifid_pki::cert::{
|
||||
deserialize_ed25519_public, deserialize_x25519_public, serialize_ed25519_public,
|
||||
};
|
||||
use trifid_pki::ed25519_dalek::{Signature, Verifier, VerifyingKey};
|
||||
use trifid_pki::x25519_dalek::PublicKey;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub enum DnclientResponse {
|
||||
CheckForUpdateResp(CheckForUpdateResponseWrapper),
|
||||
DoUpdateResp(SignedResponseWrapper),
|
||||
}
|
||||
|
||||
pub async fn dnclient_req(
|
||||
req: Json<RequestV1>,
|
||||
state: Data<AppState>,
|
||||
) -> JsonAPIResponse<DnclientResponse> {
|
||||
if req.version != 1 {
|
||||
err!(
|
||||
StatusCode::BAD_REQUEST,
|
||||
make_err!(
|
||||
"ERR_INVALID_DNCLIENT_VERSION",
|
||||
"unsupported dnclient api version",
|
||||
"version"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let mut conn = handle_error!(state.pool.get().await);
|
||||
|
||||
// get key for this host and counter
|
||||
let maybe_key: Option<HostKey> = handle_error!(host_keys::dsl::host_keys
|
||||
.filter(host_keys::host_id.eq(&req.host_id))
|
||||
.filter(host_keys::counter.eq(req.counter as i32))
|
||||
.first::<HostKey>(&mut conn)
|
||||
.await
|
||||
.optional());
|
||||
|
||||
let key = match maybe_key {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
err!(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
make_err!(
|
||||
"ERR_UNAUTHORIZED",
|
||||
"no key for host/counter pair in keystore"
|
||||
)
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let signature = handle_error!(Signature::from_slice(&req.signature));
|
||||
let key = handle_error!(VerifyingKey::from_bytes(
|
||||
&key.client_ed_pub.try_into().unwrap()
|
||||
));
|
||||
|
||||
if key.verify(req.message.as_bytes(), &signature).is_err() {
|
||||
warn!("! invalid signature from {}", req.host_id);
|
||||
err!(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
make_err!("ERR_UNAUTHORIZED", "unauthorized")
|
||||
)
|
||||
}
|
||||
|
||||
// Sig OK
|
||||
|
||||
let msg_raw = handle_error!(base64::engine::general_purpose::STANDARD.decode(&req.message));
|
||||
|
||||
let req_w: RequestWrapper = handle_error!(serde_json::from_slice(&msg_raw));
|
||||
|
||||
let host = handle_error!(
|
||||
hosts::table
|
||||
.find(&req.host_id)
|
||||
.first::<Host>(&mut conn)
|
||||
.await
|
||||
);
|
||||
|
||||
handle_error!(
|
||||
diesel::update(&host)
|
||||
.set(hosts::dsl::last_seen_at.eq(SystemTime::now()))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
);
|
||||
|
||||
match req_w.message_type.as_str() {
|
||||
"CheckForUpdate" => {
|
||||
ok!(DnclientResponse::CheckForUpdateResp(
|
||||
CheckForUpdateResponseWrapper {
|
||||
data: CheckForUpdateResponse {
|
||||
update_available: host.update_available.unwrap()
|
||||
|| req.counter < host.counter.unwrap() as u32
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
"DoUpdate" => {
|
||||
if !host.update_available.unwrap_or(false) {
|
||||
err!(
|
||||
StatusCode::BAD_REQUEST,
|
||||
make_err!("ERR_NO_CONFIG_AVAIL", "no new configuration available")
|
||||
);
|
||||
}
|
||||
|
||||
let do_update_req: DoUpdateRequest =
|
||||
handle_error!(serde_json::from_slice(&req_w.value));
|
||||
|
||||
let new_dh_pub_bytes =
|
||||
handle_error!(deserialize_x25519_public(&do_update_req.dh_pubkey_pem));
|
||||
let new_dh_pub_static: [u8; 32] = new_dh_pub_bytes.clone().try_into().unwrap();
|
||||
let new_dh_pub = PublicKey::from(new_dh_pub_static);
|
||||
|
||||
let new_config = handle_error!(generate_config(&host, new_dh_pub, state.clone()).await);
|
||||
|
||||
let new_ed_key =
|
||||
handle_error!(deserialize_ed25519_public(&do_update_req.ed_pubkey_pem));
|
||||
|
||||
let new_counter = host.counter.unwrap() + 1;
|
||||
|
||||
let (key_lockbox, trusted_key) = handle_error!(create_dnclient_ed_key(&state.config));
|
||||
|
||||
let new_key = HostKey {
|
||||
id: randid!(id "hostkey"),
|
||||
host_id: host.id.clone(),
|
||||
counter: new_counter,
|
||||
client_ed_pub: new_ed_key,
|
||||
client_dh_pub: new_dh_pub_bytes,
|
||||
client_cert: new_config.pki.cert.as_bytes().to_vec(),
|
||||
salt: key_lockbox.nonce.clone(),
|
||||
info: key_lockbox.info.clone(),
|
||||
server_ed_priv: key_lockbox.key.clone(),
|
||||
};
|
||||
|
||||
handle_error!(
|
||||
diesel::insert_into(host_keys::table)
|
||||
.values(&new_key)
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
);
|
||||
|
||||
handle_error!(
|
||||
diesel::update(&host)
|
||||
.set((
|
||||
hosts::dsl::last_seen_at.eq(SystemTime::now()),
|
||||
hosts::dsl::update_available.eq(Some(false)),
|
||||
hosts::dsl::counter.eq(Some(new_counter))
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
);
|
||||
|
||||
let config_str = handle_error!(serde_yaml::to_string(&new_config));
|
||||
|
||||
let msg = DoUpdateResponse {
|
||||
config: config_str.as_bytes().to_vec(),
|
||||
counter: new_counter as u32,
|
||||
nonce: do_update_req.nonce,
|
||||
trusted_keys: serialize_ed25519_public(trusted_key.as_bytes()),
|
||||
};
|
||||
|
||||
let msg_bytes = handle_error!(serde_json::to_vec(&msg));
|
||||
|
||||
let resp = SignedResponse {
|
||||
version: 1,
|
||||
message: msg_bytes.clone(),
|
||||
signature: sign_dnclient_with_lockbox(&key_lockbox, &msg_bytes, &state.config)
|
||||
.unwrap().to_vec(),
|
||||
};
|
||||
|
||||
let resp_w = SignedResponseWrapper { data: resp };
|
||||
|
||||
ok!(DnclientResponse::DoUpdateResp(resp_w));
|
||||
}
|
||||
_ => {
|
||||
err!(
|
||||
StatusCode::BAD_REQUEST,
|
||||
make_err!(
|
||||
"ERR_INVALID_DNCLIENT_REQ_TYPE",
|
||||
"unsupported dnclient request type",
|
||||
"message_type"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
pub mod auth;
|
||||
pub mod dnclient;
|
||||
pub mod networks;
|
||||
pub mod signup;
|
||||
pub mod totp_authenticators;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::crypt::create_signing_ca;
|
||||
use crate::models::{Network, NetworkNormalized, Organization, SigningCA, User};
|
||||
use crate::models::{Network, NetworkNormalized, Organization, User};
|
||||
use crate::response::JsonAPIResponse;
|
||||
use crate::schema::networks::dsl::networks;
|
||||
use crate::schema::organizations::dsl::organizations;
|
||||
|
@ -33,7 +33,7 @@ pub async fn create_network_req(
|
|||
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 (session_token, _) = enforce!(sess auth auth_info);
|
||||
|
||||
let user = handle_error!(
|
||||
users::table
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
use std::time::SystemTime;
|
||||
use crate::response::JsonAPIResponse;
|
||||
use crate::{AppState, randid};
|
||||
use actix_web::web::{Data, Json};
|
||||
use actix_web::http::StatusCode;
|
||||
use dnapi_rs::message::{EnrollRequest, EnrollResponse, EnrollResponseData};
|
||||
use crate::models::{EnrollmentCode, Host, HostKey};
|
||||
use crate::schema::{enrollment_codes, hosts};
|
||||
use diesel::{QueryDsl, OptionalExtension, ExpressionMethods};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use trifid_pki::x25519_dalek::PublicKey;
|
||||
use crate::config_generator::generate_config;
|
||||
use crate::crypt::create_dnclient_ed_key;
|
||||
use crate::models::Organization;
|
||||
use crate::models::{EnrollmentCode, Host, HostKey};
|
||||
use crate::response::JsonAPIResponse;
|
||||
use crate::schema::host_keys;
|
||||
use crate::schema::{enrollment_codes, hosts, organizations};
|
||||
use crate::{randid, AppState};
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::web::{Data, Json};
|
||||
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use dnapi_rs::message::{EnrollRequest, EnrollResponse, EnrollResponseData, EnrollResponseDataOrg};
|
||||
use std::time::SystemTime;
|
||||
use trifid_pki::cert::serialize_ed25519_public;
|
||||
use trifid_pki::x25519_dalek::PublicKey;
|
||||
|
||||
pub async fn enroll_req(
|
||||
req: Json<EnrollRequest>,
|
||||
|
@ -40,13 +42,13 @@ pub async fn enroll_req(
|
|||
|
||||
if token.expires < SystemTime::now() {
|
||||
err!(
|
||||
StatusCode::BAD_REQUEST,
|
||||
make_err!(
|
||||
"ERR_INVALID_VALUE",
|
||||
"does not exist (maybe it expired?)",
|
||||
"magicLinkToken"
|
||||
)
|
||||
);
|
||||
StatusCode::BAD_REQUEST,
|
||||
make_err!(
|
||||
"ERR_INVALID_VALUE",
|
||||
"does not exist (maybe it expired?)",
|
||||
"magicLinkToken"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// valid token
|
||||
|
@ -74,7 +76,11 @@ pub async fn enroll_req(
|
|||
|
||||
// reset the host's key entries
|
||||
|
||||
handle_error!(diesel::delete(host_keys::dsl::host_keys.filter(host_keys::host_id.eq(&host.id))).execute(&mut conn).await);
|
||||
handle_error!(
|
||||
diesel::delete(host_keys::dsl::host_keys.filter(host_keys::host_id.eq(&host.id)))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
);
|
||||
|
||||
let (key_lockbox, trusted_key) = handle_error!(create_dnclient_ed_key(&state.config));
|
||||
|
||||
|
@ -103,12 +109,36 @@ pub async fn enroll_req(
|
|||
.await
|
||||
);
|
||||
|
||||
let config_bytes = serde_yaml::to_string(&config).unwrap().as_bytes();
|
||||
let org = handle_error!(
|
||||
organizations::dsl::organizations
|
||||
.find(host.organization_id.clone())
|
||||
.first::<Organization>(&mut conn)
|
||||
.await
|
||||
);
|
||||
|
||||
let config_bytes = serde_yaml::to_string(&config).unwrap();
|
||||
|
||||
handle_error!(
|
||||
diesel::update(&host)
|
||||
.set((
|
||||
hosts::dsl::last_seen_at.eq(SystemTime::now()),
|
||||
hosts::dsl::update_available.eq(Some(false)),
|
||||
hosts::dsl::counter.eq(Some(1))
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
);
|
||||
|
||||
ok!(EnrollResponse::Success {
|
||||
data: EnrollResponseData {
|
||||
host_id: host.id.clone(),
|
||||
config: config_bytes.to_vec()
|
||||
config: config_bytes.as_bytes().to_vec(),
|
||||
counter: 1,
|
||||
trusted_keys: serialize_ed25519_public(trusted_key.as_bytes()),
|
||||
organization: EnrollResponseDataOrg {
|
||||
id: org.id,
|
||||
name: org.name
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ diesel::table! {
|
|||
platform -> Nullable<Varchar>,
|
||||
update_available -> Nullable<Bool>,
|
||||
tags -> Array<Nullable<Text>>,
|
||||
counter -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue