Skip to content

✨ A2A AgentCard Signature Verification - #176

Merged
usize merged 1 commit into
rossoctl:mainfrom
kevincogan:feature/a2a-strict-signature-check
Feb 16, 2026
Merged

✨ A2A AgentCard Signature Verification#176
usize merged 1 commit into
rossoctl:mainfrom
kevincogan:feature/a2a-strict-signature-check

Conversation

@kevincogan

Copy link
Copy Markdown
Contributor

Summary

Adds cryptographic signature verification for A2A AgentCards. This enables operators to ensure that AgentCard data has not been tampered.

New Features

  • Signature verification with two provider backends:

    • Secret provider: public keys stored in Kubernetes Secrets (with key-rotation fallback)
    • JWKS provider: public keys fetched from a remote JWKS endpoint
  • Canonical JSON hashing: using a struct-to-map approach that strips the signature field and empty values before signing, future-proof against field additions

  • Algorithm support: RSA (PKCS1v15 / SHA-256) and ECDSA (P-256, P-384, P-521) for both Secret and JWKS providers

  • Audit mode: for gradual rollout, logs verification failures as warnings but allows cards to sync

  • Prometheus metrics: (signature_verification_total, signature_verification_duration_seconds, signature_verification_errors_total) for monitoring verification outcomes

  • NetworkPolicy enforcement : AgentCardNetworkPolicyReconciler creates/removes NetworkPolicies based on signature verification status

  • SignatureIdentityMatch: combined check that both signature verification and identity binding pass, stored in AgentCardStatus

  • Helm values: for configuring signature verification (signatureVerification.enabled, providerType, secretName, auditMode, etc.)

  • Kubernetes Events: emitted for SignatureEvaluated (Normal) and SignatureFailed (Warning), visible via kubectl describe agentcard

Architecture Alignment

  • Signature verification works with targetRef (duck-typed workloads: Deployments, StatefulSets, Legacy Agent CRDs)
  • NetworkPolicy controller adapted to the Deployment/StatefulSet architecture, resolving workloads via spec.targetRefstatus.targetRefselector fallback
  • New status fields added to AgentCardStatus: validSignature, signatureVerificationDetails, signatureKeyId, signatureIdentityMatch
  • Updated RBAC for secrets (get/list/watch) and networking.k8s.io/networkpolicies
  • Deep copy generated for CardSignature struct

Related issue(s)

feature: Strict CardSignature Checking #161

Testing Instructions

Unit & Integration Tests

cd kagenti-operator

# Signature package unit tests (38 tests)
go test ./internal/signature/... -v -count=1

# Controller integration tests signature scenarios (6 tests, requires envtest)
go test ./internal/controller/... -v -count=1 --ginkgo.focus="Signature"

End-to-End Demo

Follow the step-by-step guide in docs/a2a-signature-verification.md which covers:

  1. Setup: kind cluster, cert-manager, operator build & deploy
  2. Demo 1: Valid signed AgentCard (verified)
  3. Demo 2: Unsigned card rejected (blocked)
  4. Demo 3: Wrong-key signature rejected (blocked)
  5. Demo 4: Audit mode (warns but allows)
  6. Demo 5: JWKS provider verification
  7. Demo 6: Zero-downtime key rotation

@kevincogan
kevincogan force-pushed the feature/a2a-strict-signature-check branch from 5247d47 to 5814365 Compare February 11, 2026 14:26
Comment thread kagenti-operator/docs/a2a-signature-verification.md Outdated
Comment thread kagenti-operator/internal/signature/verifier.go Outdated
Comment thread kagenti-operator/internal/controller/agentcard_controller.go Outdated
Comment thread kagenti-operator/internal/controller/agentcard_controller.go Outdated
Comment thread kagenti-operator/api/v1alpha1/agentcard_types.go Outdated

@usize usize left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved but with a few big caveats.

The new signature verification is excellent. It follows the spec, and we can embed a spiffe_id into it which we can compare to the allowList.

There is a big gap remaining here, which is that we aren't really binding workload identity to the signature fully.

That's because we can still sign our JWT in a valid way with a fake spiffeId in it. e.g. this test fails: 13d663d

In earlier versions of this, you'd been deriving the spiffe id, which would have helped here. But I asked you to remove it because... there's a better way we can work toward.

In a follow up patch, what we should do is replace the SecretProvider here, with a SPIFFE trust bundle.

We can do that at startup time here in Kagenti.

Then, token verification automatically gives us the strong binding we're seeking. Does it make sense?

In the interim, before we land this, can we emit a warning that looks like this:

"Identity binding is allowlist-only; SPIFFE trust bundle verification not yet available"

And leave a comment in the controller?

Then we can close this loop in the followup.

Thanks for working so hard on this.

Integrated on top of upstream/main (identity-binding, workload-based architecture).

New features:
- Signature verification with Secret and JWKS providers
- Canonical JSON hashing (struct-to-map approach, future-proof)
- ECDSA (P-256/384/521) + RSA support for both Secret and JWKS
- Audit mode for gradual rollout
- Prometheus metrics for verification outcomes
- NetworkPolicy enforcement based on signature status
- SignatureIdentityMatch: combined signature + identity binding check
- Helm values for signature verification configuration

Architecture alignment:
- Signature verification works with targetRef (duck-typed workloads)
- NetworkPolicy controller adapted to Deployment/StatefulSet architecture
- All new status fields added to AgentCardStatus
- Updated RBAC for secrets and networkpolicies
- Updated CRDs and deepcopy generated

Signed-off-by: Kevin Cogan <kevin.s.cogan@gmail.com>
@kevincogan
kevincogan force-pushed the feature/a2a-strict-signature-check branch from bf01726 to 6539184 Compare February 16, 2026 12:36
@kevincogan

Copy link
Copy Markdown
Contributor Author

Makes complete sense, I've pushed the following changes:

  1. Warning event: computeBinding() now emits an AllowlistOnly warning: "Identity binding is allowlist-only; SPIFFE trust bundle verification not yet available"
  2. Controller comment + TODO: Added a LIMITATION block above computeBinding() explaining the gap and referencing this PR for the follow-up

For the follow up, I've opened #179 to track the SPIRE init-container + trust bundle verification work, init container signs with the workload's SVID, operator verifies against the SPIRE trust bundle via the existing JWKSProvider, closing the self-asserted claim gap.

@usize
usize merged commit 9784a1b into rossoctl:main Feb 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants