cert signing on org creation

This commit is contained in:
core 2023-03-01 22:06:07 -05:00
parent 85cee5fb4e
commit 677f60fc2b
Signed by: core
GPG Key ID: FDBF740DADDCEECF
10 changed files with 133 additions and 33 deletions

View File

@ -95,7 +95,16 @@
"grouphelp": "Comma-separated list of groups. This will constrain which groups can be applied to client certs. Default: empty (any)", "grouphelp": "Comma-separated list of groups. This will constrain which groups can be applied to client certs. Default: empty (any)",
"apierror": { "apierror": {
"orgcreate": "Unable to create organization", "orgcreate": "Unable to create organization",
"TypeError: NetworkError when attempting to fetch resource": "Unable to contact the backend. Please try again later." "cacreate": "Unable to create signing CA",
"TypeError: NetworkError when attempting to fetch resource": "Unable to contact the backend. Please try again later.",
"invaliddata": "The server was unable to parse your request. Reload the page and try again. Ensure all fields are correct."
}
},
"admin": {
"apierror": {
"loadownedorg": "Unable to load organizations",
"this user does not have permission to access this org": "You do not have permission to access one of the orgs associated with your account. Your access may have been revoked. Please reload the page."
} }
} }
} }

View File

@ -75,27 +75,55 @@
let showAdditionalConstraints = false; let showAdditionalConstraints = false;
function handle(listfield: string): string[] {
if (listfield == "") {
return []
} else {
return listfield.split(',')
}
}
async function doCreateFlow() { async function doCreateFlow() {
// STEP ONE: Create the org // STEP ONE: Create the org
logger.info("Creating base organization"); logger.info("Creating organization");
let created_org_id; let created_org_id;
try { try {
let resp = await fetch_timeout(`${API_ROOT}/v1/org`, { let resp = await fetch_timeout(`${API_ROOT}/v1/org`, {
'method': 'POST', 'method': 'POST',
'headers': { 'headers': {
'Authorization': 'Bearer ' + api_token 'Authorization': 'Bearer ' + api_token
} },
'body': JSON.stringify({
'ip_ranges': handle(org_ip_range),
'subnet_ranges': handle(org_subnets),
'groups': handle(org_groups)
})
}); });
if (resp.code !== 200) { if (!resp.ok) {
let err = await resp.json().errors[0].message; if (resp.status === 422) {
// we had an invalid input
logger.error(`Invalid input`);
fullPageError = true;
fullPageErrorTitle = t('neworg.apierror.orgcreate');
fullPageErrorSubtitle = t('neworg.apierror.invaliddata');
return;
}
let err = JSON.parse(await resp.text()).errors[0].message;
logger.error(`${await resp.json().errors[0]}`); logger.error(`${await resp.json().errors[0]}`);
fullPageError = true; fullPageError = true;
fullPageErrorTitle = t('neworg.apierror.orgcreate'); fullPageErrorTitle = t('neworg.apierror.orgcreate');
fullPageErrorSubtitle = t('neworg.apierror.' + err); fullPageErrorSubtitle = t('neworg.apierror.' + err);
return; return;
} }
created_org_id = resp.json().org_id; let resp_objectified = JSON.parse(await resp.text());
logger.info("Able to create base organization with id " + created_org_id); console.log(JSON.stringify(resp_objectified));
created_org_id = resp_objectified.org_id;
logger.info("Able to create organization with id " + created_org_id);
logger.info("Org create success! Redirecting to manage page");
window.location.href = "/org/" + created_org_id;
} catch (e) { } catch (e) {
logger.error(`${e}`); logger.error(`${e}`);
fullPageError = true; fullPageError = true;

View File

@ -72,3 +72,10 @@ totp_verification_valid_for = 3600
# Do not change this value in a production instance. It will make existing data inaccessible until changed back. # Do not change this value in a production instance. It will make existing data inaccessible until changed back.
# ------- WARNING ------- # ------- WARNING -------
data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2" data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2"
# How long should CA certs be valid for before they need to be replaced (in seconds)?
# This controls the maximum amount of time a network on this instance can go
# without a rekey.
# You probably don't need to change, this, 31536000 (1 year) is a sane default.
# This value only affects new certs signed by this instance.
ca_certs_valid_for = 31536000

View File

@ -6,3 +6,4 @@ magic_links_valid_for = 86400
session_tokens_valid_for = 86400 session_tokens_valid_for = 86400
totp_verification_valid_for = 3600 totp_verification_valid_for = 3600
data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2" data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2"
ca_certs_valid_for = 31536000

View File

@ -23,8 +23,9 @@ pub struct TFConfig {
pub db_url: String, pub db_url: String,
pub base: Url, pub base: Url,
pub web_root: Url, pub web_root: Url,
pub magic_links_valid_for: i64, pub magic_links_valid_for: u64,
pub session_tokens_valid_for: i64, pub session_tokens_valid_for: u64,
pub totp_verification_valid_for: i64, pub totp_verification_valid_for: u64,
pub data_key: String pub data_key: String,
pub ca_certs_valid_for: u64
} }

View File

@ -182,6 +182,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
crate::routes::v1::user::get_user, crate::routes::v1::user::get_user,
crate::routes::v1::user::options, crate::routes::v1::user::options,
crate::routes::v1::organization::createorgoptions, crate::routes::v1::organization::createorgoptions,
crate::routes::v1::ca::get_cas_for_org,
crate::routes::v1::ca::options
]) ])
.register("/", catchers![ .register("/", catchers![
crate::routes::handler_400, crate::routes::handler_400,

View File

@ -15,11 +15,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::error::Error; use std::error::Error;
use log::info;
use rocket::form::validate::Contains; use rocket::form::validate::Contains;
use sqlx::PgPool; use sqlx::PgPool;
use trifid_pki::ca::NebulaCAPool; use trifid_pki::ca::NebulaCAPool;
pub async fn get_org_by_owner_id(user: i32, db: &PgPool) -> Result<Option<i32>, Box<dyn Error>> { pub async fn get_org_by_owner_id(user: i32, db: &PgPool) -> Result<Option<i32>, Box<dyn Error>> {
Ok(sqlx::query!("SELECT id FROM organizations WHERE owner = $1", user).fetch_optional(db).await?.map(|r| r.id)) Ok(sqlx::query!("SELECT id FROM organizations WHERE owner = $1", user).fetch_optional(db).await?.map(|r| r.id))
} }
@ -60,7 +62,9 @@ pub async fn get_associated_orgs(user: i32, db: &PgPool) -> Result<Vec<i32>, Box
} }
pub async fn user_has_org_assoc(user: i32, org: i32, db: &PgPool) -> Result<bool, Box<dyn Error>> { pub async fn user_has_org_assoc(user: i32, org: i32, db: &PgPool) -> Result<bool, Box<dyn Error>> {
Ok(get_associated_orgs(user, db).await?.contains(org)) let associated_orgs = get_associated_orgs(user, db).await?;
Ok(associated_orgs.contains(org))
} }
pub async fn get_org_ca_pool(org: i32, db: &PgPool) -> Result<NebulaCAPool, Box<dyn Error>> { pub async fn get_org_ca_pool(org: i32, db: &PgPool) -> Result<NebulaCAPool, Box<dyn Error>> {

View File

@ -22,7 +22,7 @@ use sqlx::PgPool;
use crate::config::TFConfig; use crate::config::TFConfig;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::auth::TOTPAuthenticatedUserInfo; use crate::auth::TOTPAuthenticatedUserInfo;
use crate::org::get_org_ca_pool; use crate::org::{get_associated_orgs, get_org_ca_pool};
#[options("/v1/org/<_id>/ca")] #[options("/v1/org/<_id>/ca")]
pub fn options(_id: i32) -> &'static str { pub fn options(_id: i32) -> &'static str {
@ -45,6 +45,15 @@ pub struct CA {
#[get("/v1/org/<id>/ca")] #[get("/v1/org/<id>/ca")]
pub async fn get_cas_for_org(id: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<CaList>), (Status, String)> { pub async fn get_cas_for_org(id: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<CaList>), (Status, String)> {
let associated_orgs = match get_associated_orgs(user.user_id, db.inner()).await {
Ok(r) => r,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DB_QUERY_FAILED", "an error occurred while running the database query", e)))
};
if !associated_orgs.contains(&id) {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_NOT_YOUR_ORG", "you are not authorized to view details of this org")))
}
let ca_pool = match get_org_ca_pool(id, db.inner()).await { let ca_pool = match get_org_ca_pool(id, db.inner()).await {
Ok(pool) => pool, Ok(pool) => pool,
Err(e) => { Err(e) => {
@ -74,16 +83,3 @@ pub async fn get_cas_for_org(id: i32, user: TOTPAuthenticatedUserInfo, db: &Stat
blocklisted_certs: ca_pool.cert_blocklist, blocklisted_certs: ca_pool.cert_blocklist,
}))) })))
} }
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct CreateCARequest {
pub ip_ranges: Vec<Ipv4Net>,
pub subnet_ranges: Vec<Ipv4Net>,
pub groups: Vec<String>
}
#[post("/v1/org/<id>/ca", data = "<data>")]
pub async fn create_signing_ca_req(id: i32, data: Json<CreateCARequest>, user: TOTPAuthenticatedUserInfo, config: &State<TFConfig>, db: &State<PgPool>) {
}

View File

@ -14,11 +14,16 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use rocket::{get, post, options, State}; use std::time::{Duration, SystemTime};
use ipnet::Ipv4Net;
use rocket::{get, post, options, State, error};
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use sqlx::PgPool; use sqlx::PgPool;
use trifid_pki::cert::{NebulaCertificate, NebulaCertificateDetails, serialize_ed25519_private};
use trifid_pki::ed25519_dalek::{SigningKey};
use trifid_pki::rand_core::OsRng;
use crate::auth::TOTPAuthenticatedUserInfo; use crate::auth::TOTPAuthenticatedUserInfo;
use crate::config::TFConfig; use crate::config::TFConfig;
use crate::crypto::{encrypt_with_nonce, generate_random_iv, get_cipher_from_config}; use crate::crypto::{encrypt_with_nonce, generate_random_iv, get_cipher_from_config};
@ -66,7 +71,7 @@ pub struct OrginfoStruct {
#[get("/v1/org/<orgid>")] #[get("/v1/org/<orgid>")]
pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> { pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
if !user_has_org_assoc(orgid, user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? { if !user_has_org_assoc(user.user_id, orgid, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org"))); return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org")));
} }
@ -83,12 +88,59 @@ pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State
))) )))
} }
#[post("/v1/org")] #[derive(Serialize, Deserialize)]
pub async fn create_org(user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> { #[serde(crate = "rocket::serde")]
pub struct CreateCARequest {
pub ip_ranges: Vec<Ipv4Net>,
pub subnet_ranges: Vec<Ipv4Net>,
pub groups: Vec<String>
}
#[post("/v1/org", data = "<req>")]
pub async fn create_org(req: Json<CreateCARequest>, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
if get_org_by_owner_id(user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?.is_some() { if get_org_by_owner_id(user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?.is_some() {
return Err((Status::Conflict, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_USER_OWNS_ORG", "a user can only own one organization at a time"))) return Err((Status::Conflict, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_USER_OWNS_ORG", "a user can only own one organization at a time")))
} }
// Generate the CA keypair
let private_key = SigningKey::generate(&mut OsRng);
let public_key = private_key.verifying_key();
// Create the CA certificate
let mut ca_cert = NebulaCertificate {
details: NebulaCertificateDetails {
name: format!("{}'s Organization - Root Signing CA", user.email),
ips: req.ip_ranges.clone(),
subnets: req.subnet_ranges.clone(),
groups: req.groups.clone(),
not_before: SystemTime::now(),
not_after: SystemTime::now() + Duration::from_secs(config.ca_certs_valid_for),
public_key: public_key.to_bytes(),
is_ca: true,
issuer: "".to_string(), // This is a self-signed certificate! There is no issuer present
},
signature: vec![],
};
// Self-sign the CA certificate
match ca_cert.sign(&private_key) {
Ok(_) => (),
Err(e) => {
error!("[tfapi] security: certificate signature error: {}", e);
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_CERT_SIGN_ERROR", "there was an error generating the CA certificate, please try again later")))
}
}
// PEM-encode the CA key
let ca_key_pem = serialize_ed25519_private(&private_key.to_keypair_bytes());
// PEM-encode the CA cert
let ca_cert_pem = match ca_cert.serialize_to_pem() {
Ok(pem) => pem,
Err(e) => {
error!("[tfapi] security: certificate encoding error: {}", e);
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_CERT_ENCODE_ERROR", "there was an error encoding the CA certificate, please try again later")))
}
};
// generate an AES iv to use for key encryption // generate an AES iv to use for key encryption
let iv = generate_random_iv(); let iv = generate_random_iv();
let iv_hex = hex::encode(iv); let iv_hex = hex::encode(iv);
@ -100,13 +152,13 @@ pub async fn create_org(user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, con
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_CREATE_CIPHER", "Unable to build cipher construct, please try again later", e))); return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_CREATE_CIPHER", "Unable to build cipher construct, please try again later", e)));
} }
}; };
let ca_key = match encrypt_with_nonce(b"", iv, &cipher) { let ca_key = match encrypt_with_nonce(&ca_key_pem, iv, &cipher) {
Ok(key) => hex::encode(key), Ok(key) => hex::encode(key),
Err(e) => { Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_ENCRYPT_KEY", "Unable to build cipher construct, please try again later", e))); return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_ENCRYPT_KEY", "Unable to build cipher construct, please try again later", e)));
} }
}; };
let ca_crt = hex::encode(b""); let ca_crt = hex::encode(ca_cert_pem);
let result = sqlx::query!("INSERT INTO organizations (owner, ca_key, ca_crt, iv) VALUES ($1, $2, $3, $4) RETURNING id", owner_id, ca_key, ca_crt, iv_hex).fetch_one(db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?; let result = sqlx::query!("INSERT INTO organizations (owner, ca_key, ca_crt, iv) VALUES ($1, $2, $3, $4) RETURNING id", owner_id, ca_key, ca_crt, iv_hex).fetch_one(db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?;

View File

@ -74,7 +74,7 @@ pub async fn create_totp_token(email: String, db: &PgPool, config: &TFConfig) ->
let otpid = format!("totp-{}", Uuid::new_v4()); let otpid = format!("totp-{}", Uuid::new_v4());
sqlx::query!("INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?; sqlx::query!("INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?;
Ok((otpid, totpmachine)) Ok((otpid, totpmachine))
} }