Anchor

mcp
Guvenlik Denetimi
Gecti
Health Gecti
  • License — License: Apache-2.0
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Community trust — 11 GitHub stars
Code Gecti
  • Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Gecti
  • Permissions — No dangerous permissions requested

Bu listing icin henuz AI raporu yok.

SUMMARY

Transparency-log-based provenance for MCP tool schemas, using Sigstore/Rekor to detect rug-pull, tool-poisoning, and tool-shadowing attacks.

README.md

Anchor

This is a prototype / proof of concept, not production software. It
has not been security-reviewed, its predicate types are not standardized
anywhere, and it should not be relied on to gate real tool approvals yet.
See Known limitations below.

Anchor is a CLI for detecting "rug pull" attacks on MCP (Model Context
Protocol) tools — the scenario where a client approves a tool's schema
once, and the tool's operator later silently changes that schema
(widening permissions, adding an exfiltration field, rewording the
description to something dangerous) without the client noticing.

Anchor proves provenance, not safety. It answers one narrow question:
"was this exact schema published by this exact identity, at this time,
and is that on the public record?"
It does not — and is not meant to —
judge whether a schema's contents are safe. That's a separate problem.

[!WARNING]
anchor sign, anchor rotate-identity, and scripts/e2e_test.sh all
submit a real attestation to the live, public Rekor transparency log

(rekor.sigstore.dev), under whatever OIDC identity you authenticate as
when prompted. This is not a sandbox or dry run — it's the same
production infrastructure cosign and every other Sigstore-based tool
uses. Transparency logs are append-only by design: once submitted, an
entry is permanent and public, and cannot be edited or deleted.
Know
which account you're logging in with before you run these commands.
anchor verify is read-only and safe to run freely.

How it works

  1. Canonicalize. Before hashing, a schema is transformed into its
    RFC 8785 (JCS)
    canonical form: sorted object keys, minimal whitespace, no cosmetic
    noise. This means re-ordering keys or adding a trailing newline never
    registers as a "change" — only a real difference in the JSON value
    does. (internal/canon)

  2. Attest. Anchor builds a standard
    in-toto v1 Statement
    — subject = the tool's canonical schema hash, predicateType = one of
    Anchor's two custom predicate types, predicate = the provenance
    details (publisher identity, schema version, previous hash, timestamp).
    (internal/predicate)

  3. Sign, keylessly. That Statement is handed to cosign for
    Sigstore's keyless flow:
    you authenticate with an OIDC identity (Google/GitHub/Microsoft/etc.),
    Fulcio issues a short-lived certificate binding that identity to a
    throwaway key, and the signed attestation is submitted to Rekor,
    Sigstore's public append-only transparency log. Anchor doesn't
    reimplement any of this cryptography — it shells out to cosign and
    focuses its own Go code on statement construction and trust logic.
    (internal/signer)

  4. Verify. Given a live schema file, Anchor canonicalizes and hashes
    it, then checks that hash against what's on record for that tool:

    • hash unattested anywhere → BLOCKED, likely rug pull.
    • hash attested, but by a different identity than last approved, and
      no rotation record bridges them → FLAGGED, likely rug pull via
      identity switch.
    • the tool's signed history isn't a single linear chain → FORKED,
      the publisher has equivocated (see point 6).
    • hash attested by the same identity as before → OK, and any
      content diff is shown for human review (attested ≠ silently
      rubber-stamped).
  5. Handle legitimate identity rotation. A publisher can hand off to a
    new identity deliberately, by having their current identity sign a
    separate identity-rotation attestation naming the new one. Only then
    does verify treat an identity change as authorized instead of
    suspicious. A rotation also joins both identities into one publisher
    lineage
    , so a handoff is a link in the chain rather than a second line
    of history.

  6. Enforce a strict linear history. Each revision's
    previousSchemaHash must name the revision logged immediately before
    it, and only one revision may claim to be the first. If two revisions
    claim the same predecessor (a schema history fork), or one names a
    predecessor that isn't retrievable (a gap), verify prints FORKED: divergent schema history detected, names the Rekor entries that
    disagree, and exits non-zero. Re-signing identical content and
    deliberate rollbacks are not forks. (internal/history)

  7. Bootstrap without local state. A client that never signed anything
    can rebuild a tool's history from the public log with verify --bootstrap-from-identity <publisher>: Rekor is searched by signer
    identity, Anchor filters for its own predicates and that toolId,
    follows attested rotations to the publisher's successor identities, and
    runs the point-6 checks on the result. The identity must come from you,
    never from the schema being verified — see
    docs/DESIGN.md. (internal/bootstrap)

Status

Each claim below says how it was established. Where it says live, it
means against the real, public Sigstore/Rekor infrastructure, not a mock.
Where it says fixture-tested, it means unit tests over committed JSON
fixtures — chosen deliberately, because sign writes a permanent, public,
undeletable Rekor entry, and Anchor's own verification logic must not need
new ones in order to be tested.

  • ✅ anchor sign — live. Produces a real Fulcio-issued certificate and a real
    Rekor transparency log entry.

  • ✅ anchor verify on an unmodified, attested schema — live. Prints OK: schema matches latest attested revision.

  • ✅ anchor verify on a silently mutated, unsigned schema — live. Prints
    BLOCKED: unattested schema change detected and exits non-zero. This
    is the core rug-pull-detection claim, and it's been confirmed working.

  • ✅ anchor rotate-identity, and the resulting "identity changed, but an
    attested rotation bridges it → OK" path, have been live-tested end to
    end against the real, public Sigstore/Rekor infrastructure (see
    Trying identity rotation). anchor verify
    printed OK: schema matches latest attested revision. with an explicit
    Publisher identity rotated: <old> -> <new> (attested rotation, Rekor index <n>) line, rather than flagging the identity change as
    suspicious.

  • ✅ Strict-linear previousSchemaHash enforcement: verify walks the
    whole retrievable history and fails closed on a fork, a gap, or an
    out-of-order revision. Covered by table-driven unit tests over JSON
    fixtures in
    internal/history/testdata (linear chain,
    fork mid-chain, two competing first revisions, gap, partial history,
    rotation accepted as a link from either end, rotation not signed by the
    identity handing off, empty history, duplicate identical entry,
    rollback, out-of-order, unrelated signer, case-differing signer), and
    run live (2026-10-24) against this project's real
    send_email_rotation_test history, where it passes the genuine
    two-revision chain across an attested identity rotation — from both
    ends of that rotation. With the client's known-good set to the
    post-rotation identity it printed History: 2 attested revisions, one linear chain; with it set to the pre-rotation identity (the
    second-client case in Trying identity
    rotation
    ) it printed the same line plus the
    Publisher identity rotated: acceptance. Both matter: the first
    revision is signed by the old identity, so a lineage that only
    followed rotations forwards would have excluded it and reported a false
    gap.

    The FORKED verdict has not been produced from real signed forks on
    the public log, and deliberately won't be: doing so would mean writing
    permanent, public, undeletable Rekor entries purely to test Anchor's own
    verification logic. The Rekor lookup sits behind an interface
    (rekorapi.Client) precisely so it doesn't have to be.

  • ✅ History reconstruction from the log with no local state: implemented
    and fixture-tested end to end in
    internal/bootstrap/testdata — rebuilds
    a chain, detects a fork and a gap rebuilt from entries alone, follows an
    attested rotation to a successor identity, refuses an unsigned one,
    filters out other predicate types and other tools, drops entries that
    fail verification or whose certificate names a different signer, and
    refuses rather than truncate a scan that hits its cap. Against the
    live public Rekor instance it cannot currently complete — confirmed
    again on 2026-10-24, read-only; see "Known limitations".

  • ⚠️ The "FLAGGED: publisher identity changed without an attested
    rotation" path (identity switch with no rotation record) is exercised
    by construction whenever the rotation path above hasn't been run first,
    but hasn't been deliberately live-tested as its own scenario either.

Why a local .anchor/ state directory, if Rekor is public?

Rekor's public search API supports lookup by artifact hash, by signer
email, or by log index — not by arbitrary custom fields like Anchor's
toolId. There's no way to ask the public log "give me every attestation
for tool X." So, like a real MCP client would, Anchor keeps a local,
append-only pointer index (.anchor/log-index.json) recording which Rekor
log entries belong to which tool, plus a trust-on-first-use cache of the
last known-good revision per tool (.anchor/state.json).

Critically, the local files are never trusted blindly. Every verify
re-fetches the relevant entry live from the public Rekor instance and
re-runs cosign verify-blob-attestation against the cached bundle before
using it for any trust decision — the local index is only ever a set of
pointers into a log that remains the actual source of truth. A tampered or
deleted .anchor/ directory can make Anchor less useful (nothing to
compare against, or a forced fresh trust-on-first-use), but it can't
convince verify to accept something that was never actually
transparency-logged.

Prerequisites

  • Go ≥ 1.23
  • cosign
    on your PATH (Anchor shells out to it for all signing/verification
    cryptography)
  • A browser (or a second device) to complete Sigstore's OIDC login when
    anchor sign / anchor rotate-identity prompt for it

Every sign and rotate-identity call submits a real attestation to the
public production Rekor log (rekor.sigstore.dev), tied to whatever
OIDC identity you authenticate as. That's permanent — transparency logs
don't support deletion — so know which account you're signing in with
before you run these commands.

Commands

anchor sign --tool-id <id> --publisher-identity <id> --schema-version <v> <schema-file>

(flags must come before the trailing <schema-file> argument — that's a
property of Go's stdlib flag package, not a design choice)

Canonicalizes and hashes the schema, builds the in-toto Statement, and
signs it via Sigstore's keyless flow (this is where you'll be prompted to
authenticate). --publisher-identity is the identity you're about to log
in as — Anchor cross-checks it against what the issued certificate
actually says, so a typo or logging in as the wrong account is caught
rather than silently trusted. On success it prints the Rekor UUID and log
index, and updates local state.

anchor verify --tool-id <id> [--bootstrap-from-identity <id>] <schema-file>

Canonicalizes and hashes the live schema, then applies the logic described
above. Exits non-zero on BLOCKED, FLAGGED or FORKED — wire this into
an MCP client's tool-approval path and an unattested change, an identity
switch, or a non-linear history all fail closed instead of being silently
accepted.

--bootstrap-from-identity <id> rebuilds the tool's history from the
public Rekor log first, for a client with no local state.

[!IMPORTANT]
The identity you pass must come from you, or from out-of-band
configuration you control — never from the schema being verified or
anywhere else its publisher controls. An attacker who chooses which
identity you trust doesn't need to forge anything: they name their own,
and hand you a flawless, fully transparency-logged history that is
entirely theirs. No check here can catch that. A successful bootstrap
proves the history Rekor holds for that identity, not that the
identity is the tool's rightful publisher — it's trust-on-first-use, and
the output says so.

--max-bootstrap-entries <n> (default 2000) caps how many Rekor entries
one identity may have before the scan refuses. It refuses rather than
scanning a prefix: a history with revisions missing can look perfectly
linear, so a truncated scan could turn a fork into an OK.

anchor rotate-identity --tool-id <id> --new-identity <id>

Signs (with the tool's current known-good identity — you'll be prompted
to authenticate as that account specifically) an attestation authorizing
--new-identity to publish schema revisions for this tool going forward.
A subsequent verify that sees the schema now signed by the new identity
will treat that as an attested rotation rather than a red flag.

All three commands accept --state-dir (default .anchor) if you want to
manage more than one client's state, or point at a specific project.

Running the end-to-end demo

scripts/e2e_test.sh signs the sample send_email tool schema
(sample_tool_schema.json), verifies it, then simulates a rug pull by
mutating the schema without signing the change, and verifies again to
show it gets blocked.

./scripts/e2e_test.sh [email protected]

Replace [email protected] with the identity you'll authenticate as during
the OIDC prompts (any Sigstore-supported OIDC provider — Google, GitHub,
Microsoft — works; it doesn't need to match your shell's notion of "you",
it just needs to match whichever account you log in with).

What you'll see:

  1. Sign — a browser/device-code OIDC login prompt, then a Rekor UUID
    and log index printed on success.
  2. Verify (unmodified) — OK: schema matches latest attested revision.
  3. Mutate — the script silently adds a bcc field and tweaks the
    description, without calling anchor sign. This is the rug pull.
  4. Verify (mutated) — BLOCKED: unattested schema change detected,
    and the script exits with an explicit "rug-pull correctly detected"
    message.

The whole thing runs in a temp directory (cleaned up on exit) with its own
throwaway --state-dir, so it won't touch anything in the repo.

Trying identity rotation

(Per Status: live-tested against the real Sigstore/Rekor
infrastructure. If you run this, two more real, permanent Rekor entries
get created — the rotation attestation and the re-signed schema.)

To see the third path — an identity change that Anchor accepts because
it's attested — sign the same tool again with a second OIDC identity
after running rotate-identity:

BIN=$(mktemp -d)/anchor
go build -o "$BIN" ./cmd/anchor
STATE=$(mktemp -d)/.anchor
SCHEMA=$(mktemp -d)/send_email.json
cp sample_tool_schema.json "$SCHEMA"

$BIN sign --tool-id send_email \
  --publisher-identity [email protected] --schema-version 1.0.0 --state-dir "$STATE" "$SCHEMA"

# authenticate as [email protected] when prompted here:
$BIN rotate-identity --tool-id send_email \
  --new-identity [email protected] --state-dir "$STATE"

# authenticate as [email protected] when prompted here:
$BIN sign --tool-id send_email \
  --publisher-identity [email protected] --schema-version 1.0.1 --state-dir "$STATE" "$SCHEMA"

[!NOTE]
anchor sign always fast-forwards its own --state-dir's local
known-good cache to whatever it just signed (that's how a publisher's
own client stays in sync with its own latest revision). So running
verify --state-dir "$STATE" immediately after the commands above,
against that same $STATE, will just print the plain "OK: schema
matches latest attested revision." — the known-good identity was
already fast-forwarded to the new one by the second sign call, so
there's no identity mismatch left for verify to reconcile.

To actually see the rotation-acceptance branch fire, simulate a
second client — e.g. an MCP client that trusted the tool at v1.0.0
under the old identity and hasn't seen anything since — by pointing
verify at a different --state-dir whose state.json still has
publisherIdentity set to [email protected] (copy $STATE/log-index.json
into it unchanged, so it still has pointers to the real Rekor entries):

VERIFIER=$(mktemp -d)/.anchor
mkdir -p "$VERIFIER"
cp "$STATE/log-index.json" "$VERIFIER/log-index.json"
cat > "$VERIFIER/state.json" <<EOF
{"send_email": {"toolId": "send_email", "schemaHash": "<v1.0.0's sha256:... from the first sign's output>", "publisherIdentity": "[email protected]", "schemaVersion": "1.0.0", "updatedAt": "<its timestamp>"}}
EOF

$BIN verify --tool-id send_email --state-dir "$VERIFIER" "$SCHEMA"
# -> OK, and mentions the attested identity rotation

Every record verify uses here — the schema-manifest and the
identity-rotation attestation — is still re-verified cryptographically
and re-confirmed live on the public Rekor log; only the local
"which revision did I previously trust" pointer is hand-constructed,
exactly modeling what a second client's own state.json would contain.

If you skip the rotate-identity step and just re-sign with a different
identity, the final verify will print FLAGGED: publisher identity changed without an attested rotation and exit non-zero instead.

Project layout

cmd/anchor/            CLI entry point and per-command flag handling
internal/canon/        RFC 8785 (JCS) canonicalization + SHA-256 hashing
internal/predicate/    in-toto v1 Statement + Anchor's two predicate types
internal/signer/       shells out to cosign; parses the returned bundle
internal/rekorapi/     read-only Rekor REST lookups, behind a Client interface
internal/history/      strict-linear-chain + fork detection over a tool's history
internal/bootstrap/    rebuilds a tool's history from Rekor with no local state
internal/state/        local .anchor/state.json + log-index.json
internal/diff/         human-readable diff between two schema revisions
internal/identity/     identity-string comparison (case-insensitive for email)
scripts/e2e_test.sh    the rug-pull demo described above
sample_tool_schema.json  the send_email fixture used by the demo
docs/                  design rationale, architecture walkthrough, related work

See docs/DESIGN.md for the threat model and design
rationale, docs/ARCHITECTURE.md for how a
sign/verify call flows through the packages above, and
docs/RELATED_WORK.md for how this compares to
ETDI.

Known limitations (it's a prototype)

  • No support for key-based (non-keyless) signing, custom Rekor/Fulcio
    instances, or offline/air-gapped verification.

  • The local log-index.json grows unbounded; a real client would want
    pruning/pagination for tools with long histories.

  • A fork is only detectable if both branches are retrievable. The
    linearity check (now enforced — see Status) reasons over the
    history a client can actually get hold of: its own local index, plus
    whatever --bootstrap-from-identity discovers for the publisher's
    identity and the identities it attested a rotation to. A second branch
    published under some unrelated identity is not a fork at all in
    Anchor's terms — it's an impostor, which is the FLAGGED path. A branch
    hidden from both of those is simply invisible, and there is no way around
    that short of Rekor being searchable by toolId.

  • The linearity check is slow for long histories. It re-verifies every
    revision in the chain with cosign (a subprocess each) and re-fetches
    the matched entry live from Rekor, so cost grows linearly with the number
    of revisions. Fine for the handful a real tool accumulates; not fine for
    thousands.

  • Still open, blocked by Rekor: --bootstrap-from-identity cannot
    complete against the live public log.
    The code path is finished and
    fixture-tested (see Status), and the blocker is upstream, not
    in Anchor. cosign attest-blob --new-bundle-format writes a dsse-kind
    Rekor entry, and that entry kind stores only hashes — the public
    instance does not return the DSSE payload itself. With no payload there
    is no predicate, so toolId and previousSchemaHash can't be read back
    from Rekor, and the flag fails with an explanatory error rather than
    pretending the identity published nothing.

    Re-confirmed read-only on 2026-10-24 against this project's own public
    entries. GET /api/v1/log/entries/<uuid> for Rekor index 2787179932
    returns body, integratedTime, logID, logIndex, verification —
    and no attestation key. The decoded body is kind: dsse,
    apiVersion: 0.0.1, whose spec holds exactly envelopeHash,
    payloadHash, signatures. The payload is not withheld by policy; the
    entry type never contained it. Rekor can look up entries by schema
    hash (the subject digest is indexed) and by signer identity, so it can
    still confirm "this hash was attested by this identity" — it just can't
    chain or filter by tool.

    Two ways out, neither taken here: distribute the signer's bundles
    out-of-band (e.g. published beside the schema), so the predicate comes
    from the bundle and Rekor is used only to confirm inclusion; or sign into
    an entry kind that retains the attestation, which would mean changing
    what anchor sign writes and re-testing it against the live log.

  • No security review has been done. Anchor's own predicate types
    (https://anchor.dev/schema-manifest/v1,
    https://anchor.dev/identity-rotation/v1) are project-local
    conventions, not registered or reviewed by anyone outside this repo.

License

Apache License 2.0 — see LICENSE.

Yorumlar (0)

Sonuc bulunamadi