mod memory;
mod model;
mod refusal;
pub use memory::{MemorySessionStore, SessionCommit, SessionPrune, SessionPruned, SessionWrite};
pub use model::{Replay, Session, SessionLimits, SessionPolicy};
#[cfg(any(
feature = "portable-signature-verification",
feature = "certified-proof-verification"
))]
use crate::portable::validate_claims_context;
use crate::{
admission_budget::{AdmissionBudgetError, SessionAdmissionBudget},
canonical::CanonicalAuthError,
portable::{
LoginRevocationChange, LoginRevocationError, MemoryLoginRevocations, PortableAuthority,
PortableClaimsError, PortableEncodingError,
},
session_request::{SessionRequestError, SessionTtlLimit},
};
use ic_auth_protocol_types::{AuthRole, LogoutResult, LogoutUnavailable, Principal};
use thiserror::Error;
pub struct SessionContext<'a> {
pub authority: PortableAuthority<'a>,
pub max_ttl: SessionTtlLimit,
pub role: &'a AuthRole,
pub required_scopes: &'a [String],
}
impl SessionContext<'_> {
fn validate(
&self,
policy: SessionPolicy,
ledger: &MemoryLoginRevocations,
) -> Result<(), SessionError> {
if !policy.enabled || !policy.subject_admissible {
return Err(SessionError::Disabled);
}
if ledger.audience() != *self.authority.audience
|| !self.authority.authority_shape_valid()
|| self.required_scopes.len() > crate::limits::MAX_SESSION_SCOPES
{
return Err(SessionError::InvalidPolicyOrRecord);
}
for scope in self.required_scopes {
crate::canonical::validate_scope_label(scope)?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct SessionOccupancy {
pub sessions: usize,
pub replays: usize,
pub replays_for_caller: usize,
}
pub trait SessionReader {
fn generation(&self) -> u64;
fn limits(&self) -> SessionLimits;
fn session(&self, caller: Principal) -> Result<Option<Session>, SessionError>;
fn replay_exists(&self, fingerprint: [u8; 32]) -> Result<bool, SessionError>;
fn occupancy(&self, caller: Principal) -> Result<SessionOccupancy, SessionError>;
}
pub trait SessionStore {
fn read<T>(
&mut self,
operation: impl FnOnce(&dyn SessionReader) -> Result<T, SessionError>,
) -> Result<T, SessionError>;
fn admit(
&mut self,
session: &Session,
replay: &Replay,
login: &LoginRevocationChange,
) -> Result<bool, SessionError>;
fn logout(&mut self, login: &LoginRevocationChange) -> Result<(), SessionError>;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SessionEstablished {
Created(Session),
Replaced(Session),
ExactRetry(Session),
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum SessionError {
#[error("fresh session verification budget is exhausted")]
AdmissionThrottled,
#[error("session admission clock moved backwards")]
AdmissionClockRollback,
#[error("session capability is disabled or subject is inadmissible")]
Disabled,
#[error("session policy or stored record is invalid")]
InvalidPolicyOrRecord,
#[error("session authority is no longer current")]
StaleAuthority,
#[error("session is missing or expired")]
MissingOrExpired,
#[error("proof was already consumed or retry request differs")]
ReplayConflict,
#[error("requested or required scope is not granted")]
ScopeRejected,
#[error("session/replay capacity is exhausted")]
Capacity,
#[error("encoded record budget is exhausted")]
ByteCapacity,
#[error("authority generation is exhausted")]
GenerationExhausted,
#[error("host transaction could not commit")]
StorageFailure,
#[error(transparent)]
Canonical(#[from] CanonicalAuthError),
#[cfg(any(
feature = "portable-signature-verification",
feature = "certified-proof-verification"
))]
#[error(transparent)]
Credential(#[from] crate::portable_credential::PortableCredentialError),
#[error(transparent)]
Claims(#[from] PortableClaimsError),
#[error(transparent)]
Encoding(#[from] PortableEncodingError),
#[error(transparent)]
Revocation(#[from] LoginRevocationError),
}
impl From<AdmissionBudgetError> for SessionError {
fn from(error: AdmissionBudgetError) -> Self {
match error {
AdmissionBudgetError::InvalidPolicy => Self::InvalidPolicyOrRecord,
AdmissionBudgetError::Exhausted => Self::AdmissionThrottled,
AdmissionBudgetError::ClockRollback => Self::AdmissionClockRollback,
}
}
}
impl From<SessionRequestError> for SessionError {
fn from(error: SessionRequestError) -> Self {
match error {
SessionRequestError::InvalidRequest | SessionRequestError::InvalidLifetime => {
Self::InvalidPolicyOrRecord
}
SessionRequestError::ScopeRejected => Self::ScopeRejected,
SessionRequestError::Canonical(error) => Self::Canonical(error),
}
}
}
#[cfg(any(
feature = "portable-signature-verification",
feature = "certified-proof-verification"
))]
pub struct SessionAdmissionContext<'a> {
pub verification: crate::portable_credential::PortableVerificationContext<'a>,
pub role: &'a AuthRole,
pub ttl: crate::session_request::SessionTtlPolicy,
}
#[cfg(any(
feature = "portable-signature-verification",
feature = "certified-proof-verification"
))]
pub fn establish_session(
store: &mut impl SessionStore,
ledger: &mut MemoryLoginRevocations,
budget: &mut SessionAdmissionBudget,
credential: &ic_auth_protocol_types::PortableCredential,
request: &crate::session_request::SessionRequest,
admission: &SessionAdmissionContext<'_>,
policy: SessionPolicy,
) -> Result<SessionEstablished, SessionError> {
let claims_context = admission.verification.claims();
let context = SessionContext {
authority: claims_context.authority,
max_ttl: admission.ttl.maximum(),
role: admission.role,
required_scopes: &[],
};
context.validate(policy, ledger)?;
let authority = &context.authority;
let decoded = crate::portable::DecodedPortableClaims::decode(&credential.claims)?;
let claims = &decoded.claims;
let fingerprint = decoded.fingerprint;
if ledger.is_revoked(authority.caller, authority.now_ns)? {
return Err(LoginRevocationError::Revoked.into());
}
let retry = store.read(|tx| {
if tx.generation() != policy.generation {
return Err(SessionError::StaleAuthority);
}
let previous = tx.session(authority.caller)?;
let replacing = previous.is_some();
if let Some(session) = previous
&& session.fingerprint() == fingerprint
{
if session.request_hash() != request.hash() {
return Err(SessionError::ReplayConflict);
}
session.check_live(&context, policy)?;
session.check_revocation(ledger, authority.now_ns)?;
return Ok(Some(session));
}
if tx.replay_exists(fingerprint)? {
return Err(SessionError::ReplayConflict);
}
validate_claims_context(claims, claims_context)?;
request.evaluate(
&claims.grants,
admission.role,
authority.now_ns,
admission.ttl,
)?;
let limits = tx.limits();
limits.validate()?;
limits.check_capacity(tx.occupancy(authority.caller)?, replacing)?;
ledger.check_reservation(authority.caller, claims, authority.now_ns)?;
Ok(None)
})?;
if let Some(session) = retry {
return Ok(SessionEstablished::ExactRetry(session));
}
let admission_expires_at_ns = claims.admission_expires_at_ns;
let verified = decoded.verify(budget, &credential.proof, &admission.verification)?;
let session = Session::admit(&verified, request, &context, policy, admission.ttl)?;
let replay = Replay::new(&session, admission_expires_at_ns);
ledger.with_reservation(authority.caller, &verified, authority.now_ns, |login| {
let replacing = store.admit(&session, &replay, login)?;
Ok(if replacing {
SessionEstablished::Replaced(session)
} else {
SessionEstablished::Created(session)
})
})
}
pub fn authorize_session(
store: &mut impl SessionStore,
ledger: &mut MemoryLoginRevocations,
context: &SessionContext<'_>,
policy: SessionPolicy,
) -> Result<Session, SessionError> {
context.validate(policy, ledger)?;
if ledger.is_revoked(context.authority.caller, context.authority.now_ns)? {
return Err(LoginRevocationError::Revoked.into());
}
store.read(|tx| {
if tx.generation() != policy.generation {
return Err(SessionError::StaleAuthority);
}
let session = tx
.session(context.authority.caller)?
.ok_or(SessionError::MissingOrExpired)?;
session.check_live(context, policy)?;
session.check_revocation(ledger, context.authority.now_ns)?;
if !crate::certificate_issuance::scopes_are_subset(
context.required_scopes,
session.scopes(),
) {
return Err(SessionError::ScopeRejected);
}
Ok(session)
})
}
pub fn logout_session(
store: &mut impl SessionStore,
ledger: &mut MemoryLoginRevocations,
unknown_budget: &mut SessionAdmissionBudget,
caller: Principal,
now_ns: u64,
) -> LogoutResult {
let result = ledger.with_revocation(unknown_budget, caller, now_ns, |change| {
store
.logout(change)
.map_err(|_| LoginRevocationError::Storage)
});
match result {
Ok(deny_until_ns) => LogoutResult::Revoked { deny_until_ns },
Err(LoginRevocationError::InvalidCaller) => LogoutResult::InvalidCaller,
Err(LoginRevocationError::Capacity) => {
LogoutResult::Unavailable(LogoutUnavailable::Capacity)
}
Err(LoginRevocationError::Clock) => LogoutResult::Unavailable(LogoutUnavailable::Clock),
Err(
LoginRevocationError::InvalidPolicy
| LoginRevocationError::Revoked
| LoginRevocationError::Storage,
) => LogoutResult::Unavailable(LogoutUnavailable::Storage),
}
}