Back to skill

Security audit

Skillsign — ed25519 Skill Signing

Security checks for vulnerabilities and agentic risk

Overview

This skill is a coherent local signing tool, but its verification logic can wrongly mark attacker-controlled signatures and provenance as trusted.

Install only if you treat this as an experimental local integrity helper, not as a dependable trusted-author gate. Before using it to approve skills, the signer fingerprint should be derived from the verified public key, trust should compare actual public keys, and provenance chain entries should be cryptographically authenticated or clearly labeled informational.

Vulnerability Patterns
  • Tool Hijacking and SpoofingModifies or replaces tools so legitimate-looking calls execute attacker logic
  • 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 (2)

T07 · Tool Hijacking and Spoofing

Error
Location
skillsign.py:253
Finding
Trusted signer identity can be spoofed through unbound fingerprint metadata<![CDATA[ ## Vulnerability Details **File Location**: `skillsign.py`, lines 61–62 and 253–274 **Vulnerability Type**: Authentication metadata substitution **Risk Level**: High ### Vulnerable Code ```python # Lines 61–62: all signature metadata is excluded from the manifest dirs[:] = [d for d in dirs if d != SKILLSIG_DIR] ``` ```python # Lines 253–274 # Verify cryptographic signature pub_key = load_public_key_bytes(signer["public_key"].encode("utf-8")) data = manifest_bytes(stored_manifest) try: pub_key.verify(signature, data) except InvalidSignature: print(f"❌ INVALID SIGNATURE — manifest matches but signature is forged.") sys.exit(1) # Check revocation fp = signer["fingerprint"] revoked, rev_info = is_revoked_at(fp, signer.get("signed_at", "")) if revoked: print(f"🔴 REVOKED — Signer {fp} was revoked.") print(f" Revoked at: {rev_info['revoked_at']}") print(f" Reason: {rev_info['reason']}") print(f" Signed at: {signer.get('signed_at', 'unknown')}") print(f" Signatures after revocation are not trustworthy.") sys.exit(1) # Check trust trusted = is_trusted(fp) trust_label = "TRUSTED" if trusted else "UNTRUSTED" ``` ### Technical Analysis The verifier obtains two security-sensitive identity values from `signer.json`: 1. `public_key`, which is used to verify the Ed25519 signature. 2. `fingerprint`, which is used for trust and revocation decisions. These values are treated independently. After successfully verifying the signature with the embedded public key, the implementation does not calculate the fingerprint of that verified key or compare it with `signer["fingerprint"]`. In addition, the entire `.skillsig` directory is excluded from the signed manifest. Consequently, an attacker can replace `signer.json`, `manifest.json`, and `signature.bin` without those changes being detected as ordinary folder tampering. A valid signature proves only that the supplied private key signed the supplied manifest; it does not ...[truncated 2451 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Derive the fingerprint directly from the public key that successfully verifies the signature: ```python pub_key = load_public_key_bytes(signer["public_key"].encode("utf-8")) verified_fp = pubkey_fingerprint(pub_key) if signer.get("fingerprint") != verified_fp: print("❌ INVALID SIGNER METADATA — fingerprint does not match public key.") sys.exit(1) ``` 2. Use `verified_fp`, rather than the serialized fingerprint field, for every trust and revocation decision. 3. When evaluating trust, load the trusted public-key file and compare its canonical raw key bytes with the verified signer key. Do not rely solely on the existence of a filename. 4. Cryptographically bind security-sensitive signer metadata to the signature. For example, sign a canonical envelope containing: - Manifest or manifest hash - Full signer public key or its full SHA-256 fingerprint - Signing timestamp - Tool and format version - Chain head, if provenance is supported 5. Use a full-length fingerprint internally. A shortened fingerprint may be retained only as a display value. 6. Add regression tests that construct a signature using one key while supplying another trusted fingerprint. Verification must reject the package. 7. Clearly distinguish cryptographic validity from trust status in the command output and exit behavior. ]]>

T07 · Tool Hijacking and Spoofing

Warning
Location
skillsign.py:341
Finding
Provenance chain can be modified or fabricated without detection<![CDATA[ ## Vulnerability Details **File Location**: `skillsign.py`, lines 61–62, 197–215, and 341–359 **Vulnerability Type**: Unauthenticated provenance metadata **Risk Level**: Medium ### Vulnerable Code ```python # Lines 61–62: .skillsig, including chain.json, is excluded dirs[:] = [d for d in dirs if d != SKILLSIG_DIR] ``` ```python # Lines 197–215: chain entries are appended without signing the chain chain_path = sig_dir / "chain.json" chain = [] if chain_path.exists(): with open(chain_path) as f: chain = json.load(f) chain.append({ "fingerprint": fp, "action": "sign", "timestamp": signer_info["signed_at"], "files": len(manifest), }) with open(sig_dir / "signer.json", "w") as f: json.dump(signer_info, f, indent=2) with open(chain_path, "w") as f: json.dump(chain, f, indent=2) ``` ```python # Lines 341–359: unauthenticated entries are presented as provenance def cmd_chain(args): """Show the full isnad (provenance chain).""" folder = Path(args.folder).resolve() chain_path = folder / SKILLSIG_DIR / "chain.json" if not chain_path.exists(): print(f"No provenance chain found in {folder.name}/") sys.exit(1) with open(chain_path) as f: chain = json.load(f) print(f"=== Isnād: {folder.name}/ ({len(chain)} links) ===") for i, link in enumerate(chain): trusted = is_trusted(link["fingerprint"]) trust = "TRUSTED" if trusted else "UNTRUSTED" print(f" [{i+1}] {link['fingerprint']} [{trust}]") print(f" Action: {link['action']}") print(f" Time: {link['timestamp']}") print(f" Files: {link['files']}") ``` ### Technical Analysis `chain.json` is stored inside `.skillsig`, which is deliberately excluded from the signed manifest. The chain is neither: - Covered by the current Ed25519 signature - Signed separately by each listed author - Hash-linked to preceding entries - Bound to the corresponding historical mani ...[truncated 2011 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace the plain JSON event list with a cryptographically verifiable, append-only structure. 2. Each chain entry should contain at least: - Hash of the previous chain entry - Hash of the exact manifest being signed - Signer's canonical public key or full fingerprint - Timestamp and action - Format version - Ed25519 signature over all preceding fields 3. Require each new signer to verify the existing chain before adding a link. 4. During `cmd_chain`, validate every entry in order: - Recompute its canonical serialized representation - Verify its signature - Validate the previous-entry hash - Validate the associated manifest hash - Derive the fingerprint from the verified public key - Evaluate trust only after successful cryptographic verification 5. Reject broken chains or clearly label them as unauthenticated. Do not display an unverified fingerprint as `[TRUSTED]` merely because the same text matches a local trusted-key filename. 6. Bind the authenticated chain head to the current signature envelope so an attacker cannot replace the entire chain with another internally valid chain. 7. For backward compatibility, treat existing unsigned `chain.json` files as legacy informational metadata and display an explicit warning that they do not provide verified provenance. 8. Add tests covering inserted, deleted, reordered, and modified chain entries. ]]>
Vulnerability Patterns
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (2)

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill advertises shell, file read, and file write capabilities through its documented commands, but it does not declare any explicit tool scope such as permissions or allowed-tools. That mismatch increases the risk that an agent platform grants broader access than reviewers expect, especially for a skill that manages keys, writes signature metadata, and operates on arbitrary folder paths.

Session Persistence

Medium
Category
Rogue Agent
Content
## Example Workflow

```bash
# First time: create your identity
python3 skillsign.py keygen --name parker

# Sign your skills
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Static analysis

No suspicious patterns detected.