Skip to content

Commit 68cf259

Browse files
authored
Merge pull request #114 from auths-dev/dev-securityDisplay
feat: add AssuranceLevel to show how trustworthy each platform claim is
2 parents 93a5a89 + 27a04b7 commit 68cf259

14 files changed

Lines changed: 496 additions & 9 deletions

File tree

crates/auths-cli/src/ux/format.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
88
#![allow(dead_code)] // Some functions are for future use
99

10+
use auths_verifier::AssuranceLevel;
1011
use console::{Style, Term};
1112
use serde::Serialize;
1213
use std::io::IsTerminal;
@@ -270,6 +271,59 @@ impl Output {
270271
}
271272
}
272273

274+
/// Format an assurance level badge with a visual strength meter.
275+
///
276+
/// With colors enabled:
277+
/// `████ Sovereign` (green)
278+
/// `███░ Authenticated` (cyan)
279+
/// `██░░ Token-Verified`(yellow)
280+
/// `█░░░ Self-Asserted` (dim)
281+
///
282+
/// Without colors: `[4/4 Sovereign]`, `[3/4 Authenticated]`, etc.
283+
pub fn assurance_badge(&self, level: AssuranceLevel) -> String {
284+
let score = level.score();
285+
let label = level.label();
286+
287+
if !self.colors_enabled {
288+
return format!("[{}/4 {}]", score, label);
289+
}
290+
291+
let filled = "\u{2588}".repeat(score as usize);
292+
let empty = "\u{2591}".repeat(4 - score as usize);
293+
let bar = format!("{}{}", filled, empty);
294+
295+
match level {
296+
AssuranceLevel::Sovereign => {
297+
format!(
298+
"{} {}",
299+
self.success_style.apply_to(&bar),
300+
self.success_style.apply_to(label)
301+
)
302+
}
303+
AssuranceLevel::Authenticated => {
304+
format!(
305+
"{} {}",
306+
self.info_style.apply_to(&bar),
307+
self.info_style.apply_to(label)
308+
)
309+
}
310+
AssuranceLevel::TokenVerified => {
311+
format!(
312+
"{} {}",
313+
self.warn_style.apply_to(&bar),
314+
self.warn_style.apply_to(label)
315+
)
316+
}
317+
AssuranceLevel::SelfAsserted | _ => {
318+
format!(
319+
"{} {}",
320+
self.dim_style.apply_to(&bar),
321+
self.dim_style.apply_to(label)
322+
)
323+
}
324+
}
325+
}
326+
273327
/// Format a status indicator.
274328
pub fn status(&self, passed: bool) -> &'static str {
275329
if passed {
@@ -339,4 +393,25 @@ mod tests {
339393
let kv = output.key_value("name", "value");
340394
assert_eq!(kv, "name: value");
341395
}
396+
397+
#[test]
398+
fn test_assurance_badge_no_colors() {
399+
let output = Output::new_without_colors();
400+
assert_eq!(
401+
output.assurance_badge(AssuranceLevel::Sovereign),
402+
"[4/4 Sovereign]"
403+
);
404+
assert_eq!(
405+
output.assurance_badge(AssuranceLevel::Authenticated),
406+
"[3/4 Authenticated]"
407+
);
408+
assert_eq!(
409+
output.assurance_badge(AssuranceLevel::TokenVerified),
410+
"[2/4 Token-Verified]"
411+
);
412+
assert_eq!(
413+
output.assurance_badge(AssuranceLevel::SelfAsserted),
414+
"[1/4 Self-Asserted]"
415+
);
416+
}
342417
}

crates/auths-core/src/ports/platform.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,36 @@
33
use std::future::Future;
44
use std::time::Duration;
55

6+
use auths_verifier::AssuranceLevel;
67
use thiserror::Error;
78

89
use crate::ports::network::NetworkError;
910

11+
/// Derives the cryptographic assurance level for a platform identity claim.
12+
///
13+
/// Args:
14+
/// * `platform`: Platform identifier (e.g., `"github"`, `"npm"`, `"pypi"`, `"auths"`).
15+
/// * `cross_verified`: Whether the claim has been strengthened by cross-platform verification
16+
/// (e.g., PyPI namespace verified via GitHub repo ownership).
17+
///
18+
/// Usage:
19+
/// ```rust
20+
/// # use auths_core::ports::platform::derive_assurance_level;
21+
/// # use auths_verifier::AssuranceLevel;
22+
/// assert_eq!(derive_assurance_level("github", false), AssuranceLevel::Authenticated);
23+
/// assert_eq!(derive_assurance_level("pypi", true), AssuranceLevel::TokenVerified);
24+
/// ```
25+
pub fn derive_assurance_level(platform: &str, cross_verified: bool) -> AssuranceLevel {
26+
match platform {
27+
"auths" => AssuranceLevel::Sovereign,
28+
"github" => AssuranceLevel::Authenticated,
29+
"npm" => AssuranceLevel::TokenVerified,
30+
"pypi" if cross_verified => AssuranceLevel::TokenVerified,
31+
"pypi" => AssuranceLevel::SelfAsserted,
32+
_ => AssuranceLevel::SelfAsserted,
33+
}
34+
}
35+
1036
/// Errors from platform identity claim operations (OAuth, proof publishing, registry submission).
1137
///
1238
/// Usage:
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use auths_core::ports::platform::derive_assurance_level;
2+
use auths_verifier::AssuranceLevel;
3+
4+
#[test]
5+
fn auths_is_sovereign() {
6+
assert_eq!(
7+
derive_assurance_level("auths", false),
8+
AssuranceLevel::Sovereign
9+
);
10+
}
11+
12+
#[test]
13+
fn github_is_authenticated() {
14+
assert_eq!(
15+
derive_assurance_level("github", false),
16+
AssuranceLevel::Authenticated
17+
);
18+
}
19+
20+
#[test]
21+
fn npm_is_token_verified() {
22+
assert_eq!(
23+
derive_assurance_level("npm", false),
24+
AssuranceLevel::TokenVerified
25+
);
26+
}
27+
28+
#[test]
29+
fn pypi_is_self_asserted() {
30+
assert_eq!(
31+
derive_assurance_level("pypi", false),
32+
AssuranceLevel::SelfAsserted
33+
);
34+
}
35+
36+
#[test]
37+
fn pypi_cross_verified_upgrades_to_token_verified() {
38+
assert_eq!(
39+
derive_assurance_level("pypi", true),
40+
AssuranceLevel::TokenVerified
41+
);
42+
}
43+
44+
#[test]
45+
fn unknown_platform_defaults_to_self_asserted() {
46+
assert_eq!(
47+
derive_assurance_level("unknown_platform", false),
48+
AssuranceLevel::SelfAsserted
49+
);
50+
}

crates/auths-core/tests/cases/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod assurance_level;
12
mod key_export;
23
mod namespace;
34
#[cfg(feature = "keychain-pkcs11")]

crates/auths-policy/src/compile.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
use crate::compiled::{CompiledExpr, CompiledPolicy};
77
use crate::expr::Expr;
8-
use crate::types::{CanonicalCapability, CanonicalDid, ValidatedGlob};
8+
use crate::types::{AssuranceLevel, CanonicalCapability, CanonicalDid, ValidatedGlob};
99

1010
/// Maximum length for attribute keys.
1111
const MAX_ATTR_KEY_LEN: usize = 64;
@@ -450,6 +450,34 @@ fn compile_inner(
450450
CompiledExpr::MaxChainDepth(*d)
451451
}
452452

453+
Expr::MinAssurance(s) => match s.parse::<AssuranceLevel>() {
454+
Ok(level) => CompiledExpr::MinAssurance(level),
455+
Err(_) => {
456+
errors.push(CompileError {
457+
path: path.into(),
458+
message: format!(
459+
"invalid assurance level '{}': expected one of: sovereign, authenticated, token_verified, self_asserted",
460+
s
461+
),
462+
});
463+
CompiledExpr::False
464+
}
465+
},
466+
467+
Expr::AssuranceLevelIs(s) => match s.parse::<AssuranceLevel>() {
468+
Ok(level) => CompiledExpr::AssuranceLevelIs(level),
469+
Err(_) => {
470+
errors.push(CompileError {
471+
path: path.into(),
472+
message: format!(
473+
"invalid assurance level '{}': expected one of: sovereign, authenticated, token_verified, self_asserted",
474+
s
475+
),
476+
});
477+
CompiledExpr::False
478+
}
479+
},
480+
453481
Expr::ApprovalGate {
454482
inner,
455483
approvers,

crates/auths-policy/src/compiled.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
use serde::{Deserialize, Serialize};
77

8-
use crate::types::{CanonicalCapability, CanonicalDid, ValidatedGlob};
8+
use crate::types::{AssuranceLevel, CanonicalCapability, CanonicalDid, ValidatedGlob};
99

1010
/// Compiled policy expression — validated, canonical, ready to evaluate.
1111
///
@@ -103,6 +103,11 @@ pub enum CompiledExpr {
103103
values: Vec<String>,
104104
},
105105

106+
/// Assurance level must be at least this level (uses `Ord` comparison).
107+
MinAssurance(AssuranceLevel),
108+
/// Assurance level must match exactly.
109+
AssuranceLevelIs(AssuranceLevel),
110+
106111
/// Approval gate: if inner evaluates to Allow, return RequiresApproval.
107112
ApprovalGate {
108113
/// The compiled inner expression.
@@ -226,6 +231,12 @@ fn describe_expr(expr: &CompiledExpr, depth: usize) -> String {
226231
CompiledExpr::AttrIn { key, values } => {
227232
format!("{indent}attr {key} in: [{}]", values.join(", "))
228233
}
234+
CompiledExpr::MinAssurance(level) => {
235+
format!("{indent}min assurance: {}", level.label())
236+
}
237+
CompiledExpr::AssuranceLevelIs(level) => {
238+
format!("{indent}assurance must be: {}", level.label())
239+
}
229240
CompiledExpr::ApprovalGate {
230241
approvers,
231242
ttl_seconds,

crates/auths-policy/src/context.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use chrono::{DateTime, Utc};
99
use serde_json::Value;
1010

1111
use crate::approval::ApprovalAttestation;
12-
use crate::types::{CanonicalCapability, CanonicalDid, SignerType};
12+
use crate::types::{AssuranceLevel, CanonicalCapability, CanonicalDid, SignerType};
1313

1414
/// Typed evaluation context.
1515
///
@@ -31,6 +31,10 @@ pub struct EvalContext {
3131
/// The type of entity that produced this signature (human, agent, workload).
3232
pub signer_type: Option<SignerType>,
3333

34+
// ── Assurance Level ─────────────────────────────────────────────
35+
/// Cryptographic assurance level of the platform identity claim.
36+
pub assurance_level: Option<AssuranceLevel>,
37+
3438
// ── Attestation Identity ─────────────────────────────────────────
3539
/// The DID of the attestation issuer.
3640
pub issuer: CanonicalDid,
@@ -93,6 +97,7 @@ impl EvalContext {
9397
Self {
9498
now,
9599
signer_type: None,
100+
assurance_level: None,
96101
issuer,
97102
subject,
98103
revoked: false,
@@ -137,6 +142,12 @@ impl EvalContext {
137142
self
138143
}
139144

145+
/// Set the assurance level.
146+
pub fn assurance_level(mut self, level: AssuranceLevel) -> Self {
147+
self.assurance_level = Some(level);
148+
self
149+
}
150+
140151
/// Set whether the attestation is revoked.
141152
pub fn revoked(mut self, revoked: bool) -> Self {
142153
self.revoked = revoked;

crates/auths-policy/src/decision.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ pub enum ReasonCode {
103103
ApprovalAlreadyUsed,
104104
/// Approval scope hash doesn't match the current request.
105105
ApprovalRequestMismatch,
106+
/// Assurance level meets or exceeds the minimum requirement.
107+
AssuranceMet,
108+
/// Assurance level is below the minimum requirement.
109+
AssuranceInsufficient,
106110
}
107111

108112
impl Decision {

0 commit comments

Comments
 (0)