33Node map (11 nodes):
44 resolve -> validate_schema
55 validate_schema -> check_revocation
6- check_revocation -> verify_signature (or failed)
7- verify_signature -> validate_vc (or failed)
6+ check_revocation -> verify_identity (or failed)
7+ verify_identity -> validate_vc (or failed)
88 validate_vc -> validate_delegation (or failed)
99 validate_delegation -> cross_ref_capabilities (or failed)
1010 cross_ref_capabilities -> check_reputation (or failed)
11- check_reputation -> semantic_challenge | issue_verdict (fast-path / blacklist)
11+ check_reputation -> semantic_challenge | issue_verdict (fast-path / blacklist / disabled )
1212 semantic_challenge -> issue_verdict
1313 issue_verdict -> seal_session
1414 seal_session -> END
15+
16+ Identity verification supports dual-mode auth: Ed25519 signatures (default) and
17+ OAuth bearer tokens (when airlock.oauth module is available).
1518"""
1619
1720from __future__ import annotations
2326
2427from langgraph .graph import END , StateGraph
2528
29+ from airlock .config import get_config
2630from airlock .crypto .keys import KeyPair , resolve_public_key
2731from airlock .crypto .signing import sign_attestation , verify_model
28- from airlock .config import get_config
2932from airlock .crypto .vc import extract_capabilities , validate_credential
3033from airlock .engine .state import SessionManager
3134from airlock .gateway .revocation import RedisRevocationStore , RevocationStore
@@ -84,6 +87,7 @@ class OrchestrationState(TypedDict, total=False):
8487 _challenge_outcome : str | None
8588 _tier : int # TrustTier int value
8689 _local_only : bool
90+ _bearer_token : str | None # OAuth bearer token (if present)
8791
8892
8993# ---------------------------------------------------------------------------
@@ -306,6 +310,7 @@ async def _handle_handshake(self, event: HandshakeReceived) -> None:
306310 "_challenge_outcome" : None ,
307311 "_tier" : TrustTier .UNKNOWN ,
308312 "_local_only" : getattr (request , "privacy_mode" , "any" ) == "local_only" ,
313+ "_bearer_token" : getattr (event , "bearer_token" , None ),
309314 }
310315
311316 # Run the graph synchronously through all nodes.
@@ -620,34 +625,66 @@ def _node_check_revocation(self, state: OrchestrationState) -> OrchestrationStat
620625 def _route_after_revocation (self , state : OrchestrationState ) -> str :
621626 if state .get ("failed_at" ) == "check_revocation" :
622627 return "failed"
623- return "verify_signature "
628+ return "verify_identity "
624629
625- def _node_verify_signature (self , state : OrchestrationState ) -> OrchestrationState :
626- """Node 2: verify the Ed25519 signature on the HandshakeRequest."""
630+ def _node_verify_identity (self , state : OrchestrationState ) -> OrchestrationState :
631+ """Node 2: verify identity via OAuth bearer token or Ed25519 signature.
632+
633+ Dual-mode auth: tries OAuth first (if bearer token present and oauth module
634+ available), then falls back to Ed25519 signature verification.
635+ """
627636 checks : list [CheckResult ] = list (state .get ("check_results" , []))
628637 request = state ["handshake" ]
638+ auth_method = "ed25519"
629639
630- try :
631- verify_key = resolve_public_key (request .initiator .did )
632- valid = verify_model (request , verify_key )
633- except Exception as exc :
634- valid = False
635- logger .debug ("Signature verification error: %s" , exc )
640+ # --- Try OAuth bearer token first ---
641+ oauth_validated = False
642+ bearer_token = state .get ("_bearer_token" )
643+ if bearer_token is not None :
644+ try :
645+ from airlock .oauth .token_validator import validate_access_token
646+
647+ gateway_vk = self ._airlock_keypair .verify_key if self ._airlock_keypair else None
648+ if gateway_vk is not None :
649+ token_data = validate_access_token (bearer_token , gateway_vk )
650+ if token_data .get ("sub" ) == request .initiator .did :
651+ oauth_validated = True
652+ auth_method = "oauth"
653+ except ImportError :
654+ logger .debug ("OAuth module not available, falling back to Ed25519" )
655+ except Exception as exc :
656+ logger .debug ("OAuth token validation failed: %s" , exc )
657+
658+ # --- Fall back to Ed25519 signature verification ---
659+ if not oauth_validated :
660+ try :
661+ verify_key = resolve_public_key (request .initiator .did )
662+ valid = verify_model (request , verify_key )
663+ except Exception as exc :
664+ valid = False
665+ logger .debug ("Signature verification error: %s" , exc )
666+ else :
667+ valid = True
636668
669+ detail = (
670+ f"Identity verified via { auth_method } "
671+ if valid
672+ else "Identity verification failed"
673+ )
637674 checks .append (
638675 CheckResult (
639676 check = VerificationCheck .SIGNATURE ,
640677 passed = valid ,
641- detail = "Ed25519 signature valid" if valid else "Signature verification failed" ,
678+ detail = detail ,
642679 )
643680 )
644681 state ["check_results" ] = checks
645682 state ["_sig_valid" ] = valid
646683 if valid :
647684 state ["session" ].state = VerificationState .SIGNATURE_VERIFIED
648685 else :
649- state ["error" ] = "Invalid signature "
650- state ["failed_at" ] = "verify_signature "
686+ state ["error" ] = "Invalid identity "
687+ state ["failed_at" ] = "verify_identity "
651688 state ["verdict" ] = TrustVerdict .REJECTED
652689 state ["session" ].state = VerificationState .FAILED
653690 return state
@@ -1055,7 +1092,7 @@ def _node_failed(self, state: OrchestrationState) -> OrchestrationState:
10551092 # Conditional edge functions
10561093 # ------------------------------------------------------------------
10571094
1058- def _route_after_signature (self , state : OrchestrationState ) -> str :
1095+ def _route_after_identity (self , state : OrchestrationState ) -> str :
10591096 return "validate_vc" if state .get ("_sig_valid" ) else "failed"
10601097
10611098 def _route_after_vc (self , state : OrchestrationState ) -> str :
@@ -1068,6 +1105,9 @@ def _route_after_reputation(self, state: OrchestrationState) -> str:
10681105 elif routing in ("fast_path" , "issue_verdict" ):
10691106 return "issue_verdict"
10701107 else :
1108+ cfg = get_config ()
1109+ if cfg .challenge_fallback_mode == "disabled" :
1110+ return "issue_verdict"
10711111 return "semantic_challenge"
10721112
10731113 # ------------------------------------------------------------------
@@ -1079,7 +1119,7 @@ def _build_graph(self) -> Any:
10791119
10801120 graph .add_node ("validate_schema" , self ._node_validate_schema )
10811121 graph .add_node ("check_revocation" , self ._node_check_revocation )
1082- graph .add_node ("verify_signature " , self ._node_verify_signature )
1122+ graph .add_node ("verify_identity " , self ._node_verify_identity )
10831123 graph .add_node ("validate_vc" , self ._node_validate_vc )
10841124 graph .add_node ("validate_delegation" , self ._node_validate_delegation )
10851125 graph .add_node ("cross_ref_capabilities" , self ._node_cross_ref_capabilities )
@@ -1095,11 +1135,11 @@ def _build_graph(self) -> Any:
10951135 graph .add_conditional_edges (
10961136 "check_revocation" ,
10971137 self ._route_after_revocation ,
1098- {"verify_signature " : "verify_signature " , "failed" : "failed" },
1138+ {"verify_identity " : "verify_identity " , "failed" : "failed" },
10991139 )
11001140 graph .add_conditional_edges (
1101- "verify_signature " ,
1102- self ._route_after_signature ,
1141+ "verify_identity " ,
1142+ self ._route_after_identity ,
11031143 {"validate_vc" : "validate_vc" , "failed" : "failed" },
11041144 )
11051145 graph .add_conditional_edges (
0 commit comments