Back to skill

Security audit

Agent Credential Wallets: Verifiable Intent & Delegation Chains

Security checks for vulnerabilities and agentic risk

Overview

This non-executable guide is not malicious, but its production-framed financial credential examples contain serious security and safety gaps users should review before using.

Treat this as a conceptual guide only. Do not copy the code into production commerce, identity, payment, or compliance workflows without replacing the credential, verification, endpoint, budget, and EUDI/SCA logic with audited standards-compliant implementations and explicit human approval for real transactions.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (5)

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:337
Finding

Unsigned credential payloads are presented as SD-JWT-VC credentials

Content
View full analysis
dict: """Build the issuer-signed JWT payload.""" payload = { "iss": self.issuer_did, "sub": self.subject_did, "iat": int(self.issued_at), "exp": int(self.expires_at), "vct": self.credential_type, "_sd": self.sd_digests(), "_sd_alg": "sha-256", } # Include non-disclosable claims directly for name, value in self.claims.items(): if name not in self.disclosable_claims: payload[name] = value if self.parent_hash: payload["parent_credential"] = self.parent_hash return payload def present(self, disclosed_claims: List[str]) -> dict: """Create a presentation with only the specified claims disclosed.""" selected = [d for d in self.disclosures if d.claim_name in disclosed_claims] return { "jwt_payload": self.jwt_payload(), "disclosures": [d.encode() for d in selected], "disclosed_values": { d.claim_name: d.claim_value for d in selected }, } def credential_hash(self) -> str: """SHA-256 hash of the credential payload for chain linking.""" payload_bytes = json.dumps(self.jwt_payload(), sort_keys=True).encode() return hashlib.sha256(payload_bytes).hexdigest() ``` ### Technical Analysis The implementation describes `jwt_payload()` as issuer-signed, but it only constructs and returns a Python dictionary. It never creates a compact JWT, applies a JWS signature, protects the signing algorithm, or associates the signature with a trusted issuer key. The presentation also lacks a holder key-binding JWT. Hashing the payload with SHA-256 does not authenticate it because an attacker who modifies the payload can simply recompute the hash. Disclosure digests protect neither the ...[truncated 1139 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:989
Finding

Protocol adapters accept counterparties without validating credential authenticity

Content
View full analysis
dict: """Verify an AP2 counterparty presentation. Checks: credential format, DID resolution, disclosure validation. """ if presentation.get("protocol") != "ap2": return {"valid": False, "reason": "Not an AP2 presentation"} cred = presentation.get("credential_presentation", {}) if cred.get("format") != "sd-jwt-vc": return {"valid": False, "reason": "Unsupported credential format"} # Verify the agent identity via GreenHelix agent_card = presentation.get("agent_card", {}) identity_check = api_call("get_agent_reputation", { "agent_id": agent_card.get("agent_id", ""), }) return { "valid": True, "agent_id": agent_card.get("agent_id"), "did": agent_card.get("did"), "reputation": identity_check, } ``` ```python class UCPAdapter(ProtocolAdapter): def verify_counterparty(self, presentation: dict) -> dict: if presentation.get("protocol") != "ucp": return {"valid": False, "reason": "Not a UCP presentation"} identity = presentation.get("identity", {}) commerce = presentation.get("commerce_capabilities", {}) # Verify identity and check commerce capabilities rep = api_call("get_agent_reputation", { "agent_id": identity.get("agent_id", ""), }) return { "valid": True, "agent_id": identity.get("agent_id"), "escrow_capable": commerce.get("escrow_capable", False), "reputation": rep, } ``` ```python class ACPAdapter(ProtocolAdapter): def verify_counterparty ...[truncated 3115 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:693
Finding

Payment budget enforcement accepts negative and non-finite monetary values

Content
View full analysis
dict: """Check if a transaction fits within the intent's remaining budget.""" if intent_id not in self.active_intents: return {"authorized": False, "remaining": 0, "reason": "Intent not found"} intent = self.active_intents[intent_id] if time.time() > intent["session_end"]: return {"authorized": False, "remaining": 0, "reason": "Session expired"} spent = self.session_spent.get(intent_id, 0.0) remaining = intent["spending_limit"] - spent if amount_usd > remaining: return { "authorized": False, "remaining": remaining, "reason": f"Amount ${amount_usd} exceeds remaining budget ${remaining}", } return {"authorized": True, "remaining": remaining - amount_usd} def execute_authorized_payment( self, intent_id: str, vendor_agent_id: str, amount_usd: float, description: str, ) -> dict: """Execute a payment action under a Verifiable Intent session.""" # Step 1: Check budget budget_check = self.check_intent_budget(intent_id, amount_usd) if not budget_check["authorized"]: return {"status": "denied", "reason": budget_check["reason"]} # Step 2: Create the payment intent via GreenHelix intent_result = api_call("create_intent", { "from_agent": self.wallet.agent_id, "to_agent": vendor_agent_id, "amount": str(amount_usd), "currency": "USD", "description": description, "metadata": { "vi_session": intent_id, "credential_hash": self.active_intents[intent_id][ "credential" ].credential_hash(), }, }) # Step 3: Reco ...[truncated 2266 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:1350
Finding

EUDI, QEAA, and SCA validation trusts unverified caller-controlled assertions

Content
View full analysis
dict: """Import a credential sourced from a EUDI Wallet.""" is_qualified = issuer_did in self.QUALIFIED_TRUST_SERVICES eudi_record = { "id": eudi_credential_id, "issuer": issuer_did, "is_qeaa": is_qualified, "credential_data": credential_data, "sca_method": sca_method, "sca_timestamp": sca_timestamp, "imported_at": time.time(), } self.eudi_credentials[eudi_credential_id] = eudi_record # Also issue a GreenHelix credential that references the EUDI source self.wallet.issue_credential( credential_id=f"eudi-{eudi_credential_id}", subject_did=self.wallet.did, credential_type="https://greenhelix.net/credentials/eudi-delegation/v1", claims={ "eudi_source": eudi_credential_id, "eudi_issuer": issuer_did, "is_qeaa": is_qualified, "sca_method": sca_method, "sca_timestamp": sca_timestamp, **credential_data, }, disclosable_claims=["sca_method", "eudi_issuer"], ttl_seconds=86400, ) return eudi_record ``` ```python def validate_sca_for_transaction( self, eudi_credential_id: str, amount_eur: float, max_sca_age_seconds: int = 300, ) -> dict: """Validate that SCA requirements are met for a transaction.""" if not self.check_sca_required(amount_eur): return {"sca_required": False, "authorized": True} cred = self.eudi_credentials.get(eud ...[truncated 4014 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:291
Finding

Environment-controlled API endpoint can receive production bearer credentials and sensitive records

Content
View full analysis
dict: """Call a GreenHelix REST endpoint for the given tool.""" response = session.post(f"{API_BASE}/v1/tools/{tool}", json=input_data) response.raise_for_status() return response.json() ``` ### Technical Analysis The API destination is read directly from `GREENHELIX_API_URL`, while the production bearer token is installed as a session-wide header. The code does not validate the URL scheme, hostname, port, or path before sending the request. If an attacker can influence the process environment, deployment configuration, shell profile, CI variables, or container settings, the endpoint can be redirected to an attacker-controlled HTTP or HTTPS server. The next API request then sends the bearer credential to that server. The same request may contain identity metadata, payment intents, credential hashes, compliance data, reputation metrics, or dispute evidence. No timeout is configured, and redirect behavior is not restricted; these omissions increase availability risk and can complicate endpoint-origin guarantees. ### Attack Path 1. Gain control over the application's environment or deployment configuration without needing access to the API key itself. 2. Set `GREENHELIX_API_URL` to an attacker-controlled endpoint. 3. Allow an operator or agent to run any documented operation that invokes `api_call()`. 4. The shared session sends `Authorization: Bearer ` to the configured e ...[truncated 582 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (7)

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 115)May include surrounding context.

md
**OpenAI Agent Commerce Protocol (ACP)**: Defines how agents discover, negotiate, and transact with each other. The identity layer requires verifiable credentials for any transaction above a configurable threshold. ACP's credential requirements align with the W3C VC Data Model 2.0 and accept SD-JWT-VC as a presentation format.

The convergence is not accidental. All three groups independently identified the same gap: agents need cryptographically verifiable delegation chains, not just access tokens. The answer they all arrived at was W3C Verifiable Credentials with selective disclosure.

| Protocol | Organization | Credential Format | Identity Anchor | Delegation Model |
|----------|-------------|-------------------|-----------------|------------------|

Intent-Code Divergence

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The guide repeatedly claims credentials are SD-JWT-VCs with key binding, but the code only constructs unsigned JSON payloads and disclosure lists and never signs an issuer JWT or creates a holder key-binding JWT. This breaks the core security property of verifiable credentials: presentations can be forged, modified, or replayed because there is no cryptographic proof of issuer authenticity or holder possession.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

The guide frames the examples as educational and emphasizes sandbox usage, but the code performs live HTTP POST requests and includes functions like create_intent and create_dispute that can trigger real side effects when API_BASE or credentials point to production. This mismatch increases the chance that users run the sample code believing it is inert, causing unintended purchases, disputes, compliance submissions, or data transmission.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The AP2Adapter.verify_counterparty docstring claims DID resolution and disclosure validation, but the implementation only checks the protocol/format fields and fetches reputation by agent_id. A caller could treat this function as a trust decision point and accept forged or malformed credential presentations without any cryptographic verification, DID document resolution, signature checking, disclosure digest validation, nonce binding, or chain validation.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
50% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · SKILL.md (reported line 1067)May include surrounding context.

md
"supported_currencies": protocol_metadata.get("currencies", ["USD"]),
            },
            "service_endpoint": protocol_metadata.get(
                "endpoint", f"https://api.greenhelix.net/v1"
            ),
        }

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
75% confidence
Finding

Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Content

Scanner excerpt · SKILL.md (reported line 1546)May include surrounding context.

md
### Why Credentials Need Reputation

A credential tells you what an agent is authorized to do. It does not tell you whether the agent is any good at doing it. An agent with a valid $50,000 delegation credential might have a 30% task completion rate and a history of disputed escrows. The credential is technically valid -- the human authorized the spend -- but the counterparty should not accept it without checking the agent's track record. Reputation binding attaches verifiable performance data to credentials so that counterparties can evaluate both authorization and competence in a single verification flow.

### ReputationBoundCredentialManager

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The guide states that every delegation chain enforces constraint narrowing, but the generic issue_credential method does not enforce parent-child subset checks for actions, limits, or categories. Developers may rely on this broad claim and accidentally mint over-privileged child credentials whenever they use issue_credential directly instead of the safer delegate_intent helper.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.