Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"source": "git-subdir",
"url": "https://github.com/vaaraio/vaara.git",
"path": "plugins/claude-code-vaara-governance",
"ref": "v0.41.0"
"ref": "v0.43.0"
},
"homepage": "https://vaara.io"
}
Expand Down
8 changes: 8 additions & 0 deletions src/vaara/integrations/_mcp_attest.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@ def build_attest_emitter(
f"--attest-signing-key is not a usable PEM private key: {exc}"
) from exc
if isinstance(key, EllipticCurvePrivateKey):
# ES256 emits a fixed 32-byte r||s; a non-P-256 curve would be
# mislabeled and then silently fail to sign (emit_attestation
# swallows errors), so reject it up front with a clear message.
if key.curve.name != "secp256r1":
raise AttestConfigError(
"--attest-signing-key EC key must use the P-256 (secp256r1) "
f"curve for ES256; got {key.curve.name!r}."
)
alg: str = "ES256"
elif isinstance(key, RSAPrivateKey):
alg = "RS256"
Expand Down
21 changes: 20 additions & 1 deletion src/vaara/integrations/mcp_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1393,7 +1393,9 @@ def main(argv: Optional[list[str]] = None) -> None:
"github=github-mcp-server` or `--upstream npx`).",
)

attest_emitter = _build_attest_emitter_from_args(args, upstreams=upstreams)
attest_emitter = _build_attest_emitter_from_args(
args, upstreams=_attest_upstreams_for_slots(upstreams),
)

legacy_single = (
list(next(iter(upstreams.values()))) if len(upstreams) == 1 else None
Expand Down Expand Up @@ -1467,6 +1469,23 @@ def _parse_upstream_specs(
return upstreams


def _attest_upstreams_for_slots(
upstreams: dict[str, list[str]],
) -> dict[str, list[str]]:
"""Key the attestation fingerprint table the way the proxy slots upstreams.

A single upstream (named ``NAME=CMD`` or bare ``CMD``) collapses into the
``"default"`` slot inside ``VaaraMCPProxy``, and ``_REQUEST_UPSTREAM``
resolves to ``"default"`` at runtime. The emitter must be keyed the same
way, or ``fingerprint_for("default")`` misses the precomputed cmd-hash and
emits a ``cmd:sha256:unknown-default`` placeholder. Multi-upstream fan-out
keeps the operator-supplied slot names.
"""
if len(upstreams) == 1:
return {"default": list(next(iter(upstreams.values())))}
return dict(upstreams)


def _build_attest_emitter_from_args(
args: argparse.Namespace,
*,
Expand Down
21 changes: 21 additions & 0 deletions tests/test_integrations_mcp_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,3 +723,24 @@ def test_inflight_requests_cleared_when_tools_call_raises(monkeypatch):
finally:
_REQUEST_UPSTREAM.reset(token)
assert p._inflight_requests == {}


# ---------------------------------------------------------------------------
# Attestation emitter slot-keying (pure; runs without the attestation extra)
# ---------------------------------------------------------------------------

def test_attest_upstreams_named_single_collapses_to_default():
# A named single upstream (--upstream github=CMD) collapses to the
# "default" slot inside the proxy, so the attestation fingerprint table
# must be keyed "default" too. Regression for the placeholder-fingerprint
# bug on the documented NAME=CMD single-upstream form.
from vaara.integrations.mcp_proxy import _attest_upstreams_for_slots
assert _attest_upstreams_for_slots(
{"github": ["github-mcp-server", "stdio"]}
) == {"default": ["github-mcp-server", "stdio"]}


def test_attest_upstreams_multi_preserves_names():
from vaara.integrations.mcp_proxy import _attest_upstreams_for_slots
upstreams = {"github": ["gh-srv"], "fs": ["fs-srv"]}
assert _attest_upstreams_for_slots(upstreams) == upstreams
60 changes: 60 additions & 0 deletions tests/test_integrations_mcp_proxy_attest.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,66 @@ def test_manifest_fingerprint_idempotent(monkeypatch, emitter):
assert emitter.fingerprint_for("default") == first_fp


# ---------------------------------------------------------------------------
# Key validation
# ---------------------------------------------------------------------------

def test_build_attest_emitter_rejects_non_p256_ec_key(tmp_path):
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from vaara.integrations._mcp_attest import AttestConfigError, build_attest_emitter
key = ec.generate_private_key(ec.SECP384R1())
pem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
key_path = tmp_path / "p384.pem"
key_path.write_bytes(pem)
with pytest.raises(AttestConfigError, match="secp256r1"):
build_attest_emitter(
signing_key_path=key_path,
receipts_dir=tmp_path / "r",
upstream_commands={"default": ["echo"]},
)


def test_build_attest_emitter_accepts_p256_ec_key(tmp_path):
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from vaara.integrations._mcp_attest import build_attest_emitter
key = ec.generate_private_key(ec.SECP256R1())
pem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
key_path = tmp_path / "p256.pem"
key_path.write_bytes(pem)
emitter = build_attest_emitter(
signing_key_path=key_path,
receipts_dir=tmp_path / "r",
upstream_commands={"default": ["echo"]},
)
assert emitter.fingerprint_for("default").startswith("cmd:sha256:")


def test_named_single_upstream_fingerprints_under_default(attest_key, attest_receipts_dir):
# End-to-end of the slot-keying fix: an emitter built the way main() now
# builds it for a named single upstream returns a real cmd-hash under
# "default", never the unknown-* placeholder.
from vaara.integrations._mcp_attest import build_attest_emitter
from vaara.integrations.mcp_proxy import _attest_upstreams_for_slots
emitter = build_attest_emitter(
signing_key_path=attest_key,
receipts_dir=attest_receipts_dir,
upstream_commands=_attest_upstreams_for_slots({"github": ["github-mcp-server"]}),
)
fp = emitter.fingerprint_for("default")
assert fp.startswith("cmd:sha256:")
assert "unknown" not in fp


# ---------------------------------------------------------------------------
# Config error handling
# ---------------------------------------------------------------------------
Expand Down