Skip to content

Commit 1c2fb50

Browse files
author
Emily Ehlert
committed
Switch from "validity_in_years" to flexible "validity_duration" with unit support
Enable different units to be selected (hours, days, months, years) for certificate and CA creation. Both TLS as well as SSL certs are supported.
1 parent 216d193 commit 1c2fb50

File tree

14 files changed

+191
-89
lines changed

14 files changed

+191
-89
lines changed

backend/src/api.rs

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::certs::ssh_cert::{get_ssh_pem, SSHCertificateBuilder};
1212
use crate::certs::tls_cert::{get_timestamp, get_tls_pem, TLSCertificateBuilder};
1313
use crate::constants::VAULTLS_VERSION;
1414
use crate::data::api::{CallbackQuery, ChangePasswordRequest, CreateCARequest, CreateUserCertificateRequest, CreateUserRequest, DownloadResponse, IsSetupResponse, LoginRequest, SetupRequest};
15-
use crate::data::enums::{CAType, CertificateType, PasswordRule, UserRole};
15+
use crate::data::enums::{CAType, CertificateType, PasswordRule, TimespanUnit, UserRole};
1616
use crate::data::error::ApiError;
1717
use crate::data::objects::{AppState, User};
1818
use crate::notification::mail::{MailMessage, Mailer};
@@ -81,9 +81,11 @@ pub(crate) async fn setup(
8181

8282
state.db.insert_user(user).await?;
8383

84+
let cert_validity = setup_req.validity_duration.unwrap_or(5);
85+
let cert_validity_unit = setup_req.validity_unit.unwrap_or(TimespanUnit::Year);
8486
let mut ca = TLSCertificateBuilder::new()?
8587
.set_name(&setup_req.ca_name)?
86-
.set_valid_until(setup_req.ca_validity_in_years)?
88+
.set_valid_until(cert_validity, cert_validity_unit)?
8789
.build_ca()?;
8890
ca = state.db.insert_ca(ca).await?;
8991
save_ca(&ca)?;
@@ -287,10 +289,11 @@ pub(crate) async fn create_ca(
287289
) -> Result<Json<i64>, ApiError> {
288290
let mut ca = match payload.ca_type {
289291
CAType::TLS => {
290-
let validity = payload.validity_in_years.unwrap_or(5);
292+
let cert_validity = payload.validity_duration.unwrap_or(5);
293+
let cert_validity_unit = payload.validity_unit.unwrap_or(TimespanUnit::Year);
291294
TLSCertificateBuilder::new()?
292295
.set_name(&payload.ca_name)?
293-
.set_valid_until(validity)?
296+
.set_valid_until(cert_validity, cert_validity_unit)?
294297
.build_ca()?
295298
},
296299
CAType::SSH => {
@@ -320,9 +323,10 @@ pub(crate) async fn create_user_certificate(
320323

321324
let mut ca = get_appropriate_ca(state, &payload).await?;
322325
ca = ensure_ca_validity(&mut ca, state, &payload).await?;
323-
324-
let cert_validity_in_years = payload.validity_in_years.unwrap_or(1);
325-
let mut cert = build_certificate(&payload, &ca, &cert_password, cert_validity_in_years, state).await?;
326+
327+
let cert_validity = payload.validity_duration.unwrap_or(1);
328+
let cert_validity_unit = payload.validity_unit.unwrap_or(TimespanUnit::Year);
329+
let mut cert = build_certificate(&payload, &ca, &cert_password, cert_validity, cert_validity_unit, state).await?;
326330

327331
cert = state.db.insert_user_cert(cert).await?;
328332

@@ -374,8 +378,9 @@ async fn get_appropriate_ca(state: &State<AppState>, payload: &CreateUserCertifi
374378
}
375379

376380
async fn ensure_ca_validity(ca: &mut CA, state: &State<AppState>, payload: &CreateUserCertificateRequest) -> Result<CA, ApiError> {
377-
let cert_validity_in_years = payload.validity_in_years.unwrap_or(1);
378-
let cert_validity_timestamp = get_timestamp(cert_validity_in_years)?;
381+
let cert_validity = payload.validity_duration.unwrap_or(1);
382+
let cert_validity_unit = payload.validity_unit.unwrap_or(TimespanUnit::Year);
383+
let cert_validity_timestamp = get_timestamp(cert_validity, cert_validity_unit)?;
379384

380385
if ca.valid_until == -1 || cert_validity_timestamp.0 <= ca.valid_until {
381386
return Ok(ca.clone());
@@ -400,31 +405,33 @@ async fn build_certificate(
400405
payload: &CreateUserCertificateRequest,
401406
ca: &CA,
402407
cert_password: &str,
403-
cert_validity_in_years: u64,
408+
validity_duration: u64,
409+
validity_unit: TimespanUnit,
404410
state: &State<AppState>
405411
) -> Result<Certificate, ApiError> {
406412
let cert_type = payload.cert_type.ok_or_else(|| {
407413
ApiError::BadRequest("Certificate type is required".to_string())
408414
})?;
409415

410416
match cert_type {
411-
CertificateType::SSHClient => build_ssh_cert(payload, ca, cert_password, cert_validity_in_years, true),
412-
CertificateType::SSHServer => build_ssh_cert(payload, ca, cert_password, cert_validity_in_years, false),
413-
CertificateType::TLSClient => build_tls_cert(payload, ca, cert_password, cert_validity_in_years, state, true).await,
414-
CertificateType::TLSServer => build_tls_cert(payload, ca, cert_password, cert_validity_in_years, state, false).await,
417+
CertificateType::SSHClient => build_ssh_cert(payload, ca, cert_password, validity_duration, validity_unit, true),
418+
CertificateType::SSHServer => build_ssh_cert(payload, ca, cert_password, validity_duration, validity_unit, false),
419+
CertificateType::TLSClient => build_tls_cert(payload, ca, cert_password, validity_duration, validity_unit, state, true).await,
420+
CertificateType::TLSServer => build_tls_cert(payload, ca, cert_password, validity_duration, validity_unit, state, false).await,
415421
}
416422
}
417423

418424
fn build_ssh_cert(
419425
payload: &CreateUserCertificateRequest,
420426
ca: &CA,
421427
cert_password: &str,
422-
cert_validity_in_years: u64,
428+
validity_duration: u64,
429+
validity_unit: TimespanUnit,
423430
is_client: bool,
424431
) -> Result<Certificate, ApiError> {
425432
let mut cert_builder = SSHCertificateBuilder::new()?
426433
.set_name(&payload.cert_name)?
427-
.set_valid_until(cert_validity_in_years)?
434+
.set_valid_until(validity_duration, validity_unit)?
428435
.set_renew_method(payload.renew_method.unwrap_or_default())?
429436
.set_ca(ca)?
430437
.set_user_id(payload.user_id)?;
@@ -448,13 +455,14 @@ async fn build_tls_cert(
448455
payload: &CreateUserCertificateRequest,
449456
ca: &CA,
450457
pkcs12_password: &str,
451-
cert_validity_in_years: u64,
458+
validity_duration: u64,
459+
validity_unit: TimespanUnit,
452460
state: &State<AppState>,
453461
is_client: bool,
454462
) -> Result<Certificate, ApiError> {
455463
let mut cert_builder = TLSCertificateBuilder::new()?
456464
.set_name(&payload.cert_name)?
457-
.set_valid_until(cert_validity_in_years)?
465+
.set_valid_until(validity_duration, validity_unit)?
458466
.set_renew_method(payload.renew_method.unwrap_or_default())?
459467
.set_password(pkcs12_password)?
460468
.set_ca(ca)?

backend/src/certs/ssh_cert.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::certs::common::{Certificate, CA};
2-
use crate::data::enums::{CertificateRenewMethod, CertificateType};
2+
use crate::data::enums::{CertificateRenewMethod, CertificateType, TimespanUnit};
33
use anyhow::anyhow;
44
use anyhow::Result;
55
use rand::prelude::*;
@@ -46,8 +46,15 @@ impl SSHCertificateBuilder {
4646
Ok(self)
4747
}
4848

49-
pub fn set_valid_until(mut self, years: u64) -> Result<Self> {
50-
let valid_until = self.created_on + (365 * 86400 * 1000) * years as i64;
49+
pub fn set_valid_until(mut self, duration: u64, unit: TimespanUnit) -> Result<Self> {
50+
let duration_per_unit_h = match unit {
51+
TimespanUnit::Year => 365*24,
52+
TimespanUnit::Month => 30*24,
53+
TimespanUnit::Day => 24,
54+
TimespanUnit::Hour => 1,
55+
};
56+
let duration_s = 60 * 60 * duration as i64 * duration_per_unit_h;
57+
let valid_until = self.created_on + (duration_s * 1000);
5158
self.valid_until = Some(valid_until);
5259
Ok(self)
5360
}

backend/src/certs/tls_cert.rs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use openssl::x509::extension::{AuthorityKeyIdentifier, BasicConstraints, Extende
1616
use openssl::x509::X509Builder;
1717
use tracing::info;
1818
use crate::constants::{CA_DIR_PATH, CA_FILE_PATTERN, CA_TLS_FILE_PATH};
19-
use crate::data::enums::{CAType, CertificateRenewMethod, CertificateType};
19+
use crate::data::enums::{CAType, CertificateRenewMethod, CertificateType, TimespanUnit};
2020
use crate::data::enums::CertificateType::{TLSClient, TLSServer};
2121
#[cfg(not(feature = "test-mode"))]
2222
use crate::ApiError;
@@ -38,7 +38,7 @@ impl TLSCertificateBuilder {
3838
pub fn new() -> Result<Self> {
3939
let private_key = generate_private_key()?;
4040
let asn1_serial = generate_serial_number()?;
41-
let (created_on_unix, created_on_openssl) = get_timestamp(0)?;
41+
let (created_on_unix, created_on_openssl) = get_timestamp(0, TimespanUnit::Hour)?;
4242

4343
let mut x509 = X509Builder::new()?;
4444
x509.set_version(2)?;
@@ -67,11 +67,11 @@ impl TLSCertificateBuilder {
6767
/// - Renew Method\
6868
/// - User ID\
6969
pub fn try_from(old_cert: &Certificate) -> Result<Self> {
70-
let validity_in_years = ((old_cert.valid_until - old_cert.created_on) / 1000 / 60 / 60 / 24 / 365).max(1);
70+
let validity_d = ((old_cert.valid_until - old_cert.created_on) / 1000 / 60 / 60 / 24).max(14);
7171

7272
Self::new()?
7373
.set_name(&old_cert.name)?
74-
.set_valid_until(validity_in_years as u64)?
74+
.set_valid_until(validity_d as u64, TimespanUnit::Day)?
7575
.set_password(&old_cert.password)?
7676
.set_renew_method(old_cert.renew_method)?
7777
.set_user_id(old_cert.user_id)
@@ -81,11 +81,11 @@ impl TLSCertificateBuilder {
8181
if old_ca.ca_type != TLS {
8282
return Err(anyhow!("CA is not of type SSH"));
8383
}
84-
let validity_in_years = ((old_ca.valid_until - old_ca.created_on) / 1000 / 60 / 60 / 24 / 365).max(1);
84+
let validity_h = ((old_ca.valid_until - old_ca.created_on) / 1000 / 60 / 60 / 24).max(14);
8585

8686
Self::new()?
8787
.set_name(&old_ca.name)?
88-
.set_valid_until(validity_in_years as u64)?
88+
.set_valid_until(validity_h as u64, TimespanUnit::Day)?
8989
.build_ca()
9090

9191
}
@@ -97,9 +97,9 @@ impl TLSCertificateBuilder {
9797
Ok(self)
9898
}
9999

100-
pub fn set_valid_until(mut self, years: u64) -> Result<Self, anyhow::Error> {
101-
let (valid_until_unix, valid_until_openssl) = if years != 0 {
102-
get_timestamp(years)?
100+
pub fn set_valid_until(mut self, duration: u64, unit: TimespanUnit) -> Result<Self, anyhow::Error> {
101+
let (valid_until_unix, valid_until_openssl) = if duration != 0 {
102+
get_timestamp(duration, unit)?
103103
} else {
104104
get_short_lifetime()?
105105
};
@@ -314,12 +314,19 @@ fn generate_serial_number() -> Result<Asn1Integer, ErrorStack> {
314314
}
315315

316316
/// Returns the current UNIX timestamp in milliseconds and an OpenSSL Asn1Time object.
317-
pub(crate) fn get_timestamp(from_now_in_years: u64) -> Result<(i64, Asn1Time), ErrorStack> {
318-
let time = SystemTime::now() + std::time::Duration::from_secs(60 * 60 * 24 * 365 * from_now_in_years);
319-
let time_unix = time.duration_since(UNIX_EPOCH).unwrap().as_millis() as i64;
320-
let time_openssl = Asn1Time::days_from_now(365 * from_now_in_years as u32)?;
321-
322-
Ok((time_unix, time_openssl))
317+
pub(crate) fn get_timestamp(duration: u64, unit: TimespanUnit) -> Result<(i64, Asn1Time), ErrorStack> {
318+
let duration_per_unit_h = match unit {
319+
TimespanUnit::Year => 365*24,
320+
TimespanUnit::Month => 30*24,
321+
TimespanUnit::Day => 24,
322+
TimespanUnit::Hour => 1,
323+
};
324+
let duration_s = 60 * 60 * duration * duration_per_unit_h;
325+
let time = SystemTime::now() + std::time::Duration::from_secs(duration_s);
326+
let time_unix_ms = time.duration_since(UNIX_EPOCH).unwrap().as_millis() as i64;
327+
let time_openssl = Asn1Time::from_unix(time_unix_ms / 1000)?;
328+
329+
Ok((time_unix_ms, time_openssl))
323330
}
324331

325332
/// For E2E testing generate a short lifetime certificate.

backend/src/data/api.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use rocket_okapi::okapi::schemars;
88
use rocket_okapi::{okapi, JsonSchema, OpenApiError};
99
use rocket_okapi::okapi::openapi3::{Responses, Response as OAResponse, MediaType, RefOr};
1010
use rocket_okapi::response::OpenApiResponderInner;
11-
use crate::data::enums::{CAType, CertificateRenewMethod, CertificateType, UserRole};
11+
use crate::data::enums::{CAType, CertificateRenewMethod, CertificateType, TimespanUnit, UserRole};
1212

1313
#[derive(Serialize, Deserialize, JsonSchema)]
1414
pub struct IsSetupResponse {
@@ -22,7 +22,8 @@ pub struct SetupRequest {
2222
pub name: String,
2323
pub email: String,
2424
pub ca_name: String,
25-
pub ca_validity_in_years: u64,
25+
pub validity_duration: Option<u64>,
26+
pub validity_unit: Option<TimespanUnit>,
2627
pub password: Option<String>,
2728
}
2829

@@ -48,13 +49,15 @@ pub struct CallbackQuery {
4849
pub struct CreateCARequest {
4950
pub ca_name: String,
5051
pub ca_type: CAType,
51-
pub validity_in_years: Option<u64>
52+
pub validity_duration: Option<u64>,
53+
pub validity_unit: Option<TimespanUnit>,
5254
}
5355

5456
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
5557
pub struct CreateUserCertificateRequest {
5658
pub cert_name: String,
57-
pub validity_in_years: Option<u64>,
59+
pub validity_duration: Option<u64>,
60+
pub validity_unit: Option<TimespanUnit>,
5861
pub user_id: i64,
5962
pub notify_user: Option<bool>,
6063
pub system_generated_password: bool,

backend/src/data/enums.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,14 @@ impl FromSql for CertificateRenewMethod {
118118
_ => Err(FromSqlError::InvalidType),
119119
}
120120
}
121+
}
122+
123+
#[derive(Serialize_repr, Deserialize_repr, JsonSchema, TryFromPrimitive, Clone, Debug, Copy, PartialEq, Eq, Default)]
124+
#[repr(u8)]
125+
pub enum TimespanUnit {
126+
#[default]
127+
Year = 0,
128+
Month = 1,
129+
Day = 2,
130+
Hour = 3
121131
}

backend/tests/api/api_test_functionality.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use ssh_key::certificate::CertType;
1919
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
2020
use tokio::time::sleep;
2121
use tokio_rustls::{TlsAcceptor, TlsConnector};
22-
use vaultls::data::enums::{CAType, CertificateRenewMethod, CertificateType, UserRole};
22+
use vaultls::data::enums::{CAType, CertificateRenewMethod, CertificateType, TimespanUnit, UserRole};
2323
use vaultls::data::objects::User;
2424
use x509_parser::asn1_rs::FromDer;
2525
use x509_parser::prelude::X509Certificate;
@@ -107,7 +107,8 @@ async fn test_setup_hash() -> Result<()> {
107107
name: TEST_USER_NAME.to_string(),
108108
email: TEST_USER_EMAIL.to_string(),
109109
ca_name: TEST_CA_NAME.to_string(),
110-
ca_validity_in_years: 1,
110+
validity_duration: Some(1),
111+
validity_unit: Some(TimespanUnit::Year),
111112
password: Some(password_hash.to_string()),
112113
};
113114

@@ -365,8 +366,8 @@ async fn test_create_new_ca() -> Result<()> {
365366
assert_eq!(new_cas.len(), 2);
366367
assert_eq!(new_cas[1].name, TEST_SECOND_CA_NAME.to_string());
367368
assert_eq!(new_cas[1].id, 2);
368-
assert!(now > new_cas[1].created_on && new_cas[1].created_on > now - 10000 /* 10 seconds */);
369-
assert!(valid_until > new_cas[1].valid_until && new_cas[1].valid_until > valid_until - 10000 /* 10 seconds */);
369+
assert!(now >= new_cas[1].created_on && new_cas[1].created_on > now - 10000 /* 10 seconds */);
370+
assert!(valid_until >= new_cas[1].valid_until && new_cas[1].valid_until > valid_until - 10000 /* 10 seconds */);
370371

371372
let ca_by_id_pem = client.download_tls_ca_by_id(2).await?;
372373
let ca_pem = client.download_current_tls_ca().await?;
@@ -404,7 +405,8 @@ async fn test_create_certificate_with_short_lived_ca() -> Result<()> {
404405

405406
let cert_req = CreateUserCertificateRequest {
406407
cert_name: TEST_CLIENT_CERT_NAME.to_string(),
407-
validity_in_years: Some(2),
408+
validity_duration: Some(2),
409+
validity_unit: Some(TimespanUnit::Year),
408410
user_id: 1,
409411
notify_user: None,
410412
system_generated_password: false,

backend/tests/common/test_client.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::ops::{Deref, DerefMut};
77
use serde_json::Value;
88
use vaultls::create_test_rocket;
99
use vaultls::data::api::{CreateCARequest, CreateUserCertificateRequest, CreateUserRequest, LoginRequest, SetupRequest};
10-
use vaultls::data::enums::{CAType, CertificateRenewMethod, CertificateType, UserRole};
10+
use vaultls::data::enums::{CAType, CertificateRenewMethod, CertificateType, TimespanUnit, UserRole};
1111
use x509_parser::pem::Pem;
1212
use vaultls::certs::common::{Certificate, CA};
1313
use vaultls::data::enums::CertificateType::{SSHClient, TLSClient};
@@ -46,7 +46,8 @@ impl VaulTLSClient {
4646
name: TEST_USER_NAME.to_string(),
4747
email: TEST_USER_EMAIL.to_string(),
4848
ca_name: TEST_CA_NAME.to_string(),
49-
ca_validity_in_years: 2,
49+
validity_duration: Some(2),
50+
validity_unit: Some(TimespanUnit::Year),
5051
password: Some(TEST_PASSWORD.to_string()),
5152
};
5253

@@ -83,7 +84,8 @@ impl VaulTLSClient {
8384
pub(crate) async fn create_client_cert(&self, user_id: Option<i64>, password: Option<String>, ca_id: Option<i64>) -> Result<Certificate> {
8485
let cert_req = CreateUserCertificateRequest {
8586
cert_name: TEST_CLIENT_CERT_NAME.to_string(),
86-
validity_in_years: Some(1),
87+
validity_duration: Some(1),
88+
validity_unit: Some(TimespanUnit::Year),
8789
user_id: user_id.unwrap_or(1),
8890
notify_user: None,
8991
system_generated_password: false,
@@ -108,7 +110,8 @@ impl VaulTLSClient {
108110
pub(crate) async fn create_ssh_client_cert(&self) -> Result<Certificate> {
109111
let cert_req = CreateUserCertificateRequest {
110112
cert_name: TEST_SSH_CLIENT_CERT_NAME.to_string(),
111-
validity_in_years: Some(1),
113+
validity_duration: Some(1),
114+
validity_unit: Some(TimespanUnit::Year),
112115
user_id: 1,
113116
notify_user: None,
114117
system_generated_password: false,
@@ -133,7 +136,8 @@ impl VaulTLSClient {
133136
pub(crate) async fn create_server_cert(&self) -> Result<()> {
134137
let cert_req = CreateUserCertificateRequest {
135138
cert_name: TEST_SERVER_CERT_NAME.to_string(),
136-
validity_in_years: Some(1),
139+
validity_duration: Some(1),
140+
validity_unit: Some(TimespanUnit::Year),
137141
user_id: 1,
138142
notify_user: None,
139143
system_generated_password: false,
@@ -294,7 +298,8 @@ impl VaulTLSClient {
294298
let data = CreateCARequest {
295299
ca_name: TEST_SECOND_CA_NAME.to_string(),
296300
ca_type: CAType::TLS,
297-
validity_in_years: Some(15)
301+
validity_duration: Some(15),
302+
validity_unit: Some(TimespanUnit::Year),
298303
};
299304

300305
let request = self
@@ -311,7 +316,8 @@ impl VaulTLSClient {
311316
let data = CreateCARequest {
312317
ca_name: TEST_SSH_CA_NAME.to_string(),
313318
ca_type: CAType::SSH,
314-
validity_in_years: None
319+
validity_duration: None,
320+
validity_unit: None,
315321
};
316322

317323
let request = self

0 commit comments

Comments
 (0)