From 27a04b7b53efd0637d490768094e2b063b4255e7 Mon Sep 17 00:00:00 2001 From: bordumb Date: Thu, 26 Mar 2026 23:25:13 -0700 Subject: [PATCH] feat: add AssuranceLevel to show how trustworthy each platform claim is --- crates/auths-cli/src/ux/format.rs | 75 ++++++++ crates/auths-core/src/ports/platform.rs | 26 +++ .../auths-core/tests/cases/assurance_level.rs | 50 +++++ crates/auths-core/tests/cases/mod.rs | 1 + crates/auths-policy/src/compile.rs | 30 ++- crates/auths-policy/src/compiled.rs | 13 +- crates/auths-policy/src/context.rs | 13 +- crates/auths-policy/src/decision.rs | 4 + crates/auths-policy/src/eval.rs | 101 +++++++++++ crates/auths-policy/src/expr.rs | 8 + crates/auths-policy/src/lib.rs | 4 +- crates/auths-policy/src/types.rs | 4 +- crates/auths-verifier/src/lib.rs | 5 +- crates/auths-verifier/src/types.rs | 171 ++++++++++++++++++ 14 files changed, 496 insertions(+), 9 deletions(-) create mode 100644 crates/auths-core/tests/cases/assurance_level.rs diff --git a/crates/auths-cli/src/ux/format.rs b/crates/auths-cli/src/ux/format.rs index 80f67a46..25815092 100644 --- a/crates/auths-cli/src/ux/format.rs +++ b/crates/auths-cli/src/ux/format.rs @@ -7,6 +7,7 @@ #![allow(dead_code)] // Some functions are for future use +use auths_verifier::AssuranceLevel; use console::{Style, Term}; use serde::Serialize; use std::io::IsTerminal; @@ -270,6 +271,59 @@ impl Output { } } + /// Format an assurance level badge with a visual strength meter. + /// + /// With colors enabled: + /// `████ Sovereign` (green) + /// `███░ Authenticated` (cyan) + /// `██░░ Token-Verified`(yellow) + /// `█░░░ Self-Asserted` (dim) + /// + /// Without colors: `[4/4 Sovereign]`, `[3/4 Authenticated]`, etc. + pub fn assurance_badge(&self, level: AssuranceLevel) -> String { + let score = level.score(); + let label = level.label(); + + if !self.colors_enabled { + return format!("[{}/4 {}]", score, label); + } + + let filled = "\u{2588}".repeat(score as usize); + let empty = "\u{2591}".repeat(4 - score as usize); + let bar = format!("{}{}", filled, empty); + + match level { + AssuranceLevel::Sovereign => { + format!( + "{} {}", + self.success_style.apply_to(&bar), + self.success_style.apply_to(label) + ) + } + AssuranceLevel::Authenticated => { + format!( + "{} {}", + self.info_style.apply_to(&bar), + self.info_style.apply_to(label) + ) + } + AssuranceLevel::TokenVerified => { + format!( + "{} {}", + self.warn_style.apply_to(&bar), + self.warn_style.apply_to(label) + ) + } + AssuranceLevel::SelfAsserted | _ => { + format!( + "{} {}", + self.dim_style.apply_to(&bar), + self.dim_style.apply_to(label) + ) + } + } + } + /// Format a status indicator. pub fn status(&self, passed: bool) -> &'static str { if passed { @@ -339,4 +393,25 @@ mod tests { let kv = output.key_value("name", "value"); assert_eq!(kv, "name: value"); } + + #[test] + fn test_assurance_badge_no_colors() { + let output = Output::new_without_colors(); + assert_eq!( + output.assurance_badge(AssuranceLevel::Sovereign), + "[4/4 Sovereign]" + ); + assert_eq!( + output.assurance_badge(AssuranceLevel::Authenticated), + "[3/4 Authenticated]" + ); + assert_eq!( + output.assurance_badge(AssuranceLevel::TokenVerified), + "[2/4 Token-Verified]" + ); + assert_eq!( + output.assurance_badge(AssuranceLevel::SelfAsserted), + "[1/4 Self-Asserted]" + ); + } } diff --git a/crates/auths-core/src/ports/platform.rs b/crates/auths-core/src/ports/platform.rs index deab441d..f0bd3dca 100644 --- a/crates/auths-core/src/ports/platform.rs +++ b/crates/auths-core/src/ports/platform.rs @@ -3,10 +3,36 @@ use std::future::Future; use std::time::Duration; +use auths_verifier::AssuranceLevel; use thiserror::Error; use crate::ports::network::NetworkError; +/// Derives the cryptographic assurance level for a platform identity claim. +/// +/// Args: +/// * `platform`: Platform identifier (e.g., `"github"`, `"npm"`, `"pypi"`, `"auths"`). +/// * `cross_verified`: Whether the claim has been strengthened by cross-platform verification +/// (e.g., PyPI namespace verified via GitHub repo ownership). +/// +/// Usage: +/// ```rust +/// # use auths_core::ports::platform::derive_assurance_level; +/// # use auths_verifier::AssuranceLevel; +/// assert_eq!(derive_assurance_level("github", false), AssuranceLevel::Authenticated); +/// assert_eq!(derive_assurance_level("pypi", true), AssuranceLevel::TokenVerified); +/// ``` +pub fn derive_assurance_level(platform: &str, cross_verified: bool) -> AssuranceLevel { + match platform { + "auths" => AssuranceLevel::Sovereign, + "github" => AssuranceLevel::Authenticated, + "npm" => AssuranceLevel::TokenVerified, + "pypi" if cross_verified => AssuranceLevel::TokenVerified, + "pypi" => AssuranceLevel::SelfAsserted, + _ => AssuranceLevel::SelfAsserted, + } +} + /// Errors from platform identity claim operations (OAuth, proof publishing, registry submission). /// /// Usage: diff --git a/crates/auths-core/tests/cases/assurance_level.rs b/crates/auths-core/tests/cases/assurance_level.rs new file mode 100644 index 00000000..711ec97b --- /dev/null +++ b/crates/auths-core/tests/cases/assurance_level.rs @@ -0,0 +1,50 @@ +use auths_core::ports::platform::derive_assurance_level; +use auths_verifier::AssuranceLevel; + +#[test] +fn auths_is_sovereign() { + assert_eq!( + derive_assurance_level("auths", false), + AssuranceLevel::Sovereign + ); +} + +#[test] +fn github_is_authenticated() { + assert_eq!( + derive_assurance_level("github", false), + AssuranceLevel::Authenticated + ); +} + +#[test] +fn npm_is_token_verified() { + assert_eq!( + derive_assurance_level("npm", false), + AssuranceLevel::TokenVerified + ); +} + +#[test] +fn pypi_is_self_asserted() { + assert_eq!( + derive_assurance_level("pypi", false), + AssuranceLevel::SelfAsserted + ); +} + +#[test] +fn pypi_cross_verified_upgrades_to_token_verified() { + assert_eq!( + derive_assurance_level("pypi", true), + AssuranceLevel::TokenVerified + ); +} + +#[test] +fn unknown_platform_defaults_to_self_asserted() { + assert_eq!( + derive_assurance_level("unknown_platform", false), + AssuranceLevel::SelfAsserted + ); +} diff --git a/crates/auths-core/tests/cases/mod.rs b/crates/auths-core/tests/cases/mod.rs index 75f6886c..09420035 100644 --- a/crates/auths-core/tests/cases/mod.rs +++ b/crates/auths-core/tests/cases/mod.rs @@ -1,3 +1,4 @@ +mod assurance_level; mod key_export; mod namespace; #[cfg(feature = "keychain-pkcs11")] diff --git a/crates/auths-policy/src/compile.rs b/crates/auths-policy/src/compile.rs index 5e5f465d..9a69f722 100644 --- a/crates/auths-policy/src/compile.rs +++ b/crates/auths-policy/src/compile.rs @@ -5,7 +5,7 @@ use crate::compiled::{CompiledExpr, CompiledPolicy}; use crate::expr::Expr; -use crate::types::{CanonicalCapability, CanonicalDid, ValidatedGlob}; +use crate::types::{AssuranceLevel, CanonicalCapability, CanonicalDid, ValidatedGlob}; /// Maximum length for attribute keys. const MAX_ATTR_KEY_LEN: usize = 64; @@ -450,6 +450,34 @@ fn compile_inner( CompiledExpr::MaxChainDepth(*d) } + Expr::MinAssurance(s) => match s.parse::() { + Ok(level) => CompiledExpr::MinAssurance(level), + Err(_) => { + errors.push(CompileError { + path: path.into(), + message: format!( + "invalid assurance level '{}': expected one of: sovereign, authenticated, token_verified, self_asserted", + s + ), + }); + CompiledExpr::False + } + }, + + Expr::AssuranceLevelIs(s) => match s.parse::() { + Ok(level) => CompiledExpr::AssuranceLevelIs(level), + Err(_) => { + errors.push(CompileError { + path: path.into(), + message: format!( + "invalid assurance level '{}': expected one of: sovereign, authenticated, token_verified, self_asserted", + s + ), + }); + CompiledExpr::False + } + }, + Expr::ApprovalGate { inner, approvers, diff --git a/crates/auths-policy/src/compiled.rs b/crates/auths-policy/src/compiled.rs index 9a6b86e8..6336a8d2 100644 --- a/crates/auths-policy/src/compiled.rs +++ b/crates/auths-policy/src/compiled.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; -use crate::types::{CanonicalCapability, CanonicalDid, ValidatedGlob}; +use crate::types::{AssuranceLevel, CanonicalCapability, CanonicalDid, ValidatedGlob}; /// Compiled policy expression — validated, canonical, ready to evaluate. /// @@ -103,6 +103,11 @@ pub enum CompiledExpr { values: Vec, }, + /// Assurance level must be at least this level (uses `Ord` comparison). + MinAssurance(AssuranceLevel), + /// Assurance level must match exactly. + AssuranceLevelIs(AssuranceLevel), + /// Approval gate: if inner evaluates to Allow, return RequiresApproval. ApprovalGate { /// The compiled inner expression. @@ -226,6 +231,12 @@ fn describe_expr(expr: &CompiledExpr, depth: usize) -> String { CompiledExpr::AttrIn { key, values } => { format!("{indent}attr {key} in: [{}]", values.join(", ")) } + CompiledExpr::MinAssurance(level) => { + format!("{indent}min assurance: {}", level.label()) + } + CompiledExpr::AssuranceLevelIs(level) => { + format!("{indent}assurance must be: {}", level.label()) + } CompiledExpr::ApprovalGate { approvers, ttl_seconds, diff --git a/crates/auths-policy/src/context.rs b/crates/auths-policy/src/context.rs index 16e82e35..7d2fe646 100644 --- a/crates/auths-policy/src/context.rs +++ b/crates/auths-policy/src/context.rs @@ -9,7 +9,7 @@ use chrono::{DateTime, Utc}; use serde_json::Value; use crate::approval::ApprovalAttestation; -use crate::types::{CanonicalCapability, CanonicalDid, SignerType}; +use crate::types::{AssuranceLevel, CanonicalCapability, CanonicalDid, SignerType}; /// Typed evaluation context. /// @@ -31,6 +31,10 @@ pub struct EvalContext { /// The type of entity that produced this signature (human, agent, workload). pub signer_type: Option, + // ── Assurance Level ───────────────────────────────────────────── + /// Cryptographic assurance level of the platform identity claim. + pub assurance_level: Option, + // ── Attestation Identity ───────────────────────────────────────── /// The DID of the attestation issuer. pub issuer: CanonicalDid, @@ -93,6 +97,7 @@ impl EvalContext { Self { now, signer_type: None, + assurance_level: None, issuer, subject, revoked: false, @@ -137,6 +142,12 @@ impl EvalContext { self } + /// Set the assurance level. + pub fn assurance_level(mut self, level: AssuranceLevel) -> Self { + self.assurance_level = Some(level); + self + } + /// Set whether the attestation is revoked. pub fn revoked(mut self, revoked: bool) -> Self { self.revoked = revoked; diff --git a/crates/auths-policy/src/decision.rs b/crates/auths-policy/src/decision.rs index 6369343a..fb7dbf83 100644 --- a/crates/auths-policy/src/decision.rs +++ b/crates/auths-policy/src/decision.rs @@ -103,6 +103,10 @@ pub enum ReasonCode { ApprovalAlreadyUsed, /// Approval scope hash doesn't match the current request. ApprovalRequestMismatch, + /// Assurance level meets or exceeds the minimum requirement. + AssuranceMet, + /// Assurance level is below the minimum requirement. + AssuranceInsufficient, } impl Decision { diff --git a/crates/auths-policy/src/eval.rs b/crates/auths-policy/src/eval.rs index a2d5ddf9..a88ab019 100644 --- a/crates/auths-policy/src/eval.rs +++ b/crates/auths-policy/src/eval.rs @@ -487,6 +487,35 @@ fn eval_expr(expr: &CompiledExpr, ctx: &EvalContext) -> Decision { ), }, + // ── Assurance Level ────────────────────────────────────────── + CompiledExpr::MinAssurance(min) => match &ctx.assurance_level { + Some(level) if level >= min => Decision::allow( + ReasonCode::AssuranceMet, + format!("assurance {} >= {}", level, min), + ), + Some(level) => Decision::deny( + ReasonCode::AssuranceInsufficient, + format!("assurance {} < {}", level, min), + ), + None => { + Decision::indeterminate(ReasonCode::MissingField, "no assurance level in context") + } + }, + + CompiledExpr::AssuranceLevelIs(expected) => match &ctx.assurance_level { + Some(level) if level == expected => Decision::allow( + ReasonCode::AssuranceMet, + format!("assurance is {}", expected), + ), + Some(level) => Decision::deny( + ReasonCode::AssuranceInsufficient, + format!("assurance {} != {}", level, expected), + ), + None => { + Decision::indeterminate(ReasonCode::MissingField, "no assurance level in context") + } + }, + // ── Approval Gate ─────────────────────────────────────────── CompiledExpr::ApprovalGate { inner, @@ -861,4 +890,76 @@ mod tests { .capability(cap("sign_commit")); assert!(evaluate3(&policy, &ctx).is_denied()); } + + // ── Assurance Level tests ────────────────────────────────────── + + #[test] + fn eval_min_assurance_sovereign_allows_sovereign() { + let policy = compile(&Expr::MinAssurance("sovereign".into())).unwrap(); + let ctx = base_ctx().assurance_level(crate::types::AssuranceLevel::Sovereign); + assert!(evaluate3(&policy, &ctx).is_allowed()); + } + + #[test] + fn eval_min_assurance_authenticated_allows_sovereign() { + let policy = compile(&Expr::MinAssurance("authenticated".into())).unwrap(); + let ctx = base_ctx().assurance_level(crate::types::AssuranceLevel::Sovereign); + assert!(evaluate3(&policy, &ctx).is_allowed()); + } + + #[test] + fn eval_min_assurance_authenticated_allows_authenticated() { + let policy = compile(&Expr::MinAssurance("authenticated".into())).unwrap(); + let ctx = base_ctx().assurance_level(crate::types::AssuranceLevel::Authenticated); + assert!(evaluate3(&policy, &ctx).is_allowed()); + } + + #[test] + fn eval_min_assurance_authenticated_denies_token_verified() { + let policy = compile(&Expr::MinAssurance("authenticated".into())).unwrap(); + let ctx = base_ctx().assurance_level(crate::types::AssuranceLevel::TokenVerified); + assert!(evaluate3(&policy, &ctx).is_denied()); + } + + #[test] + fn eval_min_assurance_authenticated_denies_self_asserted() { + let policy = compile(&Expr::MinAssurance("authenticated".into())).unwrap(); + let ctx = base_ctx().assurance_level(crate::types::AssuranceLevel::SelfAsserted); + assert!(evaluate3(&policy, &ctx).is_denied()); + } + + #[test] + fn eval_min_assurance_missing_is_indeterminate() { + let policy = compile(&Expr::MinAssurance("authenticated".into())).unwrap(); + let ctx = base_ctx(); + assert!(evaluate3(&policy, &ctx).is_indeterminate()); + } + + #[test] + fn eval_min_assurance_strict_mode_denies_missing() { + let policy = compile(&Expr::MinAssurance("authenticated".into())).unwrap(); + let ctx = base_ctx(); + assert!(evaluate_strict(&policy, &ctx).is_denied()); + } + + #[test] + fn eval_assurance_level_is_exact_match() { + let policy = compile(&Expr::AssuranceLevelIs("token_verified".into())).unwrap(); + let ctx = base_ctx().assurance_level(crate::types::AssuranceLevel::TokenVerified); + assert!(evaluate3(&policy, &ctx).is_allowed()); + } + + #[test] + fn eval_assurance_level_is_mismatch() { + let policy = compile(&Expr::AssuranceLevelIs("sovereign".into())).unwrap(); + let ctx = base_ctx().assurance_level(crate::types::AssuranceLevel::Authenticated); + assert!(evaluate3(&policy, &ctx).is_denied()); + } + + #[test] + fn eval_assurance_level_is_missing_is_indeterminate() { + let policy = compile(&Expr::AssuranceLevelIs("sovereign".into())).unwrap(); + let ctx = base_ctx(); + assert!(evaluate3(&policy, &ctx).is_indeterminate()); + } } diff --git a/crates/auths-policy/src/expr.rs b/crates/auths-policy/src/expr.rs index eb709cdf..5fa909da 100644 --- a/crates/auths-policy/src/expr.rs +++ b/crates/auths-policy/src/expr.rs @@ -121,6 +121,12 @@ pub enum Expr { values: Vec, }, + // ── Assurance Level ──────────────────────────────────────────── + /// Assurance level must be at least this level (uses `Ord` comparison). + MinAssurance(String), + /// Assurance level must match exactly. + AssuranceLevelIs(String), + // ── Approval Gate ───────────────────────────────────────────── /// Approval gate: if inner evaluates to Allow, return RequiresApproval instead. /// Transparent to Deny/Indeterminate — those pass through unchanged. @@ -333,6 +339,8 @@ mod tests { key: "k".into(), values: vec!["v1".into(), "v2".into()], }, + Expr::MinAssurance("authenticated".into()), + Expr::AssuranceLevelIs("sovereign".into()), Expr::ApprovalGate { inner: Box::new(Expr::HasCapability("deploy".into())), approvers: vec!["did:keri:EHuman123".into()], diff --git a/crates/auths-policy/src/lib.rs b/crates/auths-policy/src/lib.rs index f35783dd..38611481 100644 --- a/crates/auths-policy/src/lib.rs +++ b/crates/auths-policy/src/lib.rs @@ -60,6 +60,6 @@ pub use expr::Expr; pub use glob::glob_match; pub use trust::{TrustRegistry, TrustRegistryEntry, ValidatedIssuerUrl}; pub use types::{ - CanonicalCapability, CanonicalDid, CapabilityParseError, DidParseError, GlobParseError, - QuorumPolicy, SignerType, ValidatedGlob, + AssuranceLevel, AssuranceLevelParseError, CanonicalCapability, CanonicalDid, + CapabilityParseError, DidParseError, GlobParseError, QuorumPolicy, SignerType, ValidatedGlob, }; diff --git a/crates/auths-policy/src/types.rs b/crates/auths-policy/src/types.rs index 52e20314..f24bd086 100644 --- a/crates/auths-policy/src/types.rs +++ b/crates/auths-policy/src/types.rs @@ -7,8 +7,8 @@ use std::fmt; use serde::{Deserialize, Serialize}; -// CanonicalDid lives in auths-verifier (Layer 1) so all DID types are co-located. -pub use auths_verifier::types::CanonicalDid; +// CanonicalDid and AssuranceLevel live in auths-verifier (Layer 1) so all shared types are co-located. +pub use auths_verifier::types::{AssuranceLevel, AssuranceLevelParseError, CanonicalDid}; /// Re-export DidParseError from auths-verifier for backwards compatibility. pub type DidParseError = auths_verifier::DidParseError; diff --git a/crates/auths-verifier/src/lib.rs b/crates/auths-verifier/src/lib.rs index 33c8c325..4874756c 100644 --- a/crates/auths-verifier/src/lib.rs +++ b/crates/auths-verifier/src/lib.rs @@ -69,8 +69,9 @@ pub mod witness; // Re-export verification types for convenience pub use types::{ - CanonicalDid, ChainLink, DeviceDID, DidConversionError, DidParseError, IdentityDID, - VerificationReport, VerificationStatus, signer_hex_to_did, validate_did, + AssuranceLevel, AssuranceLevelParseError, CanonicalDid, ChainLink, DeviceDID, + DidConversionError, DidParseError, IdentityDID, VerificationReport, VerificationStatus, + signer_hex_to_did, validate_did, }; // Re-export action envelope diff --git a/crates/auths-verifier/src/types.rs b/crates/auths-verifier/src/types.rs index a7b62a6e..742d7ab5 100644 --- a/crates/auths-verifier/src/types.rs +++ b/crates/auths-verifier/src/types.rs @@ -662,6 +662,84 @@ impl From for CanonicalDid { } } +// ============================================================================ +// AssuranceLevel Type +// ============================================================================ + +/// Cryptographic assurance level of a platform identity claim. +/// +/// Variants are ordered from weakest to strongest so that `Ord` comparisons +/// reflect trust strength: `SelfAsserted < TokenVerified < Authenticated < Sovereign`. +/// +/// Usage: +/// ```rust +/// # use auths_verifier::types::AssuranceLevel; +/// assert!(AssuranceLevel::Sovereign > AssuranceLevel::Authenticated); +/// assert!(AssuranceLevel::SelfAsserted < AssuranceLevel::TokenVerified); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum AssuranceLevel { + /// Self-reported identity, signed only by the claimant's own key (e.g., PyPI). + SelfAsserted, + /// Bearer token validated against a platform API at time of claim (e.g., npm). + TokenVerified, + /// OAuth/OIDC challenge-response proving account control (e.g., GitHub). + Authenticated, + /// End-to-end cryptographic identity chain with no third-party trust (auths native). + Sovereign, +} + +/// Error returned when parsing an `AssuranceLevel` from a string fails. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error( + "invalid assurance level '{0}': expected one of: sovereign, authenticated, token_verified, self_asserted" +)] +pub struct AssuranceLevelParseError(pub String); + +impl AssuranceLevel { + /// Human-readable label for display. + pub fn label(&self) -> &'static str { + match self { + Self::SelfAsserted => "Self-Asserted", + Self::TokenVerified => "Token-Verified", + Self::Authenticated => "Authenticated", + Self::Sovereign => "Sovereign", + } + } + + /// Numeric score (1–4) for the assurance level. + pub fn score(&self) -> u8 { + match self { + Self::SelfAsserted => 1, + Self::TokenVerified => 2, + Self::Authenticated => 3, + Self::Sovereign => 4, + } + } +} + +impl fmt::Display for AssuranceLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.label()) + } +} + +impl FromStr for AssuranceLevel { + type Err = AssuranceLevelParseError; + + fn from_str(s: &str) -> Result { + match s.trim().to_lowercase().as_str() { + "sovereign" => Ok(Self::Sovereign), + "authenticated" => Ok(Self::Authenticated), + "token_verified" => Ok(Self::TokenVerified), + "self_asserted" => Ok(Self::SelfAsserted), + _ => Err(AssuranceLevelParseError(s.to_string())), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -737,4 +815,97 @@ mod tests { // witness_quorum should be omitted from JSON when None assert!(!json.contains("witness_quorum")); } + + // ── AssuranceLevel Tests ────────────────────────────────────────── + + #[test] + fn assurance_level_ordering() { + assert!(AssuranceLevel::SelfAsserted < AssuranceLevel::TokenVerified); + assert!(AssuranceLevel::TokenVerified < AssuranceLevel::Authenticated); + assert!(AssuranceLevel::Authenticated < AssuranceLevel::Sovereign); + } + + #[test] + fn assurance_level_serde_roundtrip() { + let variants = [ + AssuranceLevel::SelfAsserted, + AssuranceLevel::TokenVerified, + AssuranceLevel::Authenticated, + AssuranceLevel::Sovereign, + ]; + for level in variants { + let json = serde_json::to_string(&level).unwrap(); + let parsed: AssuranceLevel = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed, level); + } + } + + #[test] + fn assurance_level_serde_snake_case() { + assert_eq!( + serde_json::to_string(&AssuranceLevel::SelfAsserted).unwrap(), + "\"self_asserted\"" + ); + assert_eq!( + serde_json::to_string(&AssuranceLevel::TokenVerified).unwrap(), + "\"token_verified\"" + ); + assert_eq!( + serde_json::to_string(&AssuranceLevel::Authenticated).unwrap(), + "\"authenticated\"" + ); + assert_eq!( + serde_json::to_string(&AssuranceLevel::Sovereign).unwrap(), + "\"sovereign\"" + ); + } + + #[test] + fn assurance_level_from_str() { + assert_eq!( + "sovereign".parse::().unwrap(), + AssuranceLevel::Sovereign + ); + assert_eq!( + "authenticated".parse::().unwrap(), + AssuranceLevel::Authenticated + ); + assert_eq!( + "token_verified".parse::().unwrap(), + AssuranceLevel::TokenVerified + ); + assert_eq!( + "self_asserted".parse::().unwrap(), + AssuranceLevel::SelfAsserted + ); + assert!("invalid".parse::().is_err()); + } + + #[test] + fn assurance_level_from_str_case_insensitive() { + assert_eq!( + "SOVEREIGN".parse::().unwrap(), + AssuranceLevel::Sovereign + ); + assert_eq!( + "Authenticated".parse::().unwrap(), + AssuranceLevel::Authenticated + ); + } + + #[test] + fn assurance_level_score() { + assert_eq!(AssuranceLevel::SelfAsserted.score(), 1); + assert_eq!(AssuranceLevel::TokenVerified.score(), 2); + assert_eq!(AssuranceLevel::Authenticated.score(), 3); + assert_eq!(AssuranceLevel::Sovereign.score(), 4); + } + + #[test] + fn assurance_level_display() { + assert_eq!(AssuranceLevel::SelfAsserted.to_string(), "Self-Asserted"); + assert_eq!(AssuranceLevel::TokenVerified.to_string(), "Token-Verified"); + assert_eq!(AssuranceLevel::Authenticated.to_string(), "Authenticated"); + assert_eq!(AssuranceLevel::Sovereign.to_string(), "Sovereign"); + } }