#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ObservedSdkIdentity {
pub present: bool,
pub version: Option<String>,
}
impl ObservedSdkIdentity {
pub fn present(version: impl Into<String>) -> Self {
Self {
present: true,
version: Some(version.into()),
}
}
pub fn missing() -> Self {
Self {
present: false,
version: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VerifiedSdkIdentity {
pub version: Option<String>,
}
impl VerifiedSdkIdentity {
pub fn none() -> Self {
Self { version: None }
}
pub fn with_version(version: impl Into<String>) -> Self {
Self {
version: Some(version.into()),
}
}
pub fn is_available(&self) -> bool {
self.version.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SdkIdentityVerdict {
Ok,
Missing,
VersionDowngraded,
Forged,
Unverifiable,
}
impl SdkIdentityVerdict {
pub fn as_str(&self) -> &'static str {
match self {
SdkIdentityVerdict::Ok => "ok",
SdkIdentityVerdict::Missing => "missing",
SdkIdentityVerdict::VersionDowngraded => "downgraded",
SdkIdentityVerdict::Forged => "forged",
SdkIdentityVerdict::Unverifiable => "unverifiable",
}
}
pub fn is_suspected_tamper(&self) -> bool {
matches!(
self,
SdkIdentityVerdict::Missing | SdkIdentityVerdict::VersionDowngraded | SdkIdentityVerdict::Forged
)
}
}
pub fn classify(
observed: &ObservedSdkIdentity,
verified: &VerifiedSdkIdentity,
min_supported_version: Option<&str>,
) -> SdkIdentityVerdict {
if !observed.present {
return SdkIdentityVerdict::Missing;
}
if let (Some(claimed), Some(attested)) = (observed.version.as_deref(), verified.version.as_deref()) {
if !versions_equal(claimed, attested) {
return SdkIdentityVerdict::Forged;
}
}
if let Some(min) = min_supported_version {
match observed.version.as_deref() {
Some(claimed) if version_at_least(claimed, min) => {}
_ => return SdkIdentityVerdict::VersionDowngraded,
}
}
if !verified.is_available() && min_supported_version.is_none() {
return SdkIdentityVerdict::Unverifiable;
}
SdkIdentityVerdict::Ok
}
fn versions_equal(a: &str, b: &str) -> bool {
matches!(compare_versions(a, b), Some(std::cmp::Ordering::Equal))
}
fn version_at_least(version: &str, min: &str) -> bool {
matches!(
compare_versions(version, min),
Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
)
}
fn compare_versions(a: &str, b: &str) -> Option<std::cmp::Ordering> {
let pa = parse_components(a)?;
let pb = parse_components(b)?;
let len = pa.len().max(pb.len());
for i in 0..len {
let ca = pa.get(i).copied().unwrap_or(0);
let cb = pb.get(i).copied().unwrap_or(0);
match ca.cmp(&cb) {
std::cmp::Ordering::Equal => continue,
other => return Some(other),
}
}
Some(std::cmp::Ordering::Equal)
}
fn parse_components(v: &str) -> Option<Vec<u64>> {
if v.is_empty() {
return None;
}
v.split('.').map(|c| c.parse::<u64>().ok()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_when_not_present() {
let observed = ObservedSdkIdentity::missing();
let verdict = classify(&observed, &VerifiedSdkIdentity::none(), Some("1.0.0"));
assert_eq!(verdict, SdkIdentityVerdict::Missing);
}
#[test]
fn missing_takes_precedence_over_a_verified_reference() {
let observed = ObservedSdkIdentity::missing();
let verified = VerifiedSdkIdentity::with_version("2.0.0");
assert_eq!(classify(&observed, &verified, None), SdkIdentityVerdict::Missing);
}
#[test]
fn forged_when_observed_version_mismatches_verified() {
let observed = ObservedSdkIdentity::present("9.9.9");
let verified = VerifiedSdkIdentity::with_version("1.2.3");
assert_eq!(classify(&observed, &verified, None), SdkIdentityVerdict::Forged);
}
#[test]
fn forged_beats_downgrade_when_both_apply() {
let observed = ObservedSdkIdentity::present("0.1.0");
let verified = VerifiedSdkIdentity::with_version("2.0.0");
assert_eq!(
classify(&observed, &verified, Some("1.0.0")),
SdkIdentityVerdict::Forged
);
}
#[test]
fn ok_when_observed_matches_verified() {
let observed = ObservedSdkIdentity::present("1.2.3");
let verified = VerifiedSdkIdentity::with_version("1.2.3");
assert_eq!(classify(&observed, &verified, None), SdkIdentityVerdict::Ok);
}
#[test]
fn ok_when_observed_matches_verified_with_zero_padding() {
let observed = ObservedSdkIdentity::present("1.2");
let verified = VerifiedSdkIdentity::with_version("1.2.0");
assert_eq!(classify(&observed, &verified, None), SdkIdentityVerdict::Ok);
}
#[test]
fn version_downgraded_below_minimum() {
let observed = ObservedSdkIdentity::present("0.9.0");
let verdict = classify(&observed, &VerifiedSdkIdentity::none(), Some("1.0.0"));
assert_eq!(verdict, SdkIdentityVerdict::VersionDowngraded);
}
#[test]
fn ok_at_exactly_the_minimum() {
let observed = ObservedSdkIdentity::present("1.0.0");
let verdict = classify(&observed, &VerifiedSdkIdentity::none(), Some("1.0.0"));
assert_eq!(verdict, SdkIdentityVerdict::Ok);
}
#[test]
fn ok_above_the_minimum() {
let observed = ObservedSdkIdentity::present("1.5.2");
let verdict = classify(&observed, &VerifiedSdkIdentity::none(), Some("1.0.0"));
assert_eq!(verdict, SdkIdentityVerdict::Ok);
}
#[test]
fn unparseable_version_against_a_floor_fails_closed_as_downgrade() {
let observed = ObservedSdkIdentity::present("not-a-version");
let verdict = classify(&observed, &VerifiedSdkIdentity::none(), Some("1.0.0"));
assert_eq!(verdict, SdkIdentityVerdict::VersionDowngraded);
}
#[test]
fn unverifiable_when_present_but_no_verified_reference_and_no_floor() {
let observed = ObservedSdkIdentity::present("1.2.3");
let verdict = classify(&observed, &VerifiedSdkIdentity::none(), None);
assert_eq!(verdict, SdkIdentityVerdict::Unverifiable);
}
#[test]
fn present_without_a_claimed_version_and_no_floor_is_unverifiable() {
let observed = ObservedSdkIdentity {
present: true,
version: None,
};
let verdict = classify(&observed, &VerifiedSdkIdentity::none(), None);
assert_eq!(verdict, SdkIdentityVerdict::Unverifiable);
}
#[test]
fn as_str_labels_are_stable() {
assert_eq!(SdkIdentityVerdict::Ok.as_str(), "ok");
assert_eq!(SdkIdentityVerdict::Missing.as_str(), "missing");
assert_eq!(SdkIdentityVerdict::VersionDowngraded.as_str(), "downgraded");
assert_eq!(SdkIdentityVerdict::Forged.as_str(), "forged");
assert_eq!(SdkIdentityVerdict::Unverifiable.as_str(), "unverifiable");
}
#[test]
fn downgrade_flagged_against_a_verified_version_and_floor() {
let observed = ObservedSdkIdentity::present("1.0.0");
let verified = VerifiedSdkIdentity::with_version("1.0.0");
let verdict = classify(&observed, &verified, Some("1.5.0"));
assert_eq!(verdict, SdkIdentityVerdict::VersionDowngraded);
}
#[test]
fn ok_when_verified_current_version_clears_the_floor() {
let observed = ObservedSdkIdentity::present("2.0.0");
let verified = VerifiedSdkIdentity::with_version("2.0.0");
assert_eq!(classify(&observed, &verified, Some("1.5.0")), SdkIdentityVerdict::Ok);
}
#[test]
fn forged_when_observed_contradicts_the_verified_version() {
let observed = ObservedSdkIdentity::present("1.0.0");
let verified = VerifiedSdkIdentity::with_version("2.0.0");
assert_eq!(classify(&observed, &verified, None), SdkIdentityVerdict::Forged);
}
#[test]
fn only_missing_downgraded_forged_are_suspected_tamper() {
assert!(SdkIdentityVerdict::Missing.is_suspected_tamper());
assert!(SdkIdentityVerdict::VersionDowngraded.is_suspected_tamper());
assert!(SdkIdentityVerdict::Forged.is_suspected_tamper());
assert!(!SdkIdentityVerdict::Ok.is_suspected_tamper());
assert!(!SdkIdentityVerdict::Unverifiable.is_suspected_tamper());
}
}