use crate::asn1::kerberos_flags::KerberosFlags;
use crate::asn1::ticket_flags::TicketFlags;
use crate::proto::reply::KerberosReply;
use crate::proto::{AuthenticationRequest, Name, TicketGrantRequest};
use std::cmp;
use std::time::{Duration, SystemTime};
use tracing::{trace, warn};
pub enum TimeBoundError {
Skew,
NeverValid,
FlagsInconsistent,
RenewalNotAllowed,
}
impl TimeBoundError {
pub fn to_kerberos_reply(
&self,
service_name: &Name,
current_time: SystemTime,
) -> KerberosReply {
match self {
TimeBoundError::Skew => {
KerberosReply::error_clock_skew(service_name.clone(), current_time)
}
TimeBoundError::NeverValid => {
KerberosReply::error_never_valid(service_name.clone(), current_time)
}
TimeBoundError::FlagsInconsistent => {
KerberosReply::error_request_invalid(service_name.clone(), current_time)
}
TimeBoundError::RenewalNotAllowed => {
KerberosReply::error_renew_denied(service_name.clone(), current_time)
}
}
}
}
pub struct AuthenticationTimeBound {
auth_time: SystemTime,
start_time: SystemTime,
end_time: SystemTime,
renew_until: Option<SystemTime>,
}
impl AuthenticationTimeBound {
pub fn auth_time(&self) -> SystemTime {
self.auth_time
}
pub fn start_time(&self) -> SystemTime {
self.start_time
}
pub fn end_time(&self) -> SystemTime {
self.end_time
}
pub fn renew_until(&self) -> Option<SystemTime> {
self.renew_until
}
pub fn from_as_req(
current_time: SystemTime,
maximum_clock_skew: Duration,
minimum_ticket_lifetime: Duration,
default_ticket_lifetime: Duration,
maximum_ticket_lifetime: Duration,
maximum_renew_lifetime: Option<Duration>,
auth_req: &AuthenticationRequest,
) -> Result<AuthenticationTimeBound, TimeBoundError> {
let auth_time = current_time;
let start_time = as_req_start_time(current_time, maximum_clock_skew, auth_req.from)?;
let end_time = as_req_end_time(
minimum_ticket_lifetime,
default_ticket_lifetime,
maximum_ticket_lifetime,
start_time,
auth_req.until,
)?;
let renew_until = as_req_renew_until(
start_time,
end_time,
auth_req.renew,
maximum_renew_lifetime,
auth_req.kdc_options,
)?;
Ok(AuthenticationTimeBound {
auth_time,
start_time,
end_time,
renew_until,
})
}
}
fn as_req_start_time(
current_time: SystemTime,
maximum_clock_skew: Duration,
start_time: Option<SystemTime>,
) -> Result<SystemTime, TimeBoundError> {
let Some(requested_start_time) = start_time else {
trace!("No requested start time, using current time");
return Ok(current_time);
};
if is_within_allowed_skew(current_time, requested_start_time, maximum_clock_skew) {
Ok(requested_start_time)
} else {
Err(TimeBoundError::Skew)
}
}
fn as_req_end_time(
minimum_ticket_lifetime: Duration,
default_ticket_lifetime: Duration,
maximum_ticket_lifetime: Duration,
start_time: SystemTime,
requested_end_time: SystemTime,
) -> Result<SystemTime, TimeBoundError> {
if requested_end_time == SystemTime::UNIX_EPOCH {
trace!("Endtime set to unix epoch, granting default ticket lifetime.");
return Ok(start_time + default_ticket_lifetime);
}
match requested_end_time.duration_since(start_time) {
Ok(diff) => {
let tkt_req_time = cmp::max(diff, minimum_ticket_lifetime);
let tkt_req_time = cmp::min(tkt_req_time, maximum_ticket_lifetime);
Ok(start_time + tkt_req_time)
}
Err(_) => Err(TimeBoundError::NeverValid),
}
}
fn as_req_renew_until(
start_time: SystemTime,
end_time: SystemTime,
requested_renew_until: Option<SystemTime>,
maximum_renew_lifetime: Option<Duration>,
kdc_options: KerberosFlags,
) -> Result<Option<SystemTime>, TimeBoundError> {
let renewal_flags_set = kdc_options.contains(KerberosFlags::RenewableOk)
|| kdc_options.contains(KerberosFlags::Renewable);
match (
requested_renew_until,
maximum_renew_lifetime,
renewal_flags_set,
) {
(Some(_), _, false) => {
Err(TimeBoundError::FlagsInconsistent)
}
(_, None, _) => {
Err(TimeBoundError::RenewalNotAllowed)
}
(None, Some(maximum_renew_lifetime), _) => {
Ok(Some(start_time + maximum_renew_lifetime))
}
(Some(requested_renew_until), Some(maximum_renew_lifetime), _) => {
if requested_renew_until < end_time {
return Err(TimeBoundError::NeverValid);
}
Ok(Some(cmp::min(
requested_renew_until,
start_time + maximum_renew_lifetime,
)))
}
}
}
fn is_within_allowed_skew(
reference_time: SystemTime,
requested_time: SystemTime,
maximum_clock_skew: Duration,
) -> bool {
match reference_time.duration_since(requested_time) {
Ok(diff) => {
diff <= maximum_clock_skew
}
Err(diff) => {
diff.duration() <= maximum_clock_skew
}
}
}
pub struct TicketGrantTimeBound {
start_time: SystemTime,
end_time: SystemTime,
renew_until: Option<SystemTime>,
}
impl TicketGrantTimeBound {
pub fn start_time(&self) -> SystemTime {
self.start_time
}
pub fn end_time(&self) -> SystemTime {
self.end_time
}
pub fn renew_until(&self) -> Option<SystemTime> {
self.renew_until
}
pub fn from_tgs_req(
current_time: SystemTime,
maximum_clock_skew: Duration,
maximum_service_ticket_lifetime: Duration,
tgs_req_valid: &TicketGrantRequest,
) -> Result<TicketGrantTimeBound, TimeBoundError> {
let client_tgt = tgs_req_valid.ticket_granting_ticket();
let current_time = cmp::max(current_time, client_tgt.auth_time());
let start_time = tgs_req_start_time(
current_time,
tgs_req_valid.requested_start_time(),
maximum_clock_skew,
client_tgt.start_time(),
client_tgt.end_time(),
)?;
let end_time = tgs_req_end_time(
start_time,
tgs_req_valid.requested_end_time(),
client_tgt.end_time(),
client_tgt.renew_until(),
maximum_service_ticket_lifetime,
)?;
let renew_until = None;
Ok(TicketGrantTimeBound {
start_time,
end_time,
renew_until,
})
}
}
fn tgs_req_start_time(
current_time: SystemTime,
requested_start_time: Option<SystemTime>,
maximum_clock_skew: Duration,
client_tgt_start_time: SystemTime,
client_tgt_end_time: SystemTime,
) -> Result<SystemTime, TimeBoundError> {
let requested_start_time = requested_start_time.unwrap_or(current_time);
let requested_start_time = if requested_start_time == SystemTime::UNIX_EPOCH {
current_time
} else {
requested_start_time
};
let requested_start_time = cmp::min(requested_start_time, client_tgt_start_time);
if !is_within_allowed_skew(current_time, requested_start_time, maximum_clock_skew) {
return Err(TimeBoundError::Skew);
}
if requested_start_time > client_tgt_end_time {
return Err(TimeBoundError::NeverValid);
}
Ok(requested_start_time)
}
fn tgs_req_end_time(
start_time: SystemTime,
requested_end_time: SystemTime,
client_tgt_end_time: SystemTime,
client_tgt_renew_until: Option<SystemTime>,
maximum_service_ticket_lifetime: Duration,
) -> Result<SystemTime, TimeBoundError> {
let requested_end_time = match requested_end_time.duration_since(start_time) {
Ok(diff) => {
if diff > maximum_service_ticket_lifetime {
start_time + maximum_service_ticket_lifetime
} else {
requested_end_time
}
}
Err(_) if requested_end_time == SystemTime::UNIX_EPOCH => {
start_time + maximum_service_ticket_lifetime
}
Err(_) => return Err(TimeBoundError::NeverValid),
};
let clamp_bound = client_tgt_renew_until.unwrap_or(client_tgt_end_time);
let requested_end_time = cmp::min(requested_end_time, clamp_bound);
Ok(requested_end_time)
}
pub struct TicketRenewTimeBound {
start_time: SystemTime,
end_time: SystemTime,
renew_until: SystemTime,
}
impl TicketRenewTimeBound {
pub fn start_time(&self) -> SystemTime {
self.start_time
}
pub fn end_time(&self) -> SystemTime {
self.end_time
}
pub fn renew_until(&self) -> SystemTime {
self.renew_until
}
pub fn from_tgs_req(
current_time: SystemTime,
maximum_clock_skew: Duration,
maximum_ticket_lifetime: Duration,
tgs_req_valid: &TicketGrantRequest,
) -> Result<TicketRenewTimeBound, TimeBoundError> {
if !tgs_req_valid
.ticket_flags()
.contains(TicketFlags::Renewable)
{
warn!("Denying renewal of ticket that is not renewable.");
return Err(TimeBoundError::RenewalNotAllowed);
}
let client_tgt = tgs_req_valid.ticket_granting_ticket();
let Some(renew_until) = client_tgt.renew_until() else {
warn!("Denying renewal of ticket that has no renew time.");
return Err(TimeBoundError::RenewalNotAllowed);
};
let start_time = tgs_req_start_time(
current_time,
tgs_req_valid.requested_start_time(),
maximum_clock_skew,
client_tgt.start_time(),
client_tgt.end_time(),
)?;
let end_time = tgs_req_end_time(
start_time,
tgs_req_valid.requested_end_time(),
client_tgt.end_time(),
client_tgt.renew_until(),
maximum_ticket_lifetime,
)?;
Ok(TicketRenewTimeBound {
start_time,
end_time,
renew_until,
})
}
}