cert signing on org creation
This commit is contained in:
parent
85cee5fb4e
commit
677f60fc2b
|
@ -95,7 +95,16 @@
|
|||
"grouphelp": "Comma-separated list of groups. This will constrain which groups can be applied to client certs. Default: empty (any)",
|
||||
"apierror": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -75,27 +75,55 @@
|
|||
|
||||
let showAdditionalConstraints = false;
|
||||
|
||||
function handle(listfield: string): string[] {
|
||||
if (listfield == "") {
|
||||
return []
|
||||
} else {
|
||||
return listfield.split(',')
|
||||
}
|
||||
}
|
||||
|
||||
async function doCreateFlow() {
|
||||
// STEP ONE: Create the org
|
||||
logger.info("Creating base organization");
|
||||
logger.info("Creating organization");
|
||||
let created_org_id;
|
||||
|
||||
|
||||
|
||||
try {
|
||||
let resp = await fetch_timeout(`${API_ROOT}/v1/org`, {
|
||||
'method': 'POST',
|
||||
'headers': {
|
||||
'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) {
|
||||
let err = await resp.json().errors[0].message;
|
||||
if (!resp.ok) {
|
||||
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]}`);
|
||||
fullPageError = true;
|
||||
fullPageErrorTitle = t('neworg.apierror.orgcreate');
|
||||
fullPageErrorSubtitle = t('neworg.apierror.' + err);
|
||||
return;
|
||||
}
|
||||
created_org_id = resp.json().org_id;
|
||||
logger.info("Able to create base organization with id " + created_org_id);
|
||||
let resp_objectified = JSON.parse(await resp.text());
|
||||
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) {
|
||||
logger.error(`${e}`);
|
||||
fullPageError = true;
|
||||
|
|
|
@ -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.
|
||||
# ------- WARNING -------
|
||||
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
|
|
@ -6,3 +6,4 @@ magic_links_valid_for = 86400
|
|||
session_tokens_valid_for = 86400
|
||||
totp_verification_valid_for = 3600
|
||||
data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2"
|
||||
ca_certs_valid_for = 31536000
|
|
@ -23,8 +23,9 @@ pub struct TFConfig {
|
|||
pub db_url: String,
|
||||
pub base: Url,
|
||||
pub web_root: Url,
|
||||
pub magic_links_valid_for: i64,
|
||||
pub session_tokens_valid_for: i64,
|
||||
pub totp_verification_valid_for: i64,
|
||||
pub data_key: String
|
||||
pub magic_links_valid_for: u64,
|
||||
pub session_tokens_valid_for: u64,
|
||||
pub totp_verification_valid_for: u64,
|
||||
pub data_key: String,
|
||||
pub ca_certs_valid_for: u64
|
||||
}
|
|
@ -182,6 +182,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||
crate::routes::v1::user::get_user,
|
||||
crate::routes::v1::user::options,
|
||||
crate::routes::v1::organization::createorgoptions,
|
||||
crate::routes::v1::ca::get_cas_for_org,
|
||||
crate::routes::v1::ca::options
|
||||
])
|
||||
.register("/", catchers![
|
||||
crate::routes::handler_400,
|
||||
|
|
|
@ -15,11 +15,13 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::error::Error;
|
||||
use log::info;
|
||||
use rocket::form::validate::Contains;
|
||||
use sqlx::PgPool;
|
||||
use trifid_pki::ca::NebulaCAPool;
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
|
@ -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>> {
|
||||
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>> {
|
||||
|
|
|
@ -22,7 +22,7 @@ use sqlx::PgPool;
|
|||
use crate::config::TFConfig;
|
||||
use serde::{Serialize, Deserialize};
|
||||
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")]
|
||||
pub fn options(_id: i32) -> &'static str {
|
||||
|
@ -45,6 +45,15 @@ pub struct 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)> {
|
||||
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 {
|
||||
Ok(pool) => pool,
|
||||
Err(e) => {
|
||||
|
@ -73,17 +82,4 @@ pub async fn get_cas_for_org(id: i32, user: TOTPAuthenticatedUserInfo, db: &Stat
|
|||
trusted_cas,
|
||||
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>) {
|
||||
|
||||
}
|
|
@ -14,11 +14,16 @@
|
|||
// You should have received a copy of the GNU General Public License
|
||||
// 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::serde::json::Json;
|
||||
use serde::{Serialize, Deserialize};
|
||||
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::config::TFConfig;
|
||||
use crate::crypto::{encrypt_with_nonce, generate_random_iv, get_cipher_from_config};
|
||||
|
@ -66,7 +71,7 @@ pub struct OrginfoStruct {
|
|||
|
||||
#[get("/v1/org/<orgid>")]
|
||||
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")));
|
||||
}
|
||||
|
||||
|
@ -83,12 +88,59 @@ pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State
|
|||
)))
|
||||
}
|
||||
|
||||
#[post("/v1/org")]
|
||||
pub async fn create_org(user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
|
||||
#[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", 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() {
|
||||
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
|
||||
let iv = generate_random_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)));
|
||||
}
|
||||
};
|
||||
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),
|
||||
Err(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)))?;
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ pub async fn create_totp_token(email: String, db: &PgPool, config: &TFConfig) ->
|
|||
|
||||
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))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue