#![forbid(unsafe_code)]
#![deny(missing_docs)]
pub mod session;
pub use session::{
CsrExtensionRequest, CsrRejectionReason, NoRevocations, RevocationChecker, RevocationHandle,
SessionCertificateBundle, SessionCertificateError, SessionCertificateIssuer,
SessionCertificatePolicy, SessionCertificateProfile, SessionCertificateRequest,
SessionCertificateSigner, SessionCsrProfile, SessionExtendedKeyUsage, SessionSubjectAltName,
SignedSessionCertificate,
};
use security_core::classification::DataClassification;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ClientType {
Desktop,
Mobile,
Ci,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Platform {
MacOs,
Ios,
Android,
Windows,
Linux,
Ci,
Unsupported,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AttestationMode {
Off,
Monitor,
Enforce,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum BootstrapStatus {
Authorised,
Revoked,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum BootstrapBinding {
PerInstall,
SharedApp,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BootstrapIdentity {
pub app_id: String,
pub subject: String,
pub fingerprint: String,
pub status: BootstrapStatus,
pub binding: BootstrapBinding,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeviceAttestationEvidence {
pub provider: String,
pub challenge_id: String,
pub payload_summary: String,
pub freshness: EvidenceFreshness,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum EvidenceFreshness {
Fresh,
Stale,
Unsupported,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReleaseChannel {
Dev,
Ci,
Production,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeviceTrustRequest {
pub bootstrap: BootstrapIdentity,
pub client_type: ClientType,
pub platform: Platform,
pub release_channel: ReleaseChannel,
pub attestation_mode: AttestationMode,
pub attestation: Option<DeviceAttestationEvidence>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeviceTrustOutcome {
Trusted,
LowerTrust,
Denied,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum TrustTier {
None,
SoftwareBound,
HardwareBacked,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DeviceTrustReason {
BootstrapAuthorised,
BootstrapRevoked,
SharedBootstrapRejected,
PlatformAttestationFresh,
AttestationUnsupported,
AttestationRequired,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeviceTrustDecision {
outcome: DeviceTrustOutcome,
tier: TrustTier,
reasons: Vec<DeviceTrustReason>,
audit_classification: DataClassification,
}
impl DeviceTrustDecision {
#[must_use]
pub fn outcome(&self) -> DeviceTrustOutcome {
self.outcome
}
#[must_use]
pub fn tier(&self) -> TrustTier {
self.tier
}
#[must_use]
pub fn reasons(&self) -> &[DeviceTrustReason] {
&self.reasons
}
#[must_use]
pub fn audit_classification(&self) -> DataClassification {
self.audit_classification
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeviceTrustError {
MalformedEvidence {
field: &'static str,
},
}
impl std::fmt::Display for DeviceTrustError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MalformedEvidence { field } => {
write!(f, "malformed attestation evidence field: {field}")
}
}
}
}
impl std::error::Error for DeviceTrustError {}
#[derive(Clone, Copy, Debug)]
pub struct DeviceTrustPolicy {
production: bool,
}
impl DeviceTrustPolicy {
#[must_use]
pub fn production() -> Self {
Self { production: true }
}
pub fn evaluate(
&self,
request: &DeviceTrustRequest,
) -> Result<DeviceTrustDecision, DeviceTrustError> {
if let Some(attestation) = &request.attestation {
validate_attestation(attestation)?;
}
if request.bootstrap.status == BootstrapStatus::Revoked {
return Ok(denied(DeviceTrustReason::BootstrapRevoked));
}
if self.production && request.bootstrap.binding == BootstrapBinding::SharedApp {
return Ok(denied(DeviceTrustReason::SharedBootstrapRejected));
}
let mut reasons = vec![DeviceTrustReason::BootstrapAuthorised];
let supported_platform = supports_platform_attestation(request.platform);
match request.attestation_mode {
AttestationMode::Off => Ok(lower_trust(reasons)),
AttestationMode::Monitor => {
if let Some(attestation) = &request.attestation {
if supported_platform && attestation.freshness == EvidenceFreshness::Fresh {
reasons.push(DeviceTrustReason::PlatformAttestationFresh);
return Ok(trusted(reasons));
}
}
if !supported_platform {
reasons.push(DeviceTrustReason::AttestationUnsupported);
}
Ok(lower_trust(reasons))
}
AttestationMode::Enforce => match &request.attestation {
Some(attestation)
if supported_platform && attestation.freshness == EvidenceFreshness::Fresh =>
{
reasons.push(DeviceTrustReason::PlatformAttestationFresh);
Ok(trusted(reasons))
}
Some(attestation) if attestation.freshness == EvidenceFreshness::Unsupported => {
reasons.push(DeviceTrustReason::AttestationUnsupported);
Ok(lower_trust(reasons))
}
_ => {
reasons.push(DeviceTrustReason::AttestationRequired);
Ok(DeviceTrustDecision {
outcome: DeviceTrustOutcome::Denied,
tier: TrustTier::None,
reasons,
audit_classification: DataClassification::Confidential,
})
}
},
}
}
}
fn trusted(reasons: Vec<DeviceTrustReason>) -> DeviceTrustDecision {
DeviceTrustDecision {
outcome: DeviceTrustOutcome::Trusted,
tier: TrustTier::HardwareBacked,
reasons,
audit_classification: DataClassification::Confidential,
}
}
fn lower_trust(reasons: Vec<DeviceTrustReason>) -> DeviceTrustDecision {
DeviceTrustDecision {
outcome: DeviceTrustOutcome::LowerTrust,
tier: TrustTier::SoftwareBound,
reasons,
audit_classification: DataClassification::Confidential,
}
}
fn denied(reason: DeviceTrustReason) -> DeviceTrustDecision {
DeviceTrustDecision {
outcome: DeviceTrustOutcome::Denied,
tier: TrustTier::None,
reasons: vec![reason],
audit_classification: DataClassification::Confidential,
}
}
fn supports_platform_attestation(platform: Platform) -> bool {
matches!(
platform,
Platform::Ios | Platform::Android | Platform::Windows | Platform::MacOs
)
}
fn validate_attestation(attestation: &DeviceAttestationEvidence) -> Result<(), DeviceTrustError> {
validate_label("provider", &attestation.provider)?;
validate_label("challenge_id", &attestation.challenge_id)?;
if attestation.payload_summary.is_empty() || attestation.payload_summary.len() > 1024 {
return Err(DeviceTrustError::MalformedEvidence {
field: "payload_summary",
});
}
Ok(())
}
fn validate_label(field: &'static str, value: &str) -> Result<(), DeviceTrustError> {
if value.is_empty() || value.len() > 80 {
return Err(DeviceTrustError::MalformedEvidence { field });
}
if !value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
{
return Err(DeviceTrustError::MalformedEvidence { field });
}
Ok(())
}