Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions crates/auths-cli/src/ux/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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]"
);
}
}
26 changes: 26 additions & 0 deletions crates/auths-core/src/ports/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
50 changes: 50 additions & 0 deletions crates/auths-core/tests/cases/assurance_level.rs
Original file line number Diff line number Diff line change
@@ -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
);
}
1 change: 1 addition & 0 deletions crates/auths-core/tests/cases/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod assurance_level;
mod key_export;
mod namespace;
#[cfg(feature = "keychain-pkcs11")]
Expand Down
30 changes: 29 additions & 1 deletion crates/auths-policy/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -450,6 +450,34 @@ fn compile_inner(
CompiledExpr::MaxChainDepth(*d)
}

Expr::MinAssurance(s) => match s.parse::<AssuranceLevel>() {
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::<AssuranceLevel>() {
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,
Expand Down
13 changes: 12 additions & 1 deletion crates/auths-policy/src/compiled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -103,6 +103,11 @@ pub enum CompiledExpr {
values: Vec<String>,
},

/// 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.
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 12 additions & 1 deletion crates/auths-policy/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -31,6 +31,10 @@ pub struct EvalContext {
/// The type of entity that produced this signature (human, agent, workload).
pub signer_type: Option<SignerType>,

// ── Assurance Level ─────────────────────────────────────────────
/// Cryptographic assurance level of the platform identity claim.
pub assurance_level: Option<AssuranceLevel>,

// ── Attestation Identity ─────────────────────────────────────────
/// The DID of the attestation issuer.
pub issuer: CanonicalDid,
Expand Down Expand Up @@ -93,6 +97,7 @@ impl EvalContext {
Self {
now,
signer_type: None,
assurance_level: None,
issuer,
subject,
revoked: false,
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions crates/auths-policy/src/decision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading