Skip to content
Open
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
19 changes: 19 additions & 0 deletions a2aclient/agentcard/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"time"

"github.com/a2aproject/a2a-go/v2/a2a"
"github.com/a2aproject/a2a-go/v2/a2acrypto"
"github.com/a2aproject/a2a-go/v2/log"
)

Expand Down Expand Up @@ -62,6 +63,8 @@ type Resolver struct {
Client *http.Client
// CardParser can be used to configure AgentCard parsing.
CardParser Parser
// Verifier optionally verifies AgentCard signatures after resolution.
Verifier *a2acrypto.Verifier
}

// NewResolver is a [Resolver] constructor function.
Expand Down Expand Up @@ -131,6 +134,22 @@ func (r *Resolver) Resolve(ctx context.Context, baseURL string, opts ...ResolveO
return nil, fmt.Errorf("card parsing failed: %w", err)
}

if r.Verifier != nil && len(card.Signatures) > 0 {
var verified bool
var lastErr error
for _, sig := range card.Signatures {
if err := r.Verifier.Verify(card, &sig); err == nil {
verified = true
break
} else {
lastErr = err
}
}
if !verified {
return nil, fmt.Errorf("agent card signature verification failed: %w", lastErr)
}
}
Comment on lines +137 to +151

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.

security-high high

Requiring all signatures to be valid breaks key rotation (where some signatures might use old/expired keys or keys not present in the client's trust store). The A2A specification states that verifying at least one signature is sufficient.

Additionally, allowing unsigned cards to pass through silently when a Verifier is configured makes the client vulnerable to signature-stripping attacks (where an attacker simply removes the signatures field to bypass verification). Consider making signature enforcement configurable or requiring at least one valid signature if a verifier is set.

	if r.Verifier != nil && len(card.Signatures) > 0 {
		var verified bool
		var lastErr error
		for _, sig := range card.Signatures {
			if err := r.Verifier.Verify(card, &sig); err == nil {
				verified = true
				break
			} else {
				lastErr = err
			}
		}
		if !verified {
			return nil, fmt.Errorf("agent card signature verification failed: %w", lastErr)
		}
	}
References
  1. Prioritize adherence to the official specification over maintaining backward compatibility for undocumented features.


return card, nil
}

Expand Down
25 changes: 25 additions & 0 deletions a2aclient/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"strings"

"github.com/a2aproject/a2a-go/v2/a2a"
"github.com/a2aproject/a2a-go/v2/a2acrypto"
"github.com/a2aproject/a2a-go/v2/log"

"golang.org/x/mod/semver"
Expand All @@ -33,6 +34,7 @@ type Factory struct {
config Config
interceptors []CallInterceptor
transports map[transportKey]TransportFactory
cardVerifier *a2acrypto.Verifier
}

type transportKey struct {
Expand Down Expand Up @@ -109,6 +111,21 @@ func (f *Factory) CreateFromCard(ctx context.Context, card *a2a.AgentCard) (*Cli
protocolVersion: a2a.ProtocolVersion(selected.semver[1:]),
}
client.card.Store(card)
if f.cardVerifier != nil && len(card.Signatures) > 0 {
var verified bool
var lastErr error
for _, sig := range card.Signatures {
if err := f.cardVerifier.Verify(card, &sig); err == nil {
verified = true
break
} else {
lastErr = err
}
}
if !verified {
return nil, fmt.Errorf("agent card signature verification failed: %w", lastErr)
}
}
Comment on lines +114 to +128

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.

security-high high

Requiring all signatures to be valid breaks key rotation. The A2A specification states that verifying at least one signature is sufficient.

Additionally, allowing unsigned cards to pass through silently when a Verifier is configured makes the client vulnerable to signature-stripping attacks. Consider making signature enforcement configurable or requiring at least one valid signature if a verifier is set.

	if f.cardVerifier != nil && len(card.Signatures) > 0 {
		var verified bool
		var lastErr error
		for _, sig := range card.Signatures {
			if err := f.cardVerifier.Verify(card, &sig); err == nil {
				verified = true
				break
			} else {
				lastErr = err
			}
		}
		if !verified {
			return nil, fmt.Errorf("agent card signature verification failed: %w", lastErr)
		}
	}
References
  1. Prioritize adherence to the official specification over maintaining backward compatibility for undocumented features.

return client, nil
}

Expand Down Expand Up @@ -272,6 +289,14 @@ func WithDefaultsDisabled() FactoryOption {
return defaultsDisabledOpt{}
}

// WithCardVerifier sets a verifier to validate AgentCard signatures when creating clients.
// If set, signed cards are verified in CreateFromCard; unsigned cards pass through.
func WithCardVerifier(v *a2acrypto.Verifier) FactoryOption {
return factoryOptionFn(func(f *Factory) {
f.cardVerifier = v
})
}

// NewFactory creates a new Factory applying the provided configurations.
func NewFactory(options ...FactoryOption) *Factory {
f := &Factory{
Expand Down
71 changes: 71 additions & 0 deletions a2acrypto/a2acrypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2026 The A2A Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package a2acrypto provides utilities for AgentCard JWS signing and verification.
package a2acrypto

import (
"crypto"
)
Comment on lines +18 to +20

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.

critical

To comply with the A2A specification and RFC 8785 (JSON Canonicalization Scheme / JCS), the signatures field must be excluded from the payload before signing or verifying, and the JSON keys must be sorted lexicographically at all levels.

Since Go's standard json.Marshal sorts map keys alphabetically, we can achieve both requirements without external dependencies by marshaling the card, unmarshaling it into a map[string]any, deleting the "signatures" key, and marshaling it back.

import (
	"crypto"
	"encoding/json"
	"errors"

	"github.com/a2aproject/a2a-go/v2/a2a"
)

func preparePayload(card *a2a.AgentCard) ([]byte, error) {
	if card == nil {
		return nil, errors.New("nil agent card")
	}
	temp, err := json.Marshal(card)
	if err != nil {
		return nil, err
	}
	var m map[string]any
	if err := json.Unmarshal(temp, &m); err != nil {
		return nil, err
	}
	delete(m, "signatures")
	return json.Marshal(m)
}
References
  1. Prioritize adherence to the official specification over maintaining backward compatibility for undocumented features.


// KeyResolver resolves a key ID and JWKS URL into a public key for verification.
type KeyResolver interface {
// ResolveKey looks up the public key for the given key ID (kid) and JWKS endpoint (jku).
ResolveKey(kid, jku string) (crypto.PublicKey, error)
}

// VerifierConfig configures signature verification.
type VerifierConfig struct {
KeyResolver KeyResolver
}

// Verifier verifies AgentCard JWS signatures.
type Verifier struct {
kr KeyResolver
}

// NewVerifier creates a Verifier using the provided configuration.
func NewVerifier(config VerifierConfig) *Verifier {
return &Verifier{kr: config.KeyResolver}
}

// SignerConfig configures AgentCard signing.
type SignerConfig struct {
PrivateKey crypto.Signer
KeyID string
Algorithm string
JWKSURL string
}

// Signer creates JWS signatures for AgentCards.
type Signer struct {
key crypto.Signer
kid string
algorithm string
jwksURL string
}

// NewSigner creates a Signer using the provided configuration.
func NewSigner(config SignerConfig) *Signer {
alg := config.Algorithm
if alg == "" {
alg = inferAlgorithm(config.PrivateKey)
}
return &Signer{
key: config.PrivateKey,
kid: config.KeyID,
algorithm: alg,
jwksURL: config.JWKSURL,
}
}
61 changes: 61 additions & 0 deletions a2acrypto/alg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2025 The A2A Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package a2acrypto

import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"fmt"
)

func inferAlgorithm(key crypto.Signer) string {
switch pub := key.Public().(type) {
case *ecdsa.PublicKey:
switch pub.Curve {
case elliptic.P256():
return "ES256"
case elliptic.P384():
return "ES384"
case elliptic.P521():
return "ES512"
default:
return ""
}
case ed25519.PublicKey:
return "EdDSA"
case *rsa.PublicKey:
return "RS256"
default:
return ""
}
}

func algToHash(alg string) (crypto.Hash, error) {
switch alg {
case "ES256", "RS256":
return crypto.SHA256, nil
case "ES384", "RS384":
return crypto.SHA384, nil
case "ES512", "RS512":
return crypto.SHA512, nil
case "EdDSA":
return crypto.Hash(0), nil
default:
return 0, fmt.Errorf("unsupported algorithm: %s", alg)
}
}
Loading
Loading