@@ -64,24 +64,38 @@ class VerifyMode(Enum):
6464
6565
6666class TrustLevel (Enum ):
67- """Trust level as defined in RFC-002."""
67+ """Trust level as defined in RFC-002 §5.
68+
69+ Levels:
70+ LEVEL_0 (SS): Self-Signed - did:key, iss == sub. Development only.
71+ LEVEL_1 (REG): Registered - Account registration with CA.
72+ LEVEL_2 (DV): Domain Validated - DNS/HTTP domain ownership proof.
73+ LEVEL_3 (OV): Organization Validated - Legal entity verification.
74+ LEVEL_4 (EV): Extended Validated - Manual review + security audit.
75+ """
76+
77+ LEVEL_0 = "0"
78+ """Self-Signed (SS) - did:key, iss == sub. Development only."""
6879
6980 LEVEL_1 = "1"
70- """Domain Validated (DV ) - Basic verification ."""
81+ """Registered (REG ) - Account registration with CA ."""
7182
7283 LEVEL_2 = "2"
73- """Organization Validated (OV ) - Business verification ."""
84+ """Domain Validated (DV ) - DNS/HTTP domain ownership proof ."""
7485
7586 LEVEL_3 = "3"
76- """Extended Validation (EV) - Rigorous vetting."""
87+ """Organization Validated (OV) - Legal entity verification."""
88+
89+ LEVEL_4 = "4"
90+ """Extended Validated (EV) - Manual review + security audit."""
7791
7892 @classmethod
7993 def from_string (cls , value : str ) -> "TrustLevel" :
8094 """Create TrustLevel from string value."""
8195 for level in cls :
8296 if level .value == value :
8397 return level
84- raise ValueError (f"Unknown trust level: { value } " )
98+ raise ValueError (f"Unknown trust level: { value } . Valid levels: 0 (SS), 1 (REG), 2 (DV), 3 (OV), 4 (EV) " )
8599
86100
87101@dataclass
@@ -90,15 +104,24 @@ class BadgeClaims:
90104
91105 Attributes:
92106 jti: Unique badge identifier (UUID).
93- issuer: Badge issuer URL (CA).
107+ issuer: Badge issuer URL (CA) or did:key for self-signed .
94108 subject: Agent DID (did:web format).
95109 audience: Optional list of intended audience URLs.
96110 issued_at: When the badge was issued.
97111 expires_at: When the badge expires.
98- trust_level: Trust level (1=DV, 2=OV, 3=EV).
112+ trust_level: Trust level per RFC-002 §5:
113+ - 0 (SS): Self-Signed - Development only
114+ - 1 (REG): Registered - Account registration
115+ - 2 (DV): Domain Validated - DNS/HTTP proof
116+ - 3 (OV): Organization Validated - Legal entity
117+ - 4 (EV): Extended Validated - Security audit
99118 domain: Agent's verified domain.
100119 agent_name: Human-readable agent name.
101120 agent_id: Extracted agent ID from subject DID.
121+ ial: Identity Assurance Level (RFC-002 §7.2.1):
122+ - "0": Account-attested (no key proof)
123+ - "1": Proof of Possession (key holder verified, has cnf claim)
124+ raw_claims: Original JWT claims dict for advanced access.
102125 """
103126
104127 jti : str
@@ -110,6 +133,8 @@ class BadgeClaims:
110133 domain : str
111134 agent_name : str = ""
112135 audience : List [str ] = field (default_factory = list )
136+ ial : str = "0" # RFC-002 §7.2.1: Default IAL-0 (account-attested)
137+ raw_claims : Optional [dict ] = field (default = None , repr = False ) # For advanced access
113138
114139 @property
115140 def agent_id (self ) -> str :
@@ -133,6 +158,11 @@ def is_not_yet_valid(self) -> bool:
133158 @classmethod
134159 def from_dict (cls , data : dict ) -> "BadgeClaims" :
135160 """Create BadgeClaims from a dictionary."""
161+ # Handle audience - can be string or list
162+ aud = data .get ("aud" , [])
163+ if isinstance (aud , str ):
164+ aud = [aud ] if aud else []
165+
136166 return cls (
137167 jti = data .get ("jti" , "" ),
138168 issuer = data .get ("iss" , "" ),
@@ -142,12 +172,14 @@ def from_dict(cls, data: dict) -> "BadgeClaims":
142172 trust_level = TrustLevel .from_string (data .get ("trust_level" , "1" )),
143173 domain = data .get ("domain" , "" ),
144174 agent_name = data .get ("agent_name" , "" ),
145- audience = data .get ("aud" , []),
175+ audience = aud ,
176+ ial = data .get ("ial" , "0" ), # RFC-002 §7.2.1
177+ raw_claims = data , # Preserve for advanced access (cnf, key, etc.)
146178 )
147179
148180 def to_dict (self ) -> dict :
149181 """Convert to dictionary."""
150- return {
182+ result = {
151183 "jti" : self .jti ,
152184 "iss" : self .issuer ,
153185 "sub" : self .subject ,
@@ -157,7 +189,31 @@ def to_dict(self) -> dict:
157189 "domain" : self .domain ,
158190 "agent_name" : self .agent_name ,
159191 "aud" : self .audience ,
192+ "ial" : self .ial ,
160193 }
194+ return result
195+
196+ @property
197+ def has_key_binding (self ) -> bool :
198+ """Check if this badge has IAL-1 key binding (cnf claim).
199+
200+ Per RFC-002 §7.2.1, IAL-1 badges include a 'cnf' (confirmation) claim
201+ that cryptographically binds the badge to the agent's private key.
202+ """
203+ if self .raw_claims is None :
204+ return self .ial == "1"
205+ return "cnf" in self .raw_claims
206+
207+ @property
208+ def confirmation_key (self ) -> Optional [dict ]:
209+ """Get the confirmation key (cnf claim) if present.
210+
211+ Returns the JWK thumbprint or key from the cnf claim for IAL-1 badges.
212+ Returns None for IAL-0 badges or if cnf is not present.
213+ """
214+ if self .raw_claims is None :
215+ return None
216+ return self .raw_claims .get ("cnf" )
161217
162218
163219@dataclass
@@ -454,7 +510,11 @@ async def request_badge(
454510 ca_url: Certificate Authority URL (default: CapiscIO registry).
455511 api_key: API key for authentication with the CA.
456512 domain: Agent's domain (required for verification).
457- trust_level: Requested trust level (1=DV, 2=OV, 3=EV).
513+ trust_level: Requested trust level per RFC-002 §5:
514+ - 1 (REG): Registered - Account registration
515+ - 2 (DV): Domain Validated - DNS/HTTP proof
516+ - 3 (OV): Organization Validated - Legal entity
517+ - 4 (EV): Extended Validated - Security audit
458518 audience: Optional audience restrictions for the badge.
459519 timeout: Request timeout in seconds (not used with gRPC).
460520
0 commit comments